Skip to content

Commit

Permalink
PEP 585: allow builtins to be generic (#9564)
Browse files Browse the repository at this point in the history
Resolves #7907

Co-authored-by: cdce8p <30130371+cdce8p@users.noreply.github.com>
Co-authored-by: Ethan Smith <ethan@ethanhs.me>
Co-authored-by: hauntsaninja <>
  • Loading branch information
3 people authored Nov 26, 2020
1 parent dbca5a5 commit 52225ab
Show file tree
Hide file tree
Showing 8 changed files with 295 additions and 52 deletions.
14 changes: 10 additions & 4 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,17 @@ def get_column(self) -> int:
'builtins.frozenset': 'typing.FrozenSet',
} # type: Final

nongen_builtins = {'builtins.tuple': 'typing.Tuple',
'builtins.enumerate': ''} # type: Final
nongen_builtins.update((name, alias) for alias, name in type_aliases.items())
_nongen_builtins = {'builtins.tuple': 'typing.Tuple',
'builtins.enumerate': ''} # type: Final
_nongen_builtins.update((name, alias) for alias, name in type_aliases.items())
# Drop OrderedDict from this for backward compatibility
del nongen_builtins['collections.OrderedDict']
del _nongen_builtins['collections.OrderedDict']


def get_nongen_builtins(python_version: Tuple[int, int]) -> Dict[str, str]:
# After 3.9 with pep585 generic builtins are allowed.
return _nongen_builtins if python_version < (3, 9) else {}


RUNTIME_PROTOCOL_DECOS = ('typing.runtime_checkable',
'typing_extensions.runtime',
Expand Down
11 changes: 6 additions & 5 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
YieldExpr, ExecStmt, BackquoteExpr, ImportBase, AwaitExpr,
IntExpr, FloatExpr, UnicodeExpr, TempNode, OverloadPart,
PlaceholderNode, COVARIANT, CONTRAVARIANT, INVARIANT,
nongen_builtins, get_member_expr_fullname, REVEAL_TYPE,
get_nongen_builtins, get_member_expr_fullname, REVEAL_TYPE,
REVEAL_LOCALS, is_final_node, TypedDictExpr, type_aliases_source_versions,
EnumCallExpr, RUNTIME_PROTOCOL_DECOS, FakeExpression, Statement, AssignmentExpr,
ParamSpecExpr
Expand Down Expand Up @@ -465,7 +465,7 @@ def add_builtin_aliases(self, tree: MypyFile) -> None:
target = self.named_type_or_none(target_name, [])
assert target is not None
# Transform List to List[Any], etc.
fix_instance_types(target, self.fail, self.note)
fix_instance_types(target, self.fail, self.note, self.options.python_version)
alias_node = TypeAlias(target, alias,
line=-1, column=-1, # there is no context
no_args=True, normalized=True)
Expand Down Expand Up @@ -2589,7 +2589,7 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool:
# if the expected number of arguments is non-zero, so that aliases like A = List work.
# However, eagerly expanding aliases like Text = str is a nice performance optimization.
no_args = isinstance(res, Instance) and not res.args # type: ignore
fix_instance_types(res, self.fail, self.note)
fix_instance_types(res, self.fail, self.note, self.options.python_version)
alias_node = TypeAlias(res, self.qualified_name(lvalue.name), s.line, s.column,
alias_tvars=alias_tvars, no_args=no_args)
if isinstance(s.rvalue, (IndexExpr, CallExpr)): # CallExpr is for `void = type(None)`
Expand Down Expand Up @@ -3807,12 +3807,13 @@ def analyze_type_application(self, expr: IndexExpr) -> None:
if isinstance(target, Instance):
name = target.type.fullname
if (alias.no_args and # this avoids bogus errors for already reported aliases
name in nongen_builtins and not alias.normalized):
name in get_nongen_builtins(self.options.python_version) and
not alias.normalized):
self.fail(no_subscript_builtin_alias(name, propose_alt=False), expr)
# ...or directly.
else:
n = self.lookup_type_node(base)
if n and n.fullname in nongen_builtins:
if n and n.fullname in get_nongen_builtins(self.options.python_version):
self.fail(no_subscript_builtin_alias(n.fullname, propose_alt=False), expr)

def analyze_type_application_args(self, expr: IndexExpr) -> Optional[List[Type]]:
Expand Down
2 changes: 1 addition & 1 deletion mypy/test/testcheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@
'check-selftype.test',
'check-python2.test',
'check-columns.test',
'check-future.test',
'check-functions.test',
'check-tuples.test',
'check-expressions.test',
Expand All @@ -92,6 +91,7 @@
'check-errorcodes.test',
'check-annotated.test',
'check-parameter-specification.test',
'check-generic-alias.test',
]

# Tests that use Python 3.8-only AST features (like expression-scoped ignores):
Expand Down
43 changes: 30 additions & 13 deletions mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

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

