From 95cc86987333858409ad4840971b8ce1bee82be6 Mon Sep 17 00:00:00 2001 From: Miroslav Shubernetskiy Date: Mon, 1 May 2017 23:38:37 -0400 Subject: [PATCH 01/11] added ci option, input can be piped, main -> __main__ also fixed some edge cases in _safe_import_module tests pending for main script changes --- .importanizerc | 3 + .travis.yml | 2 +- importanize/__init__.py | 2 +- importanize/{main.py => __main__.py} | 100 +++++++++++++++++++++++---- importanize/groups.py | 5 +- importanize/parser.py | 8 +-- importanize/utils.py | 29 +++++++- setup.py | 2 +- tests/test_groups.py | 2 +- tests/test_main.py | 4 +- tests/test_parser.py | 16 ++--- tests/test_utils.py | 2 +- tox.ini | 2 +- 13 files changed, 134 insertions(+), 43 deletions(-) rename importanize/{main.py => __main__.py} (76%) diff --git a/.importanizerc b/.importanizerc index 70c7925..7aa8cba 100644 --- a/.importanizerc +++ b/.importanizerc @@ -6,6 +6,9 @@ { "type": "stdlib" }, + { + "type": "sitepackages" + }, { "type": "remainder" }, diff --git a/.travis.yml b/.travis.yml index bf3b0ee..e181da4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ language: python python: - - "3.4" + - "3.6" - "2.7" - "pypy" diff --git a/importanize/__init__.py b/importanize/__init__.py index 6110a41..9cf9f41 100755 --- a/importanize/__init__.py +++ b/importanize/__init__.py @@ -4,7 +4,7 @@ __author__ = 'Miroslav Shubernetskiy' __email__ = 'miroslav@miki725.com' -__version__ = '0.4.1' +__version__ = '0.5' __description__ = ( 'Utility for organizing Python imports using PEP8 or custom rules' ) diff --git a/importanize/main.py b/importanize/__main__.py similarity index 76% rename from importanize/main.py rename to importanize/__main__.py index 4c3d9a1..1a224f0 100644 --- a/importanize/main.py +++ b/importanize/__main__.py @@ -8,6 +8,7 @@ import os import sys from fnmatch import fnmatch +from stat import S_ISFIFO import six @@ -16,10 +17,10 @@ from .groups import ImportGroups from .parser import ( find_imports_from_lines, - get_file_artifacts, + get_text_artifacts, parse_statements, ) -from .utils import read +from .utils import force_text, read LOGGING_FORMAT = '%(levelname)s %(name)s %(message)s' @@ -88,7 +89,6 @@ def find_config(): 'path', type=six.text_type, nargs='*', - default=['.'], help='Path either to a file or directory where ' 'all Python files imports will be organized.', ) @@ -118,6 +118,13 @@ def find_config(): help='If provided, instead of changing files, modified ' 'files will be printed to stdout.' ) +parser.add_argument( + '--ci', + action='store_true', + default=False, + help='When used CI mode will check if file contains expected ' + 'imports as per importanize configuration.' +) parser.add_argument( '--version', action='store_true', @@ -133,14 +140,12 @@ def find_config(): ) -def run_importanize(path, config, args): - if config.get('exclude'): - if any(map(lambda i: fnmatch(path, i), config.get('exclude'))): - log.info('Skipping {}'.format(path)) - return False +class CIFailure(Exception): + pass - text = read(path) - file_artifacts = get_file_artifacts(path) + +def run_importanize_on_text(text, config, args): + file_artifacts = get_text_artifacts(text) # Get formatter from args or config formatter = FORMATTERS.get(args.formatter or config.get('formatter'), @@ -186,6 +191,26 @@ def run_importanize(path, config, args): lines = file_artifacts.get('sep', '\n').join(lines) + if args.ci and text != lines: + raise CIFailure() + + return lines + + +def run_importanize(path, config, args): + if config.get('exclude'): + if any(map(lambda i: fnmatch(path, i), config.get('exclude'))): + log.info('Skipping {}'.format(path)) + return + + text = read(path) + + lines = run_importanize_on_text(text, config, args) + + if text == lines: + log.info('Nothing to do in {}'.format(path)) + return + if args.print: print(lines.encode('utf-8') if not six.PY3 else lines) else: @@ -193,19 +218,23 @@ def run_importanize(path, config, args): fid.write(lines.encode('utf-8')) log.info('Successfully importanized {}'.format(path)) - return True def run(path, config, args): if not os.path.isdir(path): try: run_importanize(path, config, args) + except CIFailure: + print('Imports not organized in {}'.format(path), file=sys.stderr) + raise except Exception as e: log.exception('Error running importanize for {}' ''.format(path)) parser.error(six.text_type(e)) else: + all_successes = True + for dirpath, dirnames, filenames in os.walk(path): python_files = filter( operator.methodcaller('endswith', '.py'), @@ -219,11 +248,18 @@ def run(path, config, args): print('-' * len(path)) try: run_importanize(path, config, args) + except CIFailure: + print('Imports not organized in {}'.format(path), + file=sys.stderr) + all_successes = False except Exception as e: log.exception('Error running importanize for {}' ''.format(path)) parser.error(six.text_type(e)) + if not all_successes: + raise CIFailure() + def main(): args = parser.parse_args() @@ -244,13 +280,47 @@ def main(): 'source: https://github.com/miki725/importanize' ) print(msg.format(__description__, __version__, sys.executable)) - sys.exit(0) + return 0 if args.config is None: config = PEP8_CONFIG else: config = json.loads(read(args.config)) - for p in args.path: - path = os.path.abspath(p) - run(path, config, args) + if S_ISFIFO(os.fstat(0).st_mode): + if args.path: + parser.error('Cant supply any paths when piping input') + return 1 + + text = force_text(sys.stdin.read()) + + try: + lines = run_importanize_on_text(text, config, args) + except CIFailure: + print('Imports not organized', file=sys.stderr) + return 1 + except Exception as e: + log.exception('Error running importanize') + parser.error(six.text_type(e)) + return 1 + + sys.stdout.write(lines) + sys.stdout.flush() + + else: + all_successes = True + + for p in (args.path or ['.']): + path = os.path.abspath(p) + try: + run(path, config, args) + except CIFailure: + all_successes = False + + return int(not all_successes) + + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/importanize/groups.py b/importanize/groups.py index 2cdc54b..0adaf65 100644 --- a/importanize/groups.py +++ b/importanize/groups.py @@ -8,7 +8,7 @@ import six from .formatters import DEFAULT_FORMATTER -from .utils import is_std_lib, is_site_package +from .utils import is_site_package, is_std_lib @six.python_2_unicode_compatible @@ -121,6 +121,7 @@ class RemainderGroup(BaseImportGroup): def should_add_statement(self, statement): return True + # -- RemainderGroup goes last and catches everything left over GROUP_MAPPING = OrderedDict(( ('stdlib', StdLibGroup), @@ -150,7 +151,7 @@ def add_group(self, config): raise ValueError(msg) if config['type'] not in GROUP_MAPPING: - msg = ('"{}" is not supported import group') + msg = ('"{}" is not supported import group'.format(config['type'])) raise ValueError(msg) self.groups.append(GROUP_MAPPING[config['type']](config)) diff --git a/importanize/parser.py b/importanize/parser.py index 881db0a..37da3e8 100644 --- a/importanize/parser.py +++ b/importanize/parser.py @@ -5,7 +5,7 @@ import six from .statements import DOTS, ImportLeaf, ImportStatement -from .utils import list_split, read +from .utils import list_split STATEMENT_COMMENTS = ('noqa',) @@ -36,14 +36,14 @@ def normalized(self): return self[1:] -def get_file_artifacts(path): +def get_text_artifacts(text): """ Get artifacts for the given file. Parameters ---------- path : str - Path to a file + File content to analyze Returns ------- @@ -55,7 +55,7 @@ def get_file_artifacts(path): 'sep': '\n', } - lines = read(path).splitlines(True) + lines = text.splitlines(True) if len(lines) > 1 and lines[0][-2:] == '\r\n': artifacts['sep'] = '\r\n' diff --git a/importanize/utils.py b/importanize/utils.py index 361b071..1050d56 100644 --- a/importanize/utils.py +++ b/importanize/utils.py @@ -29,14 +29,23 @@ def ignore_site_packages_paths(): def _safe_import_module(module_name): - imported_module = sys.modules.pop(module_name, None) + # remove module and submodules + # removing submodules is necessary in cases when module + # imports an attribute from submodule + # if parent module is removed from sys.modules + # but not removing submodule will result in AttributeError + # when attempting to re-import parent module again + imported_modules = { + k: sys.modules.pop(k) + for k in list(sys.modules.keys()) + if k == module_name or k.startswith(module_name + '.') + } try: return import_module(module_name) except ImportError: return None finally: - if imported_module: - sys.modules[module_name] = imported_module + sys.modules.update(imported_modules) def is_std_lib(module_name): @@ -85,3 +94,17 @@ def list_split(iterable, split): if segment: yield segment + + +def force_text(data): + try: + return data.decode('utf-8') + except AttributeError: + return data + + +def force_bytes(data): + try: + return data.encode('utf-8') + except AttributeError: + return data diff --git a/setup.py b/setup.py index a59aacd..a17987d 100755 --- a/setup.py +++ b/setup.py @@ -41,7 +41,7 @@ def read(fname): tests_require=test_requirements, entry_points={ 'console_scripts': [ - 'importanize = importanize.main:main', + 'importanize = importanize.__main__:main', ] }, keywords=' '.join([ diff --git a/tests/test_groups.py b/tests/test_groups.py index 2cad360..9436b04 100644 --- a/tests/test_groups.py +++ b/tests/test_groups.py @@ -11,8 +11,8 @@ LocalGroup, PackagesGroup, RemainderGroup, - StdLibGroup, SitePackagesGroup, + StdLibGroup, ) from importanize.statements import ImportLeaf, ImportStatement diff --git a/tests/test_main.py b/tests/test_main.py index daa70e3..0be84b7 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -7,7 +7,7 @@ import mock import six -from importanize.main import ( +from importanize.__main__ import ( PEP8_CONFIG, find_config, main, @@ -18,7 +18,7 @@ from importanize.utils import read -TESTING_MODULE = 'importanize.main' +TESTING_MODULE = 'importanize.__main__' class TestMain(unittest.TestCase): diff --git a/tests/test_parser.py b/tests/test_parser.py index e2cc0c2..001742e 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -2,13 +2,12 @@ from __future__ import print_function, unicode_literals import unittest -import mock import six from importanize.parser import ( Token, find_imports_from_lines, - get_file_artifacts, + get_text_artifacts, parse_statements, tokenize_import_lines, ) @@ -30,23 +29,18 @@ def test_normalized(self): class TestParsing(unittest.TestCase): - @mock.patch(TESTING_MODULE + '.read') - def test_get_file_artifacts(self, mock_read): - mock_read.return_value = 'Hello\nWorld\n' - actual = get_file_artifacts(mock.sentinel.path) + def test_get_text_artifacts(self): + actual = get_text_artifacts('Hello\nWorld\n') self.assertDictEqual(actual, { 'sep': '\n', }) - mock_read.assert_called_once_with(mock.sentinel.path) - mock_read.return_value = 'Hello\r\nWorld\n' - actual = get_file_artifacts(mock.sentinel.path) + actual = get_text_artifacts('Hello\r\nWorld\n') self.assertDictEqual(actual, { 'sep': '\r\n', }) - mock_read.return_value = 'Hello' - actual = get_file_artifacts(mock.sentinel.path) + actual = get_text_artifacts('Hello') self.assertDictEqual(actual, { 'sep': '\n', }) diff --git a/tests/test_utils.py b/tests/test_utils.py index f2aff26..b167963 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -8,8 +8,8 @@ from importanize.utils import ( ignore_site_packages_paths, - is_std_lib, is_site_package, + is_std_lib, list_strip, read, ) diff --git a/tox.ini b/tox.ini index 5ab0192..412bbee 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27, py34, pypy +envlist = py27, py36, pypy [testenv] setenv = From e0338dae1aee5d3f9035e9d9560579a6703a62d7 Mon Sep 17 00:00:00 2001 From: Miroslav Shubernetskiy Date: Tue, 2 May 2017 14:39:31 -0400 Subject: [PATCH 02/11] refactored main script to be a bit simpler. also switched to pathlib --- importanize/__main__.py | 158 ++++++++++++++++++++-------------------- requirements-dev.txt | 1 + requirements.txt | 1 + 3 files changed, 83 insertions(+), 77 deletions(-) diff --git a/importanize/__main__.py b/importanize/__main__.py index 1a224f0..2236bd5 100644 --- a/importanize/__main__.py +++ b/importanize/__main__.py @@ -4,8 +4,8 @@ import inspect import json import logging -import operator import os +import pathlib import sys from fnmatch import fnmatch from stat import S_ISFIFO @@ -118,6 +118,15 @@ def find_config(): help='If provided, instead of changing files, modified ' 'files will be printed to stdout.' ) +parser.add_argument( + '--no-header', + action='store_false', + default=True, + dest='header', + help='If provided, when printing files will not print header ' + 'before each file. ' + 'Useful to leave when multiple files are importanized.' +) parser.add_argument( '--ci', action='store_true', @@ -197,65 +206,66 @@ def run_importanize_on_text(text, config, args): return lines -def run_importanize(path, config, args): - if config.get('exclude'): - if any(map(lambda i: fnmatch(path, i), config.get('exclude'))): - log.info('Skipping {}'.format(path)) - return - - text = read(path) - - lines = run_importanize_on_text(text, config, args) - - if text == lines: - log.info('Nothing to do in {}'.format(path)) - return - - if args.print: - print(lines.encode('utf-8') if not six.PY3 else lines) - else: - with open(path, 'wb') as fid: - fid.write(lines.encode('utf-8')) - - log.info('Successfully importanized {}'.format(path)) - - -def run(path, config, args): - if not os.path.isdir(path): +def run(source, config, args, path=None): + if isinstance(source, six.string_types): try: - run_importanize(path, config, args) + organized = run_importanize_on_text(source, config, args) + except CIFailure: - print('Imports not organized in {}'.format(path), file=sys.stderr) + msg = 'Imports not organized' + if path: + msg += ' in {}'.format(path) + print(msg, file=sys.stderr) raise - except Exception as e: - log.exception('Error running importanize for {}' - ''.format(path)) - parser.error(six.text_type(e)) - else: - all_successes = True + else: + if args.print and args.header and path: + print('=' * len(six.text_type(path))) + print(path) + print('-' * len(six.text_type(path))) + + if args.print: + print(organized.encode('utf-8') if not six.PY3 else organized) + + else: + if source == organized: + msg = 'Nothing to do' + if path: + msg += ' in {}'.format(path) + log.info(msg) + + else: + path.write_text(organized) + + msg = 'Successfully importanized' + if path: + msg += ' {}'.format(path) + log.info(msg) + + return organized + + elif source.is_file(): + if config.get('exclude'): + if any(map(lambda i: fnmatch(six.text_type(source), i), + config.get('exclude'))): + log.info('Skipping {}'.format(source)) + return + + text = source.read_text('utf-8') + return run(text, config, args, source) + + elif source.is_dir(): + files = ( + f for f in source.iterdir() + if not f.is_file() or f.is_file() and f.suffixes == ['.py'] + ) - for dirpath, dirnames, filenames in os.walk(path): - python_files = filter( - operator.methodcaller('endswith', '.py'), - filenames - ) - for file in python_files: - path = os.path.join(dirpath, file) - if args.print: - print('=' * len(path)) - print(path) - print('-' * len(path)) - try: - run_importanize(path, config, args) - except CIFailure: - print('Imports not organized in {}'.format(path), - file=sys.stderr) - all_successes = False - except Exception as e: - log.exception('Error running importanize for {}' - ''.format(path)) - parser.error(six.text_type(e)) + all_successes = True + for f in files: + try: + run(f, config, args, f) + except CIFailure: + all_successes = False if not all_successes: raise CIFailure() @@ -287,39 +297,33 @@ def main(): else: config = json.loads(read(args.config)) + to_importanize = [pathlib.Path(i) for i in (args.path or ['.'])] + if S_ISFIFO(os.fstat(0).st_mode): if args.path: parser.error('Cant supply any paths when piping input') return 1 - text = force_text(sys.stdin.read()) + to_importanize = [force_text(sys.stdin.read())] + args.print = True + args.header = False + if args.ci: + args.print = False + args.header = False + + all_successes = True + + for p in to_importanize: try: - lines = run_importanize_on_text(text, config, args) + run(p, config, args) except CIFailure: - print('Imports not organized', file=sys.stderr) - return 1 - except Exception as e: + all_successes = False + except Exception: log.exception('Error running importanize') - parser.error(six.text_type(e)) return 1 - sys.stdout.write(lines) - sys.stdout.flush() - - else: - all_successes = True - - for p in (args.path or ['.']): - path = os.path.abspath(p) - try: - run(path, config, args) - except CIFailure: - all_successes = False - - return int(not all_successes) - - return 0 + return int(not all_successes) if __name__ == '__main__': diff --git a/requirements-dev.txt b/requirements-dev.txt index 7bd7749..59eae6e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,5 +3,6 @@ coverage flake8 mock nose +pdbpp rednose tox diff --git a/requirements.txt b/requirements.txt index e323a45..c7de579 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ six>=1.9 +pathlib2 From 9ff7b136ba87749ec50c713c60ec76624ecf2ffe Mon Sep 17 00:00:00 2001 From: Miroslav Shubernetskiy Date: Wed, 3 May 2017 13:07:12 -0400 Subject: [PATCH 03/11] updated tests for main. using imp.find_module() vs import_module --- .importanizerc | 1 + Makefile | 3 +- importanize/__main__.py | 78 +-- importanize/statements.py | 16 +- importanize/utils.py | 55 +- tests/test_data/config.json | 16 + tests/test_data/{input.txt => input.py} | 1 + .../{output_grouped.txt => output_grouped.py} | 1 + ...e_grouped.txt => output_inline_grouped.py} | 1 + tests/test_main.py | 504 ++++++++---------- tests/test_statements.py | 32 +- tests/test_utils.py | 46 +- 12 files changed, 364 insertions(+), 390 deletions(-) create mode 100644 tests/test_data/config.json rename tests/test_data/{input.txt => input.py} (94%) rename tests/test_data/{output_grouped.txt => output_grouped.py} (94%) rename tests/test_data/{output_inline_grouped.txt => output_inline_grouped.py} (96%) diff --git a/.importanizerc b/.importanizerc index 7aa8cba..b7ad405 100644 --- a/.importanizerc +++ b/.importanizerc @@ -1,5 +1,6 @@ { "exclude": [ + "*/.tox/*", "*/test_data/*.py" ], "groups": [ diff --git a/Makefile b/Makefile index ed4a366..c16faf0 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: clean-pyc clean-build docs clean -NOSE_FLAGS=-sv --with-doctest --rednose +NOSE_FLAGS=-sv --with-doctest --rednose --exclude=test_data COVER_CONFIG_FLAGS=--with-coverage --cover-package=importanize,tests --cover-tests --cover-erase COVER_REPORT_FLAGS=--cover-html --cover-html-dir=htmlcov COVER_FLAGS=${COVER_CONFIG_FLAGS} ${COVER_REPORT_FLAGS} @@ -45,6 +45,7 @@ clean-test-all: clean-test lint: flake8 importanize tests + python -m importanize tests/ importanize/ tests/ --ci test: nosetests ${NOSE_FLAGS} tests/ diff --git a/importanize/__main__.py b/importanize/__main__.py index 2236bd5..22a8e29 100644 --- a/importanize/__main__.py +++ b/importanize/__main__.py @@ -5,11 +5,11 @@ import json import logging import os -import pathlib import sys from fnmatch import fnmatch from stat import S_ISFIFO +import pathlib2 as pathlib import six from . import __description__, __version__, formatters @@ -63,24 +63,19 @@ def find_config(): - path = os.getcwd() + path = pathlib.Path.cwd() default_config = None - found_default = '' - - while path != os.sep: - config_path = os.path.join(path, IMPORTANIZE_CONFIG) - if os.path.exists(config_path): - default_config = config_path - found_default = (' Found configuration file at {}' - ''.format(default_config)) + + while path != pathlib.Path(path.root): + config_path = path / IMPORTANIZE_CONFIG + if config_path.exists(): + default_config = six.text_type(config_path) break else: - path = os.path.dirname(path) - - return default_config, found_default + path = path.parent + return default_config -default_config, found_default = find_config() parser = argparse.ArgumentParser( description=__description__, @@ -94,15 +89,13 @@ def find_config(): ) parser.add_argument( '-c', '--config', - type=six.text_type, - default=default_config, + type=argparse.FileType('rb'), help='Path to importanize config json file. ' 'If "{}" is present in either current folder ' 'or any parent folder, that config ' 'will be used. Otherwise crude default pep8 ' - 'config will be used.{}' - ''.format(IMPORTANIZE_CONFIG, - found_default), + 'config will be used.' + ''.format(IMPORTANIZE_CONFIG), ) parser.add_argument( '-f', '--formatter', @@ -208,6 +201,11 @@ def run_importanize_on_text(text, config, args): def run(source, config, args, path=None): if isinstance(source, six.string_types): + msg = 'About to importanize' + if path: + msg += ' {}'.format(path) + log.debug(msg) + try: organized = run_importanize_on_text(source, config, args) @@ -221,7 +219,7 @@ def run(source, config, args, path=None): else: if args.print and args.header and path: print('=' * len(six.text_type(path))) - print(path) + print(six.text_type(path)) print('-' * len(six.text_type(path))) if args.print: @@ -237,10 +235,10 @@ def run(source, config, args, path=None): else: path.write_text(organized) - msg = 'Successfully importanized' - if path: - msg += ' {}'.format(path) - log.info(msg) + msg = 'Successfully importanized' + if path: + msg += ' {}'.format(path) + log.info(msg) return organized @@ -271,8 +269,9 @@ def run(source, config, args, path=None): raise CIFailure() -def main(): - args = parser.parse_args() +def main(args=None): + args = args if args is not None else sys.argv[1:] + args = parser.parse_args(args=args) # adjust logging level (logging.getLogger('') @@ -280,29 +279,33 @@ def main(): log.debug('Running importanize with {}'.format(args)) + config_path = getattr(args.config, 'name', '') or find_config() + if args.version: msg = ( 'importanize\n' '===========\n' - '{}\n\n' - 'version: {}\n' - 'python: {}\n' + '{description}\n\n' + 'version: {version}\n' + 'python: {python}\n' + 'config: {config}\n' 'source: https://github.com/miki725/importanize' ) - print(msg.format(__description__, __version__, sys.executable)) + print(msg.format( + description=__description__, + version=__version__, + python=sys.executable, + config=config_path or '', + )) return 0 - if args.config is None: - config = PEP8_CONFIG - else: - config = json.loads(read(args.config)) + config = json.loads(read(config_path)) if config_path else PEP8_CONFIG to_importanize = [pathlib.Path(i) for i in (args.path or ['.'])] if S_ISFIFO(os.fstat(0).st_mode): if args.path: - parser.error('Cant supply any paths when piping input') - return 1 + return parser.error('Cant supply any paths when piping input') to_importanize = [force_text(sys.stdin.read())] args.print = True @@ -326,5 +329,4 @@ def main(): return int(not all_successes) -if __name__ == '__main__': - sys.exit(main()) +sys.exit(main()) if __name__ == '__main__' else None diff --git a/importanize/statements.py b/importanize/statements.py index 8691517..68b0fc8 100755 --- a/importanize/statements.py +++ b/importanize/statements.py @@ -104,8 +104,19 @@ class ImportStatement(ComparatorMixin): def __init__(self, line_numbers, stem, leafs=None, comments=None, **kwargs): + as_name = None + + if ' as ' in stem: + stem, as_name = list_strip(stem.split(' as ')) + if leafs: + as_name = None + + if stem == as_name: + as_name = None + self.line_numbers = line_numbers self.stem = stem + self.as_name = as_name self.leafs = leafs or [] self.comments = comments or [] self.file_artifacts = kwargs.get('file_artifacts', {}) @@ -126,7 +137,10 @@ def root_module(self): def as_string(self): if not self.leafs: - return 'import {}'.format(self.stem) + data = 'import {}'.format(self.stem) + if self.as_name: + data += ' as {}'.format(self.as_name) + return data else: return ( 'from {} import {}' diff --git a/importanize/utils.py b/importanize/utils.py index 1050d56..991eb4b 100644 --- a/importanize/utils.py +++ b/importanize/utils.py @@ -1,51 +1,17 @@ # -*- coding: utf-8 -*- from __future__ import print_function, unicode_literals +import imp import operator -import os import sys -from contextlib import contextmanager -from importlib import import_module -@contextmanager -def ignore_site_packages_paths(): - paths = sys.path[:] +def _get_module_path(module_name): try: - # remove working directory so that all - # local imports fail - if os.getcwd() in sys.path: - sys.path.remove(os.getcwd()) - # remove all third-party paths - # so that only stdlib imports will succeed - sys.path = list(set(filter( - None, - filter(lambda i: all(('site-packages' not in i, - 'python' in i or 'pypy' in i)), - map(operator.methodcaller('lower'), sys.path)) - ))) - yield - finally: - sys.path = paths - - -def _safe_import_module(module_name): - # remove module and submodules - # removing submodules is necessary in cases when module - # imports an attribute from submodule - # if parent module is removed from sys.modules - # but not removing submodule will result in AttributeError - # when attempting to re-import parent module again - imported_modules = { - k: sys.modules.pop(k) - for k in list(sys.modules.keys()) - if k == module_name or k.startswith(module_name + '.') - } - try: - return import_module(module_name) + # TODO deprecated in Py3. + # TODO Find better way for py2 and py3 compatibility. + return imp.find_module(module_name)[1] except ImportError: - return None - finally: - sys.modules.update(imported_modules) + return '' def is_std_lib(module_name): @@ -55,16 +21,17 @@ def is_std_lib(module_name): if module_name in sys.builtin_module_names: return True - with ignore_site_packages_paths(): - return bool(_safe_import_module(module_name)) + module_path = _get_module_path(module_name) + if 'site-packages' in module_path: + return False + return 'python' in module_path or 'pypy' in module_path def is_site_package(module_name): if not module_name: return False - module = _safe_import_module(module_name) - module_path = getattr(module, "__file__", "") + module_path = _get_module_path(module_name) if "site-packages" not in module_path: return False return "python" in module_path or "pypy" in module_path diff --git a/tests/test_data/config.json b/tests/test_data/config.json new file mode 100644 index 0000000..fa465d7 --- /dev/null +++ b/tests/test_data/config.json @@ -0,0 +1,16 @@ +{ + "groups": [ + { + "type": "stdlib" + }, + { + "type": "sitepackages" + }, + { + "type": "remainder" + }, + { + "type": "local" + } + ] +} diff --git a/tests/test_data/input.txt b/tests/test_data/input.py similarity index 94% rename from tests/test_data/input.txt rename to tests/test_data/input.py index 8b9cff3..4cf3b95 100644 --- a/tests/test_data/input.txt +++ b/tests/test_data/input.py @@ -10,6 +10,7 @@ from ..othermodule import rainbows from a import b from a.b import c +import flake8 as lint # in site-package from a.b import d import z diff --git a/tests/test_data/output_grouped.txt b/tests/test_data/output_grouped.py similarity index 94% rename from tests/test_data/output_grouped.txt rename to tests/test_data/output_grouped.py index 6be7f89..8176c14 100644 --- a/tests/test_data/output_grouped.txt +++ b/tests/test_data/output_grouped.py @@ -5,6 +5,7 @@ from os import path as ospath import coverage # in site-packages +import flake8 as lint # in site-package import something # with comment import z diff --git a/tests/test_data/output_inline_grouped.txt b/tests/test_data/output_inline_grouped.py similarity index 96% rename from tests/test_data/output_inline_grouped.txt rename to tests/test_data/output_inline_grouped.py index 4f38d79..3908cea 100644 --- a/tests/test_data/output_inline_grouped.txt +++ b/tests/test_data/output_inline_grouped.py @@ -5,6 +5,7 @@ from os import path as ospath import coverage # in site-packages +import flake8 as lint # in site-package import something # with comment import z diff --git a/tests/test_main.py b/tests/test_main.py index 0be84b7..e26e1ff 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,327 +1,285 @@ # -*- coding: utf-8 -*- from __future__ import print_function, unicode_literals -import logging -import os +import sys import unittest +from copy import deepcopy import mock import six +from pathlib2 import Path +from importanize import __version__ from importanize.__main__ import ( + IMPORTANIZE_CONFIG, PEP8_CONFIG, + CIFailure, find_config, main, - parser, run, - run_importanize, + run_importanize_on_text, ) -from importanize.utils import read TESTING_MODULE = 'importanize.__main__' class TestMain(unittest.TestCase): - @mock.patch(TESTING_MODULE + '.read') - def test_run_importanize_skip(self, mock_read): - conf = { - 'exclude': ['*foo.py'], - } - self.assertFalse( - run_importanize('/path/to/importanize/file/foo.py', conf, None) + test_data = Path(__file__).parent / 'test_data' + + input_text = (test_data / 'input.py').read_text() + output_grouped = (test_data / 'output_grouped.py').read_text() + output_inline_grouped = ( + test_data / 'output_inline_grouped.py' + ).read_text() + + def test_run_importanize_on_text_grouped(self): + actual = run_importanize_on_text( + self.input_text, + PEP8_CONFIG, + mock.Mock(formatter='grouped', + ci=False), ) - self.assertFalse(mock_read.called) - @mock.patch(TESTING_MODULE + '.print', create=True) - def test_run_importanize_print(self, mock_print): - test_file = os.path.join(os.path.dirname(__file__), - 'test_data', - 'input.txt') - expected_file = os.path.join(os.path.dirname(__file__), - 'test_data', - 'output_grouped.txt') - expected = ( - read(expected_file) - if six.PY3 - else read(expected_file).encode('utf-8') + self.assertEqual(actual, self.output_grouped) + + def test_run_importanize_on_text_inline_grouped(self): + actual = run_importanize_on_text( + self.input_text, + PEP8_CONFIG, + mock.Mock(formatter='inline-grouped', + ci=False), ) - self.assertTrue( - run_importanize( - test_file, + self.assertEqual(actual, self.output_inline_grouped) + + def test_run_importanize_on_text_ci_failed(self): + with self.assertRaises(CIFailure): + run_importanize_on_text( + self.input_text, PEP8_CONFIG, - mock.MagicMock(print=True, - formatter='grouped')) + mock.Mock(formatter='grouped', + ci=True), + ) + + def test_run_importanize_on_text_ci_passed(self): + actual = run_importanize_on_text( + self.output_grouped, + PEP8_CONFIG, + mock.Mock(formatter='grouped', + ci=True), ) - # self.assertMultiLineEqual(mock_print.call_args[0][0], expected) - mock_print.assert_called_once_with(expected) - - @mock.patch(TESTING_MODULE + '.print', create=True) - def test_run_importanize_print_inline_group_formatter(self, mock_print): - test_file = os.path.join(os.path.dirname(__file__), - 'test_data', - 'input.txt') - expected_file = os.path.join(os.path.dirname(__file__), - 'test_data', - 'output_inline_grouped.txt') - expected = ( - read(expected_file) - if six.PY3 - else read(expected_file).encode('utf-8') + self.assertEqual(actual, self.output_grouped) + + @mock.patch.object(Path, 'write_text') + def test_run_text_to_file_organized(self, mock_write_text): + actual = run( + self.input_text, + PEP8_CONFIG, + mock.Mock(formatter='grouped', + ci=False, + print=False), + Path(__file__), ) - self.assertTrue( - run_importanize( - test_file, - PEP8_CONFIG, - mock.MagicMock(print=True, - formatter='inline-grouped')) + self.assertEqual(actual, self.output_grouped) + mock_write_text.assert_called_once_with(self.output_grouped) + + @mock.patch.object(Path, 'write_text') + def test_run_text_to_file_nothing_to_do(self, mock_write_text): + actual = run( + self.output_grouped, + PEP8_CONFIG, + mock.Mock(formatter='grouped', + ci=False, + print=False), + Path(__file__), ) - mock_print.assert_called_once_with(expected) + + self.assertEqual(actual, self.output_grouped) + mock_write_text.assert_not_called() @mock.patch(TESTING_MODULE + '.print', create=True) - def test_run_importanize_with_unavailable_formatter(self, mock_print): - test_file = os.path.join(os.path.dirname(__file__), - 'test_data', - 'input.txt') - expected_file = os.path.join(os.path.dirname(__file__), - 'test_data', - 'output_grouped.txt') - - expected = ( - read(expected_file) - if six.PY3 - else read(expected_file).encode('utf-8') + def test_run_text_print(self, mock_print): + actual = run( + self.input_text, + PEP8_CONFIG, + mock.Mock(formatter='grouped', + ci=False, + print=True, + header=True), + Path('foo'), ) - self.assertTrue( - run_importanize( - test_file, - PEP8_CONFIG, - mock.MagicMock(print=True, - formatter='UnavailableFormatter')) - ) - mock_print.assert_called_once_with(expected) - - @mock.patch(TESTING_MODULE + '.open', create=True) - def test_run_importanize_write(self, mock_open): - test_file = os.path.join(os.path.dirname(__file__), - 'test_data', - 'input.txt') - expected_file = os.path.join(os.path.dirname(__file__), - 'test_data', - 'output_grouped.txt') - expected = read(expected_file).encode('utf-8') - - self.assertTrue( - run_importanize( - test_file, - PEP8_CONFIG, - mock.MagicMock(print=False, - formatter='grouped')) - ) - mock_open.assert_called_once_with(test_file, 'wb') - mock_open.return_value \ - .__enter__.return_value \ - .write.assert_called_once_with(expected) - - @mock.patch(TESTING_MODULE + '.open', create=True) - def test_run_importanize_write_inline_group_formatter(self, mock_open): - test_file = os.path.join(os.path.dirname(__file__), - 'test_data', - 'input.txt') - expected_file = os.path.join(os.path.dirname(__file__), - 'test_data', - 'output_inline_grouped.txt') - expected = read(expected_file).encode('utf-8') - - self.assertTrue( - run_importanize( - test_file, - PEP8_CONFIG, - mock.MagicMock(print=False, - formatter='inline-grouped')) - ) - mock_open.assert_called_once_with(test_file, 'wb') - mock_open.return_value \ - .__enter__.return_value \ - .write.assert_called_once_with(expected) - - @mock.patch(TESTING_MODULE + '.run_importanize') - @mock.patch('os.path.isdir') - def test_run_single_file(self, mock_isdir, mock_run_importanize): - mock_isdir.return_value = False - run( - mock.sentinel.path, - mock.sentinel.config, - mock.sentinel.args, - ) - mock_run_importanize.assert_called_once_with( - mock.sentinel.path, - mock.sentinel.config, - mock.sentinel.args, - ) + self.assertEqual(actual, self.output_grouped) + mock_print.assert_has_calls([ + mock.call('==='), + mock.call('foo'), + mock.call('---'), + mock.call(self.output_grouped), + ]) - @mock.patch(TESTING_MODULE + '.parser') - @mock.patch(TESTING_MODULE + '.run_importanize') - @mock.patch('os.path.isdir') - def test_run_single_file_exception(self, - mock_isdir, - mock_run_importanize, - mock_parser): - mock_isdir.return_value = False - mock_run_importanize.side_effect = ValueError - run( - mock.sentinel.path, - mock.sentinel.config, - mock.sentinel.args, - ) - mock_run_importanize.assert_called_once_with( - mock.sentinel.path, - mock.sentinel.config, - mock.sentinel.args, - ) - mock_parser.error.assert_called_once_with(mock.ANY) - - @mock.patch(TESTING_MODULE + '.print', mock.MagicMock(), create=True) - @mock.patch(TESTING_MODULE + '.run_importanize') - @mock.patch('os.walk') - @mock.patch('os.path.isdir') - def test_run_folder(self, - mock_isdir, - mock_walk, - mock_run_importanize): - mock_isdir.return_value = True - mock_walk.return_value = [ - ( - 'root', - ['dir1', 'dir2'], - ['foo.py', 'bar.txt'], - ), - ] - - conf = mock.MagicMock(print=True) - run( - mock.sentinel.path, - mock.sentinel.config, - conf, - ) - mock_run_importanize.assert_called_once_with( - os.path.join('root', 'foo.py'), - mock.sentinel.config, - conf, + @mock.patch(TESTING_MODULE + '.print', create=True) + def test_run_text_print_no_file(self, mock_print): + actual = run( + self.input_text, + PEP8_CONFIG, + mock.Mock(formatter='grouped', + ci=False, + print=True, + header=True), ) - @mock.patch(TESTING_MODULE + '.print', mock.MagicMock(), create=True) - @mock.patch(TESTING_MODULE + '.parser') - @mock.patch(TESTING_MODULE + '.run_importanize') - @mock.patch('os.walk') - @mock.patch('os.path.isdir') - def test_run_folder_exception(self, - mock_isdir, - mock_walk, - mock_run_importanize, - mock_parser): - mock_run_importanize.side_effect = ValueError - mock_isdir.return_value = True - mock_walk.return_value = [ - ( - 'root', - ['dir1', 'dir2'], - ['foo.py', 'bar.txt'], - ), - ] - - conf = mock.MagicMock(print=True) - run( - mock.sentinel.path, - mock.sentinel.config, - conf, + self.assertEqual(actual, self.output_grouped) + mock_print.assert_has_calls([ + mock.call(self.output_grouped), + ]) + + @mock.patch(TESTING_MODULE + '.print', create=True) + def test_run_text_print_no_header(self, mock_print): + actual = run( + self.input_text, + PEP8_CONFIG, + mock.Mock(formatter='grouped', + ci=False, + print=True, + header=False), + Path('foo'), ) - mock_run_importanize.assert_called_once_with( - os.path.join('root', 'foo.py'), - mock.sentinel.config, - conf, + + self.assertEqual(actual, self.output_grouped) + mock_print.assert_has_calls([ + mock.call(self.output_grouped), + ]) + + @mock.patch(TESTING_MODULE + '.print', create=True) + def test_run_file_skipped(self, mock_print): + config = deepcopy(PEP8_CONFIG) + config['exclude'] = ['*/test_data/*.py'] + + actual = run( + self.test_data / 'input.py', + config, + mock.Mock(formatter='grouped', + ci=False, + print=True, + header=False), ) - mock_parser.error.assert_called_once_with(mock.ANY) - @mock.patch('os.path.exists') - @mock.patch('os.getcwd') - def test_find_config(self, mock_getcwd, mock_exists): - mock_getcwd.return_value = os.path.join('path', 'to', 'importanize') - mock_exists.side_effect = False, True + self.assertIsNone(actual) + mock_print.assert_not_called() - config, found = find_config() + @mock.patch(TESTING_MODULE + '.print', create=True) + def test_run_file(self, mock_print): + actual = run( + self.test_data / 'input.py', + PEP8_CONFIG, + mock.Mock(formatter='grouped', + ci=False, + print=True, + header=False), + ) - self.assertEqual(config, os.path.join('path', 'to', '.importanizerc')) - self.assertTrue(bool(found)) + self.assertEqual(actual, self.output_grouped) + mock_print.assert_called_once_with(self.output_grouped) - @mock.patch(TESTING_MODULE + '.run') - @mock.patch('logging.getLogger') - @mock.patch.object(parser, 'parse_args') - def test_main_without_config(self, - mock_parse_args, - mock_get_logger, - mock_run): - args = mock.MagicMock( - verbose=1, - version=False, - path=[os.path.join('path', '..')], - config=None, + @mock.patch(TESTING_MODULE + '.print', create=True) + def test_run_dir(self, mock_print): + actual = run( + self.test_data, + PEP8_CONFIG, + mock.Mock(formatter='grouped', + ci=False, + print=True, + header=False), ) - mock_parse_args.return_value = args - main() + self.assertIsNone(actual) + mock_print.assert_has_calls([ + mock.call(self.output_grouped), + ]) - mock_parse_args.assert_called_once_with() - mock_get_logger.assert_called_once_with('') - mock_get_logger().setLevel.assert_called_once_with(logging.INFO) - mock_run.assert_called_once_with(os.getcwd(), PEP8_CONFIG, args) + @mock.patch(TESTING_MODULE + '.print', create=True) + def test_run_dir_co(self, mock_print): + with self.assertRaises(CIFailure): + run( + self.test_data, + PEP8_CONFIG, + mock.Mock(formatter='grouped', + ci=True, + print=True, + header=False), + ) - @mock.patch(TESTING_MODULE + '.read') - @mock.patch(TESTING_MODULE + '.run') - @mock.patch('json.loads') - @mock.patch('logging.getLogger') - @mock.patch.object(parser, 'parse_args') - def test_main_with_config(self, - mock_parse_args, - mock_get_logger, - mock_loads, - mock_run, - mock_read): - args = mock.MagicMock( - verbose=1, - version=False, - path=[os.path.join('path', '..')], - config=mock.sentinel.config, - ) - mock_parse_args.return_value = args + @mock.patch.object(Path, 'cwd') + def test_find_config(self, mock_cwd): + mock_cwd.return_value = Path(__file__) - main() + config = find_config() - mock_parse_args.assert_called_once_with() - mock_get_logger.assert_called_once_with('') - mock_get_logger().setLevel.assert_called_once_with(logging.INFO) - mock_read.assert_called_once_with(mock.sentinel.config) - mock_loads.assert_called_once_with(mock_read.return_value) - mock_run.assert_called_once_with( - os.getcwd(), mock_loads.return_value, args + expected_config = Path(__file__).parent.parent.joinpath( + IMPORTANIZE_CONFIG ) + self.assertEqual(config, six.text_type(expected_config.resolve())) @mock.patch(TESTING_MODULE + '.print', create=True) - @mock.patch('sys.exit') - @mock.patch.object(parser, 'parse_args') - def test_main_version(self, mock_parse_args, mock_exit, mock_print): - mock_exit.side_effect = SystemExit - mock_parse_args.return_value = mock.MagicMock( - verbose=1, - version=True, - ) + def test_main_version(self, mock_print): + + self.assertEqual(main(['--version']), 0) + + self.assertEqual(mock_print.call_count, 1) + version = mock_print.mock_calls[0][1][0] + self.assertIn('version: {}'.format(__version__), version) + @mock.patch(TESTING_MODULE + '.S_ISFIFO', mock.Mock(return_value=True)) + def test_main_piped_with_paths(self): with self.assertRaises(SystemExit): - main() + main(['path']) + + @mock.patch(TESTING_MODULE + '.S_ISFIFO', mock.Mock(return_value=True)) + @mock.patch(TESTING_MODULE + '.print', create=True) + @mock.patch.object(sys, 'stdin') + def test_main_piped(self, mock_stdin, mock_print): + mock_stdin.read.return_value = self.input_text + actual = main([]) + + self.assertEqual(actual, 0) + mock_print.assert_called_once_with(self.output_grouped) + + @mock.patch(TESTING_MODULE + '.S_ISFIFO', mock.Mock(return_value=False)) + @mock.patch(TESTING_MODULE + '.print', create=True) + def test_main_not_piped(self, mock_print): + actual = main([ + six.text_type(self.test_data / 'input.py'), + '--config', six.text_type(self.test_data / 'config.json'), + '--print', + '--no-header', + ]) + + self.assertEqual(actual, 0) + mock_print.assert_called_once_with(self.output_grouped) + + @mock.patch(TESTING_MODULE + '.S_ISFIFO', mock.Mock(return_value=False)) + @mock.patch(TESTING_MODULE + '.print', create=True) + def test_main_not_piped_ci(self, mock_print): + actual = main([ + six.text_type(self.test_data / 'input.py'), + '--config', six.text_type(self.test_data / 'config.json'), + '--ci', + ]) + + self.assertEqual(actual, 1) + + @mock.patch(TESTING_MODULE + '.S_ISFIFO', mock.Mock(return_value=False)) + @mock.patch(TESTING_MODULE + '.print', create=True) + @mock.patch(TESTING_MODULE + '.run') + def test_main_not_piped_exception(self, mock_run, mock_print): + mock_run.side_effect = ValueError + + actual = main([ + six.text_type(self.test_data / 'input.py'), + '--config', six.text_type(self.test_data / 'config.json'), + '--ci', + ]) - mock_parse_args.assert_called_once_with() - mock_exit.assert_called_once_with(0) - mock_print.assert_called_once_with(mock.ANY) + self.assertEqual(actual, 1) diff --git a/tests/test_statements.py b/tests/test_statements.py index 9abfd12..5ae615f 100644 --- a/tests/test_statements.py +++ b/tests/test_statements.py @@ -104,16 +104,39 @@ def test_hash_mock(self, mock_as_string): class TestImportStatement(unittest.TestCase): def test_init(self): actual = ImportStatement(mock.sentinel.line_numbers, - mock.sentinel.stem) + 'foo') self.assertEqual(actual.line_numbers, mock.sentinel.line_numbers) - self.assertEqual(actual.stem, mock.sentinel.stem) + self.assertEqual(actual.stem, 'foo') + self.assertIsNone(actual.as_name) + self.assertEqual(actual.leafs, []) + + actual = ImportStatement(mock.sentinel.line_numbers, + 'foo as bar') + self.assertEqual(actual.line_numbers, mock.sentinel.line_numbers) + self.assertEqual(actual.stem, 'foo') + self.assertEqual(actual.as_name, 'bar') + self.assertEqual(actual.leafs, []) + + actual = ImportStatement(mock.sentinel.line_numbers, + 'foo as foo') + self.assertEqual(actual.line_numbers, mock.sentinel.line_numbers) + self.assertEqual(actual.stem, 'foo') + self.assertIsNone(actual.as_name) self.assertEqual(actual.leafs, []) actual = ImportStatement(mock.sentinel.line_numbers, - mock.sentinel.stem, + 'foo', + mock.sentinel.leafs) + self.assertEqual(actual.line_numbers, mock.sentinel.line_numbers) + self.assertEqual(actual.stem, 'foo') + self.assertEqual(actual.leafs, mock.sentinel.leafs) + + actual = ImportStatement(mock.sentinel.line_numbers, + 'foo as bar', mock.sentinel.leafs) self.assertEqual(actual.line_numbers, mock.sentinel.line_numbers) - self.assertEqual(actual.stem, mock.sentinel.stem) + self.assertEqual(actual.stem, 'foo') + self.assertIsNone(actual.as_name) self.assertEqual(actual.leafs, mock.sentinel.leafs) def test_root_module(self): @@ -140,6 +163,7 @@ def _test(stem, leafs, expected): self.assertEqual(statement.as_string(), expected) _test('a', [], 'import a') + _test('a as b', [], 'import a as b') _test('a.b.c', [], 'import a.b.c') _test('a.b', ['c'], 'from a.b import c') _test('a.b', ['c', 'd'], 'from a.b import c, d') diff --git a/tests/test_utils.py b/tests/test_utils.py index b167963..e369279 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,46 +1,21 @@ # -*- coding: utf-8 -*- from __future__ import print_function, unicode_literals -import os -import sys import unittest import mock from importanize.utils import ( - ignore_site_packages_paths, + force_bytes, + force_text, is_site_package, is_std_lib, + list_split, list_strip, read, ) class TestUtils(unittest.TestCase): - def _test_ignore_site_packages_paths(self, raise_msg=None): - sys.path.append(os.getcwd()) - paths = sys.path[:] - - try: - with ignore_site_packages_paths(): - self.assertNotEqual(sys.path, paths) - self.assertLess(len(sys.path), len(paths)) - if raise_msg: - raise ValueError(raise_msg) - except ValueError as e: - if raise_msg not in str(e): - # -- This only happens if there's a bug in this test - raise # pragma: no cover - - self.assertIn(os.getcwd(), sys.path) - self.assertListEqual(sys.path, paths) - sys.path.remove(os.getcwd()) - - def test_site_packages_paths(self): - self._test_ignore_site_packages_paths(raise_msg=None) - - def test_site_packages_paths_exception(self): - self._test_ignore_site_packages_paths(raise_msg="TEST EXCEPTION") - def test_is_std_lib(self): self.assertFalse(is_std_lib('')) @@ -69,7 +44,6 @@ def test_is_std_lib(self): 'operator', 'optparse', 'os', - 'pdb', 'pickle', 'pprint', 'random', @@ -159,3 +133,17 @@ def test_read(self, mock_open): .read.return_value .decode.return_value) ) + + def test_list_split(self): + self.assertEqual( + list(list_split(['foo', '/', 'bar'], '/')), + [['foo'], ['bar']] + ) + + def test_force_text(self): + self.assertEqual(force_text(b'foo'), u'foo') + self.assertEqual(force_text(u'foo'), u'foo') + + def test_force_bytes(self): + self.assertEqual(force_bytes('foo'), b'foo') + self.assertEqual(force_bytes(b'foo'), b'foo') From f090992f217b2e96143f83cca8a99fd6d7962b8a Mon Sep 17 00:00:00 2001 From: Miroslav Shubernetskiy Date: Wed, 3 May 2017 13:09:34 -0400 Subject: [PATCH 04/11] ignoring current dir in find_module --- importanize/utils.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/importanize/utils.py b/importanize/utils.py index 991eb4b..49dc0eb 100644 --- a/importanize/utils.py +++ b/importanize/utils.py @@ -2,14 +2,19 @@ from __future__ import print_function, unicode_literals import imp import operator +import os import sys def _get_module_path(module_name): + paths = sys.path[:] + if os.getcwd() in sys.path: + paths.remove(os.getcwd()) + try: # TODO deprecated in Py3. # TODO Find better way for py2 and py3 compatibility. - return imp.find_module(module_name)[1] + return imp.find_module(module_name, paths)[1] except ImportError: return '' From a1befaa2a48cdc352f45690beca15c61defba080 Mon Sep 17 00:00:00 2001 From: Miroslav Shubernetskiy Date: Wed, 3 May 2017 14:16:33 -0400 Subject: [PATCH 05/11] updated HISTORY --- AUTHORS.rst | 4 ++-- HISTORY.rst | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 2ee2c81..9d117b9 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -9,5 +9,5 @@ Development Lead Contributors ~~~~~~~~~~~~ -Benjamin Abel - https://github.com/benjaminabel -Pamela McA'Nulty - https://github.com/PamelaM +* Benjamin Abel - https://github.com/benjaminabel +* Pamela McA'Nulty - https://github.com/PamelaM diff --git a/HISTORY.rst b/HISTORY.rst index bf3644a..dbd852c 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,15 @@ History ------- +0.5.0 (2017-05-03) +~~~~~~~~~~~~~~~~~~ + +* Added ``--ci`` flag to validate import organization in files +* Added ``sitepackages`` import group. Thanks `Pamela `_. + See ``README`` for more info. +* Added pipe handling (e.g. ``cat foo.py | importanize``) +* Fixed bug which incorrectly sorted imports with aliases (e.g. ``import foo as bar``) + 0.4.1 (2015-07-28) ~~~~~~~~~~~~~~~~~~ From 98f010840604d032b2e9adf681a6c7447bb2163a Mon Sep 17 00:00:00 2001 From: Miroslav Shubernetskiy Date: Wed, 3 May 2017 14:19:57 -0400 Subject: [PATCH 06/11] releasing as Python wheels --- HISTORY.rst | 3 ++- Makefile | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index dbd852c..648468a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -8,9 +8,10 @@ History * Added ``--ci`` flag to validate import organization in files * Added ``sitepackages`` import group. Thanks `Pamela `_. - See ``README`` for more info. + See ``README`` for more info * Added pipe handling (e.g. ``cat foo.py | importanize``) * Fixed bug which incorrectly sorted imports with aliases (e.g. ``import foo as bar``) +* Released as Python `wheel `_ 0.4.1 (2015-07-28) ~~~~~~~~~~~~~~~~~~ diff --git a/Makefile b/Makefile index c16faf0..28037b2 100644 --- a/Makefile +++ b/Makefile @@ -60,9 +60,11 @@ check: lint clean-build clean-pyc clean-test test-coverage release: clean python setup.py sdist upload + python setup.py bdist_wheel upload dist: clean python setup.py sdist + python setup.py bdist_wheel ls -l dist docs: From bdff074e8aff00b3095b6e77cf84b33f183aac5a Mon Sep 17 00:00:00 2001 From: Miroslav Shubernetskiy Date: Wed, 3 May 2017 14:32:20 -0400 Subject: [PATCH 07/11] logging successful importanize only when writing to files --- importanize/__main__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/importanize/__main__.py b/importanize/__main__.py index 22a8e29..d20d25d 100644 --- a/importanize/__main__.py +++ b/importanize/__main__.py @@ -235,10 +235,10 @@ def run(source, config, args, path=None): else: path.write_text(organized) - msg = 'Successfully importanized' - if path: - msg += ' {}'.format(path) - log.info(msg) + msg = 'Successfully importanized' + if path: + msg += ' {}'.format(path) + log.info(msg) return organized From a8102a935cb22ad84153210a1725cdfe4d546c99 Mon Sep 17 00:00:00 2001 From: Miroslav Shubernetskiy Date: Wed, 3 May 2017 15:49:28 -0400 Subject: [PATCH 08/11] allowing path parameter with pipes which fixes subprocess usage --- importanize/__main__.py | 9 +++++---- tests/test_main.py | 5 ----- tox.ini | 2 +- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/importanize/__main__.py b/importanize/__main__.py index d20d25d..3b7f295 100644 --- a/importanize/__main__.py +++ b/importanize/__main__.py @@ -269,6 +269,10 @@ def run(source, config, args, path=None): raise CIFailure() +def is_piped(): + return S_ISFIFO(os.fstat(0).st_mode) + + def main(args=None): args = args if args is not None else sys.argv[1:] args = parser.parse_args(args=args) @@ -303,10 +307,7 @@ def main(args=None): to_importanize = [pathlib.Path(i) for i in (args.path or ['.'])] - if S_ISFIFO(os.fstat(0).st_mode): - if args.path: - return parser.error('Cant supply any paths when piping input') - + if is_piped() and not args.path: to_importanize = [force_text(sys.stdin.read())] args.print = True args.header = False diff --git a/tests/test_main.py b/tests/test_main.py index e26e1ff..680f7c2 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -231,11 +231,6 @@ def test_main_version(self, mock_print): version = mock_print.mock_calls[0][1][0] self.assertIn('version: {}'.format(__version__), version) - @mock.patch(TESTING_MODULE + '.S_ISFIFO', mock.Mock(return_value=True)) - def test_main_piped_with_paths(self): - with self.assertRaises(SystemExit): - main(['path']) - @mock.patch(TESTING_MODULE + '.S_ISFIFO', mock.Mock(return_value=True)) @mock.patch(TESTING_MODULE + '.print', create=True) @mock.patch.object(sys, 'stdin') diff --git a/tox.ini b/tox.ini index 412bbee..bc27627 100644 --- a/tox.ini +++ b/tox.ini @@ -12,4 +12,4 @@ whitelist_externals = make [flake8] -exclude = tests/test_data/* +exclude = .tox, tests/test_data/* From 0f36e269b938201f9f0bb60694ddf6e3eb85574b Mon Sep 17 00:00:00 2001 From: Miroslav Shubernetskiy Date: Thu, 4 May 2017 10:46:03 -0400 Subject: [PATCH 09/11] using fully resolved path for skipping files --- importanize/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/importanize/__main__.py b/importanize/__main__.py index 3b7f295..c718656 100644 --- a/importanize/__main__.py +++ b/importanize/__main__.py @@ -244,7 +244,7 @@ def run(source, config, args, path=None): elif source.is_file(): if config.get('exclude'): - if any(map(lambda i: fnmatch(six.text_type(source), i), + if any(map(lambda i: fnmatch(six.text_type(source.resolve()), i), config.get('exclude'))): log.info('Skipping {}'.format(source)) return From 8279b0be59309127102158eedb76d6c9f1e79e5e Mon Sep 17 00:00:00 2001 From: Miroslav Shubernetskiy Date: Thu, 4 May 2017 10:53:57 -0400 Subject: [PATCH 10/11] just using message text in logs --- importanize/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/importanize/__main__.py b/importanize/__main__.py index c718656..d5fe87d 100644 --- a/importanize/__main__.py +++ b/importanize/__main__.py @@ -23,7 +23,7 @@ from .utils import force_text, read -LOGGING_FORMAT = '%(levelname)s %(name)s %(message)s' +LOGGING_FORMAT = '%(message)s' IMPORTANIZE_CONFIG = '.importanizerc' PEP8_CONFIG = { 'groups': [ From 0fada4f56134e365ccd1a6b9055b992e84de80ed Mon Sep 17 00:00:00 2001 From: Miroslav Shubernetskiy Date: Thu, 4 May 2017 10:55:56 -0400 Subject: [PATCH 11/11] added history bullet point about not overriding organized files [ci skip] --- HISTORY.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index 648468a..9b19d94 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -11,6 +11,8 @@ History See ``README`` for more info * Added pipe handling (e.g. ``cat foo.py | importanize``) * Fixed bug which incorrectly sorted imports with aliases (e.g. ``import foo as bar``) +* Files are not overridden when imports are already organized. + Useful in precommit hooks which detect changed files. * Released as Python `wheel `_ 0.4.1 (2015-07-28)