8000 Simplify and tighten type aliases by ilevkivskyi · Pull Request #3524 · python/mypy · GitHub
[go: up one dir, main page]

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 5 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
25 changes: 3 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
from mypy.typeanal import has_any_from_unimported_type, replace_alias_tvars
from mypy.types import (
Type, AnyType, CallableType, Overloaded, NoneTyp, TypeVarDef,
TupleType, TypedDictType, Instance, TypeVarType, ErasedType, UnionType,
Expand Down Expand Up @@ -1730,7 +1730,8 @@ 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 = replace_alias_tvars(item, alias.tvars, [AnyType()] * len(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 @@ -1755,26 +1756,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
3 changes: 3 additions & 0 deletions mypy/fixup.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ def visit_symbol_table(self, symtab: SymbolTable) -> None:
if stnode is not None:
value.node = stnode.node
value.type_override = stnode.type_override
if not value.type_override and self.quick_and_dirty:
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't understand this -- add a comment. Is there a test case that tests this change?

Copy link
Member Author

Choose a reason for hiding this comment

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

Now I am also not sure it is needed. It was an attempt to fix #3355, but now I have a better idea. I removed this for now, and will make a separate PR later.

value.type_override = Instance(stale_info(), [])
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
5 changes: 3 additions & 2 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1808,11 +1808,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
40 changes: 23 additions & 17 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -1598,6 +1598,20 @@ 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.

res = analyze_type_alias(rvalue,
self.lookup_qualified,
self.lookup_fully_qualified,
self.tvar_scope,
self.fail, self.plugin, 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."""
# Type aliases are created only at module scope, at class and function scopes
Expand All @@ -1607,11 +1621,7 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> None:
lvalue = s.lvalues[0]
if isinstance(lvalue, NameExpr):
rvalue = s.rvalue
res = analyze_type_alias(rvalue,
self.lookup_qualified,
self.lookup_fully_qualified,
self.tvar_scope,
self.fail, allow_unnormalized=True)
res, alias_tvars = self.analyze_alias(rvalue, allow_unnormalized=True)
if res:
node = self.lookup(lvalue.name, lvalue)
if not lvalue.is_def:
Expand All @@ -1630,12 +1640,12 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> None:
return
node.kind = TYPE_ALIAS
node.type_override = res
node.alias_tvars = [name for (name, _) in
res.accept(TypeVariableQuery(self.lookup_qualified,
self.tvar_scope))]
node.alias_tvars = alias_tvars
if isinstance(s.rvalue, IndexExpr):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Add comment that explains the motivation of this if statement.

s.rvalue.analyzed = TypeAliasExpr(res,
s.rvalue.analyzed = TypeAliasExpr(res, node.alias_tvars,
fallback=self.alias_fallback(res))
s.rvalue.analyzed.line = s.rvalue.line
s.rvalue.analyzed.column = s.rvalue.column

def analyze_lvalue(self, lval: Lvalue, nested: bool = False,
add_global: bool = False,
Expand Down Expand Up @@ -3125,15 +3135,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,
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
47 changes: 24 additions & 23 deletions mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,15 +221,15 @@ def visit_unbound_type(self, t: UnboundType) -> Type:
act_len = len(an_args)
if exp_len > 0 and act_len == 0:
# Interpret bare Alias same as normal generic, i.e., Alias[Any, Any, ...]
return self.replace_alias_tvars(override, all_vars, [AnyType()] * exp_len,
t.line, t.column)
return replace_alias_tvars(override, all_vars, [AnyType()] * exp_len,
t.line, t.column)
if exp_len == 0 and act_len == 0:
return override
if act_len != exp_len:
self.fail('Bad number of arguments for type alias, expected: %s, given: %s'
% (exp_len, act_len), t)
return t
return self.replace_alias_tvars(override, all_vars, an_args, t.line, t.column)
return replace_alias_tvars(override, all_vars, an_args, t.line, t.column)
elif not isinstance(sym.node, TypeInfo):
name = sym.fullname
if name is None:
Expand Down Expand Up @@ -280,26 +280,6 @@ def visit_unbound_type(self, t: UnboundType) -> Type:
else:
return AnyType()

def replace_alias_tvars(self, tp: Type, vars: List[str], subs: List[Type],
newline: int, newcolumn: int) -> Type:
"""Replace type variables in a generic type alias tp with substitutions subs
resetting context. Length of subs should be already checked.
"""
typ_args = get_typ_args(tp)
new_args = typ_args[:]
for i, arg in enumerate(typ_args):
if isinstance(arg, (UnboundType, TypeVarType)):
tvar = arg.name
else:
tvar = None
if tvar and tvar in vars:
# Perform actual substitution...
new_args[i] = subs[vars.index(tvar)]
else:
# ...recursively, if needed.
new_args[i] = self.replace_alias_tvars(arg, vars, subs, newline, newcolumn)
return set_typ_args(tp, new_args, newline, newcolumn)

def visit_any(self, t: AnyType) -> Type:
return t

Expand Down Expand Up @@ -688,6 +668,27 @@ def visit_type_type(self, t: TypeType) -> None:
TypeVarList = List[Tuple[str, TypeVarExpr]]


def replace_alias_tvars(tp: Type, vars: List[str], subs: List[Type],
newline: int, newcolumn: int) -> Type:
"""Replace type variables in a generic type alias tp with substitutions subs
resetting context. Length of subs should be already checked.
"""
typ_args = get_typ_args(tp)
new_args = typ_args[:]
for i, arg in enumerate(typ_args):
if isinstance(arg, (UnboundType, TypeVarType)):
tvar = arg.name
else:
tvar = None
if tvar and tvar in vars:
# Perform actual substitution...
new_args[i] = subs[vars.index(tvar)]
else:
# ...recursively, if needed.
new_args[i] = replace_alias_tvars(arg, vars, subs, newline, newcolumn)
return set_typ_args(tp, new_args, newline, newcolumn)


def remove_dups(tvars: Iterable[T]) -> List[T]:
# Get unique elements in order of appearance
all_tvars = set() # type: Set[T]
Expand Down
20 changes: 20 additions & 0 deletions test-data/unit/check-generics.test
Original file line number Diff line number Diff line change
Expand Up @@ -1047,6 +1047,26 @@ reveal_type(BadGenList()) # E: Revealed type is 'builtins.list[Any]'
[builtins fixtures/list.pyi]
[out]

[case testImportedTypeAliasInRuntimeContext]
from m import Alias

n = Alias[int]([1])
reveal_type(n) # E: Revealed type is 'm.Node[builtins.list*[builtins.int]]'
bad = Alias[str]([1]) # E: List item 0 has incompatible type "int"

n2 = Alias([1]) # Same as Node[List[Any]]
reveal_type(n2) # E: Revealed type is 'm.Node[builtins.list*[Any]]'
[file m.py]
from typing import TypeVar, Generic, List
T = TypeVar('T')

class Node(Generic[T]):
def __init__(self, x: T) -> None:
self.x = x

Alias = Node[List[T]]
[builtins fixtures/list.pyi]
[out]

-- Simplified declaration of generics
-- ----------------------------------
Expand Down
0