Skip to content

Commit 3e49ef9

Browse files
ilevkivskyiJukkaL
authored andcommitted
Allow subscripted aliases at function scope (#4000)
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.
1 parent ada49c2 commit 3e49ef9

7 files changed

+133
-73
lines changed

mypy/checkexpr.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -652,7 +652,7 @@ def analyze_type_type_callee(self, item: Type, context: Context) -> Type:
652652
res = type_object_type(item.type, self.named_type)
653653
if isinstance(res, CallableType):
654654
res = res.copy_modified(from_type_type=True)
655-
return res
655+
return expand_type_by_instance(res, item)
656656
if isinstance(item, UnionType):
657657
return UnionType([self.analyze_type_type_callee(item, context)
658658
for item in item.relevant_items()], item.line)

mypy/semanal.py

Lines changed: 58 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1725,11 +1725,10 @@ def alias_fallback(self, tp: Type) -> Instance:
17251725
return Instance(fb_info, [])
17261726

17271727
def analyze_alias(self, rvalue: Expression,
1728-
allow_unnormalized: bool) -> Tuple[Optional[Type], List[str]]:
1728+
warn_bound_tvar: bool = False) -> Tuple[Optional[Type], List[str]]:
17291729
"""Check if 'rvalue' represents a valid type allowed for aliasing
17301730
(e.g. not a type variable). If yes, return the corresponding type and a list of
17311731
qualified type variable names for generic aliases.
1732-
If 'allow_unnormalized' is True, allow types like builtins.list[T].
17331732
"""
17341733
dynamic = bool(self.function_stack and self.function_stack[-1].is_dynamic())
17351734
global_scope = not self.type and not self.function_stack
@@ -1744,7 +1743,8 @@ def analyze_alias(self, rvalue: Expression,
17441743
self.is_typeshed_stub_file,
17451744
allow_unnormalized=True,
17461745
in_dynamic_func=dynamic,
1747-
global_scope=global_scope)
1746+
global_scope=global_scope,
1747+
warn_bound_tvar=warn_bound_tvar)
17481748
if res:
17491749
alias_tvars = [name for (name, _) in
17501750
res.accept(TypeVariableQuery(self.lookup_qualified, self.tvar_scope))]
@@ -1758,50 +1758,62 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> None:
17581758
For subscripted (including generic) aliases the resulting types are stored
17591759
in rvalue.analyzed.
17601760
"""
1761-
# Type aliases are created only at module scope and class scope (for subscripted types),
1762-
# at function scope assignments always create local variables with type object types.
17631761
lvalue = s.lvalues[0]
1764-
if not isinstance(lvalue, NameExpr):
1762+
if len(s.lvalues) > 1 or not isinstance(lvalue, NameExpr):
1763+
# First rule: Only simple assignments like Alias = ... create aliases.
17651764
return
1766-
if (len(s.lvalues) == 1 and not self.is_func_scope() and
1767-
not (self.type and isinstance(s.rvalue, NameExpr) and lvalue.is_def)
1768-
and not s.type):
1769-
rvalue = s.rvalue
1770-
res, alias_tvars = self.analyze_alias(rvalue, allow_unnormalized=True)
1771-
if not res:
1772-
return
1773-
node = self.lookup(lvalue.name, lvalue)
1774-
if not lvalue.is_def:
1775-
# Only a definition can create a type alias, not regular assignment.
1776-
if node and node.kind == TYPE_ALIAS or isinstance(node.node, TypeInfo):
1777-
self.fail('Cannot assign multiple types to name "{}"'
1778-
' without an explicit "Type[...]" annotation'
1779-
.format(lvalue.name), lvalue)
1780-
return
1781-
check_for_explicit_any(res, self.options, self.is_typeshed_stub_file, self.msg,
1782-
context=s)
1783-
# when this type alias gets "inlined", the Any is not explicit anymore,
1784-
# so we need to replace it with non-explicit Anys
1785-
res = make_any_non_explicit(res)
1786-
if isinstance(res, Instance) and not res.args and isinstance(rvalue, RefExpr):
1787-
# For simple (on-generic) aliases we use aliasing TypeInfo's
1788-
# to allow using them in runtime context where it makes sense.
1789-
node.node = res.type
1790-
if isinstance(rvalue, RefExpr):
1791-
sym = self.lookup_type_node(rvalue)
1792-
if sym:
1793-
node.normalized = sym.normalized
1794-
return
1795-
node.kind = TYPE_ALIAS
1796-
node.type_override = res
1797-
node.alias_tvars = alias_tvars
1798-
if isinstance(rvalue, (IndexExpr, CallExpr)):
1799-
# We only need this for subscripted aliases, since simple aliases
1800-
# are already processed using aliasing TypeInfo's above.
1801-
rvalue.analyzed = TypeAliasExpr(res, node.alias_tvars,
1802-
fallback=self.alias_fallback(res))
1803-
rvalue.analyzed.line = rvalue.line
1804-
rvalue.analyzed.column = rvalue.column
1765+
if s.type:
1766+
# Second rule: Explicit type (cls: Type[A] = A) always creates variable, not alias.
1767+
return
1768+
non_global_scope = self.type or self.is_func_scope()
1769+
if isinstance(s.rvalue, NameExpr) and non_global_scope and lvalue.is_def:
1770+
# Third rule: Non-subscripted right hand side creates a variable
1771+
# at class and function scopes. For example:
1772+
#
1773+
# class Model:
1774+
# ...
1775+
# class C:
1776+
# model = Model # this is automatically a variable with type 'Type[Model]'
1777+
#
1778+
# without this rule, this typical use case will require a lot of explicit
1779+
# annotations (see the second rule).
1780+
return
1781+
rvalue = s.rvalue
1782+
res, alias_tvars = self.analyze_alias(rvalue, warn_bound_tvar=True)
1783+
if not res:
1784+
return
1785+
node = self.lookup(lvalue.name, lvalue)
1786+
if not lvalue.is_def:
1787+
# Type aliases can't be re-defined.
1788+
if node and (node.kind == TYPE_ALIAS or isinstance(node.node, TypeInfo)):
1789+
self.fail('Cannot assign multiple types to name "{}"'
1790+
' without an explicit "Type[...]" annotation'
1791+
.format(lvalue.name), lvalue)
1792+
return
1793+
check_for_explicit_any(res, self.options, self.is_typeshed_stub_file, self.msg,
1794+
context=s)
1795+
# when this type alias gets "inlined", the Any is not explicit anymore,
1796+
# so we need to replace it with non-explicit Anys
1797+
res = make_any_non_explicit(res)
1798+
if isinstance(res, Instance) and not res.args and isinstance(rvalue, RefExpr):
1799+
# For simple (on-generic) aliases we use aliasing TypeInfo's
1800+
# to allow using them in runtime context where it makes sense.
1801+
node.node = res.type
1802+
if isinstance(rvalue, RefExpr):
1803+
sym = self.lookup_type_node(rvalue)
1804+
if sym:
1805+
node.normalized = sym.normalized
1806+
return
1807+
node.kind = TYPE_ALIAS
1808+
node.type_override = res
1809+
node.alias_tvars = alias_tvars
1810+
if isinstance(rvalue, (IndexExpr, CallExpr)):
1811+
# We only need this for subscripted aliases, since simple aliases
1812+
# are already processed using aliasing TypeInfo's above.
1813+
rvalue.analyzed = TypeAliasExpr(res, node.alias_tvars,
1814+
fallback=self.alias_fallback(res))
1815+
rvalue.analyzed.line = rvalue.line
1816+
rvalue.analyzed.column = rvalue.column
18051817

18061818
def analyze_lvalue(self, lval: Lvalue, nested: bool = False,
18071819
add_global: bool = False,
@@ -3366,7 +3378,7 @@ def visit_index_expr(self, expr: IndexExpr) -> None:
33663378
elif isinstance(expr.base, RefExpr) and expr.base.kind == TYPE_ALIAS:
33673379
# Special form -- subscripting a generic type alias.
33683380
# Perform the type substitution and create a new alias.
3369-
res, alias_tvars = self.analyze_alias(expr, allow_unnormalized=self.is_stub_file)
3381+
res, alias_tvars = self.analyze_alias(expr)
33703382
expr.analyzed = TypeAliasExpr(res, alias_tvars, fallback=self.alias_fallback(res),
33713383
in_runtime=True)
33723384
expr.analyzed.line = expr.line

mypy/typeanal.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ def analyze_type_alias(node: Expression,
6363
is_typeshed_stub: bool,
6464
allow_unnormalized: bool = False,
6565
in_dynamic_func: bool = False,
66-
global_scope: bool = True) -> Optional[Type]:
66+
global_scope: bool = True,
67+
warn_bound_tvar: bool = False) -> Optional[Type]:
6768
"""Return type if node is valid as a type alias rvalue.
6869
6970
Return None otherwise. 'node' must have been semantically analyzed.
@@ -117,7 +118,7 @@ def analyze_type_alias(node: Expression,
117118
return None
118119
analyzer = TypeAnalyser(lookup_func, lookup_fqn_func, tvar_scope, fail_func, note_func,
119120
plugin, options, is_typeshed_stub, aliasing=True,
120-
allow_unnormalized=allow_unnormalized)
121+
allow_unnormalized=allow_unnormalized, warn_bound_tvar=warn_bound_tvar)
121122
analyzer.in_dynamic_func = in_dynamic_func
122123
analyzer.global_scope = global_scope
123124
return type.accept(analyzer)
@@ -154,7 +155,8 @@ def __init__(self,
154155
aliasing: bool = False,
155156
allow_tuple_literal: bool = False,
156157
allow_unnormalized: bool = False,
157-
third_pass: bool = False) -> None:
158+
third_pass: bool = False,
159+
warn_bound_tvar: bool = False) -> None:
158160
self.lookup = lookup_func
159161
self.lookup_fqn_func = lookup_fqn_func
160162
self.fail_func = fail_func
@@ -168,6 +170,7 @@ def __init__(self,
168170
self.plugin = plugin
169171
self.options = options
170172
self.is_typeshed_stub = is_typeshed_stub
173+
self.warn_bound_tvar = warn_bound_tvar
171174
self.third_pass = third_pass
172175

173176
def visit_unbound_type(self, t: UnboundType) -> Type:
@@ -194,7 +197,11 @@ def visit_unbound_type(self, t: UnboundType) -> Type:
194197
tvar_def = self.tvar_scope.get_binding(sym)
195198
else:
196199
tvar_def = None
197-
if sym.kind == TVAR and tvar_def is not None:
200+
if self.warn_bound_tvar and sym.kind == TVAR and tvar_def is not None:
201+
self.fail('Can\'t use bound type variable "{}"'
202+
' to define generic alias'.format(t.name), t)
203+
return AnyType(TypeOfAny.from_error)
204+
elif sym.kind == TVAR and tvar_def is not None:
198205
if len(t.args) > 0:
199206
self.fail('Type variable "{}" used with arguments'.format(
200207
t.name), t)

test-data/unit/check-classes.test

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2119,17 +2119,17 @@ reveal_type(C().aa) # E: Revealed type is '__main__.A'
21192119
[out]
21202120

21212121
[case testClassValuedAttributesGeneric]
2122-
from typing import Generic, TypeVar
2122+
from typing import Generic, TypeVar, Type
21232123
T = TypeVar('T')
21242124

21252125
class A(Generic[T]):
21262126
def __init__(self, x: T) -> None:
21272127
self.x = x
21282128
class B(Generic[T]):
2129-
a = A[T]
2129+
a: Type[A[T]] = A
21302130

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

21342134
class C(Generic[T]):
21352135
a = A

test-data/unit/check-isinstance.test

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1447,16 +1447,16 @@ def f(x: Union[Type[int], Type[str], Type[List]]) -> None:
14471447
x()[1] # E: Value of type "Union[int, str]" is not indexable
14481448
else:
14491449
reveal_type(x) # E: Revealed type is 'Type[builtins.list[Any]]'
1450-
reveal_type(x()) # E: Revealed type is 'builtins.list[<nothing>]'
1450+
reveal_type(x()) # E: Revealed type is 'builtins.list[Any]'
14511451
x()[1]
14521452
reveal_type(x) # E: Revealed type is 'Union[Type[builtins.int], Type[builtins.str], Type[builtins.list[Any]]]'
1453-
reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[<nothing>]]'
1453+
reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[Any]]'
14541454
if issubclass(x, (str, (list,))):
14551455
reveal_type(x) # E: Revealed type is 'Union[Type[builtins.str], Type[builtins.list[Any]]]'
1456-
reveal_type(x()) # E: Revealed type is 'Union[builtins.str, builtins.list[<nothing>]]'
1456+
reveal_type(x()) # E: Revealed type is 'Union[builtins.str, builtins.list[Any]]'
14571457
x()[1]
14581458
reveal_type(x) # E: Revealed type is 'Union[Type[builtins.int], Type[builtins.str], Type[builtins.list[Any]]]'
1459-
reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[<nothing>]]'
1459+
reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[Any]]'
14601460
[builtins fixtures/isinstancelist.pyi]
14611461

