Skip to content

Commit 70d5f45

Browse files
ilevkivskyiJukkaL
authored andcommitted
Simplify and tighten type aliases (#3524)
Fixes #2887. Fixes #3191. In addition this prohibits reassigning aliases. Previously something like this was allowed: ``` if random(): Alias = Sequence[int] else: Alias = Sequence[float] def fun(arg: Alias) -> None: ... ``` Now this will generate an error: Cannot assign multiple types to name "Alias" without an explicit "Type[...]" annotation. See #3494 for background. Finally, this simplifies the logic in semanal.py, so that most processing of type aliases happens in one function.
1 parent 6c61c66 commit 70d5f45

10 files changed

+325
-128
lines changed

mypy/checkexpr.py

+2-22
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from typing import cast, Dict, Set, List, Tuple, Callable, Union, Optional
55

66
from mypy.errors import report_internal_error
7-
from mypy.typeanal import has_any_from_unimported_type, check_for_explicit_any
7+
from mypy.typeanal import has_any_from_unimported_type, check_for_explicit_any, set_any_tvars
88
from mypy.types import (
99
Type, AnyType, CallableType, Overloaded, NoneTyp, TypeVarDef,
1010
TupleType, TypedDictType, Instance, TypeVarType, ErasedType, UnionType,
@@ -1737,7 +1737,7 @@ def visit_type_alias_expr(self, alias: TypeAliasExpr) -> Type:
17371737
item = alias.type
17381738
if not alias.in_runtime:
17391739
# We don't replace TypeVar's with Any for alias used as Alias[T](42).
1740-
item = self.replace_tvars_any(item)
1740+
item = set_any_tvars(item, alias.tvars, alias.line, alias.column)
17411741
if isinstance(item, Instance):
17421742
# Normally we get a callable type (or overloaded) with .is_type_obj() true
17431743
# representing the class's constructor
@@ -1762,26 +1762,6 @@ def visit_type_alias_expr(self, alias: TypeAliasExpr) -> Type:
17621762
for it in tp.items()])
17631763
return AnyType()
17641764

1765-
def replace_tvars_any(self, tp: Type) -> Type:
1766-
"""Replace all type variables of a type alias tp with Any. Basically, this function
1767-
finishes what could not be done in method TypeAnalyser.visit_unbound_type()
1768-
from typeanal.py.
1769-
"""
1770-
typ_args = get_typ_args(tp)
1771-
new_args = typ_args[:]
1772-
for i, arg in enumerate(typ_args):
1773-
if isinstance(arg, UnboundType):
1774-
sym = None
1775-
try:
1776-
sym = self.chk.lookup_qualified(arg.name)
1777-
except KeyError:
1778-
pass
1779-
if sym and (sym.kind == TVAR):
1780-
new_args[i] = AnyType()
1781-
else:
1782-
new_args[i] = self.replace_tvars_any(arg)
1783-
return set_typ_args(tp, new_args, tp.line, tp.column)
1784-
17851765
def visit_list_expr(self, e: ListExpr) -> Type:
17861766
"""Type check a list expression [...]."""
17871767
return self.check_lst_expr(e.items, 'builtins.list', '<list>', e)

mypy/fixup.py

+1
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ def visit_symbol_table(self, symtab: SymbolTable) -> None:
8888
if stnode is not None:
8989
value.node = stnode.node
9090
value.type_override = stnode.type_override
91+
value.alias_tvars = stnode.alias_tvars or []
9192
elif not self.quick_and_dirty:
9293
assert stnode is not None, "Could not find cross-ref %s" % (cross_ref,)
9394
else:

mypy/nodes.py

+12-3
Original file line numberDiff line numberDiff line change
@@ -1812,11 +1812,12 @@ class TypeAliasExpr(Expression):
18121812
# (not in a type context like type annotation or base class).
18131813
in_runtime = False # type: bool
18141814

1815-
def __init__(self, type: 'mypy.types.Type', fallback: 'mypy.types.Type' = None,
1816-
in_runtime: bool = False) -> None:
1815+
def __init__(self, type: 'mypy.types.Type', tvars: List[str],
1816+
fallback: 'mypy.types.Type' = None, in_runtime: bool = False) -> None:
18171817
self.type = type
18181818
self.fallback = fallback
18191819
self.in_runtime = in_runtime
1820+
self.tvars = tvars
18201821

