Skip to content

Commit

Permalink
Allow subscripted aliases at function scope (#4000)
Browse files Browse the repository at this point in the history
Fixes #3145.

This PR allows subscripted type aliases at function scope, as discussed in 
#3145. This will simplify the rules and have other pluses (apart from fixing 
the actual issue).

In addition, I prohibit reuse of bound type variables to define generic 
aliases. Situations like this were always ambiguous.
  • Loading branch information
ilevkivskyi authored and JukkaL committed Oct 10, 2017
1 parent ada49c2 commit 3e49ef9
Show file tree
Hide file tree
Showing 7 changed files with 133 additions and 73 deletions.
2 changes: 1 addition & 1 deletion mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -652,7 +652,7 @@ def analyze_type_type_callee(self, item: Type, context: Context) -> Type:
res = type_object_type(item.type, self.named_type)
if isinstance(res, CallableType):
res = res.copy_modified(from_type_type=True)
return res
return expand_type_by_instance(res, item)
if isinstance(item, UnionType):
return UnionType([self.analyze_type_type_callee(item, context)
for item in item.relevant_items()], item.line)
Expand Down
104 changes: 58 additions & 46 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -1725,11 +1725,10 @@ def alias_fallback(self, tp: Type) -> Instance:
return Instance(fb_info, [])

