Skip to content

Commit a57e291

Browse files
AlexWaygoodGankra
andauthored
[ty] Add hint about resolved Python version when a user attempts to import a member added on a newer version (#21615)
## Summary Fixes astral-sh/ty#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 <[email protected]>
1 parent f317a71 commit a57e291

File tree

4 files changed

+72
-24
lines changed

4 files changed

+72
-24
lines changed

crates/ty/tests/cli/python_environment.rs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ fn config_override_python_version() -> anyhow::Result<()> {
3737
5 | print(sys.last_exc)
3838
| ^^^^^^^^^^^^
3939
|
40-
info: Python 3.11 was assumed when accessing `last_exc`
40+
info: The member may be available on other Python versions or platforms
41+
info: Python 3.11 was assumed when resolving the `last_exc` attribute
4142
--> pyproject.toml:3:18
4243
|
4344
2 | [tool.ty.environment]
@@ -1179,6 +1180,8 @@ fn defaults_to_a_new_python_version() -> anyhow::Result<()> {
11791180
import os
11801181
11811182
os.grantpt(1) # only available on unix, Python 3.13 or newer
1183+
1184+
from typing import LiteralString # added in Python 3.11
11821185
"#,
11831186
),
11841187
])?;
@@ -1194,8 +1197,11 @@ fn defaults_to_a_new_python_version() -> anyhow::Result<()> {
11941197
3 |
11951198
4 | os.grantpt(1) # only available on unix, Python 3.13 or newer
11961199
| ^^^^^^^^^^
1200+
5 |
1201+
6 | from typing import LiteralString # added in Python 3.11
11971202
|
1198-
info: Python 3.10 was assumed when accessing `grantpt`
1203+
info: The member may be available on other Python versions or platforms
1204+
info: Python 3.10 was assumed when resolving the `grantpt` attribute
11991205
--> ty.toml:3:18
12001206
|
12011207
2 | [environment]
@@ -1205,7 +1211,26 @@ fn defaults_to_a_new_python_version() -> anyhow::Result<()> {
12051211
|
12061212
info: rule `unresolved-attribute` is enabled by default
12071213
1208-
Found 1 diagnostic
1214+
error[unresolved-import]: Module `typing` has no member `LiteralString`
1215+
--> main.py:6:20
1216+
|
1217+
4 | os.grantpt(1) # only available on unix, Python 3.13 or newer
1218+
5 |
1219+
6 | from typing import LiteralString # added in Python 3.11
1220+
| ^^^^^^^^^^^^^
1221+
|
1222+
info: The member may be available on other Python versions or platforms
1223+
info: Python 3.10 was assumed when resolving imports
1224+
--> ty.toml:3:18
1225+
|
1226+
2 | [environment]
1227+
3 | python-version = "3.10"
1228+
| ^^^^^^ Python 3.10 assumed due to this configuration setting
1229+
4 | python-platform = "linux"
1230+
|
1231+
info: rule `unresolved-import` is enabled by default
1232+
1233+
Found 2 diagnostics
12091234
12101235
----- stderr -----
12111236
"#);
@@ -1225,6 +1250,8 @@ fn defaults_to_a_new_python_version() -> anyhow::Result<()> {
12251250
import os
12261251
12271252
os.grantpt(1) # only available on unix, Python 3.13 or newer
1253+
1254+
from typing import LiteralString # added in Python 3.11
12281255
"#,
12291256
),
12301257
])?;

crates/ty_python_semantic/resources/mdtest/snapshots/attributes.md_-_Attributes_-_Attributes_of_standa…_(49ba2c9016d64653).snap

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ error[unresolved-attribute]: Module `datetime` has no member `UTC`
3232
5 | # error: [unresolved-attribute]
3333
6 | reveal_type(datetime.fakenotreal) # revealed: Unknown
3434
|
35-
info: Python 3.10 was assumed when accessing `UTC` because it was specified on the command line
35+
info: The member may be available on other Python versions or platforms
36+
info: Python 3.10 was assumed when resolving the `UTC` attribute because it was specified on the command line
3637
info: rule `unresolved-attribute` is enabled by default
3738
3839
```

crates/ty_python_semantic/src/types/diagnostic.rs

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3630,30 +3630,32 @@ pub(super) fn report_invalid_method_override<'db>(
36303630
/// *does* exist as a submodule in the standard library on *other* Python
36313631
/// versions, we add a hint to the diagnostic that the user may have
36323632
/// misconfigured their Python version.
3633+
///
3634+
/// The function returns `true` if a hint was added, `false` otherwise.
36333635
pub(super) fn hint_if_stdlib_submodule_exists_on_other_versions(
36343636
db: &dyn Db,
3635-
mut diagnostic: LintDiagnosticGuard,
3637+
diagnostic: &mut Diagnostic,
36363638
full_submodule_name: &ModuleName,
36373639
parent_module: Module,
3638-
) {
3640+
) -> bool {
36393641
let Some(search_path) = parent_module.search_path(db) else {
3640-
return;
3642+
return false;
36413643
};
36423644

36433645
if !search_path.is_standard_library() {
3644-
return;
3646+
return false;
36453647
}
36463648

36473649
let program = Program::get(db);
36483650
let typeshed_versions = program.search_paths(db).typeshed_versions();
36493651

36503652
let Some(version_range) = typeshed_versions.exact(full_submodule_name) else {
3651-
return;
3653+
return false;
36523654
};
36533655

36543656
let python_version = program.python_version(db);
36553657
if version_range.contains(python_version) {
3656-
return;
3658+
return false;
36573659
}
36583660

36593661
diagnostic.info(format_args!(
@@ -3667,7 +3669,9 @@ pub(super) fn hint_if_stdlib_submodule_exists_on_other_versions(
36673669
version_range = version_range.diagnostic_display(),
36683670
));
36693671

3670-
add_inferred_python_version_hint_to_diagnostic(db, &mut diagnostic, "resolving modules");
3672+
add_inferred_python_version_hint_to_diagnostic(db, diagnostic, "resolving modules");
3673+
3674+
true
36713675
}
36723676

36733677
/// This function receives an unresolved `foo.bar` attribute access,
@@ -3681,8 +3685,9 @@ pub(super) fn hint_if_stdlib_submodule_exists_on_other_versions(
36813685
pub(super) fn hint_if_stdlib_attribute_exists_on_other_versions(
36823686
db: &dyn Db,
36833687
mut diagnostic: LintDiagnosticGuard,
3684-
value_type: &Type,
3688+
value_type: Type,
36853689
attr: &str,
3690+
action: &str,
36863691
) {
36873692
// Currently we limit this analysis to attributes of stdlib modules,
36883693
// 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(
37053710
// so if this lookup succeeds then we know that this lookup *could* succeed with possible
37063711
// configuration changes.
37073712
let symbol_table = place_table(db, global_scope(db, file));
3708-
if symbol_table.symbol_by_name(attr).is_none() {
3713+
let Some(symbol) = symbol_table.symbol_by_name(attr) else {
3714+
return;
3715+
};
3716+
3717+
if !symbol.is_bound() {
37093718
return;
37103719
}
37113720

3721+
diagnostic.info("The member may be available on other Python versions or platforms");
3722+
37123723
// For now, we just mention the current version they're on, and hope that's enough of a nudge.
37133724
// TODO: determine what version they need to be on
37143725
// TODO: also mention the platform we're assuming
37153726
// TODO: determine what platform they need to be on
3716-
add_inferred_python_version_hint_to_diagnostic(
3717-
db,
3718-
&mut diagnostic,
3719-
&format!("accessing `{attr}`"),
3720-
);
3727+
add_inferred_python_version_hint_to_diagnostic(db, &mut diagnostic, action);
37213728
}

crates/ty_python_semantic/src/types/infer/builder.rs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6240,18 +6240,30 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
62406240
return;
62416241
};
62426242

6243-
let diagnostic = builder.into_diagnostic(format_args!(
6243+
let mut diagnostic = builder.into_diagnostic(format_args!(
62446244
"Module `{module_name}` has no member `{name}`"
62456245
));
62466246

6247+
let mut submodule_hint_added = false;
6248+
62476249
if let Some(full_submodule_name) = full_submodule_name {
6248-
hint_if_stdlib_submodule_exists_on_other_versions(
6250+
submodule_hint_added = hint_if_stdlib_submodule_exists_on_other_versions(
62496251
self.db(),
6250-
diagnostic,
6252+
&mut diagnostic,
62516253
&full_submodule_name,
62526254
module,
62536255
);
62546256
}
6257+
6258+
if !submodule_hint_added {
6259+
hint_if_stdlib_attribute_exists_on_other_versions(
6260+
self.db(),
6261+
diagnostic,
6262+
module_ty,
6263+
name,
6264+
"resolving imports",
6265+
);
6266+
}
62556267
}
62566268

62576269
/// Infer the implicit local definition `x = <module 'whatever.thispackage.x'>` that
@@ -6335,13 +6347,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
63356347
return;
63366348
};
63376349

6338-
let diagnostic = builder.into_diagnostic(format_args!(
6350+
let mut diagnostic = builder.into_diagnostic(format_args!(
63396351
"Module `{thispackage_name}` has no submodule `{final_part}`"
63406352
));
63416353

63426354
hint_if_stdlib_submodule_exists_on_other_versions(
63436355
self.db(),
6344-
diagnostic,
6356+
&mut diagnostic,
63456357
&full_submodule_name,
63466358
module,
63476359
);
@@ -9131,8 +9143,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
91319143
hint_if_stdlib_attribute_exists_on_other_versions(
91329144
db,
91339145
diagnostic,
9134-
&value_type,
9146+
value_type,
91359147
attr_name,
9148+
&format!("resolving the `{attr_name}` attribute"),
91369149
);
91379150

91389151
fallback()

0 commit comments

Comments
 (0)