mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 05:45:24 +00:00
[red-knot] Fallback to attributes on types.ModuleType if a symbol can't be found in locals or globals (#13904)
This commit is contained in:
parent
7dd0c7f4bd
commit
d2c9f5e43c
5 changed files with 278 additions and 13 deletions
|
@ -0,0 +1,141 @@
|
|||
# Implicit globals from `types.ModuleType`
|
||||
|
||||
## Implicit `ModuleType` globals
|
||||
|
||||
All modules are instances of `types.ModuleType`.
|
||||
If a name can't be found in any local or global scope, we look it up
|
||||
as an attribute on `types.ModuleType` in typeshed
|
||||
before deciding that the name is unbound.
|
||||
|
||||
```py
|
||||
reveal_type(__name__) # revealed: str
|
||||
reveal_type(__file__) # revealed: str | None
|
||||
reveal_type(__loader__) # revealed: LoaderProtocol | None
|
||||
reveal_type(__package__) # revealed: str | None
|
||||
reveal_type(__spec__) # revealed: ModuleSpec | None
|
||||
|
||||
# TODO: generics
|
||||
reveal_type(__path__) # revealed: @Todo
|
||||
|
||||
# TODO: this should probably be added to typeshed; not sure why it isn't?
|
||||
# error: [unresolved-reference]
|
||||
# revealed: Unbound
|
||||
reveal_type(__doc__)
|
||||
|
||||
class X:
|
||||
reveal_type(__name__) # revealed: str
|
||||
|
||||
def foo():
|
||||
reveal_type(__name__) # revealed: str
|
||||
```
|
||||
|
||||
However, three attributes on `types.ModuleType` are not present as implicit
|
||||
module globals; these are excluded:
|
||||
|
||||
```py path=unbound_dunders.py
|
||||
# error: [unresolved-reference]
|
||||
# revealed: Unbound
|
||||
reveal_type(__getattr__)
|
||||
|
||||
# error: [unresolved-reference]
|
||||
# revealed: Unbound
|
||||
reveal_type(__dict__)
|
||||
|
||||
# error: [unresolved-reference]
|
||||
# revealed: Unbound
|
||||
reveal_type(__init__)
|
||||
```
|
||||
|
||||
## Accessed as attributes
|
||||
|
||||
`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:
|
||||
|
||||
```py
|
||||
import typing
|
||||
|
||||
reveal_type(typing.__name__) # revealed: str
|
||||
reveal_type(typing.__init__) # revealed: Literal[__init__]
|
||||
|
||||
# These come from `builtins.object`, not `types.ModuleType`:
|
||||
# TODO: we don't currently understand `types.ModuleType` as inheriting from `object`;
|
||||
# these should not reveal `Unbound`:
|
||||
reveal_type(typing.__eq__) # revealed: Unbound
|
||||
reveal_type(typing.__class__) # revealed: Unbound
|
||||
reveal_type(typing.__module__) # revealed: Unbound
|
||||
|
||||
# TODO: needs support for attribute access on instances, properties and generics;
|
||||
# should be `dict[str, Any]`
|
||||
reveal_type(typing.__dict__) # revealed: @Todo
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```py path=__getattr__.py
|
||||
import typing
|
||||
|
||||
reveal_type(typing.__getattr__) # revealed: Unbound
|
||||
```
|
||||
|
||||
## `types.ModuleType.__dict__` takes precedence over global variable `__dict__`
|
||||
|
||||
It's impossible to override the `__dict__` attribute of `types.ModuleType`
|
||||
instances from inside the module; we should prioritise the attribute in
|
||||
the `types.ModuleType` stub over a variable named `__dict__` in the module's
|
||||
global namespace:
|
||||
|
||||
```py path=foo.py
|
||||
__dict__ = "foo"
|
||||
|
||||
reveal_type(__dict__) # revealed: Literal["foo"]
|
||||
```
|
||||
|
||||
```py path=bar.py
|
||||
import foo
|
||||
from foo import __dict__ as foo_dict
|
||||
|
||||
# TODO: needs support for attribute access on instances, properties, and generics;
|
||||
# should be `dict[str, Any]` for both of these:
|
||||
reveal_type(foo.__dict__) # revealed: @Todo
|
||||
reveal_type(foo_dict) # revealed: @Todo
|
||||
```
|
||||
|
||||
## Conditionally global or `ModuleType` attribute
|
||||
|
||||
Attributes overridden in the module namespace take priority.
|
||||
If a builtin name is conditionally defined as a global, however,
|
||||
a name lookup should union the `ModuleType` type with the conditionally defined type:
|
||||
|
||||
```py
|
||||
__file__ = 42
|
||||
|
||||
def returns_bool() -> bool:
|
||||
return True
|
||||
|
||||
if returns_bool():
|
||||
__name__ = 1
|
||||
|
||||
reveal_type(__file__) # revealed: Literal[42]
|
||||
reveal_type(__name__) # revealed: str | Literal[1]
|
||||
```
|
||||
|
||||
## Conditionally global or `ModuleType` attribute, with annotation
|
||||
|
||||
The same is true if the name is annotated:
|
||||
|
||||
```py
|
||||
__file__: int = 42
|
||||
|
||||
def returns_bool() -> bool:
|
||||
return True
|
||||
|
||||
if returns_bool():
|
||||
__name__: int = 1
|
||||
|
||||
reveal_type(__file__) # revealed: Literal[42]
|
||||
reveal_type(__name__) # revealed: str | Literal[1]
|
||||
```
|
Loading…
Add table
Add a link
Reference in a new issue