def analyze_alias(self, rvalue: Expression,
allow_unnormalized: bool) -> Tuple[Optional[Type], List[str]]:
warn_bound_tvar: bool = False) -> Tuple[Optional[Type], List[str]]:
"""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].
"""
dynamic = bool(self.function_stack and self.function_stack[-1].is_dynamic())
global_scope = not self.type and not self.function_stack
Expand All @@ -1744,7 +1743,8 @@ def analyze_alias(self, rvalue: Expression,
self.is_typeshed_stub_file,
allow_unnormalized=True,
in_dynamic_func=dynamic,
global_scope=global_scope)
global_scope=global_scope,
warn_bound_tvar=warn_bound_tvar)
if res:
alias_tvars = [name for (name, _) in
res.accept(TypeVariableQuery(self.lookup_qualified, self.tvar_scope))]
Expand All @@ -1758,50 +1758,62 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> None:
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):
if len(s.lvalues) > 1 or not isinstance(lvalue, NameExpr):
# First rule: Only simple assignments like Alias = ... create aliases.
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):
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):
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, CallExpr)):
# 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
if s.type:
# Second rule: Explicit type (cls: Type[A] = A) always creates variable, not alias.
return
non_global_scope = self.type or self.is_func_scope()
if isinstance(s.rvalue, NameExpr) and non_global_scope and lvalue.is_def:
# Third rule: Non-subscripted right hand side creates a variable
# at class and function scopes. For example:
#
# class Model:
# ...
# class C:
# model = Model # this is automatically a variable with type 'Type[Model]'
#
# without this rule, this typical use case will require a lot of explicit
# annotations (see the second rule).
return
rvalue = s.rvalue
res, alias_tvars = self.analyze_alias(rvalue, warn_bound_tvar=True)
if not res:
return
node = self.lookup(lvalue.name, lvalue)
if not lvalue.is_def:
# Type aliases can't be re-defined.
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):
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, CallExpr)):
# 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 @@ -3366,7 +3378,7 @@ 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, alias_tvars = self.analyze_alias(expr, allow_unnormalized=self.is_stub_file)
res, alias_tvars = self.analyze_alias(expr)
expr.analyzed = TypeAliasExpr(res, alias_tvars, fallback=self.alias_fallback(res),
in_runtime=True)
expr.analyzed.line = expr.line
Expand Down
15 changes: 11 additions & 4 deletions mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ def analyze_type_alias(node: Expression,
is_typeshed_stub: bool,
allow_unnormalized: bool = False,
in_dynamic_func: bool = False,
global_scope: bool = True) -> Optional[Type]:
global_scope: bool = True,
warn_bound_tvar: bool = False) -> Optional[Type]:
"""Return type if node is valid as a type alias rvalue.
Return None otherwise. 'node' must have been semantically analyzed.
Expand Down Expand Up @@ -117,7 +118,7 @@ def analyze_type_alias(node: Expression,
return None
analyzer = TypeAnalyser(lookup_func, lookup_fqn_func, tvar_scope, fail_func, note_func,
plugin, options, is_typeshed_stub, aliasing=True,
allow_unnormalized=allow_unnormalized)
allow_unnormalized=allow_unnormalized, warn_bound_tvar=warn_bound_tvar)
analyzer.in_dynamic_func = in_dynamic_func
analyzer.global_scope = global_scope
return type.accept(analyzer)
Expand Down Expand Up @@ -154,7 +155,8 @@ def __init__(self,
aliasing: bool = False,
allow_tuple_literal: bool = False,
allow_unnormalized: bool = False,
third_pass: bool = False) -> None:
third_pass: bool = False,
warn_bound_tvar: bool = False) -> None:
self.lookup = lookup_func
self.lookup_fqn_func = lookup_fqn_func
self.fail_func = fail_func
Expand All @@ -168,6 +170,7 @@ def __init__(self,
self.plugin = plugin
self.options = options
self.is_typeshed_stub = is_typeshed_stub
self.warn_bound_tvar = warn_bound_tvar
self.third_pass = third_pass

def visit_unbound_type(self, t: UnboundType) -> Type:
Expand All @@ -194,7 +197,11 @@ def visit_unbound_type(self, t: UnboundType) -> Type:
tvar_def = self.tvar_scope.get_binding(sym)
else:
tvar_def = None
if sym.kind == TVAR and tvar_def is not None:
if self.warn_bound_tvar and sym.kind == TVAR and tvar_def is not None:
self.fail('Can\'t use bound type variable "{}"'
' to define generic alias'.format(t.name), t)
return AnyType(TypeOfAny.from_error)
elif sym.kind == TVAR and tvar_def is not None:
if len(t.args) > 0:
self.fail('Type variable "{}" used with arguments'.format(
t.name), t)
Expand Down
8 changes: 4 additions & 4 deletions test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -2119,17 +2119,17 @@ reveal_type(C().aa) # E: Revealed type is '__main__.A'
[out]

[case testClassValuedAttributesGeneric]
from typing import Generic, TypeVar
from typing import Generic, TypeVar, Type
T = TypeVar('T')

class A(Generic[T]):
def __init__(self, x: T) -> None:
self.x = x
class B(Generic[T]):
a = A[T]
a: Type[A[T]] = A

reveal_type(B[int]().a) # E: Revealed type is 'def (x: builtins.int*) -> __main__.A[builtins.int*]'
B[int]().a('hi') # E: Argument 1 has incompatible type "str"; expected "int"
reveal_type(B[int]().a) # E: Revealed type is 'Type[__main__.A[builtins.int*]]'
B[int]().a('hi') # E: Argument 1 to "A" has incompatible type "str"; expected "int"

class C(Generic[T]):
a = A
Expand Down
26 changes: 13 additions & 13 deletions test-data/unit/check-isinstance.test
Original file line number Diff line number Diff line change
Expand Up @@ -1447,16 +1447,16 @@ def f(x: Union[Type[int], Type[str], Type[List]]) -> None:
x()[1] # E: Value of type "Union[int, str]" is not indexable
else:
reveal_type(x) # E: Revealed type is 'Type[builtins.list[Any]]'
reveal_type(x()) # E: Revealed type is 'builtins.list[<nothing>]'
reveal_type(x()) # E: Revealed type is 'builtins.list[Any]'
x()[1]
reveal_type(x) # E: Revealed type is 'Union[Type[builtins.int], Type[builtins.str], Type[builtins.list[Any]]]'
reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[<nothing>]]'
reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[Any]]'
if issubclass(x, (str, (list,))):
reveal_type(x) # E: Revealed type is 'Union[Type[builtins.str], Type[builtins.list[Any]]]'
reveal_type(x()) # E: Revealed type is 'Union[builtins.str, builtins.list[<nothing>]]'
reveal_type(x()) # E: Revealed type is 'Union[builtins.str, builtins.list[Any]]'
x()[1]
reveal_type(x) # E: Revealed type is 'Union[Type[builtins.int], Type[builtins.str], Type[builtins.list[Any]]]'
reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[<nothing>]]'
reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[Any]]'
[builtins fixtures/isinstancelist.pyi]

[case testIssubclasDestructuringUnions2]
Expand All @@ -1469,40 +1469,40 @@ def f(x: Type[Union[int, str, List]]) -> None:
x()[1] # E: Value of type "Union[int, str]" is not indexable
else:
reveal_type(x) # E: Revealed type is 'Type[builtins.list[Any]]'
reveal_type(x()) # E: Revealed type is 'builtins.list[<nothing>]'
reveal_type(x()) # E: Revealed type is 'builtins.list[Any]'
x()[1]
reveal_type(x) # E: Revealed type is 'Union[Type[builtins.int], Type[builtins.str], Type[builtins.list[Any]]]'
reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[<nothing>]]'
reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[Any]]'
if issubclass(x, (str, (list,))):
reveal_type(x) # E: Revealed type is 'Union[Type[builtins.str], Type[builtins.list[Any]]]'
reveal_type(x()) # E: Revealed type is 'Union[builtins.str, builtins.list[<nothing>]]'
reveal_type(x()) # E: Revealed type is 'Union[builtins.str, builtins.list[Any]]'
x()[1]
reveal_type(x) # E: Revealed type is 'Union[Type[builtins.int], Type[builtins.str], Type[builtins.list[Any]]]'
reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[<nothing>]]'
reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[Any]]'
[builtins fixtures/isinstancelist.pyi]

[case testIssubclasDestructuringUnions3]
from typing import Union, List, Tuple, Dict, Type

def f(x: Type[Union[int, str, List]]) -> None:
reveal_type(x) # E: Revealed type is 'Union[Type[builtins.int], Type[builtins.str], Type[builtins.list[Any]]]'
reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[<nothing>]]'
reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[Any]]'
if issubclass(x, (str, (int,))):
reveal_type(x) # E: Revealed type is 'Union[Type[builtins.int], Type[builtins.str]]'
reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str]'
x()[1] # E: Value of type "Union[int, str]" is not indexable
else:
reveal_type(x) # E: Revealed type is 'Type[builtins.list[Any]]'
reveal_type(x()) # E: Revealed type is 'builtins.list[<nothing>]'
reveal_type(x()) # E: Revealed type is 'builtins.list[Any]'
x()[1]
reveal_type(x) # E: Revealed type is 'Union[Type[builtins.int], Type[builtins.str], Type[builtins.list[Any]]]'
reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[<nothing>]]'
reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[Any]]'
if issubclass(x, (str, (list,))):
reveal_type(x) # E: Revealed type is 'Union[Type[builtins.str], Type[builtins.list[Any]]]'
reveal_type(x()) # E: Revealed type is 'Union[builtins.str, builtins.list[<nothing>]]'
reveal_type(x()) # E: Revealed type is 'Union[builtins.str, builtins.list[Any]]'
x()[1]
reveal_type(x) # E: Revealed type is 'Union[Type[builtins.int], Type[builtins.str], Type[builtins.list[Any]]]'
reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[<nothing>]]'
reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[Any]]'
[builtins fixtures/isinstancelist.pyi]

[case testIssubclass]
Expand Down
42 changes: 42 additions & 0 deletions test-data/unit/check-type-aliases.test
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,48 @@ GenAlias = Sequence[T]
def fun(x: Alias) -> GenAlias[int]: pass
[out]

[case testCorrectQualifiedAliasesAlsoInFunctions]
from typing import TypeVar, Generic
T = TypeVar('T')
S = TypeVar('S')

class X(Generic[T]):
A = X[S]
def f(self) -> X[T]:
pass

a: X[T]
b: A = a
c: A[T] = a
d: A[int] = a # E: Incompatible types in assignment (expression has type "X[T]", variable has type "X[int]")

def g(self) -> None:
a: X[T]
b: X.A = a
c: X.A[T] = a
d: X.A[int] = a # E: Incompatible types in assignment (expression has type "X[T]", variable has type "X[int]")

def g(arg: X[int]) -> None:
p: X[int] = arg.f()
q: X.A = arg.f()
r: X.A[str] = arg.f() # E: Incompatible types in assignment (expression has type "X[int]", variable has type "X[str]")
[out]

[case testProhibitBoundTypeVariableReuseForAliases]
from typing import TypeVar, Generic, List
T = TypeVar('T')
class C(Generic[T]):
A = List[T] # E: Can't use bound type variable "T" to define generic alias

x: C.A
reveal_type(x) # E: Revealed type is 'builtins.list[Any]'

def f(x: T) -> T:
A = List[T] # E: Can't use bound type variable "T" to define generic alias
return x
[builtins fixtures/list.pyi]
[out]

[case testTypeAliasInBuiltins]
def f(x: bytes): pass
bytes
Expand Down
9 changes: 4 additions & 5 deletions test-data/unit/check-typevar-values.test
Original file line number Diff line number Diff line change
Expand Up @@ -556,16 +556,15 @@ def outer(x: T) -> T:

[case testClassMemberTypeVarInFunctionBody]
from typing import TypeVar, List
S = TypeVar('S')
class C:
T = TypeVar('T', bound=int)
def f(self, x: T) -> T:
L = List[C.T] # this creates a variable, not an alias
reveal_type(L) # E: Revealed type is 'Overload(def () -> builtins.list[T`-1], def (x: typing.Iterable[T`-1]) -> builtins.list[T`-1])'
y: C.T = x
L().append(x)
L = List[S]
y: L[C.T] = [x]
C.T # E: Type variable "C.T" cannot be used as an expression
A = C.T # E: Type variable "C.T" cannot be used as an expression
return L()[0]
return y[0]

[builtins fixtures/list.pyi]

Expand Down

0 comments on commit 3e49ef9

Please sign in to comment.