Skip to content

Commit 57ff82b

Browse files
ilevkivskyigvanrossum
authored andcommitted
Implement generic type aliases (#2378)
Fixes #606 as per PEP 484. Now type aliases may be generic, so that the example in PEP works. Generic type aliases are allowed for generic classes, unions, callables, and tuples. For example: Vec = Iterable[Tuple[T, T]] TInt = Tuple[T, int] UInt = Union[T, int] CBack = Callable[..., T] The aliases could be used as specified in PEP 484, e.g. either one specifies all free type variables, or if unspecified they are all substituted by Any, for example: def fun(v: Vec[T]) -> Vec[T]: # Same as Iterable[Tuple[T, T]] ... v1: Vec[int] = [] # Same as Iterable[Tuple[int, int]] v2: Vec = [] # Same as Iterable[Tuple[Any, Any]] v3: Vec[int, int] = [] # Error: Invalid alias, too many type arguments! Generic aliases may be used everywhere where a normal generic type could be used (in annotations, generic base classes etc, and as of recently also in runtime expressions). Nested and chained aliases are allowed, but excessive use of those is discouraged in the docs. Like ordinary (non-generic) aliases, generic ones may be imported from other modules. NOTE: Many examples in the tests and in docs require a recent version of typing.py from python/typing to work at runtime (see #2382). This feature may be used with older versions of typing.py by using type comments or "forward references".
1 parent 42bb085 commit 57ff82b

13 files changed

+770
-35
lines changed

docs/source/kinds_of_types.rst

+63-3
Original file line numberDiff line numberDiff line change
@@ -426,9 +426,69 @@ assigning the type to a variable:
426426
def f() -> AliasType:
427427
...
428428
429-
A type alias does not create a new type. It's just a shorthand notation
430-
for another type -- it's equivalent to the target type. Type aliases
431-
can be imported from modules like any names.
429+
Type aliases can be generic, in this case they could be used in two variants:
430+
Subscripted aliases are equivalent to original types with substituted type variables,
431+
number of type arguments must match the number of free type variables
432+
in generic type alias. Unsubscripted aliases are treated as original types with free
433+
variables replaced with ``Any``. Examples (following `PEP 484
434+
<https://www.python.org/dev/peps/pep-0484/#type-aliases>`_):
435+
436+
.. code-block:: python
437+
438+
from typing import TypeVar, Iterable, Tuple, Union, Callable
439+
S = TypeVar('S')
440+
TInt = Tuple[int, S]
441+
UInt = Union[S, int]
442+
CBack = Callable[..., S]
443+
444+
def response(query: str) -> UInt[str]: # Same as Union[str, int]
445+
...
446+
def activate(cb: CBack[S]) -> S: # Same as Callable[..., S]
447+
...
448+
table_entry: TInt # Same as Tuple[int, Any]
449+
450+
T = TypeVar('T', int, float, complex)
451+
Vec = Iterable[Tuple[T, T]]
452+
453+
def inproduct(v: Vec[T]) -> T:
454+
return sum(x*y for x, y in v)
455+
456+
def dilate(v: Vec[T], scale: T) -> Vec[T]:
457+
return ((x * scale, y * scale) for x, y in v)
458+
459+
v1: Vec[int] = [] # Same as Iterable[Tuple[int, int]]
460+
v2: Vec = [] # Same as Iterable[Tuple[Any, Any]]
461+
v3: Vec[int, int] = [] # Error: Invalid alias, too many type arguments!
462+
463+
Type aliases can be imported from modules like any names. Aliases can target another
464+
aliases (although building complex chains of aliases is not recommended, this
465+
impedes code readability, thus defeating the purpose of using aliases).
466+
Following previous examples:
467+
468+
.. code-block:: python
469+
470+
from typing import TypeVar, Generic, Optional
471+
from first_example import AliasType
472+
from second_example import Vec
473+
474+
def fun() -> AliasType:
475+
...
476+
477+
T = TypeVar('T')
478+
class NewVec(Generic[T], Vec[T]):
479+
...
480+
for i, j in NewVec[int]():
481+
...
482+
483+
OIntVec = Optional[Vec[int]]
484+
485+
.. note::
486+
487+
A type alias does not create a new type. It's just a shorthand notation for
488+
another type -- it's equivalent to the target type. For generic type aliases
489+
this means that variance of type variables used for alias definition does not
490+
apply to aliases. A parameterized generic alias is treated simply as an original
491+
type with the corresponding type variables substituted.
432492

433493
.. _newtypes:
434494

mypy/checkexpr.py

+50-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
Type, AnyType, CallableType, Overloaded, NoneTyp, Void, TypeVarDef,
77
TupleType, Instance, TypeVarId, TypeVarType, ErasedType, UnionType,
88
PartialType, DeletedType, UnboundType, UninhabitedType, TypeType,
9-
true_only, false_only, is_named_instance, function_type
9+
true_only, false_only, is_named_instance, function_type,
10+
get_typ_args, set_typ_args,
1011
)
1112
from mypy.nodes import (
1213
NameExpr, RefExpr, Var, FuncDef, OverloadedFuncDef, TypeInfo, CallExpr,
@@ -17,6 +18,7 @@
1718
ConditionalExpr, ComparisonExpr, TempNode, SetComprehension,
1819
DictionaryComprehension, ComplexExpr, EllipsisExpr, StarExpr,
1920
TypeAliasExpr, BackquoteExpr, ARG_POS, ARG_NAMED, ARG_STAR2, MODULE_REF,
21+
UNBOUND_TVAR, BOUND_TVAR,
2022
)
2123
from mypy import nodes
2224
import mypy.checker
@@ -1375,8 +1377,55 @@ def visit_type_application(self, tapp: TypeApplication) -> Type:
13751377
return AnyType()
13761378