def no_subscript_builtin_alias(name: str, propose_alt: bool = True) -> str:
msg = '"{}" is not subscriptable'.format(name.split('.')[-1])
# This should never be called if the python_version is 3.9 or newer
nongen_builtins = get_nongen_builtins((3, 8))
replacement = nongen_builtins[name]
if replacement and propose_alt:
msg += ', use "{}" instead'.format(replacement)
Expand Down Expand Up @@ -194,7 +196,7 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool)
hook = self.plugin.get_type_analyze_hook(fullname)
if hook is not None:
return hook(AnalyzeTypeContext(t, t, self))
if (fullname in nongen_builtins
if (fullname in get_nongen_builtins(self.options.python_version)
and t.args and
not self.allow_unnormalized and
not self.api.is_future_flag_set("annotations")):
Expand Down Expand Up @@ -241,6 +243,7 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool)
self.fail,
self.note,
disallow_any=disallow_any,
python_version=self.options.python_version,
use_generic_error=True,
unexpanded_type=t)
return res
Expand Down Expand Up @@ -272,7 +275,9 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Opt
self.fail("Final can be only used as an outermost qualifier"
" in a variable annotation", t)
return AnyType(TypeOfAny.from_error)
elif fullname == 'typing.Tuple':
elif (fullname == 'typing.Tuple' or
(fullname == 'builtins.tuple' and (self.options.python_version >= (3, 9) or
self.api.is_future_flag_set('annotations')))):
# Tuple is special because it is involved in builtin import cycle
# and may be not ready when used.
sym = self.api.lookup_fully_qualified_or_none('builtins.tuple')
Expand Down Expand Up @@ -305,7 +310,8 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Opt
elif fullname == 'typing.Callable':
return self.analyze_callable_type(t)
elif (fullname == 'typing.Type' or
(fullname == 'builtins.type' and self.api.is_future_flag_set('annotations'))):
(fullname == 'builtins.type' and (self.options.python_version >= (3, 9) or
self.api.is_future_flag_set('annotations')))):
if len(t.args) == 0:
if fullname == 'typing.Type':
any_type = self.get_omitted_any(t)
Expand Down Expand Up @@ -342,7 +348,8 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Opt

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

def analyze_type_with_type_info(
self, info: TypeInfo, args: Sequence[Type], ctx: Context) -> Type:
Expand All @@ -364,7 +371,8 @@ def analyze_type_with_type_info(
if len(instance.args) != len(info.type_vars) and not self.defining_alias:
fix_instance(instance, self.fail, self.note,
disallow_any=self.options.disallow_any_generics and
not self.is_typeshed_stub)
not self.is_typeshed_stub,
python_version=self.options.python_version)

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


def get_omitted_any(disallow_any: bool, fail: MsgCallback, note: MsgCallback,
orig_type: Type, fullname: Optional[str] = None,
orig_type: Type, python_version: Tuple[int, int],
fullname: Optional[str] = None,
unexpanded_type: Optional[Type] = None) -> AnyType:
if disallow_any:
nongen_builtins = get_nongen_builtins(python_version)
if fullname in nongen_builtins:
typ = orig_type
# We use a dedicated error message for builtin generics (as the most common case).
Expand Down Expand Up @@ -1019,7 +1029,8 @@ def get_omitted_any(disallow_any: bool, fail: MsgCallback, note: MsgCallback,


def fix_instance(t: Instance, fail: MsgCallback, note: MsgCallback,
disallow_any: bool, use_generic_error: bool = False,
disallow_any: bool, python_version: Tuple[int, int],
use_generic_error: bool = False,
unexpanded_type: Optional[Type] = None,) -> None:
"""Fix a malformed instance by replacing all type arguments with Any.
Expand All @@ -1030,7 +1041,8 @@ def fix_instance(t: Instance, fail: MsgCallback, note: MsgCallback,
fullname = None # type: Optional[str]
else:
fullname = t.type.fullname
any_type = get_omitted_any(disallow_any, fail, note, t, fullname, unexpanded_type)
any_type = get_omitted_any(disallow_any, fail, note, t, python_version, fullname,
unexpanded_type)
t.args = (any_type,) * len(t.type.type_vars)
return
# Invalid number of type parameters.
Expand Down Expand Up @@ -1289,21 +1301,26 @@ def make_optional_type(t: Type) -> Type:
return UnionType([t, NoneType()], t.line, t.column)


def fix_instance_types(t: Type, fail: MsgCallback, note: MsgCallback) -> None:
def fix_instance_types(t: Type, fail: MsgCallback, note: MsgCallback,
python_version: Tuple[int, int]) -> None:
"""Recursively fix all instance types (type argument count) in a given type.
For example 'Union[Dict, List[str, int]]' will be transformed into
'Union[Dict[Any, Any], List[Any]]' in place.
"""
t.accept(InstanceFixer(fail, note))
t.accept(InstanceFixer(fail, note, python_version))


class InstanceFixer(TypeTraverserVisitor):
def __init__(self, fail: MsgCallback, note: MsgCallback) -> None:
def __init__(
self, fail: MsgCallback, note: MsgCallback, python_version: Tuple[int, int]
) -> None:
self.fail = fail
self.note = note
self.python_version = python_version

def visit_instance(self, typ: Instance) -> None:
super().visit_instance(typ)
if len(typ.args) != len(typ.type.type_vars):
fix_instance(typ, self.fail, self.note, disallow_any=False, use_generic_error=True)
fix_instance(typ, self.fail, self.note, disallow_any=False,
python_version=self.python_version, use_generic_error=True)
24 changes: 0 additions & 24 deletions test-data/unit/check-future.test

This file was deleted.

Loading

0 comments on commit 52225ab

Please sign in to comment.