diff --git a/crates/ty_python_semantic/resources/mdtest/scopes/class_implicit_attrs.md b/crates/ty_python_semantic/resources/mdtest/scopes/class_implicit_attrs.md new file mode 100644 index 0000000000..009b0a0a96 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/scopes/class_implicit_attrs.md @@ -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 +. + +```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 +``` diff --git a/crates/ty_python_semantic/src/place.rs b/crates/ty_python_semantic/src/place.rs index a1319a3250..cf3fe5660b 100644 --- a/crates/ty_python_semantic/src/place.rs +++ b/crates/ty_python_semantic/src/place.rs @@ -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 +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, diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 57e3df8eb9..8a43d06ac3 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -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, || {