18211822
def accept(self, visitor: ExpressionVisitor[T]) -> T:
18221823
return visitor.visit_type_alias_expr(self)
@@ -2267,6 +2268,9 @@ class SymbolTableNode:
22672268
mod_id = '' # type: Optional[str]
22682269
# If this not None, override the type of the 'node' attribute.
22692270
type_override = None # type: Optional[mypy.types.Type]
2271+
# For generic aliases this stores the (qualified) names of type variables.
2272+
# (For example see testGenericAliasWithTypeVarsFromDifferentModules.)
2273+
alias_tvars = None # type: Optional[List[str]]
22702274
# If False, this name won't be imported via 'from <module> import *'.
22712275
# This has no effect on names within classes.
22722276
module_public = True
@@ -2278,13 +2282,15 @@ class SymbolTableNode:
22782282

22792283
def __init__(self, kind: int, node: Optional[SymbolNode], mod_id: str = None,
22802284
typ: 'mypy.types.Type' = None,
2281-
module_public: bool = True, normalized: bool = False) -> None:
2285+
module_public: bool = True, normalized: bool = False,
2286+
alias_tvars: Optional[List[str]] = None) -> None:
22822287
self.kind = kind
22832288
self.node = node
22842289
self.type_override = typ
22852290
self.mod_id = mod_id
22862291
self.module_public = module_public
22872292
self.normalized = normalized
2293+
self.alias_tvars = alias_tvars
22882294

22892295
@property
22902296
def fullname(self) -> Optional[str]:
@@ -2342,6 +2348,7 @@ def serialize(self, prefix: str, name: str) -> JsonDict:
23422348
data['node'] = self.node.serialize()
23432349
if self.type_override is not None:
23442350
data['type_override'] = self.type_override.serialize()
2351+
data['alias_tvars'] = self.alias_tvars
23452352
return data
23462353

23472354
@classmethod
@@ -2360,6 +2367,8 @@ def deserialize(cls, data: JsonDict) -> 'SymbolTableNode':
23602367
if 'type_override' in data:
23612368
typ = mypy.types.deserialize_type(data['type_override'])
23622369
stnode = SymbolTableNode(kind, node, typ=typ)
2370+
if 'alias_tvars' in data:
2371+
stnode.alias_tvars = data['alias_tvars']
23632372
if 'module_public' in data:
23642373
stnode.module_public = data['module_public']
23652374
return stnode

mypy/semanal.py

+81-56
Original file line numberDiff line numberDiff line change
@@ -1395,7 +1395,8 @@ def visit_import_from(self, imp: ImportFrom) -> None:
13951395
self.cur_mod_id,
13961396
node.type_override,
13971397
module_public=module_public,
1398-
normalized=node.normalized)
1398+
normalized=node.normalized,
1399+
alias_tvars=node.alias_tvars)
13991400
self.add_symbol(imported_id, symbol, imp)
14001401
elif module and not missing:
14011402
# Missing attribute.
@@ -1447,7 +1448,8 @@ def normalize_type_alias(self, node: SymbolTableNode,
14471448
normalized = True
14481449
if normalized:
14491450
node = SymbolTableNode(node.kind, node.node,
1450-
node.mod_id, node.type_override, normalized=True)
1451+
node.mod_id, node.type_override,
1452+
normalized=True, alias_tvars=node.alias_tvars)
14511453
return node
14521454

