Skip to content

Commit 52225ab

Browse files
AllanDaemoncdce8pemmatyping
authored
PEP 585: allow builtins to be generic (#9564)
Resolves #7907 Co-authored-by: cdce8p <[email protected]> Co-authored-by: Ethan Smith <[email protected]> Co-authored-by: hauntsaninja <>
1 parent dbca5a5 commit 52225ab

File tree

8 files changed

+295
-52
lines changed

8 files changed

+295
-52
lines changed

mypy/nodes.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,11 +138,17 @@ def get_column(self) -> int:
138138
'builtins.frozenset': 'typing.FrozenSet',
139139
} # type: Final
140140

141-
nongen_builtins = {'builtins.tuple': 'typing.Tuple',
142-
'builtins.enumerate': ''} # type: Final
143-
nongen_builtins.update((name, alias) for alias, name in type_aliases.items())
141+
_nongen_builtins = {'builtins.tuple': 'typing.Tuple',
142+
'builtins.enumerate': ''} # type: Final
143+
_nongen_builtins.update((name, alias) for alias, name in type_aliases.items())
144144
# Drop OrderedDict from this for backward compatibility
145-
del nongen_builtins['collections.OrderedDict']
145+
del _nongen_builtins['collections.OrderedDict']
146+
147+
148+
def get_nongen_builtins(python_version: Tuple[int, int]) -> Dict[str, str]:
149+
# After 3.9 with pep585 generic builtins are allowed.
150+
return _nongen_builtins if python_version < (3, 9) else {}
151+
146152

147153
RUNTIME_PROTOCOL_DECOS = ('typing.runtime_checkable',
148154
'typing_extensions.runtime',

mypy/semanal.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
YieldExpr, ExecStmt, BackquoteExpr, ImportBase, AwaitExpr,
7474
IntExpr, FloatExpr, UnicodeExpr, TempNode, OverloadPart,
7575
PlaceholderNode, COVARIANT, CONTRAVARIANT, INVARIANT,
76-
nongen_builtins, get_member_expr_fullname, REVEAL_TYPE,
76+
get_nongen_builtins, get_member_expr_fullname, REVEAL_TYPE,
7777
REVEAL_LOCALS, is_final_node, TypedDictExpr, type_aliases_source_versions,
7878
EnumCallExpr, RUNTIME_PROTOCOL_DECOS, FakeExpression, Statement, AssignmentExpr,
7979
ParamSpecExpr
@@ -465,7 +465,7 @@ def add_builtin_aliases(self, tree: MypyFile) -> None:
465465
target = self.named_type_or_none(target_name, [])
466466
assert target is not None
467467
# Transform List to List[Any], etc.
468-
fix_instance_types(target, self.fail, self.note)
468+
fix_instance_types(target, self.fail, self.note, self.options.python_version)
469469
alias_node = TypeAlias(target, alias,
470470
line=-1, column=-1, # there is no context
471471
no_args=True, normalized=True)
@@ -2589,7 +2589,7 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool:
25892589
# if the expected number of arguments is non-zero, so that aliases like A = List work.
25902590
# However, eagerly expanding aliases like Text = str is a nice performance optimization.
25912591
no_args = isinstance(res, Instance) and not res.args # type: ignore
2592-
fix_instance_types(res, self.fail, self.note)
2592+
fix_instance_types(res, self.fail, self.note, self.options.python_version)
25932593
alias_node = TypeAlias(res, self.qualified_name(lvalue.name), s.line, s.column,
25942594
alias_tvars=alias_tvars, no_args=no_args)
25952595
if isinstance(s.rvalue, (IndexExpr, CallExpr)): # CallExpr is for `void = type(None)`
@@ -3807,12 +3807,13 @@ def analyze_type_application(self, expr: IndexExpr) -> None:
38073807
if isinstance(target, Instance):
38083808
name = target.type.fullname
38093809
if (alias.no_args and # this avoids bogus errors for already reported aliases
3810-
name in nongen_builtins and not alias.normalized):
3810+
name in get_nongen_builtins(self.options.python_version) and
3811+
not alias.normalized):
38113812
self.fail(no_subscript_builtin_alias(name, propose_alt=False), expr)
38123813
# ...or directly.
38133814
else:
38143815
n = self.lookup_type_node(base)
3815-
if n and n.fullname in nongen_builtins:
3816+
if n and n.fullname in get_nongen_builtins(self.options.python_version):
38163817
self.fail(no_subscript_builtin_alias(n.fullname, propose_alt=False), expr)
38173818