13771379
def visit_type_alias_expr(self, alias: TypeAliasExpr) -> Type:
1380+
"""Get type of a type alias (could be generic) in a runtime expression."""
1381+
item = alias.type
1382+
if not alias.in_runtime:
1383+
# We don't replace TypeVar's with Any for alias used as Alias[T](42).
1384+
item = self.replace_tvars_any(item)
1385+
if isinstance(item, Instance):
1386+
# Normally we get a callable type (or overloaded) with .is_type_obj() true
1387+
# representing the class's constructor
1388+
tp = type_object_type(item.type, self.named_type)
1389+
else:
1390+
# This type is invalid in most runtime contexts
1391+
# and corresponding an error will be reported.
1392+
return alias.fallback
1393+
if isinstance(tp, CallableType):
1394+
if len(tp.variables) != len(item.args):
1395+
self.msg.incompatible_type_application(len(tp.variables),
1396+
len(item.args), item)
1397+
return AnyType()
1398+
return self.apply_generic_arguments(tp, item.args, item)
1399+
elif isinstance(tp, Overloaded):
1400+
for it in tp.items():
1401+
if len(it.variables) != len(item.args):
1402+
self.msg.incompatible_type_application(len(it.variables),
1403+
len(item.args), item)
1404+
return AnyType()
1405+
return Overloaded([self.apply_generic_arguments(it, item.args, item)
1406+
for it in tp.items()])
13781407
return AnyType()
13791408

1409+
def replace_tvars_any(self, tp: Type) -> Type:
1410+
"""Replace all type variables of a type alias tp with Any. Basically, this function
1411+
finishes what could not be done in method TypeAnalyser.visit_unbound_type()
1412+
from typeanal.py.
1413+
"""
1414+
typ_args = get_typ_args(tp)
1415+
new_args = typ_args[:]
1416+
for i, arg in enumerate(typ_args):
1417+
if isinstance(arg, UnboundType):
1418+
sym = None
1419+
try:
1420+
sym = self.chk.lookup_qualified(arg.name)
1421+
except KeyError:
1422+
pass
1423+
if sym and (sym.kind == UNBOUND_TVAR or sym.kind == BOUND_TVAR):
1424+
new_args[i] = AnyType()
1425+
else:
1426+
new_args[i] = self.replace_tvars_any(arg)
1427+
return set_typ_args(tp, new_args, tp.line, tp.column)
1428+
13801429
def visit_list_expr(self, e: ListExpr) -> Type:
13811430
"""Type check a list expression [...]."""
13821431
return self.check_lst_expr(e.items, 'builtins.list', '<list>', e)