14531455
def add_fixture_note(self, fullname: str, ctx: Context) -> None:
@@ -1491,7 +1493,8 @@ def visit_import_all(self, i: ImportAll) -> None:
14911493
self.add_symbol(name, SymbolTableNode(node.kind, node.node,
14921494
self.cur_mod_id,
14931495
node.type_override,
1494-
normalized=node.normalized), i)
1496+
normalized=node.normalized,
1497+
alias_tvars=node.alias_tvars), i)
14951498
else:
14961499
# Don't add any dummy symbols for 'from x import *' if 'x' is unknown.
14971500
pass
@@ -1563,36 +1566,11 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
15631566
allow_tuple_literal = isinstance(s.lvalues[-1], (TupleExpr, ListExpr))
15641567
s.type = self.anal_type(s.type, allow_tuple_literal=allow_tuple_literal)
15651568
else:
1566-
# For simple assignments, allow binding type aliases.
1567-
# Also set the type if the rvalue is a simple literal.
1569+
# Set the type if the rvalue is a simple literal.
15681570
if (s.type is None and len(s.lvalues) == 1 and
15691571
isinstance(s.lvalues[0], NameExpr)):
15701572
if s.lvalues[0].is_def:
15711573
s.type = self.analyze_simple_literal_type(s.rvalue)
1572-
res = analyze_type_alias(s.rvalue,
1573-
self.lookup_qualified,
1574-
self.lookup_fully_qualified,
1575-
self.tvar_scope,
1576-
self.fail,
1577-
self.plugin,
1578-
self.options,
1579-
self.is_typeshed_stub_file,
1580-
allow_unnormalized=True)
1581-
if res and (not isinstance(res, Instance) or res.args):
1582-
# TODO: What if this gets reassigned?
1583-
check_for_explicit_any(res, self.options, self.is_typeshed_stub_file, self.msg,
1584-
context=s)
1585-
# when this type alias gets "inlined", the Any is not explicit anymore,
1586-
# so we need to replace it with non-explicit Anys
1587-
res = make_any_non_explicit(res)
1588-
1589-
name = s.lvalues[0]
1590-
node = self.lookup(name.name, name)
1591-
node.kind = TYPE_ALIAS
1592-
node.type_override = res
1593-
if isinstance(s.rvalue, IndexExpr):
1594-
s.rvalue.analyzed = TypeAliasExpr(res,
1595-
fallback=self.alias_fallback(res))
15961574
if s.type:
15971575
# Store type into nodes.
15981576
for lvalue in s.lvalues:
@@ -1646,26 +1624,79 @@ def alias_fallback(self, tp: Type) -> Instance:
16461624
fb_info.mro = [fb_info, self.object_type().type]
16471625
return Instance(fb_info, [])
16481626