38183819
def analyze_type_application_args(self, expr: IndexExpr) -> Optional[List[Type]]:

mypy/test/testcheck.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@
6666
'check-selftype.test',
6767
'check-python2.test',
6868
'check-columns.test',
69-
'check-future.test',
7069
'check-functions.test',
7170
'check-tuples.test',
7271
'check-expressions.test',
@@ -92,6 +91,7 @@
9291
'check-errorcodes.test',
9392
'check-annotated.test',
9493
'check-parameter-specification.test',
94+
'check-generic-alias.test',
9595
]
9696

9797
# Tests that use Python 3.8-only AST features (like expression-scoped ignores):

mypy/typeanal.py

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
from mypy.nodes import (
2323
TypeInfo, Context, SymbolTableNode, Var, Expression,
24-
nongen_builtins, check_arg_names, check_arg_kinds, ARG_POS, ARG_NAMED,
24+
get_nongen_builtins, check_arg_names, check_arg_kinds, ARG_POS, ARG_NAMED,
2525
ARG_OPT, ARG_NAMED_OPT, ARG_STAR, ARG_STAR2, TypeVarExpr, TypeVarLikeExpr, ParamSpecExpr,
2626
TypeAlias, PlaceholderNode, SYMBOL_FUNCBASE_TYPES, Decorator, MypyFile
2727
)
@@ -94,6 +94,8 @@ def analyze_type_alias(node: Expression,
9494

9595
def no_subscript_builtin_alias(name: str, propose_alt: bool = True) -> str:
9696
msg = '"{}" is not subscriptable'.format(name.split('.')[-1])
97+
# This should never be called if the python_version is 3.9 or newer
98+
nongen_builtins = get_nongen_builtins((3, 8))
9799
replacement = nongen_builtins[name]
98100
if replacement and propose_alt:
99101
msg += ', use "{}" instead'.format(replacement)
@@ -194,7 +196,7 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool)
194196
hook = self.plugin.get_type_analyze_hook(fullname)
195197
if hook is not None:
196198
return hook(AnalyzeTypeContext(t, t, self))
197-
if (fullname in nongen_builtins
199+
if (fullname in get_nongen_builtins(self.options.python_version)
198200
and t.args and
199201
not self.allow_unnormalized and
200202
not self.api.is_future_flag_set("annotations")):
@@ -241,6 +243,7 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool)
241243
self.fail,
242244
self.note,
243245
disallow_any=disallow_any,
246+
python_version=self.options.python_version,
244247
use_generic_error=True,
245248
unexpanded_type=t)
246249
return res
@@ -272,7 +275,9 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Opt
272275
self.fail("Final can be only used as an outermost qualifier"
273276
" in a variable annotation", t)
274277
return AnyType(TypeOfAny.from_error)
275-
elif fullname == 'typing.Tuple':
278+
elif (fullname == 'typing.Tuple' or
279+
(fullname == 'builtins.tuple' and (self.options.python_version >= (3, 9) or
280+
self.api.is_future_flag_set('annotations')))):
276281
# Tuple is special because it is involved in builtin import cycle
277282
# and may be not ready when used.
278283
sym = self.api.lookup_fully_qualified_or_none('builtins.tuple')
@@ -305,7 +310,8 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Opt
305310
elif fullname == 'typing.Callable':
306311
return self.analyze_callable_type(t)
307312
elif (fullname == 'typing.Type' or
308-
(fullname == 'builtins.type' and self.api.is_future_flag_set('annotations'))):
313+
(fullname == 'builtins.type' and (self.options.python_version >= (3, 9) or
314+
self.api.is_future_flag_set('annotations')))):
309315
if len(t.args) == 0:
310316
if fullname == 'typing.Type':
311317
any_type = self.get_omitted_any(t)
@@ -342,7 +348,8 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Opt
342348

343349
def get_omitted_any(self, typ: Type, fullname: Optional[str] = None) -> AnyType:
344350
disallow_any = not self.is_typeshed_stub and self.options.disallow_any_generics
345-
return get_omitted_any(disallow_any, self.fail, self.note, typ, fullname)
351+
return get_omitted_any(disallow_any, self.fail, self.note, typ,
352+
self.options.python_version, fullname)
346353

