mirror of
https://github.com/astral-sh/ruff
synced 2026-01-21 05:20:49 -05:00
[ty] Make ModuleType and object attributes available on namespace packages (#22606)
## Summary Currently we don't think that namespace packages (e.g. `google` after you've pip-installed `google-cloud-ndb`) have attributes such as `__file__`, `__name__`, etc. This PR fixes that. ## Test Plan Mdtests and snapshots.
This commit is contained in:
@@ -8099,6 +8099,39 @@ def f(x: Intersection[int, Any] | str):
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dunder_file_attribute_completion_non_namespace_package() {
|
||||
let builder = CursorTest::builder()
|
||||
.source("module.py", "")
|
||||
.source("main.py", "import module; module.__file<CURSOR>")
|
||||
.completion_test_builder();
|
||||
|
||||
// __file__ should be `str` when accessed as an attribute on a non-namespace-package module,
|
||||
// not `str | None`
|
||||
assert_snapshot!(
|
||||
builder.skip_keywords().skip_auto_import().type_signatures().build().snapshot(),
|
||||
@"__file__ :: str",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dunder_file_attribute_completion_namespace_package() {
|
||||
let builder = CursorTest::builder()
|
||||
.source("namespace_package/foo.py", "")
|
||||
.source(
|
||||
"main.py",
|
||||
"import namespace_package; namespace_package.__file<CURSOR>",
|
||||
)
|
||||
.completion_test_builder();
|
||||
|
||||
// __file__ should be `None` when accessed as an attribute on a namespace-package module,
|
||||
// not `str | None`
|
||||
assert_snapshot!(
|
||||
builder.skip_keywords().skip_auto_import().type_signatures().build().snapshot(),
|
||||
@"__file__ :: None",
|
||||
);
|
||||
}
|
||||
|
||||
/// A way to create a simple single-file (named `main.py`) completion test
|
||||
/// builder.
|
||||
///
|
||||
|
||||
@@ -978,3 +978,39 @@ from ty_extensions import has_member, static_assert
|
||||
# TODO: this should ideally not be available:
|
||||
static_assert(not has_member(3, "__annotations__")) # error: [static-assert-error]
|
||||
```
|
||||
|
||||
### `ModuleType` attributes are available on modules
|
||||
|
||||
`namespace_package/foo.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
`regular_module.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
`regular_package/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
import namespace_package
|
||||
import regular_module
|
||||
import regular_package
|
||||
from ty_extensions import static_assert, has_member
|
||||
|
||||
static_assert(has_member(namespace_package, "__file__"))
|
||||
static_assert(has_member(namespace_package, "__name__"))
|
||||
static_assert(has_member(namespace_package, "__eq__"))
|
||||
static_assert(has_member(regular_module, "__file__"))
|
||||
static_assert(has_member(regular_module, "__name__"))
|
||||
static_assert(has_member(regular_module, "__eq__"))
|
||||
static_assert(has_member(regular_package, "__file__"))
|
||||
static_assert(has_member(regular_package, "__name__"))
|
||||
static_assert(has_member(regular_package, "__eq__"))
|
||||
```
|
||||
|
||||
@@ -76,9 +76,5 @@ reveal_type(a.__file__) # revealed: str
|
||||
```py
|
||||
import namespace
|
||||
|
||||
# TODO: `__file__` does exist on namespace packages but is set to `None`;
|
||||
# this is a false positive
|
||||
#
|
||||
# error: [unresolved-attribute] "Module `namespace` has no member `__file__`"
|
||||
reveal_type(namespace.__file__) # revealed: Unknown
|
||||
reveal_type(namespace.__file__) # revealed: None
|
||||
```
|
||||
|
||||
@@ -91,10 +91,18 @@ def nested_scope():
|
||||
`ModuleType` attributes can also be accessed as attributes on module-literal types. The special
|
||||
attributes `__dict__` and `__init__`, and all attributes on `builtins.object`, can also be accessed
|
||||
as attributes on module-literal types, despite the fact that these are inaccessible as globals from
|
||||
inside the module:
|
||||
inside the module. They can even be accessed on namespace packages:
|
||||
|
||||
`namespace_package/foo.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
import typing
|
||||
import namespace_package
|
||||
|
||||
reveal_type(typing.__name__) # revealed: str
|
||||
reveal_type(typing.__init__) # revealed: bound method ModuleType.__init__(name: str, doc: str | None = ...) -> None
|
||||
@@ -110,17 +118,26 @@ reveal_type(typing.__file__) # revealed: str
|
||||
|
||||
# These come from `builtins.object`, not `types.ModuleType`:
|
||||
reveal_type(typing.__eq__) # revealed: bound method ModuleType.__eq__(value: object, /) -> bool
|
||||
|
||||
reveal_type(typing.__class__) # revealed: <class 'ModuleType'>
|
||||
|
||||
reveal_type(typing.__dict__) # revealed: dict[str, Any]
|
||||
|
||||
reveal_type(namespace_package.__name__) # revealed: str
|
||||
reveal_type(namespace_package.__init__) # revealed: bound method ModuleType.__init__(name: str, doc: str | None = ...) -> None
|
||||
reveal_type(namespace_package.__file__) # revealed: None
|
||||
reveal_type(namespace_package.__eq__) # revealed: bound method ModuleType.__eq__(value: object, /) -> bool
|
||||
reveal_type(namespace_package.__class__) # revealed: <class 'ModuleType'>
|
||||
reveal_type(namespace_package.__dict__) # revealed: dict[str, Any]
|
||||
```
|
||||
|
||||
Typeshed includes a fake `__getattr__` method in the stub for `types.ModuleType` to help out with
|
||||
dynamic imports; but we ignore that for module-literal types where we know exactly which module
|
||||
we're dealing with:
|
||||
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
import typing
|
||||
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(typing.__getattr__) # revealed: Unknown
|
||||
```
|
||||
@@ -128,6 +145,8 @@ reveal_type(typing.__getattr__) # revealed: Unknown
|
||||
However, if we have a `ModuleType` instance, we make `__getattr__` available. This means that
|
||||
arbitrary attribute accesses are allowed (with a result type of `Any`):
|
||||
|
||||
`c.py`:
|
||||
|
||||
```py
|
||||
import types
|
||||
|
||||
|
||||
@@ -427,20 +427,14 @@ pub(crate) fn global_symbol<'db>(
|
||||
///
|
||||
/// If `requires_explicit_reexport` is [`None`], it will be inferred from the file's source type.
|
||||
/// For stub files, explicit re-export will be required, while for non-stub files, it will not.
|
||||
///
|
||||
/// `None` should be passed for the `file` parameter if looking up a symbol on a namespace package.
|
||||
pub(crate) fn imported_symbol<'db>(
|
||||
db: &'db dyn Db,
|
||||
file: File,
|
||||
file: Option<File>,
|
||||
name: &str,
|
||||
requires_explicit_reexport: Option<RequiresExplicitReExport>,
|
||||
) -> PlaceAndQualifiers<'db> {
|
||||
let requires_explicit_reexport = requires_explicit_reexport.unwrap_or_else(|| {
|
||||
if file.is_stub(db) {
|
||||
RequiresExplicitReExport::Yes
|
||||
} else {
|
||||
RequiresExplicitReExport::No
|
||||
}
|
||||
});
|
||||
|
||||
// If it's not found in the global scope, check if it's present as an instance on
|
||||
// `types.ModuleType` or `builtins.object`.
|
||||
//
|
||||
@@ -456,22 +450,49 @@ pub(crate) fn imported_symbol<'db>(
|
||||
// ignore `__getattr__`. Typeshed has a fake `__getattr__` on `types.ModuleType` to help out with
|
||||
// dynamic imports; we shouldn't use it for `ModuleLiteral` types where we know exactly which
|
||||
// module we're dealing with.
|
||||
symbol_impl(
|
||||
db,
|
||||
global_scope(db, file),
|
||||
name,
|
||||
requires_explicit_reexport,
|
||||
ConsideredDefinitions::EndOfScope,
|
||||
)
|
||||
file.map(|file| {
|
||||
let requires_explicit_reexport = requires_explicit_reexport.unwrap_or_else(|| {
|
||||
if file.is_stub(db) {
|
||||
RequiresExplicitReExport::Yes
|
||||
} else {
|
||||
RequiresExplicitReExport::No
|
||||
}
|
||||
});
|
||||
|
||||
symbol_impl(
|
||||
db,
|
||||
global_scope(db, file),
|
||||
name,
|
||||
requires_explicit_reexport,
|
||||
ConsideredDefinitions::EndOfScope,
|
||||
)
|
||||
})
|
||||
.unwrap_or_default()
|
||||
.or_fall_back_to(db, || {
|
||||
if name == "__getattr__" {
|
||||
Place::Undefined.into()
|
||||
} else if name == "__builtins__" {
|
||||
Place::bound(Type::any()).into()
|
||||
} else {
|
||||
KnownClass::ModuleType
|
||||
match name {
|
||||
"__file__" => {
|
||||
// We special-case `__file__` here because we know that for a successfully imported
|
||||
// non-namespace-package Python module, that hasn't been explicitly overridden it
|
||||
// is always a string, even though typeshed says `str | None`. For a namespace package,
|
||||
// meanwhile, it will always be `None`.
|
||||
//
|
||||
// Note that C-extension modules (stdlib examples include `sys`, `itertools`, etc.)
|
||||
// may not have a `__file__` attribute at runtime at all, but that doesn't really
|
||||
// affect the *type* of the attribute, just the *boundness*. There's no way for us
|
||||
// to know right now whether a stub represents a C extension or not, so for now we
|
||||
// do not attempt to detect this; we just infer `str` still. This matches the
|
||||
// behaviour of other major type checkers.
|
||||
if file.is_some() {
|
||||
Place::bound(KnownClass::Str.to_instance(db)).into()
|
||||
} else {
|
||||
Place::bound(Type::none(db)).into()
|
||||
}
|
||||
}
|
||||
"__getattr__" => Place::Undefined.into(),
|
||||
"__builtins__" => Place::bound(Type::any()).into(),
|
||||
_ => KnownClass::ModuleType
|
||||
.to_instance(db)
|
||||
.member_lookup_with_policy(db, name.into(), MemberLookupPolicy::NO_GETATTR_LOOKUP)
|
||||
.member_lookup_with_policy(db, name.into(), MemberLookupPolicy::NO_GETATTR_LOOKUP),
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -521,7 +542,7 @@ pub(crate) fn known_module_symbol<'db>(
|
||||
resolve_module_confident(db, &known_module.name())
|
||||
.and_then(|module| {
|
||||
let file = module.file(db)?;
|
||||
Some(imported_symbol(db, file, symbol, None))
|
||||
Some(imported_symbol(db, Some(file), symbol, None))
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
@@ -1098,18 +1119,6 @@ fn symbol_impl<'db>(
|
||||
considered_definitions: ConsideredDefinitions,
|
||||
) -> PlaceAndQualifiers<'db> {
|
||||
let _span = tracing::trace_span!("symbol", ?name).entered();
|
||||
let place = place_table(db, scope)
|
||||
.symbol_id(name)
|
||||
.map(|symbol| {
|
||||
place_by_id(
|
||||
db,
|
||||
scope,
|
||||
symbol.into(),
|
||||
requires_explicit_reexport,
|
||||
considered_definitions,
|
||||
)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
if name == "platform"
|
||||
&& file_to_module(db, scope.file(db))
|
||||
@@ -1123,43 +1132,20 @@ fn symbol_impl<'db>(
|
||||
// Fall through to the looked up type
|
||||
}
|
||||
}
|
||||
} else if name == "__file__"
|
||||
&& let Some(module) = file_to_module(db, scope.file(db))
|
||||
{
|
||||
// We special-case `__file__` here because we know that for a successfully imported
|
||||
// non-namespace-package Python module, that hasn't been explicitly overridden it
|
||||
// is always a string, even though typeshed says `str | None`. For a namespace package,
|
||||
// meanwhile, it will always be `None`.
|
||||
//
|
||||
// Note that C-extension modules (stdlib examples include `sys`, `itertools`, etc.)
|
||||
// may not have a `__file__` attribute at runtime at all, but that doesn't really
|
||||
// affect the *type* of the attribute, just the *boundness*. There's no way for us
|
||||
// to know right now whether a stub represents a C extension or not, so for now we
|
||||
// do not attempt to detect this; we just infer `str` still. This matches the
|
||||
// behaviour of other major type checkers.
|
||||
let default_type = if module.file(db).is_some() {
|
||||
KnownClass::Str.to_instance(db)
|
||||
} else {
|
||||
Type::none(db)
|
||||
};
|
||||
|
||||
match place.place {
|
||||
Place::Defined(defined_place) => match defined_place.definedness {
|
||||
Definedness::AlwaysDefined => return place,
|
||||
Definedness::PossiblyUndefined => {
|
||||
let new_type = UnionType::from_elements(db, [defined_place.ty, default_type]);
|
||||
let def_place = DefinedPlace::new(new_type)
|
||||
.with_definedness(Definedness::AlwaysDefined)
|
||||
.with_widening(defined_place.widening)
|
||||
.with_origin(defined_place.origin);
|
||||
return Place::Defined(def_place).into();
|
||||
}
|
||||
},
|
||||
Place::Undefined => return Place::bound(default_type).into(),
|
||||
}
|
||||
}
|
||||
|
||||
place
|
||||
place_table(db, scope)
|
||||
.symbol_id(name)
|
||||
.map(|symbol| {
|
||||
place_by_id(
|
||||
db,
|
||||
scope,
|
||||
symbol.into(),
|
||||
requires_explicit_reexport,
|
||||
considered_definitions,
|
||||
)
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn place_impl<'db>(
|
||||
@@ -1826,7 +1812,7 @@ pub(crate) mod implicit_globals {
|
||||
.chain(module_type_syms)
|
||||
.filter_map(move |name| {
|
||||
let place = module_type_implicit_global_symbol(db, name.as_str());
|
||||
// Only include bound symbols (not undefined or possibly-undefined)
|
||||
// Only include bound symbols
|
||||
place.place.ignore_possibly_undefined().map(|ty| (name, ty))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -945,7 +945,7 @@ impl ReachabilityConstraints {
|
||||
|
||||
match imported_symbol(
|
||||
db,
|
||||
referenced_file,
|
||||
Some(referenced_file),
|
||||
symbol.name(),
|
||||
requires_explicit_reexport,
|
||||
)
|
||||
|
||||
@@ -11343,7 +11343,7 @@ impl<'db> ModuleLiteralType<'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);
|
||||
let getattr_symbol = imported_symbol(db, Some(file), "__getattr__", None);
|
||||
// If we found a __getattr__ function, try to call it with the name argument
|
||||
if let Place::Defined(place) = getattr_symbol.place
|
||||
&& let Ok(outcome) = place.ty.try_call(
|
||||
@@ -11389,11 +11389,7 @@ impl<'db> ModuleLiteralType<'db> {
|
||||
return Place::bound(submodule).into();
|
||||
}
|
||||
|
||||
let place_and_qualifiers = self
|
||||
.module(db)
|
||||
.file(db)
|
||||
.map(|file| imported_symbol(db, file, name, None))
|
||||
.unwrap_or_default();
|
||||
let place_and_qualifiers = imported_symbol(db, self.module(db).file(db), name, None);
|
||||
|
||||
// If the normal lookup failed, try to call the module's `__getattr__` function
|
||||
if place_and_qualifiers.place.is_undefined() {
|
||||
|
||||
@@ -363,6 +363,19 @@ impl<'db> AllMembers<'db> {
|
||||
}
|
||||
|
||||
Type::ModuleLiteral(literal) => {
|
||||
// Looking up `__file__` on `types.ModuleType` will not give as precise a type
|
||||
// as we infer in type inference, but it's confuisng if autocomplete etc.
|
||||
// shows a different type in the tooltip to the one inferred by the type checker.
|
||||
let dunder_file_type = if literal.module(db).file(db).is_some() {
|
||||
KnownClass::Str.to_instance(db)
|
||||
} else {
|
||||
Type::none(db)
|
||||
};
|
||||
self.members.insert(Member {
|
||||
name: Name::new_static("__file__"),
|
||||
ty: dunder_file_type,
|
||||
});
|
||||
|
||||
self.extend_with_type(db, KnownClass::ModuleType.to_instance(db));
|
||||
let module = literal.module(db);
|
||||
|
||||
@@ -377,7 +390,7 @@ impl<'db> AllMembers<'db> {
|
||||
for (symbol_id, _) in use_def_map.all_end_of_scope_symbol_declarations() {
|
||||
let symbol_name = place_table.symbol(symbol_id).name();
|
||||
let Place::Defined(DefinedPlace { ty, .. }) =
|
||||
imported_symbol(db, file, symbol_name, None).place
|
||||
imported_symbol(db, Some(file), symbol_name, None).place
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user