1627+
def analyze_alias(self, rvalue: Expression,
1628+
allow_unnormalized: bool) -> Tuple[Optional[Type], List[str]]:
1629+
"""Check if 'rvalue' represents a valid type allowed for aliasing
1630+
(e.g. not a type variable). If yes, return the corresponding type and a list of
1631+
qualified type variable names for generic aliases.
1632+
If 'allow_unnormalized' is True, allow types like builtins.list[T].
1633+
"""
1634+
res = analyze_type_alias(rvalue,
1635+
self.lookup_qualified,
1636+
self.lookup_fully_qualified,
1637+
self.tvar_scope,
1638+
self.fail,
1639+
self.plugin,
1640+
self.options,
1641+
self.is_typeshed_stub_file,
1642+
allow_unnormalized=True)
1643+
if res:
1644+
alias_tvars = [name for (name, _) in
1645+
res.accept(TypeVariableQuery(self.lookup_qualified, self.tvar_scope))]
1646+
else:
1647+
alias_tvars = []
1648+
return res, alias_tvars
1649+
16491650
def check_and_set_up_type_alias(self, s: AssignmentStmt) -> None:
1650-
"""Check if assignment creates a type alias and set it up as needed."""
1651-
# For now, type aliases only work at the top level of a module.
1652-
if (len(s.lvalues) == 1 and not self.is_func_scope() and not self.type
1651+
"""Check if assignment creates a type alias and set it up as needed.
1652+
For simple aliases like L = List we use a simpler mechanism, just copying TypeInfo.
1653+
For subscripted (including generic) aliases the resulting types are stored
1654+
in rvalue.analyzed.
1655+
"""
1656+
# Type aliases are created only at module scope and class scope (for subscripted types),
1657+
# at function scope assignments always create local variables with type object types.
1658+
lvalue = s.lvalues[0]
1659+
if not isinstance(lvalue, NameExpr):
1660+
return
1661+
if (len(s.lvalues) == 1 and not self.is_func_scope() and
1662+
not (self.type and isinstance(s.rvalue, NameExpr) and lvalue.is_def)
16531663
and not s.type):
1654-
lvalue = s.lvalues[0]
1655-
if isinstance(lvalue, NameExpr):
1656-
if not lvalue.is_def:
1657-
# Only a definition can create a type alias, not regular assignment.
1658-
return
1659-
rvalue = s.rvalue
1664+
rvalue = s.rvalue
1665+
res, alias_tvars = self.analyze_alias(rvalue, allow_unnormalized=True)
1666+
if not res:
1667+
return
1668+
node = self.lookup(lvalue.name, lvalue)
1669+
if not lvalue.is_def:
1670+
# Only a definition can create a type alias, not regular assignment.
1671+
if node and node.kind == TYPE_ALIAS or isinstance(node.node, TypeInfo):
1672+
self.fail('Cannot assign multiple types to name "{}"'
1673+
' without an explicit "Type[...]" annotation'
1674+
.format(lvalue.name), lvalue)
1675+
return
1676+
check_for_explicit_any(res, self.options, self.is_typeshed_stub_file, self.msg,
1677+
context=s)
1678+
# when this type alias gets "inlined", the Any is not explicit anymore,
1679+
# so we need to replace it with non-explicit Anys
1680+
res = make_any_non_explicit(res)
1681+
if isinstance(res, Instance) and not res.args and isinstance(rvalue, RefExpr):
1682+
# For simple (on-generic) aliases we use aliasing TypeInfo's
1683+
# to allow using them in runtime context where it makes sense.
1684+
node.node = res.type
16601685
if isinstance(rvalue, RefExpr):
1661-
node = rvalue.node
1662-
if isinstance(node, TypeInfo):
1663-
# TODO: We should record the fact that this is a variable
1664-
# that refers to a type, rather than making this
1665-
# just an alias for the type.
1666-
sym = self.lookup_type_node(rvalue)
1667-
if sym:
1668-
self.globals[lvalue.name] = sym
1686+
sym = self.lookup_type_node(rvalue)
1687+
if sym:
1688+
node.normalized = sym.normalized
1689+
return
1690+
node.kind = TYPE_ALIAS
1691+
node.type_override = res
1692+
node.alias_tvars = alias_tvars
1693+
if isinstance(rvalue, IndexExpr):
1694+
# We only need this for subscripted aliases, since simple aliases
1695+
# are already processed using aliasing TypeInfo's above.
1696+
rvalue.analyzed = TypeAliasExpr(res, node.alias_tvars,
1697+
fallback=self.alias_fallback(res))
1698+
rvalue.analyzed.line = rvalue.line
1699+
rvalue.analyzed.column = rvalue.column
16691700

16701701
def analyze_lvalue(self, lval: Lvalue, nested: bool = False,
16711702
add_global: bool = False,
@@ -3196,17 +3227,11 @@ def visit_index_expr(self, expr: IndexExpr) -> None:
31963227
elif isinstance(expr.base, RefExpr) and expr.base.kind == TYPE_ALIAS:
31973228
# Special form -- subscripting a generic type alias.
31983229
# Perform the type substitution and create a new alias.
3199-
res = analyze_type_alias(expr,
3200-
self.lookup_qualified,
3201-
self.lookup_fully_qualified,
3202-
self.tvar_scope,
3203-
self.fail,
3204-
self.plugin,
3205-
self.options,
3206-
self.is_typeshed_stub_file,
3207-
allow_unnormalized=self.is_stub_file)
3208-
expr.analyzed = TypeAliasExpr(res, fallback=self.alias_fallback(res),
3230+
res, alias_tvars = self.analyze_alias(expr, allow_unnormalized=self.is_stub_file)
3231+
expr.analyzed = TypeAliasExpr(res, alias_tvars, fallback=self.alias_fallback(res),
32093232
in_runtime=True)
3233+
expr.analyzed.line = expr.line
3234+
expr.analyzed.column = expr.column
32103235
elif refers_to_class_or_function(expr.base):
32113236
# Special form -- type application.
32123237
# Translate index to an unanalyzed type.

mypy/treetransform.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,8 @@ def visit_type_var_expr(self, node: TypeVarExpr) -> TypeVarExpr:
477477
self.type(node.upper_bound), variance=node.variance)
478478

479479
def visit_type_alias_expr(self, node: TypeAliasExpr) -> TypeAliasExpr:
480-
return TypeAliasExpr(node.type)
480+
return TypeAliasExpr(node.type, node.tvars,
481+
fallback=node.fallback, in_runtime=node.in_runtime)
481482

482483
def visit_newtype_expr(self, node: NewTypeExpr) -> NewTypeExpr:
483484
res = NewTypeExpr(node.name, node.old_type, line=node.line)

0 commit comments

Comments
 (0)