14621462
[case testIssubclasDestructuringUnions2]
@@ -1469,40 +1469,40 @@ def f(x: Type[Union[int, str, List]]) -> None:
14691469
x()[1] # E: Value of type "Union[int, str]" is not indexable
14701470
else:
14711471
reveal_type(x) # E: Revealed type is 'Type[builtins.list[Any]]'
1472-
reveal_type(x()) # E: Revealed type is 'builtins.list[<nothing>]'
1472+
reveal_type(x()) # E: Revealed type is 'builtins.list[Any]'
14731473
x()[1]
14741474
reveal_type(x) # E: Revealed type is 'Union[Type[builtins.int], Type[builtins.str], Type[builtins.list[Any]]]'
1475-
reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[<nothing>]]'
1475+
reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[Any]]'
14761476
if issubclass(x, (str, (list,))):
14771477
reveal_type(x) # E: Revealed type is 'Union[Type[builtins.str], Type[builtins.list[Any]]]'
1478-
reveal_type(x()) # E: Revealed type is 'Union[builtins.str, builtins.list[<nothing>]]'
1478+
reveal_type(x()) # E: Revealed type is 'Union[builtins.str, builtins.list[Any]]'
14791479
x()[1]
14801480
reveal_type(x) # E: Revealed type is 'Union[Type[builtins.int], Type[builtins.str], Type[builtins.list[Any]]]'
1481-
reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[<nothing>]]'
1481+
reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[Any]]'
14821482
[builtins fixtures/isinstancelist.pyi]
14831483

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

