Skip to content

Commit

Permalink
Support TypeAliasType explicit call
Browse files Browse the repository at this point in the history
  • Loading branch information
sobolevn committed Dec 10, 2023
1 parent 0567da9 commit f9b3e71
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 8 deletions.
33 changes: 29 additions & 4 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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:
#
Expand All @@ -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

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
48 changes: 48 additions & 0 deletions test-data/unit/check-type-aliases.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]
17 changes: 14 additions & 3 deletions test-data/unit/fixtures/typing-full.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 = ...,
Expand All @@ -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: ...
8 changes: 7 additions & 1 deletion test-data/unit/lib-stub/typing_extensions.pyi
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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
Expand Down

0 comments on commit f9b3e71

Please sign in to comment.