mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 21:34:57 +00:00
[ty] Implement module-level __getattr__
support (#19791)
fix https://github.com/astral-sh/ty/issues/943 ## Summary Add module-level `__getattr__` support for ty's type checker, fixing issue https://github.com/astral-sh/ty/issues/943. Module-level `__getattr__` functions ([PEP 562](https://peps.python.org/pep-0562/)) are now respected when resolving dynamic attributes, matching the behavior of mypy and pyright. ## Implementation Thanks @sharkdp for the guidance in https://github.com/astral-sh/ty/issues/943#issuecomment-3157566579 - Adds module-specific `__getattr__` resolution in `ModuleLiteral.static_member()` - Maintains proper attribute precedence: explicit attributes > submodules > `__getattr__` ## Test Plan - New mdtest covering basic functionality, type annotations, attribute precedence, and edge cases (run ```cargo nextest run -p ty_python_semantic mdtest__import_module_getattr```) - All new tests pass, verifying `__getattr__` is called correctly and returns proper types - Existing test suite passes, ensuring no regressions introduced
This commit is contained in:
parent
44755e6e86
commit
0095ff4c1a
2 changed files with 124 additions and 2 deletions
|
@ -8335,6 +8335,25 @@ impl<'db> ModuleLiteralType<'db> {
|
|||
Some(Type::module_literal(db, importing_file, submodule))
|
||||
}
|
||||
|
||||
fn try_module_getattr(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> {
|
||||
// For module literals, we want to try calling the module's own `__getattr__` function
|
||||
// if it exists. First, we need to look up the `__getattr__` function in the module's scope.
|
||||
if let Some(file) = self.module(db).file(db) {
|
||||
let getattr_symbol = imported_symbol(db, file, "__getattr__", None);
|
||||
if let Place::Type(getattr_type, boundness) = getattr_symbol.place {
|
||||
// If we found a __getattr__ function, try to call it with the name argument
|
||||
if let Ok(outcome) = getattr_type.try_call(
|
||||
db,
|
||||
&CallArguments::positional([Type::string_literal(db, name)]),
|
||||
) {
|
||||
return Place::Type(outcome.return_type(db), boundness).into();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Place::Unbound.into()
|
||||
}
|
||||
|
||||
fn static_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> {
|
||||
// `__dict__` is a very special member that is never overridden by module globals;
|
||||
// we should always look it up directly as an attribute on `types.ModuleType`,
|
||||
|
@ -8360,10 +8379,18 @@ impl<'db> ModuleLiteralType<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
self.module(db)
|
||||
let place_and_qualifiers = self
|
||||
.module(db)
|
||||
.file(db)
|
||||
.map(|file| imported_symbol(db, file, name, None))
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_default();
|
||||
|
||||
// If the normal lookup failed, try to call the module's `__getattr__` function
|
||||
if place_and_qualifiers.place.is_unbound() {
|
||||
return self.try_module_getattr(db, name);
|
||||
}
|
||||
|
||||
place_and_qualifiers
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue