diff --git a/crates/ty/tests/cli/python_environment.rs b/crates/ty/tests/cli/python_environment.rs index 638fc6c4ca..3bc4eee1c3 100644 --- a/crates/ty/tests/cli/python_environment.rs +++ b/crates/ty/tests/cli/python_environment.rs @@ -37,7 +37,8 @@ fn config_override_python_version() -> anyhow::Result<()> { 5 | print(sys.last_exc) | ^^^^^^^^^^^^ | - info: Python 3.11 was assumed when accessing `last_exc` + info: The member may be available on other Python versions or platforms + info: Python 3.11 was assumed when resolving the `last_exc` attribute --> pyproject.toml:3:18 | 2 | [tool.ty.environment] @@ -1179,6 +1180,8 @@ fn defaults_to_a_new_python_version() -> anyhow::Result<()> { import os os.grantpt(1) # only available on unix, Python 3.13 or newer + + from typing import LiteralString # added in Python 3.11 "#, ), ])?; @@ -1194,8 +1197,11 @@ fn defaults_to_a_new_python_version() -> anyhow::Result<()> { 3 | 4 | os.grantpt(1) # only available on unix, Python 3.13 or newer | ^^^^^^^^^^ + 5 | + 6 | from typing import LiteralString # added in Python 3.11 | - info: Python 3.10 was assumed when accessing `grantpt` + info: The member may be available on other Python versions or platforms + info: Python 3.10 was assumed when resolving the `grantpt` attribute --> ty.toml:3:18 | 2 | [environment] @@ -1205,7 +1211,26 @@ fn defaults_to_a_new_python_version() -> anyhow::Result<()> { | info: rule `unresolved-attribute` is enabled by default - Found 1 diagnostic + error[unresolved-import]: Module `typing` has no member `LiteralString` + --> main.py:6:20 + | + 4 | os.grantpt(1) # only available on unix, Python 3.13 or newer + 5 | + 6 | from typing import LiteralString # added in Python 3.11 + | ^^^^^^^^^^^^^ + | + info: The member may be available on other Python versions or platforms + info: Python 3.10 was assumed when resolving imports + --> ty.toml:3:18 + | + 2 | [environment] + 3 | python-version = "3.10" + | ^^^^^^ Python 3.10 assumed due to this configuration setting + 4 | python-platform = "linux" + | + info: rule `unresolved-import` is enabled by default + + Found 2 diagnostics ----- stderr ----- "#); @@ -1225,6 +1250,8 @@ fn defaults_to_a_new_python_version() -> anyhow::Result<()> { import os os.grantpt(1) # only available on unix, Python 3.13 or newer + + from typing import LiteralString # added in Python 3.11 "#, ), ])?; diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/attributes.md_-_Attributes_-_Attributes_of_standa…_(49ba2c9016d64653).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/attributes.md_-_Attributes_-_Attributes_of_standa…_(49ba2c9016d64653).snap index a1cd9d301c..29137c8c9c 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/attributes.md_-_Attributes_-_Attributes_of_standa…_(49ba2c9016d64653).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/attributes.md_-_Attributes_-_Attributes_of_standa…_(49ba2c9016d64653).snap @@ -32,7 +32,8 @@ error[unresolved-attribute]: Module `datetime` has no member `UTC` 5 | # error: [unresolved-attribute] 6 | reveal_type(datetime.fakenotreal) # revealed: Unknown | -info: Python 3.10 was assumed when accessing `UTC` because it was specified on the command line +info: The member may be available on other Python versions or platforms +info: Python 3.10 was assumed when resolving the `UTC` attribute because it was specified on the command line info: rule `unresolved-attribute` is enabled by default ``` diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs index 30a0e283cb..ceecbce7f4 100644 --- a/crates/ty_python_semantic/src/types/diagnostic.rs +++ b/crates/ty_python_semantic/src/types/diagnostic.rs @@ -3630,30 +3630,32 @@ pub(super) fn report_invalid_method_override<'db>( /// *does* exist as a submodule in the standard library on *other* Python /// versions, we add a hint to the diagnostic that the user may have /// misconfigured their Python version. +/// +/// 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, - mut diagnostic: LintDiagnosticGuard, + diagnostic: &mut Diagnostic, full_submodule_name: &ModuleName, parent_module: Module, -) { +) -> bool { let Some(search_path) = parent_module.search_path(db) else { - return; + return false; }; if !search_path.is_standard_library() { - return; + return false; } let program = Program::get(db); let typeshed_versions = program.search_paths(db).typeshed_versions(); let Some(version_range) = typeshed_versions.exact(full_submodule_name) else { - return; + return false; }; let python_version = program.python_version(db); if version_range.contains(python_version) { - return; + return false; } diagnostic.info(format_args!( @@ -3667,7 +3669,9 @@ 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, &mut diagnostic, "resolving modules"); + add_inferred_python_version_hint_to_diagnostic(db, diagnostic, "resolving modules"); + + true } /// This function receives an unresolved `foo.bar` attribute access, @@ -3681,8 +3685,9 @@ pub(super) fn hint_if_stdlib_submodule_exists_on_other_versions( pub(super) fn hint_if_stdlib_attribute_exists_on_other_versions( db: &dyn Db, mut diagnostic: LintDiagnosticGuard, - value_type: &Type, + value_type: Type, attr: &str, + action: &str, ) { // Currently we limit this analysis to attributes of stdlib modules, // as this covers the most important cases while not being too noisy @@ -3705,17 +3710,19 @@ pub(super) fn hint_if_stdlib_attribute_exists_on_other_versions( // so if this lookup succeeds then we know that this lookup *could* succeed with possible // configuration changes. let symbol_table = place_table(db, global_scope(db, file)); - if symbol_table.symbol_by_name(attr).is_none() { + let Some(symbol) = symbol_table.symbol_by_name(attr) else { + return; + }; + + if !symbol.is_bound() { return; } + diagnostic.info("The member may be available on other Python versions or platforms"); + // For now, we just mention the current version they're on, and hope that's enough of a nudge. // 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, - &mut diagnostic, - &format!("accessing `{attr}`"), - ); + add_inferred_python_version_hint_to_diagnostic(db, &mut diagnostic, action); } diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 9ad53b3619..3451bb7638 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -6240,18 +6240,30 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { return; }; - let diagnostic = builder.into_diagnostic(format_args!( + let mut diagnostic = builder.into_diagnostic(format_args!( "Module `{module_name}` has no member `{name}`" )); + let mut submodule_hint_added = false; + if let Some(full_submodule_name) = full_submodule_name { - hint_if_stdlib_submodule_exists_on_other_versions( + submodule_hint_added = hint_if_stdlib_submodule_exists_on_other_versions( self.db(), - diagnostic, + &mut diagnostic, &full_submodule_name, module, ); } + + if !submodule_hint_added { + hint_if_stdlib_attribute_exists_on_other_versions( + self.db(), + diagnostic, + module_ty, + name, + "resolving imports", + ); + } } /// Infer the implicit local definition `x = ` that @@ -6335,13 +6347,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { return; }; - let diagnostic = builder.into_diagnostic(format_args!( + let mut diagnostic = builder.into_diagnostic(format_args!( "Module `{thispackage_name}` has no submodule `{final_part}`" )); hint_if_stdlib_submodule_exists_on_other_versions( self.db(), - diagnostic, + &mut diagnostic, &full_submodule_name, module, ); @@ -9131,8 +9143,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { hint_if_stdlib_attribute_exists_on_other_versions( db, diagnostic, - &value_type, + value_type, attr_name, + &format!("resolving the `{attr_name}` attribute"), ); fallback()