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
73 changes: 60 additions & 13 deletions crates/ty_python_semantic/resources/mdtest/call/type.md
Original file line number Diff line number Diff line change
Expand Up @@ -908,12 +908,34 @@ Bad: type[Unrelated] = type("Bad", (Base,), {})
## Special base classes

Some special base classes work with dynamic class creation, but special semantics may not be fully
synthesized:
synthesized.

### Invalid special bases

Dynamic classes cannot directly inherit from `Generic`, `Protocol`, or `TypedDict`. These special
forms require class syntax for their semantics to be properly applied:

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

T = TypeVar("T")

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

# error: [unsupported-dynamic-base] "Unsupported base for class created via `type()`"
ProtocolClass = type("ProtocolClass", (Protocol,), {})

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

### Protocol bases

Inheriting from a class that is itself a protocol is valid:

```py
# Protocol bases work - the class is created as a subclass of the protocol
from typing import Protocol
from ty_extensions import reveal_mro

Expand All @@ -930,8 +952,9 @@ reveal_type(instance) # revealed: ProtoImpl

### TypedDict bases

Inheriting from a class that is itself a TypedDict is valid:

```py
# TypedDict bases work but TypedDict semantics aren't applied to dynamic subclasses
from typing_extensions import TypedDict
from ty_extensions import reveal_mro

Expand Down Expand Up @@ -964,26 +987,26 @@ reveal_mro(Point3D) # revealed: (<class 'Point3D'>, <class 'Point'>, <class 'tu

### Enum bases

Creating a class via `type()` that inherits from any Enum class fails at runtime because `EnumMeta`
expects special attributes in the class dict that `type()` doesn't provide:

```py
# Enum subclassing via type() is not supported - EnumMeta requires special dict handling
# that type() cannot provide. This applies even to empty enums.
from enum import Enum

class Color(Enum):
RED = 1
GREEN = 2

# Enums with members are final and cannot be subclassed
# error: [subclass-of-final-class]
ExtendedColor = type("ExtendedColor", (Color,), {})

class EmptyEnum(Enum):
pass

# TODO: We should emit a diagnostic here - type() cannot create Enum subclasses
ExtendedColor = type("ExtendedColor", (Color,), {})
reveal_type(ExtendedColor) # revealed: <class 'ExtendedColor'>

# Even empty enums fail - EnumMeta requires special dict handling
# TODO: We should emit a diagnostic here too
ValidExtension = type("ValidExtension", (EmptyEnum,), {})
reveal_type(ValidExtension) # revealed: <class 'ValidExtension'>
# Empty enums fail because EnumMeta requires special dict handling
# error: [invalid-base] "Invalid base for class created via `type()`"
InvalidExtension = type("InvalidExtension", (EmptyEnum,), {})
```

## `__init_subclass__` keyword arguments
Expand Down Expand Up @@ -1046,3 +1069,27 @@ reveal_type(Dynamic) # revealed: <class 'Dynamic'>
# Metaclass attributes are accessible on the class
reveal_type(Dynamic.custom_attr) # revealed: str
```

## `final()` on dynamic classes

Using `final()` as a function (not a decorator) on dynamic classes has no effect. The class is
passed through unchanged:

```py
from typing import final

# TODO: Add a diagnostic for ineffective use of `final()` here.
FinalClass = final(type("FinalClass", (), {}))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we therefore emit a diagnostic here warning the user that this has no effect? I don't like silently doing nothing in a case where the user probably expects us to do something 😄

This can be a followup.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do. I think that should fire for "static" classes too.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, that makes sense.

reveal_type(FinalClass) # revealed: <class 'FinalClass'>

# Subclassing is allowed because `final()` as a function has no effect
class Child(FinalClass): ...

# Same with base classes
class Base: ...

# TODO: Add a diagnostic for ineffective use of `final()` here.
FinalDerived = final(type("FinalDerived", (Base,), {}))

class Child2(FinalDerived): ...
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Unsupported base for dynamic `type()` classes

<!-- snapshot-diagnostics -->

## `@final` class

Classes decorated with `@final` cannot be subclassed:

```py
from typing import final

@final
class FinalClass:
pass

X = type("X", (FinalClass,), {}) # error: [subclass-of-final-class]
```

## `Generic` base

Dynamic classes created via `type()` cannot inherit from `Generic`:

```py
from typing import Generic, TypeVar

T = TypeVar("T")

X = type("X", (Generic[T],), {}) # error: [invalid-base]
```

## `Protocol` base

Dynamic classes created via `type()` cannot inherit from `Protocol`:

```py
from typing import Protocol

X = type("X", (Protocol,), {}) # error: [unsupported-dynamic-base]
```

## `TypedDict` base

Dynamic classes created via `type()` cannot inherit from `TypedDict` directly. Use
`TypedDict("Name", ...)` instead:

