From f20cdc3c7acdd0a2e8b4afe4cb355a3202686a6a Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Mon, 17 Sep 2018 13:51:43 -0700 Subject: [PATCH 1/2] Get rid of Optional when assigning Any in an `is None` branch As a special case, when assigning Any to a variable with a declared Optional type that has been narrowed to None, replace all the Nones in the declared Union type with Any. The result of this is: ``` def f():... def g(x: Optional[int]) -> int: if x is None: x = f() reveal_type(x) # Union[int, Any] # <---- this is new (was Optional[int]) reveal_type(x) # Union[int, Any] # was also Optional[int] return x # Ok # was an error ``` Fixes #3526. --- mypy/binder.py | 18 +++++++++-- test-data/unit/check-optional.test | 49 ++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/mypy/binder.py b/mypy/binder.py index f943621aef2d1..a1efefff9be96 100644 --- a/mypy/binder.py +++ b/mypy/binder.py @@ -6,7 +6,7 @@ if MYPY: from typing import DefaultDict -from mypy.types import Type, AnyType, PartialType, UnionType, TypeOfAny +from mypy.types import Type, AnyType, PartialType, UnionType, TypeOfAny, NoneTyp from mypy.subtypes import is_subtype from mypy.join import join_simple from mypy.sametypes import is_same_type @@ -276,9 +276,23 @@ def assign_type(self, expr: Expression, # isinstance check), but now it is reassigned, so broaden back # to Any (which is the most recent enclosing type) self.put(expr, enclosing_type) + # As a special case, when assigning Any to a variable with a + # declared Optional type that has been narrowed to None, + # replace all the Nones in the declared Union type with Any. + # This overrides the normal behavior of ignoring Any assignments to variables + # in order to prevent false positives. + # (See discussion in #3526) + elif (isinstance(type, AnyType) + and isinstance(declared_type, UnionType) + and any(isinstance(item, NoneTyp) for item in declared_type.items) + and isinstance(self.most_recent_enclosing_type(expr, NoneTyp()), NoneTyp)): + # Replace any Nones in the union type with Any + new_items = [type if isinstance(item, NoneTyp) else item + for item in declared_type.items] + self.put(expr, UnionType(new_items)) elif (isinstance(type, AnyType) and not (isinstance(declared_type, UnionType) - and any(isinstance(item, AnyType) for item in declared_type.items))): + and (any(isinstance(item, AnyType) for item in declared_type.items)))): # Assigning an Any value doesn't affect the type to avoid false negatives, unless # there is an Any item in a declared union type. self.put(expr, declared_type) diff --git a/test-data/unit/check-optional.test b/test-data/unit/check-optional.test index 9b1c7a694713d..955dd1cfe3191 100644 --- a/test-data/unit/check-optional.test +++ b/test-data/unit/check-optional.test @@ -698,3 +698,52 @@ def g(x: str) -> None: pass y = int() [builtins fixtures/bool.pyi] + +[case testOptionalAssignAny1] +from typing import Optional +def f(): + return 0 + +def g(x: Optional[int]) -> int: + if x is None: + reveal_type(x) # E: Revealed type is 'None' + # As a special case for Unions containing None, during + x = f() + reveal_type(x) # E: Revealed type is 'Union[builtins.int, Any]' + reveal_type(x) # E: Revealed type is 'Union[builtins.int, Any]' + return x + +[builtins fixtures/bool.pyi] + +[case testOptionalAssignAny2] +from typing import Optional +def f(): + return 0 + +def g(x: Optional[int]) -> int: + if x is None: + reveal_type(x) # E: Revealed type is 'None' + x = 1 + reveal_type(x) # E: Revealed type is 'builtins.int' + # Since we've assigned to x, the special case None behavior shouldn't happen + x = f() + reveal_type(x) # E: Revealed type is 'Union[builtins.int, None]' + reveal_type(x) # E: Revealed type is 'Union[builtins.int, None]' + return x # E: Incompatible return value type (got "Optional[int]", expected "int") + +[builtins fixtures/bool.pyi] + +[case testOptionalAssignAny3] +from typing import Optional +def f(): + return 0 + +def g(x: Optional[int]) -> int: + if x is not None: + return x + reveal_type(x) # E: Revealed type is 'None' + x = f() + reveal_type(x) # E: Revealed type is 'Union[builtins.int, Any]' + return x + +[builtins fixtures/bool.pyi] From d5c19da8797ff2e6e6e81aa6f2cebf6e09746453 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Mon, 17 Sep 2018 15:58:11 -0700 Subject: [PATCH 2/2] remove stray extra parens --- mypy/binder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/binder.py b/mypy/binder.py index a1efefff9be96..f1b14ff6ad8ce 100644 --- a/mypy/binder.py +++ b/mypy/binder.py @@ -292,7 +292,7 @@ def assign_type(self, expr: Expression, self.put(expr, UnionType(new_items)) elif (isinstance(type, AnyType) and not (isinstance(declared_type, UnionType) - and (any(isinstance(item, AnyType) for item in declared_type.items)))): + and any(isinstance(item, AnyType) for item in declared_type.items))): # Assigning an Any value doesn't affect the type to avoid false negatives, unless # there is an Any item in a declared union type. self.put(expr, declared_type)