-
Notifications
You must be signed in to change notification settings - Fork 1.8k
[ty] Emit diagnostics for invalid base classes in type(...)
#22499
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
||
|
|
@@ -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 | ||
|
|
||
|
|
@@ -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 | ||
|
|
@@ -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", (), {})) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will do. I think that should fire for "static" classes too.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
|
||
| ``` |
Uh oh!
There was an error while loading. Please reload this page.