From c87433d96f8326270834a9460bd85e67ed1b5ccc Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Fri, 30 Mar 2018 15:09:37 -0700 Subject: [PATCH 1/9] Special case assignment to '_': always infer 'Any' Fixes #465 --- mypy/checker.py | 4 ++++ mypy/types.py | 1 + 2 files changed, 5 insertions(+) diff --git a/mypy/checker.py b/mypy/checker.py index 99ef6c8e6bd6..e1cccb8d79a2 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2016,6 +2016,10 @@ def infer_variable_type(self, name: Var, lvalue: Lvalue, # Make the type more general (strip away function names etc.). init_type = strip_type(init_type) + # Special case if target is named '_' -- the variable gets type Any. + if isinstance(lvalue, NameExpr) and lvalue.name == '_': + init_type = AnyType(TypeOfAny.special_form) + self.set_inferred_type(name, lvalue, init_type) def infer_partial_type(self, name: Var, lvalue: Lvalue, init_type: Type) -> bool: diff --git a/mypy/types.py b/mypy/types.py index 96b5849ffee2..85c0e5aae906 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -281,6 +281,7 @@ class TypeOfAny(Enum): from_error = 'from_error' # Is this a type that can't be represented in mypy's type system? For instance, type of # call to NewType...). Even though these types aren't real Anys, we treat them as such. + # Also used for variables named '_'. special_form = 'special_form' # Does this Any come from interaction with another Any? from_another_any = 'from_another_any' From 72fb7986a211ab4c84ab806b95fa1c645ab8cff2 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Fri, 30 Mar 2018 15:40:05 -0700 Subject: [PATCH 2/9] Unit tests for assignment to '_' --- test-data/unit/check-inference.test | 77 +++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test index f89a0e9513a4..c6467b772d1d 100644 --- a/test-data/unit/check-inference.test +++ b/test-data/unit/check-inference.test @@ -2303,3 +2303,80 @@ class C: 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" + +-- Special case for assignment to '_' +-- ---------------------------------- + +[case testUnusedTargetSingle] +def foo() -> None: + _ = 0 + _ = '' + _ = 0 + _ = '' + +[case testUnusedTargetTupleUnpacking] +def foo() -> None: + _, _ = (0, '') + _ = 0 + _ = '' +def bar() -> None: + t = (0, '') + _, _ = t + _ = 0 + _ = '' + +[case testUnusedTargetMultipleTargets] +def foo() -> None: + _ = x = 0 + _ = y = '' + _ = 0 + _ = '' +def bar() -> None: + x = _ = 0 + y = _ = '' + _ = 0 + _ = '' + x + 0 + y + '' + x + '' # E: Unsupported operand types for + ("int" and "str") + y + 0 # E: Unsupported operand types for + ("str" and "int") + +[case testUnusedTargetNotImport] +import d, c, b, a +[file _.py] +[file m.py] +def f(): pass +def _(): pass +[file a.py] +import _ +_ = 0 # E: Incompatible types in assignment (expression has type "int", variable has type Module) +[file b.py] +import m as _ +_ = 0 # E: Incompatible types in assignment (expression has type "int", variable has type Module) +[file c.py] +from m import _ +_ = 0 # E: Incompatible types in assignment (expression has type "int", variable has type "Callable[[], Any]") +[file d.py] +from m import f as _ +_ = 0 # E: Incompatible types in assignment (expression has type "int", variable has type "Callable[[], Any]") +[builtins fixtures/module.pyi] + +[case testUnusedTargetNotClass] +class _: + pass +_().method() # E: "_" has no attribute "method" + +[case testUnusedTargetNotDef] +def _() -> int: + pass +_() + 0 + +[case testUnusedTargetForLoop] +def f() -> None: + a = [(0, '', 0)] + for _, _, x in a: + x = 0 + x = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int") + _ = 0 + _ = '' +[builtins fixtures/list.pyi] From 9543391ec06d2978bb144b1e02ba8b7827561ab3 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Fri, 30 Mar 2018 21:15:24 -0700 Subject: [PATCH 3/9] Test 'with ... as _' and 'except ... as _' --- test-data/unit/check-inference.test | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test index c6467b772d1d..976a57d6beb6 100644 --- a/test-data/unit/check-inference.test +++ b/test-data/unit/check-inference.test @@ -2380,3 +2380,20 @@ def f() -> None: _ = 0 _ = '' [builtins fixtures/list.pyi] + +[case testUnusedTargetWithClause] +class C: + def __enter__(self) -> int: pass + def __exit__(self, *args): pass +def f() -> None: + with C() as _: pass + _ = 0 + _ = '' + +[case testUnusedTargetExceptClause] +def f() -> None: + try: pass + except BaseException as _: + _ = 0 + _ = '' +[builtins fixtures/exception.pyi] From f9c450fdc2a821b4fec02ffca02fe3c9fdcae62c Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Sat, 31 Mar 2018 21:13:07 -0700 Subject: [PATCH 4/9] Fix tests, add tests for globals and classvars, make those tests work --- mypy/semanal.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 2860bd164392..7c3012ca34d5 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1607,7 +1607,9 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: # Set the type if the rvalue is a simple literal (even if the above error occurred). if len(s.lvalues) == 1 and isinstance(s.lvalues[0], NameExpr): if s.lvalues[0].is_inferred_def: - s.type = self.analyze_simple_literal_type(s.rvalue) + # Except if it's an assignment to '_'. + if s.lvalues[0].name != '_': + s.type = self.analyze_simple_literal_type(s.rvalue) if s.type: # Store type into nodes. for lvalue in s.lvalues: From e6cd8e49066dc27101ea728b159c815fde5b9d6f Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Sun, 1 Apr 2018 15:50:37 -0700 Subject: [PATCH 5/9] Really fix and add tests --- test-data/unit/check-inference.test | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test index 976a57d6beb6..7ff7f8c743ac 100644 --- a/test-data/unit/check-inference.test +++ b/test-data/unit/check-inference.test @@ -2307,10 +2307,17 @@ class C: -- Special case for assignment to '_' -- ---------------------------------- -[case testUnusedTargetSingle] +[case testUnusedTargetLocal] def foo() -> None: _ = 0 _ = '' + +[case testUnusedTargetGlobal] +_ = 0 +_ = '' + +[case testUnusedTargetClass] +class C: _ = 0 _ = '' @@ -2369,7 +2376,7 @@ _().method() # E: "_" has no attribute "method" [case testUnusedTargetNotDef] def _() -> int: pass -_() + 0 +_() + '' # E: Unsupported operand types for + ("int" and "str") [case testUnusedTargetForLoop] def f() -> None: From 2dadd05ddfb69bd306b74c4a0ac305244f56f5e7 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Tue, 10 Apr 2018 14:08:03 -0700 Subject: [PATCH 6/9] Move the '_' detection to semanal.py and limit it to local variables only --- mypy/checker.py | 4 ---- mypy/semanal.py | 8 +++++--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index e1cccb8d79a2..99ef6c8e6bd6 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2016,10 +2016,6 @@ def infer_variable_type(self, name: Var, lvalue: Lvalue, # Make the type more general (strip away function names etc.). init_type = strip_type(init_type) - # Special case if target is named '_' -- the variable gets type Any. - if isinstance(lvalue, NameExpr) and lvalue.name == '_': - init_type = AnyType(TypeOfAny.special_form) - self.set_inferred_type(name, lvalue, init_type) def infer_partial_type(self, name: Var, lvalue: Lvalue, init_type: Type) -> bool: diff --git a/mypy/semanal.py b/mypy/semanal.py index 7c3012ca34d5..9d9fe3080f4b 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1607,9 +1607,7 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: # Set the type if the rvalue is a simple literal (even if the above error occurred). if len(s.lvalues) == 1 and isinstance(s.lvalues[0], NameExpr): if s.lvalues[0].is_inferred_def: - # Except if it's an assignment to '_'. - if s.lvalues[0].name != '_': - s.type = self.analyze_simple_literal_type(s.rvalue) + s.type = self.analyze_simple_literal_type(s.rvalue) if s.type: # Store type into nodes. for lvalue in s.lvalues: @@ -1828,6 +1826,10 @@ def analyze_lvalue(self, lval: Lvalue, nested: bool = False, lval.kind = LDEF lval.fullname = lval.name self.add_local(v, lval) + if isinstance(lval, NameExpr) and lval.name == '_' and self.is_func_scope(): + # Special case for assignment to local named '_': always infer 'Any'. + typ = AnyType(TypeOfAny.special_form) + self.store_declared_types(lval, typ) elif not self.is_func_scope() and (self.type and lval.name not in self.type.names): # Define a new attribute within class body. From c73948ae7298c7a630e37b81121fba7d1416e01d Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Tue, 10 Apr 2018 14:15:26 -0700 Subject: [PATCH 7/9] Adjust tests for the new world --- test-data/unit/check-inference.test | 30 ++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test index 7ff7f8c743ac..10f69b70255f 100644 --- a/test-data/unit/check-inference.test +++ b/test-data/unit/check-inference.test @@ -2312,14 +2312,14 @@ def foo() -> None: _ = 0 _ = '' -[case testUnusedTargetGlobal] +[case testUnusedTargetNotGlobal] _ = 0 -_ = '' +_ = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int") -[case testUnusedTargetClass] +[case testUnusedTargetNotClass] class C: _ = 0 - _ = '' + _ = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int") [case testUnusedTargetTupleUnpacking] def foo() -> None: @@ -2369,14 +2369,16 @@ _ = 0 # E: Incompatible types in assignment (expression has type "int", variabl [builtins fixtures/module.pyi] [case testUnusedTargetNotClass] -class _: - pass -_().method() # E: "_" has no attribute "method" +def foo() -> None: + class _: + pass + _().method() # E: "_" has no attribute "method" [case testUnusedTargetNotDef] -def _() -> int: - pass -_() + '' # E: Unsupported operand types for + ("int" and "str") +def foo() -> None: + def _() -> int: + pass + _() + '' # E: Unsupported operand types for + ("int" and "str") [case testUnusedTargetForLoop] def f() -> None: @@ -2397,10 +2399,12 @@ def f() -> None: _ = 0 _ = '' -[case testUnusedTargetExceptClause] +[case testUnusedTargetNotExceptClause] +# Things don't work for except clauses. +# This is due to the implementation, but it's just as well. def f() -> None: try: pass except BaseException as _: - _ = 0 - _ = '' + _ = 0 # E: Incompatible types in assignment (expression has type "int", variable has type "BaseException") + _ = '' # E: Incompatible types in assignment (expression has type "str", variable has type "BaseException") [builtins fixtures/exception.pyi] From 52fab476ea8f971c4167a4d1b0a6771d03f5b993 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Tue, 10 Apr 2018 14:36:09 -0700 Subject: [PATCH 8/9] Change import test to use locals too --- test-data/unit/check-inference.test | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test index 10f69b70255f..1971fa40bcf8 100644 --- a/test-data/unit/check-inference.test +++ b/test-data/unit/check-inference.test @@ -2351,21 +2351,31 @@ def bar() -> None: [case testUnusedTargetNotImport] import d, c, b, a [file _.py] +def f(): pass [file m.py] def f(): pass -def _(): pass +_ = f +_ = 0 # E: Incompatible types in assignment (expression has type "int", variable has type "Callable[[], Any]") [file a.py] -import _ -_ = 0 # E: Incompatible types in assignment (expression has type "int", variable has type Module) +def foo() -> None: + import _ + _.f() + _ = 0 # E: Incompatible types in assignment (expression has type "int", variable has type Module) [file b.py] -import m as _ -_ = 0 # E: Incompatible types in assignment (expression has type "int", variable has type Module) +def foo() -> None: + import m as _ + _.f() + _ = 0 # E: Incompatible types in assignment (expression has type "int", variable has type Module) [file c.py] -from m import _ -_ = 0 # E: Incompatible types in assignment (expression has type "int", variable has type "Callable[[], Any]") +def foo() -> None: + from m import _ + _() + _ = 0 # E: Incompatible types in assignment (expression has type "int", variable has type "Callable[[], Any]") [file d.py] -from m import f as _ -_ = 0 # E: Incompatible types in assignment (expression has type "int", variable has type "Callable[[], Any]") +def foo() -> None: + from m import f as _ + _() + _ = 0 # E: Incompatible types in assignment (expression has type "int", variable has type "Callable[[], Any]") [builtins fixtures/module.pyi] [case testUnusedTargetNotClass] From 4276558ff7ccee34516ba24c34ad302c9255d8c7 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Mon, 16 Apr 2018 14:22:40 -0700 Subject: [PATCH 9/9] Simplify condition, per code review --- mypy/semanal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 9d9fe3080f4b..a8469ae53ba3 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1826,7 +1826,7 @@ def analyze_lvalue(self, lval: Lvalue, nested: bool = False, lval.kind = LDEF lval.fullname = lval.name self.add_local(v, lval) - if isinstance(lval, NameExpr) and lval.name == '_' and self.is_func_scope(): + if lval.name == '_': # Special case for assignment to local named '_': always infer 'Any'. typ = AnyType(TypeOfAny.special_form) self.store_declared_types(lval, typ)