mirror of
https://github.com/astral-sh/ruff
synced 2026-01-20 21:10:48 -05:00
[ty] Override __file__ to str when applicable on imported modules (#22333)
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
# The `__file__` attribute on imported modules
|
||||
|
||||
## Module successfully resolved
|
||||
|
||||
```py
|
||||
from b import __file__ as module_path
|
||||
from stub import __file__ as stub_path
|
||||
from override import __file__ as overidden_path
|
||||
|
||||
reveal_type(__file__) # revealed: str
|
||||
reveal_type(module_path) # revealed: str
|
||||
reveal_type(stub_path) # revealed: str
|
||||
reveal_type(overidden_path) # revealed: None
|
||||
|
||||
# NOTE: This import fails at runtime as this is a C Extension
|
||||
# with no `__file__` global. It's hard for us to determine this
|
||||
# right now, however (all we know is it comes from a stub file),
|
||||
# and if it did exist then it would be of type `str` since `sys`
|
||||
# is not a namespace packages. This behaviour also matches other
|
||||
# type checkers.
|
||||
from sys import __file__ as no_path
|
||||
|
||||
reveal_type(no_path) # revealed: str
|
||||
```
|
||||
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
|
||||
```
|
||||
|
||||
`override.py`:
|
||||
|
||||
```py
|
||||
__file__ = None
|
||||
```
|
||||
|
||||
`stub.pyi`:
|
||||
|
||||
```pyi
|
||||
```
|
||||
|
||||
## Module resolution failed
|
||||
|
||||
```py
|
||||
from bar import __file__ as module_path # error: "Cannot resolve imported module `bar`"
|
||||
|
||||
reveal_type(module_path) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Non-namespace packages have `__file__` available
|
||||
|
||||
`a/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
`a/b.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
```py
|
||||
import a
|
||||
|
||||
reveal_type(a.__file__) # revealed: str
|
||||
```
|
||||
|
||||
## `__file__` is set to `None` for namespace packages
|
||||
|
||||
`namespace/c.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
```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
|
||||
```
|
||||
@@ -99,9 +99,14 @@ import typing
|
||||
reveal_type(typing.__name__) # revealed: str
|
||||
reveal_type(typing.__init__) # revealed: bound method ModuleType.__init__(name: str, doc: str | None = ...) -> None
|
||||
|
||||
# For a stub module, we don't know that `__file__` is a string (at runtime it may be entirely
|
||||
# unset, but we follow typeshed here):
|
||||
reveal_type(typing.__file__) # revealed: str | None
|
||||
# Note that since the source for the `typing` module is a stub file,
|
||||
# we can't know for sure that it's not a C extension at runtime,
|
||||
# and C extensions don't necessarily have a `__file__` global attribute
|
||||
# at all (in which case this attribute access would fail). However, we
|
||||
# *do* know that `typing` is not a namespace package, so if `__file__`
|
||||
# does exist, it will be of type `str` (`__file__` is only `None` for
|
||||
# namespace packages).
|
||||
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
|
||||
|
||||
@@ -1098,6 +1098,18 @@ 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))
|
||||
@@ -1111,20 +1123,43 @@ 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_table(db, scope)
|
||||
.symbol_id(name)
|
||||
.map(|symbol| {
|
||||
place_by_id(
|
||||
db,
|
||||
scope,
|
||||
symbol.into(),
|
||||
requires_explicit_reexport,
|
||||
considered_definitions,
|
||||
)
|
||||
})
|
||||
.unwrap_or_default()
|
||||
place
|
||||
}
|
||||
|
||||
fn place_impl<'db>(
|
||||
|
||||
Reference in New Issue
Block a user