diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md
index d1173e0013..4f79aafd9f 100644
--- a/crates/ty/docs/rules.md
+++ b/crates/ty/docs/rules.md
@@ -8,7 +8,7 @@
Default level: warn ·
Added in 0.0.1-alpha.20 ·
Related issues ·
-View source
+View source
@@ -80,7 +80,7 @@ def test(): -> "int":
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -104,7 +104,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
Default level: error ·
Added in 0.0.7 ·
Related issues ·
-View source
+View source
@@ -135,7 +135,7 @@ def f(x: object):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -167,7 +167,7 @@ f(int) # error
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -198,7 +198,7 @@ a = 1
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -230,7 +230,7 @@ class C(A, B): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -262,7 +262,7 @@ class B(A): ...
Default level: error ·
Preview (since 1.0.0) ·
Related issues ·
-View source
+View source
@@ -290,7 +290,7 @@ type B = A
Default level: warn ·
Added in 0.0.1-alpha.16 ·
Related issues ·
-View source
+View source
@@ -317,7 +317,7 @@ old_func() # emits [deprecated] diagnostic
Default level: ignore ·
Preview (since 0.0.1-alpha.1) ·
Related issues ·
-View source
+View source
@@ -346,7 +346,7 @@ false positives it can produce.
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -373,7 +373,7 @@ class B(A, A): ...
Default level: error ·
Added in 0.0.1-alpha.12 ·
Related issues ·
-View source
+View source
@@ -529,7 +529,7 @@ def test(): -> "Literal[5]":
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -559,7 +559,7 @@ class C(A, B): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -585,7 +585,7 @@ t[3] # IndexError: tuple index out of range
Default level: error ·
Added in 0.0.1-alpha.12 ·
Related issues ·
-View source
+View source
@@ -674,7 +674,7 @@ an atypical memory layout.
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -701,7 +701,7 @@ func("foo") # error: [invalid-argument-type]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -729,7 +729,7 @@ a: int = ''
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -763,7 +763,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable
Default level: error ·
Added in 0.0.1-alpha.19 ·
Related issues ·
-View source
+View source
@@ -799,7 +799,7 @@ asyncio.run(main())
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -823,7 +823,7 @@ class A(42): ... # error: [invalid-base]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -850,7 +850,7 @@ with 1:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -879,7 +879,7 @@ a: str
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -923,7 +923,7 @@ except ZeroDivisionError:
Default level: error ·
Added in 0.0.1-alpha.28 ·
Related issues ·
-View source
+View source
@@ -965,7 +965,7 @@ class D(A):
Default level: error ·
Added in 0.0.1-alpha.35 ·
Related issues ·
-View source
+View source
@@ -1009,7 +1009,7 @@ class NonFrozenChild(FrozenBase): # Error raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1041,6 +1041,55 @@ class D(Generic[U, T]): ...
- [Typing spec: Generics](https://typing.python.org/en/latest/spec/generics.html#introduction)
+## `invalid-generic-enum`
+
+
+Default level: error ·
+Added in 0.0.12 ·
+Related issues ·
+View source
+
+
+
+**What it does**
+
+Checks for enum classes that are also generic.
+
+**Why is this bad?**
+
+Enum classes cannot be generic. Python does not support generic enums:
+attempting to create one will either result in an immediate `TypeError`
+at runtime, or will create a class that cannot be specialized in the way
+that a normal generic class can.
+
+**Examples**
+
+```python
+from enum import Enum
+from typing import Generic, TypeVar
+
+T = TypeVar("T")
+
+# error: enum class cannot be generic (class creation fails with `TypeError`)
+class E[T](Enum):
+ A = 1
+
+# error: enum class cannot be generic (class creation fails with `TypeError`)
+class F(Enum, Generic[T]):
+ A = 1
+
+# error: enum class cannot be generic -- the class creation does not immediately fail...
+class G(Generic[T], Enum):
+ A = 1
+
+# ...but this raises `KeyError`:
+x: G[int]
+```
+
+**References**
+
+- [Python documentation: Enum](https://docs.python.org/3/library/enum.html)
+
## `invalid-ignore-comment`
@@ -1077,7 +1126,7 @@ a = 20 / 0 # type: ignore
Default level: error ·
Added in 0.0.1-alpha.17 ·
Related issues ·
-View source
+View source
@@ -1116,7 +1165,7 @@ carol = Person(name="Carol", age=25) # typo!
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1151,7 +1200,7 @@ def f(t: TypeVar("U")): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1185,7 +1234,7 @@ class B(metaclass=f): ...
Default level: error ·
Added in 0.0.1-alpha.20 ·
Related issues ·
-View source
+View source
@@ -1292,7 +1341,7 @@ Correct use of `@override` is enforced by ty's `invalid-explicit-override` rule.
Default level: error ·
Added in 0.0.1-alpha.19 ·
Related issues ·
-View source
+View source
@@ -1346,7 +1395,7 @@ AttributeError: Cannot overwrite NamedTuple attribute _asdict
Default level: error ·
Preview (since 1.0.0) ·
Related issues ·
-View source
+View source
@@ -1376,7 +1425,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType`
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1426,7 +1475,7 @@ def foo(x: int) -> int: ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1452,7 +1501,7 @@ def f(a: int = ''): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1483,7 +1532,7 @@ P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assig
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1517,7 +1566,7 @@ TypeError: Protocols can only inherit from other protocols, got
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1566,7 +1615,7 @@ def g():
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1591,7 +1640,7 @@ def func() -> int:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1687,7 +1736,7 @@ class C: ...
Default level: error ·
Added in 0.0.10 ·
Related issues ·
-View source
+View source
@@ -1733,7 +1782,7 @@ class MyClass:
Default level: error ·
Added in 0.0.1-alpha.6 ·
Related issues ·
-View source
+View source
@@ -1760,7 +1809,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
Default level: error ·
Added in 0.0.1-alpha.29 ·
Related issues ·
-View source
+View source
@@ -1807,7 +1856,7 @@ Bar[int] # error: too few arguments
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1837,7 +1886,7 @@ TYPE_CHECKING = ''
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1867,7 +1916,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
Default level: error ·
Added in 0.0.1-alpha.11 ·
Related issues ·
-View source
+View source
@@ -1901,7 +1950,7 @@ f(10) # Error
Default level: error ·
Added in 0.0.1-alpha.11 ·
Related issues ·
-View source
+View source
@@ -1935,7 +1984,7 @@ class C:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1970,7 +2019,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
Default level: error ·
Added in 0.0.9 ·
Related issues ·
-View source
+View source
@@ -2001,7 +2050,7 @@ class Foo(TypedDict):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2026,7 +2075,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
Default level: error ·
Added in 0.0.1-alpha.20 ·
Related issues ·
-View source
+View source
@@ -2059,7 +2108,7 @@ alice["age"] # KeyError
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2088,7 +2137,7 @@ func("string") # error: [no-matching-overload]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2114,7 +2163,7 @@ for i in 34: # TypeError: 'int' object is not iterable
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2138,7 +2187,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
Default level: error ·
Added in 0.0.1-alpha.29 ·
Related issues ·
-View source
+View source
@@ -2171,7 +2220,7 @@ class B(A):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2198,7 +2247,7 @@ f(1, x=2) # Error raised here
Default level: error ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2225,7 +2274,7 @@ f(x=1) # Error raised here
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2253,7 +2302,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2285,7 +2334,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
Default level: ignore ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2322,7 +2371,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
Default level: ignore ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2386,7 +2435,7 @@ def test(): -> "int":
Default level: warn ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2413,7 +2462,7 @@ cast(int, f()) # Redundant
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2443,7 +2492,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2472,7 +2521,7 @@ class B(A): ... # Error raised here
Default level: error ·
Preview (since 0.0.1-alpha.30) ·
Related issues ·
-View source
+View source
@@ -2506,7 +2555,7 @@ class F(NamedTuple):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2533,7 +2582,7 @@ f("foo") # Error raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2561,7 +2610,7 @@ def _(x: int):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2607,7 +2656,7 @@ class A:
Default level: warn ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2631,7 +2680,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2658,7 +2707,7 @@ f(x=1, y=2) # Error raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2686,7 +2735,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
Default level: warn ·
Added in 0.0.1-alpha.15 ·
Related issues ·
-View source
+View source
@@ -2744,7 +2793,7 @@ def g():
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2769,7 +2818,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2794,7 +2843,7 @@ print(x) # NameError: name 'x' is not defined
Default level: warn ·
Added in 0.0.1-alpha.7 ·
Related issues ·
-View source
+View source
@@ -2833,7 +2882,7 @@ class D(C): ... # error: [unsupported-base]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2870,7 +2919,7 @@ b1 < b2 < b1 # exception raised here
Default level: ignore ·
Preview (since 1.0.0) ·
Related issues ·
-View source
+View source
@@ -2911,7 +2960,7 @@ def factory(base: type[Base]) -> type:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2975,7 +3024,7 @@ to `false` to prevent this rule from reporting unused `type: ignore` comments.
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -3038,7 +3087,7 @@ def foo(x: int | str) -> int | str:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
diff --git a/crates/ty_python_semantic/resources/mdtest/enums.md b/crates/ty_python_semantic/resources/mdtest/enums.md
index 8809f41350..c3b1e55c53 100644
--- a/crates/ty_python_semantic/resources/mdtest/enums.md
+++ b/crates/ty_python_semantic/resources/mdtest/enums.md
@@ -1016,6 +1016,108 @@ class Color(Enum):
reveal_type(Color.RED != Color.RED) # revealed: bool
```
+## Generic enums are invalid
+
+Enum classes cannot be generic. Python does not support generic enums, and attempting to create one
+will result in a `TypeError` at runtime.
+
+### PEP 695 syntax
+
+Using PEP 695 type parameters on an enum is invalid:
+
+```toml
+[environment]
+python-version = "3.12"
+```
+
+```py
+from enum import Enum
+
+# error: [invalid-generic-enum] "Enum class `E` cannot be generic"
+class E[T](Enum):
+ A = 1
+ B = 2
+```
+
+### Legacy `Generic` base class
+
+Inheriting from both `Enum` and `Generic[T]` is also invalid:
+
+```py
+from enum import Enum
+from typing import Generic, TypeVar
+
+T = TypeVar("T")
+
+# error: [invalid-generic-enum] "Enum class `F` cannot be generic"
+class F(Enum, Generic[T]):
+ A = 1
+ B = 2
+```
+
+### Swapped order (`Generic` first)
+
+The order of bases doesn't matter; it's still invalid:
+
+```py
+from enum import Enum
+from typing import Generic, TypeVar
+
+T = TypeVar("T")
+
+# error: [invalid-generic-enum] "Enum class `G` cannot be generic"
+class G(Generic[T], Enum):
+ A = 1
+ B = 2
+```
+
+### Enum subclasses
+
+Subclasses of enum base classes also cannot be generic:
+
+```toml
+[environment]
+python-version = "3.12"
+```
+
+```py
+from enum import Enum, IntEnum
+from typing import Generic, TypeVar
+
+T = TypeVar("T")
+
+# error: [invalid-generic-enum] "Enum class `MyIntEnum` cannot be generic"
+class MyIntEnum[T](IntEnum):
+ A = 1
+
+# error: [invalid-generic-enum] "Enum class `MyFlagEnum` cannot be generic"
+class MyFlagEnum(IntEnum, Generic[T]):
+ A = 1
+```
+
+### Custom enum base class
+
+Even with custom enum subclasses that don't have members, they cannot be made generic:
+
+```toml
+[environment]
+python-version = "3.12"
+```
+
+```py
+from enum import Enum
+from typing import Generic, TypeVar
+
+T = TypeVar("T")
+
+class MyEnumBase(Enum):
+ def some_method(self) -> None: ...
+
+# error: [invalid-generic-enum] "Enum class `MyEnum` cannot be generic"
+class MyEnum[T](MyEnumBase):
+ A = 1
+```
+
## References
- Typing spec:
diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs
index 36302200bf..e1bed92bc3 100644
--- a/crates/ty_python_semantic/src/types/diagnostic.rs
+++ b/crates/ty_python_semantic/src/types/diagnostic.rs
@@ -73,6 +73,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&INVALID_CONTEXT_MANAGER);
registry.register_lint(&INVALID_DECLARATION);
registry.register_lint(&INVALID_EXCEPTION_CAUGHT);
+ registry.register_lint(&INVALID_GENERIC_ENUM);
registry.register_lint(&INVALID_GENERIC_CLASS);
registry.register_lint(&INVALID_LEGACY_TYPE_VARIABLE);
registry.register_lint(&INVALID_PARAMSPEC);
@@ -956,6 +957,48 @@ declare_lint! {
}
}
+declare_lint! {
+ /// ## What it does
+ /// Checks for enum classes that are also generic.
+ ///
+ /// ## Why is this bad?
+ /// Enum classes cannot be generic. Python does not support generic enums:
+ /// attempting to create one will either result in an immediate `TypeError`
+ /// at runtime, or will create a class that cannot be specialized in the way
+ /// that a normal generic class can.
+ ///
+ /// ## Examples
+ /// ```python
+ /// from enum import Enum
+ /// from typing import Generic, TypeVar
+ ///
+ /// T = TypeVar("T")
+ ///
+ /// # error: enum class cannot be generic (class creation fails with `TypeError`)
+ /// class E[T](Enum):
+ /// A = 1
+ ///
+ /// # error: enum class cannot be generic (class creation fails with `TypeError`)
+ /// class F(Enum, Generic[T]):
+ /// A = 1
+ ///
+ /// # error: enum class cannot be generic -- the class creation does not immediately fail...
+ /// class G(Generic[T], Enum):
+ /// A = 1
+ ///
+ /// # ...but this raises `KeyError`:
+ /// x: G[int]
+ /// ```
+ ///
+ /// ## References
+ /// - [Python documentation: Enum](https://docs.python.org/3/library/enum.html)
+ pub(crate) static INVALID_GENERIC_ENUM = {
+ summary: "detects generic enum classes",
+ status: LintStatus::stable("0.0.12"),
+ default_level: Level::Error,
+ }
+}
+
declare_lint! {
/// ## What it does
/// Checks for the creation of invalid generic classes
diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs
index 1d2300fb73..214584521c 100644
--- a/crates/ty_python_semantic/src/types/infer/builder.rs
+++ b/crates/ty_python_semantic/src/types/infer/builder.rs
@@ -65,15 +65,15 @@ use crate::types::diagnostic::{
CYCLIC_CLASS_DEFINITION, CYCLIC_TYPE_ALIAS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_BASE,
DUPLICATE_KW_ONLY, INCONSISTENT_MRO, INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT,
INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, INVALID_DECLARATION, INVALID_GENERIC_CLASS,
- INVALID_KEY, INVALID_LEGACY_TYPE_VARIABLE, INVALID_METACLASS, INVALID_NAMED_TUPLE,
- INVALID_NEWTYPE, INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT, INVALID_PARAMSPEC,
- INVALID_PROTOCOL, INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL,
- INVALID_TYPE_VARIABLE_CONSTRAINTS, INVALID_TYPED_DICT_STATEMENT, IncompatibleBases,
- NOT_SUBSCRIPTABLE, POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL,
- POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS, TypedDictDeleteErrorKind, UNDEFINED_REVEAL,
- UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE,
- UNSUPPORTED_DYNAMIC_BASE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY,
- hint_if_stdlib_attribute_exists_on_other_versions,
+ INVALID_GENERIC_ENUM, INVALID_KEY, INVALID_LEGACY_TYPE_VARIABLE, INVALID_METACLASS,
+ INVALID_NAMED_TUPLE, INVALID_NEWTYPE, INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT,
+ INVALID_PARAMSPEC, INVALID_PROTOCOL, INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM,
+ INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_CONSTRAINTS, INVALID_TYPED_DICT_STATEMENT,
+ IncompatibleBases, NOT_SUBSCRIPTABLE, POSSIBLY_MISSING_ATTRIBUTE,
+ POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS,
+ TypedDictDeleteErrorKind, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL,
+ UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_DYNAMIC_BASE, UNSUPPORTED_OPERATOR,
+ USELESS_OVERLOAD_BODY, hint_if_stdlib_attribute_exists_on_other_versions,
hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation,
report_bad_dunder_set_call, report_bad_frozen_dataclass_inheritance,
report_cannot_delete_typed_dict_key, report_cannot_pop_required_field_on_typed_dict,
@@ -92,6 +92,7 @@ use crate::types::diagnostic::{
report_rebound_typevar, report_slice_step_size_zero, report_unsupported_augmented_assignment,
report_unsupported_binary_operation, report_unsupported_comparison,
};
+use crate::types::enums::is_enum_class_by_inheritance;
use crate::types::function::{
FunctionDecorators, FunctionLiteral, FunctionType, KnownFunction, OverloadLiteral,
is_implicit_classmethod, is_implicit_staticmethod,
@@ -636,10 +637,22 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
continue;
}
+ // (2) Check that the class is not an enum and generic
+ if is_enum_class_by_inheritance(self.db(), class)
+ && class.generic_context(self.db()).is_some()
+ {
+ if let Some(builder) = self.context.report_lint(&INVALID_GENERIC_ENUM, class_node) {
+ builder.into_diagnostic(format_args!(
+ "Enum class `{}` cannot be generic",
+ class.name(self.db())
+ ));
+ }
+ }
+
let is_named_tuple =
CodeGeneratorKind::NamedTuple.matches(self.db(), class.into(), None);
- // (2) If it's a `NamedTuple` class, check that no field without a default value
+ // (3) If it's a `NamedTuple` class, check that no field without a default value
// appears after a field with a default value.
if is_named_tuple {
let mut field_with_default_encountered = None;
@@ -680,7 +693,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let mut disjoint_bases = IncompatibleBases::default();
- // (3) Iterate through the class's explicit bases to check for various possible errors:
+ // (4) Iterate through the class's explicit bases to check for various possible errors:
// - Check for inheritance from plain `Generic`,
// - Check for inheritance from a `@final` classes
// - If the class is a protocol class: check for inheritance from a non-protocol class
@@ -794,7 +807,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
- // (4) Check that the class's MRO is resolvable
+ // (5) Check that the class's MRO is resolvable
match class.try_mro(self.db(), None) {
Err(mro_error) => match mro_error.reason() {
StaticMroErrorKind::DuplicateBases(duplicates) => {
@@ -865,7 +878,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
- // (5) Check that @total_ordering has a valid ordering method in the MRO
+ // (6) Check that @total_ordering has a valid ordering method in the MRO
if class.total_ordering(self.db()) && !class.has_ordering_method_in_mro(self.db(), None)
{
// Find the @total_ordering decorator to report the diagnostic at its location
@@ -884,7 +897,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
- // (6) Check that the class's metaclass can be determined without error.
+ // (7) Check that the class's metaclass can be determined without error.
if let Err(metaclass_error) = class.try_metaclass(self.db()) {
match metaclass_error.reason() {
MetaclassErrorKind::Cycle => {
@@ -960,7 +973,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
- // (7) Check that the class arguments matches the arguments of the
+ // (8) Check that the class arguments matches the arguments of the
// base class `__init_subclass__` method.
if let Some(args) = class_node.arguments.as_deref() {
let call_args: CallArguments = args
@@ -1000,7 +1013,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
- // (8) If the class is generic, verify that its generic context does not violate any of
+ // (9) If the class is generic, verify that its generic context does not violate any of
// the typevar scoping rules.
if let (Some(legacy), Some(inherited)) = (
class.legacy_generic_context(self.db()),
@@ -1079,7 +1092,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
- // (9) Check that a dataclass does not have more than one `KW_ONLY`.
+ // (10) Check that a dataclass does not have more than one `KW_ONLY`.
if let Some(field_policy @ CodeGeneratorKind::DataclassLike(_)) =
CodeGeneratorKind::from_class(self.db(), class.into(), None)
{
@@ -1114,7 +1127,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
- // (10) Check for violations of the Liskov Substitution Principle,
+ // (11) Check for violations of the Liskov Substitution Principle,
// and for violations of other rules relating to invalid overrides of some sort.
overrides::check_class(&self.context, class);
@@ -1122,7 +1135,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
protocol.validate_members(&self.context);
}
- // (11) If it's a `TypedDict` class, check that it doesn't include any invalid
+ // (12) If it's a `TypedDict` class, check that it doesn't include any invalid
// statements: https://typing.python.org/en/latest/spec/typeddict.html#class-based-syntax
//
// The body of the class definition defines the items of the `TypedDict` type. It
diff --git a/ty.schema.json b/ty.schema.json
index acdf95b7ef..969aeb31a9 100644
--- a/ty.schema.json
+++ b/ty.schema.json
@@ -640,6 +640,16 @@
}
]
},
+ "invalid-generic-enum": {
+ "title": "detects generic enum classes",
+ "description": "## What it does\nChecks for enum classes that are also generic.\n\n## Why is this bad?\nEnum classes cannot be generic. Python does not support generic enums:\nattempting to create one will either result in an immediate `TypeError`\nat runtime, or will create a class that cannot be specialized in the way\nthat a normal generic class can.\n\n## Examples\n```python\nfrom enum import Enum\nfrom typing import Generic, TypeVar\n\nT = TypeVar(\"T\")\n\n# error: enum class cannot be generic (class creation fails with `TypeError`)\nclass E[T](Enum):\n A = 1\n\n# error: enum class cannot be generic (class creation fails with `TypeError`)\nclass F(Enum, Generic[T]):\n A = 1\n\n# error: enum class cannot be generic -- the class creation does not immediately fail...\nclass G(Generic[T], Enum):\n A = 1\n\n# ...but this raises `KeyError`:\nx: G[int]\n```\n\n## References\n- [Python documentation: Enum](https://docs.python.org/3/library/enum.html)",
+ "default": "error",
+ "oneOf": [
+ {
+ "$ref": "#/definitions/Level"
+ }
+ ]
+ },
"invalid-ignore-comment": {
"title": "detects ignore comments that use invalid syntax",
"description": "## What it does\nChecks for `type: ignore` and `ty: ignore` comments that are syntactically incorrect.\n\n## Why is this bad?\nA syntactically incorrect ignore comment is probably a mistake and is useless.\n\n## Examples\n```py\na = 20 / 0 # type: ignoree\n```\n\nUse instead:\n\n```py\na = 20 / 0 # type: ignore\n```",