mypy/exprtotype.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@ def expr_to_unanalyzed_type(expr: Expression) -> Type:
2020
"""
2121
if isinstance(expr, NameExpr):
2222
name = expr.name
23-
return UnboundType(name, line=expr.line)
23+
return UnboundType(name, line=expr.line, column=expr.column)
2424
elif isinstance(expr, MemberExpr):
2525
fullname = get_member_expr_fullname(expr)
2626
if fullname:
27-
return UnboundType(fullname, line=expr.line)
27+
return UnboundType(fullname, line=expr.line, column=expr.column)
2828
else:
2929
raise TypeTranslationError()
3030
elif isinstance(expr, IndexExpr):
@@ -42,7 +42,7 @@ def expr_to_unanalyzed_type(expr: Expression) -> Type:
4242
raise TypeTranslationError()
4343
elif isinstance(expr, ListExpr):
4444
return TypeList([expr_to_unanalyzed_type(t) for t in expr.items],
45-
line=expr.line)
45+
line=expr.line, column=expr.column)
4646
elif isinstance(expr, (StrExpr, BytesExpr)):
4747
# Parse string literal type.
4848
try:

mypy/fastparse.py

+1
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,7 @@ def visit_Assign(self, n: ast35.Assign) -> AssignmentStmt:
432432
typ = parse_type_comment(n.type_comment, n.lineno)
433433
elif new_syntax:
434434
typ = TypeConverter(line=n.lineno).visit(n.annotation) # type: ignore
435+
typ.column = n.annotation.col_offset
435436
if n.value is None: # always allow 'x: int'
436437
rvalue = TempNode(AnyType()) # type: Expression
437438
else:

mypy/nodes.py

+11-2
Original file line numberDiff line numberDiff line change
@@ -1732,9 +1732,18 @@ class TypeAliasExpr(Expression):
17321732
"""Type alias expression (rvalue)."""
17331733

17341734
type = None # type: mypy.types.Type
1735-
1736-
def __init__(self, type: 'mypy.types.Type') -> None:
1735+
# Simple fallback type for aliases that are invalid in runtime expressions
1736+
# (for example Union, Tuple, Callable).
1737+
fallback = None # type: mypy.types.Type
1738+
# This type alias is subscripted in a runtime expression like Alias[int](42)
1739+
# (not in a type context like type annotation or base class).
1740+
in_runtime = False # type: bool
1741+
1742+
def __init__(self, type: 'mypy.types.Type', fallback: 'mypy.types.Type' = None,
1743+
in_runtime: bool = False) -> None:
17371744
self.type = type
1745+
self.fallback = fallback
1746+
self.in_runtime = in_runtime
17381747

17391748
def accept(self, visitor: NodeVisitor[T]) -> T:
17401749
return visitor.visit_type_alias_expr(self)

mypy/semanal.py

+32-5
Original file line numberDiff line numberDiff line change
@@ -1126,7 +1126,8 @@ def visit_block_maybe(self, b: Block) -> None:
11261126
if b:
11271127
self.visit_block(b)
11281128

1129-
def anal_type(self, t: Type, allow_tuple_literal: bool = False) -> Type:
1129+
def anal_type(self, t: Type, allow_tuple_literal: bool = False,
1130+
aliasing: bool = False) -> Type:
11301131
if t:
11311132
if allow_tuple_literal:
11321133
# Types such as (t1, t2, ...) only allowed in assignment statements. They'll
@@ -1143,7 +1144,8 @@ def anal_type(self, t: Type, allow_tuple_literal: bool = False) -> Type:
11431144
return TupleType(items, self.builtin_type('builtins.tuple'), t.line)
11441145
a = TypeAnalyser(self.lookup_qualified,
11451146
self.lookup_fully_qualified,
1146-
self.fail)
1147+
self.fail,
1148+
aliasing=aliasing)
11471149
return t.accept(a)
11481150
else:
11491151
return None
@@ -1173,7 +1175,8 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
11731175
node.kind = TYPE_ALIAS
11741176
node.type_override = res
11751177
if isinstance(s.rvalue, IndexExpr):
1176-
s.rvalue.analyzed = TypeAliasExpr(res)
1178+
s.rvalue.analyzed = TypeAliasExpr(res,
1179+
fallback=self.alias_fallback(res))
11771180
if s.type:
11781181
# Store type into nodes.
11791182
for lvalue in s.lvalues:
@@ -1211,6 +1214,19 @@ def analyze_simple_literal_type(self, rvalue: Expression) -> Optional[Type]:
12111214
return self.named_type_or_none('builtins.unicode')
12121215
return None
12131216

1217+
def alias_fallback(self, tp: Type) -> Instance:
1218+
"""Make a dummy Instance with no methods. It is used as a fallback type
1219+
to detect errors for non-Instance aliases (i.e. Unions, Tuples, Callables).
1220+
"""
1221+
kind = (' to Callable' if isinstance(tp, CallableType) else
1222+
' to Tuple' if isinstance(tp, TupleType) else
1223+
' to Union' if isinstance(tp, UnionType) else '')
1224+
cdef = ClassDef('Type alias' + kind, Block([]))
1225+
fb_info = TypeInfo(SymbolTable(), cdef, self.cur_mod_id)
1226+
fb_info.bases = [self.object_type()]
1227+
fb_info.mro = [fb_info, self.object_type().type]
1228+
return Instance(fb_info, [])
1229+
12141230
def check_and_set_up_type_alias(self, s: AssignmentStmt) -> None:
12151231
"""Check if assignment creates a type alias and set it up as needed."""
12161232
# For now, type aliases only work at the top level of a module.
@@ -2361,7 +2377,16 @@ def visit_unary_expr(self, expr: UnaryExpr) -> None:
23612377

23622378
def visit_index_expr(self, expr: IndexExpr) -> None:
23632379
expr.base.accept(self)
2364-
if refers_to_class_or_function(expr.base):
2380+
if isinstance(expr.base, RefExpr) and expr.base.kind == TYPE_ALIAS:
2381+
# Special form -- subscripting a generic type alias.
2382+
# Perform the type substitution and create a new alias.
2383+
res = analyze_type_alias(expr,
2384+
self.lookup_qualified,
2385+
self.lookup_fully_qualified,
2386+
self.fail)
2387+
expr.analyzed = TypeAliasExpr(res, fallback=self.alias_fallback(res),
2388+
in_runtime=True)
2389+
elif refers_to_class_or_function(expr.base):
23652390
# Special form -- type application.
23662391
# Translate index to an unanalyzed type.
23672392
types = [] # type: List[Type]
@@ -2375,7 +2400,7 @@ def visit_index_expr(self, expr: IndexExpr) -> None:
23752400
except TypeTranslationError:
23762401
self.fail('Type expected within [...]', expr)
23772402
return
2378-
typearg = self.anal_type(typearg)
2403+
typearg = self.anal_type(typearg, aliasing=True)
23792404
types.append(typearg)
23802405
expr.analyzed = TypeApplication(expr.base, types)
23812406
expr.analyzed.line = expr.line
@@ -3051,6 +3076,8 @@ def visit_decorator(self, dec: Decorator) -> None:
30513076

30523077
def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
30533078
self.analyze(s.type)
3079+
if isinstance(s.rvalue, IndexExpr) and isinstance(s.rvalue.analyzed, TypeAliasExpr):
3080+
self.analyze(s.rvalue.analyzed.type)
30543081
super().visit_assignment_stmt(s)
30553082

30563083
def visit_cast_expr(self, e: CastExpr) -> None:

0 commit comments

Comments
 (0)