Use a tracked struct

This commit is contained in:
Charlie Marsh
2026-01-12 13:40:52 -05:00
parent b88db87755
commit 597c35458e
9 changed files with 102 additions and 83 deletions

View File

@@ -20,16 +20,13 @@ class Base: ...
class Mixin: ...
# We synthesize a class type using the name argument
Foo = type("Foo", (), {})
reveal_type(Foo) # revealed: <class 'Foo'>
reveal_type(type("Foo", (), {})) # revealed: <class 'Foo'>
# With a single base class
Foo2 = type("Foo", (Base,), {"attr": 1})
reveal_type(Foo2) # revealed: <class 'Foo'>
reveal_type(type("Foo", (Base,), {"attr": 1})) # revealed: <class 'Foo'>
# With multiple base classes
Foo3 = type("Foo", (Base, Mixin), {})
reveal_type(Foo3) # revealed: <class 'Foo'>
reveal_type(type("Foo", (Base, Mixin), {})) # revealed: <class 'Foo'>
# The inferred type is assignable to type[Base] since Foo inherits from Base
tests: list[type[Base]] = []
@@ -415,24 +412,22 @@ reveal_type(type("Bar", (int,), {}, weird_other_arg=42)) # revealed: Unknown
reveal_type(type("Baz", (), {}, metaclass=type)) # revealed: Unknown
```
The following calls are also invalid, due to incorrect argument types.
Inline calls (not assigned to a variable) fall back to regular `type` overload matching, which
produces slightly different error messages than assigned dynamic class creation:
The following calls are also invalid, due to incorrect argument types:
```py
class Base: ...
# error: 6 [invalid-argument-type] "Argument to class `type` is incorrect: Expected `str`, found `Literal[b"Foo"]`"
# error: [invalid-argument-type] "Invalid argument to parameter 1 (`name`) of `type()`: Expected `str`, found `Literal[b"Foo"]`"
type(b"Foo", (), {})
# error: 13 [invalid-argument-type] "Argument to class `type` is incorrect: Expected `tuple[type, ...]`, found `<class 'Base'>`"
# error: [invalid-argument-type] "Invalid argument to parameter 2 (`bases`) of `type()`: Expected `tuple[type, ...]`, found `<class 'Base'>`"
type("Foo", Base, {})
# error: 13 [invalid-argument-type] "Argument to class `type` is incorrect: Expected `tuple[type, ...]`, found `tuple[Literal[1], Literal[2]]`"
# error: 14 [invalid-base] "Invalid class base with type `Literal[1]`"
# error: 17 [invalid-base] "Invalid class base with type `Literal[2]`"
type("Foo", (1, 2), {})
# error: 22 [invalid-argument-type] "Argument to class `type` is incorrect: Expected `dict[str, Any]`, found `dict[str | bytes, Any]`"
# error: [invalid-argument-type] "Invalid argument to parameter 3 (`namespace`) of `type()`: Expected `dict[str, Any]`, found `dict[Unknown | bytes, Unknown | int]`"
type("Foo", (Base,), {b"attr": 1})
```
@@ -656,20 +651,24 @@ def f(*args, **kwargs):
## Explicit type annotations
TODO: Annotated assignments with `type()` calls don't currently synthesize the specific class type.
This will be fixed when we support all `type()` calls (including inline) via generic handling.
When an explicit type annotation is provided, the inferred type is checked against it:
```py
# The annotation `type` is compatible with the inferred class literal type
T: type = type("T", (), {})
reveal_type(T) # revealed: <class 'T'>
# The annotation `type[Base]` is compatible with the inferred type
class Base: ...
# TODO: Should infer `<class 'T'>` instead of `type`
T: type = type("T", (), {})
reveal_type(T) # revealed: type
# TODO: Should infer `<class 'Derived'>` instead of `type[Base]}
# error: [invalid-assignment] "Object of type `type` is not assignable to `type[Base]`"
Derived: type[Base] = type("Derived", (Base,), {})
reveal_type(Derived) # revealed: type[Base]
reveal_type(Derived) # revealed: <class 'Derived'>
# Incompatible annotation produces an error
class Unrelated: ...
# error: [invalid-assignment]
Bad: type[Unrelated] = type("Bad", (Base,), {})
```
## Special base classes

View File

@@ -169,12 +169,15 @@ Narrowing does not occur in the same way if `type` is used to dynamically create
```py
def _(x: str | int):
# Inline type() calls fall back to regular type overload matching.
# TODO: Once inline type() calls synthesize class types, this should narrow x to Never.
# The following diagnostic is valid, since the three-argument form of `type`
# can only be called with `str` as the first argument.
#
# error: 13 [invalid-argument-type] "Argument to class `type` is incorrect: Expected `str`, found `str | int`"
# error: [invalid-argument-type] "Invalid argument to parameter 1 (`name`) of `type()`: Expected `str`, found `str | int`"
if type(x, (), {}) is str:
reveal_type(x) # revealed: str | int
# But we synthesize a new class object as the result of a three-argument call to `type`,
# and we know that this synthesized class object is not the same object as the `str` class object,
# so here the type is narrowed to `Never`!
reveal_type(x) # revealed: Never
else:
reveal_type(x) # revealed: str | int
```