From 0c58fefb93b7b3ad565507dbe41e7142ecf69ac5 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 22 Jan 2018 11:42:42 +0000 Subject: [PATCH 01/12] Initial implementation of requiring "local" partial types Definitions of local partial types can't span multiple fine-grained incremental targets. --- mypy/checker.py | 65 +++++++++++++++++++++++------ mypy/checkexpr.py | 12 ++++-- mypy/dmypy_server.py | 2 +- test-data/unit/check-inference.test | 48 ++++++++++++++++++--- test-data/unit/check-optional.test | 15 +++++-- test-data/unit/check-tuples.test | 3 +- 6 files changed, 115 insertions(+), 30 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 109fc24043bd..d6844723a8be 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -107,6 +107,10 @@ ('is_upper_bound', bool), # False => precise type ]) +# Keeps track of partial types in a single scope +PartialTypeScope = NamedTuple('PartialTypeMap', [('map', Dict[Var, Context]), + ('is_function', bool)]) + class TypeChecker(NodeVisitor[None], CheckerPluginInterface): """Mypy type checker. @@ -136,7 +140,7 @@ class TypeChecker(NodeVisitor[None], CheckerPluginInterface): # Flags; true for dynamically typed functions dynamic_funcs = None # type: List[bool] # Stack of collections of variables with partial types - partial_types = None # type: List[Dict[Var, Context]] + partial_types = None # type: List[PartialTypeScope] # Vars for which partial type errors are already reported # (to avoid logically duplicate errors with different error context). partial_reported = None # type: Set[Var] @@ -632,7 +636,7 @@ def check_func_item(self, defn: FuncItem, self.dynamic_funcs.append(defn.is_dynamic() and not type_override) with self.errors.enter_function(fdef.name()) if fdef else nothing(): - with self.enter_partial_types(): + with self.enter_partial_types(is_function=True): typ = self.function_type(defn) if type_override: typ = type_override.copy_modified(line=typ.line, column=typ.column) @@ -1244,7 +1248,7 @@ def visit_class_def(self, defn: ClassDef) -> None: typ = defn.info if typ.is_protocol and typ.defn.type_vars: self.check_protocol_variance(defn) - with self.errors.enter_type(defn.name), self.enter_partial_types(): + with self.errors.enter_type(defn.name), self.enter_partial_types(is_class=True): old_binder = self.binder self.binder = ConditionalTypeBinder() with self.binder.top_frame_context(): @@ -1973,7 +1977,7 @@ def infer_partial_type(self, name: Var, lvalue: Lvalue, init_type: Type) -> bool else: return False self.set_inferred_type(name, lvalue, partial_type) - self.partial_types[-1][name] = lvalue + self.partial_types[-1].map[name] = lvalue return True def set_inferred_type(self, var: Var, lvalue: Lvalue, type: Type) -> None: @@ -3088,33 +3092,68 @@ def lookup_qualified(self, name: str) -> SymbolTableNode: raise KeyError(msg.format(last, name)) @contextmanager - def enter_partial_types(self) -> Iterator[None]: + def enter_partial_types(self, *, is_function: bool = False, + is_class: bool = False) -> Iterator[None]: """Enter a new scope for collecting partial types. Also report errors for variables which still have partial types, i.e. we couldn't infer a complete type. """ - self.partial_types.append({}) + self.partial_types.append(PartialTypeScope({}, is_function)) yield - partial_types = self.partial_types.pop() + partial_types, _ = self.partial_types.pop() if not self.current_node_deferred: for var, context in partial_types.items(): - if isinstance(var.type, PartialType) and var.type.type is None: - # None partial type: assume variable is intended to have type None + if (is_function + and isinstance(var.type, PartialType) + and var.type.type is None): + # None partial type within function: assume variable is intended to have + # type None, without requiring an annotation. + # + # Rationale: Assignments within nested functions can complete a partial type, + # so we likely have complete type information, unlike contexts outside + # a function. var.type = NoneTyp() else: if var not in self.partial_reported: self.msg.need_annotation_for_var(var, context) self.partial_reported.add(var) - var.type = AnyType(TypeOfAny.from_error) + if isinstance(var.type, PartialType) and var.type.type is None: + # None partial type outside function: assume variable is intended to have + # type None, but require an annotation since None is likely just a default + # and there is likely another assignment in a function that won't affect + # the partial type. + var.type = NoneTyp() + else: + var.type = AnyType(TypeOfAny.from_error) def find_partial_types(self, var: Var) -> Optional[Dict[Var, Context]]: - for partial_types in reversed(self.partial_types): - if var in partial_types: - return partial_types + in_scope, partial_types = self.find_partial_types2(var) + if in_scope: + return partial_types return None + def find_partial_types2(self, var: Var) -> Tuple[bool, Optional[Dict[Var, Context]]]: + # Look for partial types in all scopes within the outermost function. Don't + # look beyond the outermost function to allow local reasoning (important for + # fine-grained incremental mode). + partial_types = self.partial_types + for i, t in enumerate(partial_types): + if t.is_function: + partial_types = partial_types[i:] + break + else: + # Not within a function -- only look at the innermost scope. + partial_types = partial_types[-1:] + for scope in reversed(partial_types): + if var in scope.map: + return True, scope.map + for scope in reversed(self.partial_types): + if var in scope.map: + return False, scope.map + return False, None + def temp_node(self, t: Type, context: Optional[Context] = None) -> TempNode: """Create a temporary node with the given, fixed type.""" temp = TempNode(t) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 1cbf611beca7..44543d95604f 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -136,16 +136,20 @@ def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type: # Variable reference. result = self.analyze_var_ref(node, e) if isinstance(result, PartialType): - if result.type is None: + in_scope, partial_types = self.chk.find_partial_types2(node) + if result.type is None and in_scope: # 'None' partial type. It has a well-defined type. In an lvalue context # we want to preserve the knowledge of it being a partial type. if not lvalue: result = NoneTyp() else: - partial_types = self.chk.find_partial_types(node) if partial_types is not None and not self.chk.current_node_deferred: - context = partial_types[node] - self.msg.need_annotation_for_var(node, context) + if in_scope: + context = partial_types[node] + self.msg.need_annotation_for_var(node, context) + else: + # Defer the node -- we might get a better type in the outer scope + self.chk.handle_cannot_determine_type(node.name(), e) result = AnyType(TypeOfAny.special_form) elif isinstance(node, FuncDef): # Reference to a global function. diff --git a/mypy/dmypy_server.py b/mypy/dmypy_server.py index 5428ad7969a0..0ef5395e3907 100644 --- a/mypy/dmypy_server.py +++ b/mypy/dmypy_server.py @@ -180,7 +180,7 @@ def cmd_stop(self) -> Dict[str, object]: """Stop daemon.""" return {} - last_sources = None + last_sources = None # type: List[mypy.build.BuildSource] def cmd_check(self, files: Sequence[str]) -> Dict[str, object]: """Check a list of files.""" diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test index 3bcdf2d6d276..ecd32cdd1d4d 100644 --- a/test-data/unit/check-inference.test +++ b/test-data/unit/check-inference.test @@ -911,6 +911,8 @@ main:5: error: Incompatible types in assignment (expression has type "B", variab main:6: error: Incompatible types in assignment (expression has type "C", variable has type "A") main:10: error: Need more than 2 values to unpack (3 expected) main:12: error: '__main__.B' object is not iterable +main:14: error: Need type annotation for 'xxx' +main:14: error: Need type annotation for 'yyy' [case testInferenceOfFor3] @@ -1403,13 +1405,13 @@ b[{}] = 1 [out] [case testInferDictInitializedToEmptyAndUpdatedFromMethod] -map = {} +map = {} # E: Need type annotation for 'map' def add() -> None: map[1] = 2 [builtins fixtures/dict.pyi] [case testInferDictInitializedToEmptyAndUpdatedFromMethodUnannotated] -map = {} +map = {} # E: Need type annotation for 'map' def add(): map[1] = 2 [builtins fixtures/dict.pyi] @@ -1510,7 +1512,7 @@ x = 1 [out] [case testPartiallyInitializedVariableDoesNotEscapeScope2] -x = None +x = None # E: Need type annotation for 'x' def f() -> None: x = None x = 1 @@ -1537,14 +1539,14 @@ main:6: error: Incompatible types in assignment (expression has type "int", vari main:7: error: "None" not callable [case testGlobalInitializedToNoneSetFromFunction] -a = None +a = None # E: Need type annotation for 'a' def f(): global a a = 42 [out] [case testGlobalInitializedToNoneSetFromMethod] -a = None +a = None # E: Need type annotation for 'a' class C: def m(self): global a @@ -1923,7 +1925,7 @@ T = TypeVar('T', bound=str) def f() -> Tuple[T]: ... -x = None +x = None # E: Need type annotation for 'x' (x,) = f() [out] @@ -2001,3 +2003,37 @@ if bool(): 1() # E: "int" not callable [builtins fixtures/list.pyi] [out] + +--- + +[case testXXX-skip] +class A: + x = {} + def f(self) -> None: + #global x + self.x[0] = '' +reveal_type(A.x) +[builtins fixtures/dict.pyi] + +[case testYYY-skip] +a = [] + +def f() -> None: + a[0] + reveal_type(a) + +a.append(1) +[builtins fixtures/list.pyi] +[out] +x + +[case testZZZ-skip] +a = None + +def f() -> None: + reveal_type(a) +reveal_type(a) +a = '' +[builtins fixtures/list.pyi] +[out] +x diff --git a/test-data/unit/check-optional.test b/test-data/unit/check-optional.test index d7e550ecc212..935a0e9d6527 100644 --- a/test-data/unit/check-optional.test +++ b/test-data/unit/check-optional.test @@ -1,8 +1,15 @@ -- Tests for strict Optional behavior [case testImplicitNoneType] -x = None -x() # E: "None" not callable +def f() -> None: + x = None + x() # E: "None" not callable + +[case testImplicitNoneTypeInNestedFunction] +def f() -> None: + def g() -> None: + x = None + x() # E: "None" not callable [case testExplicitNoneType] x = None # type: None @@ -305,7 +312,7 @@ def f() -> Generator[None, None, None]: [out] [case testNoneAndStringIsNone] -a = None +a = None # type: None b = "foo" reveal_type(a and b) # E: Revealed type is 'builtins.None' @@ -559,7 +566,7 @@ x is not None and x + '42' # E: Unsupported operand types for + ("int" and "str [case testInvalidBooleanBranchIgnored] from typing import Optional -x = None +x = None # type: None x is not None and x + 42 [builtins fixtures/isinstance.pyi] diff --git a/test-data/unit/check-tuples.test b/test-data/unit/check-tuples.test index 3796fb1cb663..d2d6a0348f9a 100644 --- a/test-data/unit/check-tuples.test +++ b/test-data/unit/check-tuples.test @@ -700,8 +700,7 @@ for x in t: pass # E: Need type annotation for 'x' [case testForLoopOverNoneValuedTuple] import typing -t = () -for x in None, None: pass +for x in None, None: pass # E: Need type annotation for 'x' [builtins fixtures/for.pyi] [case testForLoopOverTupleAndSubtyping] From 3e62fdec23533484d95f35582c1ee77fb5048f27 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 7 Feb 2018 11:40:39 +0000 Subject: [PATCH 02/12] Hide local partial types behind an option --- mypy/checker.py | 32 +++-- mypy/main.py | 7 ++ mypy/options.py | 2 + test-data/unit/check-inference.test | 187 ++++++++++++++++++++++++---- test-data/unit/check-optional.test | 9 +- test-data/unit/check-tuples.test | 2 +- 6 files changed, 192 insertions(+), 47 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index d6844723a8be..595451638daa 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3105,15 +3105,12 @@ def enter_partial_types(self, *, is_function: bool = False, partial_types, _ = self.partial_types.pop() if not self.current_node_deferred: for var, context in partial_types.items(): - if (is_function + if ((not self.options.local_partial_types or is_function) and isinstance(var.type, PartialType) and var.type.type is None): - # None partial type within function: assume variable is intended to have - # type None, without requiring an annotation. - # - # Rationale: Assignments within nested functions can complete a partial type, - # so we likely have complete type information, unlike contexts outside - # a function. + # Partial types spanning multiple scopes are fine if all of the partial + # initializers are within a function, since only the topmost function is + # a separate target in fine-grained incremental mode. var.type = NoneTyp() else: if var not in self.partial_reported: @@ -3135,17 +3132,18 @@ def find_partial_types(self, var: Var) -> Optional[Dict[Var, Context]]: return None def find_partial_types2(self, var: Var) -> Tuple[bool, Optional[Dict[Var, Context]]]: - # Look for partial types in all scopes within the outermost function. Don't - # look beyond the outermost function to allow local reasoning (important for - # fine-grained incremental mode). partial_types = self.partial_types - for i, t in enumerate(partial_types): - if t.is_function: - partial_types = partial_types[i:] - break - else: - # Not within a function -- only look at the innermost scope. - partial_types = partial_types[-1:] + if self.options.local_partial_types: + # Look for partial types in all scopes within the outermost function. Don't + # look beyond the outermost function to allow local reasoning (important for + # fine-grained incremental mode). + for i, t in enumerate(partial_types): + if t.is_function: + partial_types = partial_types[i:] + break + else: + # Not within a function -- only look at the innermost scope. + partial_types = partial_types[-1:] for scope in reversed(partial_types): if var in scope.map: return True, scope.map diff --git a/mypy/main.py b/mypy/main.py index cbcab24d2b01..198cfdcbd518 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -370,6 +370,9 @@ def add_invertible_flag(flag: str, parser.add_argument('--dump-graph', action='store_true', help=argparse.SUPPRESS) # --semantic-analysis-only does exactly that. parser.add_argument('--semantic-analysis-only', action='store_true', help=argparse.SUPPRESS) + # --local-partial-types disallows partial types spanning module top level and a function + # (implicitly defined in fine-grained incremental mode) + parser.add_argument('--local-partial-types', action='store_true', help=argparse.SUPPRESS) # deprecated options parser.add_argument('--disallow-any', dest='special-opts:disallow_any', help=argparse.SUPPRESS) @@ -503,6 +506,10 @@ def add_invertible_flag(flag: str, parser.error("Can only find occurrences of class members.") if len(experiments.find_occurrences) != 2: parser.error("Can only find occurrences of non-nested class members.") + if options.fine_grained_incremental: + # Fine-grained incremental doesn't support general partial types + # (details in https://github.com/python/mypy/issues/4492) + options.local_partial_types = True # Set reports. for flag, val in vars(special_opts).items(): diff --git a/mypy/options.py b/mypy/options.py index cb7871778f29..667d88065648 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -174,6 +174,8 @@ def __init__(self) -> None: self.show_column_numbers = False # type: bool self.dump_graph = False self.dump_deps = False + # If True, partial types can't span a module top level and a function + self.local_partial_types = False def __eq__(self, other: object) -> bool: return self.__class__ == other.__class__ and self.__dict__ == other.__dict__ diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test index ecd32cdd1d4d..e66d0441726c 100644 --- a/test-data/unit/check-inference.test +++ b/test-data/unit/check-inference.test @@ -911,8 +911,6 @@ main:5: error: Incompatible types in assignment (expression has type "B", variab main:6: error: Incompatible types in assignment (expression has type "C", variable has type "A") main:10: error: Need more than 2 values to unpack (3 expected) main:12: error: '__main__.B' object is not iterable -main:14: error: Need type annotation for 'xxx' -main:14: error: Need type annotation for 'yyy' [case testInferenceOfFor3] @@ -1405,13 +1403,13 @@ b[{}] = 1 [out] [case testInferDictInitializedToEmptyAndUpdatedFromMethod] -map = {} # E: Need type annotation for 'map' +map = {} def add() -> None: map[1] = 2 [builtins fixtures/dict.pyi] [case testInferDictInitializedToEmptyAndUpdatedFromMethodUnannotated] -map = {} # E: Need type annotation for 'map' +map = {} def add(): map[1] = 2 [builtins fixtures/dict.pyi] @@ -1512,7 +1510,7 @@ x = 1 [out] [case testPartiallyInitializedVariableDoesNotEscapeScope2] -x = None # E: Need type annotation for 'x' +x = None def f() -> None: x = None x = 1 @@ -1539,14 +1537,14 @@ main:6: error: Incompatible types in assignment (expression has type "int", vari main:7: error: "None" not callable [case testGlobalInitializedToNoneSetFromFunction] -a = None # E: Need type annotation for 'a' +a = None def f(): global a a = 42 [out] [case testGlobalInitializedToNoneSetFromMethod] -a = None # E: Need type annotation for 'a' +a = None class C: def m(self): global a @@ -1792,6 +1790,7 @@ main:3: error: "int" not callable -- Tests for special cases of unification -- -------------------------------------- + [case testUnificationRedundantUnion] from typing import Union a = None # type: Union[int, str] @@ -1925,7 +1924,7 @@ T = TypeVar('T', bound=str) def f() -> Tuple[T]: ... -x = None # E: Need type annotation for 'x' +x = None (x,) = f() [out] @@ -2004,36 +2003,176 @@ if bool(): [builtins fixtures/list.pyi] [out] ---- -[case testXXX-skip] +-- --local-partial-types +-- --------------------- + + +[case testLocalPartialTypesWithGlobalInitializedToNone] +# flags: --local-partial-types +x = None # E: Need type annotation for 'x' + +def f() -> None: + global x + x = 1 # E: Incompatible types in assignment (expression has type "int", variable has type "None") + +[case testLocalPartialTypesWithGlobalInitializedToNone2] +# flags: --local-partial-types +x = None # E: Need type annotation for 'x' + +def f(): + global x + x = 1 + +reveal_type(x) # E: Revealed type is 'builtins.None' + +[case testLocalPartialTypesWithGlobalInitializedToNone3] +# flags: --local-partial-types +x = None + +def f() -> None: + global x + x = 1 # E: Incompatible types in assignment (expression has type "int", variable has type "str") + +x = '' +reveal_type(x) # E: Revealed type is 'builtins.str' + +[case testLocalPartialTypesWithGlobalInitializedToNoneStrictOptional] +# flags: --local-partial-types --strict-optional +x = None + +def f() -> None: + global x + x = 1 # E: Incompatible types in assignment (expression has type "int", variable has type "Optional[str]") + +x = '' +def g() -> None: + reveal_type(x) # E: Revealed type is 'Union[builtins.str, builtins.None]' + +[case testLocalPartialTypesWithGlobalInitializedToNone4] +# flags: --local-partial-types +a = None + +def f() -> None: + reveal_type(a) # E: Revealed type is 'builtins.str' + +# TODO: This should probably be 'builtins.str', since there could be a +# call causes a non-None value to be assigned +reveal_type(a) # E: Revealed type is 'builtins.None' +a = '' +reveal_type(a) # E: Revealed type is 'builtins.str' +[builtins fixtures/list.pyi] + +[case testLocalPartialTypesWithClassAttributeInitializedToNone] +# flags: --local-partial-types class A: - x = {} + x = None # E: Need type annotation for 'x' + + def f(self) -> None: + self.x = 1 + +[case testLocalPartialTypesWithClassAttributeInitializedToEmptyDict] +# flags: --local-partial-types +class A: + x = {} # E: Need type annotation for 'x' + def f(self) -> None: - #global x self.x[0] = '' -reveal_type(A.x) + +reveal_type(A().x) # E: Revealed type is 'Any' +reveal_type(A.x) # E: Revealed type is 'Any' [builtins fixtures/dict.pyi] -[case testYYY-skip] +[case testLocalPartialTypesWithGlobalInitializedToEmptyList] +# flags: --local-partial-types a = [] def f() -> None: a[0] - reveal_type(a) + reveal_type(a) # E: Revealed type is 'builtins.list[builtins.int]' a.append(1) +reveal_type(a) # E: Revealed type is 'builtins.list[builtins.int]' [builtins fixtures/list.pyi] -[out] -x -[case testZZZ-skip] -a = None +[case testLocalPartialTypesWithGlobalInitializedToEmptyList2] +# flags: --local-partial-types +a = [] # E: Need type annotation for 'a' def f() -> None: - reveal_type(a) -reveal_type(a) -a = '' + a.append(1) + reveal_type(a) # E: Revealed type is 'Any' + +reveal_type(a) # E: Revealed type is 'Any' [builtins fixtures/list.pyi] -[out] -x + +[case testLocalPartialTypesWithGlobalInitializedToEmptyList3] +# flags: --local-partial-types +a = [] # E: Need type annotation for 'a' + +def f(): + a.append(1) + +reveal_type(a) # E: Revealed type is 'Any' +[builtins fixtures/list.pyi] + +[case testLocalPartialTypesWithGlobalInitializedToEmptyDict] +# flags: --local-partial-types +a = {} + +def f() -> None: + a[0] + reveal_type(a) # E: Revealed type is 'builtins.dict[builtins.int, builtins.str]' + +a[0] = '' +reveal_type(a) # E: Revealed type is 'builtins.dict[builtins.int, builtins.str]' +[builtins fixtures/dict.pyi] + +[case testLocalPartialTypesWithGlobalInitializedToEmptyDict2] +# flags: --local-partial-types +a = {} # E: Need type annotation for 'a' + +def f() -> None: + a[0] = '' + reveal_type(a) # E: Revealed type is 'Any' + +reveal_type(a) # E: Revealed type is 'Any' +[builtins fixtures/dict.pyi] + +[case testLocalPartialTypesWithGlobalInitializedToEmptyDict3] +# flags: --local-partial-types +a = {} # E: Need type annotation for 'a' + +def f(): + a[0] = '' + +reveal_type(a) # E: Revealed type is 'Any' +[builtins fixtures/dict.pyi] + +[case testLocalPartialTypesWithNestedFunction] +# flags: --local-partial-types +def f() -> None: + a = {} + def g() -> None: + a[0] = '' + reveal_type(a) # E: Revealed type is 'builtins.dict[builtins.int, builtins.str]' +[builtins fixtures/dict.pyi] + +[case testLocalPartialTypesWithNestedFunction2] +# flags: --local-partial-types +def f() -> None: + a = [] + def g() -> None: + a.append(1) + reveal_type(a) # E: Revealed type is 'builtins.list[builtins.int]' +[builtins fixtures/list.pyi] + +[case testLocalPartialTypesWithNestedFunction3] +# flags: --local-partial-types +def f() -> None: + a = None + def g() -> None: + nonlocal a + a = '' + reveal_type(a) # E: Revealed type is 'builtins.str' +[builtins fixtures/dict.pyi] diff --git a/test-data/unit/check-optional.test b/test-data/unit/check-optional.test index 935a0e9d6527..d2f699dd0cdb 100644 --- a/test-data/unit/check-optional.test +++ b/test-data/unit/check-optional.test @@ -1,9 +1,8 @@ -- Tests for strict Optional behavior [case testImplicitNoneType] -def f() -> None: - x = None - x() # E: "None" not callable +x = None +x() # E: "None" not callable [case testImplicitNoneTypeInNestedFunction] def f() -> None: @@ -312,7 +311,7 @@ def f() -> Generator[None, None, None]: [out] [case testNoneAndStringIsNone] -a = None # type: None +a = None b = "foo" reveal_type(a and b) # E: Revealed type is 'builtins.None' @@ -566,7 +565,7 @@ x is not None and x + '42' # E: Unsupported operand types for + ("int" and "str [case testInvalidBooleanBranchIgnored] from typing import Optional -x = None # type: None +x = None x is not None and x + 42 [builtins fixtures/isinstance.pyi] diff --git a/test-data/unit/check-tuples.test b/test-data/unit/check-tuples.test index d2d6a0348f9a..460ff93a5b8c 100644 --- a/test-data/unit/check-tuples.test +++ b/test-data/unit/check-tuples.test @@ -700,7 +700,7 @@ for x in t: pass # E: Need type annotation for 'x' [case testForLoopOverNoneValuedTuple] import typing -for x in None, None: pass # E: Need type annotation for 'x' +for x in None, None: pass [builtins fixtures/for.pyi] [case testForLoopOverTupleAndSubtyping] From 9059894a3debf95173c5a78f4cf5e21bc15490a7 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 7 Feb 2018 12:54:54 +0000 Subject: [PATCH 03/12] Set local_partial_types correctly --- mypy/dmypy_server.py | 4 ++ mypy/main.py | 4 -- mypy/test/testdmypy.py | 1 + mypy/test/testfinegrained.py | 2 +- test-data/unit/check-dmypy-fine-grained.test | 20 ++++++++ test-data/unit/fine-grained.test | 51 ++++++++++++++++++++ 6 files changed, 77 insertions(+), 5 deletions(-) diff --git a/mypy/dmypy_server.py b/mypy/dmypy_server.py index 0ef5395e3907..c51c28d5f3ba 100644 --- a/mypy/dmypy_server.py +++ b/mypy/dmypy_server.py @@ -111,6 +111,10 @@ def __init__(self, flags: List[str]) -> None: options.cache_fine_grained = True # set this so that cache options match else: options.cache_dir = os.devnull + options.cache_dir = os.devnull + # Fine-grained incremental doesn't support general partial types + # (details in https://github.com/python/mypy/issues/4492) + options.local_partial_types = True def serve(self) -> None: """Serve requests, synchronously (no thread or fork).""" diff --git a/mypy/main.py b/mypy/main.py index 198cfdcbd518..e759c8fae58a 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -506,10 +506,6 @@ def add_invertible_flag(flag: str, parser.error("Can only find occurrences of class members.") if len(experiments.find_occurrences) != 2: parser.error("Can only find occurrences of non-nested class members.") - if options.fine_grained_incremental: - # Fine-grained incremental doesn't support general partial types - # (details in https://github.com/python/mypy/issues/4492) - options.local_partial_types = True # Set reports. for flag, val in vars(special_opts).items(): diff --git a/mypy/test/testdmypy.py b/mypy/test/testdmypy.py index 7d9f96a4c7e2..852a4c201169 100644 --- a/mypy/test/testdmypy.py +++ b/mypy/test/testdmypy.py @@ -120,6 +120,7 @@ def run_case_once(self, testcase: DataDrivenTestCase, incremental_step: int) -> if 'fine-grained' in testcase.file: server_options.append('--experimental') options.fine_grained_incremental = True + options.local_partial_types = True self.server = dmypy_server.Server(server_options) # TODO: Fix ugly API self.server.options = options diff --git a/mypy/test/testfinegrained.py b/mypy/test/testfinegrained.py index 5cd5c04320ae..540bf51bf1cd 100644 --- a/mypy/test/testfinegrained.py +++ b/mypy/test/testfinegrained.py @@ -142,7 +142,7 @@ def build(self, options.fine_grained_incremental = not build_cache options.use_fine_grained_cache = enable_cache and not build_cache options.cache_fine_grained = enable_cache - + options.local_partial_types = True main_path = os.path.join(test_temp_dir, 'main') with open(main_path, 'w') as f: f.write(source) diff --git a/test-data/unit/check-dmypy-fine-grained.test b/test-data/unit/check-dmypy-fine-grained.test index 23907d256ea8..b51e92e1c968 100644 --- a/test-data/unit/check-dmypy-fine-grained.test +++ b/test-data/unit/check-dmypy-fine-grained.test @@ -181,3 +181,23 @@ tmp/a.py:1: error: "int" not callable [delete nonexistent_stub.pyi.2] [out1] [out2] + +[case testPartialNoneTypeFineGrainedIncremental] +# cmd: mypy -m a b +[file a.py] +import b +b.y +x = None +def f() -> None: + global x + x = '' +[file b.py] +y = 0 +[file b.py.2] +y = '' +[out1] +tmp/a.py:3: error: Need type annotation for 'x' +tmp/a.py:6: error: Incompatible types in assignment (expression has type "str", variable has type "None") +[out2] +tmp/a.py:3: error: Need type annotation for 'x' +tmp/a.py:6: error: Incompatible types in assignment (expression has type "str", variable has type "None") diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index a0838915910d..909fb89fcc97 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -1796,3 +1796,54 @@ def foo(x: Point) -> int: [out] == b.py:3: error: Unsupported operand types for + ("int" and "str") + +[case testNonePartialType] +import a +a.y + +x = None + +def f() -> None: + global x + x = 1 +[file a.py] +y = 0 +[file a.py.2] +y = '' +[out] +main:4: error: Need type annotation for 'x' +main:8: error: Incompatible types in assignment (expression has type "int", variable has type "None") +== +main:4: error: Need type annotation for 'x' +main:8: error: Incompatible types in assignment (expression has type "int", variable has type "None") + +[case testNonePartialType2] +import a +a.y + +x = None + +def f(): + global x + x = 1 +[file a.py] +y = 0 +[file a.py.2] +y = '' +[out] +main:4: error: Need type annotation for 'x' +== +main:4: error: Need type annotation for 'x' + +[case testNonePartialType3] +import a +[file a.py] +[file a.py.2] +y = None +def f() -> None: + global y + y = '' +[out] +== +a.py:1: error: Need type annotation for 'y' +a.py:4: error: Incompatible types in assignment (expression has type "str", variable has type "None") From 9d242db7d8fbd2f1f3f115f9a3d6aec9978594fc Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 8 Feb 2018 11:46:59 +0000 Subject: [PATCH 04/12] Some refactoring --- mypy/checker.py | 35 +++++++++++++++++++++++------------ mypy/checkexpr.py | 2 +- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 595451638daa..d415528eacd6 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3126,28 +3126,39 @@ def enter_partial_types(self, *, is_function: bool = False, var.type = AnyType(TypeOfAny.from_error) def find_partial_types(self, var: Var) -> Optional[Dict[Var, Context]]: - in_scope, partial_types = self.find_partial_types2(var) + """Look for partial type scope containing variable that is in scope.""" + in_scope, partial_types = self.find_partial_types_in_all_scopes(var) if in_scope: return partial_types return None - def find_partial_types2(self, var: Var) -> Tuple[bool, Optional[Dict[Var, Context]]]: - partial_types = self.partial_types + def find_partial_types_in_all_scopes(self, var: Var) -> Tuple[bool, + Optional[Dict[Var, Context]]]: + """Look for partial type scope containing variable. + + Return tuple (is the scope active, scope). + """ + active = self.partial_types + inactive = [] # type: List[PartialTypeScope] if self.options.local_partial_types: - # Look for partial types in all scopes within the outermost function. Don't - # look beyond the outermost function to allow local reasoning (important for - # fine-grained incremental mode). - for i, t in enumerate(partial_types): + # All scopes within the outermost function are active. Scopes out of + # the outermost function are inactive to allow local reasoning (important + # for fine-grained incremental mode). + for i, t in enumerate(self.partial_types): if t.is_function: - partial_types = partial_types[i:] + active = self.partial_types[i:] + inactive = self.partial_types[:i] break else: - # Not within a function -- only look at the innermost scope. - partial_types = partial_types[-1:] - for scope in reversed(partial_types): + # Not within a function -- only the innermost scope is in scope. + active = self.partial_types[-1:] + inactive = self.partial_types[:-1] + # First look within in-scope partial types. + for scope in reversed(active): if var in scope.map: return True, scope.map - for scope in reversed(self.partial_types): + # Then for out-of-scope partial types. + for scope in reversed(inactive): if var in scope.map: return False, scope.map return False, None diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 44543d95604f..acb51b6d1827 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -136,7 +136,7 @@ def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type: # Variable reference. result = self.analyze_var_ref(node, e) if isinstance(result, PartialType): - in_scope, partial_types = self.chk.find_partial_types2(node) + in_scope, partial_types = self.chk.find_partial_types_in_all_scopes(node) if result.type is None and in_scope: # 'None' partial type. It has a well-defined type. In an lvalue context # we want to preserve the knowledge of it being a partial type. From 20a5c54a71613d78e13884e25fa3e7407a46ce8d Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 8 Feb 2018 12:40:45 +0000 Subject: [PATCH 05/12] Add two failing test cases --- test-data/unit/check-inference.test | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test index e66d0441726c..79c0f0e53b6c 100644 --- a/test-data/unit/check-inference.test +++ b/test-data/unit/check-inference.test @@ -2176,3 +2176,32 @@ def f() -> None: a = '' reveal_type(a) # E: Revealed type is 'builtins.str' [builtins fixtures/dict.pyi] + +[case testLocalPartialTypesWithInheritance-skip] +# flags: --local-partial-types +from typing import Optional + +class A: + x: Optional[str] + +class B(A): + x = None # Generates an invalid request for a type annotation + +reveal_type(B.x) # E: Revealed type is 'builtins.None' + +[case testLocalPartialTypesWithInheritance2-skip] +# flags: --local-partial-types +# This is failing because of https://github.com/python/mypy/issues/4552 +from typing import Optional + +class X: pass +class Y(X): pass + +class A: + x: Optional[X] + +class B(A): + x = None + x = Y() + +reveal_type(B.x) # E: revealed type is 'Union[__main__.Y, builtins.None]' From 07599a8c9f898e5b01ef2fae708421cfa6d93134 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 8 Feb 2018 13:09:46 +0000 Subject: [PATCH 06/12] Fix special cases --- mypy/checker.py | 20 ++++++++++++++++---- test-data/unit/check-inference.test | 24 ++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index d415528eacd6..1eef7d21ca42 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3105,12 +3105,15 @@ def enter_partial_types(self, *, is_function: bool = False, partial_types, _ = self.partial_types.pop() if not self.current_node_deferred: for var, context in partial_types.items(): - if ((not self.options.local_partial_types or is_function) + # Partial types spanning multiple scopes are fine if all of the partial + # initializers are within a function, since only the topmost function is + # a separate target in fine-grained incremental mode. + allow_none = (not self.options.local_partial_types + or is_function + or (is_class and self.is_defined_in_base_class(var))) + if (allow_none and isinstance(var.type, PartialType) and var.type.type is None): - # Partial types spanning multiple scopes are fine if all of the partial - # initializers are within a function, since only the topmost function is - # a separate target in fine-grained incremental mode. var.type = NoneTyp() else: if var not in self.partial_reported: @@ -3125,6 +3128,15 @@ def enter_partial_types(self, *, is_function: bool = False, else: var.type = AnyType(TypeOfAny.from_error) + def is_defined_in_base_class(self, var: Var) -> bool: + if var.info is not None: + for base in var.info.mro[1:]: + if base.get(var.name()) is not None: + return True + if var.info.fallback_to_any: + return True + return False + def find_partial_types(self, var: Var) -> Optional[Dict[Var, Context]]: """Look for partial type scope containing variable that is in scope.""" in_scope, partial_types = self.find_partial_types_in_all_scopes(var) diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test index 79c0f0e53b6c..d35694cd93aa 100644 --- a/test-data/unit/check-inference.test +++ b/test-data/unit/check-inference.test @@ -2177,7 +2177,7 @@ def f() -> None: reveal_type(a) # E: Revealed type is 'builtins.str' [builtins fixtures/dict.pyi] -[case testLocalPartialTypesWithInheritance-skip] +[case testLocalPartialTypesWithInheritance] # flags: --local-partial-types from typing import Optional @@ -2185,10 +2185,30 @@ class A: x: Optional[str] class B(A): - x = None # Generates an invalid request for a type annotation + x = None reveal_type(B.x) # E: Revealed type is 'builtins.None' +[case testLocalPartialTypesWithInheritance2] +# flags: --local-partial-types --strict-optional +class A: + x: str + +class B(A): + x = None # E: Incompatible types in assignment (expression has type "None", base class "A" defined the type as "str") + +[case testLocalPartialTypesWithAnyBaseClass] +# flags: --local-partial-types --strict-optional +from typing import Any + +A: Any + +class B(A): + x = None + +class C(B): + y = None + [case testLocalPartialTypesWithInheritance2-skip] # flags: --local-partial-types # This is failing because of https://github.com/python/mypy/issues/4552 From 3f57aae4e45e2ebaa14c7bc9f1e9e6a48465dabd Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 8 Feb 2018 14:06:55 +0000 Subject: [PATCH 07/12] Fix crash --- mypy/checker.py | 2 +- test-data/unit/check-inference.test | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 1eef7d21ca42..7483843441b7 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1468,7 +1468,7 @@ def check_assignment(self, lvalue: Lvalue, rvalue: Expression, infer_lvalue_type lvalue_type.item.type.is_protocol)): self.msg.concrete_only_assign(lvalue_type, rvalue) return - if rvalue_type and infer_lvalue_type: + if rvalue_type and infer_lvalue_type and not isinstance(lvalue_type, PartialType): self.binder.assign_type(lvalue, rvalue_type, lvalue_type, False) elif index_lvalue: self.check_indexed_assignment(index_lvalue, rvalue, lvalue) diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test index d35694cd93aa..86653ed2b926 100644 --- a/test-data/unit/check-inference.test +++ b/test-data/unit/check-inference.test @@ -2225,3 +2225,17 @@ class B(A): x = Y() reveal_type(B.x) # E: revealed type is 'Union[__main__.Y, builtins.None]' + +[case testLocalPartialTypesBinderSpecialCase] +# flags: --local-partial-types +from typing import List + +def f(x): pass + +class A: + x = None # E: Need type annotation for 'x' + + def f(self, p: List[str]) -> None: + self.x = f(p) + f(z for z in p) +[builtins fixtures/list.pyi] From 3cc3ce4ac1dc05cc2414a388298f1d954a19460e Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 8 Feb 2018 14:49:13 +0000 Subject: [PATCH 08/12] Avoid generating multiple messages from a single error --- mypy/checker.py | 11 +++-------- test-data/unit/check-dmypy-fine-grained.test | 2 -- test-data/unit/check-inference.test | 2 +- test-data/unit/fine-grained.test | 3 --- 4 files changed, 4 insertions(+), 14 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 7483843441b7..641c206bed88 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3119,14 +3119,9 @@ def enter_partial_types(self, *, is_function: bool = False, if var not in self.partial_reported: self.msg.need_annotation_for_var(var, context) self.partial_reported.add(var) - if isinstance(var.type, PartialType) and var.type.type is None: - # None partial type outside function: assume variable is intended to have - # type None, but require an annotation since None is likely just a default - # and there is likely another assignment in a function that won't affect - # the partial type. - var.type = NoneTyp() - else: - var.type = AnyType(TypeOfAny.from_error) + # Give the variable an 'Any' type to avoid generating multiple errors + # from a single missing annotation. + var.type = AnyType(TypeOfAny.from_error) def is_defined_in_base_class(self, var: Var) -> bool: if var.info is not None: diff --git a/test-data/unit/check-dmypy-fine-grained.test b/test-data/unit/check-dmypy-fine-grained.test index b51e92e1c968..0544c9b700a7 100644 --- a/test-data/unit/check-dmypy-fine-grained.test +++ b/test-data/unit/check-dmypy-fine-grained.test @@ -197,7 +197,5 @@ y = 0 y = '' [out1] tmp/a.py:3: error: Need type annotation for 'x' -tmp/a.py:6: error: Incompatible types in assignment (expression has type "str", variable has type "None") [out2] tmp/a.py:3: error: Need type annotation for 'x' -tmp/a.py:6: error: Incompatible types in assignment (expression has type "str", variable has type "None") diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test index 86653ed2b926..331d71d02967 100644 --- a/test-data/unit/check-inference.test +++ b/test-data/unit/check-inference.test @@ -2014,7 +2014,7 @@ x = None # E: Need type annotation for 'x' def f() -> None: global x - x = 1 # E: Incompatible types in assignment (expression has type "int", variable has type "None") + x = 1 [case testLocalPartialTypesWithGlobalInitializedToNone2] # flags: --local-partial-types diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 909fb89fcc97..1bff479b605b 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -1812,10 +1812,8 @@ y = 0 y = '' [out] main:4: error: Need type annotation for 'x' -main:8: error: Incompatible types in assignment (expression has type "int", variable has type "None") == main:4: error: Need type annotation for 'x' -main:8: error: Incompatible types in assignment (expression has type "int", variable has type "None") [case testNonePartialType2] import a @@ -1846,4 +1844,3 @@ def f() -> None: [out] == a.py:1: error: Need type annotation for 'y' -a.py:4: error: Incompatible types in assignment (expression has type "str", variable has type "None") From b165730fc71080504f5821d8b095dc300ec0cdbd Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 8 Feb 2018 15:20:22 +0000 Subject: [PATCH 09/12] Minor tweaks --- mypy/checker.py | 25 ++++++++++++++++++++----- mypy/dmypy_server.py | 1 - mypy/test/testfinegrained.py | 1 + test-data/unit/check-inference.test | 2 +- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 641c206bed88..f3d3e7a890a5 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3096,7 +3096,7 @@ def enter_partial_types(self, *, is_function: bool = False, is_class: bool = False) -> Iterator[None]: """Enter a new scope for collecting partial types. - Also report errors for variables which still have partial + Also report errors for (some) variables which still have partial types, i.e. we couldn't infer a complete type. """ self.partial_types.append(PartialTypeScope({}, is_function)) @@ -3105,9 +3105,19 @@ def enter_partial_types(self, *, is_function: bool = False, partial_types, _ = self.partial_types.pop() if not self.current_node_deferred: for var, context in partial_types.items(): - # Partial types spanning multiple scopes are fine if all of the partial - # initializers are within a function, since only the topmost function is - # a separate target in fine-grained incremental mode. + # If we require local partial types, there are a few exceptions where + # we fall back to inferring just "None" as the type from a None initaliazer: + # + # 1. If all happens within a single function this is acceptable, since only + # the topmost function is a separate target in fine-grained incremental mode. + # We primarily want to avoid "splitting" partial types across targets. + # + # 2. A None initializer in the class body if the attribute is defined in a base + # class is fine, since the attribute is already defined and it's currently okay + # to vary the type of an attribute covariantly. The None type will still be + # checked for compatibility with base classes elsewhere. Without this exception + # mypy could require an annotation for an attribute that already has been + # declared in a base class, which would be bad. allow_none = (not self.options.local_partial_types or is_function or (is_class and self.is_defined_in_base_class(var))) @@ -3133,7 +3143,12 @@ def is_defined_in_base_class(self, var: Var) -> bool: return False def find_partial_types(self, var: Var) -> Optional[Dict[Var, Context]]: - """Look for partial type scope containing variable that is in scope.""" + """Look for an active partial type scope containing variable. + + A scope is active if assignments in the current context can refine a partial + type originally defined in the scope. This is affected by the local_partial_types + configuration option. + """ in_scope, partial_types = self.find_partial_types_in_all_scopes(var) if in_scope: return partial_types diff --git a/mypy/dmypy_server.py b/mypy/dmypy_server.py index c51c28d5f3ba..16786924eb8b 100644 --- a/mypy/dmypy_server.py +++ b/mypy/dmypy_server.py @@ -111,7 +111,6 @@ def __init__(self, flags: List[str]) -> None: options.cache_fine_grained = True # set this so that cache options match else: options.cache_dir = os.devnull - options.cache_dir = os.devnull # Fine-grained incremental doesn't support general partial types # (details in https://github.com/python/mypy/issues/4492) options.local_partial_types = True diff --git a/mypy/test/testfinegrained.py b/mypy/test/testfinegrained.py index 540bf51bf1cd..469f97e6b519 100644 --- a/mypy/test/testfinegrained.py +++ b/mypy/test/testfinegrained.py @@ -143,6 +143,7 @@ def build(self, options.use_fine_grained_cache = enable_cache and not build_cache options.cache_fine_grained = enable_cache options.local_partial_types = True + main_path = os.path.join(test_temp_dir, 'main') with open(main_path, 'w') as f: f.write(source) diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test index 331d71d02967..fd0ef35b3936 100644 --- a/test-data/unit/check-inference.test +++ b/test-data/unit/check-inference.test @@ -2057,7 +2057,7 @@ def f() -> None: reveal_type(a) # E: Revealed type is 'builtins.str' # TODO: This should probably be 'builtins.str', since there could be a -# call causes a non-None value to be assigned +# call that causes a non-None value to be assigned reveal_type(a) # E: Revealed type is 'builtins.None' a = '' reveal_type(a) # E: Revealed type is 'builtins.str' From bf0a9a1dcae73061eb148fed31c30e42af5bcc96 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 8 Feb 2018 16:33:17 +0000 Subject: [PATCH 10/12] Add test cases --- test-data/unit/check-inference.test | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test index fd0ef35b3936..3cfe06f199f6 100644 --- a/test-data/unit/check-inference.test +++ b/test-data/unit/check-inference.test @@ -2239,3 +2239,21 @@ class A: self.x = f(p) f(z for z in p) [builtins fixtures/list.pyi] + +[case testLocalPartialTypesAccessPartialNoneAttribute] +# flags: --local-partial-types +class C: + a = None # E: Need type annotation for 'a' + + def f(self, x) -> None: + # TODO: It would be better for the type to be Any here + C.a.y # E: "None" has no attribute "y" + +[case testLocalPartialTypesAccessPartialNoneAttribute] +# flags: --local-partial-types +class C: + a = None # E: Need type annotation for 'a' + + def f(self, x) -> None: + # TODO: It would be better for the type to be Any here + self.a.y # E: "None" has no attribute "y" From 1763a8c398544199515483790928c9bfde10e21b Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 8 Feb 2018 17:41:24 +0000 Subject: [PATCH 11/12] Minor test updates --- test-data/unit/check-inference.test | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test index 3cfe06f199f6..bd909240e96d 100644 --- a/test-data/unit/check-inference.test +++ b/test-data/unit/check-inference.test @@ -2016,6 +2016,9 @@ def f() -> None: global x x = 1 +# TODO: "Any" could be a better type here to avoid multiple error messages +reveal_type(x) # E: Revealed type is 'builtins.None' + [case testLocalPartialTypesWithGlobalInitializedToNone2] # flags: --local-partial-types x = None # E: Need type annotation for 'x' @@ -2024,6 +2027,7 @@ def f(): global x x = 1 +# TODO: "Any" could be a better type here to avoid multiple error messages reveal_type(x) # E: Revealed type is 'builtins.None' [case testLocalPartialTypesWithGlobalInitializedToNone3] From 3f3e4f06cd3cce12032cb6c8e62de7a65afc12f1 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 19 Feb 2018 17:45:05 +0000 Subject: [PATCH 12/12] Update based on feedback --- mypy/checker.py | 8 +++++--- test-data/unit/check-inference.test | 17 +++++++++++++++++ test-data/unit/fine-grained.test | 17 +++++++++++++++++ 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index aa8a0935a01d..922238739475 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -107,9 +107,11 @@ ('is_upper_bound', bool), # False => precise type ]) -# Keeps track of partial types in a single scope -PartialTypeScope = NamedTuple('PartialTypeMap', [('map', Dict[Var, Context]), - ('is_function', bool)]) +# Keeps track of partial types in a single scope. In fine-grained incremental +# mode partial types initially defined at the top level cannot be completed in +# a function, and we use the 'is_function' attribute to enforce this. +PartialTypeScope = NamedTuple('PartialTypeScope', [('map', Dict[Var, Context]), + ('is_function', bool)]) class TypeChecker(NodeVisitor[None], CheckerPluginInterface): diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test index bd909240e96d..9db6d23d2c8c 100644 --- a/test-data/unit/check-inference.test +++ b/test-data/unit/check-inference.test @@ -2213,6 +2213,23 @@ class B(A): class C(B): y = None +[case testLocalPartialTypesInMultipleMroItems] +# flags: --local-partial-types --strict-optional +from typing import Optional + +class A: + x: Optional[str] + +class B(A): + x = None + +class C(B): + x = None + +# TODO: Inferring None below is unsafe (https://github.com/python/mypy/issues/3208) +reveal_type(B.x) # E: Revealed type is 'builtins.None' +reveal_type(C.x) # E: Revealed type is 'builtins.None' + [case testLocalPartialTypesWithInheritance2-skip] # flags: --local-partial-types # This is failing because of https://github.com/python/mypy/issues/4552 diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 1bff479b605b..59ddaf3a6b47 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -1844,3 +1844,20 @@ def f() -> None: [out] == a.py:1: error: Need type annotation for 'y' + +[case testNonePartialType4] +import a +[file a.py] +y = None +def f() -> None: + global y + y = '' +[file a.py.2] +from typing import Optional +y: Optional[str] = None +def f() -> None: + global y + y = '' +[out] +a.py:1: error: Need type annotation for 'y' +==