Merge branch 'dcreager/source-order-constraints' into dcreager/callable-return

* dcreager/source-order-constraints: (30 commits)
  clippy
  fix test expectations (again)
  include source_order in display_graph output
  place bounds/constraints first
  don't always bump
  only fold once
  document display source_order
  more comments
  remove now-unused items
  fix test expectation
  use source order in specialize_constrained too
  document overall approach
  more comment
  reuse self source_order
  sort specialize_constrained by source_order
  lots of renaming
  remove source_order_for
  simpler source_order_for
  doc
  restore TODOs
  ...
This commit is contained in:
Douglas Creager
2025-12-15 10:52:26 -05:00
37 changed files with 1748 additions and 698 deletions

View File

@@ -201,7 +201,7 @@ python-version = "3.12"
```py
type IntOrStr = int | str
reveal_type(IntOrStr.__or__) # revealed: bound method typing.TypeAliasType.__or__(right: Any, /) -> _SpecialForm
reveal_type(IntOrStr.__or__) # revealed: bound method TypeAliasType.__or__(right: Any, /) -> _SpecialForm
```
## Method calls on types not disjoint from `None`

View File

@@ -567,7 +567,7 @@ def f(x: int):
super(x, x)
type IntAlias = int
# error: [invalid-super-argument] "`typing.TypeAliasType` is not a valid class"
# error: [invalid-super-argument] "`TypeAliasType` is not a valid class"
super(IntAlias, 0)
# error: [invalid-super-argument] "`str` is not an instance or subclass of `<class 'int'>` in `super(<class 'int'>, str)` call"

View File

@@ -521,6 +521,73 @@ frozen = MyFrozenChildClass()
del frozen.x # TODO this should emit an [invalid-assignment]
```
### frozen/non-frozen inheritance
If a non-frozen dataclass inherits from a frozen dataclass, an exception is raised at runtime. We
catch this error:
<!-- snapshot-diagnostics -->
`a.py`:
```py
from dataclasses import dataclass
@dataclass(frozen=True)
class FrozenBase:
x: int
@dataclass
# error: [invalid-frozen-dataclass-subclass] "Non-frozen dataclass `Child` cannot inherit from frozen dataclass `FrozenBase`"
class Child(FrozenBase):
y: int
```
Frozen dataclasses inheriting from non-frozen dataclasses are also illegal:
`b.py`:
```py
from dataclasses import dataclass
@dataclass
class Base:
x: int
@dataclass(frozen=True)
# error: [invalid-frozen-dataclass-subclass] "Frozen dataclass `FrozenChild` cannot inherit from non-frozen dataclass `Base`"
class FrozenChild(Base):
y: int
```
Example of diagnostics when there are multiple files involved:
`module.py`:
```py
import dataclasses
@dataclasses.dataclass(frozen=False)
class NotFrozenBase:
x: int
```
`main.py`:
```py
from functools import total_ordering
from typing import final
from dataclasses import dataclass
from module import NotFrozenBase
@final
@dataclass(frozen=True)
@total_ordering
class FrozenChild(NotFrozenBase): # error: [invalid-frozen-dataclass-subclass]
y: str
```
### `match_args`
If `match_args` is set to `True` (the default), the `__match_args__` attribute is a tuple created

View File

@@ -9,7 +9,7 @@ from typing import ParamSpec
P = ParamSpec("P")
reveal_type(type(P)) # revealed: <class 'ParamSpec'>
reveal_type(P) # revealed: typing.ParamSpec
reveal_type(P) # revealed: ParamSpec
reveal_type(P.__name__) # revealed: Literal["P"]
```

View File

