diff --git a/README.md b/README.md index 4617b46..40df4b7 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ the standard library. (Other modules may have side effects that make them unsafe to remove automatically.) Removal of unused variables is also disabled by default. -autoflake also removes useless ``pass`` statements. +autoflake also removes useless ``pass`` statements by default. ## Example @@ -106,6 +106,10 @@ options: remove all unused imports (not just those from the standard library) --ignore-init-module-imports exclude __init__.py when removing unused imports + --ignore-pass-statements + keep all `pass` statements + --ignore-pass-after-docstring + keep `pass` statements after a new line ending on """ --remove-duplicate-keys remove all duplicate keys in objects --remove-unused-variables diff --git a/autoflake.py b/autoflake.py index 84521f2..086ec80 100755 --- a/autoflake.py +++ b/autoflake.py @@ -743,7 +743,10 @@ def is_literal_or_name(value): return re.match(r"^\w+\s*$", value) -def useless_pass_line_numbers(source): +def useless_pass_line_numbers( + source, + ignore_pass_after_docstring=False, +): """Yield line numbers of unneeded "pass" statements.""" sio = io.StringIO(source) previous_token_type = None @@ -770,24 +773,45 @@ def useless_pass_line_numbers(source): last_pass_row = start_row last_pass_indentation = get_indentation(line) - # Trailing "pass". - if ( - is_pass - and previous_token_type != tokenize.INDENT - and not previous_line.rstrip().endswith("\\") - ): - yield start_row + is_trailing_pass = ( + previous_token_type != tokenize.INDENT + and not previous_line.rstrip().endswith("\\") + ) + + is_pass_after_docstring = ( + previous_token_type == tokenize.NEWLINE + and previous_line.rstrip().endswith('"""') + ) + + # Trailing "pass". + if is_trailing_pass: + if is_pass_after_docstring and ignore_pass_after_docstring: + continue + else: + yield start_row previous_token_type = token_type previous_line = line -def filter_useless_pass(source): +def filter_useless_pass( + source, + ignore_pass_statements=False, + ignore_pass_after_docstring=False, +): """Yield code with useless "pass" lines removed.""" - try: - marked_lines = frozenset(useless_pass_line_numbers(source)) - except (SyntaxError, tokenize.TokenError): + if ignore_pass_statements: marked_lines = frozenset() + else: + try: + marked_lines = frozenset( + useless_pass_line_numbers( + source, + ignore_pass_after_docstring, + ), + ) + except (SyntaxError, tokenize.TokenError): + marked_lines = frozenset() sio = io.StringIO(source) for line_number, line in enumerate(sio.readlines(), start=1): @@ -822,6 +846,8 @@ def fix_code( remove_unused_variables=False, remove_rhs_for_unused_variables=False, ignore_init_module_imports=False, + ignore_pass_statements=False, + ignore_pass_after_docstring=False, ): """Return code with all filtering run on it.""" if not source: @@ -849,6 +875,8 @@ def fix_code( ignore_init_module_imports=ignore_init_module_imports, ), ), + ignore_pass_statements=ignore_pass_statements, + ignore_pass_after_docstring=ignore_pass_after_docstring, ), ) @@ -902,6 +930,8 @@ def _fix_file( remove_unused_variables=args["remove_unused_variables"], remove_rhs_for_unused_variables=(args["remove_rhs_for_unused_variables"]), ignore_init_module_imports=ignore_init_module_imports, + ignore_pass_statements=args["ignore_pass_statements"], + ignore_pass_after_docstring=args["ignore_pass_after_docstring"], ) if original_source != filtered_source: @@ -1268,6 +1298,16 @@ def _main(argv, standard_out, standard_error, standard_input=None) -> int: action="store_true", help="remove RHS of statements when removing unused " "variables (unsafe)", ) + parser.add_argument( + "--ignore-pass-statements", + action="store_true", + help="ignore all pass statements", + ) + parser.add_argument( + "--ignore-pass-after-docstring", + action="store_true", + help='ignore pass statements after a newline ending on \'"""\'', + ) parser.add_argument( "--version", action="version", diff --git a/test_autoflake.py b/test_autoflake.py index 8154fcd..e8f30ff 100755 --- a/test_autoflake.py +++ b/test_autoflake.py @@ -1412,6 +1412,72 @@ def test_fix_code_should_ignore_duplicate_key_with_no_comma(self): ), ) + def test_fix_code_keeps_pass_statements(self): + code = """\ + if True: + pass + else: + def foo(): + \"\"\" A docstring. \"\"\" + pass + def bar(): + # abc + pass + def blah(): + 123 + pass + pass # Nope. + pass + """ + + self.assertEqual( + code, + "".join( + autoflake.fix_code( + code, + ignore_pass_statements=True, + ), + ), + ) + + def test_fix_code_keeps_passes_after_docstrings(self): + actual = autoflake.fix_code( + """\ + if True: + pass + else: + def foo(): + \"\"\" A docstring. \"\"\" + pass + def bar(): + # abc + pass + def blah(): + 123 + pass + pass # Nope. + pass + """, + ignore_pass_after_docstring=True, + ) + + expected = """\ + if True: + pass + else: + def foo(): + \"\"\" A docstring. \"\"\" + pass + def bar(): + # abc + pass + def blah(): + 123 + pass # Nope. + """ + + self.assertEqual(actual, expected) + def test_useless_pass_line_numbers(self): self.assertEqual( [1], @@ -1458,6 +1524,37 @@ def test_useless_pass_line_numbers_with_more_complex(self): ), ) + def test_useless_pass_line_numbers_after_docstring(self): + actual_pass_line_numbers = list( + autoflake.useless_pass_line_numbers( + """\ + @abc.abstractmethod + def some_abstract_method(): + \"\"\"Some docstring.\"\"\" + pass + """, + ), + ) + + expected_pass_line_numbers = [4] + self.assertEqual(expected_pass_line_numbers, actual_pass_line_numbers) + + def test_useless_pass_line_numbers_keep_pass_after_docstring(self): + actual_pass_line_numbers = list( + autoflake.useless_pass_line_numbers( + """\ + @abc.abstractmethod + def some_abstract_method(): + \"\"\"Some docstring.\"\"\" + pass + """, + ignore_pass_after_docstring=True, + ), + ) + + expected_pass_line_numbers = [] + self.assertEqual(expected_pass_line_numbers, actual_pass_line_numbers) + def test_filter_useless_pass(self): self.assertEqual( """\ @@ -1545,7 +1642,54 @@ def blah(): ), ) - def test_filter_useless_pass_with_try(self): + def test_filter_useless_pass_keep_pass_after_docstring(self): + source = """\ + def foo(): + \"\"\" This is not a useless 'pass'. \"\"\" + pass + + @abc.abstractmethod + def bar(): + \"\"\" + Also this is not a useless 'pass'. + \"\"\" + pass + """ + self.assertEqual( + source, + "".join( + autoflake.filter_useless_pass( + source, + ignore_pass_after_docstring=True, + ), + ), + ) + + def test_filter_useless_pass_keeps_pass_statements(self): + source = """\ + if True: + pass + pass + pass + pass + else: + pass + True + x = 1 + pass + """ + + self.assertEqual( + source, + "".join( + autoflake.filter_useless_pass( + source, + ignore_pass_statements=True, + ), + ), + ) + + def test_filter_useless_paspasss_with_try(self): self.assertEqual( """\ import os