Skip to content

Simplify and tighten type aliases #3524

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Jul 7, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 2 additions & 22 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from typing import cast, Dict, Set, List, Tuple, Callable, Union, Optional

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

def replace_tvars_any(self, tp: Type) -> Type:
"""Replace all type variables of a type alias tp with Any. Basically, this function
finishes what could not be done in method TypeAnalyser.visit_unbound_type()
from typeanal.py.
"""
typ_args = get_typ_args(tp)
new_args = typ_args[:]
for i, arg in enumerate(typ_args):
if isinstance(arg, UnboundType):
sym = None
try:
sym = self.chk.lookup_qualified(arg.name)
except KeyError:
pass
if sym and (sym.kind == TVAR):
new_args[i] = AnyType()
else:
new_args[i] = self.replace_tvars_any(arg)
return set_typ_args(tp, new_args, tp.line, tp.column)

def visit_list_expr(self, e: ListExpr) -> Type:
"""Type check a list expression [...]."""
return self.check_lst_expr(e.items, 'builtins.list', '<list>', e)
Expand Down
1 change: 1 addition & 0 deletions mypy/fixup.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ def visit_symbol_table(self, symtab: SymbolTable) -> None:
if stnode is not None:
value.node = stnode.node
value.type_override = stnode.type_override
value.alias_tvars = stnode.alias_tvars or []
elif not self.quick_and_dirty:
assert stnode is not None, "Could not find cross-ref %s" % (cross_ref,)
else:
Expand Down
15 changes: 12 additions & 3 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1812,11 +1812,12 @@ class TypeAliasExpr(Expression):
# (not in a type context like type annotation or base class).
in_runtime = False # type: bool

def __init__(self, type: 'mypy.types.Type', fallback: 'mypy.types.Type' = None,
in_runtime: bool = False) -> None:
def __init__(self, type: 'mypy.types.Type', tvars: List[str],
fallback: 'mypy.types.Type' = None, in_runtime: bool = False) -> None:
self.type = type
self.fallback = fallback
self.in_runtime = in_runtime
self.tvars = tvars

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

def __init__(self, kind: int, node: Optional[SymbolNode], mod_id: str = None,
typ: 'mypy.types.Type' = None,
module_public: bool = True, normalized: bool = False) -> None:
module_public: bool = True, normalized: bool = False,
alias_tvars: Optional[List[str]] = None) -> None:
self.kind = kind
self.node = node
self.type_override = typ
self.mod_id = mod_id
self.module_public = module_public
self.normalized = normalized
self.alias_tvars = alias_tvars

@property
def fullname(self) -> Optional[str]:
Expand Down Expand Up @@ -2342,6 +2348,7 @@ def serialize(self, prefix: str, name: str) -> JsonDict:
data['node'] = self.node.serialize()
if self.type_override is not None:
data['type_override'] = self.type_override.serialize()
data['alias_tvars'] = self.alias_tvars
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a test case that triggers this (and the corresponding deserialization bit)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! Added an incremental test.

return data

@classmethod
Expand All @@ -2360,6 +2367,8 @@ def deserialize(cls, data: JsonDict) -> 'SymbolTableNode':
if 'type_override' in data:
typ = mypy.types.deserialize_type(data['type_override'])
stnode = SymbolTableNode(kind, node, typ=typ)
if 'alias_tvars' in data:
stnode.alias_tvars = data['alias_tvars']
if 'module_public' in data:
stnode.module_public = data['module_public']
return stnode
Expand Down
137 changes: 81 additions & 56 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -1395,7 +1395,8 @@ def visit_import_from(self, imp: ImportFrom) -> None:
self.cur_mod_id,
node.type_override,
module_public=module_public,
normalized=node.normalized)
normalized=node.normalized,
alias_tvars=node.alias_tvars)
self.add_symbol(imported_id, symbol, imp)
elif module and not missing:
# Missing attribute.
Expand Down Expand Up @@ -1447,7 +1448,8 @@ def normalize_type_alias(self, node: SymbolTableNode,
normalized = True
if normalized:
node = SymbolTableNode(node.kind, node.node,
node.mod_id, node.type_override, normalized=True)
node.mod_id, node.type_override,
normalized=True, alias_tvars=node.alias_tvars)
return node

