Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 103 additions & 1 deletion crates/ty_python_semantic/resources/mdtest/call/type.md
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,17 @@ class Y(C, B): ...
Conflict = type("Conflict", (X, Y), {})
```

## Cyclic base class MRO

A dynamic class inheriting from a static class with a cyclic MRO also produces an error:

```pyi
class Cyclic(Cyclic): ... # error: [cyclic-class-definition]

# error: [cyclic-class-definition]
CyclicChild = type("CyclicChild", (Cyclic,), {})
```

## `inconsistent-mro` errors with autofixes

A common cause of "inconsistent MRO" errors is where a class inherits from `Generic[]`, but
Expand Down Expand Up @@ -785,6 +796,8 @@ Y = type("Y", bases, {})

## Cyclic functional class definitions

### Self-referential

Self-referential class definitions using `type()` are detected. The name being defined is referenced
in the bases tuple before it's available:

Expand All @@ -793,6 +806,50 @@ in the bases tuple before it's available:
X = type("X", (X,), {})
```

### No string literal bases

String literals directly in the bases tuple are not valid class bases:

```py
# error: [invalid-base] "Invalid class base with type `Literal["X"]`"
X = type("X", ("X",), {})
```

### Forward references via string annotations

However, forward references via string annotations are supported, similar to regular class
definitions. This works with `NamedTuple` where field annotations can be forward references:

```py
from typing import NamedTuple

# Forward reference in NamedTuple field annotation
X = type("X", (NamedTuple("NT", [("field", "X | int")]),), {})
reveal_type(X) # revealed: <class 'X'>
```

### Static class inheriting from dynamic class with forward ref

Forward references also work when a static class inherits from a dynamic class that references it:

```py
from typing import NamedTuple

# Static class inheriting from dynamic class with forward ref back to static class
class Y(type("X", (NamedTuple("NT", [("field", "Y | int")]),), {})): ...

reveal_type(Y) # revealed: <class 'Y'>
```

Forward references via subscript annotations on generic bases are supported:

```py
# Forward reference to X via subscript annotation in tuple base
# (This fails at runtime, but we should handle it without panicking)
X = type("X", (tuple["X | None"],), {})
reveal_type(X) # revealed: <class 'X'>
```

## Dynamic class names (non-literal strings)

When the class name is not a string literal, we still create a class literal type but with a
Expand Down Expand Up @@ -953,7 +1010,7 @@ Dynamic classes cannot directly inherit from `Generic`, `Protocol`, or `TypedDic
forms require class syntax for their semantics to be properly applied:

```py
from typing import Generic, Protocol, TypeVar
from typing import Generic, NamedTuple, Protocol, TypeVar
from typing_extensions import TypedDict

T = TypeVar("T")
Expand All @@ -966,6 +1023,19 @@ ProtocolClass = type("ProtocolClass", (Protocol,), {})

# error: [invalid-base] "Invalid base for class created via `type()`"
TypedDictClass = type("TypedDictClass", (TypedDict,), {})

# error: [invalid-base] "Invalid class base with type `<special-form 'typing.NamedTuple'>`"
NamedTupleClass = type("NamedTupleClass", (NamedTuple,), {"x": int})
```

`NamedTuple` is also not allowed as a base for dynamic classes, since creating a NamedTuple requires
class syntax for the field declarations to be properly processed:

```py
from typing import NamedTuple

# error: [invalid-base] "Invalid class base with type `<special-form 'typing.NamedTuple'>`"
NT = type("NT", (NamedTuple,), {})
```

### Protocol bases
Expand Down Expand Up @@ -1127,3 +1197,35 @@ FinalDerived = final(type("FinalDerived", (Base,), {}))

class Child2(FinalDerived): ...
```

## Calling `type` via unannotated parameter

When `type` is captured as an unannotated parameter default (a common Python optimization pattern),
the parameter type is inferred as `Unknown | type[type]`. This union bypasses the early-return guard
for `type()` calls, but should not panic.

```py
def _check_type_strict(obj, t, type=type, tuple=tuple):
if type(t) is tuple:
return type(obj) in t
else:
return type(obj) is t
```

## Three-argument `type()` in a union

When `type` is one member of a union, three-argument `type()` calls go through normal call binding
instead of the early-return path, so dynamic class creation is missed.

```py
def f(flag: bool):
if flag:
x = type
else:
x = int

# TODO: should be `type[MyClass] | int`, but the `type` arm misses dynamic class creation
# because the early-return guard only matches `ClassLiteral`, not union members.
MyClass = x("MyClass", (), {}) # error: [no-matching-overload]
reveal_type(MyClass) # revealed: type | Unknown
```
17 changes: 17 additions & 0 deletions crates/ty_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1179,6 +1179,23 @@ impl<'db> Type<'db> {
.and_then(|instance| instance.own_tuple_spec(db))
}

/// If this type is a fixed-length tuple instance, returns a slice of its element types.
///
/// Returns `None` if this is not a tuple instance, or if it's a variable-length tuple.
fn fixed_tuple_elements(&self, db: &'db dyn Db) -> Option<Cow<'db, [Type<'db>]>> {
let tuple_spec = self.tuple_instance_spec(db)?;
match tuple_spec {
Cow::Borrowed(spec) => {
let elements = spec.as_fixed_length()?.elements_slice();
Some(Cow::Borrowed(elements))
}
Cow::Owned(spec) => {
let elements = spec.as_fixed_length()?.elements_slice();
Some(Cow::Owned(elements.to_vec()))
}
}
}

/// Returns the materialization of this type depending on the given `variance`.
///
/// More concretely, `T'`, the materialization of `T`, is the type `T` with all occurrences of
Expand Down
Loading