diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 8d500c54364a..f64164c14905 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -1007,17 +1007,19 @@ def analyze_unbound_type_without_type_info( # context. This is slightly problematic as it allows using the type 'Any' # as a base class -- however, this will fail soon at runtime so the problem # is pretty minor. + # + # Also allow indexing a tuple-typed value that yields Any (or type / Type[Any]) + # in base class position, e.g. class C(args[1]) where args: tuple[int, Any]. if isinstance(sym.node, Var): typ = get_proper_type(sym.node.type) - if isinstance(typ, AnyType): - return AnyType( - TypeOfAny.from_unimported_type, missing_import_name=typ.missing_import_name - ) - elif self.allow_type_any: - if isinstance(typ, Instance) and typ.type.fullname == "builtins.type": - return AnyType(TypeOfAny.special_form) - if isinstance(typ, TypeType) and isinstance(typ.item, AnyType): - return AnyType(TypeOfAny.from_another_any, source_any=typ.item) + runtime_type = self.anal_type_from_runtime_value(typ) + if runtime_type is not None: + return runtime_type + if self.allow_type_any and t.args: + item = self.indexed_tuple_item_type(typ, t.args) + runtime_type = self.anal_type_from_runtime_value(item) + if runtime_type is not None: + return runtime_type # Option 2: # Unbound type variable. Currently these may be still valid, # for example when defining a generic type alias. @@ -1121,6 +1123,49 @@ def analyze_unbound_type_without_type_info( # see https://github.com/python/mypy/issues/4987. return t + def anal_type_from_runtime_value(self, typ: ProperType | None) -> Type | None: + """Return a type if a runtime value of this type may be used as a type. + + This is used for dynamic base classes: a variable of type Any, type, or + Type[Any] is a valid base. Returns None if the value is not valid as a type. + """ + if isinstance(typ, AnyType): + return AnyType( + TypeOfAny.from_unimported_type, missing_import_name=typ.missing_import_name + ) + if self.allow_type_any: + if isinstance(typ, Instance) and typ.type.fullname == "builtins.type": + return AnyType(TypeOfAny.special_form) + if isinstance(typ, TypeType) and isinstance(typ.item, AnyType): + return AnyType(TypeOfAny.from_another_any, source_any=typ.item) + return None + + def indexed_tuple_item_type( + self, typ: ProperType | None, args: tuple[Type, ...] + ) -> ProperType | None: + """If this looks like indexing a tuple-typed value, return the item type.""" + if typ is None or len(args) != 1: + return None + if isinstance(typ, Instance) and typ.type.fullname in TUPLE_NAMES and typ.args: + # Homogeneous tuple[T, ...]: any index has type T. + return get_proper_type(typ.args[0]) + if isinstance(typ, TupleType): + items = flatten_nested_tuples(typ.items) + if find_unpack_in_list(items) is not None: + return None + index = args[0] + if not isinstance(index, RawExpressionType) or not isinstance( + index.literal_value, int + ): + return None + i = index.literal_value + n = len(items) + if i < 0: + i += n + if 0 <= i < n: + return get_proper_type(items[i]) + return None + def visit_any(self, t: AnyType) -> Type: return t diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index dc5d74abc07d..98771ec77790 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -9727,3 +9727,88 @@ def f() -> None: class X: ... undefined # E: Name "undefined" is not defined + +[case testSubclassAnyFromIndexedTuple] +# https://github.com/python/mypy/issues/21998 +from typing import Any, Tuple + +def from_tuple(t: Tuple[int, Any]) -> None: + class Sub(t[1]): + pass + +def from_star_any(*args: Any) -> None: + class Sub(args[0]): + pass + +def from_any_param(base: Any) -> None: + class Sub(base): + pass + +def from_annotated_index(*args: Any) -> None: + x: Any = args[0] + class Sub(x): + pass +[builtins fixtures/tuple.pyi] + +[case testSubclassAnyFromUnpackedStarArgs] +# https://github.com/python/mypy/issues/21998 +# flags: --python-version 3.12 +from typing import Any + +def from_unpacked(*args: *tuple[int, Any]) -> None: + class Sub(args[1]): + pass + +def from_unpacked_type_annotation(*args: *tuple[int, Any]) -> None: + x: type = args[1] + class Sub(x): + pass +[builtins fixtures/tuple.pyi] + +[case testSubclassAnyFromNestedUnpackedStarArgs] +# https://github.com/python/mypy/issues/21998 +# flags: --python-version 3.12 +from typing import Any + +def from_unpacked(*args: *tuple[int, *tuple[Any]]) -> None: + reveal_type(args) # N: Revealed type is "tuple[builtins.int, Any]" + class Sub(args[1]): + pass + +def from_unpacked_type_annotation(*args: *tuple[int, *tuple[Any]]) -> None: + reveal_type(args) # N: Revealed type is "tuple[builtins.int, Any]" + x: type = args[1] + class Sub(x): + pass +[builtins fixtures/tuple.pyi] + +[case testSubclassIndexedNonAnyStillInvalid] +# https://github.com/python/mypy/issues/21998 +from typing import Any, Tuple + +def from_str_tuple(t: Tuple[int, str]) -> None: + class Sub(t[1]): pass # E: Variable "t" is not valid as a type \ + # N: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases \ + # E: Invalid base class "t" \ + # E: Invalid type: try using Literal[1] instead? + +def from_int_index(t: Tuple[int, Any]) -> None: + class Sub(t[0]): pass # E: Variable "t" is not valid as a type \ + # N: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases \ + # E: Invalid base class "t" \ + # E: Invalid type: try using Literal[0] instead? + +def from_int_star(*args: int) -> None: + class Sub(args[0]): pass # E: Variable "args" is not valid as a type \ + # N: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases \ + # E: Invalid base class "args" \ + # E: Invalid type: try using Literal[0] instead? + +def unannotated_local(base: Any) -> None: + # Class bases are resolved before type inference, so an unannotated + # local still needs an explicit Any/type annotation. + x = base + class Sub(x): pass # E: Variable "x" is not valid as a type \ + # N: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases \ + # E: Invalid base class "x" +[builtins fixtures/tuple.pyi]