Add Python 3.13 guard

This commit is contained in:
Charlie Marsh 2025-12-13 16:14:40 -05:00
parent d625a304ce
commit acb9e1563e
2 changed files with 51 additions and 13 deletions

View File

@ -2,17 +2,45 @@
## Class body implicit attributes ## Class body implicit attributes
Python makes certain names available implicitly inside class body scopes. These are `__qualname__`, Python makes certain names available implicitly inside class body scopes. These are `__qualname__`
`__module__`, and `__firstlineno__`, as documented at and `__module__`, as documented at
<https://docs.python.org/3/reference/datamodel.html#creating-the-class-object>. <https://docs.python.org/3/reference/datamodel.html#creating-the-class-object>.
```py ```py
class Foo: class Foo:
reveal_type(__qualname__) # revealed: str reveal_type(__qualname__) # revealed: str
reveal_type(__module__) # revealed: str reveal_type(__module__) # revealed: str
```
## `__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 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 ## Nested classes
These implicit attributes are also available in nested classes, and refer to the nested class: These implicit attributes are also available in nested classes, and refer to the nested class:
@ -32,17 +60,32 @@ within the class body:
```py ```py
__qualname__ = 42 __qualname__ = 42
__module__ = 42 __module__ = 42
__firstlineno__ = "not an int"
class Foo: class Foo:
# Inside the class body, these are the implicit class attributes # Inside the class body, these are the implicit class attributes
reveal_type(__qualname__) # revealed: str reveal_type(__qualname__) # revealed: str
reveal_type(__module__) # revealed: str reveal_type(__module__) # revealed: str
reveal_type(__firstlineno__) # revealed: int
# Outside the class, the globals are visible # Outside the class, the globals are visible
reveal_type(__qualname__) # revealed: Literal[42] reveal_type(__qualname__) # revealed: Literal[42]
reveal_type(__module__) # 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"] reveal_type(__firstlineno__) # revealed: Literal["not an int"]
``` ```

View File

@ -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};
@ -1646,17 +1647,11 @@ pub(crate) fn class_body_implicit_symbol<'db>(
name: &str, name: &str,
) -> PlaceAndQualifiers<'db> { ) -> PlaceAndQualifiers<'db> {
match name { match name {
// __qualname__ is the fully-qualified name of the class
"__qualname__" => Place::bound(KnownClass::Str.to_instance(db)).into(), "__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(), "__module__" => Place::bound(KnownClass::Str.to_instance(db)).into(),
"__firstlineno__" if Program::get(db).python_version(db) >= PythonVersion::PY313 => {
// __firstlineno__ was added in Python 3.13, but we don't version-check since Place::bound(KnownClass::Int.to_instance(db)).into()
// 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(), _ => Place::Undefined.into(),
} }
} }