Hello community,
here is the log from the commit of package python-autoflake for
openSUSE:Factory checked in at 2020-09-07 21:35:05
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-autoflake (Old)
and /work/SRC/openSUSE:Factory/.python-autoflake.new.3399 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-autoflake"
Mon Sep 7 21:35:05 2020 rev:6 rq:832643 version:1.4
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-autoflake/python-autoflake.changes
2019-09-16 10:50:36.195170672 +0200
+++
/work/SRC/openSUSE:Factory/.python-autoflake.new.3399/python-autoflake.changes
2020-09-07 21:35:31.745385570 +0200
@@ -1,0 +2,7 @@
+Mon Aug 31 04:16:47 UTC 2020 - Steve Kowalik <[email protected]>
+
+- Update to 1.4:
+ * No upstream changelog
+- Switch from setup.py test to pytest
+
+-------------------------------------------------------------------
Old:
----
autoflake-1.3.1.tar.gz
New:
----
autoflake-1.4.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-autoflake.spec ++++++
--- /var/tmp/diff_new_pack.I9S9Jj/_old 2020-09-07 21:35:33.429386350 +0200
+++ /var/tmp/diff_new_pack.I9S9Jj/_new 2020-09-07 21:35:33.429386350 +0200
@@ -1,7 +1,7 @@
#
# spec file for package python-autoflake
#
-# Copyright (c) 2019 SUSE LINUX GmbH, Nuernberg, Germany.
+# Copyright (c) 2020 SUSE LLC
#
# All modifications and additions to the file contributed by third parties
# remain the property of their copyright owners, unless otherwise agreed
@@ -18,13 +18,13 @@
%{?!python_module:%define python_module() python-%{**} python3-%{**}}
Name: python-autoflake
-Version: 1.3.1
+Version: 1.4
Release: 0
Summary: Program to removes unused Python imports and variables
License: MIT
-Group: Development/Languages/Python
URL: https://github.com/myint/autoflake
Source:
https://files.pythonhosted.org/packages/source/a/autoflake/autoflake-%{version}.tar.gz
+BuildRequires: %{python_module pytest}
BuildRequires: %{python_module setuptools}
BuildRequires: fdupes
BuildRequires: python-rpm-macros
@@ -63,7 +63,7 @@
%check
export $LANG=en_US.UTF-8
-%python_exec setup.py test
+%pytest
%post
%python_install_alternative autoflake
++++++ autoflake-1.3.1.tar.gz -> autoflake-1.4.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/autoflake-1.3.1/AUTHORS.rst
new/autoflake-1.4/AUTHORS.rst
--- old/autoflake-1.3.1/AUTHORS.rst 2019-06-02 17:57:42.000000000 +0200
+++ new/autoflake-1.4/AUTHORS.rst 2019-08-24 19:44:29.000000000 +0200
@@ -13,3 +13,4 @@
- Nobuhiro Kasai (https://github.com/sh4869)
- James Curtin (https://github.com/jamescurtin)
- Sargun Dhillon (https://github.com/sargun)
+- Anton Ogorodnikov (https://github.com/arxell)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/autoflake-1.3.1/PKG-INFO new/autoflake-1.4/PKG-INFO
--- old/autoflake-1.3.1/PKG-INFO 2019-08-04 17:14:15.000000000 +0200
+++ new/autoflake-1.4/PKG-INFO 2020-08-23 03:20:53.520906700 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 1.1
Name: autoflake
-Version: 1.3.1
+Version: 1.4
Summary: Removes unused imports and unused variables
Home-page: https://github.com/myint/autoflake
Author: Steven Myint
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/autoflake-1.3.1/autoflake.egg-info/PKG-INFO
new/autoflake-1.4/autoflake.egg-info/PKG-INFO
--- old/autoflake-1.3.1/autoflake.egg-info/PKG-INFO 2019-08-04
17:14:15.000000000 +0200
+++ new/autoflake-1.4/autoflake.egg-info/PKG-INFO 2020-08-23
03:20:53.000000000 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 1.1
Name: autoflake
-Version: 1.3.1
+Version: 1.4
Summary: Removes unused imports and unused variables
Home-page: https://github.com/myint/autoflake
Author: Steven Myint
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/autoflake-1.3.1/autoflake.py
new/autoflake-1.4/autoflake.py
--- old/autoflake-1.3.1/autoflake.py 2019-08-04 17:13:52.000000000 +0200
+++ new/autoflake-1.4/autoflake.py 2020-08-23 03:19:47.000000000 +0200
@@ -32,9 +32,11 @@
import distutils.sysconfig
import fnmatch
import io
+import logging
import os
import re
import signal
+import string
import sys
import tokenize
@@ -43,9 +45,12 @@
import pyflakes.reporter
-__version__ = '1.3.1'
+__version__ = '1.4'
+_LOGGER = logging.getLogger('autoflake')
+_LOGGER.propagate = False
+
ATOMS = frozenset([tokenize.NAME, tokenize.NUMBER, tokenize.STRING])
EXCEPT_REGEX = re.compile(r'^\s*except [\s,()\w]+ as \w+:$')
@@ -69,17 +74,18 @@
def standard_paths():
"""Yield paths to standard modules."""
for is_plat_spec in [True, False]:
+
+ # Yield lib paths.
path = distutils.sysconfig.get_python_lib(standard_lib=True,
plat_specific=is_plat_spec)
-
for name in os.listdir(path):
yield name
- try:
- for name in os.listdir(os.path.join(path, 'lib-dynload')):
+ # Yield lib-dynload paths.
+ dynload_path = os.path.join(path, 'lib-dynload')
+ if os.path.isdir(dynload_path):
+ for name in os.listdir(dynload_path):
yield name
- except OSError: # pragma: no cover
- pass
def standard_package_names():
@@ -88,7 +94,7 @@
if name.startswith('_') or '-' in name:
continue
- if '.' in name and name.rsplit('.')[-1] not in ['so', 'py', 'pyc']:
+ if '.' in name and not name.endswith(("so", "py", "pyc")):
continue
yield name.split('.')[0]
@@ -250,10 +256,6 @@
if symbol in line:
return True
- # Ignore doctests.
- if line.lstrip().startswith('>'):
- return True
-
return multiline_statement(line, previous_line)
@@ -271,6 +273,182 @@
return True
+class PendingFix(object):
+ """Allows a rewrite operation to span multiple lines.
+
+ In the main rewrite loop, every time a helper function returns a
+ ``PendingFix`` object instead of a string, this object will be called
+ with the following line.
+ """
+
+ def __init__(self, line):
+ """Analyse and store the first line."""
+ self.accumulator = collections.deque([line])
+
+ def __call__(self, line):
+ """Process line considering the accumulator.
+
+ Return self to keep processing the following lines or a string
+ with the final result of all the lines processed at once.
+ """
+ raise NotImplementedError("Abstract method needs to be overwritten")
+
+
+def _valid_char_in_line(char, line):
+ """Return True if a char appears in the line and is not commented."""
+ comment_index = line.find('#')
+ char_index = line.find(char)
+ valid_char_in_line = (
+ char_index >= 0 and
+ (comment_index > char_index or comment_index < 0)
+ )
+ return valid_char_in_line
+
+
+def _top_module(module_name):
+ """Return the name of the top level module in the hierarchy."""
+ if module_name[0] == '.':
+ return '%LOCAL_MODULE%'
+ return module_name.split('.')[0]
+
+
+def _modules_to_remove(unused_modules, safe_to_remove=SAFE_IMPORTS):
+ """Discard unused modules that are not safe to remove from the list."""
+ return [x for x in unused_modules if _top_module(x) in safe_to_remove]
+
+
+def _segment_module(segment):
+ """Extract the module identifier inside the segment.
+
+ It might be the case the segment does not have a module (e.g. is composed
+ just by a parenthesis or line continuation and whitespace). In this
+ scenario we just keep the segment... These characters are not valid in
+ identifiers, so they will never be contained in the list of unused modules
+ anyway.
+ """
+ return segment.strip(string.whitespace + ',\\()') or segment
+
+
+class FilterMultilineImport(PendingFix):
+ """Remove unused imports from multiline import statements.
+
+ This class handles both the cases: "from imports" and "direct imports".
+
+ Some limitations exist (e.g. imports with comments, lines joined by ``;``,
+ etc). In these cases, the statement is left unchanged to avoid problems.
+ """
+
+ IMPORT_RE = re.compile(r'\bimport\b\s*')
+ INDENTATION_RE = re.compile(r'^\s*')
+ BASE_RE = re.compile(r'\bfrom\s+([^ ]+)')
+ SEGMENT_RE = re.compile(
+ r'([^,\s]+(?:[\s\\]+as[\s\\]+[^,\s]+)?[,\s\\)]*)', re.M)
+ # ^ module + comma + following space (including new line and continuation)
+ IDENTIFIER_RE = re.compile(r'[^,\s]+')
+
+ def __init__(self, line, unused_module=(), remove_all_unused_imports=False,
+ safe_to_remove=SAFE_IMPORTS, previous_line=''):
+ """Receive the same parameters as ``filter_unused_import``."""
+ self.remove = unused_module
+ self.parenthesized = '(' in line
+ self.from_, imports = self.IMPORT_RE.split(line, maxsplit=1)
+ match = self.BASE_RE.search(self.from_)
+ self.base = match.group(1) if match else None
+ self.give_up = False
+
+ if not remove_all_unused_imports:
+ if self.base and _top_module(self.base) not in safe_to_remove:
+ self.give_up = True
+ else:
+ self.remove = _modules_to_remove(self.remove, safe_to_remove)
+
+ if '\\' in previous_line:
+ # Ignore tricky things like "try: \<new line> import" ...
+ self.give_up = True
+
+ self.analyze(line)
+
+ PendingFix.__init__(self, imports)
+
+ def is_over(self, line=None):
+ """Return True if the multiline import statement is over."""
+ line = line or self.accumulator[-1]
+
+ if self.parenthesized:
+ return _valid_char_in_line(')', line)
+
+ return not _valid_char_in_line('\\', line)
+
+ def analyze(self, line):
+ """Decide if the statement will be fixed or left unchanged."""
+ if any(ch in line for ch in ';:#'):
+ self.give_up = True
+
+ def fix(self, accumulated):
+ """Given a collection of accumulated lines, fix the entire import."""
+ old_imports = ''.join(accumulated)
+ ending = get_line_ending(old_imports)
+ # Split imports into segments that contain the module name +
+ # comma + whitespace and eventual <newline> \ ( ) chars
+ segments = [x for x in self.SEGMENT_RE.findall(old_imports) if x]
+ modules = [_segment_module(x) for x in segments]
+ keep = _filter_imports(modules, self.base, self.remove)
+
+ # Short-circuit if no import was discarded
+ if len(keep) == len(segments):
+ return self.from_ + 'import ' + ''.join(accumulated)
+
+ fixed = ''
+ if keep:
+ # Since it is very difficult to deal with all the line breaks and
+ # continuations, let's use the code layout that already exists and
+ # just replace the module identifiers inside the first N-1 segments
+ # + the last segment
+ templates = list(zip(modules, segments))
+ templates = templates[:len(keep)-1] + templates[-1:]
+ # It is important to keep the last segment, since it might contain
+ # important chars like `)`
+ fixed = ''.join(
+ template.replace(module, keep[i])
+ for i, (module, template) in enumerate(templates)
+ )
+
+ # Fix the edge case: inline parenthesis + just one surviving import
+ if self.parenthesized and any(ch not in fixed for ch in '()'):
+ fixed = fixed.strip(string.whitespace + '()') + ending
+
+ # Replace empty imports with a "pass" statement
+ empty = len(fixed.strip(string.whitespace + '\\(),')) < 1
+ if empty:
+ indentation = self.INDENTATION_RE.search(self.from_).group(0)
+ return indentation + 'pass' + ending
+
+ return self.from_ + 'import ' + fixed
+
+ def __call__(self, line=None):
+ """Accumulate all the lines in the import and then trigger the fix."""
+ if line:
+ self.accumulator.append(line)
+ self.analyze(line)
+ if not self.is_over(line):
+ return self
+ if self.give_up:
+ return self.from_ + 'import ' + ''.join(self.accumulator)
+
+ return self.fix(self.accumulator)
+
+
+def _filter_imports(imports, parent=None, unused_module=()):
+ # We compare full module name (``a.module`` not `module`) to
+ # guarantee the exact same module as detected from pyflakes.
+ sep = '' if parent and parent[-1] == '.' else '.'
+
+ def full_name(name):
+ return name if parent is None else parent + sep + name
+
+ return [x for x in imports if full_name(x) not in unused_module]
+
+
def filter_from_import(line, unused_module):
"""Parse and filter ``from something import a, b, c``.
@@ -282,15 +460,8 @@
base_module = re.search(pattern=r'\bfrom\s+([^ ]+)',
string=indentation).group(1)
- # Create an imported module list with base module name
- # ex ``from a import b, c as d`` -> ``['a.b', 'a.c as d']``
- imports = re.split(pattern=r',', string=imports.strip())
- imports = [base_module + '.' + x.strip() for x in imports]
-
- # We compare full module name (``a.module`` not `module`) to
- # guarantee the exact same module as detected from pyflakes.
- filtered_imports = [x.replace(base_module + '.', '')
- for x in imports if x not in unused_module]
+ imports = re.split(pattern=r'\s*,\s*', string=imports.strip())
+ filtered_imports = _filter_imports(imports, base_module, unused_module)
# All of the import in this statement is unused
if not filtered_imports:
@@ -387,26 +558,32 @@
sio = io.StringIO(source)
previous_line = ''
+ result = None
for line_number, line in enumerate(sio.readlines(), start=1):
- if '#' in line:
- yield line
+ if isinstance(result, PendingFix):
+ result = result(line)
+ elif '#' in line:
+ result = line
elif line_number in marked_import_line_numbers:
- yield filter_unused_import(
+ result = filter_unused_import(
line,
unused_module=marked_unused_module[line_number],
remove_all_unused_imports=remove_all_unused_imports,
imports=imports,
previous_line=previous_line)
elif line_number in marked_variable_line_numbers:
- yield filter_unused_variable(line)
+ result = filter_unused_variable(line)
elif line_number in marked_key_line_numbers:
- yield filter_duplicate_key(line, line_messages[line_number],
- line_number, marked_key_line_numbers,
- source)
+ result = filter_duplicate_key(line, line_messages[line_number],
+ line_number, marked_key_line_numbers,
+ source)
elif line_number in marked_star_import_line_numbers:
- yield filter_star_import(line, undefined_names)
+ result = filter_star_import(line, undefined_names)
else:
- yield line
+ result = line
+
+ if not isinstance(result, PendingFix):
+ yield result
previous_line = line
@@ -428,9 +605,16 @@
def filter_unused_import(line, unused_module, remove_all_unused_imports,
imports, previous_line=''):
"""Return line if used, otherwise return None."""
- if multiline_import(line, previous_line):
+ # Ignore doctests.
+ if line.lstrip().startswith('>'):
return line
+ if multiline_import(line, previous_line):
+ filt = FilterMultilineImport(line, unused_module,
+ remove_all_unused_imports,
+ imports, previous_line)
+ return filt()
+
is_from_import = line.lstrip().startswith('from')
if ',' in line and not is_from_import:
@@ -651,12 +835,15 @@
if original_source != filtered_source:
if args.check:
- standard_out.write('Unused imports/variables detected.')
+ standard_out.write(
+ '{filename}: Unused imports/variables detected'.format(
+ filename=filename))
sys.exit(1)
if args.in_place:
with open_with_encoding(filename, mode='w',
encoding=encoding) as output_file:
output_file.write(filtered_source)
+ _LOGGER.info('Fixed %s', filename)
else:
diff = get_diff_text(
io.StringIO(original_source).readlines(),
@@ -666,6 +853,8 @@
else:
if args.check:
standard_out.write('No issues detected!\n')
+ else:
+ _LOGGER.debug('Clean %s: nothing to fix', filename)
def open_with_encoding(filename, encoding, mode='r',
@@ -769,6 +958,7 @@
def match_file(filename, exclude):
"""Return True if file is okay for modifying/recursing."""
if is_exclude_file(filename, exclude):
+ _LOGGER.debug('Skipped %s: matched to exclude pattern', filename)
return False
if not os.path.isdir(filename) and not is_python_file(filename):
@@ -792,6 +982,8 @@
else:
if not is_exclude_file(name, exclude):
yield name
+ else:
+ _LOGGER.debug('Skipped %s: matched to exclude pattern', name)
def _main(argv, standard_out, standard_error):
@@ -832,13 +1024,26 @@
help='remove unused variables')
parser.add_argument('--version', action='version',
version='%(prog)s ' + __version__)
+ parser.add_argument('-v', '--verbose', action='count', dest='verbosity',
+ default=0, help='print more verbose logs (you can '
+ 'repeat `-v` to make it more verbose)')
parser.add_argument('files', nargs='+', help='files to format')
args = parser.parse_args(argv[1:])
+ if standard_error is None:
+ _LOGGER.addHandler(logging.NullHandler())
+ else:
+ _LOGGER.addHandler(logging.StreamHandler(standard_error))
+ loglevels = [logging.WARNING, logging.INFO, logging.DEBUG]
+ try:
+ loglevel = loglevels[args.verbosity]
+ except IndexError: # Too much -v
+ loglevel = loglevels[-1]
+ _LOGGER.setLevel(loglevel)
+
if args.remove_all_unused_imports and args.imports:
- print('Using both --remove-all and --imports is redundant',
- file=standard_error)
+ _LOGGER.error('Using both --remove-all and --imports is redundant')
return 1
if args.exclude:
@@ -852,7 +1057,7 @@
try:
fix_file(name, args=args, standard_out=standard_out)
except IOError as exception:
- print(unicode(exception), file=standard_error)
+ _LOGGER.error(unicode(exception))
failure = True
return 1 if failure else 0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/autoflake-1.3.1/test_autoflake.py
new/autoflake-1.4/test_autoflake.py
--- old/autoflake-1.3.1/test_autoflake.py 2019-06-02 17:53:59.000000000
+0200
+++ new/autoflake-1.4/test_autoflake.py 2020-08-23 03:19:31.000000000 +0200
@@ -6,6 +6,7 @@
from __future__ import unicode_literals
import contextlib
+import functools
import io
import os
import re
@@ -422,13 +423,12 @@
unused_module=['foo.abc', 'foo.subprocess',
'foo.math']))
- def test_filter_code_should_ignore_multiline_imports(self):
+ def test_filter_code_multiline_imports(self):
self.assertEqual(
r"""\
import os
pass
-import os, \
- math, subprocess
+import os
os.foo()
""",
''.join(autoflake.filter_code(r"""\
@@ -439,6 +439,39 @@
os.foo()
""")))
+ def test_filter_code_multiline_from_imports(self):
+ self.assertEqual(
+ r"""\
+import os
+pass
+from os.path import (
+ join,
+)
+join('a', 'b')
+pass
+os.foo()
+from os.path import \
+ isdir
+isdir('42')
+""",
+ ''.join(autoflake.filter_code(r"""\
+import os
+import re
+from os.path import (
+ exists,
+ join,
+)
+join('a', 'b')
+from os.path import \
+ abspath, basename, \
+ commonpath
+os.foo()
+from os.path import \
+ isfile \
+ , isdir
+isdir('42')
+""")))
+
def test_filter_code_should_ignore_semicolons(self):
self.assertEqual(
r"""\
@@ -1624,6 +1657,473 @@
process.communicate()[1].decode())
+class MultilineFromImportTests(unittest.TestCase):
+ def test_is_over(self):
+ filt = autoflake.FilterMultilineImport('from . import (\n')
+ self.assertTrue(filt.is_over('module)\n'))
+ self.assertTrue(filt.is_over(' )\n'))
+ self.assertTrue(filt.is_over(' ) # comment\n'))
+ self.assertTrue(filt.is_over('from module import (a, b)\n'))
+ self.assertFalse(filt.is_over('# )'))
+ self.assertFalse(filt.is_over('module\n'))
+ self.assertFalse(filt.is_over('module, \\\n'))
+ self.assertFalse(filt.is_over('\n'))
+
+ filt = autoflake.FilterMultilineImport('from . import module, \\\n')
+ self.assertTrue(filt.is_over('module\n'))
+ self.assertTrue(filt.is_over('\n'))
+ self.assertTrue(filt.is_over('m1, m2 # comment with \\\n'))
+ self.assertFalse(filt.is_over('m1, m2 \\\n'))
+ self.assertFalse(filt.is_over('m1, m2 \\ #\n'))
+ self.assertFalse(filt.is_over('m1, m2 \\ # comment with \\\n'))
+ self.assertFalse(filt.is_over('\\\n'))
+
+ # "Multiline" imports that are not really multiline
+ filt = autoflake.FilterMultilineImport('import os; '
+ 'import math, subprocess')
+ self.assertTrue(filt.is_over())
+
+ unused = ()
+
+ def assert_fix(self, lines, result, remove_all=True):
+ fixer = autoflake.FilterMultilineImport(
+ lines[0],
+ remove_all_unused_imports=remove_all,
+ unused_module=self.unused
+ )
+ fixed = functools.reduce(lambda acc, x: acc(x), lines[1:], fixer())
+ self.assertEqual(fixed, result)
+
+ def test_fix(self):
+ self.unused = ['third_party.lib' + str(x) for x in (1, 3, 4)]
+
+ # Example m0 (isort)
+ self.assert_fix([
+ 'from third_party import (lib1, lib2, lib3,\n',
+ ' lib4, lib5, lib6)\n'
+ ],
+ 'from third_party import (lib2, lib5, lib6)\n'
+ )
+
+ # Example m1(isort)
+ self.assert_fix([
+ 'from third_party import (lib1,\n',
+ ' lib2,\n',
+ ' lib3,\n',
+ ' lib4,\n',
+ ' lib5,\n',
+ ' lib6)\n'
+ ],
+ 'from third_party import (lib2,\n'
+ ' lib5,\n'
+ ' lib6)\n'
+ )
+
+ # Variation m1(isort)
+ self.assert_fix([
+ 'from third_party import (lib1\n',
+ ' ,lib2\n',
+ ' ,lib3\n',
+ ' ,lib4\n',
+ ' ,lib5\n',
+ ' ,lib6)\n'
+ ],
+ 'from third_party import (lib2\n'
+ ' ,lib5\n'
+ ' ,lib6)\n'
+ )
+
+ # Example m2 (isort)
+ self.assert_fix([
+ 'from third_party import \\\n',
+ ' lib1, lib2, lib3, \\\n',
+ ' lib4, lib5, lib6\n'
+ ],
+ 'from third_party import \\\n'
+ ' lib2, lib5, lib6\n'
+ )
+
+ # Example m3 (isort)
+ self.assert_fix([
+ 'from third_party import (\n',
+ ' lib1,\n',
+ ' lib2,\n',
+ ' lib3,\n',
+ ' lib4,\n',
+ ' lib5\n',
+ ')\n'
+ ],
+ 'from third_party import (\n'
+ ' lib2,\n'
+ ' lib5\n'
+ ')\n'
+ )
+
+ # Example m4 (isort)
+ self.assert_fix([
+ 'from third_party import (\n',
+ ' lib1, lib2, lib3, lib4,\n',
+ ' lib5, lib6)\n'
+ ],
+ 'from third_party import (\n'
+ ' lib2, lib5, lib6)\n'
+ )
+
+ # Example m5 (isort)
+ self.assert_fix([
+ 'from third_party import (\n',
+ ' lib1, lib2, lib3, lib4,\n',
+ ' lib5, lib6\n',
+ ')\n'
+ ],
+ 'from third_party import (\n'
+ ' lib2, lib5, lib6\n'
+ ')\n'
+ )
+
+ # Some Deviations
+ self.assert_fix([
+ 'from third_party import (\n',
+ ' lib1\\\n', # only unused + line continuation
+ ' ,lib2, \n',
+ ' libA\n', # used import with no commas
+ ' ,lib3, \n', # leading and trailing commas with unused import
+ ' libB, \n',
+ ' \\\n', # empty line with continuation
+ ' lib4,\n', # unused import with comment
+ ')\n'
+ ],
+ 'from third_party import (\n'
+ ' lib2\\\n'
+ ' ,libA, \n'
+ ' libB,\n'
+ ')\n',
+ )
+
+ self.assert_fix([
+ 'from third_party import (\n',
+ ' lib1\n',
+ ',\n',
+ ' lib2\n',
+ ',\n',
+ ' lib3\n',
+ ',\n',
+ ' lib4\n',
+ ',\n',
+ ' lib5\n',
+ ')\n'
+ ],
+ 'from third_party import (\n'
+ ' lib2\n'
+ ',\n'
+ ' lib5\n'
+ ')\n'
+ )
+
+ self.assert_fix([
+ 'from third_party import (\n',
+ ' lib1 \\\n',
+ ', \\\n',
+ ' lib2 \\\n',
+ ',\\\n',
+ ' lib3\n',
+ ',\n',
+ ' lib4\n',
+ ',\n',
+ ' lib5 \\\n',
+ ')\n'
+ ],
+ 'from third_party import (\n'
+ ' lib2 \\\n'
+ ', \\\n'
+ ' lib5 \\\n'
+ ')\n'
+ )
+
+ def test_indentation(self):
+ # Some weird indentation examples
+ self.unused = ['third_party.lib' + str(x) for x in (1, 3, 4)]
+ self.assert_fix([
+ ' from third_party import (\n',
+ ' lib1, lib2, lib3, lib4,\n',
+ ' lib5, lib6\n',
+ ')\n'
+ ],
+ ' from third_party import (\n'
+ ' lib2, lib5, lib6\n'
+ ')\n'
+ )
+ self.assert_fix([
+ '\tfrom third_party import \\\n',
+ '\t\tlib1, lib2, lib3, \\\n',
+ '\t\tlib4, lib5, lib6\n'
+ ],
+ '\tfrom third_party import \\\n'
+ '\t\tlib2, lib5, lib6\n'
+ )
+
+ def test_fix_relative(self):
+ # Example m0 (isort)
+ self.unused = ['.lib' + str(x) for x in (1, 3, 4)]
+ self.assert_fix([
+ 'from . import (lib1, lib2, lib3,\n',
+ ' lib4, lib5, lib6)\n'
+ ],
+ 'from . import (lib2, lib5, lib6)\n'
+ )
+
+ # Example m1(isort)
+ self.unused = ['..lib' + str(x) for x in (1, 3, 4)]
+ self.assert_fix([
+ 'from .. import (lib1,\n',
+ ' lib2,\n',
+ ' lib3,\n',
+ ' lib4,\n',
+ ' lib5,\n',
+ ' lib6)\n'
+ ],
+ 'from .. import (lib2,\n'
+ ' lib5,\n'
+ ' lib6)\n'
+ )
+
+ # Example m2 (isort)
+ self.unused = ['...lib' + str(x) for x in (1, 3, 4)]
+ self.assert_fix([
+ 'from ... import \\\n',
+ ' lib1, lib2, lib3, \\\n',
+ ' lib4, lib5, lib6\n'
+ ],
+ 'from ... import \\\n'
+ ' lib2, lib5, lib6\n'
+ )
+
+ # Example m3 (isort)
+ self.unused = ['.parent.lib' + str(x) for x in (1, 3, 4)]
+ self.assert_fix([
+ 'from .parent import (\n',
+ ' lib1,\n',
+ ' lib2,\n',
+ ' lib3,\n',
+ ' lib4,\n',
+ ' lib5\n',
+ ')\n'
+ ],
+ 'from .parent import (\n'
+ ' lib2,\n'
+ ' lib5\n'
+ ')\n'
+ )
+
+ def test_fix_without_from(self):
+ self.unused = ['lib' + str(x) for x in (1, 3, 4)]
+
+ # Multiline but not "from"
+ self.assert_fix([
+ 'import \\\n',
+ ' lib1, lib2, lib3 \\\n',
+ ' ,lib4, lib5, lib6\n'
+ ],
+ 'import \\\n'
+ ' lib2, lib5, lib6\n'
+ )
+ self.assert_fix([
+ 'import lib1, lib2, lib3, \\\n',
+ ' lib4, lib5, lib6\n'
+ ],
+ 'import lib2, lib5, lib6\n'
+ )
+
+ # Problematic example without "from"
+ self.assert_fix([
+ 'import \\\n',
+ ' lib1,\\\n',
+ ' lib2, \\\n',
+ ' libA\\\n', # used import with no commas
+ ' ,lib3, \\\n', # leading and trailing commas with unused
+ ' libB, \\\n',
+ ' \\ \n', # empty line with continuation
+ ' lib4\\\n', # unused import with comment
+ '\n'
+ ],
+ 'import \\\n'
+ ' lib2,\\\n'
+ ' libA, \\\n'
+ ' libB\\\n'
+ '\n'
+ )
+
+ self.unused = ['lib{}.x.y.z'.format(x) for x in (1, 3, 4)]
+ self.assert_fix([
+ 'import \\\n',
+ ' lib1.x.y.z \\',
+ ' , \\\n',
+ ' lib2.x.y.z \\\n',
+ ' , \\\n',
+ ' lib3.x.y.z \\\n',
+ ' , \\\n',
+ ' lib4.x.y.z \\\n',
+ ' , \\\n',
+ ' lib5.x.y.z\n'
+ ],
+ 'import \\\n'
+ ' lib2.x.y.z \\'
+ ' , \\\n'
+ ' lib5.x.y.z\n'
+ )
+
+ def test_give_up(self):
+ # Semicolon
+ self.unused = ['lib' + str(x) for x in (1, 3, 4)]
+ self.assert_fix([
+ 'import \\\n',
+ ' lib1, lib2, lib3, \\\n',
+ ' lib4, lib5; import lib6\n'
+ ],
+ 'import \\\n'
+ ' lib1, lib2, lib3, \\\n'
+ ' lib4, lib5; import lib6\n'
+ )
+ # Comments
+ self.unused = ['.lib' + str(x) for x in (1, 3, 4)]
+ self.assert_fix([
+ 'from . import ( # comment\n',
+ ' lib1,\\\n', # only unused + line continuation
+ ' lib2, \n',
+ ' libA\n', # used import with no commas
+ ' ,lib3, \n', # leading and trailing commas with unused import
+ ' libB, \n',
+ ' \\ \n', # empty line with continuation
+ ' lib4, # noqa \n', # unused import with comment
+ ') ; import sys\n'
+ ],
+ 'from . import ( # comment\n'
+ ' lib1,\\\n'
+ ' lib2, \n'
+ ' libA\n'
+ ' ,lib3, \n'
+ ' libB, \n'
+ ' \\ \n'
+ ' lib4, # noqa \n'
+ ') ; import sys\n'
+ )
+
+ def test_just_one_import_used(self):
+ self.unused = ['lib2']
+ self.assert_fix([
+ 'import \\\n',
+ ' lib1\n'
+ ],
+ 'import \\\n'
+ ' lib1\n'
+ )
+ self.assert_fix([
+ 'import \\\n',
+ ' lib2\n'
+ ],
+ 'pass\n'
+ )
+ # Example from issue #8
+ self.unused = ['re.subn']
+ self.assert_fix([
+ '\tfrom re import (subn)\n',
+ ],
+ '\tpass\n',
+ )
+
+ def test_just_one_import_left(self):
+ # Examples from issue #8
+ self.unused = ['math.sqrt']
+ self.assert_fix([
+ 'from math import (\n',
+ ' sqrt,\n',
+ ' log\n',
+ ' )\n'
+ ],
+ 'from math import (\n'
+ ' log\n'
+ ' )\n'
+ )
+ self.unused = ['module.b']
+ self.assert_fix([
+ 'from module import (a, b)\n',
+ ],
+ 'from module import a\n',
+ )
+ self.assert_fix([
+ 'from module import (a,\n',
+ ' b)\n',
+ ],
+ 'from module import a\n',
+ )
+ self.unused = []
+ self.assert_fix([
+ 'from re import (subn)\n',
+ ],
+ 'from re import (subn)\n',
+ )
+
+ def test_no_empty_imports(self):
+ self.unused = ['lib' + str(x) for x in (1, 3, 4)]
+ self.assert_fix([
+ 'import \\\n',
+ ' lib1, lib3, \\\n',
+ ' lib4 \n'
+ ],
+ 'pass \n'
+ )
+
+ # Indented parenthesized block
+ self.unused = ['.parent.lib' + str(x) for x in (1, 3, 4)]
+ self.assert_fix([
+ '\t\tfrom .parent import (\n',
+ ' lib1,\n',
+ ' lib3,\n',
+ ' lib4,\n',
+ ')\n'
+ ],
+ '\t\tpass\n'
+ )
+
+ def test_without_remove_all(self):
+ self.unused = ['lib' + str(x) for x in (1, 3, 4)]
+ self.assert_fix([
+ 'import \\\n',
+ ' lib1,\\\n',
+ ' lib3,\\\n',
+ ' lib4\n',
+ ],
+ 'import \\\n'
+ ' lib1,\\\n'
+ ' lib3,\\\n'
+ ' lib4\n',
+ remove_all=False
+ )
+
+ self.unused += ['os.path.' + x for x in ('dirname', 'isdir', 'join')]
+ self.assert_fix([
+ 'from os.path import (\n',
+ ' dirname,\n',
+ ' isdir,\n',
+ ' join,\n',
+ ')\n'
+ ],
+ 'pass\n',
+ remove_all=False
+ )
+ self.assert_fix([
+ 'import \\\n',
+ ' os.path.dirname, \\\n',
+ ' lib1, \\\n',
+ ' lib3\n',
+ ],
+ 'import \\\n'
+ ' lib1, \\\n'
+ ' lib3\n',
+ remove_all=False
+ )
+
+
@contextlib.contextmanager
def temporary_file(contents, directory='.', suffix='.py', prefix=''):
"""Write contents to temporary file and yield it."""