From cab93bb8d4253b9ae0190425550fe83d0e2bd6b5 Mon Sep 17 00:00:00 2001 From: Vasilis Themelis Date: Sat, 26 Aug 2023 16:14:24 +0100 Subject: [PATCH] Add `pass` to removed blocks of code Solves #352 more generally. --- .gitignore | 1 + src/typeguard/_transformer.py | 21 ++++++++++++---- tests/test_transformer.py | 47 +++++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index d66eb5ae..6d027d31 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ .idea .tox .coverage +**/.coverage.* .cache .eggs/ *.egg-info/ diff --git a/src/typeguard/_transformer.py b/src/typeguard/_transformer.py index 24c723c3..541db56a 100644 --- a/src/typeguard/_transformer.py +++ b/src/typeguard/_transformer.py @@ -55,6 +55,7 @@ copy_location, expr, fix_missing_locations, + iter_fields, keyword, walk, ) @@ -498,6 +499,21 @@ def __init__( self.target_node: FunctionDef | AsyncFunctionDef | None = None self.target_lineno = target_lineno + def generic_visit(self, node: AST) -> AST: + non_empty_list_fields = [] + for field, val in iter_fields(node): + if isinstance(val, list) and len(val) > 0: + non_empty_list_fields.append(field) + + node = super().generic_visit(node) + + # Add `pass` to list fields that were optimised away + for field in non_empty_list_fields: + if not hasattr(node, field) or not getattr(node, field): + setattr(node, field, [Pass()]) + + return node + @contextmanager def _use_memo( self, node: ClassDef | FunctionDef | AsyncFunctionDef @@ -1175,11 +1191,6 @@ def visit_If(self, node: If) -> Any: """ self.generic_visit(node) - # Fix empty node body (caused by removal of classes/functions not on the target - # path) - if not node.body: - node.body.append(Pass()) - if ( self._memo is self._module_memo and isinstance(node.test, Name) diff --git a/tests/test_transformer.py b/tests/test_transformer.py index 67890cad..5c63d13e 100644 --- a/tests/test_transformer.py +++ b/tests/test_transformer.py @@ -1523,6 +1523,53 @@ def foo(x: str) -> None: ) +def test_dont_leave_empty_ast_container_nodes_2() -> None: + # Regression test for #352 + node = parse( + dedent( + """ + try: + + class A: + ... + + def func(): + ... + + except: + + class A: + ... + + def func(): + ... + + + def foo(x: str) -> None: + pass + """ + ) + ) + TypeguardTransformer(["foo"]).visit(node) + assert ( + unparse(node) + == dedent( + """ + try: + pass + except: + pass + + def foo(x: str) -> None: + from typeguard import TypeCheckMemo + from typeguard._functions import check_argument_types + memo = TypeCheckMemo(globals(), locals()) + check_argument_types('foo', {'x': (x, str)}, memo) + """ + ).strip() + ) + + def test_dont_parse_annotated_2nd_arg() -> None: # Regression test for #352 node = parse(