@@ -22,7 +22,7 @@ from typing import TypeVar
T = TypeVar("T")
reveal_type(type(T)) # revealed: <class 'TypeVar'>
reveal_type(T) # revealed: typing.TypeVar
reveal_type(T) # revealed: TypeVar
reveal_type(T.__name__) # revealed: Literal["T"]
```
@@ -146,7 +146,7 @@ from typing import TypeVar
T = TypeVar("T", default=int)
reveal_type(type(T)) # revealed: <class 'TypeVar'>
reveal_type(T) # revealed: typing.TypeVar
reveal_type(T) # revealed: TypeVar
reveal_type(T.__default__) # revealed: int
reveal_type(T.__bound__) # revealed: None
reveal_type(T.__constraints__) # revealed: tuple[()]
@@ -187,7 +187,7 @@ from typing import TypeVar
T = TypeVar("T", bound=int)
reveal_type(type(T)) # revealed: <class 'TypeVar'>
reveal_type(T) # revealed: typing.TypeVar
reveal_type(T) # revealed: TypeVar
reveal_type(T.__bound__) # revealed: int
reveal_type(T.__constraints__) # revealed: tuple[()]
@@ -211,7 +211,7 @@ from typing import TypeVar
T = TypeVar("T", int, str)
reveal_type(type(T)) # revealed: <class 'TypeVar'>
reveal_type(T) # revealed: typing.TypeVar
reveal_type(T) # revealed: TypeVar
reveal_type(T.__constraints__) # revealed: tuple[int, str]
S = TypeVar("S")

View File

@@ -12,7 +12,7 @@ python-version = "3.13"
```py
def foo1[**P]() -> None:
reveal_type(P) # revealed: typing.ParamSpec
reveal_type(P) # revealed: ParamSpec
```
## Bounds and constraints
@@ -45,14 +45,14 @@ The default value for a `ParamSpec` can be either a list of types, `...`, or ano
```py
def foo2[**P = ...]() -> None:
reveal_type(P) # revealed: typing.ParamSpec
reveal_type(P) # revealed: ParamSpec
def foo3[**P = [int, str]]() -> None:
reveal_type(P) # revealed: typing.ParamSpec
reveal_type(P) # revealed: ParamSpec
def foo4[**P, **Q = P]():
reveal_type(P) # revealed: typing.ParamSpec
reveal_type(Q) # revealed: typing.ParamSpec
reveal_type(P) # revealed: ParamSpec
reveal_type(Q) # revealed: ParamSpec
```
Other values are invalid.

View File

@@ -17,7 +17,7 @@ instances of `typing.TypeVar`, just like legacy type variables.
```py
def f[T]():
reveal_type(type(T)) # revealed: <class 'TypeVar'>
reveal_type(T) # revealed: typing.TypeVar
reveal_type(T) # revealed: TypeVar
reveal_type(T.__name__) # revealed: Literal["T"]
```
@@ -33,7 +33,7 @@ python-version = "3.13"
```py
def f[T = int]():
reveal_type(type(T)) # revealed: <class 'TypeVar'>
reveal_type(T) # revealed: typing.TypeVar
reveal_type(T) # revealed: TypeVar
reveal_type(T.__default__) # revealed: int
reveal_type(T.__bound__) # revealed: None
reveal_type(T.__constraints__) # revealed: tuple[()]
@@ -66,7 +66,7 @@ class Invalid[S = T]: ...
```py
def f[T: int]():
reveal_type(type(T)) # revealed: <class 'TypeVar'>
reveal_type(T) # revealed: typing.TypeVar
reveal_type(T) # revealed: TypeVar
reveal_type(T.__bound__) # revealed: int
reveal_type(T.__constraints__) # revealed: tuple[()]
@@ -79,7 +79,7 @@ def g[S]():
```py
def f[T: (int, str)]():
reveal_type(type(T)) # revealed: <class 'TypeVar'>
reveal_type(T) # revealed: typing.TypeVar
reveal_type(T) # revealed: TypeVar
reveal_type(T.__constraints__) # revealed: tuple[int, str]
reveal_type(T.__bound__) # revealed: None

View File

