Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: PyCQA/isort
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 4.3.14
Choose a base ref
...
head repository: PyCQA/isort
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 4.3.15
Choose a head ref

Commits on Mar 1, 2019

  1. Copy the full SHA
    4a52002 View commit details

Commits on Mar 2, 2019

  1. Merge pull request #866 from asottile/patch-1

    Fix whitespace in warning message
    timothycrosley authored Mar 2, 2019
    Copy the full SHA
    bde366e View commit details

Commits on Mar 3, 2019

  1. Merge in fix for #417

    jpgrayson authored and timothycrosley committed Mar 3, 2019
    Copy the full SHA
    7c69483 View commit details
  2. tests: fix default_settings_path

    Restore `chdir` after tests.
    
    This is relevant for when pytest-cov would display coverage in the end,
    and would use absolute paths due to this.
    blueyed authored and timothycrosley committed Mar 3, 2019
    Copy the full SHA
    8ddec25 View commit details
  3. Fix test_inconsistent_behavior_in_python_2_and_3_issue_479

    This also failed for me with "future" being installed.
    
    Not sure if it makes sense to keep it in the first place, since isort
    appears to require py34 now.
    
    Ref: #479 (comment)
    blueyed authored and timothycrosley committed Mar 3, 2019
    Copy the full SHA
    de86190 View commit details
  4. Merge in no_lines_before fix

    blueyed authored and timothycrosley committed Mar 3, 2019
    Copy the full SHA
    cb633f2 View commit details
  5. Update changelog

    timothycrosley committed Mar 3, 2019
    Copy the full SHA
    db0107b View commit details
  6. Copy the full SHA
    ac94680 View commit details
  7. Copy the full SHA
    6cb20d7 View commit details
  8. Expand mac testing

    timothycrosley committed Mar 3, 2019
    Copy the full SHA
    379865f View commit details
  9. Copy the full SHA
    e35b181 View commit details
  10. Copy the full SHA
    e987c46 View commit details
  11. Move script

    timothycrosley committed Mar 3, 2019
    Copy the full SHA
    40f9446 View commit details
  12. Copy the full SHA
    5540530 View commit details
  13. Merge pull request #869 from timothycrosley/feature/add-mac-testing

    Attempt to add mac testing
    timothycrosley authored Mar 3, 2019
    Copy the full SHA
    f85db35 View commit details
  14. Fix spelling error

    timothycrosley committed Mar 3, 2019
    Copy the full SHA
    a19662a View commit details
  15. Copy the full SHA
    f6ff653 View commit details

Commits on Mar 4, 2019

  1. Add downloads link

    timothycrosley committed Mar 4, 2019
    Copy the full SHA
    1ec68ab View commit details
  2. Add LRU cache to RequirementsFinder._get_names

    This should speed up the isort invocations significantly, like what
    #856 intended to do, but somehow
    missed the mark.
    
    The `_get_names` method of RequirementsFinder seem to be the biggest culprit
    and not `_get_files_from_dir`. I think I confused my previous benchmark results
    due to `PipfileFinder` and `RequirementsFinder` being enabled in the same
    change, so I may potentially have swapped around the benchmark results 🤦‍♂️
    Tenzer committed Mar 4, 2019
    Copy the full SHA
    eb653c8 View commit details

Commits on Mar 5, 2019

  1. Lookup real path of virtualenv

    This ensures we don't get confused and incorrectly classify third party
    libraries as first party libraries.
    
    Fixes #876.
    brianmay committed Mar 5, 2019
    Copy the full SHA
    7834801 View commit details
  2. Merge pull request #877 from brianmay/fix_symlink_virtualenv

    Lookup real path of virtualenv
    timothycrosley authored Mar 5, 2019
    Copy the full SHA
    d0d4790 View commit details
  3. Copy the full SHA
    2130dff View commit details
  4. Merge pull request #874 from Tenzer/RequirementsFinder-lru-cache-3

    Add LRU cache to RequirementsFinder._get_names
    timothycrosley authored Mar 5, 2019
    Copy the full SHA
    bcec89b View commit details
  5. 2
    Copy the full SHA
    2c59158 View commit details
  6. Fix should skip usage

    timothycrosley committed Mar 5, 2019
    Copy the full SHA
    c7422c7 View commit details

Commits on Mar 6, 2019

  1. Copy the full SHA
    175b7f5 View commit details
  2. Fix filename replace

    timothycrosley committed Mar 6, 2019
    Copy the full SHA
    ea62412 View commit details
  3. Copy the full SHA
    2eb3264 View commit details
  4. Add test for pants

    timothycrosley committed Mar 6, 2019
    Copy the full SHA
    0ef06b1 View commit details
  5. Add missing file

    timothycrosley committed Mar 6, 2019
    Copy the full SHA
    61563f3 View commit details
  6. Copy the full SHA
    ccca48c View commit details
  7. Copy the full SHA
    df6de29 View commit details
  8. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    f0434ef View commit details
  9. Update changelog

    timothycrosley committed Mar 6, 2019
    Copy the full SHA
    9f89ff8 View commit details
  10. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    83c3b63 View commit details
  11. Fix newline argument

    timothycrosley committed Mar 6, 2019

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    130bf3a View commit details
  12. Bump version

    timothycrosley committed Mar 6, 2019

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    6eb4725 View commit details
  13. Copy the full SHA
    a278be9 View commit details
  14. Copy the full SHA
    138a4a1 View commit details

Commits on Mar 7, 2019

  1. Merge in fix for quiet mode

    emilmelnikov authored and timothycrosley committed Mar 7, 2019
    Copy the full SHA
    be50253 View commit details
  2. Copy the full SHA
    2ebab7c View commit details
  3. Copy the full SHA
    35765c5 View commit details
  4. Merge pull request #886 from timothycrosley/feature/anaconda-support

    Feature/anaconda support
    timothycrosley authored Mar 7, 2019
    Copy the full SHA
    d6f200b View commit details
  5. Copy the full SHA
    3a856aa View commit details

Commits on Mar 8, 2019

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    4f4064b View commit details
  2. Copy the full SHA
    f981631 View commit details
  3. Copy the full SHA
    476ebe4 View commit details
  4. Fix issue #890

    timothycrosley committed Mar 8, 2019
    Copy the full SHA
    12c034a View commit details
  5. Copy the full SHA
    fa3421e View commit details
  6. Copy the full SHA
    855806f View commit details
Showing with 439 additions and 105 deletions.
  1. +4 −4 .env
  2. +1 −1 .gitignore
  3. +41 −18 .travis.yml
  4. +36 −2 CHANGELOG.md
  5. +3 −0 README.rst
  6. +1 −1 isort/__init__.py
  7. +5 −1 isort/__main__.py
  8. +33 −8 isort/finders.py
  9. +101 −44 isort/isort.py
  10. +22 −10 isort/main.py
  11. +26 −1 isort/pie_slice.py
  12. +15 −7 isort/settings.py
  13. +28 −0 scripts/before_install.sh
  14. +1 −1 setup.py
  15. +122 −7 test_isort.py
8 changes: 4 additions & 4 deletions .env
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ fi
export PROJECT_NAME=$OPEN_PROJECT_NAME
export PROJECT_DIR="$PWD"

