From ad4eff0cb0012064882ab78ebc4ec120bb0401a9 Mon Sep 17 00:00:00 2001 From: adhikasp Date: Tue, 29 Aug 2017 09:26:15 +0700 Subject: [PATCH 1/7] Add `populate_all` feature Closes https://github.com/myint/autoflake/issues/16 --- README.rst | 2 ++ autoflake.py | 60 +++++++++++++++++++++++++++++++++++++++++++---- test_autoflake.py | 43 +++++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 274a3e4..a46da95 100644 --- a/README.rst +++ b/README.rst @@ -112,6 +112,8 @@ Below is the full listing of options:: this only triggers if there is only one star import in the file; this is skipped if there are any uses of `__all__` or `del` in the file + --populate-all populate `__all__` with unused import found in the + code. --remove-all-unused-imports remove all unused imports (not just those from the standard library) diff --git a/autoflake.py b/autoflake.py index 46d7a88..9d40269 100755 --- a/autoflake.py +++ b/autoflake.py @@ -294,7 +294,8 @@ def break_up_import(line): def filter_code(source, additional_imports=None, expand_star_imports=False, remove_all_unused_imports=False, - remove_unused_variables=False): + remove_unused_variables=False, + populate_all=False): """Yield code with unused imports removed.""" imports = SAFE_IMPORTS if additional_imports: @@ -335,6 +336,10 @@ def filter_code(source, additional_imports=None, else: marked_variable_line_numbers = frozenset() + if populate_all: + marked_import_line_numbers = frozenset() + source = populate_all_with_modules(source, marked_unused_module) + sio = io.StringIO(source) previous_line = '' for line_number, line in enumerate(sio.readlines(), start=1): @@ -478,6 +483,44 @@ def filter_useless_pass(source): yield line +def populate_all_with_modules(source, marked_unused_module): + all_syntax = re.search('^__all__(.)+\]', source, flags=re.MULTILINE) + if all_syntax: + # If there are existing `__all__`, parse it and append to it + insert_position = all_syntax.span()[0] + end_position = all_syntax.span()[1] + all_modules = all_syntax.group().split('=')[1].strip() + all_modules = ast.literal_eval(all_modules) + else: + # If no existing `__all__`, always append in EOF + insert_position = len(source) + end_position = -1 + all_modules = [] + + for modules in marked_unused_module.values(): + # Get the imported name, `a.b.Foo` -> Foo + all_modules += [get_imported_name(name) for name in modules] + + new_all_syntax = '__all__ = ' + str(all_modules) + source = source[:insert_position] + new_all_syntax + source[end_position:] + return source + + +def get_imported_name(module): + """ + Return only imported name from pyflakes full module path + + Example: + - `a.b.Foo` -> `Foo` + - `a as b` -> b + """ + if '.' in module: + return str(module.split('.')[-1]) + elif ' as ' in module: + return str(module.split(' as ')[-1]) + # str() to force python 2 to not use unicode + return str(module) + def get_indentation(line): """Return leading whitespace.""" if line.strip(): @@ -497,7 +540,8 @@ def get_line_ending(line): def fix_code(source, additional_imports=None, expand_star_imports=False, - remove_all_unused_imports=False, remove_unused_variables=False): + remove_all_unused_imports=False, remove_unused_variables=False, + populate_all=False): """Return code with all filtering run on it.""" if not source: return source @@ -515,9 +559,10 @@ def fix_code(source, additional_imports=None, expand_star_imports=False, additional_imports=additional_imports, expand_star_imports=expand_star_imports, remove_all_unused_imports=remove_all_unused_imports, - remove_unused_variables=remove_unused_variables)))) + remove_unused_variables=remove_unused_variables, + populate_all=populate_all)))) - if filtered_source == source: + if filtered_source == source or populate_all: break source = filtered_source @@ -537,7 +582,9 @@ def fix_file(filename, args, standard_out): additional_imports=args.imports.split(',') if args.imports else None, expand_star_imports=args.expand_star_imports, remove_all_unused_imports=args.remove_all_unused_imports, - remove_unused_variables=args.remove_unused_variables) + remove_unused_variables=args.remove_unused_variables, + populate_all=args.populate_modules_under_all, + ) if original_source != filtered_source: if args.in_place: @@ -692,6 +739,9 @@ def _main(argv, standard_out, standard_error): 'one star import in the file; this is skipped if ' 'there are any uses of `__all__` or `del` in the ' 'file') + parser.add_argument('--populate-modules-under-all', action='store_true', + help='populate `__all__` with unused import found in ' + 'the code.') parser.add_argument('--remove-all-unused-imports', action='store_true', help='remove all unused imports (not just those from ' 'the standard library)') diff --git a/test_autoflake.py b/test_autoflake.py index ca54190..e6e8e30 100755 --- a/test_autoflake.py +++ b/test_autoflake.py @@ -485,6 +485,49 @@ def foo(): """ self.assertEqual(line, ''.join(autoflake.filter_code(line))) + def test_filter_code_populate_all(self): + self.assertEqual(""" +import math +import sys +__all__ = ['math', 'sys'] +""", ''.join(autoflake.filter_code(""" +import math +import sys +""", populate_all=True))) + + def test_filter_code_populate_all_appending(self): + self.assertEqual(""" +import math +import sys +__all__ = ['math', 'sys'] +""", ''.join(autoflake.filter_code(""" +import math +import sys +__all__ = ['math'] +""", populate_all=True))) + + def test_filter_code_populate_all_ignore_comment(self): + self.assertEqual(""" +import math +import sys +# __all__ = ['math'] +__all__ = ['math', 'sys'] +""", ''.join(autoflake.filter_code(""" +import math +import sys +# __all__ = ['math'] +""", populate_all=True))) + + def test_filter_code_populate_all_from_import(self): + self.assertEqual(""" +from a.b import Foo +from a.c import Bar +__all__ = ['Foo', 'Bar'] +""", ''.join(autoflake.filter_code(""" +from a.b import Foo +from a.c import Bar +""", populate_all=True))) + def test_fix_code(self): self.assertEqual( """\ From 87b40d50f757e138e721b3b0b53c928c0a3768f7 Mon Sep 17 00:00:00 2001 From: adhikasp Date: Mon, 4 Sep 2017 11:26:59 +0700 Subject: [PATCH 2/7] Add test case and update code --- autoflake.py | 10 ++++++---- test_autoflake.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/autoflake.py b/autoflake.py index 9d40269..72eb57d 100755 --- a/autoflake.py +++ b/autoflake.py @@ -515,11 +515,13 @@ def get_imported_name(module): - `a as b` -> b """ if '.' in module: - return str(module.split('.')[-1]) - elif ' as ' in module: - return str(module.split(' as ')[-1]) + name = module.split('.')[-1] + elif re.search(r'\bas\b', module): + name = re.split(r'\bas\b', module)[-1] + else: + name = module # str() to force python 2 to not use unicode - return str(module) + return str(name.strip()) def get_indentation(line): """Return leading whitespace.""" diff --git a/test_autoflake.py b/test_autoflake.py index e6e8e30..3693655 100755 --- a/test_autoflake.py +++ b/test_autoflake.py @@ -526,6 +526,22 @@ def test_filter_code_populate_all_from_import(self): """, ''.join(autoflake.filter_code(""" from a.b import Foo from a.c import Bar +""", populate_all=True))) + + def test_filter_code_populate_all_as(self): + self.assertEqual(""" +import math as m +__all__ = ['m'] +""", ''.join(autoflake.filter_code(""" +import math as m +""", populate_all=True))) + + def test_filter_code_populate_all_with_tab(self): + self.assertEqual(""" +import math\tas\tm +__all__ = ['m'] +""", ''.join(autoflake.filter_code(""" +import math\tas\tm """, populate_all=True))) def test_fix_code(self): From 27a89d9642dd95422d26c86f37769108875276d5 Mon Sep 17 00:00:00 2001 From: adhikasp Date: Mon, 4 Sep 2017 11:28:24 +0700 Subject: [PATCH 3/7] Update name to dunder --- autoflake.py | 18 +++++++++--------- test_autoflake.py | 24 ++++++++++++------------ 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/autoflake.py b/autoflake.py index 72eb57d..e18847e 100755 --- a/autoflake.py +++ b/autoflake.py @@ -295,7 +295,7 @@ def filter_code(source, additional_imports=None, expand_star_imports=False, remove_all_unused_imports=False, remove_unused_variables=False, - populate_all=False): + populate_dunder_all=False): """Yield code with unused imports removed.""" imports = SAFE_IMPORTS if additional_imports: @@ -336,9 +336,9 @@ def filter_code(source, additional_imports=None, else: marked_variable_line_numbers = frozenset() - if populate_all: + if populate_dunder_all: marked_import_line_numbers = frozenset() - source = populate_all_with_modules(source, marked_unused_module) + source = populate_dunder_all_with_modules(source, marked_unused_module) sio = io.StringIO(source) previous_line = '' @@ -483,7 +483,7 @@ def filter_useless_pass(source): yield line -def populate_all_with_modules(source, marked_unused_module): +def populate_dunder_all_with_modules(source, marked_unused_module): all_syntax = re.search('^__all__(.)+\]', source, flags=re.MULTILINE) if all_syntax: # If there are existing `__all__`, parse it and append to it @@ -543,7 +543,7 @@ def get_line_ending(line): def fix_code(source, additional_imports=None, expand_star_imports=False, remove_all_unused_imports=False, remove_unused_variables=False, - populate_all=False): + populate_dunder_all=False): """Return code with all filtering run on it.""" if not source: return source @@ -562,9 +562,9 @@ def fix_code(source, additional_imports=None, expand_star_imports=False, expand_star_imports=expand_star_imports, remove_all_unused_imports=remove_all_unused_imports, remove_unused_variables=remove_unused_variables, - populate_all=populate_all)))) + populate_dunder_all=populate_dunder_all)))) - if filtered_source == source or populate_all: + if filtered_source == source or populate_dunder_all: break source = filtered_source @@ -585,7 +585,7 @@ def fix_file(filename, args, standard_out): expand_star_imports=args.expand_star_imports, remove_all_unused_imports=args.remove_all_unused_imports, remove_unused_variables=args.remove_unused_variables, - populate_all=args.populate_modules_under_all, + populate_dunder_all=args.populate_modules_dunder_all, ) if original_source != filtered_source: @@ -741,7 +741,7 @@ def _main(argv, standard_out, standard_error): 'one star import in the file; this is skipped if ' 'there are any uses of `__all__` or `del` in the ' 'file') - parser.add_argument('--populate-modules-under-all', action='store_true', + parser.add_argument('--populate-modules-dunder-all', action='store_true', help='populate `__all__` with unused import found in ' 'the code.') parser.add_argument('--remove-all-unused-imports', action='store_true', diff --git a/test_autoflake.py b/test_autoflake.py index 3693655..7d6af22 100755 --- a/test_autoflake.py +++ b/test_autoflake.py @@ -485,7 +485,7 @@ def foo(): """ self.assertEqual(line, ''.join(autoflake.filter_code(line))) - def test_filter_code_populate_all(self): + def test_filter_code_populate_dunder_all(self): self.assertEqual(""" import math import sys @@ -493,9 +493,9 @@ def test_filter_code_populate_all(self): """, ''.join(autoflake.filter_code(""" import math import sys -""", populate_all=True))) +""", populate_dunder_all=True))) - def test_filter_code_populate_all_appending(self): + def test_filter_code_populate_dunder_all_appending(self): self.assertEqual(""" import math import sys @@ -504,9 +504,9 @@ def test_filter_code_populate_all_appending(self): import math import sys __all__ = ['math'] -""", populate_all=True))) +""", populate_dunder_all=True))) - def test_filter_code_populate_all_ignore_comment(self): + def test_filter_code_populate_dunder_all_ignore_comment(self): self.assertEqual(""" import math import sys @@ -516,9 +516,9 @@ def test_filter_code_populate_all_ignore_comment(self): import math import sys # __all__ = ['math'] -""", populate_all=True))) +""", populate_dunder_all=True))) - def test_filter_code_populate_all_from_import(self): + def test_filter_code_populate_dunder_all_from_import(self): self.assertEqual(""" from a.b import Foo from a.c import Bar @@ -526,23 +526,23 @@ def test_filter_code_populate_all_from_import(self): """, ''.join(autoflake.filter_code(""" from a.b import Foo from a.c import Bar -""", populate_all=True))) +""", populate_dunder_all=True))) - def test_filter_code_populate_all_as(self): + def test_filter_code_populate_dunder_all_as(self): self.assertEqual(""" import math as m __all__ = ['m'] """, ''.join(autoflake.filter_code(""" import math as m -""", populate_all=True))) +""", populate_dunder_all=True))) - def test_filter_code_populate_all_with_tab(self): + def test_filter_code_populate_dunder_all_with_tab(self): self.assertEqual(""" import math\tas\tm __all__ = ['m'] """, ''.join(autoflake.filter_code(""" import math\tas\tm -""", populate_all=True))) +""", populate_dunder_all=True))) def test_fix_code(self): self.assertEqual( From 310e1a372986ce951f6d1be24544bb02b23b0309 Mon Sep 17 00:00:00 2001 From: Steven Myint Date: Sun, 8 Oct 2017 12:26:03 -0700 Subject: [PATCH 4/7] Do not output empty `__all__` --- autoflake.py | 18 +++++++++++++----- test_autoflake.py | 15 ++++++++++++--- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/autoflake.py b/autoflake.py index e18847e..aab39e8 100755 --- a/autoflake.py +++ b/autoflake.py @@ -484,6 +484,7 @@ def filter_useless_pass(source): def populate_dunder_all_with_modules(source, marked_unused_module): + """Return source with `__all__` properly populated.""" all_syntax = re.search('^__all__(.)+\]', source, flags=re.MULTILINE) if all_syntax: # If there are existing `__all__`, parse it and append to it @@ -501,18 +502,24 @@ def populate_dunder_all_with_modules(source, marked_unused_module): # Get the imported name, `a.b.Foo` -> Foo all_modules += [get_imported_name(name) for name in modules] - new_all_syntax = '__all__ = ' + str(all_modules) - source = source[:insert_position] + new_all_syntax + source[end_position:] - return source + if all_modules: + new_all_syntax = '__all__ = ' + str(all_modules) + return ( + source[:insert_position] + + new_all_syntax + + source[end_position:] + ) + else: + return source def get_imported_name(module): - """ - Return only imported name from pyflakes full module path + """Return only imported name from pyflakes full module path. Example: - `a.b.Foo` -> `Foo` - `a as b` -> b + """ if '.' in module: name = module.split('.')[-1] @@ -523,6 +530,7 @@ def get_imported_name(module): # str() to force python 2 to not use unicode return str(name.strip()) + def get_indentation(line): """Return leading whitespace.""" if line.strip(): diff --git a/test_autoflake.py b/test_autoflake.py index 7d6af22..e2bb71e 100755 --- a/test_autoflake.py +++ b/test_autoflake.py @@ -544,6 +544,15 @@ def test_filter_code_populate_dunder_all_with_tab(self): import math\tas\tm """, populate_dunder_all=True))) + def test_filter_code_populate_dunder_all_with_no_change(self): + code = """ +def foo(): + bar = 0 +""" + self.assertEqual( + code, + ''.join(autoflake.filter_code(code, populate_dunder_all=True))) + def test_fix_code(self): self.assertEqual( """\ @@ -1046,7 +1055,7 @@ def test_exclude(self): temp_directory = tempfile.mkdtemp(dir='.') try: with open(os.path.join(temp_directory, 'a.py'), 'w') as output: - output.write("import re\n") + output.write('import re\n') os.mkdir(os.path.join(temp_directory, 'd')) with open(os.path.join(temp_directory, 'd', 'b.py'), @@ -1054,8 +1063,8 @@ def test_exclude(self): output.write('import os\n') p = subprocess.Popen(list(AUTOFLAKE_COMMAND) + - [temp_directory, '--recursive', '--exclude=a*'], - stdout=subprocess.PIPE) + [temp_directory, '--recursive', '--exclude=a*'], + stdout=subprocess.PIPE) result = p.communicate()[0].decode('utf-8') self.assertNotIn('import re', result) From 7b2b5ae1c361fe3ff089d86ca3b0461707cecf5b Mon Sep 17 00:00:00 2001 From: Steven Myint Date: Sun, 8 Oct 2017 12:32:21 -0700 Subject: [PATCH 5/7] Add option to fuzzer --- test_fuzz.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test_fuzz.py b/test_fuzz.py index 7fd8246..d2a5656 100755 --- a/test_fuzz.py +++ b/test_fuzz.py @@ -149,6 +149,10 @@ def process_args(): parser.add_argument('--imports', help='pass to the autoflake "--imports" option') + parser.add_argument('--populate-modules-dunder-all', action='store_true', + help='populate `__all__` with unused import found in ' + 'the code.') + parser.add_argument('--remove-all-unused-imports', action='store_true', help='pass "--remove-all-unused-imports" option to ' 'autoflake') @@ -190,6 +194,9 @@ def check(args): if args.remove_unused_variables: options.append('--remove-unused-variables') + if args.populate_modules_dunder_all: + options.append('--populate-modules-dunder-all') + filenames = dir_paths completed_filenames = set() From f2c425141f5be8bffa178bdc47252863701330ad Mon Sep 17 00:00:00 2001 From: Steven Myint Date: Sun, 8 Oct 2017 12:43:19 -0700 Subject: [PATCH 6/7] Avoid complex cases --- autoflake.py | 19 +++++++------------ test_autoflake.py | 30 ++++++++++-------------------- 2 files changed, 17 insertions(+), 32 deletions(-) diff --git a/autoflake.py b/autoflake.py index aab39e8..27274fe 100755 --- a/autoflake.py +++ b/autoflake.py @@ -485,18 +485,13 @@ def filter_useless_pass(source): def populate_dunder_all_with_modules(source, marked_unused_module): """Return source with `__all__` properly populated.""" - all_syntax = re.search('^__all__(.)+\]', source, flags=re.MULTILINE) - if all_syntax: - # If there are existing `__all__`, parse it and append to it - insert_position = all_syntax.span()[0] - end_position = all_syntax.span()[1] - all_modules = all_syntax.group().split('=')[1].strip() - all_modules = ast.literal_eval(all_modules) - else: - # If no existing `__all__`, always append in EOF - insert_position = len(source) - end_position = -1 - all_modules = [] + if re.search(r'\b__all__\b', source): + # If there are existing `__all__`, don't mess with it. + return source + + insert_position = len(source) + end_position = -1 + all_modules = [] for modules in marked_unused_module.values(): # Get the imported name, `a.b.Foo` -> Foo diff --git a/test_autoflake.py b/test_autoflake.py index e2bb71e..b33789a 100755 --- a/test_autoflake.py +++ b/test_autoflake.py @@ -495,28 +495,18 @@ def test_filter_code_populate_dunder_all(self): import sys """, populate_dunder_all=True))) - def test_filter_code_populate_dunder_all_appending(self): - self.assertEqual(""" -import math -import sys -__all__ = ['math', 'sys'] -""", ''.join(autoflake.filter_code(""" -import math -import sys -__all__ = ['math'] -""", populate_dunder_all=True))) - - def test_filter_code_populate_dunder_all_ignore_comment(self): - self.assertEqual(""" -import math -import sys -# __all__ = ['math'] -__all__ = ['math', 'sys'] -""", ''.join(autoflake.filter_code(""" + def test_filter_code_populate_dunder_all_should_not_create_a_mess(self): + code = """ import math import sys -# __all__ = ['math'] -""", populate_dunder_all=True))) +__all__ = [ + 'math', 'sys' +] +import abc +""" + self.assertEqual( + code, + ''.join(autoflake.filter_code(code, populate_dunder_all=True))) def test_filter_code_populate_dunder_all_from_import(self): self.assertEqual(""" From ca4bc3dadd4c34257f0764cb6724d0e6e0883812 Mon Sep 17 00:00:00 2001 From: Steven Myint Date: Sun, 8 Oct 2017 13:28:55 -0700 Subject: [PATCH 7/7] Add a test case to demonstrate a bug --- test_autoflake.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test_autoflake.py b/test_autoflake.py index b33789a..a97667d 100755 --- a/test_autoflake.py +++ b/test_autoflake.py @@ -503,6 +503,14 @@ def test_filter_code_populate_dunder_all_should_not_create_a_mess(self): 'math', 'sys' ] import abc +""" + self.assertEqual( + code, + ''.join(autoflake.filter_code(code, populate_dunder_all=True))) + + def test_filter_code_populate_dunder_all_should_ignore_dotted_import(self): + code = """ +import foo.bar """ self.assertEqual( code,