diff options
author | Timothy Edmund Crosley <timothy.crosley@gmail.com> | 2017-04-01 16:22:11 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-04-01 16:22:11 -0700 |
commit | e70653f984e40e441098864e275f75e34bffa744 (patch) | |
tree | 03ca912c906dc258dc55b358f671bcff2308c2a2 | |
parent | 7bb5b45c291bdd7ef7e7e589e77de61e3bde68e8 (diff) | |
parent | 7778d2bde51af7ff2d46fbf1f84a568e55745397 (diff) | |
download | isort-e70653f984e40e441098864e275f75e34bffa744.tar.gz |
Merge branch 'develop' into feature/trailing_comma_in_single_wrap
-rw-r--r-- | ACKNOWLEDGEMENTS.md | 3 | ||||
-rw-r--r-- | CHANGELOG.md | 7 | ||||
-rw-r--r-- | README.rst | 2 | ||||
-rw-r--r-- | appveyor.yml | 12 | ||||
-rw-r--r-- | isort/__main__.py (renamed from isort/__main__) | 0 | ||||
-rw-r--r-- | isort/isort.py | 101 | ||||
-rwxr-xr-x | isort/main.py | 8 | ||||
-rw-r--r-- | isort/settings.py | 3 | ||||
-rwxr-xr-x | setup.py | 1 | ||||
-rw-r--r-- | test_isort.py | 109 |
10 files changed, 206 insertions, 40 deletions
diff --git a/ACKNOWLEDGEMENTS.md b/ACKNOWLEDGEMENTS.md index 56b26198..e59a24b1 100644 --- a/ACKNOWLEDGEMENTS.md +++ b/ACKNOWLEDGEMENTS.md @@ -67,7 +67,10 @@ Code Contributors - Dan Palmer (@danpalmer) - Andy Boot (@bootandy) - @m7v8 +- John Vandenberg (@jayvdb) - Adam Chainz (@adamchainz) +- @Brightcells +- Jonas Trappenberg (@teeberg) Documenters =================== diff --git a/CHANGELOG.md b/CHANGELOG.md index 9972e04a..bdffa81e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,3 +58,10 @@ Changelog ### 4.2.5 - Fixed an issue that caused modules to inccorectly be matched as thirdparty when they simply had `src` in the leading path, even if they weren't withing $VIRTUALENV/src #414 + +### 4.2.6 +- Added --enforce-whitespace option to go along with --check-only for more exact checks (issue #423) +- Fixed imports with a tailing '\' and no space in-between getting removed (issue #425) + +### 4.2.7 +- Added `--virtual-env` switch command line option @@ -29,7 +29,7 @@ isort your python imports for you so you don't have to. isort is a Python utility / library to sort imports alphabetically, and automatically separated into sections. It provides a command line utility, Python library and `plugins for various editors <https://github.com/timothycrosley/isort/wiki/isort-Plugins>`_ to quickly sort all your imports. -It currently cleanly supports Python 2.6 - 3.5 using `pies <https://github.com/timothycrosley/pies>`_ to achieve this without ugly hacks and/or py2to3. +It currently cleanly supports Python 2.6 - 3.5 without any dependencies. .. image:: https://raw.github.com/timothycrosley/isort/develop/example.gif :alt: Example Usage diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 00000000..65e0d282 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,12 @@ +init: + - "SET PATH=C:\\Python27\\Scripts;%PATH%" + - "ECHO PATH=%PATH%" + - python --version + +build: off + +install: + - pip install tox + +test_script: + - tox -e isort-check,py26,py27,py33,py34,py35 diff --git a/isort/__main__ b/isort/__main__.py index 94b1d057..94b1d057 100644 --- a/isort/__main__ +++ b/isort/__main__.py diff --git a/isort/isort.py b/isort/isort.py index 7864c010..869affe5 100644 --- a/isort/isort.py +++ b/isort/isort.py @@ -38,7 +38,6 @@ from difflib import unified_diff from fnmatch import fnmatch from glob import glob from sys import path as PYTHONPATH -from sys import stdout from . import settings from .natural import nsorted @@ -162,20 +161,24 @@ class SortImports(object): return if check: - if self.output.replace("\n", "").replace(" ", "") == file_contents.replace("\n", "").replace(" ", ""): + check_output = self.output + check_against = file_contents + if not self.config.get('enforce_white_space', False): + check_output = check_output.replace("\n", "").replace(" ", "") + check_against = check_against.replace("\n", "").replace(" ", "") + + if check_output == check_against: if self.config['verbose']: print("SUCCESS: {0} Everything Looks Good!".format(self.file_path)) - else: - print("ERROR: {0} Imports are incorrectly sorted.".format(self.file_path)) - self.incorrectly_sorted = True - if show_diff or self.config.get('show_diff', False) is True: - self._show_diff(file_contents) - return + return + + print("ERROR: {0} Imports are incorrectly sorted.".format(self.file_path)) + self.incorrectly_sorted = True if show_diff or self.config.get('show_diff', False) is True: self._show_diff(file_contents) elif write_to_stdout: - stdout.write(self.output) + sys.stdout.write(self.output) elif file_name: if ask_to_apply: if self.output == file_contents: @@ -201,7 +204,7 @@ class SortImports(object): if self.file_path else datetime.now()), tofiledate=str(datetime.now()) ): - stdout.write(line) + sys.stdout.write(line) @staticmethod def _strip_top_comments(lines): @@ -248,15 +251,20 @@ class SortImports(object): paths += [path for path in glob('{0}/src/*'.format(virtual_env)) if os.path.isdir(path)] virtual_env_src = '{0}/src/'.format(virtual_env) + # handle case-insensitive paths on windows + stdlib_lib_prefix = os.path.normcase(get_stdlib_path()) + for prefix in paths: module_path = "/".join((prefix, module_name.replace(".", "/"))) package_path = "/".join((prefix, module_name.split(".")[0])) - if (os.path.exists(module_path + ".py") or os.path.exists(module_path + ".so") or - (os.path.exists(package_path) and os.path.isdir(package_path))): + is_module = (exists_case_sensitive(module_path + ".py") or + exists_case_sensitive(module_path + ".so")) + is_package = exists_case_sensitive(package_path) and os.path.isdir(package_path) + if is_module or is_package: if ('site-packages' in prefix or 'dist-packages' in prefix or (virtual_env and virtual_env_src in prefix)): return self.sections.THIRDPARTY - elif 'python2' in prefix.lower() or 'python3' in prefix.lower(): + elif os.path.normcase(prefix).startswith(stdlib_lib_prefix): return self.sections.STDLIB else: return self.config['default_section'] @@ -328,11 +336,14 @@ class SortImports(object): cont_line = self._wrap(self.config['indent'] + splitter.join(next_line).lstrip()) if self.config['use_parentheses']: - if self.config.get('include_trailing_comma', 0): - cont_line += "," - if wrap_mode in (settings.WrapModes.VERTICAL_HANGING_INDENT, settings.WrapModes.VERTICAL_GRID_GROUPED,): - cont_line += "\n" - return "{0}{1} (\n{2})".format(line, splitter, cont_line) + return "{0}{1} (\n{2}{3}{4})".format( + line, splitter, cont_line, + "," if self.config['include_trailing_comma'] else "", + "\n" if wrap_mode in ( + settings.WrapModes.VERTICAL_HANGING_INDENT, + settings.WrapModes.VERTICAL_GRID_GROUPED, + ) else "" + ) return "{0}{1} \\\n{2}".format(line, splitter, cont_line) elif len(line) > self.config['line_length'] and wrap_mode == settings.WrapModes.NOQA: if "# NOQA" not in line: @@ -422,10 +433,21 @@ class SortImports(object): import_statement = self._add_comments(comments, import_start + (", ").join(from_imports)) if not from_imports: import_statement = "" - if len(from_imports) > 1 and ( - len(import_statement) > self.config['line_length'] - or self.config.get('force_grid_wrap') - ): + + do_multiline_reformat = False + + if self.config.get('force_grid_wrap') and len(from_imports) > 1: + do_multiline_reformat = True + + if len(import_statement) > self.config['line_length'] and len(from_imports) > 1: + do_multiline_reformat = True + + # If line too long AND have imports AND we are NOT using GRID or VERTICAL wrap modes + if (len(import_statement) > self.config['line_length'] and len(from_imports) > 0 + and self.config.get('multi_line_output', 0) not in (1, 0)): + do_multiline_reformat = True + + if do_multiline_reformat: output_mode = settings.WrapModes._fields[self.config.get('multi_line_output', 0)].lower() formatter = getattr(self, "_output_" + output_mode, self._output_grid) @@ -449,7 +471,8 @@ class SortImports(object): new_import_statement = formatter(import_start, copy.copy(from_imports), dynamic_indent, indent, line_length, comments) lines = new_import_statement.split("\n") - elif len(import_statement) > self.config['line_length']: + + if not do_multiline_reformat and len(import_statement) > self.config['line_length']: import_statement = self._wrap(import_statement) if import_statement: @@ -780,7 +803,7 @@ class SortImports(object): if import_string.strip().endswith(" import") or line.strip().startswith("import "): import_string += "\n" + line else: - import_string = import_string.rstrip().rstrip("\\") + line.lstrip() + import_string = import_string.rstrip().rstrip("\\") + " " + line.lstrip() if import_type == "from": import_string = import_string.replace("import(", "import (") @@ -848,8 +871,7 @@ class SortImports(object): and not 'isort:imports-' in last): self.comments['above']['straight'].setdefault(module, []).insert(0, self.out_lines.pop(-1)) - if len(self.out_lines) > max(self.import_index - 1, self._first_comment_index_end, - 1) - 1: + if len(self.out_lines) > 0: last = self.out_lines[-1].rstrip() else: last = "" @@ -880,3 +902,30 @@ def coding_check(fname, default='utf-8'): break return coding + + +def get_stdlib_path(): + """Returns the path to the standard lib for the current path installation. + + This function can be dropped and "sysconfig.get_paths()" used directly once Python 2.6 support is dropped. + """ + if sys.version_info >= (2, 7): + import sysconfig + return sysconfig.get_paths()['stdlib'] + else: + return os.path.join(sys.prefix, 'lib') + + +def exists_case_sensitive(path): + """ + Returns if the given path exists and also matches the case on Windows. + + When finding files that can be imported, it is important for the cases to match because while + file os.path.exists("module.py") and os.path.exists("MODULE.py") both return True on Windows, Python + can only import using the case of the real file. + """ + result = os.path.exists(path) + if sys.platform.startswith('win') and result: + directory, basename = os.path.split(path) + result = basename in os.listdir(directory) + return result diff --git a/isort/main.py b/isort/main.py index 600fbad5..b5ca93d9 100755 --- a/isort/main.py +++ b/isort/main.py @@ -162,6 +162,8 @@ def create_parser(): help='Force sortImports to recognize a module as being part of a third party library.') parser.add_argument('-p', '--project', dest='known_first_party', action='append', help='Force sortImports to recognize a module as being part of the current python project.') + parser.add_argument('--virtual-env', dest='virtual_env', + help='Virtual environment to use for determining whether a package is third-party') parser.add_argument('-m', '--multi_line', dest='multi_line_output', type=int, choices=[0, 1, 2, 3, 4, 5], help='Multi line output (0-grid, 1-vertical, 2-hanging, 3-vert-hanging, 4-vert-grid, ' '5-vert-grid-grouped).') @@ -181,6 +183,8 @@ def create_parser(): parser.add_argument('-c', '--check-only', action='store_true', default=False, dest="check", help='Checks the file for unsorted / unformatted imports and prints them to the ' 'command line without modifying the file.') + parser.add_argument('-ws', '--enforce-white-space', action='store_true', default=False, dest="enforce_white_space", + help='Tells isort to enforce white space difference when --check-only is being used.') parser.add_argument('-sl', '--force-single-line-imports', dest='force_single_line', action='store_true', help='Forces all from imports to appear on their own line') parser.add_argument('--force_single_line_imports', dest='force_single_line', action='store_true', @@ -243,6 +247,10 @@ def main(): print(INTRO) return + if 'settings_path' in arguments: + sp = arguments['settings_path'] + arguments['settings_path'] = os.path.abspath(sp) if os.path.isdir(sp) else os.path.dirname(os.path.abspath(sp)) + file_names = arguments.pop('files', []) if file_names == ['-']: SortImports(file_contents=sys.stdin.read(), write_to_stdout=True, **arguments) diff --git a/isort/settings.py b/isort/settings.py index 26cec5bd..d17ba58a 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -103,7 +103,8 @@ default = {'force_to_top': [], 'force_alphabetical_sort': False, 'force_grid_wrap': False, 'force_sort_within_sections': False, - 'show_diff': False} + 'show_diff': False, + 'enforce_white_space': False} @lru_cache() @@ -45,7 +45,6 @@ setup(name='isort', author='Timothy Crosley', author_email='timothy.crosley@gmail.com', url='https://github.com/timothycrosley/isort', - download_url='https://github.com/timothycrosley/isort/archive/4.2.5.tar.gz', license="MIT", entry_points={ 'console_scripts': [ diff --git a/test_isort.py b/test_isort.py index b60aca24..9520fea7 100644 --- a/test_isort.py +++ b/test_isort.py @@ -28,7 +28,7 @@ import os import shutil import tempfile -from isort.isort import SortImports +from isort.isort import exists_case_sensitive, SortImports from isort.pie_slice import * from isort.settings import WrapModes @@ -435,14 +435,48 @@ def test_custom_indent(): def test_use_parentheses(): test_input = ( - "from fooooooooooooooooooooooooo.baaaaaaaaaaaaaaaaaaarrrrrrr import \\" + "from fooooooooooooooooooooooooo.baaaaaaaaaaaaaaaaaaarrrrrrr import " " my_custom_function as my_special_function" ) test_output = SortImports( - file_contents=test_input, known_third_party=['django'], - line_length=79, use_parentheses=True, + file_contents=test_input, line_length=79, use_parentheses=True ).output - assert '(' in test_output + + assert test_output == ( + "from fooooooooooooooooooooooooo.baaaaaaaaaaaaaaaaaaarrrrrrr import (\n" + " my_custom_function as my_special_function)\n" + ) + + test_output = SortImports( + file_contents=test_input, line_length=79, use_parentheses=True, + include_trailing_comma=True, + ).output + + assert test_output == ( + "from fooooooooooooooooooooooooo.baaaaaaaaaaaaaaaaaaarrrrrrr import (\n" + " my_custom_function as my_special_function,)\n" + ) + + test_output = SortImports( + file_contents=test_input, line_length=79, use_parentheses=True, + multi_line_output=WrapModes.VERTICAL_HANGING_INDENT + ).output + + assert test_output == ( + "from fooooooooooooooooooooooooo.baaaaaaaaaaaaaaaaaaarrrrrrr import (\n" + " my_custom_function as my_special_function\n)\n" + ) + + test_output = SortImports( + file_contents=test_input, line_length=79, use_parentheses=True, + multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, + include_trailing_comma=True + ).output + + assert test_output == ( + "from fooooooooooooooooooooooooo.baaaaaaaaaaaaaaaaaaarrrrrrr import (\n" + " my_custom_function as my_special_function,\n)\n" + ) def test_skip(): @@ -771,6 +805,7 @@ def test_force_single_line_long_imports(): assert test_output == ("from veryveryveryveryveryvery import big\n" "from veryveryveryveryveryvery import small # NOQA\n") + def test_titled_imports(): """Tests setting custom titled/commented import sections.""" test_input = ("import sys\n" @@ -830,6 +865,20 @@ def test_multiline_import(): assert SortImports(file_contents=test_input, **custom_configuration).output == expected_output +def test_single_multiline(): + """Test the case where a single import spawns multiple lines.""" + test_input = ("from os import\\\n" + " getuid\n" + "\n" + "print getuid()\n") + output = SortImports(file_contents=test_input).output + assert output == ( + "from os import getuid\n" + "\n" + "print getuid()\n" + ) + + def test_atomic_mode(): # without syntax error, everything works OK test_input = ("from b import d, c\n" @@ -1485,15 +1534,14 @@ def test_fcntl(): def test_import_split_is_word_boundary_aware(): """Test to ensure that isort splits words in a boundry aware mannor""" - test_input = ("from mycompany.model.size_value_array_import_func import (" - " get_size_value_array_import_func_jobs," - ")") + test_input = ("from mycompany.model.size_value_array_import_func import \\\n" + " get_size_value_array_import_func_jobs") test_output = SortImports(file_contents=test_input, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, line_length=79).output - - assert test_output == ("from mycompany.model.size_value_array_import_func import \\\n" - " get_size_value_array_import_func_jobs\n") + assert test_output == ("from mycompany.model.size_value_array_import_func import (\n" + " get_size_value_array_import_func_jobs\n" + ")\n") def test_other_file_encodings(): @@ -1780,6 +1828,18 @@ def test_import_by_paren_issue_375(): assert SortImports(file_contents=test_input).output == 'from .models import Bar, Foo\n' +def test_import_by_paren_issue_460(): + """Test to ensure isort can doesnt move comments around """ + test_input = """ +# First comment +# Second comment +# third comment +import io +import os +""" + assert SortImports(file_contents=(test_input)).output == test_input + + def test_function_with_docstring(): """Test to ensure isort can correctly sort imports when the first found content is a function with a docstring""" add_imports = ['from __future__ import unicode_literals'] @@ -1809,3 +1869,30 @@ def test_plone_style(): options = {'force_single_line': True, 'force_alphabetical_sort': True} assert SortImports(file_contents=test_input, **options).output == test_input + + +def test_third_party_case_sensitive(): + """Modules which match builtins by name but not on case should not be picked up on Windows.""" + test_input = ("import thirdparty\n" + "import os\n" + "import ABC\n") + + expected_output = ('import os\n' + '\n' + 'import ABC\n' + 'import thirdparty\n') + assert SortImports(file_contents=test_input).output == expected_output + + +def test_exists_case_sensitive_file(tmpdir): + """Test exists_case_sensitive function for a file.""" + tmpdir.join('module.py').ensure(file=1) + assert exists_case_sensitive(str(tmpdir.join('module.py'))) + assert not exists_case_sensitive(str(tmpdir.join('MODULE.py'))) + + +def test_exists_case_sensitive_directory(tmpdir): + """Test exists_case_sensitive function for a directory.""" + tmpdir.join('pkg').ensure(dir=1) + assert exists_case_sensitive(str(tmpdir.join('pkg'))) + assert not exists_case_sensitive(str(tmpdir.join('PKG'))) |