Skip to content

Commit ba88339

Browse files
committed
Support typing.TypeAlias.
I noticed that we seem to be holding up usage of TypeAlias in typeshed: python/typeshed#7493 (comment). TypeAlias is relatively simple, so I went ahead and added support for it in both .py and .pyi files. Resolves #787. PiperOrigin-RevId: 435095246
1 parent fe79105 commit ba88339

File tree

5 files changed

+66
-16
lines changed

5 files changed

+66
-16
lines changed

pytype/annotation_utils.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,8 @@ def apply_annotation(self, node, op, name, value):
334334
# Experimental "inferred type": see b/213607272.
335335
return AnnotatedValue(None, value)
336336
frame = self.ctx.vm.frame
337-
with self.ctx.vm.generate_late_annotations(self.ctx.vm.simple_stack()):
337+
stack = self.ctx.vm.simple_stack()
338+
with self.ctx.vm.generate_late_annotations(stack):
338339
var, errorlog = abstract_utils.eval_expr(self.ctx, node, frame.f_globals,
339340
frame.f_locals, annot)
340341
if errorlog:
@@ -350,6 +351,10 @@ def apply_annotation(self, node, op, name, value):
350351
# We do not want to instantiate a TypedDict annotation if we have a
351352
# concrete value.
352353
return AnnotatedValue(typ, value)
354+
elif typ.full_name == "typing.TypeAlias":
355+
# Validate that 'value' is a legal type alias.
356+
self.extract_annotation(node, value, name, stack)
357+
return AnnotatedValue(None, value)
353358
else:
354359
return AnnotatedValue(typ, annot_val)
355360

pytype/overlays/typing_extensions_overlay.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ def __init__(self, ctx):
1616
"Protocol": _build("typing.Protocol"),
1717
"runtime": _build("typing.runtime_checkable"),
1818
"SupportsIndex": _build("typing_extensions.SupportsIndex", ast),
19+
"TypeAlias": _build("typing.TypeAlias"),
1920
"TypedDict": typing_overlay.typing_overlay["TypedDict"],
2021
}
2122
for pyval in ast.aliases + ast.classes + ast.constants + ast.functions:

pytype/pyi/parser.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
_ANNOTATED_IDS = (
4545
"Annotated", "typing.Annotated", "typing_extensions.Annotated")
4646
_FINAL_IDS = ("typing.Final", "typing_extensions.Final")
47+
_TYPE_ALIAS_IDS = ("typing.TypeAlias", "typing_extensions.TypeAlias")
4748

4849
#------------------------------------------------------
4950
# imports
@@ -414,18 +415,22 @@ def visit_AnnAssign(self, node):
414415
typ = node.annotation
415416
val = self.convert_node(node.value)
416417
msg = f"Default value for {name}: {typ.name} can only be '...', got {val}"
417-
if typ.name and pytd_utils.MatchesFullName(typ, _FINAL_IDS):
418-
if isinstance(node.value, types.Pyval):
419-
# to_pytd_literal raises an exception if the value is a float, but
420-
# checking upfront allows us to generate a nicer error message.
421-
if isinstance(node.value.value, float):
422-
msg = (f"Default value for {name}: Final can only be '...' or a "
423-
f"legal Literal parameter, got {val}")
424-
else:
425-
typ = node.value.to_pytd_literal()
418+
if typ.name:
419+
if pytd_utils.MatchesFullName(typ, _FINAL_IDS):
420+
if isinstance(node.value, types.Pyval):
421+
# to_pytd_literal raises an exception if the value is a float, but
422+
# checking upfront allows us to generate a nicer error message.
423+
if isinstance(node.value.value, float):
424+
msg = (f"Default value for {name}: Final can only be '...' or a "
425+
f"legal Literal parameter, got {val}")
426+
else:
427+
typ = node.value.to_pytd_literal()
428+
val = pytd.AnythingType()
429+
elif isinstance(val, pytd.NamedType):
430+
typ = pytd.Literal(val)
426431
val = pytd.AnythingType()
427-
elif isinstance(val, pytd.NamedType):
428-
typ = pytd.Literal(val)
432+
elif pytd_utils.MatchesFullName(typ, _TYPE_ALIAS_IDS):
433+
typ = pytd.GenericType(pytd.NamedType("type"), (val,))
429434
val = pytd.AnythingType()
430435
if val and not types.is_any(val):
431436
raise ParseError(msg)

pytype/stubs/builtins/typing.pytd

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -609,9 +609,9 @@ if sys.version_info >= (3, 6):
609609
class _TypedDict(Mapping[str, object]): ...
610610

611611

612-
# The following types are new in Python 3.8 or 3.9; we don't put a version
613-
# guard here because we want to reuse the definitions for typing_extensions in
614-
# lower versions.
612+
# The following types are available only in newer versions; we don't put a
613+
# version guard here because we want to reuse the definitions for
614+
# typing_extensions in lower versions.
615615

616616
# if sys.version_info >= (3, 8):
617617
def final(x: _T) -> _T: ...
@@ -625,6 +625,9 @@ class TypedDict: ...
625625
# if sys.version_info >= (3, 9):
626626
class Annotated: ...
627627

628+
# if sys.version_info >= (3, 10):
629+
class TypeAlias: ...
630+
628631
TYPE_CHECKING = ... # type: bool
629632

630633
# This does not exist at runtime; it needs to be here because it's used by
@@ -635,6 +638,5 @@ class _Alias:
635638
if sys.version_info >= (3, 10):
636639
class Concatenate: ...
637640
class ParamSpec: ...
638-
class TypeAlias: ...
639641
class TypeGuard: ...
640642
def is_typeddict(tp) -> bool: ...

pytype/tests/test_typing2.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -936,5 +936,42 @@ def f(x: Literal[True]) -> Optional[str]: ...
936936
""")
937937

938938

939+
class TypeAliasTest(test_base.BaseTest):
940+
"""Tests for typing.TypeAlias."""
941+
942+
def test_basic(self):
943+
for suffix in ("", "_extensions"):
944+
typing_module = f"typing{suffix}"
945+
with self.subTest(typing_module=typing_module):
946+
ty = self.Infer(f"""
947+
from {typing_module} import TypeAlias
948+
X: TypeAlias = int
949+
""")
950+
self.assertTypesMatchPytd(ty, """
951+
from typing import Type
952+
X: Type[int]
953+
""")
954+
955+
def test_bad_alias(self):
956+
self.CheckWithErrors("""
957+
from typing import TypeAlias
958+
X: TypeAlias = 0 # invalid-annotation
959+
""")
960+
961+
def test_pyi(self):
962+
for suffix in ("", "_extensions"):
963+
typing_module = f"typing{suffix}"
964+
with self.subTest(typing_module=typing_module):
965+
with file_utils.Tempdir() as d:
966+
d.create_file("foo.pyi", f"""
967+
from {typing_module} import TypeAlias
968+
X: TypeAlias = int
969+
""")
970+
self.Check("""
971+
import foo
972+
assert_type(foo.X, "Type[int]")
973+
""", pythonpath=[d.path])
974+
975+
939976
if __name__ == "__main__":
940977
test_base.main()

0 commit comments

Comments
 (0)