```py
from typing_extensions import TypedDict

X = type("X", (TypedDict,), {}) # error: [invalid-base]
```

## Enum base

Dynamic classes created via `type()` cannot inherit from Enum classes because `EnumMeta` expects
special dict attributes that `type()` doesn't provide:

```py
from enum import Enum

class MyEnum(Enum):
pass

X = type("X", (MyEnum,), {}) # error: [invalid-base]
```

## Enum with members

Enums with members are final and cannot be subclassed at all:

```py
from enum import Enum

class Color(Enum):
RED = 1
GREEN = 2

X = type("X", (Color,), {}) # error: [subclass-of-final-class]
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---

---
mdtest name: unsupported_base_dynamic_type.md - Unsupported base for dynamic `type()` classes - Enum base
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_base_dynamic_type.md
---

# Python source files

## mdtest_snippet.py

```
1 | from enum import Enum
2 |
3 | class MyEnum(Enum):
4 | pass
5 |
6 | X = type("X", (MyEnum,), {}) # error: [invalid-base]
```

# Diagnostics

```
error[invalid-base]: Invalid base for class created via `type()`
--> src/mdtest_snippet.py:6:16
|
4 | pass
5 |
6 | X = type("X", (MyEnum,), {}) # error: [invalid-base]
| ^^^^^^ Has type `<class 'MyEnum'>`
|
info: Creating an enum class via `type()` is not supported
info: Consider using `Enum("X", [])` instead
info: rule `invalid-base` is enabled by default

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---

---
mdtest name: unsupported_base_dynamic_type.md - Unsupported base for dynamic `type()` classes - Enum with members
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_base_dynamic_type.md
---

# Python source files

## mdtest_snippet.py

```
1 | from enum import Enum
2 |
3 | class Color(Enum):
4 | RED = 1
5 | GREEN = 2
6 |
7 | X = type("X", (Color,), {}) # error: [subclass-of-final-class]
```

# Diagnostics

```
error[subclass-of-final-class]: Class `X` cannot inherit from final class `Color`
--> src/mdtest_snippet.py:7:16
|
5 | GREEN = 2
6 |
7 | X = type("X", (Color,), {}) # error: [subclass-of-final-class]
| ^^^^^
|
info: rule `subclass-of-final-class` is enabled by default

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---

---
mdtest name: unsupported_base_dynamic_type.md - Unsupported base for dynamic `type()` classes - `@final` class
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_base_dynamic_type.md
---

# Python source files

## mdtest_snippet.py

```
1 | from typing import final
2 |
3 | @final
4 | class FinalClass:
5 | pass
6 |
7 | X = type("X", (FinalClass,), {}) # error: [subclass-of-final-class]
```

# Diagnostics

```
error[subclass-of-final-class]: Class `X` cannot inherit from final class `FinalClass`
--> src/mdtest_snippet.py:7:16
|
5 | pass
6 |
7 | X = type("X", (FinalClass,), {}) # error: [subclass-of-final-class]
| ^^^^^^^^^^
|
info: rule `subclass-of-final-class` is enabled by default

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---

---
mdtest name: unsupported_base_dynamic_type.md - Unsupported base for dynamic `type()` classes - `Generic` base
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_base_dynamic_type.md
---

# Python source files

## mdtest_snippet.py

```
1 | from typing import Generic, TypeVar
2 |
3 | T = TypeVar("T")
4 |
5 | X = type("X", (Generic[T],), {}) # error: [invalid-base]
```

# Diagnostics

```
error[invalid-base]: Invalid base for class created via `type()`
--> src/mdtest_snippet.py:5:16
|
3 | T = TypeVar("T")
4 |
5 | X = type("X", (Generic[T],), {}) # error: [invalid-base]
| ^^^^^^^^^^ Has type `<special-form 'typing.Generic[T]'>`
|
info: Classes created via `type()` cannot be generic
info: Consider using `class X(Generic[...]): ...` instead
info: rule `invalid-base` is enabled by default

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---

---
mdtest name: unsupported_base_dynamic_type.md - Unsupported base for dynamic `type()` classes - `Protocol` base
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unsupported_base_dynamic_type.md
---

# Python source files

## mdtest_snippet.py

```
1 | from typing import Protocol
2 |
3 | X = type("X", (Protocol,), {}) # error: [unsupported-dynamic-base]
```

# Diagnostics

```
info[unsupported-dynamic-base]: Unsupported base for class created via `type()`
--> src/mdtest_snippet.py:3:16
|
1 | from typing import Protocol
2 |
3 | X = type("X", (Protocol,), {}) # error: [unsupported-dynamic-base]
| ^^^^^^^^ Has type `<special-form 'typing.Protocol'>`
|
info: Classes created via `type()` cannot be protocols
info: Consider using `class X(Protocol): ...` instead
info: rule `unsupported-dynamic-base` is enabled by default

```
Loading