mirror of https://github.com/astral-sh/ruff
[ty] Add hint about resolved Python version when a user attempts to import a member added on a newer version (#21615)
## Summary Fixes https://github.com/astral-sh/ty/issues/1620. #20909 added hints if you do something like this and your Python version is set to 3.10 or lower: ```py import typing typing.LiteralString ``` And we also have hints if you try to do something like this and your Python version is set too low: ```py from stdlib_module import new_submodule ``` But we don't currently have any subdiagnostic hint if you do something like _this_ and your Python version is set too low: ```py from typing import LiteralString ``` This PR adds that hint! ## Test Plan snapshots --------- Co-authored-by: Aria Desires <aria.desires@gmail.com>
This commit is contained in:
parent
f317a71682
commit
a57e291311
|
|
@ -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
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = <module 'whatever.thispackage.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()
|
||||
|
|
|
|||
Loading…
Reference in New Issue