14871487
def f(x: Type[Union[int, str, List]]) -> None:
14881488
reveal_type(x) # E: Revealed type is 'Union[Type[builtins.int], Type[builtins.str], Type[builtins.list[Any]]]'
1489-
reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[<nothing>]]'
1489+
reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[Any]]'
14901490
if issubclass(x, (str, (int,))):
14911491
reveal_type(x) # E: Revealed type is 'Union[Type[builtins.int], Type[builtins.str]]'
14921492
reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str]'
14931493
x()[1] # E: Value of type "Union[int, str]" is not indexable
14941494
else:
14951495
reveal_type(x) # E: Revealed type is 'Type[builtins.list[Any]]'
1496-
reveal_type(x()) # E: Revealed type is 'builtins.list[<nothing>]'
1496+
reveal_type(x()) # E: Revealed type is 'builtins.list[Any]'
14971497
x()[1]
14981498
reveal_type(x) # E: Revealed type is 'Union[Type[builtins.int], Type[builtins.str], Type[builtins.list[Any]]]'
1499-
reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[<nothing>]]'
1499+
reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[Any]]'
15001500
if issubclass(x, (str, (list,))):
15011501
reveal_type(x) # E: Revealed type is 'Union[Type[builtins.str], Type[builtins.list[Any]]]'
1502-
reveal_type(x()) # E: Revealed type is 'Union[builtins.str, builtins.list[<nothing>]]'
1502+
reveal_type(x()) # E: Revealed type is 'Union[builtins.str, builtins.list[Any]]'
15031503
x()[1]
15041504
reveal_type(x) # E: Revealed type is 'Union[Type[builtins.int], Type[builtins.str], Type[builtins.list[Any]]]'
1505-
reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[<nothing>]]'
1505+
reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[Any]]'
15061506
[builtins fixtures/isinstancelist.pyi]
15071507

15081508
[case testIssubclass]

test-data/unit/check-type-aliases.test

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,48 @@ GenAlias = Sequence[T]
9696
def fun(x: Alias) -> GenAlias[int]: pass
9797
[out]
9898

99+
[case testCorrectQualifiedAliasesAlsoInFunctions]
100+
from typing import TypeVar, Generic
101+
T = TypeVar('T')
102+
S = TypeVar('S')
103+
104+
class X(Generic[T]):
105+
A = X[S]
106+
def f(self) -> X[T]:
107+
pass
108+
109+
a: X[T]
110+
b: A = a
111+
c: A[T] = a
112+
d: A[int] = a # E: Incompatible types in assignment (expression has type "X[T]", variable has type "X[int]")
113+
114+
def g(self) -> None:
115+
a: X[T]
116+
b: X.A = a
117+
c: X.A[T] = a
118+
d: X.A[int] = a # E: Incompatible types in assignment (expression has type "X[T]", variable has type "X[int]")
119+
120+
def g(arg: X[int]) -> None:
121+
p: X[int] = arg.f()
122+
q: X.A = arg.f()
123+
r: X.A[str] = arg.f() # E: Incompatible types in assignment (expression has type "X[int]", variable has type "X[str]")
124+
[out]
125+
126+
[case testProhibitBoundTypeVariableReuseForAliases]
127+
from typing import TypeVar, Generic, List
128+
T = TypeVar('T')
129+
class C(Generic[T]):
130+
A = List[T] # E: Can't use bound type variable "T" to define generic alias
131+
132+
x: C.A
133+
reveal_type(x) # E: Revealed type is 'builtins.list[Any]'
134+
135+
def f(x: T) -> T:
136+
A = List[T] # E: Can't use bound type variable "T" to define generic alias
137+
return x
138+
[builtins fixtures/list.pyi]
139+
[out]
140+
99141
[case testTypeAliasInBuiltins]
100142
def f(x: bytes): pass
101143
bytes

test-data/unit/check-typevar-values.test

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -556,16 +556,15 @@ def outer(x: T) -> T:
556556

557557
[case testClassMemberTypeVarInFunctionBody]
558558
from typing import TypeVar, List
559+
S = TypeVar('S')
559560
class C:
560561
T = TypeVar('T', bound=int)
561562
def f(self, x: T) -> T:
562-
L = List[C.T] # this creates a variable, not an alias
563-
reveal_type(L) # E: Revealed type is 'Overload(def () -> builtins.list[T`-1], def (x: typing.Iterable[T`-1]) -> builtins.list[T`-1])'
564-
y: C.T = x
565-
L().append(x)
563+
L = List[S]
564+
y: L[C.T] = [x]
566565
C.T # E: Type variable "C.T" cannot be used as an expression
567566
A = C.T # E: Type variable "C.T" cannot be used as an expression
568-
return L()[0]
567+
return y[0]
569568

570569
[builtins fixtures/list.pyi]
571570

0 commit comments

Comments
 (0)