347354
def analyze_type_with_type_info(
348355
self, info: TypeInfo, args: Sequence[Type], ctx: Context) -> Type:
@@ -364,7 +371,8 @@ def analyze_type_with_type_info(
364371
if len(instance.args) != len(info.type_vars) and not self.defining_alias:
365372
fix_instance(instance, self.fail, self.note,
366373
disallow_any=self.options.disallow_any_generics and
367-
not self.is_typeshed_stub)
374+
not self.is_typeshed_stub,
375+
python_version=self.options.python_version)
368376

369377
tup = info.tuple_type
370378
if tup is not None:
@@ -979,9 +987,11 @@ def tuple_type(self, items: List[Type]) -> TupleType:
979987

980988

981989
def get_omitted_any(disallow_any: bool, fail: MsgCallback, note: MsgCallback,
982-
orig_type: Type, fullname: Optional[str] = None,
990+
orig_type: Type, python_version: Tuple[int, int],
991+
fullname: Optional[str] = None,
983992
unexpanded_type: Optional[Type] = None) -> AnyType:
984993
if disallow_any:
994+
nongen_builtins = get_nongen_builtins(python_version)
985995
if fullname in nongen_builtins:
986996
typ = orig_type
987997
# We use a dedicated error message for builtin generics (as the most common case).
@@ -1019,7 +1029,8 @@ def get_omitted_any(disallow_any: bool, fail: MsgCallback, note: MsgCallback,
10191029

10201030

10211031
def fix_instance(t: Instance, fail: MsgCallback, note: MsgCallback,
1022-
disallow_any: bool, use_generic_error: bool = False,
1032+
disallow_any: bool, python_version: Tuple[int, int],
1033+
use_generic_error: bool = False,
10231034
unexpanded_type: Optional[Type] = None,) -> None:
10241035
"""Fix a malformed instance by replacing all type arguments with Any.
10251036
@@ -1030,7 +1041,8 @@ def fix_instance(t: Instance, fail: MsgCallback, note: MsgCallback,
10301041
fullname = None # type: Optional[str]
10311042
else:
10321043
fullname = t.type.fullname
1033-
any_type = get_omitted_any(disallow_any, fail, note, t, fullname, unexpanded_type)
1044+
any_type = get_omitted_any(disallow_any, fail, note, t, python_version, fullname,
1045+
unexpanded_type)
10341046
t.args = (any_type,) * len(t.type.type_vars)
10351047
return
10361048
# Invalid number of type parameters.
@@ -1289,21 +1301,26 @@ def make_optional_type(t: Type) -> Type:
12891301
return UnionType([t, NoneType()], t.line, t.column)
12901302

12911303

1292-
def fix_instance_types(t: Type, fail: MsgCallback, note: MsgCallback) -> None:
1304+
def fix_instance_types(t: Type, fail: MsgCallback, note: MsgCallback,
1305+
python_version: Tuple[int, int]) -> None:
12931306
"""Recursively fix all instance types (type argument count) in a given type.
12941307
12951308
For example 'Union[Dict, List[str, int]]' will be transformed into
12961309
'Union[Dict[Any, Any], List[Any]]' in place.
12971310
"""
1298-
t.accept(InstanceFixer(fail, note))
1311+
t.accept(InstanceFixer(fail, note, python_version))
12991312

13001313

13011314
class InstanceFixer(TypeTraverserVisitor):
1302-
def __init__(self, fail: MsgCallback, note: MsgCallback) -> None:
1315+
def __init__(
1316+
self, fail: MsgCallback, note: MsgCallback, python_version: Tuple[int, int]
1317+
) -> None:
13031318
self.fail = fail
13041319
self.note = note
1320+
self.python_version = python_version
13051321

13061322
def visit_instance(self, typ: Instance) -> None:
13071323
super().visit_instance(typ)
13081324
if len(typ.args) != len(typ.type.type_vars):
1309-
fix_instance(typ, self.fail, self.note, disallow_any=False, use_generic_error=True)
1325+
fix_instance(typ, self.fail, self.note, disallow_any=False,
1326+
python_version=self.python_version, use_generic_error=True)

test-data/unit/check-future.test

Lines changed: 0 additions & 24 deletions
This file was deleted.

0 commit comments

Comments
 (0)