def add_fixture_note(self, fullname: str, ctx: Context) -> None:
Expand Down Expand Up @@ -1491,7 +1493,8 @@ def visit_import_all(self, i: ImportAll) -> None:
self.add_symbol(name, SymbolTableNode(node.kind, node.node,
self.cur_mod_id,
node.type_override,
normalized=node.normalized), i)
normalized=node.normalized,
alias_tvars=node.alias_tvars), i)
else:
# Don't add any dummy symbols for 'from x import *' if 'x' is unknown.
pass
Expand Down Expand Up @@ -1563,36 +1566,11 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
allow_tuple_literal = isinstance(s.lvalues[-1], (TupleExpr, ListExpr))
s.type = self.anal_type(s.type, allow_tuple_literal=allow_tuple_literal)
else:
# For simple assignments, allow binding type aliases.
# Also set the type if the rvalue is a simple literal.
# Set the type if the rvalue is a simple literal.
if (s.type is None and len(s.lvalues) == 1 and
isinstance(s.lvalues[0], NameExpr)):
if s.lvalues[0].is_def:
s.type = self.analyze_simple_literal_type(s.rvalue)
res = analyze_type_alias(s.rvalue,
self.lookup_qualified,
self.lookup_fully_qualified,
self.tvar_scope,
self.fail,
self.plugin,
self.options,
self.is_typeshed_stub_file,
allow_unnormalized=True)
if res and (not isinstance(res, Instance) or res.args):
# TODO: What if this gets reassigned?
check_for_explicit_any(res, self.options, self.is_typeshed_stub_file, self.msg,
context=s)
# when this type alias gets "inlined", the Any is not explicit anymore,
# so we need to replace it with non-explicit Anys
res = make_any_non_explicit(res)

name = s.lvalues[0]
node = self.lookup(name.name, name)
node.kind = TYPE_ALIAS
node.type_override = res
if isinstance(s.rvalue, IndexExpr):
s.rvalue.analyzed = TypeAliasExpr(res,
fallback=self.alias_fallback(res))
if s.type:
# Store type into nodes.
for lvalue in s.lvalues:
Expand Down Expand Up @@ -1646,26 +1624,79 @@ def alias_fallback(self, tp: Type) -> Instance:
fb_info.mro = [fb_info, self.object_type().type]
return Instance(fb_info, [])

def analyze_alias(self, rvalue: Expression,
allow_unnormalized: bool) -> Tuple[Optional[Type], List[str]]:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add docstring. Explain arguments and the return value.

"""Check if 'rvalue' represents a valid type allowed for aliasing
(e.g. not a type variable). If yes, return the corresponding type and a list of
qualified type variable names for generic aliases.
If 'allow_unnormalized' is True, allow types like builtins.list[T].
"""
res = analyze_type_alias(rvalue,
self.lookup_qualified,
self.lookup_fully_qualified,
self.tvar_scope,
self.fail,
self.plugin,
self.options,
self.is_typeshed_stub_file,
allow_unnormalized=True)
if res:
alias_tvars = [name for (name, _) in
res.accept(TypeVariableQuery(self.lookup_qualified, self.tvar_scope))]
else:
alias_tvars = []
return res, alias_tvars

def check_and_set_up_type_alias(self, s: AssignmentStmt) -> None:
"""Check if assignment creates a type alias and set it up as needed."""
# For now, type aliases only work at the top level of a module.
if (len(s.lvalues) == 1 and not self.is_func_scope() and not self.type
"""Check if assignment creates a type alias and set it up as needed.
For simple aliases like L = List we use a simpler mechanism, just copying TypeInfo.
For subscripted (including generic) aliases the resulting types are stored
in rvalue.analyzed.
"""
# Type aliases are created only at module scope and class scope (for subscripted types),
# at function scope assignments always create local variables with type object types.
lvalue = s.lvalues[0]
if not isinstance(lvalue, NameExpr):
return
if (len(s.lvalues) == 1 and not self.is_func_scope() and
not (self.type and isinstance(s.rvalue, NameExpr) and lvalue.is_def)
and not s.type):
lvalue = s.lvalues[0]
if isinstance(lvalue, NameExpr):
if not lvalue.is_def:
# Only a definition can create a type alias, not regular assignment.
return
rvalue = s.rvalue
rvalue = s.rvalue
res, alias_tvars = self.analyze_alias(rvalue, allow_unnormalized=True)
if not res:
return
node = self.lookup(lvalue.name, lvalue)
if not lvalue.is_def:
# Only a definition can create a type alias, not regular assignment.
if node and node.kind == TYPE_ALIAS or isinstance(node.node, TypeInfo):
self.fail('Cannot assign multiple types to name "{}"'
' without an explicit "Type[...]" annotation'
.format(lvalue.name), lvalue)
return
check_for_explicit_any(res, self.options, self.is_typeshed_stub_file, self.msg,
context=s)
# when this type alias gets "inlined", the Any is not explicit anymore,
# so we need to replace it with non-explicit Anys
res = make_any_non_explicit(res)
if isinstance(res, Instance) and not res.args and isinstance(rvalue, RefExpr):
# For simple (on-generic) aliases we use aliasing TypeInfo's
# to allow using them in runtime context where it makes sense.
node.node = res.type
if isinstance(rvalue, RefExpr):
node = rvalue.node
if isinstance(node, TypeInfo):
# TODO: We should record the fact that this is a variable
# that refers to a type, rather than making this
# just an alias for the type.
sym = self.lookup_type_node(rvalue)
if sym:
self.globals[lvalue.name] = sym
sym = self.lookup_type_node(rvalue)
if sym:
node.normalized = sym.normalized
return
node.kind = TYPE_ALIAS
node.type_override = res
node.alias_tvars = alias_tvars
if isinstance(rvalue, IndexExpr):
# We only need this for subscripted aliases, since simple aliases
# are already processed using aliasing TypeInfo's above.
rvalue.analyzed = TypeAliasExpr(res, node.alias_tvars,
fallback=self.alias_fallback(res))
rvalue.analyzed.line = rvalue.line
rvalue.analyzed.column = rvalue.column

def analyze_lvalue(self, lval: Lvalue, nested: bool = False,
add_global: bool = False,
Expand Down Expand Up @@ -3196,17 +3227,11 @@ def visit_index_expr(self, expr: IndexExpr) -> None:
elif isinstance(expr.base, RefExpr) and expr.base.kind == TYPE_ALIAS:
# Special form -- subscripting a generic type alias.
# Perform the type substitution and create a new alias.
res = analyze_type_alias(expr,
self.lookup_qualified,
self.lookup_fully_qualified,
self.tvar_scope,
self.fail,
self.plugin,
self.options,
self.is_typeshed_stub_file,
allow_unnormalized=self.is_stub_file)
expr.analyzed = TypeAliasExpr(res, fallback=self.alias_fallback(res),
res, alias_tvars = self.analyze_alias(expr, allow_unnormalized=self.is_stub_file)
expr.analyzed = TypeAliasExpr(res, alias_tvars, fallback=self.alias_fallback(res),
in_runtime=True)
expr.analyzed.line = expr.line
expr.analyzed.column = expr.column
elif refers_to_class_or_function(expr.base):
# Special form -- type application.
# Translate index to an unanalyzed type.
Expand Down
3 changes: 2 additions & 1 deletion mypy/treetransform.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,8 @@ def visit_type_var_expr(self, node: TypeVarExpr) -> TypeVarExpr:
self.type(node.upper_bound), variance=node.variance)

def visit_type_alias_expr(self, node: TypeAliasExpr) -> TypeAliasExpr:
return TypeAliasExpr(node.type)
return TypeAliasExpr(node.type, node.tvars,
fallback=node.fallback, in_runtime=node.in_runtime)

def visit_newtype_expr(self, node: NewTypeExpr) -> NewTypeExpr:
res = NewTypeExpr(node.name, node.old_type, line=node.line)
Expand Down
Loading