Add support for __qualname__ and other class attributes

This commit is contained in:
Charlie Marsh 2025-12-13 14:29:59 -05:00
parent ff0ed4e752
commit d625a304ce
3 changed files with 126 additions and 3 deletions

View File

@ -0,0 +1,76 @@
# Implicit class body attributes
## Class body implicit attributes
Python makes certain names available implicitly inside class body scopes. These are `__qualname__`,
`__module__`, and `__firstlineno__`, 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(__firstlineno__) # revealed: int
```
## 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
__firstlineno__ = "not an int"
class Foo:
# Inside the class body, these are the implicit class attributes
reveal_type(__qualname__) # revealed: str
reveal_type(__module__) # revealed: str
reveal_type(__firstlineno__) # revealed: int
# Outside the class, the globals are visible
reveal_type(__qualname__) # revealed: Literal[42]
reveal_type(__module__) # revealed: Literal[42]
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
```

View File

@ -1633,6 +1633,34 @@ 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__`, 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__ is the fully-qualified name of the class
"__qualname__" => Place::bound(KnownClass::Str.to_instance(db)).into(),
// __module__ is the module name where the class is defined
"__module__" => Place::bound(KnownClass::Str.to_instance(db)).into(),
// __firstlineno__ was added in Python 3.13, but we don't version-check since
// it's always available in the class body namespace (just the __firstlineno__
// attribute on the class itself may not be present)
"__firstlineno__" => Place::bound(KnownClass::Int.to_instance(db)).into(),
_ => Place::Undefined.into(),
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub(crate) enum RequiresExplicitReExport {
Yes,

View File

@ -28,9 +28,9 @@ use crate::module_resolver::{
use crate::node_key::NodeKey;
use crate::place::{
ConsideredDefinitions, Definedness, LookupError, Place, PlaceAndQualifiers, TypeOrigin,
builtins_module_scope, builtins_symbol, explicit_global_symbol, global_symbol,
module_type_implicit_global_declaration, module_type_implicit_global_symbol, place,
place_from_bindings, place_from_declarations, typing_extensions_symbol,
builtins_module_scope, builtins_symbol, class_body_implicit_symbol, explicit_global_symbol,
global_symbol, module_type_implicit_global_declaration, module_type_implicit_global_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::{HasScopedUseId, ScopedUseId};
@ -9170,6 +9170,25 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
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() {
if 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.
// Avoid infinite recursion if `self.scope` already is the module's global scope.
.or_fall_back_to(db, || {