Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent from * import * to be splitted #15

Merged
merged 8 commits into from
Apr 19, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@ build/
dist/
htmlcov/
autoflake.egg-info/
/.coverage
/pypi_tmp/
/tmp.*/
5 changes: 3 additions & 2 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ Author
------
- Steven Myint (https://github.com/myint)

Patches
-------
Contributors
------------
- tell-k (https://github.com/tell-k)
- Adhika Setya Pramudita (https://github.com/adhikasp)
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ include test_autoflake.py
exclude .travis.yml
exclude Makefile
exclude test_acid.py
exclude test_acid_pypi.py
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ check:
pylint \
--reports=no \
--disable=invalid-name,no-member,too-few-public-methods,no-else-return \
--disable=redefined-builtin \
--rcfile=/dev/null \
autoflake.py setup.py
pydocstyle autoflake.py setup.py
Expand Down
86 changes: 72 additions & 14 deletions autoflake.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

import ast
import difflib
from collections import defaultdict
import io
import os
import re
Expand Down Expand Up @@ -106,6 +107,17 @@ def unused_import_line_numbers(messages):
yield message.lineno


def unused_import_module_name(messages):
"""Yield line number and module name of unused imports."""
pattern = r'\'(.+?)\''
for message in messages:
if isinstance(message, pyflakes.messages.UnusedImport):
module_name = re.search(pattern, str(message))
module_name = module_name.group()[1:-1]
if module_name:
yield (message.lineno, module_name)


def unused_variable_line_numbers(messages):
"""Yield line numbers of unused variables."""
for message in messages:
Expand Down Expand Up @@ -203,13 +215,48 @@ def multiline_statement(line, previous_line=''):
return True


def filter_from_import(line, unused_module):
"""Parse and filter ``from something import a, b, c``.

Return line without unused import modules, or `pass` if all of the
module in import is unused.

"""
(indentation, imports) = re.split(pattern=r'\bimport\b',
string=line, maxsplit=1)
base_module = re.search(pattern=r'from\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]

# All of the import in this statement is unused
if not filtered_imports:
return get_indentation(line) + 'pass' + get_line_ending(line)

indentation += 'import '

return (
indentation +
', '.join(sorted(filtered_imports)) +
get_line_ending(line))


def break_up_import(line):
"""Return line with imports on separate lines."""
assert '\\' not in line
assert '(' not in line
assert ')' not in line
assert ';' not in line
assert '#' not in line
assert not re.match(line, r'^\s*from\s')

newline = get_line_ending(line)
if not newline:
Expand Down Expand Up @@ -238,6 +285,9 @@ def filter_code(source, additional_imports=None,

marked_import_line_numbers = frozenset(
unused_import_line_numbers(messages))
marked_unused_module = defaultdict(lambda: [])
for line_number, module_name in unused_import_module_name(messages):
marked_unused_module[line_number].append(module_name)

if remove_unused_variables:
marked_variable_line_numbers = frozenset(
Expand All @@ -253,6 +303,7 @@ def filter_code(source, additional_imports=None,
elif line_number in marked_import_line_numbers:
yield 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)
Expand All @@ -264,25 +315,32 @@ def filter_code(source, additional_imports=None,
previous_line = line


def filter_unused_import(line, remove_all_unused_imports, imports,
previous_line=''):
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):
return line
elif ',' in line:

is_from_import = re.match(r'^\s*from\s', line)

if ',' in line and not is_from_import:
return break_up_import(line)

package = extract_package_name(line)
if not remove_all_unused_imports and package not in imports:
return line

if ',' in line:
assert is_from_import
return filter_from_import(line, unused_module)
else:
package = extract_package_name(line)
if not remove_all_unused_imports and package not in imports:
return line
else:
# We need to replace import with "pass" in case the import is the
# only line inside a block. For example,
# "if True:\n import os". In such cases, if the import is
# removed, the block will be left hanging with no body.
return (get_indentation(line) +
'pass' +
get_line_ending(line))
# We need to replace import with "pass" in case the import is the
# only line inside a block. For example,
# "if True:\n import os". In such cases, if the import is
# removed, the block will be left hanging with no body.
return (get_indentation(line) +
'pass' +
get_line_ending(line))


def filter_unused_variable(line, previous_line=''):
Expand Down
4 changes: 2 additions & 2 deletions test_acid.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def run(filename, command, verbose=False, options=None):
try:
check_syntax(temp_filename, raise_error=True)
except (SyntaxError, TypeError,
UnicodeDecodeError) as exception:
UnicodeDecodeError, ValueError) as exception:
sys.stderr.write('autoflake broke ' + filename + '\n' +
str(exception) + '\n')
return False
Expand Down Expand Up @@ -121,7 +121,7 @@ def check_syntax(filename, raise_error=False):
try:
compile(input_file.read(), '<string>', 'exec', dont_inherit=True)
return True
except (SyntaxError, TypeError, UnicodeDecodeError):
except (SyntaxError, TypeError, UnicodeDecodeError, ValueError):
if raise_error:
raise
else:
Expand Down
142 changes: 142 additions & 0 deletions test_acid_pypi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
#!/usr/bin/env python

"""Run acid test against latest packages on PyPI."""

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import os
import subprocess
import sys
import tarfile
import zipfile

import test_acid


TMP_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)),
'pypi_tmp')


def latest_packages(last_hours):
"""Return names of latest released packages on PyPI."""
process = subprocess.Popen(
['yolk', '--latest-releases={hours}'.format(hours=last_hours)],
stdout=subprocess.PIPE)

for line in process.communicate()[0].decode('utf-8').split('\n'):
if line:
yield line.split()[0]


def download_package(name, output_directory):
"""Download package to output_directory.

Raise CalledProcessError on failure.

"""
subprocess.check_call(['yolk', '--fetch-package={name}'.format(name=name)],
cwd=output_directory)


def extract_package(path, output_directory):
"""Extract package at path."""
if path.lower().endswith('.tar.gz'):
try:
tar = tarfile.open(path)
tar.extractall(path=output_directory)
tar.close()
return True
except (tarfile.ReadError, IOError):
return False
elif path.lower().endswith('.zip'):
try:
archive = zipfile.ZipFile(path)
archive.extractall(path=output_directory)
archive.close()
except (zipfile.BadZipfile, IOError):
return False
return True

return False


def main():
"""Run main."""
try:
os.mkdir(TMP_DIR)
except OSError:
pass

args = test_acid.process_args()
if args.files:
# Copy
names = list(args.files)
else:
names = None

checked_packages = []
skipped_packages = []
last_hours = 1
while True:
if args.files:
if not names:
break
else:
while not names:
# Continually populate if user did not specify a package
# explicitly.
names = [p for p in latest_packages(last_hours)
if p not in checked_packages and
p not in skipped_packages]

if not names:
last_hours *= 2

package_name = names.pop(0)
print(package_name, file=sys.stderr)

package_tmp_dir = os.path.join(TMP_DIR, package_name)
try:
os.mkdir(package_tmp_dir)
except OSError:
print('Skipping already checked package', file=sys.stderr)
skipped_packages.append(package_name)
continue

try:
download_package(
package_name,
output_directory=package_tmp_dir)
except subprocess.CalledProcessError:
print('yolk fetch failed', file=sys.stderr)
continue

for download_name in os.listdir(package_tmp_dir):
try:
if not extract_package(
os.path.join(package_tmp_dir, download_name),
output_directory=package_tmp_dir):
print('Could not extract package', file=sys.stderr)
continue
except UnicodeDecodeError:
print('Could not extract package', file=sys.stderr)
continue

args.files = [package_tmp_dir]
if test_acid.check(args):
checked_packages.append(package_name)
else:
return 1

if checked_packages:
print('\nTested packages:\n ' + '\n '.join(checked_packages),
file=sys.stderr)


if __name__ == '__main__':
try:
sys.exit(main())
except KeyboardInterrupt:
sys.exit(1)
Loading