[ty] Move Python version source out of Program (#27434)

## Summary

`PythonVersionWithSource` contains from where ty derived the
`PythonVersion`. If the Python version comes from the project's
`pyproject.toml`, then the source includes the configuration file name
and the text range of the `requires-python` or `python-version`
key-value pair. Any change to that `pyproject.toml` would result in a
new and different `PythonVersionWithSource`, which, in turn, results in
a distinct interned `Program`. This results in invalidating every single
type inference query (more accurately, we now get a new instance of each
type inference query, but we loose all cached results).

Storing `PythonVersionWithSource` also has the downside that it prevents
scripts that otherwise have identical `Program` settings to share the
same `Program`, only because their `python-version` comes from different
script metadata blocks.

This PR moves `PythonVersionWithSource` out of `Program`. `Program`
should only contain information that changes type inference.
`PythonVersionWithSource` does not. `Db` now has a new
`python_version_with_source` method that, given a file, returns the
source for it (A script can return a different source than a project
file).

## Test Plan

Testing: Semantic, project, core, server, Markdown, and corpus tests
pass; repository hooks pass.
This commit is contained in:
Micha Reiser authored and GitHub committed 2026-08-04 08:51:18 +02:00
1 parent aa5b04ad92
commit e87ddc00ea
14 files changed
+62 -34

No files matched your search

+9 -1
View File
@@ -17,7 +17,7 @@ use salsa::{Database, Event, Setter};
use ty_python_core::ProgramFile;
use ty_python_core::program::{FallibleStrategy, MisconfigurationStrategy, UseDefaultStrategy};
use ty_python_semantic::lint::{LintRegistry, RuleSelection};
use ty_python_semantic::{AnalysisSettings, Db as SemanticDb};
use ty_python_semantic::{AnalysisSettings, Db as SemanticDb, PythonVersionWithSource};
mod changes;
@@ -542,6 +542,10 @@ impl SemanticDb for ProjectDatabase {
self.project().program(self).program_file(self, file)
}
fn python_version_with_source(&self, _file: File) -> &PythonVersionWithSource {
&self.project().program_settings(self).python_version
}
fn rule_selection(&self, file: File) -> &RuleSelection {
let settings = file_settings(self, file);
settings.rules(self)
@@ -784,6 +788,10 @@ pub(crate) mod testing {
self.project().program(self).program_file(self, file)
}
fn python_version_with_source(&self, _file: File) -> &PythonVersionWithSource {
&self.project().program_settings(self).python_version
}
#[inline]
fn check_file(&self, file: File) -> Vec<Diagnostic> {
crate::check_file(self, file)
+1 -6
View File
@@ -14,11 +14,6 @@ pub use ty_module_resolver::{FallibleStrategy, MisconfigurationStrategy, UseDefa
#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)]
pub struct Program<'db> {
// FIXME: Move the source out of `Program`. Different source locations prevent otherwise
// equivalent programs from being reused across scripts.
#[returns(ref)]
pub python_version_with_source: PythonVersionWithSource,
#[returns(ref)]
pub python_platform: PythonPlatform,
@@ -39,7 +34,7 @@ impl<'db> Program<'db> {
let resolver_environment =
ResolverEnvironment::new(db, python_version.version, &search_paths);
Program::new(db, python_version, python_platform, resolver_environment)
Program::new(db, python_platform, resolver_environment)
}
pub fn python_version(self, db: &'db dyn Db) -> PythonVersion {
+8 -1
View File
@@ -1,5 +1,5 @@
use crate::AnalysisSettings;
use crate::lint::{LintRegistry, RuleSelection};
use crate::{AnalysisSettings, PythonVersionWithSource};
use ruff_db::diagnostic::Diagnostic;
use ruff_db::files::File;
use ty_python_core::{Db as PythonCoreDb, ProgramFile};
@@ -12,6 +12,9 @@ pub trait Db: PythonCoreDb {
/// Returns the program file for `file`.
fn program_file(&self, file: File) -> ProgramFile<'_>;
/// Returns the Python version and its configuration source for `file`.
fn python_version_with_source(&self, file: File) -> &PythonVersionWithSource;
/// Resolves the rule selection for a given file.
fn rule_selection(&self, file: File) -> &RuleSelection;
@@ -177,6 +180,10 @@ pub(crate) mod tests {
self.program().program_file(self, file)
}
fn python_version_with_source(&self, _file: File) -> &PythonVersionWithSource {
&self.program_settings.python_version
}
fn rule_selection(&self, _file: File) -> &RuleSelection {
&self.rule_selection
}
@@ -1,6 +1,6 @@
use crate::{
Db, ProgramEnvironment, PythonVersionSource, PythonVersionWithSource,
lint::lint_documentation_url, types::TypeCheckDiagnostics,
Db, PythonVersionSource, PythonVersionWithSource, lint::lint_documentation_url,
types::TypeCheckDiagnostics,
};
use levenshtein::{HideUnderscoredSuggestions, find_best_suggestion};
use ruff_db::{
@@ -46,12 +46,11 @@ pub fn inferred_python_version_source_annotation(
/// configuration files, or defaults.
pub(crate) fn add_inferred_python_version_hint_to_diagnostic(
db: &dyn Db,
env: &ProgramEnvironment,
file: File,
diagnostic: &mut Diagnostic,
action: &str,
) {
let PythonVersionWithSource { version, source } =
env.program(db).python_version_with_source(db);
let PythonVersionWithSource { version, source } = db.python_version_with_source(file);
match source {
crate::PythonVersionSource::Cli => {
+1 -1
View File
@@ -206,7 +206,7 @@ pub fn check_file(db: &dyn Db, file: ProgramFile<'_>) -> Result<Box<[Diagnostic]
let mut error = Diagnostic::invalid_syntax(source_file, error, error);
add_inferred_python_version_hint_to_diagnostic(
db,
&ProgramEnvironment::from_file(file),
source_file,
&mut error,
"parsing syntax",
);
@@ -5,8 +5,7 @@ use super::{
CallArguments, CallDunderError, ClassBase, ClassLiteral, GenericAlias, KnownClass,
StaticClassLiteral, add_inferred_python_version_hint_to_diagnostic,
};
use crate::diagnostic::did_you_mean;
use crate::diagnostic::format_enumeration;
use crate::diagnostic::{did_you_mean, format_enumeration};
use crate::lint::{Level, LintRegistryBuilder, LintStatus};
use crate::place::{DefinedPlace, Place, place_from_bindings};
use crate::suppression::FileSuppressionId;
@@ -37,6 +36,7 @@ use itertools::Itertools;
use ruff_db::source::source_text;
use ruff_db::{
diagnostic::{Annotation, Diagnostic, Span, SubDiagnostic, SubDiagnosticSeverity},
files::File,
parsed::parsed_module,
};
use ruff_diagnostics::{Edit, Fix, IsolationLevel};
@@ -4689,6 +4689,7 @@ pub(super) fn report_invalid_total_ordering_call(
/// The function returns `true` if a hint was added, `false` otherwise.
pub(super) fn hint_if_stdlib_submodule_exists_on_other_versions(
db: &dyn Db,
file: File,
env: &ProgramEnvironment<'_>,
diagnostic: &mut Diagnostic,
full_submodule_name: &ModuleName,
@@ -4722,7 +4723,7 @@ pub(super) fn hint_if_stdlib_submodule_exists_on_other_versions(
version_range = version_range.diagnostic_display(),
));
add_inferred_python_version_hint_to_diagnostic(db, env, diagnostic, "resolving modules");
add_inferred_python_version_hint_to_diagnostic(db, file, diagnostic, "resolving modules");
true
}
@@ -4737,7 +4738,7 @@ pub(super) fn hint_if_stdlib_submodule_exists_on_other_versions(
/// misconfigured their Python version.
pub(super) fn hint_if_stdlib_attribute_exists_on_other_versions(
db: &dyn Db,
env: &ProgramEnvironment<'_>,
source_file: ProgramFile<'_>,
mut diagnostic: LintDiagnosticGuard,
value_type: Type,
attr: &str,
@@ -4750,7 +4751,7 @@ pub(super) fn hint_if_stdlib_attribute_exists_on_other_versions(
return;
};
let module = module_ty.module(db);
let Some(file) = module.file(db) else {
let Some(module_file) = module.file(db) else {
return;
};
let Some(search_path) = module.search_path(db) else {
@@ -4763,7 +4764,7 @@ pub(super) fn hint_if_stdlib_attribute_exists_on_other_versions(
// We populate place_table entries for stdlib items across all known versions and platforms,
// so if this lookup succeeds then we know that this lookup *could* succeed with possible
// configuration changes.
let program_file = ProgramFile::new(db, file, env.program(db));
let program_file = ProgramFile::new(db, module_file, source_file.program(db));
let symbol_table = place_table(db, global_scope(db, program_file));
let Some(symbol) = symbol_table.symbol_by_name(attr) else {
return;
@@ -4779,7 +4780,12 @@ pub(super) fn hint_if_stdlib_attribute_exists_on_other_versions(
// TODO: determine what version they need to be on
// TODO: also mention the platform we're assuming
// TODO: determine what platform they need to be on
add_inferred_python_version_hint_to_diagnostic(db, env, &mut diagnostic, action);
add_inferred_python_version_hint_to_diagnostic(
db,
source_file.file(db),
&mut diagnostic,
action,
);
}
pub(super) fn report_invalid_concatenate_last_arg<'db>(
@@ -10163,7 +10163,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
));
add_inferred_python_version_hint_to_diagnostic(
db,
self.program_environment(),
self.file(),
&mut diagnostic,
"resolving types",
);
@@ -10509,7 +10509,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
} else {
hint_if_stdlib_attribute_exists_on_other_versions(
db,
env,
self.program_file(),
diagnostic,
value_type,
attr_name,
@@ -86,7 +86,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
));
add_inferred_python_version_hint_to_diagnostic(
db,
self.program_environment(),
self.file(),
&mut diagnostic,
"resolving modules",
);
@@ -515,6 +515,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
if let Some(full_submodule_name) = full_submodule_name {
submodule_hint_added = hint_if_stdlib_submodule_exists_on_other_versions(
db,
self.file(),
self.program_environment(),
&mut diagnostic,
&full_submodule_name,
@@ -525,7 +526,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
if !submodule_hint_added {
hint_if_stdlib_attribute_exists_on_other_versions(
db,
self.program_environment(),
self.program_file(),
diagnostic,
module_ty,
name,
@@ -638,6 +639,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
hint_if_stdlib_submodule_exists_on_other_versions(
self.db(),
self.file(),
self.program_environment(),
&mut diagnostic,
&full_submodule_name,
@@ -356,7 +356,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
));
add_inferred_python_version_hint_to_diagnostic(
db,
self.program_environment(),
self.file(),
&mut diagnostic,
"inferring types",
);
@@ -202,7 +202,7 @@ fn program_file_changes_with_python_version() -> anyhow::Result<()> {
let equivalent_program = Program::from_settings(
&db,
ProgramSettings {
python_version: program.python_version_with_source(&db).clone(),
python_version: db.program_settings().python_version.clone(),
python_platform: program.python_platform(&db).clone(),
search_paths: program.search_paths(&db).clone(),
},
+7 -1
View File
@@ -8,7 +8,9 @@ use ruff_db::vendored::VendoredFileSystem;
use ty_python_core::program::ProgramSettings;
use ty_python_semantic::lint::{LintRegistry, RuleSelection};
use ty_python_semantic::pull_types::pull_types;
use ty_python_semantic::{AnalysisSettings, Db as _, check_file_unwrap, default_lint_registry};
use ty_python_semantic::{
AnalysisSettings, Db as _, PythonVersionWithSource, check_file_unwrap, default_lint_registry,
};
use ruff_db::diagnostic::Diagnostic;
use test_case::test_case;
@@ -220,6 +222,10 @@ impl ty_python_semantic::Db for CorpusDb {
self.program().program_file(self, file)
}
fn python_version_with_source(&self, _file: File) -> &PythonVersionWithSource {
&self.program_settings.python_version
}
fn rule_selection(&self, _file: File) -> &RuleSelection {
&self.rule_selection
}
@@ -68,11 +68,7 @@ fn debug_information(session: &Session) -> crate::Result<String> {
writeln!(buffer, "Project at {}", db.project().root(db))?;
let program = db.project().program(db);
writeln!(buffer, "Program:")?;
writeln!(
buffer,
" python-version: {}",
program.python_version_with_source(db).version
)?;
writeln!(buffer, " python-version: {}", program.python_version(db))?;
writeln!(buffer, " python-platform: {}", program.python_platform(db))?;
let mut writer = IndentingWriter {
inner: &mut buffer,
+6 -1
View File
@@ -19,7 +19,8 @@ use ty_python_core::program::ProgramSettings;
use ty_python_core::{Db as _, ProgramFile, TestProgramDb};
use ty_python_semantic::lint::{LintRegistry, RuleSelection};
use ty_python_semantic::{
AnalysisSettings, Db as SemanticDb, check_file_unwrap, default_lint_registry,
AnalysisSettings, Db as SemanticDb, PythonVersionWithSource, check_file_unwrap,
default_lint_registry,
};
#[salsa::db]
@@ -146,6 +147,10 @@ impl SemanticDb for Db {
self.program().program_file(self, file)
}
fn python_version_with_source(&self, _file: File) -> &PythonVersionWithSource {
&self.settings().program(self).python_version
}
fn rule_selection(&self, file: File) -> &RuleSelection {
file_settings(self, file).rules(self)
}
@@ -127,6 +127,10 @@ impl SemanticDb for TestDb {
self.program().program_file(self, file)
}
fn python_version_with_source(&self, _file: File) -> &PythonVersionWithSource {
&self.program_settings.python_version
}
fn rule_selection(&self, _file: File) -> &RuleSelection {
&self.rule_selection
}