mirror of https://github.com/astral-sh/ruff
[ty] Add support for `__qualname__` and other implicit class attributes (#21966)
## Summary Closes https://github.com/astral-sh/ty/issues/1873
This commit is contained in:
parent
a544c59186
commit
be8eb92946
|
|
@ -0,0 +1,120 @@
|
||||||
|
# Implicit class body attributes
|
||||||
|
|
||||||
|
## Class body implicit attributes
|
||||||
|
|
||||||
|
Python makes certain names available implicitly inside class body scopes. These are `__qualname__`,
|
||||||
|
`__module__`, and `__doc__`, as documented at
|
||||||
|
<https://docs.python.org/3/reference/datamodel.html#creating-the-class-object>.
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Foo:
|
||||||
|
reveal_type(__qualname__) # revealed: str
|
||||||
|
reveal_type(__module__) # revealed: str
|
||||||
|
reveal_type(__doc__) # revealed: str | None
|
||||||
|
```
|
||||||
|
|
||||||
|
## `__firstlineno__` (Python 3.13+)
|
||||||
|
|
||||||
|
Python 3.13 added `__firstlineno__` to the class body namespace.
|
||||||
|
|
||||||
|
### Available in Python 3.13+
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.13"
|
||||||
|
```
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Foo:
|
||||||
|
reveal_type(__firstlineno__) # revealed: int
|
||||||
|
```
|
||||||
|
|
||||||
|
### Not available in Python 3.12 and earlier
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.12"
|
||||||
|
```
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Foo:
|
||||||
|
# error: [unresolved-reference]
|
||||||
|
__firstlineno__
|
||||||
|
```
|
||||||
|
|
||||||
|
## Nested classes
|
||||||
|
|
||||||
|
These implicit attributes are also available in nested classes, and refer to the nested class:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Outer:
|
||||||
|
class Inner:
|
||||||
|
reveal_type(__qualname__) # revealed: str
|
||||||
|
reveal_type(__module__) # revealed: str
|
||||||
|
```
|
||||||
|
|
||||||
|
## Class body implicit attributes have priority over globals
|
||||||
|
|
||||||
|
If a global variable with the same name exists, the class body implicit attribute takes priority
|
||||||
|
within the class body:
|
||||||
|
|
||||||
|
```py
|
||||||
|
__qualname__ = 42
|
||||||
|
__module__ = 42
|
||||||
|
|
||||||
|
class Foo:
|
||||||
|
# Inside the class body, these are the implicit class attributes
|
||||||
|
reveal_type(__qualname__) # revealed: str
|
||||||
|
reveal_type(__module__) # revealed: str
|
||||||
|
|
||||||
|
# Outside the class, the globals are visible
|
||||||
|
reveal_type(__qualname__) # revealed: Literal[42]
|
||||||
|
reveal_type(__module__) # revealed: Literal[42]
|
||||||
|
```
|
||||||
|
|
||||||
|
## `__firstlineno__` has priority over globals (Python 3.13+)
|
||||||
|
|
||||||
|
The same applies to `__firstlineno__` on Python 3.13+:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.13"
|
||||||
|
```
|
||||||
|
|
||||||
|
```py
|
||||||
|
__firstlineno__ = "not an int"
|
||||||
|
|
||||||
|
class Foo:
|
||||||
|
reveal_type(__firstlineno__) # revealed: int
|
||||||
|
|
||||||
|
reveal_type(__firstlineno__) # revealed: Literal["not an int"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Class body implicit attributes are not visible in methods
|
||||||
|
|
||||||
|
The implicit class body attributes are only available directly in the class body, not in nested
|
||||||
|
function scopes (methods):
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Foo:
|
||||||
|
# Available directly in the class body
|
||||||
|
x = __qualname__
|
||||||
|
reveal_type(x) # revealed: str
|
||||||
|
|
||||||
|
def method(self):
|
||||||
|
# Not available in methods - falls back to builtins/globals
|
||||||
|
# error: [unresolved-reference]
|
||||||
|
__qualname__
|
||||||
|
```
|
||||||
|
|
||||||
|
## Real-world use case: logging
|
||||||
|
|
||||||
|
A common use case is defining a logger with the class name:
|
||||||
|
|
||||||
|
```py
|
||||||
|
import logging
|
||||||
|
|
||||||
|
class MyClass:
|
||||||
|
logger = logging.getLogger(__qualname__)
|
||||||
|
reveal_type(logger) # revealed: Logger
|
||||||
|
```
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use ruff_db::files::File;
|
use ruff_db::files::File;
|
||||||
|
use ruff_python_ast::PythonVersion;
|
||||||
|
|
||||||
use crate::dunder_all::dunder_all_names;
|
use crate::dunder_all::dunder_all_names;
|
||||||
use crate::module_resolver::{KnownModule, file_to_module, resolve_module_confident};
|
use crate::module_resolver::{KnownModule, file_to_module, resolve_module_confident};
|
||||||
|
|
@ -1633,6 +1634,35 @@ mod implicit_globals {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Looks up the type of an "implicit class body symbol". Returns [`Place::Undefined`] if
|
||||||
|
/// `name` is not present as an implicit symbol in class bodies.
|
||||||
|
///
|
||||||
|
/// Implicit class body symbols are symbols such as `__qualname__`, `__module__`, `__doc__`,
|
||||||
|
/// and `__firstlineno__` that Python implicitly makes available inside a class body during
|
||||||
|
/// class creation.
|
||||||
|
///
|
||||||
|
/// See <https://docs.python.org/3/reference/datamodel.html#creating-the-class-object>
|
||||||
|
pub(crate) fn class_body_implicit_symbol<'db>(
|
||||||
|
db: &'db dyn Db,
|
||||||
|
name: &str,
|
||||||
|
) -> PlaceAndQualifiers<'db> {
|
||||||
|
match name {
|
||||||
|
"__qualname__" => Place::bound(KnownClass::Str.to_instance(db)).into(),
|
||||||
|
"__module__" => Place::bound(KnownClass::Str.to_instance(db)).into(),
|
||||||
|
// __doc__ is `str` if there's a docstring, `None` if there isn't
|
||||||
|
"__doc__" => Place::bound(UnionType::from_elements(
|
||||||
|
db,
|
||||||
|
[KnownClass::Str.to_instance(db), Type::none(db)],
|
||||||
|
))
|
||||||
|
.into(),
|
||||||
|
// __firstlineno__ was added in Python 3.13
|
||||||
|
"__firstlineno__" if Program::get(db).python_version(db) >= PythonVersion::PY313 => {
|
||||||
|
Place::bound(KnownClass::Int.to_instance(db)).into()
|
||||||
|
}
|
||||||
|
_ => Place::Undefined.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
pub(crate) enum RequiresExplicitReExport {
|
pub(crate) enum RequiresExplicitReExport {
|
||||||
Yes,
|
Yes,
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,9 @@ use crate::module_resolver::{
|
||||||
use crate::node_key::NodeKey;
|
use crate::node_key::NodeKey;
|
||||||
use crate::place::{
|
use crate::place::{
|
||||||
ConsideredDefinitions, Definedness, LookupError, Place, PlaceAndQualifiers, TypeOrigin,
|
ConsideredDefinitions, Definedness, LookupError, Place, PlaceAndQualifiers, TypeOrigin,
|
||||||
builtins_module_scope, builtins_symbol, explicit_global_symbol, global_symbol,
|
builtins_module_scope, builtins_symbol, class_body_implicit_symbol, explicit_global_symbol,
|
||||||
module_type_implicit_global_declaration, module_type_implicit_global_symbol, place,
|
global_symbol, module_type_implicit_global_declaration, module_type_implicit_global_symbol,
|
||||||
place_from_bindings, place_from_declarations, typing_extensions_symbol,
|
place, place_from_bindings, place_from_declarations, typing_extensions_symbol,
|
||||||
};
|
};
|
||||||
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
|
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
|
||||||
use crate::semantic_index::ast_ids::{HasScopedUseId, ScopedUseId};
|
use crate::semantic_index::ast_ids::{HasScopedUseId, ScopedUseId};
|
||||||
|
|
@ -9210,6 +9210,25 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||||
}
|
}
|
||||||
|
|
||||||
PlaceAndQualifiers::from(Place::Undefined)
|
PlaceAndQualifiers::from(Place::Undefined)
|
||||||
|
// If we're in a class body, check for implicit class body symbols first.
|
||||||
|
// These take precedence over globals.
|
||||||
|
.or_fall_back_to(db, || {
|
||||||
|
if scope.node(db).scope_kind().is_class()
|
||||||
|
&& let Some(symbol) = place_expr.as_symbol()
|
||||||
|
{
|
||||||
|
let implicit = class_body_implicit_symbol(db, symbol.name());
|
||||||
|
if implicit.place.is_definitely_bound() {
|
||||||
|
return implicit.map_type(|ty| {
|
||||||
|
self.narrow_place_with_applicable_constraints(
|
||||||
|
place_expr,
|
||||||
|
ty,
|
||||||
|
&constraint_keys,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Place::Undefined.into()
|
||||||
|
})
|
||||||
// No nonlocal binding? Check the module's explicit globals.
|
// No nonlocal binding? Check the module's explicit globals.
|
||||||
// Avoid infinite recursion if `self.scope` already is the module's global scope.
|
// Avoid infinite recursion if `self.scope` already is the module's global scope.
|
||||||
.or_fall_back_to(db, || {
|
.or_fall_back_to(db, || {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue