Skip to content

Commit 4ca67f9

Browse files
ilevkivskyigvanrossum
authored andcommitted
Prohibit list[int], etc (those fail at runtime) (#2869)
Fixes #2428 All of the following are now allowed, but fail at runtime: ``` list[int] dict[int, str] set[int] tuple[int] frozenset[int] enumerate[int] collections.defaultdict[int, str] collections.Counter[str] collections.ChainMap[str, str] ``` I prohibit those by simply tracking whether a corresponding symbol table node was normalized or not. @gvanrossum I make an exclusion for stubs, because a have found dozens of places where this is used in typeshed, if you think that it also makes sense to prohibit this in stubs, then I will make an additional PR to typeshed.
1 parent 9142a47 commit 4ca67f9

File tree

6 files changed

+119
-15
lines changed

6 files changed

+119
-15
lines changed

mypy/build.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
from os.path import dirname, basename
2222

2323
from typing import (AbstractSet, Dict, Iterable, Iterator, List,
24-
NamedTuple, Optional, Set, Tuple, Union)
24+
NamedTuple, Optional, Set, Tuple, Union, TYPE_CHECKING)
25+
if TYPE_CHECKING:
26+
from typing import Deque
2527

2628
from mypy.nodes import (MypyFile, Node, ImportBase, Import, ImportFrom, ImportAll)
2729
from mypy.semanal import FirstPass, SemanticAnalyzer, ThirdPass
@@ -1618,7 +1620,7 @@ def load_graph(sources: List[BuildSource], manager: BuildManager) -> Graph:
16181620
# The deque is used to implement breadth-first traversal.
16191621
# TODO: Consider whether to go depth-first instead. This may
16201622
# affect the order in which we process files within import cycles.
1621-
new = collections.deque() # type: collections.deque[State]
1623+
new = collections.deque() # type: Deque[State]
16221624
entry_points = set() # type: Set[str]
16231625
# Seed the graph with the initial root sources.
16241626
for bs in sources:

mypy/nodes.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ def get_column(self) -> int: pass
8383
'typing.List': '__builtins__.list',
8484
'typing.Dict': '__builtins__.dict',
8585
'typing.Set': '__builtins__.set',
86+
'typing.FrozenSet': '__builtins__.frozenset',
8687
}
8788

8889
reverse_type_aliases = dict((name.replace('__builtins__', 'builtins'), alias)
@@ -95,6 +96,15 @@ def get_column(self) -> int: pass
9596
'typing.Deque': '__mypy_collections__.deque',
9697
}
9798

99+
reverse_collection_aliases = dict((name.replace('__mypy_collections__', 'collections'), alias)
100+
for alias, name in
101+
collections_type_aliases.items()) # type: Dict[str, str]
102+
103+
nongen_builtins = {'builtins.tuple': 'typing.Tuple',
104+
'builtins.enumerate': ''}
105+
nongen_builtins.update(reverse_type_aliases)
106+
nongen_builtins.update(reverse_collection_aliases)
107+
98108

99109
# See [Note Literals and literal_hash] below
100110
Key = tuple
@@ -2160,17 +2170,20 @@ class SymbolTableNode:
21602170
# For deserialized MODULE_REF nodes, the referenced module name;
21612171
# for other nodes, optionally the name of the referenced object.
21622172
cross_ref = None # type: Optional[str]
2173+
# Was this node created by normalіze_type_alias?
2174+
normalized = False # type: bool
21632175

21642176
def __init__(self, kind: int, node: Optional[SymbolNode], mod_id: str = None,
21652177
typ: 'mypy.types.Type' = None,
21662178
tvar_def: 'mypy.types.TypeVarDef' = None,
2167-
module_public: bool = True) -> None:
2179+
module_public: bool = True, normalized: bool = False) -> None:
21682180
self.kind = kind
21692181
self.node = node
21702182
self.type_override = typ
21712183
self.mod_id = mod_id
21722184
self.tvar_def = tvar_def
21732185
self.module_public = module_public
2186+
self.normalized = normalized
21742187

21752188
@property
21762189
def fullname(self) -> str:

mypy/semanal.py

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
SetComprehension, DictionaryComprehension, TYPE_ALIAS, TypeAliasExpr,
6666
YieldExpr, ExecStmt, Argument, BackquoteExpr, ImportBase, AwaitExpr,
6767
IntExpr, FloatExpr, UnicodeExpr, EllipsisExpr, TempNode,
68-
COVARIANT, CONTRAVARIANT, INVARIANT, UNBOUND_IMPORTED, LITERAL_YES,
68+
COVARIANT, CONTRAVARIANT, INVARIANT, UNBOUND_IMPORTED, LITERAL_YES, nongen_builtins,
6969
collections_type_aliases, get_member_expr_fullname,
7070
)
7171
from mypy.typevars import has_no_typevars, fill_typevars
@@ -79,7 +79,9 @@
7979
TupleType, UnionType, StarType, EllipsisType, function_type, TypedDictType,
8080
)
8181
from mypy.nodes import implicit_module_attrs
82-
from mypy.typeanal import TypeAnalyser, TypeAnalyserPass3, analyze_type_alias
82+
from mypy.typeanal import (
83+
TypeAnalyser, TypeAnalyserPass3, analyze_type_alias, no_subscript_builtin_alias,
84+
)
8385
from mypy.exprtotype import expr_to_unanalyzed_type, TypeTranslationError
8486
from mypy.sametypes import is_same_type
8587
from mypy.options import Options
@@ -1248,7 +1250,8 @@ def visit_import_from(self, imp: ImportFrom) -> None:
12481250
symbol = SymbolTableNode(node.kind, node.node,
12491251
self.cur_mod_id,
12501252
node.type_override,
1251-
module_public=module_public)
1253+
module_public=module_public,
1254+
normalized=node.normalized)
12521255
self.add_symbol(imported_id, symbol, imp)
12531256
elif module and not missing:
12541257
# Missing attribute.
@@ -1284,13 +1287,19 @@ def process_import_over_existing_name(self,
12841287

12851288
def normalize_type_alias(self, node: SymbolTableNode,
12861289
ctx: Context) -> SymbolTableNode:
1290+
normalized = False
12871291
if node.fullname in type_aliases:
12881292
# Node refers to an aliased type such as typing.List; normalize.
12891293
node = self.lookup_qualified(type_aliases[node.fullname], ctx)
1294+
normalized = True
12901295
if node.fullname in collections_type_aliases:
12911296
# Similar, but for types from the collections module like typing.DefaultDict
12921297
self.add_module_symbol('collections', '__mypy_collections__', False, ctx)
12931298
node = self.lookup_qualified(collections_type_aliases[node.fullname], ctx)
1299+
normalized = True
1300+
if normalized:
1301+
node = SymbolTableNode(node.kind, node.node,
1302+
node.mod_id, node.type_override, normalized=True)
12941303
return node
12951304

12961305
def correct_relative_import(self, node: Union[ImportFrom, ImportAll]) -> str:
@@ -1326,7 +1335,8 @@ def visit_import_all(self, i: ImportAll) -> None:
13261335
continue
13271336
self.add_symbol(name, SymbolTableNode(node.kind, node.node,
13281337
self.cur_mod_id,
1329-
node.type_override), i)
1338+
node.type_override,
1339+
normalized=node.normalized), i)
13301340
else:
13311341
# Don't add any dummy symbols for 'from x import *' if 'x' is unknown.
13321342
pass
@@ -1365,7 +1375,8 @@ def anal_type(self, t: Type, allow_tuple_literal: bool = False,
13651375
self.lookup_fully_qualified,
13661376
self.fail,
13671377
aliasing=aliasing,
1368-
allow_tuple_literal=allow_tuple_literal)
1378+
allow_tuple_literal=allow_tuple_literal,
1379+
allow_unnormalized=self.is_stub_file)
13691380
return t.accept(a)
13701381
else:
13711382
return None
@@ -1388,7 +1399,7 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
13881399
res = analyze_type_alias(s.rvalue,
13891400
self.lookup_qualified,
13901401
self.lookup_fully_qualified,
1391-
self.fail)
1402+
self.fail, allow_unnormalized=True)
13921403
if res and (not isinstance(res, Instance) or res.args):
13931404
# TODO: What if this gets reassigned?
13941405
name = s.lvalues[0]
@@ -1465,7 +1476,9 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> None:
14651476
# TODO: We should record the fact that this is a variable
14661477
# that refers to a type, rather than making this
14671478
# just an alias for the type.
1468-
self.globals[lvalue.name].node = node
1479+
sym = self.lookup_type_node(rvalue)
1480+
if sym:
1481+
self.globals[lvalue.name] = sym
14691482

14701483
def analyze_lvalue(self, lval: Lvalue, nested: bool = False,
14711484
add_global: bool = False,
@@ -2705,7 +2718,7 @@ def visit_index_expr(self, expr: IndexExpr) -> None:
27052718
res = analyze_type_alias(expr,
27062719
self.lookup_qualified,
27072720
self.lookup_fully_qualified,
2708-
self.fail)
2721+
self.fail, allow_unnormalized=self.is_stub_file)
27092722
expr.analyzed = TypeAliasExpr(res, fallback=self.alias_fallback(res),
27102723
in_runtime=True)
27112724
elif refers_to_class_or_function(expr.base):
@@ -2726,9 +2739,23 @@ def visit_index_expr(self, expr: IndexExpr) -> None:
27262739
types.append(typearg)
27272740
expr.analyzed = TypeApplication(expr.base, types)
27282741
expr.analyzed.line = expr.line
2742+
# list, dict, set are not directly subscriptable
2743+
n = self.lookup_type_node(expr.base)
2744+
if n and not n.normalized and n.fullname in nongen_builtins:
2745+
self.fail(no_subscript_builtin_alias(n.fullname, propose_alt=False), expr)
27292746
else:
27302747
expr.index.accept(self)
27312748

2749+
def lookup_type_node(self, expr: Expression) -> Optional[SymbolTableNode]:
2750+
try:
2751+
t = expr_to_unanalyzed_type(expr)
2752+
except TypeTranslationError:
2753+
return None
2754+
if isinstance(t, UnboundType):
2755+
n = self.lookup_qualified(t.name, expr)
2756+
return n
2757+
return None
2758+
27322759
def visit_slice_expr(self, expr: SliceExpr) -> None:
27332760
if expr.begin_index:
27342761
expr.begin_index.accept(self)

mypy/typeanal.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from mypy.nodes import (
1313
BOUND_TVAR, UNBOUND_TVAR, TYPE_ALIAS, UNBOUND_IMPORTED,
1414
TypeInfo, Context, SymbolTableNode, Var, Expression,
15-
IndexExpr, RefExpr
15+
IndexExpr, RefExpr, nongen_builtins,
1616
)
1717
from mypy.sametypes import is_same_type
1818
from mypy.exprtotype import expr_to_unanalyzed_type, TypeTranslationError
@@ -33,7 +33,8 @@
3333
def analyze_type_alias(node: Expression,
3434
lookup_func: Callable[[str, Context], SymbolTableNode],
3535
lookup_fqn_func: Callable[[str], SymbolTableNode],
36-
fail_func: Callable[[str, Context], None]) -> Type:
36+
fail_func: Callable[[str, Context], None],
37+
allow_unnormalized: bool = False) -> Type:
3738
"""Return type if node is valid as a type alias rvalue.
3839
3940
Return None otherwise. 'node' must have been semantically analyzed.
@@ -68,10 +69,19 @@ def analyze_type_alias(node: Expression,
6869
except TypeTranslationError:
6970
fail_func('Invalid type alias', node)
7071
return None
71-
analyzer = TypeAnalyser(lookup_func, lookup_fqn_func, fail_func, aliasing=True)
72+
analyzer = TypeAnalyser(lookup_func, lookup_fqn_func, fail_func, aliasing=True,
73+
allow_unnormalized=allow_unnormalized)
7274
return type.accept(analyzer)
7375

7476

77+
def no_subscript_builtin_alias(name: str, propose_alt: bool = True) -> str:
78+
msg = '"{}" is not subscriptable'.format(name.split('.')[-1])
79+
replacement = nongen_builtins[name]
80+
if replacement and propose_alt:
81+
msg += ', use "{}" instead'.format(replacement)
82+
return msg
83+
84+
7585
class TypeAnalyser(TypeVisitor[Type]):
7686
"""Semantic analyzer for types (semantic analysis pass 2).
7787
@@ -83,14 +93,16 @@ def __init__(self,
8393
lookup_fqn_func: Callable[[str], SymbolTableNode],
8494
fail_func: Callable[[str, Context], None], *,
8595
aliasing: bool = False,
86-
allow_tuple_literal: bool = False) -> None:
96+
allow_tuple_literal: bool = False,
97+
allow_unnormalized: bool = False) -> None:
8798
self.lookup = lookup_func
8899
self.lookup_fqn_func = lookup_fqn_func
89100
self.fail = fail_func
90101
self.aliasing = aliasing
91102
self.allow_tuple_literal = allow_tuple_literal
92103
# Positive if we are analyzing arguments of another (outer) type
93104
self.nesting_level = 0
105+
self.allow_unnormalized = allow_unnormalized
94106

95107
def visit_unbound_type(self, t: UnboundType) -> Type:
96108
if t.optional:
@@ -106,6 +118,9 @@ def visit_unbound_type(self, t: UnboundType) -> Type:
106118
self.fail('Internal error (node is None, kind={})'.format(sym.kind), t)
107119
return AnyType()
108120
fullname = sym.node.fullname()
121+
if (fullname in nongen_builtins and t.args and
122+
not sym.normalized and not self.allow_unnormalized):
123+
self.fail(no_subscript_builtin_alias(fullname), t)
109124
if sym.kind == BOUND_TVAR:
110125
if len(t.args) > 0:
111126
self.fail('Type variable "{}" used with arguments'.format(

test-data/unit/check-generics.test

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -978,6 +978,30 @@ Bad = A[int] # type: ignore
978978
reveal_type(Bad) # E: Revealed type is 'Any'
979979
[out]
980980

981+
[case testNoSubscriptionOfBuiltinAliases]
982+
from typing import List, TypeVar
983+
984+
list[int]() # E: "list" is not subscriptable
985+
986+
ListAlias = List
987+
def fun() -> ListAlias[int]:
988+
pass
989+
990+
reveal_type(fun()) # E: Revealed type is 'builtins.list[builtins.int]'
991+
992+
BuiltinAlias = list
993+
BuiltinAlias[int]() # E: "list" is not subscriptable
994+
995+
#check that error is reported only once, and type is still stored
996+
T = TypeVar('T')
997+
BadGenList = list[T] # E: "list" is not subscriptable
998+
999+
reveal_type(BadGenList[int]()) # E: Revealed type is 'builtins.list[builtins.int*]'
1000+
reveal_type(BadGenList()) # E: Revealed type is 'builtins.list[Any]'
1001+
1002+
[builtins fixtures/list.pyi]
1003+
[out]
1004+
9811005

9821006
-- Simplified declaration of generics
9831007
-- ----------------------------------

test-data/unit/pythoneval.test

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1100,6 +1100,29 @@ _program.py:9: error: Incompatible types in assignment (expression has type "int
11001100
_program.py:19: error: List item 0 has incompatible type "Tuple[str, List[None]]"
11011101
_program.py:23: error: Invalid index type "str" for "dict"; expected type "int"
11021102

1103+
[case testNoSubcriptionOfStdlibCollections]
1104+
import collections
1105+
from collections import Counter
1106+
from typing import TypeVar
1107+
1108+
collections.defaultdict[int, str]()
1109+
Counter[int]()
1110+
1111+
T = TypeVar('T')
1112+
DDint = collections.defaultdict[T, int]
1113+
1114+
d = DDint[str]()
1115+
d[0] = 1
1116+
1117+
def f(d: collections.defaultdict[int, str]) -> None:
1118+
...
1119+
[out]
1120+
_program.py:5: error: "defaultdict" is not subscriptable
1121+
_program.py:6: error: "Counter" is not subscriptable
1122+
_program.py:9: error: "defaultdict" is not subscriptable
1123+
_program.py:12: error: Invalid index type "int" for "dict"; expected type "str"
1124+
_program.py:14: error: "defaultdict" is not subscriptable, use "typing.DefaultDict" instead
1125+
11031126
[case testCollectionsAliases]
11041127
import typing as t
11051128
import collections as c

0 commit comments

Comments
 (0)