[ty] Add version hint for failed stdlib attribute accesses (#20909)

This is the ultra-minimal implementation of

* https://github.com/astral-sh/ty/issues/296

that was previously discussed as a good starting point. In particular we
don't actually bother trying to figure out the exact python versions,
but we still mention "hey btw for No Reason At All... you're on python
3.10" when you try to access something that has a definition rooted in
the stdlib that we believe exists sometimes.
This commit is contained in:
Aria Desires 2025-10-16 10:07:33 -04:00 committed by GitHub
parent a67e0690f2
commit 7155a62e5c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 215 additions and 73 deletions

View file

@ -2558,6 +2558,28 @@ class C:
reveal_type(C().x) # revealed: Unknown | tuple[Divergent, Literal[1]]
```
## Attributes of standard library modules that aren't yet defined
For attributes of stdlib modules that exist in future versions, we can give better diagnostics.
<!-- snapshot-diagnostics -->
```toml
[environment]
python-version = "3.10"
```
`main.py`:
```py
import datetime
# error: [unresolved-attribute]
reveal_type(datetime.UTC) # revealed: Unknown
# error: [unresolved-attribute]
reveal_type(datetime.fakenotreal) # revealed: Unknown
```
## References
Some of the tests in the *Class and instance variables* section draw inspiration from

View file

@ -0,0 +1,51 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: attributes.md - Attributes - Attributes of standard library modules that aren't yet defined
mdtest path: crates/ty_python_semantic/resources/mdtest/attributes.md
---
# Python source files
## main.py
```
1 | import datetime
2 |
3 | # error: [unresolved-attribute]
4 | reveal_type(datetime.UTC) # revealed: Unknown
5 | # error: [unresolved-attribute]
6 | reveal_type(datetime.fakenotreal) # revealed: Unknown
```
# Diagnostics
```
error[unresolved-attribute]: Type `<module 'datetime'>` has no attribute `UTC`
--> src/main.py:4:13
|
3 | # error: [unresolved-attribute]
4 | reveal_type(datetime.UTC) # revealed: Unknown
| ^^^^^^^^^^^^
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: rule `unresolved-attribute` is enabled by default
```
```
error[unresolved-attribute]: Type `<module 'datetime'>` has no attribute `fakenotreal`
--> src/main.py:6:13
|
4 | reveal_type(datetime.UTC) # revealed: Unknown
5 | # error: [unresolved-attribute]
6 | reveal_type(datetime.fakenotreal) # revealed: Unknown
| ^^^^^^^^^^^^^^^^^^^^
|
info: rule `unresolved-attribute` is enabled by default
```

View file

@ -181,7 +181,8 @@ impl PlaceTable {
}
/// Looks up a symbol by its name and returns a reference to it, if it exists.
#[cfg(test)]
///
/// This should only be used in diagnostics and tests.
pub(crate) fn symbol_by_name(&self, name: &str) -> Option<&Symbol> {
self.symbols.symbol_id(name).map(|id| self.symbol(id))
}

View file

@ -8,6 +8,7 @@ use super::{
use crate::lint::{Level, LintRegistryBuilder, LintStatus};
use crate::semantic_index::definition::{Definition, DefinitionKind};
use crate::semantic_index::place::{PlaceTable, ScopedPlaceId};
use crate::semantic_index::{global_scope, place_table};
use crate::suppression::FileSuppressionId;
use crate::types::call::CallError;
use crate::types::class::{DisjointBase, DisjointBaseKind, Field};
@ -29,7 +30,7 @@ use crate::{
use itertools::Itertools;
use ruff_db::diagnostic::{Annotation, Diagnostic, Span, SubDiagnostic, SubDiagnosticSeverity};
use ruff_python_ast::name::Name;
use ruff_python_ast::{self as ast, AnyNodeRef};
use ruff_python_ast::{self as ast, AnyNodeRef, Identifier};
use ruff_text_size::{Ranged, TextRange};
use rustc_hash::FxHashSet;
use std::fmt::Formatter;
@ -3140,6 +3141,56 @@ pub(super) fn hint_if_stdlib_submodule_exists_on_other_versions(
add_inferred_python_version_hint_to_diagnostic(db, &mut diagnostic, "resolving modules");
}
/// This function receives an unresolved `foo.bar` attribute access,
/// where `foo` can be resolved to have a type but that type does not
/// have a `bar` attribute.
///
/// If the type of `foo` has a definition that originates in the
/// standard library and `foo.bar` *does* exist as an attribute on *other*
/// Python versions, we add a hint to the diagnostic that the user may have
/// misconfigured their Python version.
pub(super) fn hint_if_stdlib_attribute_exists_on_other_versions(
db: &dyn Db,
mut diagnostic: LintDiagnosticGuard,
value_type: &Type,
attr: &Identifier,
) {
// Currently we limit this analysis to attributes of stdlib modules,
// as this covers the most important cases while not being too noisy
// about basic typos or special types like `super(C, self)`
let Type::ModuleLiteral(module_ty) = value_type else {
return;
};
let module = module_ty.module(db);
let Some(file) = module.file(db) else {
return;
};
let Some(search_path) = module.search_path(db) else {
return;
};
if !search_path.is_standard_library() {
return;
}
// 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 symbol_table = place_table(db, global_scope(db, file));
if symbol_table.symbol_by_name(attr).is_none() {
return;
}
// 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.id),
);
}
/// Suggest a name from `existing_names` that is similar to `wrong_name`.
fn did_you_mean<S: AsRef<str>, T: AsRef<str>>(
existing_names: impl Iterator<Item = S>,

View file

@ -59,6 +59,7 @@ use crate::types::diagnostic::{
IncompatibleBases, NON_SUBSCRIPTABLE, POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT,
SUBCLASS_OF_FINAL_CLASS, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL,
UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY,
hint_if_stdlib_attribute_exists_on_other_versions,
hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation,
report_bad_dunder_set_call, report_cannot_pop_required_field_on_typed_dict,
report_duplicate_bases, report_implicit_return_type, report_index_out_of_bounds,
@ -7497,13 +7498,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
),
);
} else {
builder.into_diagnostic(
let diagnostic = builder.into_diagnostic(
format_args!(
"Type `{}` has no attribute `{}`",
value_type.display(db),
attr.id
),
);
hint_if_stdlib_attribute_exists_on_other_versions(db, diagnostic, &value_type, attr);
}
}
}