-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[red-knot] Fallback to attributes on types.ModuleType if a symbol can't be found in locals or globals #13904
Changes from all commits
b758655
7ceac1a
939c9c5
dfcd9a6
bc7cc49
0a40501
eee3e80
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Sigh. I think this is not worth special-casing in red-knot; we should just accept it as an inaccuracy in type inference that we have to live with unless and until it's fixed in typeshed. |
||
|
||
`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" | ||
AlexWaygood marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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] | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -47,17 +47,27 @@ impl Symbol { | |
pub fn is_bound(&self) -> bool { | ||
self.flags.contains(SymbolFlags::IS_BOUND) | ||
} | ||
|
||
/// Is the symbol declared in its containing scope? | ||
pub fn is_declared(&self) -> bool { | ||
self.flags.contains(SymbolFlags::IS_DECLARED) | ||
} | ||
} | ||
|
||
bitflags! { | ||
/// Flags that can be queried to obtain information about a symbol in a given scope. | ||
/// | ||
/// See the doc-comment at the top of [`super::use_def`] for explanations of what it | ||
/// means for a symbol to be *bound* as opposed to *declared*. | ||
#[derive(Copy, Clone, Debug, Eq, PartialEq)] | ||
struct SymbolFlags: u8 { | ||
const IS_USED = 1 << 0; | ||
const IS_BOUND = 1 << 1; | ||
const IS_BOUND = 1 << 1; | ||
const IS_DECLARED = 1 << 2; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you add some comment explaining There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added a link to Carl's essay at the top of https://github.com/astral-sh/ruff/blob/main/crates/red_knot_python_semantic/src/semantic_index/use_def.rs |
||
/// TODO: This flag is not yet set by anything | ||
const MARKED_GLOBAL = 1 << 2; | ||
const MARKED_GLOBAL = 1 << 3; | ||
/// TODO: This flag is not yet set by anything | ||
const MARKED_NONLOCAL = 1 << 3; | ||
const MARKED_NONLOCAL = 1 << 4; | ||
} | ||
} | ||
|
||
|
@@ -298,6 +308,10 @@ impl SymbolTableBuilder { | |
self.table.symbols[id].insert_flags(SymbolFlags::IS_BOUND); | ||
} | ||
|
||
pub(super) fn mark_symbol_declared(&mut self, id: ScopedSymbolId) { | ||
self.table.symbols[id].insert_flags(SymbolFlags::IS_DECLARED); | ||
} | ||
|
||
pub(super) fn mark_symbol_used(&mut self, id: ScopedSymbolId) { | ||
self.table.symbols[id].insert_flags(SymbolFlags::IS_USED); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably because it's on
builtins.object
?The awkward thing is that I think
__doc__
might be the only attribute onobject
that is accessible in every module as a global name.I think all of them except for
__module__
are accessible on module objects as an attribute.You'll know better than I whether it's likely we can get
__doc__
added specifically toModuleType
(despite it already being onobject
) to help support fixing this TODO with fewer special cases, or whether we should instead just bite the bullet and handle it as a special case.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah... I think it would be fine to add
__doc__
toModuleType
in typeshed if we add a comment saying why we have the "redundant" override fromobject
... I'll try to put up a typeshed PR today...There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Filed python/typeshed#12918