Skip to content

Commit fc72019

Browse files
committed
Start work on inferring via partial types
This is still very incomplete.
1 parent 1d04eb7 commit fc72019

File tree

10 files changed

+115
-12
lines changed

10 files changed

+115
-12
lines changed

mypy/checker.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
from mypy.types import (
3030
Type, AnyType, CallableType, Void, FunctionLike, Overloaded, TupleType,
3131
Instance, NoneTyp, UnboundType, ErrorType, TypeTranslator, strip_type,
32-
UnionType, TypeVarType,
32+
UnionType, TypeVarType, PartialType
3333
)
3434
from mypy.sametypes import is_same_type
3535
from mypy.messages import MessageBuilder
@@ -332,7 +332,8 @@ class TypeChecker(NodeVisitor[Type]):
332332
breaking_out = False
333333
# Do weak type checking in this file
334334
weak_opts = set() # type: Set[str]
335-
335+
# Stack of collections of variables with partial types.
336+
partial_types = None # type: List[Set[Var]]
336337
globals = None # type: SymbolTable
337338
locals = None # type: SymbolTable
338339
modules = None # type: Dict[str, MypyFile]
@@ -358,6 +359,7 @@ def __init__(self, errors: Errors, modules: Dict[str, MypyFile],
358359
self.dynamic_funcs = []
359360
self.function_stack = []
360361
self.weak_opts = set() # type: Set[str]
362+
self.partial_types = []
361363

362364
def visit_file(self, file_node: MypyFile, path: str) -> None:
363365
"""Type check a mypy file with the given path."""
@@ -367,10 +369,12 @@ def visit_file(self, file_node: MypyFile, path: str) -> None:
367369
self.globals = file_node.names
368370
self.locals = None
369371
self.weak_opts = file_node.weak_opts
372+
self.enter_partial_types()
370373

371374
for d in file_node.defs:
372375
self.accept(d)
373376

377+
self.leave_partial_types()
374378
self.errors.set_ignored_lines(set())
375379

376380
def accept(self, node: Node, type_context: Type = None) -> Type:
@@ -1238,10 +1242,13 @@ def infer_variable_type(self, name: Var, lvalue: Node,
12381242
self.check_not_void(init_type, context)
12391243
self.set_inference_error_fallback_type(name, lvalue, init_type, context)
12401244
elif not self.is_valid_inferred_type(init_type):
1241-
# We cannot use the type of the initialization expression for type
1242-
# inference (it's not specific enough).
1243-
self.fail(messages.NEED_ANNOTATION_FOR_VAR, context)
1244-
self.set_inference_error_fallback_type(name, lvalue, init_type, context)
1245+
# We cannot use the type of the initialization expression for full type
1246+
# inference (it's not specific enough), but we might be able to give
1247+
# partial type which will be made more specific later. A partial type
1248+
# gets generated in assignment like 'x = []' where item type is not known.
1249+
if not self.infer_partial_type(name, lvalue, init_type):
1250+
self.fail(messages.NEED_ANNOTATION_FOR_VAR, context)
1251+
self.set_inference_error_fallback_type(name, lvalue, init_type, context)
12451252
else:
12461253
# Infer type of the target.
12471254

@@ -1250,6 +1257,16 @@ def infer_variable_type(self, name: Var, lvalue: Node,
12501257

12511258
self.set_inferred_type(name, lvalue, init_type)
12521259

1260+
def infer_partial_type(self, name: Var, lvalue: Node, init_type: Type) -> bool:
1261+
if not isinstance(init_type, Instance):
1262+
return False
1263+
if init_type.type.fullname() == 'builtins.list' and isinstance(init_type.args[0], NoneTyp):
1264+
partial_type = PartialType(init_type.type, name)
1265+
self.set_inferred_type(name, lvalue, partial_type)
1266+
self.partial_types[-1].add(name)
1267+
return True
1268+
return False
1269+
12531270
def set_inferred_type(self, var: Var, lvalue: Node, type: Type) -> None:
12541271
"""Store inferred variable type.
12551272
@@ -2032,6 +2049,12 @@ def enter(self) -> None:
20322049
def leave(self) -> None:
20332050
self.locals = None
20342051

2052+
def enter_partial_types(self) -> None:
2053+
self.partial_types.append(set())
2054+
2055+
def leave_partial_types(self) -> None:
2056+
self.partial_types.pop()
2057+
20352058
def is_within_function(self) -> bool:
20362059
"""Are we currently type checking within a function?
20372060

mypy/checkexpr.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
ListComprehension, GeneratorExpr, SetExpr, MypyFile, Decorator,
1515
ConditionalExpr, ComparisonExpr, TempNode, SetComprehension,
1616
DictionaryComprehension, ComplexExpr, EllipsisExpr, LITERAL_TYPE,
17-
TypeAliasExpr, YieldExpr, BackquoteExpr
17+
TypeAliasExpr, YieldExpr, BackquoteExpr, ARG_POS
1818
)
1919
from mypy.errors import Errors
2020
from mypy.nodes import function_type
@@ -110,13 +110,28 @@ def visit_call_expr(self, e: CallExpr) -> Type:
110110
if e.analyzed:
111111
# It's really a special form that only looks like a call.
112112
return self.accept(e.analyzed)
113+
self.try_infer_partial_type(e)
113114
self.accept(e.callee)
114115
# Access callee type directly, since accept may return the Any type
115116
# even if the type is known (in a dynamically typed function). This
116117
# way we get a more precise callee in dynamically typed functions.
117118
callee_type = self.chk.type_map[e.callee]
118119
return self.check_call_expr_with_callee_type(callee_type, e)
119120

121+
def try_infer_partial_type(self, e: CallExpr) -> None:
122+
partial_types = self.chk.partial_types[-1]
123+
if not partial_types:
124+
# Fast path leave -- no partial types in the current scope.
125+
return
126+
if isinstance(e.callee, MemberExpr) and isinstance(e.callee.expr, RefExpr):
127+
var = e.callee.expr.node
128+
if var in partial_types and e.callee.name == 'append' and e.arg_kinds == [ARG_POS]:
129+
# We can infer a full type for a partial List type.
130+
item_type = self.accept(e.args[0])
131+
var.type = self.chk.named_generic_type('builtins.list', [item_type])
132+
partial_types.remove(var)
133+
return
134+
120135
def check_call_expr_with_callee_type(self, callee_type: Type,
121136
e: CallExpr) -> Type:
122137
"""Type check call expression.

mypy/checkmember.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from mypy.types import (
66
Type, Instance, AnyType, TupleType, CallableType, FunctionLike, TypeVarDef,
7-
Overloaded, TypeVarType, TypeTranslator, UnionType
7+
Overloaded, TypeVarType, TypeTranslator, UnionType, PartialType
88
)
99
from mypy.nodes import TypeInfo, FuncBase, Var, FuncDef, SymbolNode, Context
1010
from mypy.nodes import ARG_POS, function_type, Decorator, OverloadedFuncDef

mypy/expandtype.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
from mypy.types import (
44
Type, Instance, CallableType, TypeVisitor, UnboundType, ErrorType, AnyType,
5-
Void, NoneTyp, TypeVarType, Overloaded, TupleType, UnionType, ErasedType, TypeList
5+
Void, NoneTyp, TypeVarType, Overloaded, TupleType, UnionType, ErasedType, TypeList,
6+
PartialType
67
)
78

89

@@ -92,6 +93,9 @@ def visit_tuple_type(self, t: TupleType) -> Type:
9293
def visit_union_type(self, t: UnionType) -> Type:
9394
return UnionType(self.expand_types(t.items), t.line)
9495

96+
def visit_partial_type(self, t: PartialType) -> Type:
97+
return t
98+
9599
def expand_types(self, types: List[Type]) -> List[Type]:
96100
a = [] # type: List[Type]
97101
for t in types:

mypy/sametypes.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from mypy.types import (
44
Type, UnboundType, ErrorType, AnyType, NoneTyp, Void, TupleType, UnionType, CallableType,
5-
TypeVarType, Instance, TypeVisitor, ErasedType, TypeList, Overloaded
5+
TypeVarType, Instance, TypeVisitor, ErasedType, TypeList, Overloaded, PartialType
66
)
77

88

@@ -97,3 +97,8 @@ def visit_overloaded(self, left: Overloaded) -> bool:
9797
return is_same_types(left.items(), self.right.items())
9898
else:
9999
return False
100+
101+
def visit_partial_type(self, left: PartialType) -> bool:
102+
# A partial type is not fully defined, so the result is indeterminate. We shouldn't
103+
# get here.
104+
raise RuntimeError

mypy/subtypes.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from mypy.types import (
44
Type, AnyType, UnboundType, TypeVisitor, ErrorType, Void, NoneTyp,
55
Instance, TypeVarType, CallableType, TupleType, UnionType, Overloaded, ErasedType, TypeList,
6-
is_named_instance
6+
PartialType, is_named_instance
77
)
88
import mypy.applytype
99
import mypy.constraints
@@ -184,6 +184,10 @@ def visit_union_type(self, left: UnionType) -> bool:
184184
return all(is_subtype(item, self.right, self.check_type_parameter)
185185
for item in left.items)
186186

187+
def visit_partial_type(self, left: PartialType) -> bool:
188+
# This is indeterminate as we don't really know the complete type yet.
189+
raise RuntimeError
190+
187191

188192
def is_callable_subtype(left: CallableType, right: CallableType,
189193
ignore_return: bool = False) -> bool:

mypy/test/data/check-inference.test

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1012,3 +1012,15 @@ a = x1
10121012
a = x2
10131013
a = x3 # E: Incompatible types in assignment (expression has type List[B], variable has type List[A])
10141014
[builtins fixtures/list.py]
1015+
1016+
1017+
-- Inferring type of variable when initialized to an empty collection
1018+
-- ------------------------------------------------------------------
1019+
1020+
1021+
[case testInferListInitializedToEmpty]
1022+
a = []
1023+
a.append(1)
1024+
a.append('') # E: Argument 1 to "append" of "list" has incompatible type "str"; expected "int"
1025+
[builtins fixtures/list.py]
1026+
[out]

mypy/test/data/fixtures/list.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class list(Iterable[T], Generic[T]):
1414
def __iter__(self) -> Iterator[T]: pass
1515
def __mul__(self, x: int) -> list[T]: pass
1616
def __getitem__(self, x: int) -> T: pass
17+
def append(self, x: T) -> None: pass
1718

1819
class tuple: pass
1920
class function: pass

mypy/typeanal.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from mypy.types import (
66
Type, UnboundType, TypeVarType, TupleType, UnionType, Instance, AnyType, CallableType,
7-
Void, NoneTyp, TypeList, TypeVarDef, TypeVisitor, StarType, EllipsisType
7+
Void, NoneTyp, TypeList, TypeVarDef, TypeVisitor, StarType, PartialType, EllipsisType
88
)
99
from mypy.nodes import (
1010
GDEF, TYPE_ALIAS, TypeInfo, Context, SymbolTableNode, BOUND_TVAR, TypeVarExpr, Var, Node,
@@ -193,6 +193,9 @@ def visit_star_type(self, t: StarType) -> Type:
193193
def visit_union_type(self, t: UnionType) -> Type:
194194
return UnionType(self.anal_array(t.items), t.line)
195195

196+
def visit_partial_type(self, t: PartialType) -> Type:
197+
assert False, "Internal error: Unexpected partial type"
198+
196199
def visit_ellipsis_type(self, t: EllipsisType) -> Type:
197200
self.fail("Unexpected '...'", t)
198201
return AnyType()
@@ -355,3 +358,6 @@ def visit_type_list(self, t: TypeList) -> None:
355358

356359
def visit_type_var(self, t: TypeVarType) -> None:
357360
pass
361+
362+
def visit_partial_type(self, t: PartialType) -> None:
363+
pass

mypy/types.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,26 @@ def has_readable_member(self, name: str) -> bool:
501501
for x in self.items)
502502

503503

504+
class PartialType(Type):
505+
"""Type such as List[?] where type arguments are unknown.
506+
507+
These are used for inferring types in multiphase initialization such as this:
508+
509+
x = [] # x gets a partial type List[?], as item type is unknown
510+
x.append(1) # partial type gets replaced with normal type List[int]
511+
"""
512+
513+
type = None # type: mypy.nodes.TypeInfo
514+
var = None # type: mypy.nodes.Var
515+
516+
def __init__(self, type: 'mypy.nodes.TypeInfo', var: 'mypy.nodes.Var') -> None:
517+
self.type = type
518+
self.var = var
519+
520+
def accept(self, visitor: 'TypeVisitor[T]') -> T:
521+
return visitor.visit_partial_type(self)
522+
523+
504524
class EllipsisType(Type):
505525
"""The type ... (ellipsis).
506526
@@ -581,6 +601,10 @@ def visit_star_type(self, t: StarType) -> T:
581601
def visit_union_type(self, t: UnionType) -> T:
582602
pass
583603

604+
@abstractmethod
605+
def visit_partial_type(self, t: PartialType) -> T:
606+
pass
607+
584608
def visit_ellipsis_type(self, t: EllipsisType) -> T:
585609
raise self._notimplemented_helper()
586610

@@ -619,6 +643,9 @@ def visit_instance(self, t: Instance) -> Type:
619643
def visit_type_var(self, t: TypeVarType) -> Type:
620644
return t
621645

646+
def visit_partial_type(self, t: PartialType) -> Type:
647+
return t
648+
622649
def visit_callable_type(self, t: CallableType) -> Type:
623650
return t.copy_modified(arg_types=self.translate_types(t.arg_types),
624651
ret_type=t.ret_type.accept(self),
@@ -773,6 +800,9 @@ def visit_union_type(self, t):
773800
s = self.list_str(t.items)
774801
return 'Union[{}]'.format(s)
775802

803+
def visit_partial_type(self, t: PartialType) -> str:
804+
return '{}[{}]'.format(t.info.name(), ', '.join(['?'] * len(t.info.type_vars)))
805+
776806
def visit_ellipsis_type(self, t):
777807
return '...'
778808

@@ -840,6 +870,9 @@ def visit_erased_type(self, t: ErasedType) -> bool:
840870
def visit_type_var(self, t: TypeVarType) -> bool:
841871
return self.default
842872

873+
def visit_partial_type(self, t: PartialType) -> bool:
874+
return self.default
875+
843876
def visit_instance(self, t: Instance) -> bool:
844877
return self.query_types(t.args)
845878

0 commit comments

Comments
 (0)