diff --git a/mypy/semanal.py b/mypy/semanal.py index 4128369ace5d..285a68c246f4 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -52,7 +52,7 @@ from contextlib import contextmanager from typing import Any, Callable, Collection, Final, Iterable, Iterator, List, TypeVar, cast -from typing_extensions import TypeAlias as _TypeAlias +from typing_extensions import TypeAlias as _TypeAlias, TypeGuard from mypy import errorcodes as codes, message_registry from mypy.constant_fold import constant_fold_expr @@ -3554,7 +3554,14 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool: # unless using PEP 613 `cls: TypeAlias = A` return False - if isinstance(s.rvalue, CallExpr) and s.rvalue.analyzed: + # It can be `A = TypeAliasType('A', ...)` call, in this case, + # we just take the second argument and analyze it: + if self.check_type_alias_type_call(lvalue.name, s.rvalue): + rvalue = s.rvalue.args[1] + else: + rvalue = s.rvalue + + if isinstance(rvalue, CallExpr) and rvalue.analyzed: return False existing = self.current_symbol_table().get(lvalue.name) @@ -3580,7 +3587,7 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool: return False non_global_scope = self.type or self.is_func_scope() - if not pep_613 and isinstance(s.rvalue, RefExpr) and non_global_scope: + if not pep_613 and isinstance(rvalue, RefExpr) and non_global_scope: # Fourth rule (special case): Non-subscripted right hand side creates a variable # at class and function scopes. For example: # @@ -3592,7 +3599,6 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool: # without this rule, this typical use case will require a lot of explicit # annotations (see the second rule). return False - rvalue = s.rvalue if not pep_613 and not self.can_be_type_alias(rvalue): return False @@ -3713,6 +3719,19 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool: self.note("Use variable annotation syntax to define protocol members", s) return True + def check_type_alias_type_call(self, name: str, rvalue: Expression) -> TypeGuard[CallExpr]: + if not isinstance(rvalue, CallExpr): + return False + + names = ["typing_extensions.TypeAliasType"] + if self.options.python_version >= (3, 12): + names.append("typing.TypeAliasType") + if not refers_to_fullname(rvalue.callee, tuple(names)): + return False + + # TODO: we probably need to also analyze `type_params=` usage here. + return self.check_typevarlike_name(rvalue, name, rvalue) + def disable_invalid_recursive_aliases( self, s: AssignmentStmt, current_node: TypeAlias ) -> None: @@ -5131,6 +5150,12 @@ def visit_call_expr(self, expr: CallExpr) -> None: expr.analyzed = OpExpr("divmod", expr.args[0], expr.args[1]) expr.analyzed.line = expr.line expr.analyzed.accept(self) + elif refers_to_fullname( + expr.callee, ("typing.TypeAliasType", "typing_extensions.TypeAliasType") + ): + with self.allow_unbound_tvars_set(): + for a in expr.args: + a.accept(self) else: # Normal call expression. for a in expr.args: diff --git a/test-data/unit/check-type-aliases.test b/test-data/unit/check-type-aliases.test index 4364a9bfa9dc..0085e8444a07 100644 --- a/test-data/unit/check-type-aliases.test +++ b/test-data/unit/check-type-aliases.test @@ -1066,3 +1066,51 @@ def eval(e: Expr) -> int: elif e[0] == 456: return -eval(e[1]) [builtins fixtures/dict-full.pyi] + +[case testTypeAliasType] +from typing import Union +from typing_extensions import TypeAliasType + +TestType = TypeAliasType("TestType", Union[int, str]) +x: TestType = 42 +y: TestType = 'a' +z: TestType = object() # E: Incompatible types in assignment (expression has type "object", variable has type "Union[int, str]") +[builtins fixtures/tuple.pyi] + +[case testTypeAliasTypeInvalid] +from typing_extensions import TypeAliasType + +TestType = TypeAliasType("T", int) # E: String argument 1 "T" to TypeAliasType(...) does not match variable name "TestType" + +T1 = T2 = TypeAliasType("T", int) +t1: T1 # E: Variable "__main__.T1" is not valid as a type \ + # N: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases + +T3 = TypeAliasType("T3", -1) +t3: T3 # E: Variable "__main__.T3" is not valid as a type \ + # N: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases +[builtins fixtures/tuple.pyi] + +[case testTypeAliasTypeGeneric] +from typing_extensions import TypeAliasType +from typing import Dict, TypeVar + +K = TypeVar('K') +V = TypeVar('V') + +TestType = TypeAliasType("TestType", Dict[K, V], type_params=(K, V)) +x: TestType[int, str] = {1: 'a'} +y: TestType[str, int] = {'a': 1} +z: TestType[str, int] = {1: 'a'} # E: Dict entry 0 has incompatible type "int": "str"; expected "str": "int" +[builtins fixtures/dict.pyi] + +[case testTypeAliasType312] +# flags: --python-version 3.12 +from typing import Union, TypeAliasType + +TestType = TypeAliasType("TestType", int | str) +x: TestType = 42 +y: TestType = 'a' +z: TestType = object() # E: Incompatible types in assignment (expression has type "object", variable has type "Union[int, str]") +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] diff --git a/test-data/unit/fixtures/typing-full.pyi b/test-data/unit/fixtures/typing-full.pyi index ca8a2413f05f..f7da75fa4cd0 100644 --- a/test-data/unit/fixtures/typing-full.pyi +++ b/test-data/unit/fixtures/typing-full.pyi @@ -10,13 +10,17 @@ from abc import abstractmethod, ABCMeta class GenericMeta(type): pass +class _SpecialForm: ... +class TypeVar: ... +class ParamSpec: ... +class TypeVarTuple: ... + def cast(t, o): ... def assert_type(o, t): ... overload = 0 Any = 0 Union = 0 Optional = 0 -TypeVar = 0 Generic = 0 Protocol = 0 Tuple = 0 @@ -39,6 +43,8 @@ U = TypeVar('U') V = TypeVar('V') S = TypeVar('S') +def final(x: T) -> T: ... + class NamedTuple(tuple[Any, ...]): ... # Note: definitions below are different from typeshed, variances are declared @@ -182,8 +188,6 @@ class _TypedDict(Mapping[str, object]): def update(self: T, __m: T) -> None: ... def __delitem__(self, k: NoReturn) -> None: ... -class _SpecialForm: pass - def dataclass_transform( *, eq_default: bool = ..., @@ -199,3 +203,10 @@ def reveal_type(__obj: T) -> T: ... # Only exists in type checking time: def type_check_only(__func_or_class: T) -> T: ... + +# Was added in 3.12 +@final +class TypeAliasType: + def __init__( + self, name: str, value: Any, *, type_params: Tuple[Union[TypeVar, ParamSpec, TypeVarTuple], ...] = () + ) -> None: ... diff --git a/test-data/unit/lib-stub/typing_extensions.pyi b/test-data/unit/lib-stub/typing_extensions.pyi index 7aca6fad1b42..631351f0e15b 100644 --- a/test-data/unit/lib-stub/typing_extensions.pyi +++ b/test-data/unit/lib-stub/typing_extensions.pyi @@ -1,5 +1,5 @@ import typing -from typing import Any, Callable, Mapping, Iterable, Iterator, NoReturn as NoReturn, Dict, Tuple, Type +from typing import Any, Callable, Mapping, Iterable, Iterator, NoReturn as NoReturn, Dict, Tuple, Type, Union from typing import TYPE_CHECKING as TYPE_CHECKING from typing import NewType as NewType, overload as overload @@ -39,6 +39,12 @@ Never: _SpecialForm TypeVarTuple: _SpecialForm Unpack: _SpecialForm +@final +class TypeAliasType: + def __init__( + self, name: str, value: Any, *, type_params: Tuple[Union[TypeVar, ParamSpec, TypeVarTuple], ...] = () + ) -> None: ... + # Fallback type for all typed dicts (does not exist at runtime). class _TypedDict(Mapping[str, object]): # Needed to make this class non-abstract. It is explicitly declared abstract in