From 1f95975ef56d913b45a52b6367feaa090bf0acb4 Mon Sep 17 00:00:00 2001 From: Peter McCormick Date: Thu, 2 Jun 2016 15:02:59 -0400 Subject: [PATCH 1/6] Add support for tuple values on except clauses Issue #1590 --- mypy/checker.py | 39 +++++++++++++++++++++------- mypy/test/data/check-statements.test | 32 +++++++++++++++++++++++ 2 files changed, 62 insertions(+), 9 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 33f23312918b..2cabbf726298 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1839,18 +1839,39 @@ def exception_type(self, n: Node) -> Type: self.fail('Unsupported exception', n) return AnyType() + def check_for_function_like_exception_type(self, type: Type, context: Context) -> Type: + if not isinstance(type, FunctionLike): + return None + + item = type.items()[0] + ret = item.ret_type + if (is_subtype(ret, self.named_type('builtins.BaseException')) + and item.is_type_obj()): + return ret + else: + self.fail(messages.INVALID_EXCEPTION_TYPE, context) + return AnyType() + def check_exception_type(self, type: Type, context: Context) -> Type: - if isinstance(type, FunctionLike): - item = type.items()[0] - ret = item.ret_type - if (is_subtype(ret, self.named_type('builtins.BaseException')) - and item.is_type_obj()): - return ret - else: - self.fail(messages.INVALID_EXCEPTION_TYPE, context) - return AnyType() + ret = self.check_for_function_like_exception_type(type, context) + if ret is not None: + return ret elif isinstance(type, AnyType): return AnyType() + elif isinstance(type, TupleType): + t = None # type: Type + for item in type.items: + tt = self.check_for_function_like_exception_type(item, context) + + if tt is None: + tt = AnyType() + break + + if t: + t = join_types(t, tt) + else: + t = tt + return t else: self.fail(messages.INVALID_EXCEPTION_TYPE, context) return AnyType() diff --git a/mypy/test/data/check-statements.test b/mypy/test/data/check-statements.test index c0f03516d8b8..348116758964 100644 --- a/mypy/test/data/check-statements.test +++ b/mypy/test/data/check-statements.test @@ -573,6 +573,8 @@ except (E1, E2) as e1: except (E2, E1) as e2: a = e2 # type: E1 b = e2 # type: E2 # E: Incompatible types in assignment (expression has type "E1", variable has type "E2") +except (E1, E2, int) as e3: # E: Exception type must be derived from BaseException + pass [builtins fixtures/exception.py] [case testReuseTryExceptionVariable] @@ -618,6 +620,36 @@ except exc as e: pass # E: Exception type must be derived from BaseE except BaseException() as b: pass # E: Exception type must be derived from BaseException [builtins fixtures/exception.py] +[case testTupleValueAsExceptionType] +import typing +def exc() -> BaseException: pass +class E1(BaseException): pass +class E2(E1): pass +class E3(E1): pass + +exs1 = (E1, E2) +try: pass +except exs1 as e1: + reveal_type(e1) # E: Revealed type is '__main__.E1' + +exs3 = (E2, E3) +try: pass +except exs3 as e3: + reveal_type(e3) # E: Revealed type is '__main__.E1' +[builtins fixtures/exception.py] + +[case testInvalidTupleValueAsExceptionType] +import typing +def exc() -> BaseException: pass +class E1(BaseException): pass +class E2(E1): pass +class E3(E1): pass + +exs1 = (E1, E2, int) +try: pass +except exs1 as e: pass # E: Exception type must be derived from BaseException +[builtins fixtures/exception.py] + [case testOverloadedExceptionType] from typing import overload class E(BaseException): From 6fa639b9e68ff909b7c593a2d0270490cba3a064 Mon Sep 17 00:00:00 2001 From: Peter McCormick Date: Thu, 2 Jun 2016 19:05:08 -0400 Subject: [PATCH 2/6] Highlight sibling sub-classes with better variable names --- mypy/test/data/check-statements.test | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/mypy/test/data/check-statements.test b/mypy/test/data/check-statements.test index 348116758964..aa84c7782fc1 100644 --- a/mypy/test/data/check-statements.test +++ b/mypy/test/data/check-statements.test @@ -577,6 +577,18 @@ except (E1, E2, int) as e3: # E: Exception type must be derived from BaseExcepti pass [builtins fixtures/exception.py] +[case testExceptWithMultipleTypes3] +import typing +class E1(BaseException): pass +class E1_1(E1): pass +class E1_2(E1): pass +try: pass +except (E1, E1_1, E1_2) as e1: + reveal_type(e1) # E: Revealed type is '__main__.E1' +except (E1_1, E1_2) as e2: + reveal_type(e2) # E: Revealed type is '__main__.E1' +[builtins fixtures/exception.py] + [case testReuseTryExceptionVariable] import typing class E1(BaseException): pass @@ -624,18 +636,18 @@ except BaseException() as b: pass # E: Exception type must be derived from BaseE import typing def exc() -> BaseException: pass class E1(BaseException): pass -class E2(E1): pass -class E3(E1): pass +class E1_1(E1): pass +class E1_2(E1): pass -exs1 = (E1, E2) +exs1 = (E1, E1_2) try: pass except exs1 as e1: reveal_type(e1) # E: Revealed type is '__main__.E1' -exs3 = (E2, E3) +exs2 = (E1_1, E1_2) try: pass -except exs3 as e3: - reveal_type(e3) # E: Revealed type is '__main__.E1' +except exs2 as e2: + reveal_type(e2) # E: Revealed type is '__main__.E1' [builtins fixtures/exception.py] [case testInvalidTupleValueAsExceptionType] @@ -643,7 +655,6 @@ import typing def exc() -> BaseException: pass class E1(BaseException): pass class E2(E1): pass -class E3(E1): pass exs1 = (E1, E2, int) try: pass From 55a3ee9e97f75ef5d5923844546855688cdc6537 Mon Sep 17 00:00:00 2001 From: Peter McCormick Date: Thu, 2 Jun 2016 19:13:19 -0400 Subject: [PATCH 3/6] Make return type properly reflect optionality --- mypy/checker.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 2cabbf726298..b6d7014e7b9d 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1839,7 +1839,8 @@ def exception_type(self, n: Node) -> Type: self.fail('Unsupported exception', n) return AnyType() - def check_for_function_like_exception_type(self, type: Type, context: Context) -> Type: + def check_for_function_like_exception_type(self, type: Type, + context: Context) -> Optional[Type]: if not isinstance(type, FunctionLike): return None From 1b4b19732985c0d73679b7d7b1336fa514e5d18a Mon Sep 17 00:00:00 2001 From: Peter McCormick Date: Thu, 2 Jun 2016 20:23:20 -0400 Subject: [PATCH 4/6] Reworked exception handler type checking --- mypy/checker.py | 72 +++++++++------------------- mypy/test/data/check-statements.test | 18 +++++-- 2 files changed, 35 insertions(+), 55 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index b6d7014e7b9d..4a737f3803b0 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1782,7 +1782,7 @@ def visit_try_stmt(self, s: TryStmt) -> Type: for i in range(len(s.handlers)): self.binder.push_frame() if s.types[i]: - t = self.exception_type(s.types[i]) + t = self.visit_except_handler_test(s.types[i]) if s.vars[i]: # To support local variables, we make this a definition line, # causing assignment to set the variable's type. @@ -1822,60 +1822,32 @@ def visit_try_stmt(self, s: TryStmt) -> Type: if s.finally_body: self.accept(s.finally_body) - def exception_type(self, n: Node) -> Type: - if isinstance(n, TupleExpr): - t = None # type: Type - for item in n.items: - tt = self.exception_type(item) - if t: - t = join_types(t, tt) - else: - t = tt - return t - else: - # A single exception type; should evaluate to a type object type. - type = self.accept(n) - return self.check_exception_type(type, n) - self.fail('Unsupported exception', n) - return AnyType() + def visit_except_handler_test(self, n: Node) -> Type: + """Type check an exception handler test clause.""" + type = self.accept(n) + if isinstance(type, AnyType): + return type - def check_for_function_like_exception_type(self, type: Type, - context: Context) -> Optional[Type]: - if not isinstance(type, FunctionLike): - return None + all_types = [] # type: List[Type] + test_types = type.items if isinstance(type, TupleType) else [ type ] - item = type.items()[0] - ret = item.ret_type - if (is_subtype(ret, self.named_type('builtins.BaseException')) - and item.is_type_obj()): - return ret - else: - self.fail(messages.INVALID_EXCEPTION_TYPE, context) - return AnyType() + for ttype in test_types: + except_type = None # type: Optional[Type] - def check_exception_type(self, type: Type, context: Context) -> Type: - ret = self.check_for_function_like_exception_type(type, context) - if ret is not None: - return ret - elif isinstance(type, AnyType): - return AnyType() - elif isinstance(type, TupleType): - t = None # type: Type - for item in type.items: - tt = self.check_for_function_like_exception_type(item, context) + if isinstance(ttype, FunctionLike): + item = ttype.items()[0] + ret_type = item.ret_type + if (is_subtype(ret_type, self.named_type('builtins.BaseException')) + and item.is_type_obj()): + except_type = ret_type - if tt is None: - tt = AnyType() - break + if except_type is None: + self.fail(messages.INVALID_EXCEPTION_TYPE, n) + return AnyType() - if t: - t = join_types(t, tt) - else: - t = tt - return t - else: - self.fail(messages.INVALID_EXCEPTION_TYPE, context) - return AnyType() + all_types.append(except_type) + + return UnionType.make_simplified_union(all_types) def visit_for_stmt(self, s: ForStmt) -> Type: """Type check a for statement.""" diff --git a/mypy/test/data/check-statements.test b/mypy/test/data/check-statements.test index aa84c7782fc1..b046bb2847c2 100644 --- a/mypy/test/data/check-statements.test +++ b/mypy/test/data/check-statements.test @@ -584,9 +584,13 @@ class E1_1(E1): pass class E1_2(E1): pass try: pass except (E1, E1_1, E1_2) as e1: - reveal_type(e1) # E: Revealed type is '__main__.E1' + x = e1 # type: E1 + y = e1 # type: E1_1 # E: Incompatible types in assignment (expression has type "E1", variable has type "E1_1") + z = e1 # type: E1_2 # E: Incompatible types in assignment (expression has type "E1", variable has type "E1_2") except (E1_1, E1_2) as e2: - reveal_type(e2) # E: Revealed type is '__main__.E1' + a = e2 # type: E1 + b = e2 # type: E1_1 # E: Incompatible types in assignment (expression has type "Union[E1_1, E1_2]", variable has type "E1_1") + c = e2 # type: E1_2 # E: Incompatible types in assignment (expression has type "Union[E1_1, E1_2]", variable has type "E1_2") [builtins fixtures/exception.py] [case testReuseTryExceptionVariable] @@ -639,15 +643,19 @@ class E1(BaseException): pass class E1_1(E1): pass class E1_2(E1): pass -exs1 = (E1, E1_2) +exs1 = (E1, E1_1, E1_2) try: pass except exs1 as e1: - reveal_type(e1) # E: Revealed type is '__main__.E1' + x = e1 # type: E1 + y = e1 # type: E1_1 # E: Incompatible types in assignment (expression has type "E1", variable has type "E1_1") + z = e1 # type: E1_2 # E: Incompatible types in assignment (expression has type "E1", variable has type "E1_2") exs2 = (E1_1, E1_2) try: pass except exs2 as e2: - reveal_type(e2) # E: Revealed type is '__main__.E1' + a = e2 # type: E1 + b = e2 # type: E1_1 # E: Incompatible types in assignment (expression has type "Union[E1_1, E1_2]", variable has type "E1_1") + c = e2 # type: E1_2 # E: Incompatible types in assignment (expression has type "Union[E1_1, E1_2]", variable has type "E1_2") [builtins fixtures/exception.py] [case testInvalidTupleValueAsExceptionType] From 3d36e066e42f7928ae713b673ff649a05bf7e629 Mon Sep 17 00:00:00 2001 From: Peter McCormick Date: Thu, 2 Jun 2016 20:30:59 -0400 Subject: [PATCH 5/6] Whitespace lint fixes --- mypy/checker.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 4a737f3803b0..427405125c93 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1828,11 +1828,11 @@ def visit_except_handler_test(self, n: Node) -> Type: if isinstance(type, AnyType): return type - all_types = [] # type: List[Type] - test_types = type.items if isinstance(type, TupleType) else [ type ] + all_types = [] # type: List[Type] + test_types = type.items if isinstance(type, TupleType) else [type] for ttype in test_types: - except_type = None # type: Optional[Type] + except_type = None # type: Optional[Type] if isinstance(ttype, FunctionLike): item = ttype.items()[0] From 7c3df543d8173cde84b55f917d4c984c9bee2a99 Mon Sep 17 00:00:00 2001 From: Peter McCormick Date: Thu, 2 Jun 2016 20:41:07 -0400 Subject: [PATCH 6/6] Restructure logic for clarity --- mypy/checker.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 427405125c93..0e29e337764a 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1832,20 +1832,18 @@ def visit_except_handler_test(self, n: Node) -> Type: test_types = type.items if isinstance(type, TupleType) else [type] for ttype in test_types: - except_type = None # type: Optional[Type] - - if isinstance(ttype, FunctionLike): - item = ttype.items()[0] - ret_type = item.ret_type - if (is_subtype(ret_type, self.named_type('builtins.BaseException')) - and item.is_type_obj()): - except_type = ret_type + if not isinstance(ttype, FunctionLike): + self.fail(messages.INVALID_EXCEPTION_TYPE, n) + return AnyType() - if except_type is None: + item = ttype.items()[0] + ret_type = item.ret_type + if not (is_subtype(ret_type, self.named_type('builtins.BaseException')) + and item.is_type_obj()): self.fail(messages.INVALID_EXCEPTION_TYPE, n) return AnyType() - all_types.append(except_type) + all_types.append(ret_type) return UnionType.make_simplified_union(all_types)