From ac3e599a0168ab9345ddcfa12d55701a2dc4479e Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Sat, 21 Jun 2025 23:27:00 +0200 Subject: [PATCH 1/6] Combine the revealed types of multiple iteration steps in a more robust manner. --- mypy/checker.py | 10 ++-- mypy/errors.py | 60 +++++++++----------- mypy/messages.py | 22 ++++++- test-data/unit/check-inference.test | 4 +- test-data/unit/check-narrowing.test | 2 +- test-data/unit/check-redefine2.test | 4 +- test-data/unit/check-typevar-tuple.test | 2 +- test-data/unit/check-union-error-syntax.test | 7 ++- 8 files changed, 63 insertions(+), 48 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 596564c98a40..e34feca84a6e 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -643,8 +643,8 @@ def accept_loop( for error_info in watcher.yield_error_infos(): self.msg.fail(*error_info[:2], code=error_info[2]) - for note_info in watcher.yield_note_infos(self.options): - self.note(*note_info) + for note_info, context in watcher.yield_note_infos(self.options): + self.msg.reveal_type(note_info, context) # If exit_condition is set, assume it must be False on exit from the loop: if exit_condition: @@ -3037,7 +3037,7 @@ def is_noop_for_reachability(self, s: Statement) -> bool: if isinstance(s.expr, EllipsisExpr): return True elif isinstance(s.expr, CallExpr): - with self.expr_checker.msg.filter_errors(): + with self.expr_checker.msg.filter_errors(filter_revealed_type=True): typ = get_proper_type( self.expr_checker.accept( s.expr, allow_none_return=True, always_allow_any=True @@ -4987,8 +4987,8 @@ def visit_try_stmt(self, s: TryStmt) -> None: for error_info in watcher.yield_error_infos(): self.msg.fail(*error_info[:2], code=error_info[2]) - for note_info in watcher.yield_note_infos(self.options): - self.msg.note(*note_info) + for note_info, context in watcher.yield_note_infos(self.options): + self.msg.reveal_type(note_info, context) def visit_try_without_finally(self, s: TryStmt, try_frame: bool) -> None: """Type check a try statement, ignoring the finally block. diff --git a/mypy/errors.py b/mypy/errors.py index 5dd411c39e95..5373fe619a6a 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -15,6 +15,8 @@ from mypy.nodes import Context from mypy.options import Options from mypy.scope import Scope +from mypy.typeops import make_simplified_union +from mypy.types import Type from mypy.util import DEFAULT_SOURCE_OFFSET, is_typeshed_file from mypy.version import __version__ as mypy_version @@ -166,6 +168,10 @@ class ErrorWatcher: out by one of the ErrorWatcher instances. """ + # public attribute for the special treatment of `reveal_type` by + # `MessageBuilder.reveal_type`: + filter_revealed_type: bool + def __init__( self, errors: Errors, @@ -173,11 +179,13 @@ def __init__( filter_errors: bool | Callable[[str, ErrorInfo], bool] = False, save_filtered_errors: bool = False, filter_deprecated: bool = False, + filter_revealed_type: bool = False ) -> None: self.errors = errors self._has_new_errors = False self._filter = filter_errors self._filter_deprecated = filter_deprecated + self.filter_revealed_type = filter_revealed_type self._filtered: list[ErrorInfo] | None = [] if save_filtered_errors else None def __enter__(self) -> Self: @@ -236,15 +244,15 @@ class IterationDependentErrors: # the error report occurs but really all unreachable lines. unreachable_lines: list[set[int]] - # One set of revealed types for each `reveal_type` statement. Each created set can - # grow during the iteration. Meaning of the tuple items: function_or_member, line, - # column, end_line, end_column: - revealed_types: dict[tuple[str | None, int, int, int, int], set[str]] + # One list of revealed types for each `reveal_type` statement. Each created list + # can grow during the iteration. Meaning of the tuple items: line, column, + # end_line, end_column: + revealed_types: dict[tuple[int, int, int | None, int | None], list[Type]] def __init__(self) -> None: self.uselessness_errors = [] self.unreachable_lines = [] - self.revealed_types = defaultdict(set) + self.revealed_types = defaultdict(list) class IterationErrorWatcher(ErrorWatcher): @@ -287,15 +295,6 @@ def on_error(self, file: str, info: ErrorInfo) -> bool: iter_errors.unreachable_lines[-1].update(range(info.line, info.end_line + 1)) return True - if info.code == codes.MISC and info.message.startswith("Revealed type is "): - key = info.function_or_member, info.line, info.column, info.end_line, info.end_column - types = info.message.split('"')[1] - if types.startswith("Union["): - iter_errors.revealed_types[key].update(types[6:-1].split(", ")) - else: - iter_errors.revealed_types[key].add(types) - return True - return super().on_error(file, info) def yield_error_infos(self) -> Iterator[tuple[str, Context, ErrorCode]]: @@ -318,21 +317,14 @@ def yield_error_infos(self) -> Iterator[tuple[str, Context, ErrorCode]]: context.end_column = error_info[5] yield error_info[1], context, error_info[0] - def yield_note_infos(self, options: Options) -> Iterator[tuple[str, Context]]: + def yield_note_infos(self, options: Options) -> Iterator[tuple[Type, Context]]: """Yield all types revealed in at least one iteration step.""" for note_info, types in self.iteration_dependent_errors.revealed_types.items(): - sorted_ = sorted(types, key=lambda typ: typ.lower()) - if len(types) == 1: - revealed = sorted_[0] - elif options.use_or_syntax(): - revealed = " | ".join(sorted_) - else: - revealed = f"Union[{', '.join(sorted_)}]" - context = Context(line=note_info[1], column=note_info[2]) - context.end_line = note_info[3] - context.end_column = note_info[4] - yield f'Revealed type is "{revealed}"', context + context = Context(line=note_info[0], column=note_info[1]) + context.end_line = note_info[2] + context.end_column = note_info[3] + yield make_simplified_union(types), context class Errors: @@ -596,18 +588,20 @@ def _add_error_info(self, file: str, info: ErrorInfo) -> None: if info.code in (IMPORT, IMPORT_UNTYPED, IMPORT_NOT_FOUND): self.seen_import_error = True + @property + def watchers(self) -> Iterator[ErrorWatcher]: + """Yield the `ErrorWatcher` stack from top to bottom.""" + i = len(self._watchers) + while i > 0: + i -= 1 + yield self._watchers[i] + def _filter_error(self, file: str, info: ErrorInfo) -> bool: """ process ErrorWatcher stack from top to bottom, stopping early if error needs to be filtered out """ - i = len(self._watchers) - while i > 0: - i -= 1 - w = self._watchers[i] - if w.on_error(file, info): - return True - return False + return any(w.on_error(file, info) for w in self.watchers) def add_error_info(self, info: ErrorInfo) -> None: file, lines = info.origin diff --git a/mypy/messages.py b/mypy/messages.py index 13a4facc82b0..0b91cba382b8 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -23,7 +23,7 @@ from mypy import errorcodes as codes, message_registry from mypy.erasetype import erase_type from mypy.errorcodes import ErrorCode -from mypy.errors import ErrorInfo, Errors, ErrorWatcher +from mypy.errors import ErrorInfo, Errors, ErrorWatcher, IterationErrorWatcher from mypy.nodes import ( ARG_NAMED, ARG_NAMED_OPT, @@ -188,12 +188,14 @@ def filter_errors( filter_errors: bool | Callable[[str, ErrorInfo], bool] = True, save_filtered_errors: bool = False, filter_deprecated: bool = False, + filter_revealed_type: bool = False, ) -> ErrorWatcher: return ErrorWatcher( self.errors, filter_errors=filter_errors, save_filtered_errors=save_filtered_errors, filter_deprecated=filter_deprecated, + filter_revealed_type=filter_revealed_type, ) def add_errors(self, errors: list[ErrorInfo]) -> None: @@ -1738,6 +1740,24 @@ def invalid_signature_for_special_method( ) def reveal_type(self, typ: Type, context: Context) -> None: + + # Search for an error watcher that modifies the "normal" behaviour (we do not + # rely on the normal `ErrorWatcher` filtering approach because we might need to + # collect the original types for a later unionised response): + for watcher in self.errors.watchers: + # The `reveal_type` statement should be ignored: + if watcher.filter_revealed_type: + return + # The `reveal_type` statement might be visited iteratively due to being + # placed in a loop or so. Hence, we collect the respective types of + # individual iterations so that we can report them all in one step later: + if isinstance(watcher, IterationErrorWatcher): + watcher.iteration_dependent_errors.revealed_types[ + (context.line, context.column, context.end_line, context.end_column) + ].append(typ) + return + + # Nothing special here; just create the note: visitor = TypeStrVisitor(options=self.options) self.note(f'Revealed type is "{typ.accept(visitor)}"', context) diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test index 90cb7d3799cf..6564fb3192d0 100644 --- a/test-data/unit/check-inference.test +++ b/test-data/unit/check-inference.test @@ -343,7 +343,7 @@ for var2 in [g, h, i, j, k, l]: reveal_type(var2) # N: Revealed type is "Union[builtins.int, builtins.str]" for var3 in [m, n, o, p, q, r]: - reveal_type(var3) # N: Revealed type is "Union[Any, builtins.int]" + reveal_type(var3) # N: Revealed type is "Union[builtins.int, Any]" T = TypeVar("T", bound=Type[Foo]) @@ -1247,7 +1247,7 @@ class X(TypedDict): x: X for a in ("hourly", "daily"): - reveal_type(a) # N: Revealed type is "Union[Literal['daily']?, Literal['hourly']?]" + reveal_type(a) # N: Revealed type is "Union[Literal['hourly']?, Literal['daily']?]" reveal_type(x[a]) # N: Revealed type is "builtins.int" reveal_type(a.upper()) # N: Revealed type is "builtins.str" c = a diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test index 7a053e1c5cab..e322bd7a37b8 100644 --- a/test-data/unit/check-narrowing.test +++ b/test-data/unit/check-narrowing.test @@ -2346,7 +2346,7 @@ def f() -> bool: ... y = None while f(): - reveal_type(y) # N: Revealed type is "Union[builtins.int, None]" + reveal_type(y) # N: Revealed type is "Union[None, builtins.int]" y = 1 reveal_type(y) # N: Revealed type is "Union[builtins.int, None]" diff --git a/test-data/unit/check-redefine2.test b/test-data/unit/check-redefine2.test index 924e66584669..3523772611aa 100644 --- a/test-data/unit/check-redefine2.test +++ b/test-data/unit/check-redefine2.test @@ -628,7 +628,7 @@ def f1() -> None: def f2() -> None: x = None while int(): - reveal_type(x) # N: Revealed type is "Union[builtins.str, None]" + reveal_type(x) # N: Revealed type is "Union[None, builtins.str]" if int(): x = "" reveal_type(x) # N: Revealed type is "Union[None, builtins.str]" @@ -923,7 +923,7 @@ class X(TypedDict): x: X for a in ("hourly", "daily"): - reveal_type(a) # N: Revealed type is "Union[Literal['daily']?, Literal['hourly']?]" + reveal_type(a) # N: Revealed type is "Union[Literal['hourly']?, Literal['daily']?]" reveal_type(x[a]) # N: Revealed type is "builtins.int" reveal_type(a.upper()) # N: Revealed type is "builtins.str" c = a diff --git a/test-data/unit/check-typevar-tuple.test b/test-data/unit/check-typevar-tuple.test index f44758f7b51b..db0e26ba2b36 100644 --- a/test-data/unit/check-typevar-tuple.test +++ b/test-data/unit/check-typevar-tuple.test @@ -989,7 +989,7 @@ from typing_extensions import Unpack def pipeline(*xs: Unpack[Tuple[int, Unpack[Tuple[float, ...]], bool]]) -> None: for x in xs: - reveal_type(x) # N: Revealed type is "Union[builtins.float, builtins.int]" + reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.float]" [builtins fixtures/tuple.pyi] [case testFixedUnpackItemInInstanceArguments] diff --git a/test-data/unit/check-union-error-syntax.test b/test-data/unit/check-union-error-syntax.test index d41281b774e1..e938598aaefe 100644 --- a/test-data/unit/check-union-error-syntax.test +++ b/test-data/unit/check-union-error-syntax.test @@ -62,17 +62,18 @@ x = 3 # E: Incompatible types in assignment (expression has type "Literal[3]", v try: x = 1 x = "" + x = {1: ""} finally: - reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]" + reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str, builtins.dict[builtins.int, builtins.str]]" [builtins fixtures/isinstancelist.pyi] [case testOrSyntaxRecombined] # flags: --python-version 3.10 --no-force-union-syntax --allow-redefinition-new --local-partial-types # The following revealed type is recombined because the finally body is visited twice. -# ToDo: Improve this recombination logic, especially (but not only) for the "or syntax". try: x = 1 x = "" + x = {1: ""} finally: - reveal_type(x) # N: Revealed type is "builtins.int | builtins.str | builtins.str" + reveal_type(x) # N: Revealed type is "builtins.int | builtins.str | builtins.dict[builtins.int, builtins.str]" [builtins fixtures/isinstancelist.pyi] From 5e30652d5eb63a8a2b6fd96b7eb03b038804ca8f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 21 Jun 2025 21:49:23 +0000 Subject: [PATCH 2/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/errors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/errors.py b/mypy/errors.py index 5373fe619a6a..fdd945d0d259 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -179,7 +179,7 @@ def __init__( filter_errors: bool | Callable[[str, ErrorInfo], bool] = False, save_filtered_errors: bool = False, filter_deprecated: bool = False, - filter_revealed_type: bool = False + filter_revealed_type: bool = False, ) -> None: self.errors = errors self._has_new_errors = False From 6d7ea084b04e0e58f643894b3844b28067a4ac46 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Mon, 23 Jun 2025 22:41:16 +0200 Subject: [PATCH 3/6] remove now unnecessary argument --- mypy/checker.py | 4 ++-- mypy/errors.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index e34feca84a6e..23e8e35b425c 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -643,7 +643,7 @@ def accept_loop( for error_info in watcher.yield_error_infos(): self.msg.fail(*error_info[:2], code=error_info[2]) - for note_info, context in watcher.yield_note_infos(self.options): + for note_info, context in watcher.yield_note_infos(): self.msg.reveal_type(note_info, context) # If exit_condition is set, assume it must be False on exit from the loop: @@ -4987,7 +4987,7 @@ def visit_try_stmt(self, s: TryStmt) -> None: for error_info in watcher.yield_error_infos(): self.msg.fail(*error_info[:2], code=error_info[2]) - for note_info, context in watcher.yield_note_infos(self.options): + for note_info, context in watcher.yield_note_infos(): self.msg.reveal_type(note_info, context) def visit_try_without_finally(self, s: TryStmt, try_frame: bool) -> None: diff --git a/mypy/errors.py b/mypy/errors.py index fdd945d0d259..a76059ce411f 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -317,7 +317,7 @@ def yield_error_infos(self) -> Iterator[tuple[str, Context, ErrorCode]]: context.end_column = error_info[5] yield error_info[1], context, error_info[0] - def yield_note_infos(self, options: Options) -> Iterator[tuple[Type, Context]]: + def yield_note_infos(self) -> Iterator[tuple[Type, Context]]: """Yield all types revealed in at least one iteration step.""" for note_info, types in self.iteration_dependent_errors.revealed_types.items(): From 150e5b843e144114c5a4ed7e4f4930119937c5db Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Tue, 24 Jun 2025 21:37:40 +0200 Subject: [PATCH 4/6] refactoring --- mypy/checker.py | 11 ++-------- mypy/errors.py | 56 +++++++++++++++++++++++------------------------- mypy/messages.py | 14 +++++++++++- 3 files changed, 42 insertions(+), 39 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 23e8e35b425c..5247f07493d9 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -641,10 +641,7 @@ def accept_loop( if iter == 20: raise RuntimeError("Too many iterations when checking a loop") - for error_info in watcher.yield_error_infos(): - self.msg.fail(*error_info[:2], code=error_info[2]) - for note_info, context in watcher.yield_note_infos(): - self.msg.reveal_type(note_info, context) + self.msg.iteration_dependent_errors(iter_errors) # If exit_condition is set, assume it must be False on exit from the loop: if exit_condition: @@ -4984,11 +4981,7 @@ def visit_try_stmt(self, s: TryStmt) -> None: if not self.binder.is_unreachable(): with IterationErrorWatcher(self.msg.errors, iter_errors) as watcher: self.accept(s.finally_body) - - for error_info in watcher.yield_error_infos(): - self.msg.fail(*error_info[:2], code=error_info[2]) - for note_info, context in watcher.yield_note_infos(): - self.msg.reveal_type(note_info, context) + self.msg.iteration_dependent_errors(iter_errors) def visit_try_without_finally(self, s: TryStmt, try_frame: bool) -> None: """Type check a try statement, ignoring the finally block. diff --git a/mypy/errors.py b/mypy/errors.py index a76059ce411f..8f997d889721 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -255,6 +255,33 @@ def __init__(self) -> None: self.revealed_types = defaultdict(list) + def yield_uselessness_error_infos(self) -> Iterator[tuple[str, Context, ErrorCode]]: + """Report only those `unreachable`, `redundant-expr`, and `redundant-casts` + errors that could not be ruled out in any iteration step.""" + + persistent_uselessness_errors = set() + for candidate in set(chain(*self.uselessness_errors)): + if all( + (candidate in errors) or (candidate[2] in lines) + for errors, lines in zip(self.uselessness_errors, self.unreachable_lines) + ): + persistent_uselessness_errors.add(candidate) + for error_info in persistent_uselessness_errors: + context = Context(line=error_info[2], column=error_info[3]) + context.end_line = error_info[4] + context.end_column = error_info[5] + yield error_info[1], context, error_info[0] + + def yield_revealed_type_infos(self) -> Iterator[tuple[Type, Context]]: + """Yield all types revealed in at least one iteration step.""" + + for note_info, types in self.revealed_types.items(): + context = Context(line=note_info[0], column=note_info[1]) + context.end_line = note_info[2] + context.end_column = note_info[3] + yield make_simplified_union(types), context + + class IterationErrorWatcher(ErrorWatcher): """Error watcher that filters and separately collects `unreachable` errors, `redundant-expr` and `redundant-casts` errors, and revealed types when analysing @@ -297,35 +324,6 @@ def on_error(self, file: str, info: ErrorInfo) -> bool: return super().on_error(file, info) - def yield_error_infos(self) -> Iterator[tuple[str, Context, ErrorCode]]: - """Report only those `unreachable`, `redundant-expr`, and `redundant-casts` - errors that could not be ruled out in any iteration step.""" - - persistent_uselessness_errors = set() - iter_errors = self.iteration_dependent_errors - for candidate in set(chain(*iter_errors.uselessness_errors)): - if all( - (candidate in errors) or (candidate[2] in lines) - for errors, lines in zip( - iter_errors.uselessness_errors, iter_errors.unreachable_lines - ) - ): - persistent_uselessness_errors.add(candidate) - for error_info in persistent_uselessness_errors: - context = Context(line=error_info[2], column=error_info[3]) - context.end_line = error_info[4] - context.end_column = error_info[5] - yield error_info[1], context, error_info[0] - - def yield_note_infos(self) -> Iterator[tuple[Type, Context]]: - """Yield all types revealed in at least one iteration step.""" - - for note_info, types in self.iteration_dependent_errors.revealed_types.items(): - context = Context(line=note_info[0], column=note_info[1]) - context.end_line = note_info[2] - context.end_column = note_info[3] - yield make_simplified_union(types), context - class Errors: """Container for compile errors. diff --git a/mypy/messages.py b/mypy/messages.py index 0b91cba382b8..02253ea160e6 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -23,7 +23,13 @@ from mypy import errorcodes as codes, message_registry from mypy.erasetype import erase_type from mypy.errorcodes import ErrorCode -from mypy.errors import ErrorInfo, Errors, ErrorWatcher, IterationErrorWatcher +from mypy.errors import ( + ErrorInfo, + Errors, + ErrorWatcher, + IterationDependentErrors, + IterationErrorWatcher, +) from mypy.nodes import ( ARG_NAMED, ARG_NAMED_OPT, @@ -2501,6 +2507,12 @@ def match_statement_inexhaustive_match(self, typ: Type, context: Context) -> Non code=codes.EXHAUSTIVE_MATCH, ) + def iteration_dependent_errors(self, iter_errors: IterationDependentErrors) -> None: + for error_info in iter_errors.yield_uselessness_error_infos(): + self.fail(*error_info[:2], code=error_info[2]) + for note_info, context in iter_errors.yield_revealed_type_infos(): + self.reveal_type(note_info, context) + def quote_type_string(type_string: str) -> str: """Quotes a type representation for use in messages.""" From 2a686e3928390510c8957759d933506636726a73 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 24 Jun 2025 19:39:23 +0000 Subject: [PATCH 5/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/errors.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mypy/errors.py b/mypy/errors.py index 8f997d889721..c636ac36decb 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -254,7 +254,6 @@ def __init__(self) -> None: self.unreachable_lines = [] self.revealed_types = defaultdict(list) - def yield_uselessness_error_infos(self) -> Iterator[tuple[str, Context, ErrorCode]]: """Report only those `unreachable`, `redundant-expr`, and `redundant-casts` errors that could not be ruled out in any iteration step.""" From 968ac4b4cca1e4b367c6a8f6ae824ffc59136aae Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Tue, 24 Jun 2025 21:51:39 +0200 Subject: [PATCH 6/6] complete refactoring --- mypy/checker.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 5247f07493d9..afac9c85aeee 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -618,7 +618,7 @@ def accept_loop( if on_enter_body is not None: on_enter_body() - with IterationErrorWatcher(self.msg.errors, iter_errors) as watcher: + with IterationErrorWatcher(self.msg.errors, iter_errors): self.accept(body) partials_new = sum(len(pts.map) for pts in self.partial_types) @@ -4962,7 +4962,7 @@ def visit_try_stmt(self, s: TryStmt) -> None: if s.finally_body: # First we check finally_body is type safe on all abnormal exit paths iter_errors = IterationDependentErrors() - with IterationErrorWatcher(self.msg.errors, iter_errors) as watcher: + with IterationErrorWatcher(self.msg.errors, iter_errors): self.accept(s.finally_body) if s.finally_body: @@ -4979,7 +4979,7 @@ def visit_try_stmt(self, s: TryStmt) -> None: # that follows the try statement.) assert iter_errors is not None if not self.binder.is_unreachable(): - with IterationErrorWatcher(self.msg.errors, iter_errors) as watcher: + with IterationErrorWatcher(self.msg.errors, iter_errors): self.accept(s.finally_body) self.msg.iteration_dependent_errors(iter_errors)