if [ ! -d "venv" ]; then
if [ ! -d ".venv" ]; then
if ! hash pyvenv 2>/dev/null; then
function pyvenv()
{
@@ -31,13 +31,13 @@ if [ ! -d "venv" ]; then
fi

echo "Making venv for $PROJECT_NAME"
pyvenv venv
. venv/bin/activate
pyvenv .venv
. .venv/bin/activate
python setup.py install
pip install -r requirements.txt
fi

. venv/bin/activate
. .venv/bin/activate

# Let's make sure this is a hubflow enabled repo
yes | git hf init >/dev/null 2>/dev/null
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -65,5 +65,5 @@ atlassian-ide-plugin.xml
pip-selfcheck.json

# Python3 Venv Files
venv/
.venv/
pyvenv.cfg
59 changes: 41 additions & 18 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -4,23 +4,46 @@ language: python
cache: pip
matrix:
include:
- env: TOXENV=isort-check
- env: TOXENV=lint
- python: 2.7
env: TOXENV=py27
- python: 3.4
env: TOXENV=py34
- python: 3.5
env: TOXENV=py35
- python: 3.6
env: TOXENV=py36
- python: 3.7
env: TOXENV=py37
- python: pypy2.7-6.0
env: TOXENV=pypy
- python: pypy3.5-6.0
env: TOXENV=pypy3
- env: TOXENV=isort-check
- env: TOXENV=lint
- os: linux
python: 2.7
env: TOXENV=py27
- os: linux
python: 3.4
env: TOXENV=py34
- os: linux
python: 3.5
env: TOXENV=py35
- os: linux
python: 3.6
env: TOXENV=py36
- os: linux
python: 3.7
env: TOXENV=py37
- os: linux
python: pypy2.7-6.0
env: TOXENV=pypy
- os: linux
python: pypy3.5-6.0
env: TOXENV=pypy3
- os: osx
language: generic
env: TOXENV=py36
before_install:
- "./scripts/before_install.sh"
install:
- pip install tox
- pip install tox
script:
- tox
- tox
deploy:
provider: pypi
user: timothycrosley
distributions: sdist bdist_wheel
skip_existing: true
on:
tags: false
branch: master
condition: "$TOXENV = py37"
password:
secure: SSFcjBL3dhWvSbo21icmnHQFV7mXfv/eDzxrefHUDMk37MWrvtNKchH8zz7wjAsf2PH1VYL1zkEFwnzuzHgs2aFCK7HDUwAaDSIcvPmJg9Oty+o2WQw16m7UnUac9MIZGmBHQaZuUTw0VJpm3GuPSXtdFJwFq3Tk3TIyUipEwg8=
38 changes: 36 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,43 @@
Changelog
=========
### 4.3.9 - Feburary 25, 2019 - hot fix release
### 4.3.15 - March 10, 2019 - hot fix release
- Fixed a regression with handling streaming input from pipes (Issue #895)
- Fixed handling of \x0c whitespace character (Issue #811)
- Improved CLI documentation

### 4.3.14 - March 9, 2019 - hot fix release
- Fixed a regression with */directory/*.py style patterns

### 4.3.13 - March 8, 2019 - hot fix release
- Fixed the inability to accurately determine import section when a mix of conda and virtual environments are used.
- Fixed some output being printed even when --quiet mode is enabled.
- Fixed issue #890 interoperability with PyCharm by allowing case sensitive non type grouped sorting.
- Fixed issue #889 under some circumstances isort will incorrectly add a new line at the beginning of a file.
- Fixed issue #885 many files not being skipped according to set skip settings.
- Fixed issue #842 streaming encoding improvements.

### 4.3.12 - March 6, 2019 - hot fix release
- Fix error caused when virtual environment not detected

### 4.3.11 - March 6, 2019 - hot fix release
- Fixed issue #876: confused by symlinks pointing to virtualenv gives FIRSTPARTY not THIRDPARTY
- Fixed issue #873: current version skips every file on travis
- Additional caching to reduce performance regression introduced in 4.3.5
- Improved handling of pex files and other binary Python files

### 4.3.10 - March 2, 2019 - hot fix release
- Fixed Windows incompatibilities (Issue #835)
- Fixed relative import sorting bug (Issue #417)
- Fixed "no_lines_before" to also be respected from previous empty sections.
- Fixed slow-down introduced by finders mechanism by adding a LRU cache (issue #848)
- Fixed issue #842 default encoding not-set in Python2
- Restored Windows automated testing
- Added Mac automated testing

### 4.3.9 - February 25, 2019 - hot fix release
- Fixed a bug that led to an incompatibility with black: #831

### 4.3.8 - Feburary 25, 2019 - hot fix release
### 4.3.8 - February 25, 2019 - hot fix release
- Fixed a bug that led to the recursive option not always been available from the command line.

### 4.3.7 - February 25, 2019 - hot fix release
3 changes: 3 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
@@ -24,6 +24,9 @@
:alt: Join the chat at https://gitter.im/timothycrosley/isort
:target: https://gitter.im/timothycrosley/isort?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge

.. image:: https://pepy.tech/badge/isort
:alt: Downloads
:target: https://pepy.tech/project/isort

isort your python imports for you so you don't have to.

2 changes: 1 addition & 1 deletion isort/__init__.py
Original file line number Diff line number Diff line change
@@ -25,4 +25,4 @@
from . import settings # noqa: F401
from .isort import SortImports # noqa: F401

__version__ = "4.3.9"
__version__ = "4.3.15"
6 changes: 5 additions & 1 deletion isort/__main__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from __future__ import absolute_import

from isort.main import main
from isort.pie_slice import apply_changes_to_python_environment

apply_changes_to_python_environment()

from isort.main import main # noqa: E402 isort:skip

main()
41 changes: 33 additions & 8 deletions isort/finders.py
Original file line number Diff line number Diff line change
@@ -130,16 +130,13 @@ class PathFinder(BaseFinder):
def __init__(self, config, sections):
super(PathFinder, self).__init__(config, sections)

# Use a copy of sys.path to avoid any unintended modifications
# to it - e.g. `+=` used below will change paths in place and
# if not copied, consequently sys.path, which will grow unbounded
# with duplicates on every call to this method.
self.paths = list(sys.path)
# restore the original import path (i.e. not the path to bin/isort)
self.paths[0] = os.getcwd()
self.paths = [os.getcwd()]

# virtual env
self.virtual_env = self.config.get('virtual_env') or os.environ.get('VIRTUAL_ENV')
if self.virtual_env:
self.virtual_env = os.path.realpath(self.virtual_env)
self.virtual_env_src = False
if self.virtual_env:
self.virtual_env_src = '{0}/src/'.format(self.virtual_env)
@@ -153,6 +150,17 @@ def __init__(self, config, sections):
if os.path.isdir(path):
self.paths.append(path)

# conda
self.conda_env = self.config.get('conda_env') or os.environ.get('CONDA_PREFIX')
if self.conda_env:
self.conda_env = os.path.realpath(self.conda_env)
for path in glob('{0}/lib/python*/site-packages'.format(self.conda_env)):
if path not in self.paths:
self.paths.append(path)
for path in glob('{0}/lib/python*/*/site-packages'.format(self.conda_env)):
if path not in self.paths:
self.paths.append(path)

# handle case-insensitive paths on windows
self.stdlib_lib_prefix = os.path.normcase(sysconfig.get_paths()['stdlib'])
if self.stdlib_lib_prefix not in self.paths:
@@ -161,12 +169,18 @@ def __init__(self, config, sections):
# handle compiled libraries
self.ext_suffix = sysconfig.get_config_var("EXT_SUFFIX") or ".so"

# add system paths
for path in sys.path[1:]:
if path not in self.paths:
self.paths.append(path)

def find(self, module_name):
for prefix in self.paths:
package_path = "/".join((prefix, module_name.split(".")[0]))
is_module = (exists_case_sensitive(package_path + ".py") or
exists_case_sensitive(package_path + ".so") or
exists_case_sensitive(package_path + self.ext_suffix))
exists_case_sensitive(package_path + self.ext_suffix) or
exists_case_sensitive(package_path + "/__init__.py"))
is_package = exists_case_sensitive(package_path) and os.path.isdir(package_path)
if is_module or is_package:
if 'site-packages' in prefix:
@@ -175,6 +189,8 @@ def find(self, module_name):
return self.sections.THIRDPARTY
if self.virtual_env and self.virtual_env_src in prefix:
return self.sections.THIRDPARTY
if self.conda_env and self.conda_env in prefix:
return self.sections.THIRDPARTY
if os.path.normcase(prefix).startswith(self.stdlib_lib_prefix):
return self.sections.STDLIB
return self.config['default_section']
@@ -297,11 +313,20 @@ def _get_files_from_dir_cached(cls, path):
def _get_names(self, path):
"""Load required packages from path to requirements file
"""
return RequirementsFinder._get_names_cached(path)

@classmethod
@lru_cache(maxsize=16)
def _get_names_cached(cls, path):
results = []

with chdir(os.path.dirname(path)):
requirements = parse_requirements(path, session=PipSession())
for req in requirements:
if req.name:
yield req.name
results.append(req.name)

return results


class PipfileFinder(ReqsBaseFinder):
145 changes: 101 additions & 44 deletions isort/isort.py
Original file line number Diff line number Diff line change
@@ -26,6 +26,7 @@
"""
from __future__ import absolute_import, division, print_function, unicode_literals

import codecs
import copy
import io
import itertools
@@ -46,8 +47,9 @@ class SortImports(object):
incorrectly_sorted = False
skipped = False

def __init__(self, file_path=None, file_contents=None, write_to_stdout=False, check=False,
show_diff=False, settings_path=None, ask_to_apply=False, **setting_overrides):
def __init__(self, file_path=None, file_contents=None, file_=None, write_to_stdout=False, check=False,
show_diff=False, settings_path=None, ask_to_apply=False, run_path='', check_skip=True,
**setting_overrides):
if not settings_path and file_path:
settings_path = os.path.dirname(os.path.abspath(file_path))
settings_path = settings_path or os.getcwd()
@@ -93,17 +95,68 @@ def __init__(self, file_path=None, file_contents=None, write_to_stdout=False, ch
self.file_path = file_path or ""
if file_path:
file_path = os.path.abspath(file_path)
if settings.should_skip(file_path, self.config):
self.skipped = True
if self.config['verbose']:
print("WARNING: {0} was skipped as it's listed in 'skip' setting"
" or matches a glob in 'skip_glob' setting".format(file_path))
file_contents = None
elif not file_contents:
self.file_path = file_path
self.file_encoding = coding_check(file_path)
with io.open(file_path, encoding=self.file_encoding, newline='') as file_to_import_sort:
file_contents = file_to_import_sort.read()
if check_skip:
if run_path and file_path.startswith(run_path):
file_name = file_path.replace(run_path, '', 1)
else:
file_name = file_path
run_path = ''

if settings.should_skip(file_name, self.config, run_path):
self.skipped = True
if self.config['verbose']:
print("WARNING: {0} was skipped as it's listed in 'skip' setting"
" or matches a glob in 'skip_glob' setting".format(file_path))
file_contents = None
if not self.skipped and not file_contents:
with io.open(file_path, 'rb') as f:
file_encoding = coding_check(f)
with io.open(file_path, encoding=file_encoding, newline='') as file_to_import_sort:
try:
file_contents = file_to_import_sort.read()
self.file_path = file_path
self.file_encoding = file_encoding
encoding_success = True
except UnicodeDecodeError:
encoding_success = False

if not encoding_success:
with io.open(file_path, newline='') as file_to_import_sort:
try:
file_contents = file_to_import_sort.read()
self.file_path = file_path
self.file_encoding = file_to_import_sort.encoding
except UnicodeDecodeError:
encoding_success = False
file_contents = None
self.skipped = True
if self.config['verbose']:
print("WARNING: {} was skipped as it couldn't be opened with the given "
"{} encoding or {} fallback encoding".format(file_path,
self.file_encoding,
file_to_import_sort.encoding))
elif file_:
try:
file_.seek(0)
self.file_encoding = coding_check(file_)
file_.seek(0)
except io.UnsupportedOperation:
pass
reader = codecs.getreader(self.file_encoding)
file_contents = reader(file_).read()

# try to decode file_contents
if file_contents:
try:
basestring
# python 2
need_decode = (str, bytes)
except NameError:
# python 3
need_decode = bytes

if isinstance(file_contents, need_decode):
file_contents = file_contents.decode(coding_check(file_contents.splitlines()))

if file_contents is None or ("isort:" + "skip_file") in file_contents:
self.skipped = True
@@ -170,8 +223,8 @@ def __init__(self, file_path=None, file_contents=None, write_to_stdout=False, ch
check_output = self.output
check_against = file_contents
if self.config['ignore_whitespace']:
check_output = check_output.replace(self.line_separator, "").replace(" ", "")
check_against = check_against.replace(self.line_separator, "").replace(" ", "")
check_output = check_output.replace(self.line_separator, "").replace(" ", "").replace("\x0c", "")
check_against = check_against.replace(self.line_separator, "").replace(" ", "").replace("\x0c", "")

if check_output.strip() == check_against.strip():
if self.config['verbose']:
@@ -198,7 +251,8 @@ def __init__(self, file_path=None, file_contents=None, write_to_stdout=False, ch
if answer in ('quit', 'q'):
sys.exit(1)
with io.open(self.file_path, encoding=self.file_encoding, mode='w', newline='') as output_file:
print("Fixing {0}".format(self.file_path))
if not self.config['quiet']:
print("Fixing {0}".format(self.file_path))
output_file.write(self.output)

@property
@@ -255,13 +309,10 @@ def _at_end(self):

@staticmethod
def _module_key(module_name, config, sub_imports=False, ignore_case=False, section_name=None):
dots = 0
while module_name.startswith('.'):
dots += 1
module_name = module_name[1:]

if dots:
module_name = '{} {}'.format(('.' * dots), module_name)
match = re.match(r'^(\.+)\s*(.*)', module_name)
if match:
sep = ' ' if config['reverse_relative'] else '_'
module_name = sep.join(match.groups())

prefix = ""
if ignore_case:
@@ -276,7 +327,8 @@ def _module_key(module_name, config, sub_imports=False, ignore_case=False, secti
prefix = "B"
else:
prefix = "C"
module_name = module_name.lower()
if not config['case_sensitive']:
module_name = module_name.lower()
if section_name is None or 'length_sort_' + str(section_name).lower() not in config:
length_sort = config['length_sort']
else:
@@ -525,7 +577,7 @@ def _add_formatted_imports(self):
sections = ('no_sections', )

output = []
prev_section_has_imports = False
pending_lines_before = False
for section in sections:
straight_modules = self.imports[section]['straight']
straight_modules = nsorted(straight_modules, key=lambda key: self._module_key(key, self.config, section_name=section))
@@ -558,8 +610,11 @@ def by_module(line):
line = line.lower()
return '{0}{1}'.format(section, line)
section_output = nsorted(section_output, key=by_module)

section_name = section
no_lines_before = section_name in self.config['no_lines_before']

if section_output:
section_name = section
if section_name in self.place_imports:
self.place_imports[section_name] = section_output
continue
@@ -569,11 +624,16 @@ def by_module(line):
section_comment = "# {0}".format(section_title)
if section_comment not in self.out_lines[0:1] and section_comment not in self.in_lines[0:1]:
section_output.insert(0, section_comment)
if prev_section_has_imports and section_name in self.config['no_lines_before']:
while output and output[-1].strip() == '':
output.pop()
output += section_output + ([''] * self.config['lines_between_sections'])
prev_section_has_imports = bool(section_output)

if pending_lines_before or not no_lines_before:
output += ([''] * self.config['lines_between_sections'])

output += section_output

pending_lines_before = False
else:
pending_lines_before = pending_lines_before or not no_lines_before

while output and output[-1].strip() == '':
output.pop()
while output and output[0].strip() == '':
@@ -957,7 +1017,7 @@ def _parse(self):
'isort:imports-' not in last):
self.comments['above']['straight'].setdefault(module, []).insert(0,
self.out_lines.pop(-1))
if len(self.out_lines) > 0:
if len(self.out_lines) > 0 and len(self.out_lines) != self._first_comment_index_end:
last = self.out_lines[-1].rstrip()
else:
last = ""
@@ -974,19 +1034,16 @@ def _parse(self):
self.imports[placed_module][import_type][module] = None


def coding_check(fname, default='utf-8'):
def coding_check(lines, default='utf-8'):

# see https://www.python.org/dev/peps/pep-0263/
pattern = re.compile(br'coding[:=]\s*([-\w.]+)')

coding = default
with io.open(fname, 'rb') as f:
for line_number, line in enumerate(f, 1):
groups = re.findall(pattern, line)
if groups:
coding = groups[0].decode('ascii')
break
if line_number > 2:
break

return coding
for line_number, line in enumerate(lines, 1):
groups = re.findall(pattern, line)
if groups:
return groups[0].decode('ascii')
if line_number > 2:
break

return default
32 changes: 22 additions & 10 deletions isort/main.py
Original file line number Diff line number Diff line change
@@ -61,6 +61,8 @@ def is_python_file(path):
_root, ext = os.path.splitext(path)
if ext in ('.py', '.pyi'):
return True
if ext in ('.pex', ):
return False

# Skip editor backup files.
if path.endswith('~'):
@@ -97,21 +99,16 @@ def iter_source_code(paths, config, skipped):

for path in paths:
if os.path.isdir(path):
if should_skip(path, config, os.getcwd()):
skipped.append(path)
continue

for dirpath, dirnames, filenames in os.walk(
path, topdown=True, followlinks=True
):
for dirpath, dirnames, filenames in os.walk(path, topdown=True, followlinks=True):
for dirname in list(dirnames):
if should_skip(dirname, config, dirpath):
skipped.append(dirname)
dirnames.remove(dirname)
for filename in filenames:
filepath = os.path.join(dirpath, filename)
if is_python_file(filepath):
if should_skip(filename, config, dirpath):
relative_file = os.path.relpath(filepath, path)
if should_skip(relative_file, config, path):
skipped.append(filename)
else:
yield filepath
@@ -176,7 +173,9 @@ def run(self):

def parse_args(argv=None):
parser = argparse.ArgumentParser(description='Sort Python import definitions alphabetically '
'within logical sections.')
'within logical sections. Run with no arguments to run '
'interactively. Run with `-` as the first argument to read from '
'stdin. Otherwise provide a list of files to sort.')
inline_args_group = parser.add_mutually_exclusive_group()
parser.add_argument('-a', '--add-import', dest='add_imports', action='append',
help='Adds the specified import line to all files, '
@@ -254,6 +253,8 @@ def parse_args(argv=None):
parser.add_argument('-r', dest='ambiguous_r_flag', action='store_true')
parser.add_argument('-rm', '--remove-import', dest='remove_imports', action='append',
help='Removes the specified import from all files.')
parser.add_argument('-rr', '--reverse-relative', dest='reverse_relative', action='store_true',
help='Reverse order of relative imports.')
parser.add_argument('-rc', '--recursive', dest='recursive', action='store_true',
help='Recursively look for Python files of which to sort imports')
parser.add_argument('-s', '--skip', help='Files that sort imports should skip over. If you want to skip multiple '
@@ -278,6 +279,8 @@ def parse_args(argv=None):
help='Shows verbose output, such as when files are skipped or when a check is successful.')
parser.add_argument('--virtual-env', dest='virtual_env',
help='Virtual environment to use for determining whether a package is third-party')
parser.add_argument('--conda-env', dest='conda_env',
help='Conda environment to use for determining whether a package is third-party')
parser.add_argument('-vn', '--version-number', action='version', version=__version__,
help='Returns just the current version number without the logo')
parser.add_argument('-w', '--line-width', help='The max length of an import line (used for wrapping long imports).',
@@ -291,6 +294,8 @@ def parse_args(argv=None):
parser.add_argument('--unsafe', dest='unsafe', action='store_true',
help='Tells isort to look for files in standard library directories, etc. '
'where it may not be safe to operate in')
parser.add_argument('--case-sensitive', dest='case_sensitive', action='store_true',
help='Tells isort to include casing when sorting module names')
parser.add_argument('files', nargs='*', help='One or more Python source files that need their imports sorted.')

arguments = {key: value for key, value in vars(parser.parse_args(argv)).items() if value}
@@ -312,6 +317,7 @@ def main(argv=None):
'-rc for recursive')
sys.exit(1)

arguments['check_skip'] = False
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))
@@ -326,7 +332,13 @@ def main(argv=None):

file_names = arguments.pop('files', [])
if file_names == ['-']:
SortImports(file_contents=sys.stdin.read(), write_to_stdout=True, **arguments)
try:
# python 3
file_ = sys.stdin.buffer
except AttributeError:
# python 2
file_ = sys.stdin
SortImports(file_=file_, write_to_stdout=True, **arguments)
else:
if not file_names:
file_names = ['.']
27 changes: 26 additions & 1 deletion isort/pie_slice.py
Original file line number Diff line number Diff line change
@@ -30,14 +30,39 @@
PY3 = sys.version_info[0] == 3
VERSION = sys.version_info

__all__ = ['PY2', 'PY3', 'lru_cache']
__all__ = ['PY2', 'PY3', 'lru_cache', 'apply_changes_to_python_environment']


if PY3:
input = input

def apply_changes_to_python_environment():
pass
else:
input = raw_input # noqa: F821

python_environment_changes_applied = False

import sys
stdout = sys.stdout
stderr = sys.stderr

def apply_changes_to_python_environment():
global python_environment_changes_applied
if python_environment_changes_applied or sys.getdefaultencoding() == 'utf-8':
python_environment_changes_applied = True
return

try:
reload(sys)
sys.stdout = stdout
sys.stderr = stderr
sys.setdefaultencoding('utf-8')
except NameError: # Python 3
sys.exit('This should not happen!')

python_environment_changes_applied = True


if sys.version_info < (3, 2):
try:
22 changes: 15 additions & 7 deletions isort/settings.py
Original file line number Diff line number Diff line change
@@ -58,7 +58,8 @@
DEFAULT_SECTIONS = ('FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER')

safety_exclude_re = re.compile(
r"/(\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|_build|buck-out|build|dist|lib/python[0-9].[0-9]+)/"
r"/(\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|_build|buck-out|build|dist|\.pants\.d"
r"|lib/python[0-9].[0-9]+)/"
)

WrapModes = ('GRID', 'VERTICAL', 'HANGING_INDENT', 'VERTICAL_HANGING_INDENT', 'VERTICAL_GRID', 'VERTICAL_GRID_GROUPED',
@@ -129,6 +130,7 @@
'length_sort': False,
'add_imports': [],
'remove_imports': [],
'reverse_relative': False,
'force_single_line': False,
'default_section': 'FIRSTPARTY',
'import_heading_future': '',
@@ -160,7 +162,8 @@
'no_lines_before': [],
'no_inline_sort': False,
'ignore_comments': False,
'safety_excludes': True}
'safety_excludes': True,
'case_sensitive': False}


@lru_cache()
@@ -287,7 +290,7 @@ def _get_config_data(file_path, sections):
settings.update(config_section)
else:
warnings.warn(
"Found %s but toml package is not installed. To configure"
"Found %s but toml package is not installed. To configure "
"isort with %s, install with 'isort[pyproject]'." % (file_path, file_path)
)
else:
@@ -316,14 +319,19 @@ def _get_config_data(file_path, sections):


def should_skip(filename, config, path=''):
"""Returns True if the file should be skipped based on the passed in settings."""
"""Returns True if the file and/or folder should be skipped based on the passed in settings."""
os_path = os.path.join(path, filename)

normalized_path = os_path.replace('\\', '/')
if normalized_path[1:2] == ':':
normalized_path = normalized_path[2:]

if config['safety_excludes'] and safety_exclude_re.search(normalized_path):
return True
if path and config['safety_excludes']:
check_exclude = '/' + filename.replace('\\', '/') + '/'
if path and os.path.basename(path) in ('lib', ):
check_exclude = '/' + os.path.basename(path) + check_exclude
if safety_exclude_re.search(check_exclude):
return True

for skip_path in config['skip']:
if posixpath.abspath(normalized_path) == posixpath.abspath(skip_path.replace('\\', '/')):
@@ -336,7 +344,7 @@ def should_skip(filename, config, path=''):
position = os.path.split(position[0])

for glob in config['skip_glob']:
if fnmatch.fnmatch(filename, glob):
if fnmatch.fnmatch(filename, glob) or fnmatch.fnmatch('/' + filename, glob):
return True

if not (os.path.isfile(os_path) or os.path.isdir(os_path) or os.path.islink(os_path)):
28 changes: 28 additions & 0 deletions scripts/before_install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#! /bin/bash

echo $TRAVIS_OS_NAME

if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then

# Travis has an old version of pyenv by default, upgrade it
brew update > /dev/null 2>&1
brew outdated pyenv || brew upgrade pyenv

pyenv --version

# Find the latest requested version of python
case "$TOXENV" in
py34)
python_minor=4;;
py35)
python_minor=5;;
py36)
python_minor=6;;
py37)
python_minor=7;;
esac
latest_version=`pyenv install --list | grep -e "^[ ]*3\.$python_minor" | tail -1`

pyenv install $latest_version
pyenv local $latest_version
fi
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@
readme = f.read()

setup(name='isort',
version='4.3.9',
version='4.3.15',
description='A Python utility / library to sort Python imports.',
long_description=readme,
author='Timothy Crosley',
129 changes: 122 additions & 7 deletions test_isort.py
Original file line number Diff line number Diff line change
@@ -72,8 +72,9 @@ def default_settings_path(tmpdir_factory):
config_file = config_dir.join('.editorconfig').strpath
with open(config_file, 'w') as editorconfig:
editorconfig.write(TEST_DEFAULT_CONFIG)
os.chdir(config_dir.strpath)
return config_dir.strpath

with config_dir.as_cwd():
yield config_dir.strpath


def test_happy_path():
@@ -2241,7 +2242,8 @@ def test_inconsistent_behavior_in_python_2_and_3_issue_479():
"""Test to ensure Python 2 and 3 have the same behavior"""
test_input = ('from future.standard_library import hooks\n'
'from workalendar.europe import UnitedKingdom\n')
assert SortImports(file_contents=test_input).output == test_input
assert SortImports(file_contents=test_input,
known_first_party=["future"]).output == test_input


def test_sort_within_section_comments_issue_436():
@@ -2391,6 +2393,18 @@ def test_not_splitted_sections():
assert SortImports(file_contents=test_input, no_lines_before=['STDLIB']).output == test_input


def test_no_lines_before_empty_section():
test_input = ('import first\n'
'import custom\n')
assert SortImports(
file_contents=test_input,
known_third_party=["first"],
known_custom=["custom"],
sections=['THIRDPARTY', 'LOCALFOLDER', 'CUSTOM'],
no_lines_before=['THIRDPARTY', 'LOCALFOLDER', 'CUSTOM'],
).output == test_input


def test_no_inline_sort():
"""Test to ensure multiple `from` imports in one line are not sorted if `--no-inline-sort` flag
is enabled. If `--force-single-line-imports` flag is enabled, then `--no-inline-sort` is ignored."""
@@ -2723,24 +2737,65 @@ def test_command_line(tmpdir, capfd, multiprocess):
assert str(tmpdir.join("file2.py")) in out


@pytest.mark.parametrize("quiet", (False, True))
def test_quiet(tmpdir, capfd, quiet):
if sys.platform.startswith("win"):
return
from isort.main import main
tmpdir.join("file1.py").write("import re\nimport os")
tmpdir.join("file2.py").write("")
arguments = ["-rc", str(tmpdir)]
if quiet:
arguments.append("-q")
main(arguments)
out, err = capfd.readouterr()
assert not err
assert bool(out) != quiet


@pytest.mark.parametrize('enabled', (False, True))
def test_safety_excludes(tmpdir, enabled):
tmpdir.join("victim.py").write("# ...")
tmpdir.mkdir(".tox").join("verysafe.py").write("# ...")
toxdir = tmpdir.mkdir(".tox")
toxdir.join("verysafe.py").write("# ...")
tmpdir.mkdir("lib").mkdir("python3.7").join("importantsystemlibrary.py").write("# ...")
tmpdir.mkdir(".pants.d").join("pants.py").write("import os")
config = dict(settings.default.copy(), safety_excludes=enabled)
skipped = []
codes = [str(tmpdir)],
main.iter_source_code(codes, config, skipped)

# if enabled files within nested unsafe directories should be skipped
file_names = set(os.path.relpath(f, str(tmpdir)) for f in main.iter_source_code([str(tmpdir)], config, skipped))
if enabled:
assert file_names == {'victim.py'}
assert len(skipped) == 2
assert len(skipped) == 3
else:
assert file_names == {os.sep.join(('.tox', 'verysafe.py')),
os.sep.join(('lib', 'python3.7', 'importantsystemlibrary.py')), 'victim.py'}
os.sep.join(('lib', 'python3.7', 'importantsystemlibrary.py')),
os.sep.join(('.pants.d', 'pants.py')),
'victim.py'}
assert not skipped

# directly pointing to files within unsafe directories shouldn't skip them either way
file_names = set(os.path.relpath(f, str(toxdir)) for f in main.iter_source_code([str(toxdir)], config, skipped))
assert file_names == {'verysafe.py'}


@pytest.mark.parametrize('skip_glob_assert', (([], 0, {os.sep.join(('code', 'file.py'))}), (['**/*.py'], 1, {}),
(['*/code/*.py'], 1, {})))
def test_skip_glob(tmpdir, skip_glob_assert):
skip_glob, skipped_count, file_names = skip_glob_assert
base_dir = tmpdir.mkdir('build')
code_dir = base_dir.mkdir('code')
code_dir.join('file.py').write('import os')

config = dict(settings.default.copy(), skip_glob=skip_glob)
skipped = []
file_names = set(os.path.relpath(f, str(base_dir)) for f in main.iter_source_code([str(base_dir)], config, skipped))
assert len(skipped) == skipped_count
assert file_names == file_names


def test_comments_not_removed_issue_576():
test_input = ('import distutils\n'
@@ -2749,7 +2804,7 @@ def test_comments_not_removed_issue_576():
assert SortImports(file_contents=test_input).output == test_input


def test_inconsistent_relative_imports_issue_577():
def test_reverse_relative_imports_issue_417():
test_input = ('from . import ipsum\n'
'from . import lorem\n'
'from .dolor import consecteur\n'
@@ -2762,6 +2817,24 @@ def test_inconsistent_relative_imports_issue_577():
'from ... import dui\n'
'from ...eu import dignissim\n'
'from ...ex import metus\n')
assert SortImports(file_contents=test_input,
force_single_line=True,
reverse_relative=True).output == test_input


def test_inconsistent_relative_imports_issue_577():
test_input = ('from ... import diam\n'
'from ... import dui\n'
'from ...eu import dignissim\n'
'from ...ex import metus\n'
'from .. import donec\n'
'from .. import euismod\n'
'from ..mi import iaculis\n'
'from ..nec import tempor\n'
'from . import ipsum\n'
'from . import lorem\n'
'from .dolor import consecteur\n'
'from .sit import apidiscing\n')
assert SortImports(file_contents=test_input, force_single_line=True).output == test_input


@@ -2773,3 +2846,45 @@ def test_unwrap_issue_762():
test_input = ('from os.\\\n'
' path import (join, split)')
assert SortImports(file_contents=test_input).output == 'from os.path import join, split\n'


def test_ensure_support_for_non_typed_but_cased_alphabetic_sort_issue_890():
test_input = ('from pkg import BALL\n'
'from pkg import RC\n'
'from pkg import Action\n'
'from pkg import Bacoo\n'
'from pkg import RCNewCode\n'
'from pkg import actual\n'
'from pkg import rc\n'
'from pkg import recorder\n')
expected_output = ('from pkg import Action\n'
'from pkg import BALL\n'
'from pkg import Bacoo\n'
'from pkg import RC\n'
'from pkg import RCNewCode\n'
'from pkg import actual\n'
'from pkg import rc\n'
'from pkg import recorder\n')
assert SortImports(file_contents=test_input, case_sensitive=True, order_by_type=False,
force_single_line=True).output == expected_output


def test_to_ensure_empty_line_not_added_to_file_start_issue_889():
test_input = ('# comment\n'
'import os\n'
'# comment2\n'
'import sys\n')
assert SortImports(file_contents=test_input).output == test_input


def test_to_ensure_correctly_handling_of_whitespace_only_issue_811(capsys):
test_input = ('import os\n'
'import sys\n'
'\n'
'\x0c\n'
'def my_function():\n'
' print("hi")\n')
SortImports(file_contents=test_input, ignore_whitespace=True)
out, err = capsys.readouterr()
assert out == ''
assert err == ''