From 79206a6ec951c4fd46da07d139ad669d70f78a47 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Thu, 7 Oct 2021 11:44:36 +0300 Subject: [PATCH 1/6] Now typecheckes `traceback` in `raise e, msg, traceback` on py2 --- mypy/checker.py | 48 +++++++++++++++++++++++++++++----- test-data/unit/pythoneval.test | 17 ++++++++++++ 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 01e5fe688086..adae94efc0c9 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3471,24 +3471,58 @@ def type_check_raise(self, e: Expression, s: RaiseStmt, if isinstance(typ, DeletedType): self.msg.deleted_as_rvalue(typ, e) return + exc_type = self.named_type('builtins.BaseException') - expected_type = UnionType([exc_type, TypeType(exc_type)]) + expected_type_items = [exc_type, TypeType(exc_type)] if optional: - expected_type.items.append(NoneType()) - if self.options.python_version[0] == 2: + expected_type_items.append(NoneType()) + + if (self.options.python_version[0] == 2 + and isinstance(typ, TupleType) + and len(typ.items) >= 2): # allow `raise type, value, traceback` # https://docs.python.org/2/reference/simple_stmts.html#the-raise-statement - # TODO: Also check tuple item types. any_type = AnyType(TypeOfAny.implementation_artifact) tuple_type = self.named_type('builtins.tuple') - expected_type.items.append(TupleType([any_type, any_type], tuple_type)) - expected_type.items.append(TupleType([any_type, any_type, any_type], tuple_type)) - self.check_subtype(typ, expected_type, s, message_registry.INVALID_EXCEPTION) + + # Right now we push all types as `Any`, + # because we will type-check `traceback` later. + # `msg` is always `Any`. + expected_type_items.extend([ + # `raise Exception, msg` case: + TupleType([TypeType(exc_type), any_type], tuple_type), + # `raise Exception, msg, traceback` case: + TupleType([TypeType(exc_type), any_type, any_type], tuple_type), + ]) + + self.check_subtype( + typ, UnionType.make_union(expected_type_items), + s, message_registry.INVALID_EXCEPTION, + ) if isinstance(typ, FunctionLike): # https://github.com/python/mypy/issues/11089 self.expr_checker.check_call(typ, [], [], e) + # `raise Exception, msg, traceback` case + if (self.options.python_version[0] == 2 + and isinstance(typ, TupleType) + and len(typ.items) == 3): + # Now, we typecheck `traceback` argument it is present. + # We do this after the main check for better error message + # and better ordering: first about `BaseException` subtype, + # then about `traceback` type. + + traceback_type = UnionType.make_union([ + self.named_type('types.TracebackType'), + NoneType(), + ]) + self.check_subtype( + typ.items[2], + traceback_type, s, + 'Third argument to raise must have "{}" type'.format(traceback_type), + ) + def visit_try_stmt(self, s: TryStmt) -> None: """Type check a try statement.""" # Our enclosing frame will get the result if the try/except falls through. diff --git a/test-data/unit/pythoneval.test b/test-data/unit/pythoneval.test index 2fdbac433f14..b1de98e611ed 100644 --- a/test-data/unit/pythoneval.test +++ b/test-data/unit/pythoneval.test @@ -1531,3 +1531,20 @@ _testNoPython3StubAvailable.py:1: note: See https://mypy.readthedocs.io/en/stabl _testNoPython3StubAvailable.py:3: error: Library stubs not installed for "maxminddb" (or incompatible with Python 3.6) _testNoPython3StubAvailable.py:3: note: Hint: "python3 -m pip install types-maxminddb" _testNoPython3StubAvailable.py:3: note: (or run "mypy --install-types" to install all missing stub packages) + +[case testRaiseTupleOfThreeOnPython2] +from types import TracebackType +from typing import Optional + +raise Exception # ok +raise Exception(1) # ok +raise Exception, 1 # ok + +e = None # type: Optional[TracebackType] +raise Exception, 1, e # ok +raise Exception, 1, None + +raise int, 1 # E: Exception must be derived from BaseException +raise Exception, 1, 1 # E: Third argument to raise must have "Union[types.TracebackType, None]" type +raise int, 1, 1 # E: Exception must be derived from BaseException \ + # E: Third argument to raise must have "Union[types.TracebackType, None]" type From 6ad05fb2171fb106e38637d3d03ad849d157bc3e Mon Sep 17 00:00:00 2001 From: sobolevn Date: Thu, 7 Oct 2021 12:21:22 +0300 Subject: [PATCH 2/6] Moves test to `check-python2.test` --- test-data/unit/check-python2.test | 26 ++++++++++++++++++++------ test-data/unit/pythoneval.test | 17 ----------------- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/test-data/unit/check-python2.test b/test-data/unit/check-python2.test index f9837b8cfd03..f214c1359817 100644 --- a/test-data/unit/check-python2.test +++ b/test-data/unit/check-python2.test @@ -62,18 +62,32 @@ A.f(1) A.f('') # E: Argument 1 to "f" of "A" has incompatible type "str"; expected "int" [builtins_py2 fixtures/staticmethod.pyi] -[case testRaiseTuple] -import typing -raise BaseException, "a" -raise BaseException, "a", None -[builtins_py2 fixtures/exception.pyi] - [case testRaiseTupleTypeFail] import typing x = None # type: typing.Type[typing.Tuple[typing.Any, typing.Any, typing.Any]] raise x # E: Exception must be derived from BaseException [builtins_py2 fixtures/exception.pyi] +[case testRaiseTupleOfThreeOnPython2] +from types import TracebackType +from typing import Optional + +raise Exception # ok +raise Exception(1) # ok +raise Exception, 1 # ok + +e = None # type: Optional[TracebackType] +raise Exception, 1, e # ok +raise Exception, 1, None + +raise int, 1 # E: Exception must be derived from BaseException +raise Exception, 1, 1 # E: Third argument to raise must have "Union[types.TracebackType, None]" type +raise int, 1, 1 # E: Exception must be derived from BaseException \ + # E: Third argument to raise must have "Union[types.TracebackType, None]" type +[builtins_py2 fixtures/exception.pyi] +[file types.pyi] +class TracebackType: pass + [case testTryExceptWithTuple] try: None diff --git a/test-data/unit/pythoneval.test b/test-data/unit/pythoneval.test index b1de98e611ed..2fdbac433f14 100644 --- a/test-data/unit/pythoneval.test +++ b/test-data/unit/pythoneval.test @@ -1531,20 +1531,3 @@ _testNoPython3StubAvailable.py:1: note: See https://mypy.readthedocs.io/en/stabl _testNoPython3StubAvailable.py:3: error: Library stubs not installed for "maxminddb" (or incompatible with Python 3.6) _testNoPython3StubAvailable.py:3: note: Hint: "python3 -m pip install types-maxminddb" _testNoPython3StubAvailable.py:3: note: (or run "mypy --install-types" to install all missing stub packages) - -[case testRaiseTupleOfThreeOnPython2] -from types import TracebackType -from typing import Optional - -raise Exception # ok -raise Exception(1) # ok -raise Exception, 1 # ok - -e = None # type: Optional[TracebackType] -raise Exception, 1, e # ok -raise Exception, 1, None - -raise int, 1 # E: Exception must be derived from BaseException -raise Exception, 1, 1 # E: Third argument to raise must have "Union[types.TracebackType, None]" type -raise int, 1, 1 # E: Exception must be derived from BaseException \ - # E: Third argument to raise must have "Union[types.TracebackType, None]" type From bdf9414498eb0e0e17fc893df35d201aa3f80401 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Thu, 7 Oct 2021 12:23:17 +0300 Subject: [PATCH 3/6] Moves test to `check-python2.test` --- test-data/unit/check-python2.test | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/test-data/unit/check-python2.test b/test-data/unit/check-python2.test index f214c1359817..4a128bacb8d4 100644 --- a/test-data/unit/check-python2.test +++ b/test-data/unit/check-python2.test @@ -72,13 +72,19 @@ raise x # E: Exception must be derived from BaseException from types import TracebackType from typing import Optional +e = None # type: Optional[TracebackType] + +raise BaseException # ok +raise BaseException(1) # ok +raise BaseException, 1 # ok +raise BaseException, 1, e # ok +raise BaseException, 1, None # ok + raise Exception # ok raise Exception(1) # ok raise Exception, 1 # ok - -e = None # type: Optional[TracebackType] raise Exception, 1, e # ok -raise Exception, 1, None +raise Exception, 1, None # ok raise int, 1 # E: Exception must be derived from BaseException raise Exception, 1, 1 # E: Third argument to raise must have "Union[types.TracebackType, None]" type From 9e524f95b8fbc7605bc1db2188b61174927aeefe Mon Sep 17 00:00:00 2001 From: sobolevn Date: Thu, 7 Oct 2021 20:23:57 +0300 Subject: [PATCH 4/6] Checks `raise` on python2 --- mypy/checker.py | 103 +++++++++++++++++++----------- mypy/fastparse2.py | 4 ++ mypy/nodes.py | 5 +- test-data/unit/check-python2.test | 41 +++++++++++- 4 files changed, 112 insertions(+), 41 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index adae94efc0c9..0a768b271604 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3462,7 +3462,7 @@ def visit_raise_stmt(self, s: RaiseStmt) -> None: if s.expr: self.type_check_raise(s.expr, s) if s.from_expr: - self.type_check_raise(s.from_expr, s, True) + self.type_check_raise(s.from_expr, s, optional=True) self.binder.unreachable() def type_check_raise(self, e: Expression, s: RaiseStmt, @@ -3472,55 +3472,84 @@ def type_check_raise(self, e: Expression, s: RaiseStmt, self.msg.deleted_as_rvalue(typ, e) return + if self.options.python_version[0] == 2: + # Since `raise` has very different rule on python2, we use a different helper. + # https://github.com/python/mypy/pull/11289 + self._type_check_raise_python2(e, s, typ) + return + + # Python3 case: exc_type = self.named_type('builtins.BaseException') expected_type_items = [exc_type, TypeType(exc_type)] if optional: + # This is used for `x` part in a case like `raise e from x`, + # where we allow `raise e from None`. Is not reachable on python2. expected_type_items.append(NoneType()) - if (self.options.python_version[0] == 2 - and isinstance(typ, TupleType) - and len(typ.items) >= 2): - # allow `raise type, value, traceback` - # https://docs.python.org/2/reference/simple_stmts.html#the-raise-statement - any_type = AnyType(TypeOfAny.implementation_artifact) - tuple_type = self.named_type('builtins.tuple') - - # Right now we push all types as `Any`, - # because we will type-check `traceback` later. - # `msg` is always `Any`. - expected_type_items.extend([ - # `raise Exception, msg` case: - TupleType([TypeType(exc_type), any_type], tuple_type), - # `raise Exception, msg, traceback` case: - TupleType([TypeType(exc_type), any_type, any_type], tuple_type), - ]) - self.check_subtype( - typ, UnionType.make_union(expected_type_items), - s, message_registry.INVALID_EXCEPTION, + typ, UnionType.make_union(expected_type_items), s, + message_registry.INVALID_EXCEPTION, ) if isinstance(typ, FunctionLike): # https://github.com/python/mypy/issues/11089 self.expr_checker.check_call(typ, [], [], e) - # `raise Exception, msg, traceback` case - if (self.options.python_version[0] == 2 - and isinstance(typ, TupleType) - and len(typ.items) == 3): - # Now, we typecheck `traceback` argument it is present. - # We do this after the main check for better error message - # and better ordering: first about `BaseException` subtype, - # then about `traceback` type. - - traceback_type = UnionType.make_union([ - self.named_type('types.TracebackType'), - NoneType(), - ]) + def _type_check_raise_python2(self, e: Expression, s: RaiseStmt, typ: ProperType) -> None: + # Python2 has two possible major cases: + # 1. `raise expr`, where `expr` is some expression, it can be: + # - Exception typ + # - Exception instance + # - Old style class (not supported) + # - Tuple, where 0th item is exception type or instance + # 2. `raise exc, msg, traceback`, where: + # - `exc` is exception type (not instance!) + # - `traceback` is `types.TracebackType | None` + # Important note: `raise exc, msg` is not the same as `raise (exc, msg)` + # We call `raise exc, msg, traceback` - legacy mode. + exc_type = self.named_type('builtins.BaseException') + + if (not s.legacy_mode and (isinstance(typ, TupleType) and typ.items + or (isinstance(typ, Instance) and typ.args and typ.type.fullname == 'builtins.tuple'))): + # `raise (exc, ...)` case: + item = typ.items[0] if isinstance(typ, TupleType) else typ.args[0] + self.check_subtype( + item, UnionType([exc_type, TypeType(exc_type)]), s, + 'When raising a tuple, first element must by derived from BaseException', + ) + return + elif s.legacy_mode: + # `raise Exception, msg` case + # `raise Exception, msg, traceback` case + # https://docs.python.org/2/reference/simple_stmts.html#the-raise-statement + assert isinstance(typ, TupleType) # Is set in fastparse2.py + self.check_subtype( + typ.items[0], TypeType(exc_type), s, + 'First argument must be BaseException subtype', + ) + + # Typecheck `traceback` part: + if len(typ.items) == 3: + # Now, we typecheck `traceback` argument if it is present. + # We do this after the main check for better error message + # and better ordering: first about `BaseException` subtype, + # then about `traceback` type. + traceback_type = UnionType.make_union([ + self.named_type('types.TracebackType'), + NoneType(), + ]) + self.check_subtype( + typ.items[2], traceback_type, s, + 'Third argument to raise must have "{}" type'.format(traceback_type), + ) + else: + expected_type_items = [ + # `raise Exception` and `raise Exception()` cases: + exc_type, TypeType(exc_type), + ] self.check_subtype( - typ.items[2], - traceback_type, s, - 'Third argument to raise must have "{}" type'.format(traceback_type), + typ, UnionType.make_union(expected_type_items), + s, message_registry.INVALID_EXCEPTION, ) def visit_try_stmt(self, s: TryStmt) -> None: diff --git a/mypy/fastparse2.py b/mypy/fastparse2.py index 2d288bf158e5..33f34082cf83 100644 --- a/mypy/fastparse2.py +++ b/mypy/fastparse2.py @@ -664,19 +664,23 @@ def visit_With(self, n: ast27.With) -> WithStmt: typ) return self.set_line(stmt, n) + # 'raise' [test [',' test [',' test]]] def visit_Raise(self, n: ast27.Raise) -> RaiseStmt: + legacy_mode = False if n.type is None: e = None else: if n.inst is None: e = self.visit(n.type) else: + legacy_mode = True if n.tback is None: e = TupleExpr([self.visit(n.type), self.visit(n.inst)]) else: e = TupleExpr([self.visit(n.type), self.visit(n.inst), self.visit(n.tback)]) stmt = RaiseStmt(e, None) + stmt.legacy_mode = legacy_mode return self.set_line(stmt, n) # TryExcept(stmt* body, excepthandler* handlers, stmt* orelse) diff --git a/mypy/nodes.py b/mypy/nodes.py index 435ffa9293cb..987c8e0786b1 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -1288,16 +1288,19 @@ def accept(self, visitor: StatementVisitor[T]) -> T: class RaiseStmt(Statement): - __slots__ = ('expr', 'from_expr') + __slots__ = ('expr', 'from_expr', 'legacy_mode') # Plain 'raise' is a valid statement. expr: Optional[Expression] from_expr: Optional[Expression] + # Is set when python2 has `raise exc, msg, traceback`. + legacy_mode: bool def __init__(self, expr: Optional[Expression], from_expr: Optional[Expression]) -> None: super().__init__() self.expr = expr self.from_expr = from_expr + self.legacy_mode = False def accept(self, visitor: StatementVisitor[T]) -> T: return visitor.visit_raise_stmt(self) diff --git a/test-data/unit/check-python2.test b/test-data/unit/check-python2.test index 4a128bacb8d4..f9dd8f96f951 100644 --- a/test-data/unit/check-python2.test +++ b/test-data/unit/check-python2.test @@ -70,26 +70,61 @@ raise x # E: Exception must be derived from BaseException [case testRaiseTupleOfThreeOnPython2] from types import TracebackType -from typing import Optional +from typing import Optional, Tuple, Type e = None # type: Optional[TracebackType] raise BaseException # ok raise BaseException(1) # ok +raise (BaseException,) # ok +raise (BaseException(1),) # ok raise BaseException, 1 # ok raise BaseException, 1, e # ok raise BaseException, 1, None # ok raise Exception # ok raise Exception(1) # ok +raise (Exception,) # ok +raise (Exception(1),) # ok raise Exception, 1 # ok raise Exception, 1, e # ok raise Exception, 1, None # ok -raise int, 1 # E: Exception must be derived from BaseException +raise int, 1 # E: First argument must be BaseException subtype +raise Exception(1), 1 # E: First argument must be BaseException subtype +raise Exception(1), 1, None # E: First argument must be BaseException subtype raise Exception, 1, 1 # E: Third argument to raise must have "Union[types.TracebackType, None]" type -raise int, 1, 1 # E: Exception must be derived from BaseException \ +raise int, 1, 1 # E: First argument must be BaseException subtype \ # E: Third argument to raise must have "Union[types.TracebackType, None]" type + +t1 = (BaseException,) +t2 = (Exception(1), 2, 3, 4) # type: Tuple[Exception, int, int, int] +t3 = (Exception,) # type: Tuple[Type[Exception], ...] +t4 = (Exception(1),) # type: Tuple[Exception, ...] + +raise t1 # ok +raise t2 # ok +raise t3 # ok +raise t4 # ok + +raise t1, 1, None # E: First argument must be BaseException subtype +raise t2, 1 # E: First argument must be BaseException subtype +raise t3, 1, e # E: First argument must be BaseException subtype +raise t4, 1, 1 # E: First argument must be BaseException subtype \ + # E: Third argument to raise must have "Union[types.TracebackType, None]" type + +w1 = () +w2 = (1, Exception) +w3 = (1,) # type: Tuple[int, ...] + +raise w1 # E: Exception must be derived from BaseException +raise w2 # E: When raising a tuple, first element must by derived from BaseException +raise w3 # E: When raising a tuple, first element must by derived from BaseException + +try: + pass +except Exception: + raise # ok [builtins_py2 fixtures/exception.pyi] [file types.pyi] class TracebackType: pass From 66ae2d00649a3fe5b9d03ae23da968ea19e6b9a7 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sat, 9 Oct 2021 10:06:52 +0300 Subject: [PATCH 5/6] Fixes flake8 --- mypy/checker.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 0a768b271604..06797ff7e218 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3510,7 +3510,8 @@ def _type_check_raise_python2(self, e: Expression, s: RaiseStmt, typ: ProperType exc_type = self.named_type('builtins.BaseException') if (not s.legacy_mode and (isinstance(typ, TupleType) and typ.items - or (isinstance(typ, Instance) and typ.args and typ.type.fullname == 'builtins.tuple'))): + or (isinstance(typ, Instance) and typ.args + and typ.type.fullname == 'builtins.tuple'))): # `raise (exc, ...)` case: item = typ.items[0] if isinstance(typ, TupleType) else typ.args[0] self.check_subtype( From 217ce48da3a922b71e692c07f454785bd7408668 Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Sat, 9 Oct 2021 18:16:00 +0300 Subject: [PATCH 6/6] Update mypy/checker.py Co-authored-by: Jelle Zijlstra --- mypy/checker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 06797ff7e218..1c007e733eb0 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3483,7 +3483,7 @@ def type_check_raise(self, e: Expression, s: RaiseStmt, expected_type_items = [exc_type, TypeType(exc_type)] if optional: # This is used for `x` part in a case like `raise e from x`, - # where we allow `raise e from None`. Is not reachable on python2. + # where we allow `raise e from None`. expected_type_items.append(NoneType()) self.check_subtype(