Skip to content

Commit e04bf78

Browse files
authored
Implementing background infrastructure for recursive types: Part 1 (#7330)
During planning discussions one of the main concerns about recursive types was the fact that we have hundreds of places where certain types are special-cased using `isinstance()`, and fixing all of them will take weeks. So I did a little experiment this weekend, to understand how bad it _actually_ is. I wrote a simple mypy plugin for mypy self-check, and it discovered 800+ such call sites. This looks pretty bad, but it turns out that fixing half of them (roughly 400 plugin errors) took me less than 2 days. This is kind of a triumph of our tooling :-) (i.e. mypy plugin + PyCharm plugin). Taking into account results of this experiment I propose to actually go ahead and implement recursive types. Here are some comments: * There will be four subsequent PRs: second part of `isinstance()` cleanup, implementing visitors and related methods everywhere, actual core implementation, adding extra tests for tricky recursion patterns. * The core idea of implementation stays the same as we discussed with @JukkaL: `TypeAliasType` and `TypeAlias` node will essentially match logic between `Instance` and `TypeInfo` (but structurally, as for protocols) * I wanted to make `PlaceholderType` a non-`ProperType`, but it didn't work immediately because we call `make_union()` during semantic analysis. If this seems important, this can be done with a bit more effort. * I make `TypeType.item` a proper type (following PEP 484, only very limited things can be passed to `Type[...]`). I also make `UnionType.items` proper types, mostly because of `make_simplified_union()`. Finally, I make `FuncBase.type` a proper type, I think a type alias can never appear there. * It is sometimes hard to decide where exactly is to call `get_proper_type()`, I tried to balance calling them not too soon and not too late, depending of every individual case. Please review, I am open to modifying logic in some places.
1 parent 7fb7e26 commit e04bf78

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+659
-320
lines changed

misc/proper_plugin.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
from mypy.plugin import Plugin, FunctionContext
2+
from mypy.types import Type, Instance, CallableType, UnionType, get_proper_type
3+
4+
import os.path
5+
from typing_extensions import Type as typing_Type
6+
from typing import Optional, Callable
7+
8+
FILE_WHITELIST = [
9+
'checker.py',
10+
'checkexpr.py',
11+
'checkmember.py',
12+
'messages.py',
13+
'semanal.py',
14+
'typeanal.py'
15+
]
16+
17+
18+
class ProperTypePlugin(Plugin):
19+
"""
20+
A plugin to ensure that every type is expanded before doing any special-casing.
21+
22+
This solves the problem that we have hundreds of call sites like:
23+
24+
if isinstance(typ, UnionType):
25+
... # special-case union
26+
27+
But after introducing a new type TypeAliasType (and removing immediate expansion)
28+
all these became dangerous because typ may be e.g. an alias to union.
29+
"""
30+
def get_function_hook(self, fullname: str
31+
) -> Optional[Callable[[FunctionContext], Type]]:
32+
if fullname == 'builtins.isinstance':
33+
return isinstance_proper_hook
34+
return None
35+
36+
37+
def isinstance_proper_hook(ctx: FunctionContext) -> Type:
38+
if os.path.split(ctx.api.path)[-1] in FILE_WHITELIST:
39+
return ctx.default_return_type
40+
for arg in ctx.arg_types[0]:
41+
if is_improper_type(arg):
42+
right = get_proper_type(ctx.arg_types[1][0])
43+
if isinstance(right, CallableType) and right.is_type_obj():
44+
if right.type_object().fullname() in ('mypy.types.Type',
45+
'mypy.types.ProperType',
46+
'mypy.types.TypeAliasType'):
47+
# Special case: things like assert isinstance(typ, ProperType) are always OK.
48+
return ctx.default_return_type
49+
if right.type_object().fullname() in ('mypy.types.UnboundType',
50+
'mypy.types.TypeVarType'):
51+
# Special case: these are not valid targets for a type alias and thus safe.
52+
return ctx.default_return_type
53+
ctx.api.fail('Never apply isinstance() to unexpanded types;'
54+
' use mypy.types.get_proper_type() first', ctx.context)
55+
return ctx.default_return_type
56+
57+
58+
def is_improper_type(typ: Type) -> bool:
59+
"""Is this a type that is not a subtype of ProperType?"""
60+
typ = get_proper_type(typ)
61+
if isinstance(typ, Instance):
62+
info = typ.type
63+
return info.has_base('mypy.types.Type') and not info.has_base('mypy.types.ProperType')
64+
if isinstance(typ, UnionType):
65+
return any(is_improper_type(t) for t in typ.items)
66+
return False
67+
68+
69+
def plugin(version: str) -> typing_Type[ProperTypePlugin]:
70+
return ProperTypePlugin

mypy/applytype.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
import mypy.subtypes
44
import mypy.sametypes
55
from mypy.expandtype import expand_type
6-
from mypy.types import Type, TypeVarId, TypeVarType, CallableType, AnyType, PartialType
6+
from mypy.types import (
7+
Type, TypeVarId, TypeVarType, CallableType, AnyType, PartialType, get_proper_types
8+
)
79
from mypy.messages import MessageBuilder
810
from mypy.nodes import Context
911

@@ -25,10 +27,10 @@ def apply_generic_arguments(callable: CallableType, orig_types: Sequence[Optiona
2527
assert len(tvars) == len(orig_types)
2628
# Check that inferred type variable values are compatible with allowed
2729
# values and bounds. Also, promote subtype values to allowed values.
28-
types = list(orig_types)
30+
types = get_proper_types(orig_types)
2931
for i, type in enumerate(types):
3032
assert not isinstance(type, PartialType), "Internal error: must never apply partial type"
31-
values = callable.variables[i].values
33+
values = get_proper_types(callable.variables[i].values)
3234
if type is None:
3335
continue
3436
if values:

mypy/argmap.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
from typing import List, Optional, Sequence, Callable, Set
44

5-
from mypy.types import Type, Instance, TupleType, AnyType, TypeOfAny, TypedDictType
5+
from mypy.types import (
6+
Type, Instance, TupleType, AnyType, TypeOfAny, TypedDictType, get_proper_type
7+
)
68
from mypy import nodes
79

810

@@ -34,7 +36,7 @@ def map_actuals_to_formals(actual_kinds: List[int],
3436
formal_to_actual[fi].append(ai)
3537
elif actual_kind == nodes.ARG_STAR:
3638
# We need to know the actual type to map varargs.
37-
actualt = actual_arg_type(ai)
39+
actualt = get_proper_type(actual_arg_type(ai))
3840
if isinstance(actualt, TupleType):
3941
# A tuple actual maps to a fixed number of formals.
4042
for _ in range(len(actualt.items)):
@@ -65,7 +67,7 @@ def map_actuals_to_formals(actual_kinds: List[int],
6567
formal_to_actual[formal_kinds.index(nodes.ARG_STAR2)].append(ai)
6668
else:
6769
assert actual_kind == nodes.ARG_STAR2
68-
actualt = actual_arg_type(ai)
70+
actualt = get_proper_type(actual_arg_type(ai))
6971
if isinstance(actualt, TypedDictType):
7072
for name, value in actualt.items.items():
7173
if name in formal_names:
@@ -153,6 +155,7 @@ def expand_actual_type(self,
153155
This is supposed to be called for each formal, in order. Call multiple times per
154156
formal if multiple actuals map to a formal.
155157
"""
158+
actual_type = get_proper_type(actual_type)
156159
if actual_kind == nodes.ARG_STAR:
157160
if isinstance(actual_type, Instance):
158161
if actual_type.type.fullname() == 'builtins.list':

mypy/binder.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
from typing import Dict, List, Set, Iterator, Union, Optional, Tuple, cast
55
from typing_extensions import DefaultDict
66

7-
from mypy.types import Type, AnyType, PartialType, UnionType, TypeOfAny, NoneType
7+
from mypy.types import (
8+
Type, AnyType, PartialType, UnionType, TypeOfAny, NoneType, get_proper_type
9+
)
810
from mypy.subtypes import is_subtype
911
from mypy.join import join_simple
1012
from mypy.sametypes import is_same_type
@@ -191,7 +193,7 @@ def update_from_options(self, frames: List[Frame]) -> bool:
191193

192194
type = resulting_values[0]
193195
assert type is not None
194-
declaration_type = self.declarations.get(key)
196+
declaration_type = get_proper_type(self.declarations.get(key))
195197
if isinstance(declaration_type, AnyType):
196198
# At this point resulting values can't contain None, see continue above
197199
if not all(is_same_type(type, cast(Type, t)) for t in resulting_values[1:]):
@@ -246,6 +248,9 @@ def assign_type(self, expr: Expression,
246248
type: Type,
247249
declared_type: Optional[Type],
248250
restrict_any: bool = False) -> None:
251+
type = get_proper_type(type)
252+
declared_type = get_proper_type(declared_type)
253+
249254
if self.type_assignments is not None:
250255
# We are in a multiassign from union, defer the actual binding,
251256
# just collect the types.
@@ -270,7 +275,7 @@ def assign_type(self, expr: Expression,
270275
# times?
271276
return
272277

273-
enclosing_type = self.most_recent_enclosing_type(expr, type)
278+
enclosing_type = get_proper_type(self.most_recent_enclosing_type(expr, type))
274279
if isinstance(enclosing_type, AnyType) and not restrict_any:
275280
# If x is Any and y is int, after x = y we do not infer that x is int.
276281
# This could be changed.
@@ -287,7 +292,8 @@ def assign_type(self, expr: Expression,
287292
elif (isinstance(type, AnyType)
288293
and isinstance(declared_type, UnionType)
289294
and any(isinstance(item, NoneType) for item in declared_type.items)
290-
and isinstance(self.most_recent_enclosing_type(expr, NoneType()), NoneType)):
295+
and isinstance(get_proper_type(self.most_recent_enclosing_type(expr, NoneType())),
296+
NoneType)):
291297
# Replace any Nones in the union type with Any
292298
new_items = [type if isinstance(item, NoneType) else item
293299
for item in declared_type.items]
@@ -320,6 +326,7 @@ def invalidate_dependencies(self, expr: BindableExpression) -> None:
320326
self._cleanse_key(dep)
321327

322328
def most_recent_enclosing_type(self, expr: BindableExpression, type: Type) -> Optional[Type]:
329+
type = get_proper_type(type)
323330
if isinstance(type, AnyType):
324331
return get_declaration(expr)
325332
key = literal_hash(expr)
@@ -412,7 +419,7 @@ def top_frame_context(self) -> Iterator[Frame]:
412419

413420
def get_declaration(expr: BindableExpression) -> Optional[Type]:
414421
if isinstance(expr, RefExpr) and isinstance(expr.node, Var):
415-
type = expr.node.type
422+
type = get_proper_type(expr.node.type)
416423
if not isinstance(type, PartialType):
417424
return type
418425
return None

mypy/checker.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,7 @@ def check_overlapping_overloads(self, defn: OverloadedFuncDef) -> None:
465465
impl_type = None # type: Optional[CallableType]
466466
if defn.impl:
467467
if isinstance(defn.impl, FuncDef):
468-
inner_type = defn.impl.type
468+
inner_type = defn.impl.type # type: Optional[Type]
469469
elif isinstance(defn.impl, Decorator):
470470
inner_type = defn.impl.var.type
471471
else:
@@ -3650,8 +3650,8 @@ def find_isinstance_check(self, node: Expression
36503650
# Restrict the type of the variable to True-ish/False-ish in the if and else branches
36513651
# respectively
36523652
vartype = type_map[node]
3653-
if_type = true_only(vartype)
3654-
else_type = false_only(vartype)
3653+
if_type = true_only(vartype) # type: Type
3654+
else_type = false_only(vartype) # type: Type
36553655
ref = node # type: Expression
36563656
if_map = {ref: if_type} if not isinstance(if_type, UninhabitedType) else None
36573657
else_map = {ref: else_type} if not isinstance(else_type, UninhabitedType) else None
@@ -4139,7 +4139,7 @@ def or_conditional_maps(m1: TypeMap, m2: TypeMap) -> TypeMap:
41394139
# expressions whose type is refined by both conditions. (We do not
41404140
# learn anything about expressions whose type is refined by only
41414141
# one condition.)
4142-
result = {}
4142+
result = {} # type: Dict[Expression, Type]
41434143
for n1 in m1:
41444144
for n2 in m2:
41454145
if literal_hash(n1) == literal_hash(n2):

mypy/checkexpr.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
TupleType, TypedDictType, Instance, TypeVarType, ErasedType, UnionType,
1919
PartialType, DeletedType, UninhabitedType, TypeType, TypeOfAny, LiteralType, LiteralValue,
2020
true_only, false_only, is_named_instance, function_type, callable_type, FunctionLike,
21-
StarType, is_optional, remove_optional, is_generic_instance
21+
StarType, is_optional, remove_optional, is_generic_instance, get_proper_type
2222
)
2323
from mypy.nodes import (
2424
NameExpr, RefExpr, Var, FuncDef, OverloadedFuncDef, TypeInfo, CallExpr,
@@ -587,6 +587,7 @@ def apply_function_plugin(self,
587587
# Apply method plugin
588588
method_callback = self.plugin.get_method_hook(fullname)
589589
assert method_callback is not None # Assume that caller ensures this
590+
object_type = get_proper_type(object_type)
590591
return method_callback(
591592
MethodContext(object_type, formal_arg_types, formal_arg_kinds,
592593
callee.arg_names, formal_arg_names,
@@ -608,6 +609,7 @@ def apply_method_signature_hook(
608609
for formal, actuals in enumerate(formal_to_actual):
609610
for actual in actuals:
610611
formal_arg_exprs[formal].append(args[actual])
612+
object_type = get_proper_type(object_type)
611613
return signature_hook(
612614
MethodSigContext(object_type, formal_arg_exprs, callee, context, self.chk))
613615
else:
@@ -2710,7 +2712,7 @@ def visit_typeddict_index_expr(self, td_type: TypedDictType, index: Expression)
27102712
else:
27112713
typ = self.accept(index)
27122714
if isinstance(typ, UnionType):
2713-
key_types = typ.items
2715+
key_types = list(typ.items) # type: List[Type]
27142716
else:
27152717
key_types = [typ]
27162718

@@ -3549,7 +3551,7 @@ def has_member(self, typ: Type, member: str) -> bool:
35493551
elif isinstance(typ, TypeType):
35503552
# Type[Union[X, ...]] is always normalized to Union[Type[X], ...],
35513553
# so we don't need to care about unions here.
3552-
item = typ.item
3554+
item = typ.item # type: Type
35533555
if isinstance(item, TypeVarType):
35543556
item = item.upper_bound
35553557
if isinstance(item, TupleType):
@@ -3743,8 +3745,7 @@ def narrow_type_from_binder(self, expr: Expression, known_type: Type, # noqa
37433745
not is_overlapping_types(known_type, restriction,
37443746
prohibit_none_typevar_overlap=True)):
37453747
return None
3746-
ans = narrow_declared_type(known_type, restriction)
3747-
return ans
3748+
return narrow_declared_type(known_type, restriction)
37483749
return known_type
37493750

37503751

mypy/checkmember.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from mypy.types import (
77
Type, Instance, AnyType, TupleType, TypedDictType, CallableType, FunctionLike, TypeVarDef,
88
Overloaded, TypeVarType, UnionType, PartialType, UninhabitedType, TypeOfAny, LiteralType,
9-
DeletedType, NoneType, TypeType, function_type, get_type_vars,
9+
DeletedType, NoneType, TypeType, function_type, get_type_vars, get_proper_type
1010
)
1111
from mypy.nodes import (
1212
TypeInfo, FuncBase, Var, FuncDef, SymbolNode, Context, MypyFile, TypeVarExpr,
@@ -371,8 +371,8 @@ def analyze_member_var_access(name: str,
371371
fullname = '{}.{}'.format(method.info.fullname(), name)
372372
hook = mx.chk.plugin.get_attribute_hook(fullname)
373373
if hook:
374-
result = hook(AttributeContext(mx.original_type, result,
375-
mx.context, mx.chk))
374+
result = hook(AttributeContext(get_proper_type(mx.original_type),
375+
result, mx.context, mx.chk))
376376
return result
377377
else:
378378
setattr_meth = info.get_method('__setattr__')
@@ -511,7 +511,7 @@ def analyze_var(name: str,
511511
mx.msg.read_only_property(name, itype.type, mx.context)
512512
if mx.is_lvalue and var.is_classvar:
513513
mx.msg.cant_assign_to_classvar(name, mx.context)
514-
result = t
514+
result = t # type: Type
515515
if var.is_initialized_in_class and isinstance(t, FunctionLike) and not t.is_type_obj():
516516
if mx.is_lvalue:
517517
if var.is_property:
@@ -552,7 +552,8 @@ def analyze_var(name: str,
552552
result = analyze_descriptor_access(mx.original_type, result, mx.builtin_type,
553553
mx.msg, mx.context, chk=mx.chk)
554554
if hook:
555-
result = hook(AttributeContext(mx.original_type, result, mx.context, mx.chk))
555+
result = hook(AttributeContext(get_proper_type(mx.original_type),
556+
result, mx.context, mx.chk))
556557
return result
557558

558559

mypy/checkstrformat.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from typing_extensions import Final, TYPE_CHECKING
77

88
from mypy.types import (
9-
Type, AnyType, TupleType, Instance, UnionType, TypeOfAny
9+
Type, AnyType, TupleType, Instance, UnionType, TypeOfAny, get_proper_type
1010
)
1111
from mypy.nodes import (
1212
StrExpr, BytesExpr, UnicodeExpr, TupleExpr, DictExpr, Context, Expression, StarExpr
@@ -137,7 +137,7 @@ def check_simple_str_interpolation(self, specifiers: List[ConversionSpecifier],
137137
if checkers is None:
138138
return
139139

140-
rhs_type = self.accept(replacements)
140+
rhs_type = get_proper_type(self.accept(replacements))
141141
rep_types = [] # type: List[Type]
142142
if isinstance(rhs_type, TupleType):
143143
rep_types = rhs_type.items

mypy/constraints.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
CallableType, Type, TypeVisitor, UnboundType, AnyType, NoneType, TypeVarType, Instance,
88
TupleType, TypedDictType, UnionType, Overloaded, ErasedType, PartialType, DeletedType,
99
UninhabitedType, TypeType, TypeVarId, TypeQuery, is_named_instance, TypeOfAny, LiteralType,
10+
ProperType, get_proper_type
1011
)
1112
from mypy.maptype import map_instance_to_supertype
1213
import mypy.subtypes
@@ -88,6 +89,8 @@ def infer_constraints(template: Type, actual: Type,
8889
8990
The constraints are represented as Constraint objects.
9091
"""
92+
template = get_proper_type(template)
93+
actual = get_proper_type(actual)
9194

9295
# If the template is simply a type variable, emit a Constraint directly.
9396
# We need to handle this case before handling Unions for two reasons:
@@ -199,12 +202,12 @@ def is_same_constraint(c1: Constraint, c2: Constraint) -> bool:
199202
and mypy.sametypes.is_same_type(c1.target, c2.target))
200203

201204

202-
def simplify_away_incomplete_types(types: List[Type]) -> List[Type]:
205+
def simplify_away_incomplete_types(types: Iterable[Type]) -> List[Type]:
203206
complete = [typ for typ in types if is_complete_type(typ)]
204207
if complete:
205208
return complete
206209
else:
207-
return types
210+
return list(types)
208211

209212

210213
def is_complete_type(typ: Type) -> bool:
@@ -229,9 +232,9 @@ class ConstraintBuilderVisitor(TypeVisitor[List[Constraint]]):
229232

230233
# The type that is compared against a template
231234
# TODO: The value may be None. Is that actually correct?
232-
actual = None # type: Type
235+
actual = None # type: ProperType
233236

234-
def __init__(self, actual: Type, direction: int) -> None:
237+
def __init__(self, actual: ProperType, direction: int) -> None:
235238
# Direction must be SUBTYPE_OF or SUPERTYPE_OF.
236239
self.actual = actual
237240
self.direction = direction
@@ -298,7 +301,7 @@ def visit_instance(self, template: Instance) -> List[Constraint]:
298301
if isinstance(actual, Instance):
299302
instance = actual
300303
erased = erase_typevars(template)
301-
assert isinstance(erased, Instance)
304+
assert isinstance(erased, Instance) # type: ignore
302305
# We always try nominal inference if possible,
303306
# it is much faster than the structural one.
304307
if (self.direction == SUBTYPE_OF and

0 commit comments

Comments
 (0)