@@ -400,7 +400,7 @@ reveal_type(ListOrTuple) # revealed: <types.UnionType special form 'list[T@List
reveal_type(ListOrTupleLegacy)
reveal_type(MyCallable) # revealed: <typing.Callable special form '(**P@MyCallable) -> T@MyCallable'>
reveal_type(AnnotatedType) # revealed: <special form 'typing.Annotated[T@AnnotatedType, <metadata>]'>
reveal_type(TransparentAlias) # revealed: typing.TypeVar
reveal_type(TransparentAlias) # revealed: TypeVar
reveal_type(MyOptional) # revealed: <types.UnionType special form 'T@MyOptional | None'>
def _(

View File

@@ -12,7 +12,7 @@ python-version = "3.12"
```py
type IntOrStr = int | str
reveal_type(IntOrStr) # revealed: typing.TypeAliasType
reveal_type(IntOrStr) # revealed: TypeAliasType
reveal_type(IntOrStr.__name__) # revealed: Literal["IntOrStr"]
x: IntOrStr = 1
@@ -205,7 +205,7 @@ from typing_extensions import TypeAliasType, Union
IntOrStr = TypeAliasType("IntOrStr", Union[int, str])
reveal_type(IntOrStr) # revealed: typing.TypeAliasType
reveal_type(IntOrStr) # revealed: TypeAliasType
reveal_type(IntOrStr.__name__) # revealed: Literal["IntOrStr"]

View File

@@ -13,7 +13,7 @@ diagnostic message for `invalid-exception-caught` expects to construct `typing.P
def foo[**P]() -> None:
try:
pass
# error: [invalid-exception-caught] "Invalid object caught in an exception handler: Object has type `typing.ParamSpec`"
# error: [invalid-exception-caught] "Invalid object caught in an exception handler: Object has type `ParamSpec`"
except P:
pass
```

View File

@@ -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
```

View File

@@ -0,0 +1,154 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: dataclasses.md - Dataclasses - Other dataclass parameters - frozen/non-frozen inheritance
mdtest path: crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md
---
# Python source files
## a.py
```
1 | from dataclasses import dataclass
2 |
3 | @dataclass(frozen=True)
4 | class FrozenBase:
5 | x: int
6 |
7 | @dataclass
8 | # error: [invalid-frozen-dataclass-subclass] "Non-frozen dataclass `Child` cannot inherit from frozen dataclass `FrozenBase`"
9 | class Child(FrozenBase):
10 | y: int
```
## b.py
```
1 | from dataclasses import dataclass
2 |
3 | @dataclass
4 | class Base:
5 | x: int
6 |
7 | @dataclass(frozen=True)
8 | # error: [invalid-frozen-dataclass-subclass] "Frozen dataclass `FrozenChild` cannot inherit from non-frozen dataclass `Base`"
9 | class FrozenChild(Base):
10 | y: int
```
## module.py
```
1 | import dataclasses
2 |
3 | @dataclasses.dataclass(frozen=False)
4 | class NotFrozenBase:
5 | x: int
```
## main.py
```
1 | from functools import total_ordering
2 | from typing import final
3 | from dataclasses import dataclass
4 |
5 | from module import NotFrozenBase
6 |
7 | @final
8 | @dataclass(frozen=True)
9 | @total_ordering
10 | class FrozenChild(NotFrozenBase): # error: [invalid-frozen-dataclass-subclass]
11 | y: str
```
# Diagnostics
```
error[invalid-frozen-dataclass-subclass]: Non-frozen dataclass cannot inherit from frozen dataclass
--> src/a.py:7:1
|
5 | x: int
6 |
7 | @dataclass
| ---------- `Child` dataclass parameters
8 | # error: [invalid-frozen-dataclass-subclass] "Non-frozen dataclass `Child` cannot inherit from frozen dataclass `FrozenBase`"
9 | class Child(FrozenBase):
| ^^^^^^----------^ Subclass `Child` is not frozen but base class `FrozenBase` is
10 | y: int
|
info: This causes the class creation to fail
info: Base class definition
--> src/a.py:3:1
|
1 | from dataclasses import dataclass
2 |
3 | @dataclass(frozen=True)
| ----------------------- `FrozenBase` dataclass parameters
4 | class FrozenBase:
| ^^^^^^^^^^ `FrozenBase` definition
5 | x: int
|
info: rule `invalid-frozen-dataclass-subclass` is enabled by default
```
```
error[invalid-frozen-dataclass-subclass]: Frozen dataclass cannot inherit from non-frozen dataclass
--> src/b.py:7:1
|
5 | x: int
6 |
7 | @dataclass(frozen=True)
| ----------------------- `FrozenChild` dataclass parameters
8 | # error: [invalid-frozen-dataclass-subclass] "Frozen dataclass `FrozenChild` cannot inherit from non-frozen dataclass `Base`"
9 | class FrozenChild(Base):
| ^^^^^^^^^^^^----^ Subclass `FrozenChild` is frozen but base class `Base` is not
10 | y: int
|
info: This causes the class creation to fail
info: Base class definition
--> src/b.py:3:1
|
1 | from dataclasses import dataclass
2 |
3 | @dataclass
| ---------- `Base` dataclass parameters
4 | class Base:
| ^^^^ `Base` definition
5 | x: int
|
info: rule `invalid-frozen-dataclass-subclass` is enabled by default
```
```
error[invalid-frozen-dataclass-subclass]: Frozen dataclass cannot inherit from non-frozen dataclass
--> src/main.py:8:1
|
7 | @final
8 | @dataclass(frozen=True)
| ----------------------- `FrozenChild` dataclass parameters
9 | @total_ordering
10 | class FrozenChild(NotFrozenBase): # error: [invalid-frozen-dataclass-subclass]
| ^^^^^^^^^^^^-------------^ Subclass `FrozenChild` is frozen but base class `NotFrozenBase` is not
11 | y: str
|
info: This causes the class creation to fail
info: Base class definition
--> src/module.py:3:1
|
1 | import dataclasses
2 |
3 | @dataclasses.dataclass(frozen=False)
| ------------------------------------ `NotFrozenBase` dataclass parameters
4 | class NotFrozenBase:
| ^^^^^^^^^^^^^ `NotFrozenBase` definition
5 | x: int
|
info: rule `invalid-frozen-dataclass-subclass` is enabled by default
```