From 9e07f4fad8438ee51d4aa32917337544fbf8523b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 25 Jun 2019 18:10:30 -0700 Subject: [PATCH 0001/1439] Merge in latest hot fix version bump --- isort/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/__init__.py b/isort/__init__.py index e7913371b..804216cea 100644 --- a/isort/__init__.py +++ b/isort/__init__.py @@ -22,4 +22,4 @@ from . import settings # noqa: F401 from .compat import SortImports # noqa: F401 -__version__ = "4.3.19" +__version__ = "4.3.21" diff --git a/setup.py b/setup.py index 212692e12..7ca16da3c 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ readme = f.read() setup(name='isort', - version='4.3.19', + version='4.3.21', description='A Python utility / library to sort Python imports.', long_description=readme, author='Timothy Crosley', From 24d1970a47855d4d1b3238de0c677e50c8dc48ed Mon Sep 17 00:00:00 2001 From: Timothy Edmund Crosley Date: Wed, 26 Jun 2019 15:51:47 -0700 Subject: [PATCH 0002/1439] Create FUNDING.yml Add tidelift funding badge --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..06b15ff09 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +tidelift: "pypi/isort" From ca6c28c19018d22fc4d184a7009f386d347c5f74 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 26 Jun 2019 16:59:52 -0700 Subject: [PATCH 0003/1439] Add security contact information --- README.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.rst b/README.rst index 1f644c114..1b2b32643 100644 --- a/README.rst +++ b/README.rst @@ -634,6 +634,12 @@ isort themselves, add isort to the setup_requires of your ``setup()`` like so: ] ) +Security contact information +========== + +To report a security vulnerability, please use the +.. Tidelift security contact: ttps://tidelift.com/security. +Tidelift will coordinate the fix and disclosure. Why isort? ========== From f0032290949035a8a694fdb78d78ddde92defc92 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 26 Jun 2019 17:03:47 -0700 Subject: [PATCH 0004/1439] Fix tidelift link --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 1b2b32643..d0bfee6d7 100644 --- a/README.rst +++ b/README.rst @@ -637,8 +637,8 @@ isort themselves, add isort to the setup_requires of your ``setup()`` like so: Security contact information ========== -To report a security vulnerability, please use the -.. Tidelift security contact: ttps://tidelift.com/security. +To report a security vulnerability, please use the Tidelift_security_contact_ +.. Tidelift_security_contact_: https://tidelift.com/security. Tidelift will coordinate the fix and disclosure. Why isort? From 050cc910890ffa91933d29c7cd59e3c905f587e0 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 26 Jun 2019 17:07:01 -0700 Subject: [PATCH 0005/1439] Fix tidelift link --- README.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index d0bfee6d7..3cc0235ba 100644 --- a/README.rst +++ b/README.rst @@ -637,9 +637,10 @@ isort themselves, add isort to the setup_requires of your ``setup()`` like so: Security contact information ========== -To report a security vulnerability, please use the Tidelift_security_contact_ -.. Tidelift_security_contact_: https://tidelift.com/security. -Tidelift will coordinate the fix and disclosure. +To report a security vulnerability, please use the `Tidelift security +contact`_. Tidelift will coordinate the fix and disclosure. + +.. _Tidelift security contact: https://tidelift.com/security Why isort? ========== From b023502b939f01b958574d79293f6c9e46a9ad80 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 26 Jun 2019 17:48:23 -0700 Subject: [PATCH 0006/1439] Add link to tidelift --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index 3cc0235ba..6a4b09e31 100644 --- a/README.rst +++ b/README.rst @@ -33,6 +33,8 @@ isort is a Python utility / library to sort imports alphabetically, and automati It provides a command line utility, Python library and `plugins for various editors `_ to quickly sort all your imports. It requires Python 3.5+ to run but supports formatting Python 2 code too. +`Get professionally supported isort with the Tidelift Subscription `_ + .. image:: https://raw.github.com/timothycrosley/isort/develop/example.gif :alt: Example Usage From ee1186d4e222b9829881debc7138cc6dd9c6b3c6 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 26 Jun 2019 18:01:48 -0700 Subject: [PATCH 0007/1439] Add additional information about Tidelift subscription --- README.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.rst b/README.rst index 6a4b09e31..a74ea8513 100644 --- a/README.rst +++ b/README.rst @@ -33,8 +33,25 @@ isort is a Python utility / library to sort imports alphabetically, and automati It provides a command line utility, Python library and `plugins for various editors `_ to quickly sort all your imports. It requires Python 3.5+ to run but supports formatting Python 2 code too. + +######## + +.. image:: https://nedbatchelder.com/pix/Tidelift_Logos_RGB_Tidelift_Shorthand_On-White_small.png + :width: 75 + :alt: Tidelift + `Get professionally supported isort with the Tidelift Subscription `_ +Professional support for isort is available as part of the `Tidelift +Subscription`_. Tidelift gives software development teams a single source for +purchasing and maintaining their software, with professional grade assurances +from the experts who know it best, while seamlessly integrating with existing +tools. + +.. _Tidelift Subscription: https://tidelift.com/subscription/pkg/pypi-isort?utm_source=pypi-isort&utm_medium=referral&utm_campaign=readme + +######## + .. image:: https://raw.github.com/timothycrosley/isort/develop/example.gif :alt: Example Usage From a1c82f26896f117ac511b16a999fe5b3212f5f9a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 26 Jun 2019 18:03:40 -0700 Subject: [PATCH 0008/1439] Final notification about tidelift support --- README.rst | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.rst b/README.rst index a74ea8513..abc493345 100644 --- a/README.rst +++ b/README.rst @@ -36,10 +36,6 @@ It requires Python 3.5+ to run but supports formatting Python 2 code too. ######## -.. image:: https://nedbatchelder.com/pix/Tidelift_Logos_RGB_Tidelift_Shorthand_On-White_small.png - :width: 75 - :alt: Tidelift - `Get professionally supported isort with the Tidelift Subscription `_ Professional support for isort is available as part of the `Tidelift From 7e04c5485cccce02b7e0893a939ba9cbe98c29be Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 3 Jul 2019 11:46:02 -0700 Subject: [PATCH 0009/1439] Pin tomlkit version for testing --- requirements.txt | 1 + tox.ini | 1 + 2 files changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index f539487a2..c24a72e32 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ pipfile pyproject requirementslib pipreqs +tomlkit==0.5.3 diff --git a/tox.ini b/tox.ini index 1a0fd0e0a..cb1c2f102 100644 --- a/tox.ini +++ b/tox.ini @@ -11,6 +11,7 @@ deps = pytest coverage: pytest-cov pip==19.1.1 + tomlkit==0.5.3 extras = pipfile pyproject From 47571635eaa44ee6a110139c771df3bf322e54c3 Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Wed, 3 Jul 2019 20:25:42 +1000 Subject: [PATCH 0010/1439] Fix up typo, replace resovled with resolved --- test_isort.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_isort.py b/test_isort.py index 088f4f1ae..86c7d8688 100644 --- a/test_isort.py +++ b/test_isort.py @@ -1965,7 +1965,7 @@ def test_forced_sepatate_globs(): def test_no_additional_lines_issue_358(): - """Test to ensure issue 358 is resovled and running isort multiple times does not add extra newlines""" + """Test to ensure issue 358 is resolved and running isort multiple times does not add extra newlines""" test_input = ('"""This is a docstring"""\n' '# This is a comment\n' 'from __future__ import (\n' From af012ef18e4680f2b3780d36bca9e3f25fa19a60 Mon Sep 17 00:00:00 2001 From: Mateo D'Agaro Date: Thu, 8 Aug 2019 23:07:38 -0700 Subject: [PATCH 0011/1439] Issue #960: Respect .gitignore Add flag -git/--exclude-gitignore to ignore files found in a .gitignore. --- isort/main.py | 2 ++ isort/settings.py | 9 ++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/isort/main.py b/isort/main.py index 8d5e2353f..e3b6d89ed 100644 --- a/isort/main.py +++ b/isort/main.py @@ -217,6 +217,8 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: 'length') parser.add_argument('-fss', '--force-sort-within-sections', action='store_true', dest="force_sort_within_sections", help='Force imports to be sorted by module, independent of import_type') + parser.add_argument('-git', '--exclude-gitignore', action='store_true', dest="exclude_gitignore", + help='Treat project as a git respository and ignore files listed in .gitignore') parser.add_argument('-i', '--indent', help='String to place for indents defaults to " " (4 spaces).', dest='indent', type=str) parser.add_argument('-j', '--jobs', help='Number of files to process in parallel.', diff --git a/isort/settings.py b/isort/settings.py index 9ab88f65b..66b631a46 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -28,6 +28,7 @@ import os import posixpath import re +import subprocess import warnings from distutils.util import strtobool from functools import lru_cache @@ -169,7 +170,8 @@ def from_string(value: str) -> 'WrapModes': 'no_inline_sort': False, 'ignore_comments': False, 'safety_excludes': True, - 'case_sensitive': False} + 'case_sensitive': False, + 'exclude_gitignore': False} @lru_cache() @@ -401,6 +403,11 @@ def file_should_be_skipped( if posixpath.abspath(normalized_path) == posixpath.abspath(skip_path.replace('\\', '/')): return True + if config['exclude_gitignore']: + result = subprocess.run(['git', 'check-ignore', '--quiet', filename]) + if result.returncode == 0: + return True + position = os.path.split(filename) while position[1]: if position[1] in config['skip']: From 41974d262af659b7482ec916d6608603852004f7 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Thu, 8 Aug 2019 09:56:54 -0700 Subject: [PATCH 0012/1439] Add NPM node_modules to default exclusion list Projects frequently have Python and JavaScript coexisting. NPM is the de facto standard package manager for JavaScript. Ignore third party packages by default so projects don't need configure this. --- isort/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/settings.py b/isort/settings.py index 9ab88f65b..b4d3911a9 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -53,7 +53,7 @@ safety_exclude_re = re.compile( r"/(\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|_build|buck-out|build|dist|\.pants\.d" - r"|lib/python[0-9].[0-9]+)/" + r"|lib/python[0-9].[0-9]+|node_modules)/" ) From 8924fe26bdf3cc6c1bb118c331ba9ac96ab2f3d1 Mon Sep 17 00:00:00 2001 From: jaswanth098 Date: Mon, 5 Aug 2019 12:42:13 +0530 Subject: [PATCH 0013/1439] bug fix type int for wrap length --- isort/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/main.py b/isort/main.py index 8d5e2353f..e603b4199 100644 --- a/isort/main.py +++ b/isort/main.py @@ -284,7 +284,7 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: 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).', dest='line_length', type=int) - parser.add_argument('-wl', '--wrap-length', dest='wrap_length', + parser.add_argument('-wl', '--wrap-length', dest='wrap_length', type=int, help="Specifies how long lines that are wrapped should be, if not set line_length is used.") parser.add_argument('-ws', '--ignore-whitespace', action='store_true', dest="ignore_whitespace", help='Tells isort to ignore whitespace differences when --check-only is being used.') From cec922efa020adeeba3196a48327295e08e66acb Mon Sep 17 00:00:00 2001 From: Timothy Edmund Crosley Date: Wed, 14 Aug 2019 01:44:42 -0700 Subject: [PATCH 0014/1439] Update ACKNOWLEDGEMENTS.md Add @jaswanth098 to acknowledgements --- ACKNOWLEDGEMENTS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ACKNOWLEDGEMENTS.md b/ACKNOWLEDGEMENTS.md index 432862b7a..971ee83c6 100644 --- a/ACKNOWLEDGEMENTS.md +++ b/ACKNOWLEDGEMENTS.md @@ -80,6 +80,7 @@ Code Contributors - Brian F. Baron (@briabar) - Madison Caldwell (@madirey) - Matt Yule-Bennett (@mattbennett) +- Jaswanth Kumar (@jaswanth098) Documenters =================== From 572b98c737e6cd666409bc397a1f6a7b89d4bdee Mon Sep 17 00:00:00 2001 From: Zarathustra2 Date: Thu, 25 Jul 2019 15:58:14 +0200 Subject: [PATCH 0015/1439] Identify running python version for correct standard library list --- isort/settings.py | 90 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 78 insertions(+), 12 deletions(-) diff --git a/isort/settings.py b/isort/settings.py index b4d3911a9..fd57605c9 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -28,6 +28,7 @@ import os import posixpath import re +import sys import warnings from distutils.util import strtobool from functools import lru_cache @@ -72,17 +73,7 @@ def from_string(value: str) -> 'WrapModes': return getattr(WrapModes, str(value), None) or WrapModes(int(value)) -# Note that none of these lists must be complete as they are simply fallbacks for when included auto-detection fails. -default = {'force_to_top': [], - 'skip': [], - 'skip_glob': [], - 'line_length': 79, - 'wrap_length': 0, - 'line_ending': None, - 'sections': DEFAULT_SECTIONS, - 'no_sections': False, - 'known_future_library': ['__future__'], - 'known_standard_library': ['AL', 'BaseHTTPServer', 'Bastion', 'CGIHTTPServer', 'Carbon', 'ColorPicker', +_known_standard_library_3 = ['AL', 'BaseHTTPServer', 'Bastion', 'CGIHTTPServer', 'Carbon', 'ColorPicker', 'ConfigParser', 'Cookie', 'DEVICE', 'DocXMLRPCServer', 'EasyDialogs', 'FL', 'FrameWork', 'GL', 'HTMLParser', 'MacOS', 'MimeWriter', 'MiniAEFrame', 'Nav', 'PixMapWrapper', 'Queue', 'SUNAUDIODEV', 'ScrolledText', 'SimpleHTTPServer', @@ -126,7 +117,82 @@ def from_string(value: str) -> 'WrapModes': 'urlparse', 'usercustomize', 'uu', 'uuid', 'venv', 'videoreader', 'warnings', 'wave', 'weakref', 'webbrowser', 'whichdb', 'winreg', 'winsound', 'wsgiref', 'xdrlib', 'xml', 'xmlrpc', 'xmlrpclib', 'zipapp', 'zipfile', - 'zipimport', 'zlib'], + 'zipimport', 'zlib'] +_known_standard_library_2 = ['AL', 'BaseHTTPServer', 'Bastion', 'CGIHTTPServer', 'Carbon', 'ColorPicker', + 'ConfigParser', 'Cookie', 'DEVICE', 'DocXMLRPCServer', 'EasyDialogs', 'FL', + 'FrameWork', 'GL', 'HTMLParser', 'MacOS', 'MimeWriter', 'MiniAEFrame', 'Nav', + 'PixMapWrapper', 'Queue', 'SUNAUDIODEV', 'ScrolledText', 'SimpleHTTPServer', + 'SimpleXMLRPCServer', 'SocketServer', 'StringIO', 'Tix', 'Tkinter', 'UserDict', + 'UserList', 'UserString', 'W', '__builtin__', 'abc', 'aepack', 'aetools', + 'aetypes', 'aifc', 'al', 'anydbm', 'applesingle', 'argparse', 'array', 'ast', + 'asynchat', 'asyncio', 'asyncore', 'atexit', 'audioop', 'autoGIL', 'base64', + 'bdb', 'binascii', 'binhex', 'bisect', 'bsddb', 'buildtools', 'builtins', + 'bz2', 'cPickle', 'cProfile', 'cStringIO', 'calendar', 'cd', 'cfmfile', 'cgi', + 'cgitb', 'chunk', 'cmath', 'cmd', 'code', 'codecs', 'codeop', 'collections', + 'colorsys', 'commands', 'compileall', 'compiler', 'concurrent', 'configparser', + 'contextlib', 'contextvars', 'cookielib', 'copy', 'copy_reg', 'copyreg', 'crypt', 'csv', + 'ctypes', 'curses', 'dataclasses', 'datetime', 'dbhash', 'dbm', 'decimal', 'difflib', + 'dircache', 'dis', 'distutils', 'dl', 'doctest', 'dumbdbm', 'dummy_thread', + 'dummy_threading', 'email', 'encodings', 'ensurepip', 'enum', 'errno', + 'exceptions', 'faulthandler', 'fcntl', 'filecmp', 'fileinput', 'findertools', + 'fl', 'flp', 'fm', 'fnmatch', 'formatter', 'fpectl', 'fpformat', 'fractions', + 'ftplib', 'functools', 'future_builtins', 'gc', 'gdbm', 'gensuitemodule', + 'getopt', 'getpass', 'gettext', 'gl', 'glob', 'grp', 'gzip', 'hashlib', + 'heapq', 'hmac', 'hotshot', 'html', 'htmlentitydefs', 'htmllib', 'http', + 'httplib', 'ic', 'icopen', 'imageop', 'imaplib', 'imgfile', 'imghdr', 'imp', + 'importlib', 'imputil', 'inspect', 'io', 'ipaddress', 'itertools', 'jpeg', + 'json', 'keyword', 'lib2to3', 'linecache', 'locale', 'logging', 'lzma', + 'macerrors', 'macostools', 'macpath', 'macresource', 'mailbox', 'mailcap', + 'marshal', 'math', 'md5', 'mhlib', 'mimetools', 'mimetypes', 'mimify', 'mmap', + 'modulefinder', 'msilib', 'msvcrt', 'multifile', 'multiprocessing', 'mutex', + 'netrc', 'new', 'nis', 'nntplib', 'numbers', 'operator', 'optparse', 'os', + 'ossaudiodev', 'parser', 'pathlib', 'pdb', 'pickle', 'pickletools', 'pipes', + 'pkgutil', 'platform', 'plistlib', 'popen2', 'poplib', 'posix', 'posixfile', + 'pprint', 'profile', 'pstats', 'pty', 'pwd', 'py_compile', 'pyclbr', 'pydoc', + 'queue', 'quopri', 'random', 're', 'readline', 'reprlib', 'resource', 'rexec', + 'rfc822', 'rlcompleter', 'robotparser', 'runpy', 'sched', 'secrets', 'select', + 'selectors', 'sets', 'sgmllib', 'sha', 'shelve', 'shlex', 'shutil', 'signal', + 'site', 'sitecustomize', 'smtpd', 'smtplib', 'sndhdr', 'socket', 'socketserver', + 'spwd', 'sqlite3', 'ssl', 'stat', 'statistics', 'statvfs', 'string', 'stringprep', + 'struct', 'subprocess', 'sunau', 'sunaudiodev', 'symbol', 'symtable', 'sys', + 'sysconfig', 'syslog', 'tabnanny', 'tarfile', 'telnetlib', 'tempfile', 'termios', + 'test', 'textwrap', 'this', 'thread', 'threading', 'time', 'timeit', 'tkinter', + 'token', 'tokenize', 'trace', 'traceback', 'tracemalloc', 'ttk', 'tty', 'turtle', + 'turtledemo', 'types', 'typing', 'unicodedata', 'unittest', 'urllib', 'urllib2', + 'urlparse', 'user', 'usercustomize', 'uu', 'uuid', 'venv', 'videoreader', + 'warnings', 'wave', 'weakref', 'webbrowser', 'whichdb', 'winreg', 'winsound', + 'wsgiref', 'xdrlib', 'xml', 'xmlrpc', 'xmlrpclib', 'zipapp', 'zipfile', + 'zipimport', 'zlib'] + + +def _get_known_standard_library() -> list: + """ + Returns the correct standard library based on the python interpreter + + See Issue 889 and 778 for more information + """ + + major, minor = sys.version_info[0:2] + if major == 3: + return _known_standard_library_3 + elif major == 2 and minor == 7: + return _known_standard_library_2 + + # ToDo: Raise Exception otherwise ? + # ToDo: Add Test Cases + + +# Note that none of these lists must be complete as they are simply fallbacks for when included auto-detection fails. +default = {'force_to_top': [], + 'skip': [], + 'skip_glob': [], + 'line_length': 79, + 'wrap_length': 0, + 'line_ending': None, + 'sections': DEFAULT_SECTIONS, + 'no_sections': False, + 'known_future_library': ['__future__'], + 'known_standard_library': _get_known_standard_library(), 'known_third_party': ['google.appengine.api'], 'known_first_party': [], 'multi_line_output': WrapModes.GRID, From 23f7653d9e949e406dc07773cea65a252177609d Mon Sep 17 00:00:00 2001 From: Zarathustra2 Date: Fri, 26 Jul 2019 12:14:23 +0200 Subject: [PATCH 0016/1439] Added python-version flag and first test case --- isort/main.py | 3 +++ isort/settings.py | 46 ++++++++++++++++++++++++++++++++++------------ test_isort.py | 21 +++++++++++++++++++++ 3 files changed, 58 insertions(+), 12 deletions(-) diff --git a/isort/main.py b/isort/main.py index e603b4199..64bf2f473 100644 --- a/isort/main.py +++ b/isort/main.py @@ -298,6 +298,9 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: parser.add_argument('--filter-files', dest='filter_files', action='store_true', help='Tells isort to filter files even when they are explicitly passed in as part of the command') parser.add_argument('files', nargs='*', help='One or more Python source files that need their imports sorted.') + parser.add_argument('-py', '--python-version', action='store', dest='py_version', + help='Tells isort to sort the standard library based on the python version. ' + 'Default is the version of the running interpreter, for instance: -py 3, -py 2.7') arguments = {key: value for key, value in vars(parser.parse_args(argv)).items() if value} if 'dont_order_by_type' in arguments: diff --git a/isort/settings.py b/isort/settings.py index fd57605c9..83bcd207b 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -33,7 +33,7 @@ from distutils.util import strtobool from functools import lru_cache from pathlib import Path -from typing import Any, Callable, Dict, Iterable, List, Mapping, MutableMapping, Union +from typing import Any, Callable, Dict, Iterable, List, Mapping, MutableMapping, Union, Optional from .utils import difference, union @@ -73,6 +73,7 @@ def from_string(value: str) -> 'WrapModes': return getattr(WrapModes, str(value), None) or WrapModes(int(value)) +# ToDo: save data in txt files? _known_standard_library_3 = ['AL', 'BaseHTTPServer', 'Bastion', 'CGIHTTPServer', 'Carbon', 'ColorPicker', 'ConfigParser', 'Cookie', 'DEVICE', 'DocXMLRPCServer', 'EasyDialogs', 'FL', 'FrameWork', 'GL', 'HTMLParser', 'MacOS', 'MimeWriter', 'MiniAEFrame', 'Nav', @@ -165,21 +166,42 @@ def from_string(value: str) -> 'WrapModes': 'zipimport', 'zlib'] -def _get_known_standard_library() -> list: +def _get_default(py_version: Optional[str]) -> dict: """ - Returns the correct standard library based on the python interpreter + Returns the correct standard library based on either the passed py_version flag or the python interpreter See Issue 889 and 778 for more information """ - major, minor = sys.version_info[0:2] + if py_version is None: + major, minor = sys.version_info[0:2] + else: + minor = 0 + + # we have a minor + if "." in py_version: + # we do not care about patches just major and minor + splits = py_version.split(".")[0:2] + major = int(splits[0]) + minor = int(splits[1]) + else: + major = int(py_version) + + _default = default.copy() + if major == 3: - return _known_standard_library_3 + standard_library = _known_standard_library_3 elif major == 2 and minor == 7: - return _known_standard_library_2 + standard_library = _known_standard_library_2 + else: + raise ValueError( + "The python version %s is not supported. " + "You can set a python version with the -py or --python-version flag " % py_version + ) + + _default['known_standard_library'] = standard_library - # ToDo: Raise Exception otherwise ? - # ToDo: Add Test Cases + return _default # Note that none of these lists must be complete as they are simply fallbacks for when included auto-detection fails. @@ -192,7 +214,6 @@ def _get_known_standard_library() -> list: 'sections': DEFAULT_SECTIONS, 'no_sections': False, 'known_future_library': ['__future__'], - 'known_standard_library': _get_known_standard_library(), 'known_third_party': ['google.appengine.api'], 'known_first_party': [], 'multi_line_output': WrapModes.GRID, @@ -239,8 +260,8 @@ def _get_known_standard_library() -> list: @lru_cache() -def from_path(path: Union[str, Path]) -> Dict[str, Any]: - computed_settings = default.copy() +def from_path(path: Union[str, Path], py_version: Optional[str] = None) -> Dict[str, Any]: + computed_settings = _get_default(py_version) isort_defaults = ['~/.isort.cfg'] if appdirs: isort_defaults = [appdirs.user_config_dir('isort.cfg')] + isort_defaults @@ -257,7 +278,8 @@ def from_path(path: Union[str, Path]) -> Dict[str, Any]: def prepare_config(settings_path: Path, **setting_overrides: Any) -> Dict[str, Any]: - config = from_path(settings_path).copy() + py_version = setting_overrides.pop("py_version", None) + config = from_path(settings_path, py_version).copy() for key, value in setting_overrides.items(): access_key = key.replace('not_', '').lower() # The sections config needs to retain order and can't be converted to a set. diff --git a/test_isort.py b/test_isort.py index 86c7d8688..dd30505ad 100644 --- a/test_isort.py +++ b/test_isort.py @@ -3352,3 +3352,24 @@ def test_pyi_formatting_issue_942(tmpdir): source_pyi = tmpdir.join('source.pyi') source_pyi.write(test_input) assert SortImports(file_path=str(source_pyi)).output.splitlines() == expected_pyi_output + + +def test_python_version(): + from isort.main import parse_args + + # test that the py_version can be added as flag + args = parse_args(['-py=2.7']) + assert args["py_version"] == "2.7" + + args = parse_args(['--python-version=3']) + assert args["py_version"] == "3" + + test_input = ('import os\n' + '\n' + 'import user\n') + assert SortImports(file_contents=test_input, py_version="3").output == test_input + + # user is part of the standard library in python 2 + output_python_2 = ('import os\n' + 'import user\n') + assert SortImports(file_contents=test_input, py_version="2.7").output == output_python_2 From de344acf3603fcf952341ac963c0e64eca7cbce0 Mon Sep 17 00:00:00 2001 From: Zarathustra2 Date: Fri, 26 Jul 2019 12:22:35 +0200 Subject: [PATCH 0017/1439] Fixed import ordering --- isort/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/settings.py b/isort/settings.py index 83bcd207b..521056ec2 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -33,7 +33,7 @@ from distutils.util import strtobool from functools import lru_cache from pathlib import Path -from typing import Any, Callable, Dict, Iterable, List, Mapping, MutableMapping, Union, Optional +from typing import Any, Callable, Dict, Iterable, List, Mapping, MutableMapping, Optional, Union from .utils import difference, union From 30d542dce3051a1b59142b2c53d205180bd94910 Mon Sep 17 00:00:00 2001 From: Zarathustra2 Date: Sat, 27 Jul 2019 23:21:50 +0200 Subject: [PATCH 0018/1439] Python files with standard libs --- isort/settings.py | 100 ++------------------------------------ isort/stdlibs/py_three.py | 52 ++++++++++++++++++++ isort/stdlibs/py_two.py | 49 +++++++++++++++++++ 3 files changed, 106 insertions(+), 95 deletions(-) create mode 100644 isort/stdlibs/py_three.py create mode 100644 isort/stdlibs/py_two.py diff --git a/isort/settings.py b/isort/settings.py index 521056ec2..88b125ddc 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -35,6 +35,9 @@ from pathlib import Path from typing import Any, Callable, Dict, Iterable, List, Mapping, MutableMapping, Optional, Union +from isort.stdlibs.py_three import standard_library_3 +from isort.stdlibs.py_two import standard_library_2 + from .utils import difference, union try: @@ -73,99 +76,6 @@ def from_string(value: str) -> 'WrapModes': return getattr(WrapModes, str(value), None) or WrapModes(int(value)) -# ToDo: save data in txt files? -_known_standard_library_3 = ['AL', 'BaseHTTPServer', 'Bastion', 'CGIHTTPServer', 'Carbon', 'ColorPicker', - 'ConfigParser', 'Cookie', 'DEVICE', 'DocXMLRPCServer', 'EasyDialogs', 'FL', - 'FrameWork', 'GL', 'HTMLParser', 'MacOS', 'MimeWriter', 'MiniAEFrame', 'Nav', - 'PixMapWrapper', 'Queue', 'SUNAUDIODEV', 'ScrolledText', 'SimpleHTTPServer', - 'SimpleXMLRPCServer', 'SocketServer', 'StringIO', 'Tix', 'Tkinter', 'UserDict', - 'UserList', 'UserString', 'W', '__builtin__', 'abc', 'aepack', 'aetools', - 'aetypes', 'aifc', 'al', 'anydbm', 'applesingle', 'argparse', 'array', 'ast', - 'asynchat', 'asyncio', 'asyncore', 'atexit', 'audioop', 'autoGIL', 'base64', - 'bdb', 'binascii', 'binhex', 'bisect', 'bsddb', 'buildtools', 'builtins', - 'bz2', 'cPickle', 'cProfile', 'cStringIO', 'calendar', 'cd', 'cfmfile', 'cgi', - 'cgitb', 'chunk', 'cmath', 'cmd', 'code', 'codecs', 'codeop', 'collections', - 'colorsys', 'commands', 'compileall', 'compiler', 'concurrent', 'configparser', - 'contextlib', 'contextvars', 'cookielib', 'copy', 'copy_reg', 'copyreg', 'crypt', 'csv', - 'ctypes', 'curses', 'dataclasses', 'datetime', 'dbhash', 'dbm', 'decimal', 'difflib', - 'dircache', 'dis', 'distutils', 'dl', 'doctest', 'dumbdbm', 'dummy_thread', - 'dummy_threading', 'email', 'encodings', 'ensurepip', 'enum', 'errno', - 'exceptions', 'faulthandler', 'fcntl', 'filecmp', 'fileinput', 'findertools', - 'fl', 'flp', 'fm', 'fnmatch', 'formatter', 'fpectl', 'fpformat', 'fractions', - 'ftplib', 'functools', 'future_builtins', 'gc', 'gdbm', 'gensuitemodule', - 'getopt', 'getpass', 'gettext', 'gl', 'glob', 'grp', 'gzip', 'hashlib', - 'heapq', 'hmac', 'hotshot', 'html', 'htmlentitydefs', 'htmllib', 'http', - 'httplib', 'ic', 'icopen', 'imageop', 'imaplib', 'imgfile', 'imghdr', 'imp', - 'importlib', 'imputil', 'inspect', 'io', 'ipaddress', 'itertools', 'jpeg', - 'json', 'keyword', 'lib2to3', 'linecache', 'locale', 'logging', 'lzma', - 'macerrors', 'macostools', 'macpath', 'macresource', 'mailbox', 'mailcap', - 'marshal', 'math', 'md5', 'mhlib', 'mimetools', 'mimetypes', 'mimify', 'mmap', - 'modulefinder', 'msilib', 'msvcrt', 'multifile', 'multiprocessing', 'mutex', - 'netrc', 'new', 'nis', 'nntplib', 'numbers', 'operator', 'optparse', 'os', - 'ossaudiodev', 'parser', 'pathlib', 'pdb', 'pickle', 'pickletools', 'pipes', - 'pkgutil', 'platform', 'plistlib', 'popen2', 'poplib', 'posix', 'posixfile', - 'pprint', 'profile', 'pstats', 'pty', 'pwd', 'py_compile', 'pyclbr', 'pydoc', - 'queue', 'quopri', 'random', 're', 'readline', 'reprlib', 'resource', 'rexec', - 'rfc822', 'rlcompleter', 'robotparser', 'runpy', 'sched', 'secrets', 'select', - 'selectors', 'sets', 'sgmllib', 'sha', 'shelve', 'shlex', 'shutil', 'signal', - 'site', 'sitecustomize', 'smtpd', 'smtplib', 'sndhdr', 'socket', 'socketserver', - 'spwd', 'sqlite3', 'ssl', 'stat', 'statistics', 'statvfs', 'string', 'stringprep', - 'struct', 'subprocess', 'sunau', 'sunaudiodev', 'symbol', 'symtable', 'sys', - 'sysconfig', 'syslog', 'tabnanny', 'tarfile', 'telnetlib', 'tempfile', 'termios', - 'test', 'textwrap', 'this', 'thread', 'threading', 'time', 'timeit', 'tkinter', - 'token', 'tokenize', 'trace', 'traceback', 'tracemalloc', 'ttk', 'tty', 'turtle', - 'turtledemo', 'types', 'typing', 'unicodedata', 'unittest', 'urllib', 'urllib2', - 'urlparse', 'usercustomize', 'uu', 'uuid', 'venv', 'videoreader', - 'warnings', 'wave', 'weakref', 'webbrowser', 'whichdb', 'winreg', 'winsound', - 'wsgiref', 'xdrlib', 'xml', 'xmlrpc', 'xmlrpclib', 'zipapp', 'zipfile', - 'zipimport', 'zlib'] -_known_standard_library_2 = ['AL', 'BaseHTTPServer', 'Bastion', 'CGIHTTPServer', 'Carbon', 'ColorPicker', - 'ConfigParser', 'Cookie', 'DEVICE', 'DocXMLRPCServer', 'EasyDialogs', 'FL', - 'FrameWork', 'GL', 'HTMLParser', 'MacOS', 'MimeWriter', 'MiniAEFrame', 'Nav', - 'PixMapWrapper', 'Queue', 'SUNAUDIODEV', 'ScrolledText', 'SimpleHTTPServer', - 'SimpleXMLRPCServer', 'SocketServer', 'StringIO', 'Tix', 'Tkinter', 'UserDict', - 'UserList', 'UserString', 'W', '__builtin__', 'abc', 'aepack', 'aetools', - 'aetypes', 'aifc', 'al', 'anydbm', 'applesingle', 'argparse', 'array', 'ast', - 'asynchat', 'asyncio', 'asyncore', 'atexit', 'audioop', 'autoGIL', 'base64', - 'bdb', 'binascii', 'binhex', 'bisect', 'bsddb', 'buildtools', 'builtins', - 'bz2', 'cPickle', 'cProfile', 'cStringIO', 'calendar', 'cd', 'cfmfile', 'cgi', - 'cgitb', 'chunk', 'cmath', 'cmd', 'code', 'codecs', 'codeop', 'collections', - 'colorsys', 'commands', 'compileall', 'compiler', 'concurrent', 'configparser', - 'contextlib', 'contextvars', 'cookielib', 'copy', 'copy_reg', 'copyreg', 'crypt', 'csv', - 'ctypes', 'curses', 'dataclasses', 'datetime', 'dbhash', 'dbm', 'decimal', 'difflib', - 'dircache', 'dis', 'distutils', 'dl', 'doctest', 'dumbdbm', 'dummy_thread', - 'dummy_threading', 'email', 'encodings', 'ensurepip', 'enum', 'errno', - 'exceptions', 'faulthandler', 'fcntl', 'filecmp', 'fileinput', 'findertools', - 'fl', 'flp', 'fm', 'fnmatch', 'formatter', 'fpectl', 'fpformat', 'fractions', - 'ftplib', 'functools', 'future_builtins', 'gc', 'gdbm', 'gensuitemodule', - 'getopt', 'getpass', 'gettext', 'gl', 'glob', 'grp', 'gzip', 'hashlib', - 'heapq', 'hmac', 'hotshot', 'html', 'htmlentitydefs', 'htmllib', 'http', - 'httplib', 'ic', 'icopen', 'imageop', 'imaplib', 'imgfile', 'imghdr', 'imp', - 'importlib', 'imputil', 'inspect', 'io', 'ipaddress', 'itertools', 'jpeg', - 'json', 'keyword', 'lib2to3', 'linecache', 'locale', 'logging', 'lzma', - 'macerrors', 'macostools', 'macpath', 'macresource', 'mailbox', 'mailcap', - 'marshal', 'math', 'md5', 'mhlib', 'mimetools', 'mimetypes', 'mimify', 'mmap', - 'modulefinder', 'msilib', 'msvcrt', 'multifile', 'multiprocessing', 'mutex', - 'netrc', 'new', 'nis', 'nntplib', 'numbers', 'operator', 'optparse', 'os', - 'ossaudiodev', 'parser', 'pathlib', 'pdb', 'pickle', 'pickletools', 'pipes', - 'pkgutil', 'platform', 'plistlib', 'popen2', 'poplib', 'posix', 'posixfile', - 'pprint', 'profile', 'pstats', 'pty', 'pwd', 'py_compile', 'pyclbr', 'pydoc', - 'queue', 'quopri', 'random', 're', 'readline', 'reprlib', 'resource', 'rexec', - 'rfc822', 'rlcompleter', 'robotparser', 'runpy', 'sched', 'secrets', 'select', - 'selectors', 'sets', 'sgmllib', 'sha', 'shelve', 'shlex', 'shutil', 'signal', - 'site', 'sitecustomize', 'smtpd', 'smtplib', 'sndhdr', 'socket', 'socketserver', - 'spwd', 'sqlite3', 'ssl', 'stat', 'statistics', 'statvfs', 'string', 'stringprep', - 'struct', 'subprocess', 'sunau', 'sunaudiodev', 'symbol', 'symtable', 'sys', - 'sysconfig', 'syslog', 'tabnanny', 'tarfile', 'telnetlib', 'tempfile', 'termios', - 'test', 'textwrap', 'this', 'thread', 'threading', 'time', 'timeit', 'tkinter', - 'token', 'tokenize', 'trace', 'traceback', 'tracemalloc', 'ttk', 'tty', 'turtle', - 'turtledemo', 'types', 'typing', 'unicodedata', 'unittest', 'urllib', 'urllib2', - 'urlparse', 'user', 'usercustomize', 'uu', 'uuid', 'venv', 'videoreader', - 'warnings', 'wave', 'weakref', 'webbrowser', 'whichdb', 'winreg', 'winsound', - 'wsgiref', 'xdrlib', 'xml', 'xmlrpc', 'xmlrpclib', 'zipapp', 'zipfile', - 'zipimport', 'zlib'] - - def _get_default(py_version: Optional[str]) -> dict: """ Returns the correct standard library based on either the passed py_version flag or the python interpreter @@ -190,9 +100,9 @@ def _get_default(py_version: Optional[str]) -> dict: _default = default.copy() if major == 3: - standard_library = _known_standard_library_3 + standard_library = standard_library_3 elif major == 2 and minor == 7: - standard_library = _known_standard_library_2 + standard_library = standard_library_2 else: raise ValueError( "The python version %s is not supported. " diff --git a/isort/stdlibs/py_three.py b/isort/stdlibs/py_three.py new file mode 100644 index 000000000..744e19d1a --- /dev/null +++ b/isort/stdlibs/py_three.py @@ -0,0 +1,52 @@ +""" +File contains the standard library of python3 + +If in any minor the standard library changes, a new list should be +created and the names adjusted +""" + +standard_library_3 = ['AL', 'BaseHTTPServer', 'Bastion', 'CGIHTTPServer', 'Carbon', 'ColorPicker', + 'ConfigParser', 'Cookie', 'DEVICE', 'DocXMLRPCServer', 'EasyDialogs', 'FL', + 'FrameWork', 'GL', 'HTMLParser', 'MacOS', 'MimeWriter', 'MiniAEFrame', 'Nav', + 'PixMapWrapper', 'Queue', 'SUNAUDIODEV', 'ScrolledText', 'SimpleHTTPServer', + 'SimpleXMLRPCServer', 'SocketServer', 'StringIO', 'Tix', 'Tkinter', 'UserDict', + 'UserList', 'UserString', 'W', '__builtin__', 'abc', 'aepack', 'aetools', + 'aetypes', 'aifc', 'al', 'anydbm', 'applesingle', 'argparse', 'array', 'ast', + 'asynchat', 'asyncio', 'asyncore', 'atexit', 'audioop', 'autoGIL', 'base64', + 'bdb', 'binascii', 'binhex', 'bisect', 'bsddb', 'buildtools', 'builtins', + 'bz2', 'cPickle', 'cProfile', 'cStringIO', 'calendar', 'cd', 'cfmfile', 'cgi', + 'cgitb', 'chunk', 'cmath', 'cmd', 'code', 'codecs', 'codeop', 'collections', + 'colorsys', 'commands', 'compileall', 'compiler', 'concurrent', 'configparser', + 'contextlib', 'contextvars', 'cookielib', 'copy', 'copy_reg', 'copyreg', 'crypt', 'csv', + 'ctypes', 'curses', 'dataclasses', 'datetime', 'dbhash', 'dbm', 'decimal', 'difflib', + 'dircache', 'dis', 'distutils', 'dl', 'doctest', 'dumbdbm', 'dummy_thread', + 'dummy_threading', 'email', 'encodings', 'ensurepip', 'enum', 'errno', + 'exceptions', 'faulthandler', 'fcntl', 'filecmp', 'fileinput', 'findertools', + 'fl', 'flp', 'fm', 'fnmatch', 'formatter', 'fpectl', 'fpformat', 'fractions', + 'ftplib', 'functools', 'future_builtins', 'gc', 'gdbm', 'gensuitemodule', + 'getopt', 'getpass', 'gettext', 'gl', 'glob', 'grp', 'gzip', 'hashlib', + 'heapq', 'hmac', 'hotshot', 'html', 'htmlentitydefs', 'htmllib', 'http', + 'httplib', 'ic', 'icopen', 'imageop', 'imaplib', 'imgfile', 'imghdr', 'imp', + 'importlib', 'imputil', 'inspect', 'io', 'ipaddress', 'itertools', 'jpeg', + 'json', 'keyword', 'lib2to3', 'linecache', 'locale', 'logging', 'lzma', + 'macerrors', 'macostools', 'macpath', 'macresource', 'mailbox', 'mailcap', + 'marshal', 'math', 'md5', 'mhlib', 'mimetools', 'mimetypes', 'mimify', 'mmap', + 'modulefinder', 'msilib', 'msvcrt', 'multifile', 'multiprocessing', 'mutex', + 'netrc', 'new', 'nis', 'nntplib', 'numbers', 'operator', 'optparse', 'os', + 'ossaudiodev', 'parser', 'pathlib', 'pdb', 'pickle', 'pickletools', 'pipes', + 'pkgutil', 'platform', 'plistlib', 'popen2', 'poplib', 'posix', 'posixfile', + 'pprint', 'profile', 'pstats', 'pty', 'pwd', 'py_compile', 'pyclbr', 'pydoc', + 'queue', 'quopri', 'random', 're', 'readline', 'reprlib', 'resource', 'rexec', + 'rfc822', 'rlcompleter', 'robotparser', 'runpy', 'sched', 'secrets', 'select', + 'selectors', 'sets', 'sgmllib', 'sha', 'shelve', 'shlex', 'shutil', 'signal', + 'site', 'sitecustomize', 'smtpd', 'smtplib', 'sndhdr', 'socket', 'socketserver', + 'spwd', 'sqlite3', 'ssl', 'stat', 'statistics', 'statvfs', 'string', 'stringprep', + 'struct', 'subprocess', 'sunau', 'sunaudiodev', 'symbol', 'symtable', 'sys', + 'sysconfig', 'syslog', 'tabnanny', 'tarfile', 'telnetlib', 'tempfile', 'termios', + 'test', 'textwrap', 'this', 'thread', 'threading', 'time', 'timeit', 'tkinter', + 'token', 'tokenize', 'trace', 'traceback', 'tracemalloc', 'ttk', 'tty', 'turtle', + 'turtledemo', 'types', 'typing', 'unicodedata', 'unittest', 'urllib', 'urllib2', + 'urlparse', 'usercustomize', 'uu', 'uuid', 'venv', 'videoreader', + 'warnings', 'wave', 'weakref', 'webbrowser', 'whichdb', 'winreg', 'winsound', + 'wsgiref', 'xdrlib', 'xml', 'xmlrpc', 'xmlrpclib', 'zipapp', 'zipfile', + 'zipimport', 'zlib'] diff --git a/isort/stdlibs/py_two.py b/isort/stdlibs/py_two.py new file mode 100644 index 000000000..f00c6f3e1 --- /dev/null +++ b/isort/stdlibs/py_two.py @@ -0,0 +1,49 @@ +""" +File contains the standard library of python 2.7 +""" + +standard_library_2 = ['AL', 'BaseHTTPServer', 'Bastion', 'CGIHTTPServer', 'Carbon', 'ColorPicker', + 'ConfigParser', 'Cookie', 'DEVICE', 'DocXMLRPCServer', 'EasyDialogs', 'FL', + 'FrameWork', 'GL', 'HTMLParser', 'MacOS', 'MimeWriter', 'MiniAEFrame', 'Nav', + 'PixMapWrapper', 'Queue', 'SUNAUDIODEV', 'ScrolledText', 'SimpleHTTPServer', + 'SimpleXMLRPCServer', 'SocketServer', 'StringIO', 'Tix', 'Tkinter', 'UserDict', + 'UserList', 'UserString', 'W', '__builtin__', 'abc', 'aepack', 'aetools', + 'aetypes', 'aifc', 'al', 'anydbm', 'applesingle', 'argparse', 'array', 'ast', + 'asynchat', 'asyncio', 'asyncore', 'atexit', 'audioop', 'autoGIL', 'base64', + 'bdb', 'binascii', 'binhex', 'bisect', 'bsddb', 'buildtools', 'builtins', + 'bz2', 'cPickle', 'cProfile', 'cStringIO', 'calendar', 'cd', 'cfmfile', 'cgi', + 'cgitb', 'chunk', 'cmath', 'cmd', 'code', 'codecs', 'codeop', 'collections', + 'colorsys', 'commands', 'compileall', 'compiler', 'concurrent', 'configparser', + 'contextlib', 'contextvars', 'cookielib', 'copy', 'copy_reg', 'copyreg', 'crypt', 'csv', + 'ctypes', 'curses', 'dataclasses', 'datetime', 'dbhash', 'dbm', 'decimal', 'difflib', + 'dircache', 'dis', 'distutils', 'dl', 'doctest', 'dumbdbm', 'dummy_thread', + 'dummy_threading', 'email', 'encodings', 'ensurepip', 'enum', 'errno', + 'exceptions', 'faulthandler', 'fcntl', 'filecmp', 'fileinput', 'findertools', + 'fl', 'flp', 'fm', 'fnmatch', 'formatter', 'fpectl', 'fpformat', 'fractions', + 'ftplib', 'functools', 'future_builtins', 'gc', 'gdbm', 'gensuitemodule', + 'getopt', 'getpass', 'gettext', 'gl', 'glob', 'grp', 'gzip', 'hashlib', + 'heapq', 'hmac', 'hotshot', 'html', 'htmlentitydefs', 'htmllib', 'http', + 'httplib', 'ic', 'icopen', 'imageop', 'imaplib', 'imgfile', 'imghdr', 'imp', + 'importlib', 'imputil', 'inspect', 'io', 'ipaddress', 'itertools', 'jpeg', + 'json', 'keyword', 'lib2to3', 'linecache', 'locale', 'logging', 'lzma', + 'macerrors', 'macostools', 'macpath', 'macresource', 'mailbox', 'mailcap', + 'marshal', 'math', 'md5', 'mhlib', 'mimetools', 'mimetypes', 'mimify', 'mmap', + 'modulefinder', 'msilib', 'msvcrt', 'multifile', 'multiprocessing', 'mutex', + 'netrc', 'new', 'nis', 'nntplib', 'numbers', 'operator', 'optparse', 'os', + 'ossaudiodev', 'parser', 'pathlib', 'pdb', 'pickle', 'pickletools', 'pipes', + 'pkgutil', 'platform', 'plistlib', 'popen2', 'poplib', 'posix', 'posixfile', + 'pprint', 'profile', 'pstats', 'pty', 'pwd', 'py_compile', 'pyclbr', 'pydoc', + 'queue', 'quopri', 'random', 're', 'readline', 'reprlib', 'resource', 'rexec', + 'rfc822', 'rlcompleter', 'robotparser', 'runpy', 'sched', 'secrets', 'select', + 'selectors', 'sets', 'sgmllib', 'sha', 'shelve', 'shlex', 'shutil', 'signal', + 'site', 'sitecustomize', 'smtpd', 'smtplib', 'sndhdr', 'socket', 'socketserver', + 'spwd', 'sqlite3', 'ssl', 'stat', 'statistics', 'statvfs', 'string', 'stringprep', + 'struct', 'subprocess', 'sunau', 'sunaudiodev', 'symbol', 'symtable', 'sys', + 'sysconfig', 'syslog', 'tabnanny', 'tarfile', 'telnetlib', 'tempfile', 'termios', + 'test', 'textwrap', 'this', 'thread', 'threading', 'time', 'timeit', 'tkinter', + 'token', 'tokenize', 'trace', 'traceback', 'tracemalloc', 'ttk', 'tty', 'turtle', + 'turtledemo', 'types', 'typing', 'unicodedata', 'unittest', 'urllib', 'urllib2', + 'urlparse', 'user', 'usercustomize', 'uu', 'uuid', 'venv', 'videoreader', + 'warnings', 'wave', 'weakref', 'webbrowser', 'whichdb', 'winreg', 'winsound', + 'wsgiref', 'xdrlib', 'xml', 'xmlrpc', 'xmlrpclib', 'zipapp', 'zipfile', + 'zipimport', 'zlib'] From 4aa737d06685eef0e68adfac45f7de16dedb7c93 Mon Sep 17 00:00:00 2001 From: Zarathustra2 Date: Wed, 31 Jul 2019 10:28:22 +0200 Subject: [PATCH 0019/1439] Fixing travis import error, adding all Value option for py-version flag --- isort/settings.py | 8 ++++++-- isort/stdlibs/__init__.py | 0 test_isort.py | 4 ++++ 3 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 isort/stdlibs/__init__.py diff --git a/isort/settings.py b/isort/settings.py index 88b125ddc..aed5f20ff 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -79,13 +79,15 @@ def from_string(value: str) -> 'WrapModes': def _get_default(py_version: Optional[str]) -> dict: """ Returns the correct standard library based on either the passed py_version flag or the python interpreter + Additionaly users have the option to pass all as value instead of an + version. As an result code will be checked against both standard libraries - python2 & python3 See Issue 889 and 778 for more information """ if py_version is None: major, minor = sys.version_info[0:2] - else: + elif py_version is not "all": minor = 0 # we have a minor @@ -99,7 +101,9 @@ def _get_default(py_version: Optional[str]) -> dict: _default = default.copy() - if major == 3: + if py_version is "all": + standard_library = list(set(standard_library_3 + standard_library_2)) + elif major == 3: standard_library = standard_library_3 elif major == 2 and minor == 7: standard_library = standard_library_2 diff --git a/isort/stdlibs/__init__.py b/isort/stdlibs/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test_isort.py b/test_isort.py index dd30505ad..bb5305ac8 100644 --- a/test_isort.py +++ b/test_isort.py @@ -3373,3 +3373,7 @@ def test_python_version(): output_python_2 = ('import os\n' 'import user\n') assert SortImports(file_contents=test_input, py_version="2.7").output == output_python_2 + + test_input = ('import os\nimport xml') + + print(SortImports(file_contents=test_input, py_version="all").output ) From cd487823d3e38d655f52459ea02b18c76e002860 Mon Sep 17 00:00:00 2001 From: Zarathustra2 Date: Wed, 31 Jul 2019 10:39:58 +0200 Subject: [PATCH 0020/1439] Travis Import Error: exporting Pythonpath before install --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 28ee6346c..78622c9fa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,6 +24,7 @@ matrix: before_install: - "./scripts/before_install.sh" +- "export PYTHONPATH=$PYTHONPATH:$(pwd)" install: - pip install tox script: From b496146619d555961bf29acab606ee105c23d40c Mon Sep 17 00:00:00 2001 From: Zarathustra2 Date: Sat, 3 Aug 2019 19:24:38 +0200 Subject: [PATCH 0021/1439] Fixed import error by using find_packages() in setup.py. Otherwise every package would have to be listed. --- .travis.yml | 1 - isort/settings.py | 5 ++--- setup.py | 4 ++-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 78622c9fa..28ee6346c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,7 +24,6 @@ matrix: before_install: - "./scripts/before_install.sh" -- "export PYTHONPATH=$PYTHONPATH:$(pwd)" install: - pip install tox script: diff --git a/isort/settings.py b/isort/settings.py index aed5f20ff..bf156ef3b 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -35,9 +35,8 @@ from pathlib import Path from typing import Any, Callable, Dict, Iterable, List, Mapping, MutableMapping, Optional, Union -from isort.stdlibs.py_three import standard_library_3 -from isort.stdlibs.py_two import standard_library_2 - +from .stdlibs.py_three import standard_library_3 +from .stdlibs.py_two import standard_library_2 from .utils import difference, union try: diff --git a/setup.py b/setup.py index 7ca16da3c..f1e0747ce 100755 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -from setuptools import setup +from setuptools import find_packages, setup with open('README.rst') as f: readme = f.read() @@ -20,7 +20,7 @@ 'distutils.commands': ['isort = isort.main:ISortCommand'], 'pylama.linter': ['isort = isort.pylama_isort:Linter'], }, - packages=['isort'], + packages=find_packages(), extras_require={ 'pipfile': ['pipreqs', 'requirementslib'], 'pyproject': ['toml'], From 2a6700f619c22f2a26560a10781e389e37fa2dc4 Mon Sep 17 00:00:00 2001 From: Timothy Edmund Crosley Date: Wed, 14 Aug 2019 08:24:46 -0700 Subject: [PATCH 0022/1439] Update ACKNOWLEDGEMENTS.md Add Dario Navin (@Zarathustra2) to acknowledgements --- ACKNOWLEDGEMENTS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ACKNOWLEDGEMENTS.md b/ACKNOWLEDGEMENTS.md index 971ee83c6..c51a26046 100644 --- a/ACKNOWLEDGEMENTS.md +++ b/ACKNOWLEDGEMENTS.md @@ -81,6 +81,8 @@ Code Contributors - Madison Caldwell (@madirey) - Matt Yule-Bennett (@mattbennett) - Jaswanth Kumar (@jaswanth098) +- Dario Navin (@Zarathustra2) + Documenters =================== From 6c0ec9f62f8d1d33f8b10d032a499c3e6dff5ff0 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Wed, 14 Aug 2019 03:53:12 -0700 Subject: [PATCH 0023/1439] Specify targeted Python version to mypy Discovered a typing bug where a Path object was passed to os.path.relpath(), but Path support was not added until Python 3.6. https://docs.python.org/3/library/os.path.html#os.path.relpath > Changed in version 3.6: Accepts a path-like object. --- isort/compat.py | 3 ++- setup.cfg | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/isort/compat.py b/isort/compat.py index 2d6317ba3..9f6186366 100644 --- a/isort/compat.py +++ b/isort/compat.py @@ -95,7 +95,8 @@ def __init__( absolute_file_path = resolve(file_path) if check_skip: if run_path and run_path in absolute_file_path.parents: - file_name = os.path.relpath(absolute_file_path, run_path) + # TODO: Drop str() when isort is Python 3.6+. + file_name = os.path.relpath(str(absolute_file_path), run_path) else: file_name = str(absolute_file_path) run_path = '' diff --git a/setup.cfg b/setup.cfg index 14571a023..bfc6e1694 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,6 +14,7 @@ ignore = license_file = LICENSE [mypy] +python_version = 3.5 follow_imports = silent ignore_missing_imports = True strict_optional = True From c8ed93536828a351dc2f21381d227c2600624557 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Wed, 14 Aug 2019 04:06:46 -0700 Subject: [PATCH 0024/1439] Remove deprecated license_file from setup.cfg Starting with wheel 0.32.0 (2018-09-29), the "license_file" option is deprecated. https://wheel.readthedocs.io/en/stable/news.html The wheel will continue to include LICENSE, it is now included automatically: https://wheel.readthedocs.io/en/stable/user_guide.html#including-license-files-in-the-generated-wheel-file --- setup.cfg | 3 --- 1 file changed, 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index bfc6e1694..19f4f8574 100644 --- a/setup.cfg +++ b/setup.cfg @@ -10,9 +10,6 @@ ignore = # W504 line break after binary operator W504 -[metadata] -license_file = LICENSE - [mypy] python_version = 3.5 follow_imports = silent From fad7265b63b5cd6df0c30a7c41117139f5371109 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Wed, 14 Aug 2019 04:12:55 -0700 Subject: [PATCH 0025/1439] Enable mypy warn_no_return in mypy for stricter type checking Warns when a function that normally returns a value doesn't. Fixed all cases discovered by the tool. --- isort/finders.py | 6 ++++++ isort/isort.py | 1 + setup.cfg | 1 - 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/isort/finders.py b/isort/finders.py index c7e24b62d..2605d7433 100644 --- a/isort/finders.py +++ b/isort/finders.py @@ -57,12 +57,14 @@ def find(self, module_name: str) -> Optional[str]: if fnmatch(module_name, path_glob) or fnmatch(module_name, '.' + path_glob): return forced_separate + return None class LocalFinder(BaseFinder): def find(self, module_name: str) -> Optional[str]: if module_name.startswith("."): return self.sections.LOCALFOLDER + return None class KnownPatternFinder(BaseFinder): @@ -106,6 +108,7 @@ def find(self, module_name: str) -> Optional[str]: for pattern, placement in self.known_patterns: if pattern.match(module_name_to_check): return placement + return None class PathFinder(BaseFinder): @@ -176,6 +179,7 @@ def find(self, module_name: str) -> Optional[str]: if os.path.normcase(prefix).startswith(self.stdlib_lib_prefix): return self.sections.STDLIB return self.config['default_section'] + return None class ReqsBaseFinder(BaseFinder): @@ -269,6 +273,7 @@ def find(self, module_name: str) -> Optional[str]: for name in self.names: if module_name == name: return self.sections.THIRDPARTY + return None class RequirementsFinder(ReqsBaseFinder): @@ -390,3 +395,4 @@ def find(self, module_name: str) -> Optional[str]: module_name)) if section is not None: return section + return None diff --git a/isort/isort.py b/isort/isort.py index c5342d39b..93d42a6dd 100644 --- a/isort/isort.py +++ b/isort/isort.py @@ -159,6 +159,7 @@ def _import_type(line: str) -> Optional[str]: return "straight" elif line.startswith('from '): return "from" + return None def _at_end(self) -> bool: """returns True if we are at the end of the file.""" diff --git a/setup.cfg b/setup.cfg index 19f4f8574..f8324a66c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,7 +15,6 @@ python_version = 3.5 follow_imports = silent ignore_missing_imports = True strict_optional = True -warn_no_return = False check_untyped_defs = True allow_redefinition = True From 1ab38f4f7840a3c19bf961a24630a992a8373a76 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Wed, 14 Aug 2019 04:59:59 -0700 Subject: [PATCH 0026/1439] Simplify subprocess use with high level run API Per the Python docs, subprocess.run() is the preferred interface for running subprocess and replaces the older API (such as subprocess.check_output()) https://docs.python.org/3/library/subprocess.html > The recommended approach to invoking subprocesses is to use the run() > function for all use cases it can handle. For more advanced use cases, > the underlying Popen interface can be used directly. > > > The run() function was added in Python 3.5; if you need to retain > compatibility with older versions, see the Older high-level API > section. Allows for: - Return a str from get_output() to decode once instead of every line of the result. - Pass command arguments as a list of strings to ensure no possibility for unescaped shell injection. --- isort/hooks.py | 17 +++++++++-------- test_isort.py | 28 ++++++++++++++++++++-------- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/isort/hooks.py b/isort/hooks.py index a14493f04..2e0752704 100644 --- a/isort/hooks.py +++ b/isort/hooks.py @@ -28,17 +28,18 @@ from isort import SortImports -def get_output(command: str) -> bytes: +def get_output(command: List[str]) -> str: """ Run a command and return raw output :param str command: the command to run :returns: the stdout output of the command """ - return subprocess.check_output(command.split()) + result = subprocess.run(command, stdout=subprocess.PIPE, check=True) + return result.stdout.decode() -def get_lines(command: str) -> List[str]: +def get_lines(command: List[str]) -> List[str]: """ Run a command and return lines of output @@ -46,7 +47,7 @@ def get_lines(command: str) -> List[str]: :returns: list of whitespace-stripped lines output by command """ stdout = get_output(command) - return [line.strip().decode() for line in stdout.splitlines()] + return [line.strip() for line in stdout.splitlines()] def git_hook(strict=False, modify=False): @@ -64,19 +65,19 @@ def git_hook(strict=False, modify=False): """ # Get list of files modified and staged - diff_cmd = "git diff-index --cached --name-only --diff-filter=ACMRTUXB HEAD" + diff_cmd = ["git", "diff-index", "--cached", "--name-only", "--diff-filter=ACMRTUXB HEAD"] files_modified = get_lines(diff_cmd) errors = 0 for filename in files_modified: if filename.endswith('.py'): # Get the staged contents of the file - staged_cmd = "git show :%s" % filename + staged_cmd = ["git", "show", ":%s" % filename] staged_contents = get_output(staged_cmd) sort = SortImports( file_path=filename, - file_contents=staged_contents.decode(), + file_contents=staged_contents, check=True ) @@ -85,7 +86,7 @@ def git_hook(strict=False, modify=False): if modify: SortImports( file_path=filename, - file_contents=staged_contents.decode(), + file_contents=staged_contents, check=False, ) diff --git a/test_isort.py b/test_isort.py index bb5305ac8..ff061da0f 100644 --- a/test_isort.py +++ b/test_isort.py @@ -24,9 +24,9 @@ import os import os.path import posixpath +import subprocess import sys import sysconfig -from subprocess import check_output from tempfile import NamedTemporaryFile from typing import Any, Dict, List @@ -3230,11 +3230,15 @@ def test_settings_path_skip_issue_909(tmpdir): test_run_directory = os.getcwd() os.chdir(str(base_dir)) with pytest.raises(Exception): # without the settings path provided: the command should not skip & identify errors - check_output(['isort', '--check-only']) - results = check_output(['isort', '--check-only', '--settings-path=conf/.isort.cfg']) + subprocess.run(['isort', '--check-only'], check=True) + result = subprocess.run( + ['isort', '--check-only', '--settings-path=conf/.isort.cfg'], + stdout=subprocess.PIPE, + check=True + ) os.chdir(str(test_run_directory)) - assert b'skipped 2' in results.lower() + assert b'skipped 2' in result.stdout.lower() def test_skip_paths_issue_938(tmpdir): @@ -3261,16 +3265,24 @@ def test_skip_paths_issue_938(tmpdir): test_run_directory = os.getcwd() os.chdir(str(base_dir)) - results = check_output(['isort', 'dont_skip.py', 'migrations/file_glob_skip.py']) + result = subprocess.run( + ['isort', 'dont_skip.py', 'migrations/file_glob_skip.py'], + stdout=subprocess.PIPE, + check=True, + ) os.chdir(str(test_run_directory)) - assert b'skipped' not in results.lower() + assert b'skipped' not in result.stdout.lower() os.chdir(str(base_dir)) - results = check_output(['isort', '--filter-files', '--settings-path=conf/.isort.cfg', 'dont_skip.py', 'migrations/file_glob_skip.py']) + result = subprocess.run( + ['isort', '--filter-files', '--settings-path=conf/.isort.cfg', 'dont_skip.py', 'migrations/file_glob_skip.py'], + stdout=subprocess.PIPE, + check=True, + ) os.chdir(str(test_run_directory)) - assert b'skipped 1' in results.lower() + assert b'skipped 1' in result.stdout.lower() def test_failing_file_check_916(): From 0aa6267a475a425704c91b70d47e1af08b049c67 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Wed, 14 Aug 2019 05:57:59 -0700 Subject: [PATCH 0027/1439] Unpin mypy requirements Typing of the stdlib is being updated all the time. Ensure this project uses an up to date version of mypy for more accurate results. --- mypy-requirements.txt | 3 --- tox.ini | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) delete mode 100644 mypy-requirements.txt diff --git a/mypy-requirements.txt b/mypy-requirements.txt deleted file mode 100644 index 09bb06ad2..000000000 --- a/mypy-requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -mypy==0.701 -typing==3.6.6 -mypy-extensions==0.4.1 diff --git a/tox.ini b/tox.ini index cb1c2f102..2c9acb8c6 100644 --- a/tox.ini +++ b/tox.ini @@ -26,7 +26,7 @@ commands = python setup.py isort [testenv:mypy] basepython = python3 -deps = -r{toxinidir}/mypy-requirements.txt +deps = mypy commands = mypy . skip_install = True From a5192e329b6baf2dff297adc9ffebe6bb7ded2e6 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Wed, 14 Aug 2019 14:18:47 -0700 Subject: [PATCH 0028/1439] Restore lint tox environment Was mistakenly removed in commit 55f1ae707d429c9605f39812b11b9b75140f5749. --- isort/settings.py | 4 ++-- test_isort.py | 7 +++++-- tox.ini | 5 +++++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/isort/settings.py b/isort/settings.py index bf156ef3b..cf5441a04 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -86,7 +86,7 @@ def _get_default(py_version: Optional[str]) -> dict: if py_version is None: major, minor = sys.version_info[0:2] - elif py_version is not "all": + elif py_version != "all": minor = 0 # we have a minor @@ -100,7 +100,7 @@ def _get_default(py_version: Optional[str]) -> dict: _default = default.copy() - if py_version is "all": + if py_version == "all": standard_library = list(set(standard_library_3 + standard_library_2)) elif major == 3: standard_library = standard_library_3 diff --git a/test_isort.py b/test_isort.py index ff061da0f..f6b8a050b 100644 --- a/test_isort.py +++ b/test_isort.py @@ -2211,7 +2211,10 @@ def test_long_alias_using_paren_issue_957(): out = SortImports(file_contents=test_input, line_length=50, use_parentheses=True, multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, check=True).output assert out == expected_output - test_input = ('from deep.deep.deep.deep.deep.deep.deep.deep.deep.package import very_very_very_very_very_very_very_very_very_very_long_module as very_very_very_very_very_very_very_very_very_very_long_alias\n') + test_input = ( + 'from deep.deep.deep.deep.deep.deep.deep.deep.deep.package ' + 'import very_very_very_very_very_very_very_very_very_very_long_module as very_very_very_very_very_very_very_very_very_very_long_alias\n' + ) expected_output = ('from deep.deep.deep.deep.deep.deep.deep.deep.deep.package import (\n' ' very_very_very_very_very_very_very_very_very_very_long_module as very_very_very_very_very_very_very_very_very_very_long_alias\n' ')\n') @@ -3388,4 +3391,4 @@ def test_python_version(): test_input = ('import os\nimport xml') - print(SortImports(file_contents=test_input, py_version="all").output ) + print(SortImports(file_contents=test_input, py_version="all").output) diff --git a/tox.ini b/tox.ini index 2c9acb8c6..dcac1ddef 100644 --- a/tox.ini +++ b/tox.ini @@ -24,6 +24,11 @@ commands = pytest {posargs} basepython = python3 commands = python setup.py isort +[testenv:lint] +deps = flake8 +commands = flake8 +skip_install = True + [testenv:mypy] basepython = python3 deps = mypy From fe4b15a71d4c651c933936be2d70c196494e675b Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Wed, 14 Aug 2019 14:00:30 -0700 Subject: [PATCH 0029/1439] Enable mypy option disallow_any_generics for stricter type checking --- isort/finders.py | 2 +- isort/settings.py | 2 +- setup.cfg | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/isort/finders.py b/isort/finders.py index 2605d7433..0fb884dbe 100644 --- a/isort/finders.py +++ b/isort/finders.py @@ -71,7 +71,7 @@ class KnownPatternFinder(BaseFinder): def __init__(self, config: Mapping[str, Any], sections: Any) -> None: super().__init__(config, sections) - self.known_patterns = [] # type: List[Tuple[Pattern, str]] + self.known_patterns = [] # type: List[Tuple[Pattern[str], str]] for placement in reversed(self.sections): known_placement = KNOWN_SECTION_MAPPING.get(placement, placement) config_key = 'known_{0}'.format(known_placement.lower()) diff --git a/isort/settings.py b/isort/settings.py index cf5441a04..c3fdecdd9 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -75,7 +75,7 @@ def from_string(value: str) -> 'WrapModes': return getattr(WrapModes, str(value), None) or WrapModes(int(value)) -def _get_default(py_version: Optional[str]) -> dict: +def _get_default(py_version: Optional[str]) -> Dict[str, Any]: """ Returns the correct standard library based on either the passed py_version flag or the python interpreter Additionaly users have the option to pass all as value instead of an diff --git a/setup.cfg b/setup.cfg index f8324a66c..49a95ee5a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,6 +14,7 @@ ignore = python_version = 3.5 follow_imports = silent ignore_missing_imports = True +disallow_any_generics = True strict_optional = True check_untyped_defs = True allow_redefinition = True From 8883e51b8906b72d048436644bcc311179550b3a Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Wed, 14 Aug 2019 17:05:17 -0700 Subject: [PATCH 0030/1439] Add return type annotations to test_isort.py Helps move closer to mypy strict mode. --- test_isort.py | 260 +++++++++++++++++++++++++------------------------- 1 file changed, 130 insertions(+), 130 deletions(-) diff --git a/test_isort.py b/test_isort.py index f6b8a050b..7f1d9a4fc 100644 --- a/test_isort.py +++ b/test_isort.py @@ -76,7 +76,7 @@ def default_settings_path(tmpdir_factory): yield config_dir.strpath -def test_happy_path(): +def test_happy_path() -> None: """Test the most basic use case, straight imports no code, simply not organized by category.""" test_input = ("import sys\n" "import os\n" @@ -91,7 +91,7 @@ def test_happy_path(): "import myproject.test\n") -def test_code_intermixed(): +def test_code_intermixed() -> None: """Defines what should happen when isort encounters imports intermixed with code. @@ -111,7 +111,7 @@ def test_code_intermixed(): "print('I like to put code between imports cause I want stuff to break')\n") -def test_correct_space_between_imports(): +def test_correct_space_between_imports() -> None: """Ensure after imports a correct amount of space (in newlines) is enforced. @@ -158,7 +158,7 @@ def test_correct_space_between_imports(): "print('yo')\n") -def test_sort_on_number(): +def test_sort_on_number() -> None: """Ensure numbers get sorted logically (10 > 9 not the other way around)""" test_input = ("import lib10\n" "import lib9\n") @@ -167,7 +167,7 @@ def test_sort_on_number(): "import lib10\n") -def test_line_length(): +def test_line_length() -> None: """Ensure isort enforces the set line_length.""" assert len(SortImports(file_contents=REALLY_LONG_IMPORT, line_length=80).output.split("\n")[0]) <= 80 assert len(SortImports(file_contents=REALLY_LONG_IMPORT, line_length=120).output.split("\n")[0]) <= 120 @@ -214,7 +214,7 @@ def test_line_length(): " lib22)\n") -def test_output_modes(): +def test_output_modes() -> None: """Test setting isort to use various output modes works as expected""" test_output_grid = SortImports(file_contents=REALLY_LONG_IMPORT, multi_line_output=WrapModes.GRID, line_length=40).output @@ -403,7 +403,7 @@ def test_output_modes(): ")\n") -def test_qa_comment_case(): +def test_qa_comment_case() -> None: test_input = "from veryveryveryveryveryveryveryveryveryveryvery import X # NOQA" test_output = SortImports(file_contents=test_input, line_length=40, multi_line_output=WrapModes.NOQA).output assert test_output == "from veryveryveryveryveryveryveryveryveryveryvery import X # NOQA\n" @@ -413,7 +413,7 @@ def test_qa_comment_case(): assert test_output == "import veryveryveryveryveryveryveryveryveryveryvery # NOQA\n" -def test_length_sort(): +def test_length_sort() -> None: """Test setting isort to sort on length instead of alphabetically.""" test_input = ("import medium_sizeeeeeeeeeeeeee\n" "import shortie\n" @@ -426,7 +426,7 @@ def test_length_sort(): "import looooooooooooooooooooooooooooooooooooooong\n") -def test_length_sort_section(): +def test_length_sort_section() -> None: """Test setting isort to sort on length instead of alphabetically for a specific section.""" test_input = ("import medium_sizeeeeeeeeeeeeee\n" "import shortie\n" @@ -444,7 +444,7 @@ def test_length_sort_section(): "import shortie\n") -def test_convert_hanging(): +def test_convert_hanging() -> None: """Ensure that isort will convert hanging indents to correct indent method.""" test_input = ("from third_party import lib1, lib2, \\\n" @@ -467,7 +467,7 @@ def test_convert_hanging(): " lib22)\n") -def test_custom_indent(): +def test_custom_indent() -> None: """Ensure setting a custom indent will work as expected.""" test_output = SortImports(file_contents=REALLY_LONG_IMPORT, multi_line_output=WrapModes.HANGING_INDENT, line_length=40, indent=" ", balanced_wrapping=False).output @@ -502,7 +502,7 @@ def test_custom_indent(): " lib20, lib21, lib22\n") -def test_use_parentheses(): +def test_use_parentheses() -> None: test_input = ( "from fooooooooooooooooooooooooo.baaaaaaaaaaaaaaaaaaarrrrrrr import " " my_custom_function as my_special_function" @@ -548,7 +548,7 @@ def test_use_parentheses(): ) -def test_skip(): +def test_skip() -> None: """Ensure skipping a single import will work as expected.""" test_input = ("import myproject\n" "import django\n" @@ -564,7 +564,7 @@ def test_skip(): "import sys # isort:skip this import needs to be placed here\n") -def test_skip_with_file_name(): +def test_skip_with_file_name() -> None: """Ensure skipping a file works even when file_contents is provided.""" test_input = ("import django\n" "import myproject\n") @@ -575,7 +575,7 @@ def test_skip_with_file_name(): assert sort_imports.output is None -def test_skip_within_file(): +def test_skip_within_file() -> None: """Ensure skipping a whole file works.""" test_input = ("# isort:skip_file\n" "import django\n" @@ -585,7 +585,7 @@ def test_skip_within_file(): assert sort_imports.output is None -def test_force_to_top(): +def test_force_to_top() -> None: """Ensure forcing a single import to the top of its category works as expected.""" test_input = ("import lib6\n" "import lib2\n" @@ -598,7 +598,7 @@ def test_force_to_top(): "import lib6\n") -def test_add_imports(): +def test_add_imports() -> None: """Ensures adding imports works as expected.""" test_input = ("import lib6\n" "import lib2\n" @@ -660,7 +660,7 @@ def test_add_imports(): assert test_output == ("import lib4\n") -def test_remove_imports(): +def test_remove_imports() -> None: """Ensures removing imports works as expected.""" test_input = ("import lib6\n" "import lib2\n" @@ -682,7 +682,7 @@ def test_remove_imports(): "import lib5\n") -def test_explicitly_local_import(): +def test_explicitly_local_import() -> None: """Ensure that explicitly local imports are separated.""" test_input = ("import lib1\n" "import lib2\n" @@ -695,7 +695,7 @@ def test_explicitly_local_import(): "from . import lib7\n") -def test_quotes_in_file(): +def test_quotes_in_file() -> None: """Ensure imports within triple quotes don't get imported.""" test_input = ('import os\n' '\n' @@ -768,7 +768,7 @@ def test_check_newline_in_imports(capsys): assert 'SUCCESS' in out -def test_forced_separate(): +def test_forced_separate() -> None: """Ensure that forcing certain sub modules to show separately works as expected.""" test_input = ('import sys\n' 'import warnings\n' @@ -798,7 +798,7 @@ def test_forced_separate(): line_length=120, order_by_type=False).output == test_input -def test_default_section(): +def test_default_section() -> None: """Test to ensure changing the default section works as expected.""" test_input = ("import sys\n" "import os\n" @@ -822,7 +822,7 @@ def test_default_section(): "import django.settings\n") -def test_first_party_overrides_standard_section(): +def test_first_party_overrides_standard_section() -> None: """Test to ensure changing the default section works as expected.""" test_input = ("from HTMLParser import HTMLParseError, HTMLParser\n" "import sys\n" @@ -838,7 +838,7 @@ def test_first_party_overrides_standard_section(): "import profile.test\n") -def test_thirdy_party_overrides_standard_section(): +def test_thirdy_party_overrides_standard_section() -> None: """Test to ensure changing the default section works as expected.""" test_input = ("import sys\n" "import os\n" @@ -852,7 +852,7 @@ def test_thirdy_party_overrides_standard_section(): "import profile.test\n") -def test_known_pattern_path_expansion(): +def test_known_pattern_path_expansion() -> None: """Test to ensure patterns ending with path sep gets expanded and nested packages treated as known patterns""" test_input = ("from kate_plugin import isort_plugin\n" "import sys\n" @@ -872,7 +872,7 @@ def test_known_pattern_path_expansion(): "from kate_plugin import isort_plugin\n") -def test_force_single_line_imports(): +def test_force_single_line_imports() -> None: """Test to ensure forcing imports to each have their own line works as expected.""" test_input = ("from third_party import lib1, lib2, \\\n" " lib3, lib4, lib5, lib6, lib7, \\\n" @@ -904,7 +904,7 @@ def test_force_single_line_imports(): "from third_party import lib22\n") -def test_force_single_line_long_imports(): +def test_force_single_line_long_imports() -> None: test_input = ("from veryveryveryveryveryvery import small, big\n") test_output = SortImports(file_contents=test_input, multi_line_output=WrapModes.NOQA, line_length=40, force_single_line=True).output @@ -912,7 +912,7 @@ def test_force_single_line_long_imports(): "from veryveryveryveryveryvery import small # NOQA\n") -def test_titled_imports(): +def test_titled_imports() -> None: """Tests setting custom titled/commented import sections.""" test_input = ("import sys\n" "import unicodedata\n" @@ -937,7 +937,7 @@ def test_titled_imports(): assert test_second_run == test_output -def test_balanced_wrapping(): +def test_balanced_wrapping() -> None: """Tests balanced wrapping mode, where the length of individual lines maintain width.""" test_input = ("from __future__ import (absolute_import, division, print_function,\n" " unicode_literals)") @@ -946,7 +946,7 @@ def test_balanced_wrapping(): " print_function, unicode_literals)\n") -def test_relative_import_with_space(): +def test_relative_import_with_space() -> None: """Tests the case where the relation and the module that is being imported from is separated with a space.""" test_input = ("from ... fields.sproqet import SproqetCollection") assert SortImports(file_contents=test_input).output == ("from ...fields.sproqet import SproqetCollection\n") @@ -958,7 +958,7 @@ def test_relative_import_with_space(): assert SortImports(file_contents=test_input).output == test_output -def test_multiline_import(): +def test_multiline_import() -> None: """Test the case where import spawns multiple lines with inconsistent indentation.""" test_input = ("from pkg \\\n" " import stuff, other_suff \\\n" @@ -977,7 +977,7 @@ def test_multiline_import(): assert SortImports(file_contents=test_input, **custom_configuration).output == expected_output -def test_single_multiline(): +def test_single_multiline() -> None: """Test the case where a single import spawns multiple lines.""" test_input = ("from os import\\\n" " getuid\n" @@ -991,7 +991,7 @@ def test_single_multiline(): ) -def test_atomic_mode(): +def test_atomic_mode() -> None: # without syntax error, everything works OK test_input = ("from b import d, c\n" "from a import f, e\n") @@ -1003,7 +1003,7 @@ def test_atomic_mode(): assert SortImports(file_contents=test_input, atomic=True).output == test_input -def test_order_by_type(): +def test_order_by_type() -> None: test_input = "from module import Class, CONSTANT, function" assert SortImports(file_contents=test_input, order_by_type=True).output == ("from module import CONSTANT, Class, function\n") @@ -1032,7 +1032,7 @@ def test_order_by_type(): "from subprocess import PIPE, STDOUT, Popen\n") -def test_custom_lines_after_import_section(): +def test_custom_lines_after_import_section() -> None: """Test the case where the number of lines to output after imports has been explicitly set.""" test_input = ("from a import b\n" "foo = 'bar'\n") @@ -1049,7 +1049,7 @@ def test_custom_lines_after_import_section(): "foo = 'bar'\n") -def test_smart_lines_after_import_section(): +def test_smart_lines_after_import_section() -> None: """Tests the default 'smart' behavior for dealing with lines after the import section""" # one space if not method or class after imports test_input = ("from a import b\n" @@ -1120,7 +1120,7 @@ def test_smart_lines_after_import_section(): " pass\n") -def test_settings_combine_instead_of_overwrite(): +def test_settings_combine_instead_of_overwrite() -> None: """Test to ensure settings combine logically, instead of fully overwriting.""" assert set(SortImports(known_standard_library=['not_std_library']).config['known_standard_library']) == \ set(SortImports().config['known_standard_library'] + ['not_std_library']) @@ -1129,7 +1129,7 @@ def test_settings_combine_instead_of_overwrite(): {item for item in SortImports().config['known_standard_library'] if item != 'thread'} -def test_combined_from_and_as_imports(): +def test_combined_from_and_as_imports() -> None: """Test to ensure it's possible to combine from and as imports.""" test_input = ("from translate.misc.multistring import multistring\n" "from translate.storage import base, factory\n" @@ -1140,7 +1140,7 @@ def test_combined_from_and_as_imports(): assert SortImports(file_contents=test_input, keep_direct_and_as_imports=True).output == test_output -def test_as_imports_with_line_length(): +def test_as_imports_with_line_length() -> None: """Test to ensure it's possible to combine from and as imports.""" test_input = ("from translate.storage import base as storage_base\n" "from translate.storage.placeables import general, parse as rich_parse\n") @@ -1150,7 +1150,7 @@ def test_as_imports_with_line_length(): "from translate.storage.placeables import \\\n parse as rich_parse\n") -def test_keep_comments(): +def test_keep_comments() -> None: """Test to ensure isort properly keeps comments in tact after sorting.""" # Straight Import test_input = ("import foo # bar\n") @@ -1206,7 +1206,7 @@ def test_keep_comments(): ) -def test_multiline_split_on_dot(): +def test_multiline_split_on_dot() -> None: """Test to ensure isort correctly handles multiline imports, even when split right after a '.'""" test_input = ("from my_lib.my_package.test.level_1.level_2.level_3.level_4.level_5.\\\n" " my_module import my_function") @@ -1215,7 +1215,7 @@ def test_multiline_split_on_dot(): " my_function\n") -def test_import_star(): +def test_import_star() -> None: """Test to ensure isort handles star imports correctly""" test_input = ("from blah import *\n" "from blah import _potato\n") @@ -1224,7 +1224,7 @@ def test_import_star(): assert SortImports(file_contents=test_input, combine_star=True).output == ("from blah import *\n") -def test_include_trailing_comma(): +def test_include_trailing_comma() -> None: """Test for the include_trailing_comma option""" test_output_grid = SortImports( file_contents=SHORT_IMPORT, @@ -1313,7 +1313,7 @@ def test_include_trailing_comma(): ) -def test_similar_to_std_library(): +def test_similar_to_std_library() -> None: """Test to ensure modules that are named similarly to a standard library import don't end up clobbered""" test_input = ("import datetime\n" "\n" @@ -1322,7 +1322,7 @@ def test_similar_to_std_library(): assert SortImports(file_contents=test_input, known_third_party=["requests", "times"]).output == test_input -def test_correctly_placed_imports(): +def test_correctly_placed_imports() -> None: """Test to ensure comments stay on correct placement after being sorted""" test_input = ("from a import b # comment for b\n" "from a import c # comment for c\n") @@ -1379,7 +1379,7 @@ def test_correctly_placed_imports(): known_third_party=["django", "model_mommy"]).output == test_input -def test_auto_detection(): +def test_auto_detection() -> None: """Initial test to ensure isort auto-detection works correctly - will grow over time as new issues are raised.""" # Issue 157 @@ -1394,7 +1394,7 @@ def test_auto_detection(): assert SortImports(file_contents=test_input, default_section="THIRDPARTY").output == test_input -def test_same_line_statements(): +def test_same_line_statements() -> None: """Ensure isort correctly handles the case where a single line contains multiple statements including an import""" test_input = ("import pdb; import nose\n") assert SortImports(file_contents=test_input).output == ("import pdb\n" @@ -1406,7 +1406,7 @@ def test_same_line_statements(): assert SortImports(file_contents=test_input).output == test_input -def test_long_line_comments(): +def test_long_line_comments() -> None: """Ensure isort correctly handles comments at the end of extremely long lines""" test_input = ("from foo.utils.fabric_stuff.live import check_clean_live, deploy_live, sync_live_envdir, " "update_live_app, update_live_cron # noqa\n" @@ -1419,14 +1419,14 @@ def test_long_line_comments(): " sync_stage_envdir, update_stage_app, update_stage_cron)\n") -def test_tab_character_in_import(): +def test_tab_character_in_import() -> None: """Ensure isort correctly handles import statements that contain a tab character""" test_input = ("from __future__ import print_function\n" "from __future__ import\tprint_function\n") assert SortImports(file_contents=test_input).output == "from __future__ import print_function\n" -def test_split_position(): +def test_split_position() -> None: """Ensure isort splits on import instead of . when possible""" test_input = ("from p24.shared.exceptions.master.host_state_flag_unchanged import HostStateUnchangedException\n") assert SortImports(file_contents=test_input, line_length=80).output == \ @@ -1434,7 +1434,7 @@ def test_split_position(): " HostStateUnchangedException\n") -def test_place_comments(): +def test_place_comments() -> None: """Ensure manually placing imports works as expected""" test_input = ("import sys\n" "import os\n" @@ -1463,7 +1463,7 @@ def test_place_comments(): assert test_output == expected_output -def test_placement_control(): +def test_placement_control() -> None: """Ensure that most specific placement control match wins""" test_input = ("import os\n" "import sys\n" @@ -1489,7 +1489,7 @@ def test_placement_control(): "import p24.shared.media_wiki_syntax as syntax\n") -def test_custom_sections(): +def test_custom_sections() -> None: """Ensure that most specific placement control match wins""" test_input = ("import os\n" "import sys\n" @@ -1537,7 +1537,7 @@ def test_custom_sections(): "import p24.shared.media_wiki_syntax as syntax\n") -def test_glob_known(): +def test_glob_known() -> None: """Ensure that most specific placement control match wins""" test_input = ("import os\n" "from django_whatever import whatever\n" @@ -1569,7 +1569,7 @@ def test_glob_known(): "from . import another\n") -def test_sticky_comments(): +def test_sticky_comments() -> None: """Test to ensure it is possible to make comments 'stick' above imports""" test_input = ("import os\n" "\n" @@ -1585,26 +1585,26 @@ def test_sticky_comments(): assert SortImports(file_contents=test_input).output == test_input -def test_zipimport(): +def test_zipimport() -> None: """Imports ending in "import" shouldn't be clobbered""" test_input = "from zipimport import zipimport\n" assert SortImports(file_contents=test_input).output == test_input -def test_from_ending(): +def test_from_ending() -> None: """Imports ending in "from" shouldn't be clobbered.""" test_input = "from foo import get_foo_from, get_foo\n" expected_output = "from foo import get_foo, get_foo_from\n" assert SortImports(file_contents=test_input).output == expected_output -def test_from_first(): +def test_from_first() -> None: """Tests the setting from_first works correctly""" test_input = "from os import path\nimport os\n" assert SortImports(file_contents=test_input, from_first=True).output == test_input -def test_top_comments(): +def test_top_comments() -> None: """Ensure correct behavior with top comments""" test_input = ("# -*- encoding: utf-8 -*-\n" "# Test comment\n" @@ -1626,13 +1626,13 @@ def test_top_comments(): assert SortImports(file_contents=test_input).output == test_input -def test_consistency(): +def test_consistency() -> None: """Ensures consistency of handling even when dealing with non ordered-by-type imports""" test_input = "from sqlalchemy.dialects.postgresql import ARRAY, array\n" assert SortImports(file_contents=test_input, order_by_type=True).output == test_input -def test_force_grid_wrap(): +def test_force_grid_wrap() -> None: """Ensures removing imports works as expected.""" test_input = ( "from bar import lib2\n" @@ -1657,7 +1657,7 @@ def test_force_grid_wrap(): assert test_output == test_input -def test_force_grid_wrap_long(): +def test_force_grid_wrap_long() -> None: """Ensure that force grid wrap still happens with long line length""" test_input = ( "from foo import lib6, lib7\n" @@ -1679,7 +1679,7 @@ def test_force_grid_wrap_long(): """ -def test_uses_jinja_variables(): +def test_uses_jinja_variables() -> None: """Test a basic set of imports that use jinja variables""" test_input = ("import sys\n" "import os\n" @@ -1699,7 +1699,7 @@ def test_uses_jinja_variables(): assert SortImports(file_contents=test_input).output == test_input -def test_fcntl(): +def test_fcntl() -> None: """Test to ensure fcntl gets correctly recognized as stdlib import""" test_input = ("import fcntl\n" "import os\n" @@ -1707,7 +1707,7 @@ def test_fcntl(): assert SortImports(file_contents=test_input).output == test_input -def test_import_split_is_word_boundary_aware(): +def test_import_split_is_word_boundary_aware() -> None: """Test to ensure that isort splits words in a boundary aware manner""" test_input = ("from mycompany.model.size_value_array_import_func import \\\n" " get_size_value_array_import_func_jobs") @@ -1728,7 +1728,7 @@ def test_other_file_encodings(tmpdir): assert SortImports(file_path=str(tmp_fname), settings_path=os.getcwd()).output == file_contents -def test_comment_at_top_of_file(): +def test_comment_at_top_of_file() -> None: """Test to ensure isort correctly handles top of file comments""" test_input = ("# Comment one\n" "from django import forms\n" @@ -1741,7 +1741,7 @@ def test_comment_at_top_of_file(): assert SortImports(file_contents=test_input).output == test_input -def test_alphabetic_sorting(): +def test_alphabetic_sorting() -> None: """Test to ensure isort correctly handles single line imports""" test_input = ("import unittest\n" "\n" @@ -1764,7 +1764,7 @@ def test_alphabetic_sorting(): assert SortImports(file_contents=test_input).output == test_input -def test_alphabetic_sorting_multi_line(): +def test_alphabetic_sorting_multi_line() -> None: """Test to ensure isort correctly handles multiline import see: issue 364""" test_input = ("from a import (CONSTANT_A, cONSTANT_B, CONSTANT_C, CONSTANT_D, CONSTANT_E,\n" " CONSTANT_F, CONSTANT_G, CONSTANT_H, CONSTANT_I, CONSTANT_J)\n") @@ -1772,7 +1772,7 @@ def test_alphabetic_sorting_multi_line(): assert SortImports(file_contents=test_input, **options).output == test_input -def test_comments_not_duplicated(): +def test_comments_not_duplicated() -> None: """Test to ensure comments aren't duplicated: issue 303""" test_input = ('from flask import url_for\n' "# Whole line comment\n" @@ -1783,7 +1783,7 @@ def test_comments_not_duplicated(): assert output.count("# inline comment\n") == 1 -def test_top_of_line_comments(): +def test_top_of_line_comments() -> None: """Test to ensure top of line comments stay where they should: issue 260""" test_input = ('# -*- coding: utf-8 -*-\n' 'from django.db import models\n' @@ -1798,7 +1798,7 @@ def test_top_of_line_comments(): assert output.startswith('# -*- coding: utf-8 -*-\n') -def test_basic_comment(): +def test_basic_comment() -> None: """Test to ensure a basic comment wont crash isort""" test_input = ('import logging\n' '# Foo\n' @@ -1806,7 +1806,7 @@ def test_basic_comment(): assert SortImports(file_contents=test_input).output == test_input -def test_shouldnt_add_lines(): +def test_shouldnt_add_lines() -> None: """Ensure that isort doesn't add a blank line when a top of import comment is present, issue #316""" test_input = ('"""Text"""\n' '# This is a comment\n' @@ -1881,7 +1881,7 @@ def test_pyproject_conf_file(tmpdir): assert SortImports(file_contents=test_input, settings_path=str(tmpdir)).output == correct_output -def test_alphabetic_sorting_no_newlines(): +def test_alphabetic_sorting_no_newlines() -> None: '''Test to ensure that alphabetical sort does not erroneously introduce new lines (issue #328)''' test_input = "import os\n" test_output = SortImports(file_contents=test_input, force_alphabetical_sort_within_sections=True).output @@ -1898,7 +1898,7 @@ def test_alphabetic_sorting_no_newlines(): assert test_input == test_output -def test_sort_within_section(): +def test_sort_within_section() -> None: '''Test to ensure its possible to force isort to sort within sections''' test_input = ('from Foob import ar\n' 'import foo\n' @@ -1917,7 +1917,7 @@ def test_sort_within_section(): assert test_output == test_input -def test_sorting_with_two_top_comments(): +def test_sorting_with_two_top_comments() -> None: '''Test to ensure isort will sort files that contain 2 top comments''' test_input = ('#! comment1\n' "''' comment2\n" @@ -1931,7 +1931,7 @@ def test_sorting_with_two_top_comments(): 'import b\n') -def test_lines_between_sections(): +def test_lines_between_sections() -> None: """Test to ensure lines_between_sections works""" test_input = ('from bar import baz\n' 'import os\n') @@ -1941,7 +1941,7 @@ def test_lines_between_sections(): 'from bar import baz\n') -def test_forced_sepatate_globs(): +def test_forced_sepatate_globs() -> None: """Test to ensure that forced_separate glob matches lines""" test_input = ('import os\n' '\n' @@ -1964,7 +1964,7 @@ def test_forced_sepatate_globs(): 'from myproject.foo.models import Foo\n') -def test_no_additional_lines_issue_358(): +def test_no_additional_lines_issue_358() -> None: """Test to ensure issue 358 is resolved and running isort multiple times does not add extra newlines""" test_input = ('"""This is a docstring"""\n' '# This is a comment\n' @@ -2027,7 +2027,7 @@ def test_no_additional_lines_issue_358(): assert test_output == expected_output -def test_import_by_paren_issue_375(): +def test_import_by_paren_issue_375() -> None: """Test to ensure isort can correctly handle sorting imports where the paren is directly by the import body""" test_input = ('from .models import(\n' ' Foo,\n' @@ -2036,7 +2036,7 @@ 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(): +def test_import_by_paren_issue_460() -> None: """Test to ensure isort can doesnt move comments around """ test_input = """ # First comment @@ -2048,7 +2048,7 @@ def test_import_by_paren_issue_460(): assert SortImports(file_contents=(test_input)).output == test_input -def test_function_with_docstring(): +def test_function_with_docstring() -> None: """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'] test_input = ('def foo():\n' @@ -2063,7 +2063,7 @@ def test_function_with_docstring(): assert SortImports(file_contents=test_input, add_imports=add_imports).output == expected_output -def test_plone_style(): +def test_plone_style() -> None: """Test to ensure isort correctly plone style imports""" test_input = ("from django.contrib.gis.geos import GEOSException\n" "from plone.app.testing import getRoles\n" @@ -2079,7 +2079,7 @@ def test_plone_style(): assert SortImports(file_contents=test_input, **options).output == test_input -def test_third_party_case_sensitive(): +def test_third_party_case_sensitive() -> None: """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" @@ -2116,7 +2116,7 @@ def test_sys_path_mutation(tmpdir): assert len(sys.path) == expected_length -def test_long_single_line(): +def test_long_single_line() -> None: """Test to ensure long single lines get handled correctly""" output = SortImports(file_contents="from ..views import (" " _a," @@ -2133,7 +2133,7 @@ def test_long_single_line(): assert len(line) <= 79 -def test_import_inside_class_issue_432(): +def test_import_inside_class_issue_432() -> None: """Test to ensure issue 432 is resolved and isort doesn't insert imports in the middle of classes""" test_input = ("# coding=utf-8\n" "class Foo:\n" @@ -2149,14 +2149,14 @@ def test_import_inside_class_issue_432(): assert SortImports(file_contents=test_input, add_imports=['import baz']).output == expected_output -def test_wildcard_import_without_space_issue_496(): +def test_wildcard_import_without_space_issue_496() -> None: """Test to ensure issue #496: wildcard without space, is resolved""" test_input = 'from findorserver.coupon.models import*' expected_output = 'from findorserver.coupon.models import *\n' assert SortImports(file_contents=test_input).output == expected_output -def test_import_line_mangles_issues_491(): +def test_import_line_mangles_issues_491() -> None: """Test to ensure comment on import with parens doesn't cause issues""" test_input = ('import os # ([\n' '\n' @@ -2164,7 +2164,7 @@ def test_import_line_mangles_issues_491(): assert SortImports(file_contents=test_input).output == test_input -def test_import_line_mangles_issues_505(): +def test_import_line_mangles_issues_505() -> None: """Test to ensure comment on import with parens doesn't cause issues""" test_input = ('from sys import * # (\n' '\n' @@ -2174,14 +2174,14 @@ def test_import_line_mangles_issues_505(): assert SortImports(file_contents=test_input).output == test_input -def test_import_line_mangles_issues_439(): +def test_import_line_mangles_issues_439() -> None: """Test to ensure comment on import with parens doesn't cause issues""" test_input = ('import a # () import\n' 'from b import b\n') assert SortImports(file_contents=test_input).output == test_input -def test_alias_using_paren_issue_466(): +def test_alias_using_paren_issue_466() -> None: """Test to ensure issue #466: Alias causes slash incorrectly is resolved""" test_input = 'from django.db.backends.mysql.base import DatabaseWrapper as MySQLDatabaseWrapper\n' expected_output = ('from django.db.backends.mysql.base import (\n' @@ -2196,7 +2196,7 @@ def test_alias_using_paren_issue_466(): use_parentheses=True).output == expected_output -def test_long_alias_using_paren_issue_957(): +def test_long_alias_using_paren_issue_957() -> None: test_input = ('from package import module as very_very_very_very_very_very_very_very_very_very_long_alias\n') expected_output = ('from package import (\n' ' module as very_very_very_very_very_very_very_very_very_very_long_alias\n' @@ -2249,7 +2249,7 @@ def test_ignore_whitespace(capsys): assert out == '' -def test_import_wraps_with_comment_issue_471(): +def test_import_wraps_with_comment_issue_471() -> None: """Test to ensure issue #471 is resolved""" test_input = ('from very_long_module_name import SuperLongClassName #@UnusedImport' ' -- long string of comments which wrap over') @@ -2259,7 +2259,7 @@ def test_import_wraps_with_comment_issue_471(): use_parentheses=True).output == expected_output -def test_import_case_produces_inconsistent_results_issue_472(): +def test_import_case_produces_inconsistent_results_issue_472() -> None: """Test to ensure sorting imports with same name but different case produces the same result across platforms""" test_input = ('from sqlalchemy.dialects.postgresql import ARRAY\n' 'from sqlalchemy.dialects.postgresql import array\n') @@ -2269,7 +2269,7 @@ def test_import_case_produces_inconsistent_results_issue_472(): assert SortImports(file_contents=test_input).output == test_input -def test_inconsistent_behavior_in_python_2_and_3_issue_479(): +def test_inconsistent_behavior_in_python_2_and_3_issue_479() -> None: """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') @@ -2277,7 +2277,7 @@ def test_inconsistent_behavior_in_python_2_and_3_issue_479(): known_first_party=["future"]).output == test_input -def test_sort_within_section_comments_issue_436(): +def test_sort_within_section_comments_issue_436() -> None: """Test to ensure sort within sections leaves comments untouched""" test_input = ('import os.path\n' 'import re\n' @@ -2289,7 +2289,7 @@ def test_sort_within_section_comments_issue_436(): assert SortImports(file_contents=test_input, force_sort_within_sections=True).output == test_input -def test_sort_within_sections_with_force_to_top_issue_473(): +def test_sort_within_sections_with_force_to_top_issue_473() -> None: """Test to ensure it's possible to sort within sections with items forced to top""" test_input = ('import z\n' 'import foo\n' @@ -2298,7 +2298,7 @@ def test_sort_within_sections_with_force_to_top_issue_473(): force_to_top=['z']).output == test_input -def test_correct_number_of_new_lines_with_comment_issue_435(): +def test_correct_number_of_new_lines_with_comment_issue_435() -> None: """Test to ensure that injecting a comment in-between imports doesn't mess up the new line spacing""" test_input = ('import foo\n' '\n' @@ -2310,7 +2310,7 @@ def test_correct_number_of_new_lines_with_comment_issue_435(): assert SortImports(file_contents=test_input).output == test_input -def test_future_below_encoding_issue_545(): +def test_future_below_encoding_issue_545() -> None: """Test to ensure future is always below comment""" test_input = ('#!/usr/bin/env python\n' 'from __future__ import print_function\n' @@ -2326,7 +2326,7 @@ def test_future_below_encoding_issue_545(): assert SortImports(file_contents=test_input).output == expected_output -def test_no_extra_lines_issue_557(): +def test_no_extra_lines_issue_557() -> None: """Test to ensure no extra lines are prepended""" test_input = ('import os\n' '\n' @@ -2337,7 +2337,7 @@ def test_no_extra_lines_issue_557(): force_sort_within_sections=True).output == expected_output -def test_long_import_wrap_support_with_mode_2(): +def test_long_import_wrap_support_with_mode_2() -> None: """Test to ensure mode 2 still allows wrapped imports with slash""" test_input = ('from foobar.foobar.foobar.foobar import \\\n' ' an_even_longer_function_name_over_80_characters\n') @@ -2345,7 +2345,7 @@ def test_long_import_wrap_support_with_mode_2(): line_length=80).output == test_input -def test_pylint_comments_incorrectly_wrapped_issue_571(): +def test_pylint_comments_incorrectly_wrapped_issue_571() -> None: """Test to ensure pylint comments don't get wrapped""" test_input = ('from PyQt5.QtCore import QRegExp # @UnresolvedImport pylint: disable=import-error,' 'useless-suppression\n') @@ -2354,7 +2354,7 @@ def test_pylint_comments_incorrectly_wrapped_issue_571(): assert SortImports(file_contents=test_input, line_length=60).output == expected_output -def test_ensure_async_methods_work_issue_537(): +def test_ensure_async_methods_work_issue_537() -> None: """Test to ensure async methods are correctly identified""" test_input = ('from myapp import myfunction\n' '\n' @@ -2364,7 +2364,7 @@ def test_ensure_async_methods_work_issue_537(): assert SortImports(file_contents=test_input).output == test_input -def test_ensure_as_imports_sort_correctly_within_from_imports_issue_590(): +def test_ensure_as_imports_sort_correctly_within_from_imports_issue_590() -> None: """Test to ensure combination from and as import statements are sorted correct""" test_input = ('from os import defpath\n' 'from os import pathsep as separator\n') @@ -2379,7 +2379,7 @@ def test_ensure_as_imports_sort_correctly_within_from_imports_issue_590(): assert SortImports(file_contents=test_input, force_single_line=True).output == test_input -def test_ensure_line_endings_are_preserved_issue_493(): +def test_ensure_line_endings_are_preserved_issue_493() -> None: """Test to ensure line endings are not converted""" test_input = ('from os import defpath\r\n' 'from os import pathsep as separator\r\n') @@ -2392,7 +2392,7 @@ def test_ensure_line_endings_are_preserved_issue_493(): assert SortImports(file_contents=test_input).output == test_input -def test_not_splitted_sections(): +def test_not_splitted_sections() -> None: whiteline = '\n' stdlib_section = 'import unittest\n' firstparty_section = 'from app.pkg1 import mdl1\n' @@ -2425,7 +2425,7 @@ 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(): +def test_no_lines_before_empty_section() -> None: test_input = ('import first\n' 'import custom\n') assert SortImports( @@ -2437,7 +2437,7 @@ def test_no_lines_before_empty_section(): ).output == test_input -def test_no_inline_sort(): +def test_no_inline_sort() -> None: """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.""" test_input = 'from foo import a, c, b\n' @@ -2452,7 +2452,7 @@ def test_no_inline_sort(): assert SortImports(file_contents=test_input, no_inline_sort=True, force_single_line=True).output == expected -def test_relative_import_of_a_module(): +def test_relative_import_of_a_module() -> None: """Imports can be dynamically created (PEP302) and is used by modules such as six. This test ensures that these types of imports are still sorted to the correct type instead of being categorized as local.""" test_input = ('from __future__ import absolute_import\n' @@ -2476,7 +2476,7 @@ def test_relative_import_of_a_module(): assert sorted_result == expected_results -def test_escaped_parens_sort(): +def test_escaped_parens_sort() -> None: test_input = ('from foo import \\ \n' '(a,\n' 'b,\n' @@ -2507,7 +2507,7 @@ def test_is_python_typing_stub(tmpdir): assert is_python_file(str(stub)) is True -def test_to_ensure_imports_are_brought_to_top_issue_651(): +def test_to_ensure_imports_are_brought_to_top_issue_651() -> None: test_input = ('from __future__ import absolute_import, unicode_literals\n' '\n' 'VAR = """\n' @@ -2524,7 +2524,7 @@ def test_to_ensure_imports_are_brought_to_top_issue_651(): assert SortImports(file_contents=test_input).output == expected_output -def test_to_ensure_importing_from_imports_module_works_issue_662(): +def test_to_ensure_importing_from_imports_module_works_issue_662() -> None: test_input = ('@wraps(fun)\n' 'def __inner(*args, **kwargs):\n' ' from .imports import qualname\n' @@ -2532,7 +2532,7 @@ def test_to_ensure_importing_from_imports_module_works_issue_662(): assert SortImports(file_contents=test_input).output == test_input -def test_to_ensure_no_unexpected_changes_issue_666(): +def test_to_ensure_no_unexpected_changes_issue_666() -> None: test_input = ('from django.conf import settings\n' 'from django.core.management import call_command\n' 'from django.core.management.base import BaseCommand\n' @@ -2549,7 +2549,7 @@ def test_to_ensure_no_unexpected_changes_issue_666(): assert SortImports(file_contents=test_input).output == test_input -def test_to_ensure_tabs_dont_become_space_issue_665(): +def test_to_ensure_tabs_dont_become_space_issue_665() -> None: test_input = ('import os\n' '\n' '\n' @@ -2558,7 +2558,7 @@ def test_to_ensure_tabs_dont_become_space_issue_665(): assert SortImports(file_contents=test_input).output == test_input -def test_new_lines_are_preserved(): +def test_new_lines_are_preserved() -> None: with NamedTemporaryFile('w', suffix='py', delete=False) as rn_newline: pass @@ -2705,7 +2705,7 @@ def test_pipfile_finder(tmpdir): pipfile.remove() -def test_monkey_patched_urllib(): +def test_monkey_patched_urllib() -> None: with pytest.raises(ImportError): # Previous versions of isort monkey patched urllib which caused unusual # importing for other projects. @@ -2735,7 +2735,7 @@ def test_path_finder(monkeypatch): assert finder.find("example_5") == finder.sections.FIRSTPARTY -def test_argument_parsing(): +def test_argument_parsing() -> None: from isort.main import parse_args args = parse_args(['-dt', '-t', 'foo', '--skip=bar', 'baz.py']) assert args['order_by_type'] is False @@ -2823,14 +2823,14 @@ def test_skip_glob(tmpdir, skip_glob_assert): assert file_names == file_names -def test_comments_not_removed_issue_576(): +def test_comments_not_removed_issue_576() -> None: test_input = ('import distutils\n' '# this comment is important and should not be removed\n' 'from sys import api_version as api_version\n') assert SortImports(file_contents=test_input).output == test_input -def test_reverse_relative_imports_issue_417(): +def test_reverse_relative_imports_issue_417() -> None: test_input = ('from . import ipsum\n' 'from . import lorem\n' 'from .dolor import consecteur\n' @@ -2848,7 +2848,7 @@ def test_reverse_relative_imports_issue_417(): reverse_relative=True).output == test_input -def test_inconsistent_relative_imports_issue_577(): +def test_inconsistent_relative_imports_issue_577() -> None: test_input = ('from ... import diam\n' 'from ... import dui\n' 'from ...eu import dignissim\n' @@ -2864,7 +2864,7 @@ def test_inconsistent_relative_imports_issue_577(): assert SortImports(file_contents=test_input, force_single_line=True).output == test_input -def test_unwrap_issue_762(): +def test_unwrap_issue_762() -> None: test_input = ('from os.path \\\n' 'import (join, split)\n') assert SortImports(file_contents=test_input).output == 'from os.path import join, split\n' @@ -2874,7 +2874,7 @@ def test_unwrap_issue_762(): assert SortImports(file_contents=test_input).output == 'from os.path import join, split\n' -def test_multiple_as_imports(): +def test_multiple_as_imports() -> None: test_input = ('from a import b as b\n' 'from a import b as bb\n' 'from a import b as bb_\n') @@ -2939,7 +2939,7 @@ def test_multiple_as_imports(): assert test_output == test_input -def test_all_imports_from_single_module(): +def test_all_imports_from_single_module() -> None: test_input = ('import a\n' 'from a import *\n' 'from a import b as d\n' @@ -3129,7 +3129,7 @@ def test_all_imports_from_single_module(): assert test_output == 'import a\nfrom a import *\n' -def test_noqa_issue_679(): +def test_noqa_issue_679() -> None: # Test to ensure that NOQA notation is being observed as expected test_input = ('import os\n' '\n' @@ -3161,7 +3161,7 @@ def test_extract_multiline_output_wrap_setting_from_a_config_file(tmpdir: py.pat assert config['multi_line_output'] == WrapModes.VERTICAL_GRID_GROUPED -def test_ensure_support_for_non_typed_but_cased_alphabetic_sort_issue_890(): +def test_ensure_support_for_non_typed_but_cased_alphabetic_sort_issue_890() -> None: test_input = ('from pkg import BALL\n' 'from pkg import RC\n' 'from pkg import Action\n' @@ -3182,7 +3182,7 @@ def test_ensure_support_for_non_typed_but_cased_alphabetic_sort_issue_890(): force_single_line=True).output == expected_output -def test_to_ensure_empty_line_not_added_to_file_start_issue_889(): +def test_to_ensure_empty_line_not_added_to_file_start_issue_889() -> None: test_input = ('# comment\n' 'import os\n' '# comment2\n' @@ -3203,7 +3203,7 @@ def test_to_ensure_correctly_handling_of_whitespace_only_issue_811(capsys): assert err == '' -def test_standard_library_deprecates_user_issue_778(): +def test_standard_library_deprecates_user_issue_778() -> None: test_input = ('import os\n' '\n' 'import user\n') @@ -3288,7 +3288,7 @@ def test_skip_paths_issue_938(tmpdir): assert b'skipped 1' in result.stdout.lower() -def test_failing_file_check_916(): +def test_failing_file_check_916() -> None: test_input = ('#!/usr/bin/env python\n' '# -*- coding: utf-8 -*-\n' 'from __future__ import unicode_literals\n') @@ -3307,7 +3307,7 @@ def test_failing_file_check_916(): assert not SortImports(file_contents=expected_output, check=True, **settings).incorrectly_sorted -def test_import_heading_issue_905(): +def test_import_heading_issue_905() -> None: config = {'import_heading_stdlib': 'Standard library imports', 'import_heading_thirdparty': 'Third party imports', 'import_heading_firstparty': 'Local imports', @@ -3324,7 +3324,7 @@ def test_import_heading_issue_905(): assert SortImports(file_contents=test_input, **config).output == test_input -def test_isort_keeps_comments_issue_691(): +def test_isort_keeps_comments_issue_691() -> None: test_input = ('import os\n' '# This will make sure the app is always imported when\n' '# Django starts so that shared_task will use this app.\n' @@ -3369,7 +3369,7 @@ def test_pyi_formatting_issue_942(tmpdir): assert SortImports(file_path=str(source_pyi)).output.splitlines() == expected_pyi_output -def test_python_version(): +def test_python_version() -> None: from isort.main import parse_args # test that the py_version can be added as flag From f7ce039ece48b7d8dfc3061c90fbc7a566d0b22a Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Wed, 14 Aug 2019 17:39:41 -0700 Subject: [PATCH 0031/1439] Prefer self.method() over MyClass.method() More Pythonic and friendly to subclassing. --- isort/finders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/finders.py b/isort/finders.py index 0fb884dbe..884891340 100644 --- a/isort/finders.py +++ b/isort/finders.py @@ -283,7 +283,7 @@ class RequirementsFinder(ReqsBaseFinder): def _get_files_from_dir(self, path: str) -> Iterator[str]: """Return paths to requirements files from passed dir. """ - return RequirementsFinder._get_files_from_dir_cached(path) + return self._get_files_from_dir_cached(path) @classmethod @lru_cache(maxsize=16) @@ -315,7 +315,7 @@ def _get_files_from_dir_cached(cls, path): def _get_names(self, path: str) -> Iterator[str]: """Load required packages from path to requirements file """ - for i in RequirementsFinder._get_names_cached(path): + for i in self._get_names_cached(path): yield i @classmethod From afa21c7b62e0c4a464ec99103919bfa2c2ef1855 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Thu, 15 Aug 2019 03:47:39 -0700 Subject: [PATCH 0032/1439] Introduce Black to automate code formatting Use Black, the uncompromising Python code formatter, to format the project and simplify contributions. Contributors no longer need to think or care about the project's preferred style, just let the tools handle it instead. Black is becoming more and more popular across Python projects and has recently became a project under the Python Software Foundation. This is especially important with the adoption of mypy as type annotations create another obstacle or consideration in code formatting, but Black handles this just fine. Many projects use both Black and isort. Using Black internally also helps ensure continued compatibility between the two popular projects. For more details on Black, see: https://github.com/psf/black > Black is the uncompromising Python code formatter. By using it, you > agree to cede control over minutiae of hand-formatting. In return, > Black gives you speed, determinism, and freedom from pycodestyle > nagging about formatting. You will save time and mental energy for > more important matters. --- .travis.yml | 1 + isort/compat.py | 158 +- isort/finders.py | 125 +- isort/format.py | 29 +- isort/hooks.py | 18 +- isort/isort.py | 1003 ++++-- isort/main.py | 714 +++-- isort/natural.py | 6 +- isort/pylama_isort.py | 19 +- isort/settings.py | 317 +- isort/stdlibs/py_three.py | 361 ++- isort/stdlibs/py_two.py | 362 ++- isort/utils.py | 12 +- kate_plugin/isort_plugin.py | 24 +- kate_plugin/isort_plugin_old.py | 24 +- setup.cfg | 18 +- setup.py | 82 +- test_isort.py | 5111 ++++++++++++++++++------------- tox.ini | 5 + 19 files changed, 5335 insertions(+), 3054 deletions(-) diff --git a/.travis.yml b/.travis.yml index 28ee6346c..fd4a56696 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,7 @@ env: python: 3.7 matrix: include: + - env: TOXENV=black - env: TOXENV=isort-check - env: TOXENV=lint - env: TOXENV=mypy diff --git a/isort/compat.py b/isort/compat.py index 9f6186366..a82a65ba4 100644 --- a/isort/compat.py +++ b/isort/compat.py @@ -10,16 +10,16 @@ from isort.isort import _SortImports -def determine_file_encoding(file_path: Path, default: str = 'utf-8') -> str: +def determine_file_encoding(file_path: Path, default: str = "utf-8") -> str: # see https://www.python.org/dev/peps/pep-0263/ - pattern = re.compile(br'coding[:=]\s*([-\w.]+)') + pattern = re.compile(br"coding[:=]\s*([-\w.]+)") coding = default - with file_path.open('rb') as f: + with file_path.open("rb") as f: for line_number, line in enumerate(f, 1): groups = re.findall(pattern, line) if groups: - coding = groups[0].decode('ascii') + coding = groups[0].decode("ascii") break if line_number > 2: break @@ -27,15 +27,17 @@ def determine_file_encoding(file_path: Path, default: str = 'utf-8') -> str: return coding -def read_file_contents(file_path: Path, encoding: str, fallback_encoding: str) -> Tuple[Optional[str], Optional[str]]: - with file_path.open(encoding=encoding, newline='') as file_to_import_sort: +def read_file_contents( + file_path: Path, encoding: str, fallback_encoding: str +) -> Tuple[Optional[str], Optional[str]]: + with file_path.open(encoding=encoding, newline="") as file_to_import_sort: try: file_contents = file_to_import_sort.read() return file_contents, encoding except UnicodeDecodeError: pass - with file_path.open(encoding=fallback_encoding, newline='') as file_to_import_sort: + with file_path.open(encoding=fallback_encoding, newline="") as file_to_import_sort: try: file_contents = file_to_import_sort.read() return file_contents, fallback_encoding @@ -50,7 +52,9 @@ def resolve(path: Path) -> Path: return Path(os.path.abspath(str(path))) -def get_settings_path(settings_path: Optional[Path], current_file_path: Optional[Path]) -> Path: +def get_settings_path( + settings_path: Optional[Path], current_file_path: Optional[Path] +) -> Path: if settings_path: return settings_path @@ -65,28 +69,29 @@ class SortImports(object): skipped = False def __init__( - self, - file_path: Optional[str] = None, - file_contents: Optional[str] = None, - write_to_stdout: bool = False, - check: bool = False, - show_diff: bool = False, - settings_path: Optional[str] = None, - ask_to_apply: bool = False, - run_path: str = '', - check_skip: bool = True, - extension: Optional[str] = None, - **setting_overrides: Any + self, + file_path: Optional[str] = None, + file_contents: Optional[str] = None, + write_to_stdout: bool = False, + check: bool = False, + show_diff: bool = False, + settings_path: Optional[str] = None, + ask_to_apply: bool = False, + run_path: str = "", + check_skip: bool = True, + extension: Optional[str] = None, + **setting_overrides: Any ): file_path = None if file_path is None else Path(file_path) file_name = None settings_path = None if settings_path is None else Path(settings_path) - self.config = settings.prepare_config(get_settings_path(settings_path, file_path), - **setting_overrides) + self.config = settings.prepare_config( + get_settings_path(settings_path, file_path), **setting_overrides + ) self.output = None - file_encoding = 'utf-8' + file_encoding = "utf-8" self.file_path = None if file_path: @@ -99,13 +104,17 @@ def __init__( file_name = os.path.relpath(str(absolute_file_path), run_path) else: file_name = str(absolute_file_path) - run_path = '' + run_path = "" if settings.file_should_be_skipped(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(absolute_file_path)) + 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( + absolute_file_path + ) + ) file_contents = None if not self.skipped and not file_contents: @@ -114,16 +123,22 @@ def __init__( # default encoding for open(mode='r') on the system fallback_encoding = locale.getpreferredencoding(False) - file_contents, used_encoding = read_file_contents(absolute_file_path, - encoding=preferred_encoding, - fallback_encoding=fallback_encoding) + file_contents, used_encoding = read_file_contents( + absolute_file_path, + encoding=preferred_encoding, + fallback_encoding=fallback_encoding, + ) if used_encoding is 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(str(absolute_file_path), - file_encoding, - fallback_encoding)) + if self.config["verbose"]: + print( + "WARNING: {} was skipped as it couldn't be opened with the given " + "{} encoding or {} fallback encoding".format( + str(absolute_file_path), + file_encoding, + fallback_encoding, + ) + ) else: file_encoding = used_encoding @@ -134,49 +149,65 @@ def __init__( return if not extension: - extension = file_name.split('.')[-1] if file_name else "py" + extension = file_name.split(".")[-1] if file_name else "py" - self.sorted_imports = _SortImports(file_contents=file_contents, - config=self.config, - extension=extension) + self.sorted_imports = _SortImports( + file_contents=file_contents, config=self.config, extension=extension + ) self.output = self.sorted_imports.output - if self.config['atomic']: - logging_file_path = str(self.file_path or '') + if self.config["atomic"]: + logging_file_path = str(self.file_path or "") try: - out_lines_without_top_comment = self.sorted_imports.get_out_lines_without_top_comment() - compile(out_lines_without_top_comment, logging_file_path, 'exec', 0, 1) + out_lines_without_top_comment = ( + self.sorted_imports.get_out_lines_without_top_comment() + ) + compile(out_lines_without_top_comment, logging_file_path, "exec", 0, 1) except SyntaxError: self.output = file_contents self.incorrectly_sorted = True try: - in_lines_without_top_comment = self.sorted_imports.get_in_lines_without_top_comment() - compile(in_lines_without_top_comment, logging_file_path, 'exec', 0, 1) - print("ERROR: {0} isort would have introduced syntax errors, please report to the project!". - format(logging_file_path)) + in_lines_without_top_comment = ( + self.sorted_imports.get_in_lines_without_top_comment() + ) + compile( + in_lines_without_top_comment, logging_file_path, "exec", 0, 1 + ) + print( + "ERROR: {0} isort would have introduced syntax errors, please report to the project!".format( + logging_file_path + ) + ) except SyntaxError: - print("ERROR: {0} File contains syntax errors.".format(logging_file_path)) + print( + "ERROR: {0} File contains syntax errors.".format( + logging_file_path + ) + ) return if check: check_output = self.output check_against = file_contents - if self.config['ignore_whitespace']: + if self.config["ignore_whitespace"]: check_output = self.sorted_imports.remove_whitespaces(check_output) check_against = self.sorted_imports.remove_whitespaces(check_against) - current_input_sorted_correctly = (self.sorted_imports - .check_if_input_already_sorted(check_output, check_against, - logging_file_path=str(self.file_path or ''))) + current_input_sorted_correctly = self.sorted_imports.check_if_input_already_sorted( + check_output, check_against, logging_file_path=str(self.file_path or "") + ) if current_input_sorted_correctly: return else: self.incorrectly_sorted = True - if show_diff or self.config['show_diff']: - show_unified_diff(file_input=file_contents, file_output=self.output, - file_path=self.file_path) + if show_diff or self.config["show_diff"]: + show_unified_diff( + file_input=file_contents, + file_output=self.output, + file_path=self.file_path, + ) elif write_to_stdout: sys.stdout.write(self.output) @@ -187,14 +218,21 @@ def __init__( return if ask_to_apply: - show_unified_diff(file_input=file_contents, file_output=self.output, - file_path=self.file_path) - apply_changes = ask_whether_to_apply_changes_to_file(str(self.file_path)) + show_unified_diff( + file_input=file_contents, + file_output=self.output, + file_path=self.file_path, + ) + apply_changes = ask_whether_to_apply_changes_to_file( + str(self.file_path) + ) if not apply_changes: return - with self.file_path.open('w', encoding=file_encoding, newline='') as output_file: - if not self.config['quiet']: + with self.file_path.open( + "w", encoding=file_encoding, newline="" + ) as output_file: + if not self.config["quiet"]: print("Fixing {0}".format(self.file_path)) output_file.write(self.output) diff --git a/isort/finders.py b/isort/finders.py index 884891340..51c8d83ee 100644 --- a/isort/finders.py +++ b/isort/finders.py @@ -10,7 +10,19 @@ from fnmatch import fnmatch from functools import lru_cache from glob import glob -from typing import Any, Dict, Iterable, Iterator, List, Mapping, Optional, Pattern, Sequence, Tuple, Type +from typing import ( + Any, + Dict, + Iterable, + Iterator, + List, + Mapping, + Optional, + Pattern, + Sequence, + Tuple, + Type, +) from .utils import chdir, exists_case_sensitive @@ -30,10 +42,10 @@ Pipfile = None KNOWN_SECTION_MAPPING = { - 'STDLIB': 'STANDARD_LIBRARY', - 'FUTURE': 'FUTURE_LIBRARY', - 'FIRSTPARTY': 'FIRST_PARTY', - 'THIRDPARTY': 'THIRD_PARTY', + "STDLIB": "STANDARD_LIBRARY", + "FUTURE": "FUTURE_LIBRARY", + "FIRSTPARTY": "FIRST_PARTY", + "THIRDPARTY": "THIRD_PARTY", } @@ -49,13 +61,13 @@ def find(self, module_name: str) -> Optional[str]: class ForcedSeparateFinder(BaseFinder): def find(self, module_name: str) -> Optional[str]: - for forced_separate in self.config['forced_separate']: + for forced_separate in self.config["forced_separate"]: # Ensure all forced_separate patterns will match to end of string path_glob = forced_separate - if not forced_separate.endswith('*'): - path_glob = '%s*' % forced_separate + if not forced_separate.endswith("*"): + path_glob = "%s*" % forced_separate - if fnmatch(module_name, path_glob) or fnmatch(module_name, '.' + path_glob): + if fnmatch(module_name, path_glob) or fnmatch(module_name, "." + path_glob): return forced_separate return None @@ -74,7 +86,7 @@ def __init__(self, config: Mapping[str, Any], sections: Any) -> None: self.known_patterns = [] # type: List[Tuple[Pattern[str], str]] for placement in reversed(self.sections): known_placement = KNOWN_SECTION_MAPPING.get(placement, placement) - config_key = 'known_{0}'.format(known_placement.lower()) + config_key = "known_{0}".format(known_placement.lower()) known_patterns = self.config.get(config_key, []) known_patterns = [ pattern @@ -82,7 +94,7 @@ def __init__(self, config: Mapping[str, Any], sections: Any) -> None: for pattern in self._parse_known_pattern(known_pattern) ] for known_pattern in known_patterns: - regexp = '^' + known_pattern.replace('*', '.*').replace('?', '.?') + '$' + regexp = "^" + known_pattern.replace("*", ".*").replace("?", ".?") + "$" self.known_patterns.append((re.compile(regexp), placement)) def _parse_known_pattern(self, pattern: str) -> List[str]: @@ -102,8 +114,10 @@ def _parse_known_pattern(self, pattern: str) -> List[str]: def find(self, module_name: str) -> Optional[str]: # Try to find most specific placement instruction match (if any) - parts = module_name.split('.') - module_names_to_check = ('.'.join(parts[:first_k]) for first_k in range(len(parts), 0, -1)) + parts = module_name.split(".") + module_names_to_check = ( + ".".join(parts[:first_k]) for first_k in range(len(parts), 0, -1) + ) for module_name_to_check in module_names_to_check: for pattern, placement in self.known_patterns: if pattern.match(module_name_to_check): @@ -119,35 +133,41 @@ def __init__(self, config: Mapping[str, Any], sections: Any) -> None: self.paths = [os.getcwd()] # virtual env - self.virtual_env = self.config.get('virtual_env') or os.environ.get('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 = '' + self.virtual_env_src = "" if self.virtual_env: - self.virtual_env_src = '{0}/src/'.format(self.virtual_env) - for path in glob('{0}/lib/python*/site-packages'.format(self.virtual_env)): + self.virtual_env_src = "{0}/src/".format(self.virtual_env) + for path in glob("{0}/lib/python*/site-packages".format(self.virtual_env)): if path not in self.paths: self.paths.append(path) - for path in glob('{0}/lib/python*/*/site-packages'.format(self.virtual_env)): + for path in glob( + "{0}/lib/python*/*/site-packages".format(self.virtual_env) + ): if path not in self.paths: self.paths.append(path) - for path in glob('{0}/src/*'.format(self.virtual_env)): + for path in glob("{0}/src/*".format(self.virtual_env)): if os.path.isdir(path): self.paths.append(path) # conda - self.conda_env = self.config.get('conda_env') or os.environ.get('CONDA_PREFIX') or '' + self.conda_env = ( + self.config.get("conda_env") or os.environ.get("CONDA_PREFIX") or "" + ) 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)): + 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)): + 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']) + self.stdlib_lib_prefix = os.path.normcase(sysconfig.get_paths()["stdlib"]) if self.stdlib_lib_prefix not in self.paths: self.paths.append(self.stdlib_lib_prefix) @@ -162,15 +182,19 @@ def __init__(self, config: Mapping[str, Any], sections: Any) -> None: def find(self, module_name: str) -> Optional[str]: 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) or - exists_case_sensitive(package_path + "/__init__.py")) - is_package = exists_case_sensitive(package_path) and os.path.isdir(package_path) + is_module = ( + exists_case_sensitive(package_path + ".py") + or exists_case_sensitive(package_path + ".so") + or 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: + if "site-packages" in prefix: return self.sections.THIRDPARTY - if 'dist-packages' in prefix: + if "dist-packages" in prefix: return self.sections.THIRDPARTY if self.virtual_env and self.virtual_env_src in prefix: return self.sections.THIRDPARTY @@ -178,14 +202,16 @@ def find(self, module_name: str) -> Optional[str]: return self.sections.THIRDPARTY if os.path.normcase(prefix).startswith(self.stdlib_lib_prefix): return self.sections.STDLIB - return self.config['default_section'] + return self.config["default_section"] return None class ReqsBaseFinder(BaseFinder): enabled = False - def __init__(self, config: Mapping[str, Any], sections: Any, path: str = '.') -> None: + def __init__( + self, config: Mapping[str, Any], sections: Any, path: str = "." + ) -> None: super().__init__(config, sections) self.path = path if self.enabled: @@ -210,7 +236,7 @@ def _load_mapping() -> Optional[Dict[str, str]]: if not pipreqs: return None path = os.path.dirname(inspect.getfile(pipreqs)) - path = os.path.join(path, 'mapping') + path = os.path.join(path, "mapping") with open(path) as f: # pypi_name: import_name mappings = {} # type: Dict[str, str] @@ -231,7 +257,7 @@ def _load_names(self) -> List[str]: @staticmethod def _get_parents(path: str) -> Iterator[str]: - prev = '' + prev = "" while path != prev: prev = path yield path @@ -258,14 +284,14 @@ def _normalize_name(self, name: str) -> str: """ if self.mapping: name = self.mapping.get(name, name) - return name.lower().replace('-', '_') + return name.lower().replace("-", "_") def find(self, module_name: str) -> Optional[str]: # required lib not installed yet if not self.enabled: return None - module_name, _sep, _submodules = module_name.partition('.') + module_name, _sep, _submodules = module_name.partition(".") module_name = module_name.lower() if not module_name: return None @@ -277,7 +303,7 @@ def find(self, module_name: str) -> Optional[str]: class RequirementsFinder(ReqsBaseFinder): - exts = ('.txt', '.in') + exts = (".txt", ".in") enabled = bool(parse_requirements) def _get_files_from_dir(self, path: str) -> Iterator[str]: @@ -291,7 +317,7 @@ def _get_files_from_dir_cached(cls, path): results = [] for fname in os.listdir(path): - if 'requirements' not in fname: + if "requirements" not in fname: continue full_path = os.path.join(path, fname) @@ -342,13 +368,13 @@ def _get_names(self, path: str) -> Iterator[str]: yield req.name def _get_files_from_dir(self, path: str) -> Iterator[str]: - if 'Pipfile' in os.listdir(path): + if "Pipfile" in os.listdir(path): yield path class DefaultFinder(BaseFinder): def find(self, module_name: str) -> Optional[str]: - return self.config['default_section'] + return self.config["default_section"] class FindersManager(object): @@ -366,9 +392,9 @@ def __init__( self, config: Mapping[str, Any], sections: Any, - finder_classes: Optional[Iterable[Type[BaseFinder]]] = None + finder_classes: Optional[Iterable[Type[BaseFinder]]] = None, ) -> None: - self.verbose = config.get('verbose', False) # type: bool + self.verbose = config.get("verbose", False) # type: bool if finder_classes is None: finder_classes = self._default_finders_classes @@ -379,8 +405,11 @@ def __init__( except Exception as exception: # if one finder fails to instantiate isort can continue using the rest if self.verbose: - print('{} encountered an error ({}) during instantiation and cannot be used'.format(finder_cls.__name__, - str(exception))) + print( + "{} encountered an error ({}) during instantiation and cannot be used".format( + finder_cls.__name__, str(exception) + ) + ) self.finders = tuple(finders) # type: Tuple[BaseFinder, ...] def find(self, module_name: str) -> Optional[str]: @@ -390,9 +419,11 @@ def find(self, module_name: str) -> Optional[str]: except Exception as exception: # isort has to be able to keep trying to identify the correct import section even if one approach fails if self.verbose: - print('{} encountered an error ({}) while trying to identify the {} module'.format(finder.__class__.__name__, - str(exception), - module_name)) + print( + "{} encountered an error ({}) while trying to identify the {} module".format( + finder.__class__.__name__, str(exception), module_name + ) + ) if section is not None: return section return None diff --git a/isort/format.py b/isort/format.py index e008eab0c..018bd102a 100644 --- a/isort/format.py +++ b/isort/format.py @@ -28,18 +28,23 @@ def format_natural(import_line: str) -> str: return import_line -def show_unified_diff(*, file_input: str, file_output: str, file_path: Optional[Path]) -> None: - file_name = '' if file_path is None else str(file_path) - file_mtime = str(datetime.now() if file_path is None - else datetime.fromtimestamp(file_path.stat().st_mtime)) +def show_unified_diff( + *, file_input: str, file_output: str, file_path: Optional[Path] +) -> None: + file_name = "" if file_path is None else str(file_path) + file_mtime = str( + datetime.now() + if file_path is None + else datetime.fromtimestamp(file_path.stat().st_mtime) + ) unified_diff_lines = unified_diff( file_input.splitlines(keepends=True), file_output.splitlines(keepends=True), - fromfile=file_name + ':before', - tofile=file_name + ':after', + fromfile=file_name + ":before", + tofile=file_name + ":after", fromfiledate=file_mtime, - tofiledate=str(datetime.now()) + tofiledate=str(datetime.now()), ) for line in unified_diff_lines: sys.stdout.write(line) @@ -47,10 +52,12 @@ def show_unified_diff(*, file_input: str, file_output: str, file_path: Optional[ def ask_whether_to_apply_changes_to_file(file_path: str) -> bool: answer = None - while answer not in ('yes', 'y', 'no', 'n', 'quit', 'q'): - answer = input("Apply suggested changes to '{0}' [y/n/q]? ".format(file_path)).lower() - if answer in ('no', 'n'): + while answer not in ("yes", "y", "no", "n", "quit", "q"): + answer = input( + "Apply suggested changes to '{0}' [y/n/q]? ".format(file_path) + ).lower() + if answer in ("no", "n"): return False - if answer in ('quit', 'q'): + if answer in ("quit", "q"): sys.exit(1) return True diff --git a/isort/hooks.py b/isort/hooks.py index 2e0752704..c2e59b30f 100644 --- a/isort/hooks.py +++ b/isort/hooks.py @@ -65,29 +65,31 @@ def git_hook(strict=False, modify=False): """ # Get list of files modified and staged - diff_cmd = ["git", "diff-index", "--cached", "--name-only", "--diff-filter=ACMRTUXB HEAD"] + diff_cmd = [ + "git", + "diff-index", + "--cached", + "--name-only", + "--diff-filter=ACMRTUXB HEAD", + ] files_modified = get_lines(diff_cmd) errors = 0 for filename in files_modified: - if filename.endswith('.py'): + if filename.endswith(".py"): # Get the staged contents of the file staged_cmd = ["git", "show", ":%s" % filename] staged_contents = get_output(staged_cmd) sort = SortImports( - file_path=filename, - file_contents=staged_contents, - check=True + file_path=filename, file_contents=staged_contents, check=True ) if sort.incorrectly_sorted: errors += 1 if modify: SortImports( - file_path=filename, - file_contents=staged_contents, - check=False, + file_path=filename, file_contents=staged_contents, check=False ) return errors if strict else 0 diff --git a/isort/isort.py b/isort/isort.py index 93d42a6dd..070e1e82c 100644 --- a/isort/isort.py +++ b/isort/isort.py @@ -28,7 +28,17 @@ import itertools import re from collections import OrderedDict, defaultdict, namedtuple -from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple +from typing import ( + TYPE_CHECKING, + Any, + Dict, + Iterable, + List, + Mapping, + Optional, + Sequence, + Tuple, +) from isort import utils from isort.format import format_natural, format_simplified @@ -41,48 +51,68 @@ if TYPE_CHECKING: from mypy_extensions import TypedDict - CommentsAboveDict = TypedDict('CommentsAboveDict', { - 'straight': Dict[str, Any], - 'from': Dict[str, Any] - }) - CommentsDict = TypedDict('CommentsDict', { - 'from': Dict[str, Any], - 'straight': Dict[str, Any], - 'nested': Dict[str, Any], - 'above': CommentsAboveDict - }) + CommentsAboveDict = TypedDict( + "CommentsAboveDict", {"straight": Dict[str, Any], "from": Dict[str, Any]} + ) + CommentsDict = TypedDict( + "CommentsDict", + { + "from": Dict[str, Any], + "straight": Dict[str, Any], + "nested": Dict[str, Any], + "above": CommentsAboveDict, + }, + ) class _SortImports(object): - def __init__(self, file_contents: str, config: Dict[str, Any], extension: str = "py") -> None: + def __init__( + self, file_contents: str, config: Dict[str, Any], extension: str = "py" + ) -> None: self.config = config self.extension = extension self.place_imports = {} # type: Dict[str, List[str]] self.import_placements = {} # type: Dict[str, str] - self.remove_imports = [format_simplified(removal) for removal in self.config['remove_imports']] - self.add_imports = [format_natural(addition) for addition in self.config['add_imports']] - self._section_comments = ["# " + value for key, value in self.config.items() - if key.startswith('import_heading') and value] + self.remove_imports = [ + format_simplified(removal) for removal in self.config["remove_imports"] + ] + self.add_imports = [ + format_natural(addition) for addition in self.config["add_imports"] + ] + self._section_comments = [ + "# " + value + for key, value in self.config.items() + if key.startswith("import_heading") and value + ] self.line_separator = self.determine_line_separator(file_contents) self.in_lines = file_contents.split(self.line_separator) self.original_num_of_lines = len(self.in_lines) - if (self.original_num_of_lines > 1 or self.in_lines[:1] not in ([], [""])) or self.config['force_adds']: + if ( + self.original_num_of_lines > 1 or self.in_lines[:1] not in ([], [""]) + ) or self.config["force_adds"]: for add_import in self.add_imports: self.in_lines.append(add_import) self.number_of_lines = len(self.in_lines) self.out_lines = [] # type: List[str] - self.comments = {'from': {}, 'straight': {}, 'nested': {}, 'above': {'straight': {}, 'from': {}}} # type: CommentsDict + self.comments = { + "from": {}, + "straight": {}, + "nested": {}, + "above": {"straight": {}, "from": {}}, + } # type: CommentsDict self.imports = OrderedDict() # type: OrderedDict[str, Dict[str, Any]] self.as_map = defaultdict(list) # type: Dict[str, List[str]] - section_names = self.config['sections'] - self.sections = namedtuple('Sections', section_names)(*[name for name in section_names]) # type: Any - for section in itertools.chain(self.sections, self.config['forced_separate']): - self.imports[section] = {'straight': OrderedDict(), 'from': OrderedDict()} + section_names = self.config["sections"] + self.sections = namedtuple("Sections", section_names)( + *[name for name in section_names] + ) # type: Any + for section in itertools.chain(self.sections, self.config["forced_separate"]): + self.imports[section] = {"straight": OrderedDict(), "from": OrderedDict()} self.finder = FindersManager(config=self.config, sections=self.sections) @@ -100,10 +130,11 @@ def __init__(self, file_contents: str, config: Dict[str, Any], extension: str = self.output = self.line_separator.join(self.out_lines) def remove_whitespaces(self, contents: str) -> str: - contents = (contents - .replace(self.line_separator, "") - .replace(" ", "") - .replace("\x0c", "")) + contents = ( + contents.replace(self.line_separator, "") + .replace(" ", "") + .replace("\x0c", "") + ) return contents def get_out_lines_without_top_comment(self) -> str: @@ -112,10 +143,11 @@ def get_out_lines_without_top_comment(self) -> str: def get_in_lines_without_top_comment(self) -> str: return self._strip_top_comments(self.in_lines, self.line_separator) - def check_if_input_already_sorted(self, output: str, check_against: str, - *, logging_file_path: str) -> bool: + def check_if_input_already_sorted( + self, output: str, check_against: str, *, logging_file_path: str + ) -> bool: if output.strip() == check_against.strip(): - if self.config['verbose']: + if self.config["verbose"]: print("SUCCESS: {0} Everything Looks Good!".format(logging_file_path)) return True @@ -123,8 +155,8 @@ def check_if_input_already_sorted(self, output: str, check_against: str, return False def determine_line_separator(self, file_contents: str) -> str: - if self.config['line_ending']: - return self.config['line_ending'] + if self.config["line_ending"]: + return self.config["line_ending"] else: return utils.infer_line_separator(file_contents) @@ -155,9 +187,9 @@ def _import_type(line: str) -> Optional[str]: """If the current line is an import line it will return its type (from or straight)""" if "isort:skip" in line or "NOQA" in line: return None - elif line.startswith('import '): + elif line.startswith("import "): return "straight" - elif line.startswith('from '): + elif line.startswith("from "): return "from" return None @@ -171,11 +203,11 @@ def _module_key( config: Mapping[str, Any], sub_imports: bool = False, ignore_case: bool = False, - section_name: Optional[Any] = None + section_name: Optional[Any] = None, ) -> str: - match = re.match(r'^(\.+)\s*(.*)', module_name) + match = re.match(r"^(\.+)\s*(.*)", module_name) if match: - sep = ' ' if config['reverse_relative'] else '_' + sep = " " if config["reverse_relative"] else "_" module_name = sep.join(match.groups()) prefix = "" @@ -184,159 +216,247 @@ def _module_key( else: module_name = str(module_name) - if sub_imports and config['order_by_type']: + if sub_imports and config["order_by_type"]: if module_name.isupper() and len(module_name) > 1: prefix = "A" elif module_name[0:1].isupper(): prefix = "B" else: prefix = "C" - if not config['case_sensitive']: + 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'] + if ( + section_name is None + or "length_sort_" + str(section_name).lower() not in config + ): + length_sort = config["length_sort"] else: - length_sort = config['length_sort_' + str(section_name).lower()] - return "{0}{1}{2}".format(module_name in config['force_to_top'] and "A" or "B", prefix, - length_sort and (str(len(module_name)) + ":" + module_name) or module_name) + length_sort = config["length_sort_" + str(section_name).lower()] + return "{0}{1}{2}".format( + module_name in config["force_to_top"] and "A" or "B", + prefix, + length_sort and (str(len(module_name)) + ":" + module_name) or module_name, + ) def _add_comments( - self, - comments: Optional[Sequence[str]], - original_string: str = "" + self, comments: Optional[Sequence[str]], original_string: str = "" ) -> str: """ Returns a string with comments added if ignore_comments is not set. """ - if self.config['ignore_comments']: + if self.config["ignore_comments"]: return self._strip_comments(original_string)[0] if not comments: return original_string else: - return "{0}{1} {2}".format(self._strip_comments(original_string)[0], - self.config['comment_prefix'], - "; ".join(comments)) + return "{0}{1} {2}".format( + self._strip_comments(original_string)[0], + self.config["comment_prefix"], + "; ".join(comments), + ) def _wrap(self, line: str) -> str: """ Returns an import wrapped to the specified line-length, if possible. """ - wrap_mode = self.config['multi_line_output'] - if len(line) > self.config['line_length'] and wrap_mode != WrapModes.NOQA: + wrap_mode = self.config["multi_line_output"] + if len(line) > self.config["line_length"] and wrap_mode != WrapModes.NOQA: line_without_comment = line comment = None - if '#' in line: - line_without_comment, comment = line.split('#', 1) + if "#" in line: + line_without_comment, comment = line.split("#", 1) for splitter in ("import ", ".", "as "): exp = r"\b" + re.escape(splitter) + r"\b" - if re.search(exp, line_without_comment) and not line_without_comment.strip().startswith(splitter): + if re.search( + exp, line_without_comment + ) and not line_without_comment.strip().startswith(splitter): line_parts = re.split(exp, line_without_comment) if comment: - line_parts[-1] = '{0}#{1}'.format(line_parts[-1], comment) + line_parts[-1] = "{0}#{1}".format(line_parts[-1], comment) next_line = [] - while (len(line) + 2) > (self.config['wrap_length'] or self.config['line_length']) and line_parts: + while (len(line) + 2) > ( + self.config["wrap_length"] or self.config["line_length"] + ) and line_parts: next_line.append(line_parts.pop()) line = splitter.join(line_parts) if not line: line = next_line.pop() - cont_line = self._wrap(self.config['indent'] + splitter.join(next_line).lstrip()) - if self.config['use_parentheses']: + cont_line = self._wrap( + self.config["indent"] + splitter.join(next_line).lstrip() + ) + if self.config["use_parentheses"]: if splitter == "as ": - output = "{0}{1}{2}".format(line, splitter, cont_line.lstrip()) + output = "{0}{1}{2}".format( + line, splitter, cont_line.lstrip() + ) else: output = "{0}{1}({2}{3}{4}{5})".format( - line, splitter, self.line_separator, cont_line, - "," if self.config['include_trailing_comma'] else "", - self.line_separator if wrap_mode in {WrapModes.VERTICAL_HANGING_INDENT, - WrapModes.VERTICAL_GRID_GROUPED} - else "") + line, + splitter, + self.line_separator, + cont_line, + "," if self.config["include_trailing_comma"] else "", + self.line_separator + if wrap_mode + in { + WrapModes.VERTICAL_HANGING_INDENT, + WrapModes.VERTICAL_GRID_GROUPED, + } + else "", + ) lines = output.split(self.line_separator) - if self.config['comment_prefix'] in lines[-1] and lines[-1].endswith(')'): - line, comment = lines[-1].split(self.config['comment_prefix'], 1) - lines[-1] = line + ')' + self.config['comment_prefix'] + comment[:-1] + if self.config["comment_prefix"] in lines[-1] and lines[ + -1 + ].endswith(")"): + line, comment = lines[-1].split( + self.config["comment_prefix"], 1 + ) + lines[-1] = ( + line + + ")" + + self.config["comment_prefix"] + + comment[:-1] + ) return self.line_separator.join(lines) - return "{0}{1}\\{2}{3}".format(line, splitter, self.line_separator, cont_line) - elif len(line) > self.config['line_length'] and wrap_mode == settings.WrapModes.NOQA: + return "{0}{1}\\{2}{3}".format( + line, splitter, self.line_separator, cont_line + ) + elif ( + len(line) > self.config["line_length"] + and wrap_mode == settings.WrapModes.NOQA + ): if "# NOQA" not in line: - return "{0}{1} NOQA".format(line, self.config['comment_prefix']) + return "{0}{1} NOQA".format(line, self.config["comment_prefix"]) return line - def _add_straight_imports(self, straight_modules: Iterable[str], section: str, section_output: List[str]) -> None: + def _add_straight_imports( + self, straight_modules: Iterable[str], section: str, section_output: List[str] + ) -> None: for module in straight_modules: if module in self.remove_imports: continue import_definition = [] if module in self.as_map: - if self.config['keep_direct_and_as_imports'] and self.imports[section]['straight'][module]: + if ( + self.config["keep_direct_and_as_imports"] + and self.imports[section]["straight"][module] + ): import_definition.append("import {0}".format(module)) - import_definition.extend("import {0} as {1}".format(module, as_import) - for as_import in self.as_map[module]) + import_definition.extend( + "import {0} as {1}".format(module, as_import) + for as_import in self.as_map[module] + ) else: import_definition.append("import {0}".format(module)) - comments_above = self.comments['above']['straight'].pop(module, None) + comments_above = self.comments["above"]["straight"].pop(module, None) if comments_above: section_output.extend(comments_above) - section_output.extend(self._add_comments(self.comments['straight'].get(module), idef) - for idef in import_definition) + section_output.extend( + self._add_comments(self.comments["straight"].get(module), idef) + for idef in import_definition + ) - def _add_from_imports(self, from_modules: Iterable[str], section: str, section_output: List[str], ignore_case: bool) -> None: + def _add_from_imports( + self, + from_modules: Iterable[str], + section: str, + section_output: List[str], + ignore_case: bool, + ) -> None: for module in from_modules: if module in self.remove_imports: continue import_start = "from {0} import ".format(module) - from_imports = list(self.imports[section]['from'][module]) - if not self.config['no_inline_sort'] or self.config['force_single_line']: - from_imports = nsorted(from_imports, key=lambda key: self._module_key(key, self.config, True, ignore_case, section_name=section)) + from_imports = list(self.imports[section]["from"][module]) + if not self.config["no_inline_sort"] or self.config["force_single_line"]: + from_imports = nsorted( + from_imports, + key=lambda key: self._module_key( + key, self.config, True, ignore_case, section_name=section + ), + ) if self.remove_imports: - from_imports = [line for line in from_imports if not "{0}.{1}".format(module, line) in - self.remove_imports] - - sub_modules = ['{0}.{1}'.format(module, from_import) for from_import in from_imports] + from_imports = [ + line + for line in from_imports + if not "{0}.{1}".format(module, line) in self.remove_imports + ] + + sub_modules = [ + "{0}.{1}".format(module, from_import) for from_import in from_imports + ] as_imports = { - from_import: ["{0} as {1}".format(from_import, as_module) - for as_module in self.as_map[sub_module]] + from_import: [ + "{0} as {1}".format(from_import, as_module) + for as_module in self.as_map[sub_module] + ] for from_import, sub_module in zip(from_imports, sub_modules) if sub_module in self.as_map } - if self.config['combine_as_imports'] and not ("*" in from_imports and self.config['combine_star']): - if not self.config['no_inline_sort']: + if self.config["combine_as_imports"] and not ( + "*" in from_imports and self.config["combine_star"] + ): + if not self.config["no_inline_sort"]: for as_import in as_imports: as_imports[as_import] = nsorted(as_imports[as_import]) for from_import in copy.copy(from_imports): if from_import in as_imports: idx = from_imports.index(from_import) - if self.config['keep_direct_and_as_imports'] and self.imports[section]['from'][module][from_import]: - from_imports[(idx+1):(idx+1)] = as_imports.pop(from_import) + if ( + self.config["keep_direct_and_as_imports"] + and self.imports[section]["from"][module][from_import] + ): + from_imports[(idx + 1) : (idx + 1)] = as_imports.pop( + from_import + ) else: - from_imports[idx:(idx+1)] = as_imports.pop(from_import) + from_imports[idx : (idx + 1)] = as_imports.pop(from_import) while from_imports: - comments = self.comments['from'].pop(module, ()) - if "*" in from_imports and self.config['combine_star']: - import_statement = self._wrap(self._add_comments(comments, "{0}*".format(import_start))) + comments = self.comments["from"].pop(module, ()) + if "*" in from_imports and self.config["combine_star"]: + import_statement = self._wrap( + self._add_comments(comments, "{0}*".format(import_start)) + ) from_imports = None - elif self.config['force_single_line']: + elif self.config["force_single_line"]: import_statements = [] while from_imports: from_import = from_imports.pop(0) - single_import_line = self._add_comments(comments, import_start + from_import) - comment = self.comments['nested'].get(module, {}).pop(from_import, None) + single_import_line = self._add_comments( + comments, import_start + from_import + ) + comment = ( + self.comments["nested"] + .get(module, {}) + .pop(from_import, None) + ) if comment: - single_import_line += "{0} {1}".format(comments and ";" or self.config['comment_prefix'], - comment) + single_import_line += "{0} {1}".format( + comments and ";" or self.config["comment_prefix"], + comment, + ) if from_import in as_imports: - if self.config['keep_direct_and_as_imports'] and self.imports[section]['from'][module][from_import]: + if ( + self.config["keep_direct_and_as_imports"] + and self.imports[section]["from"][module][from_import] + ): import_statements.append(self._wrap(single_import_line)) - from_comments = self.comments['straight'].get('{}.{}'.format(module, from_import)) - import_statements.extend(self._add_comments(from_comments, - self._wrap(import_start + as_import)) - for as_import in nsorted(as_imports[from_import])) + from_comments = self.comments["straight"].get( + "{}.{}".format(module, from_import) + ) + import_statements.extend( + self._add_comments( + from_comments, self._wrap(import_start + as_import) + ) + for as_import in nsorted(as_imports[from_import]) + ) else: import_statements.append(self._wrap(single_import_line)) comments = None @@ -345,32 +465,63 @@ def _add_from_imports(self, from_modules: Iterable[str], section: str, section_o while from_imports and from_imports[0] in as_imports: from_import = from_imports.pop(0) as_imports[from_import] = nsorted(as_imports[from_import]) - from_comments = self.comments['straight'].get('{}.{}'.format(module, from_import)) - above_comments = self.comments['above']['from'].pop(module, None) + from_comments = self.comments["straight"].get( + "{}.{}".format(module, from_import) + ) + above_comments = self.comments["above"]["from"].pop( + module, None + ) if above_comments: section_output.extend(above_comments) - if self.config['keep_direct_and_as_imports'] and self.imports[section]['from'][module][from_import]: - section_output.append(self._add_comments(from_comments, self._wrap(import_start + from_import))) - section_output.extend(self._add_comments(from_comments, self._wrap(import_start + as_import)) - for as_import in as_imports[from_import]) + if ( + self.config["keep_direct_and_as_imports"] + and self.imports[section]["from"][module][from_import] + ): + section_output.append( + self._add_comments( + from_comments, + self._wrap(import_start + from_import), + ) + ) + section_output.extend( + self._add_comments( + from_comments, self._wrap(import_start + as_import) + ) + for as_import in as_imports[from_import] + ) star_import = False if "*" in from_imports: - section_output.append(self._add_comments(comments, "{0}*".format(import_start))) - from_imports.remove('*') + section_output.append( + self._add_comments(comments, "{0}*".format(import_start)) + ) + from_imports.remove("*") star_import = True comments = None for from_import in copy.copy(from_imports): - if from_import in as_imports and not self.config['keep_direct_and_as_imports']: + if ( + from_import in as_imports + and not self.config["keep_direct_and_as_imports"] + ): continue - comment = self.comments['nested'].get(module, {}).pop(from_import, None) + comment = ( + self.comments["nested"] + .get(module, {}) + .pop(from_import, None) + ) if comment: - single_import_line = self._add_comments(comments, import_start + from_import) - single_import_line += "{0} {1}".format(comments and ";" or self.config['comment_prefix'], - comment) - above_comments = self.comments['above']['from'].pop(module, None) + single_import_line = self._add_comments( + comments, import_start + from_import + ) + single_import_line += "{0} {1}".format( + comments and ";" or self.config["comment_prefix"], + comment, + ) + above_comments = self.comments["above"]["from"].pop( + module, None + ) if above_comments: section_output.extend(above_comments) section_output.append(self._wrap(single_import_line)) @@ -378,61 +529,97 @@ def _add_from_imports(self, from_modules: Iterable[str], section: str, section_o comments = None from_import_section = [] - while from_imports and (from_imports[0] not in as_imports or - (self.config['keep_direct_and_as_imports'] and - self.config['combine_as_imports'] and - self.imports[section]['from'][module][from_import])): + while from_imports and ( + from_imports[0] not in as_imports + or ( + self.config["keep_direct_and_as_imports"] + and self.config["combine_as_imports"] + and self.imports[section]["from"][module][from_import] + ) + ): from_import_section.append(from_imports.pop(0)) if star_import: - import_statement = import_start + (", ").join(from_import_section) + import_statement = import_start + (", ").join( + from_import_section + ) else: - import_statement = self._add_comments(comments, import_start + (", ").join(from_import_section)) + import_statement = self._add_comments( + comments, import_start + (", ").join(from_import_section) + ) if not from_import_section: import_statement = "" do_multiline_reformat = False - force_grid_wrap = self.config['force_grid_wrap'] + force_grid_wrap = self.config["force_grid_wrap"] if force_grid_wrap and len(from_import_section) >= force_grid_wrap: do_multiline_reformat = True - if len(import_statement) > self.config['line_length'] and len(from_import_section) > 1: + if ( + len(import_statement) > self.config["line_length"] + and len(from_import_section) > 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_import_section) > 0 and - self.config['multi_line_output'] not in (settings.WrapModes.GRID, settings.WrapModes.VERTICAL)): + if ( + len(import_statement) > self.config["line_length"] + and len(from_import_section) > 0 + and self.config["multi_line_output"] + not in (settings.WrapModes.GRID, settings.WrapModes.VERTICAL) + ): do_multiline_reformat = True if do_multiline_reformat: - import_statement = self._multi_line_reformat(import_start, from_import_section, comments) - if self.config['multi_line_output'] == settings.WrapModes.GRID: - self.config['multi_line_output'] = settings.WrapModes.VERTICAL_GRID + import_statement = self._multi_line_reformat( + import_start, from_import_section, comments + ) + if self.config["multi_line_output"] == settings.WrapModes.GRID: + self.config[ + "multi_line_output" + ] = settings.WrapModes.VERTICAL_GRID try: - other_import_statement = self._multi_line_reformat(import_start, from_import_section, comments) - if (max(len(x) - for x in import_statement.split('\n')) > self.config['line_length']): + other_import_statement = self._multi_line_reformat( + import_start, from_import_section, comments + ) + if ( + max(len(x) for x in import_statement.split("\n")) + > self.config["line_length"] + ): import_statement = other_import_statement finally: - self.config['multi_line_output'] = settings.WrapModes.GRID - if not do_multiline_reformat and len(import_statement) > self.config['line_length']: + self.config[ + "multi_line_output" + ] = settings.WrapModes.GRID + if ( + not do_multiline_reformat + and len(import_statement) > self.config["line_length"] + ): import_statement = self._wrap(import_statement) if import_statement: - above_comments = self.comments['above']['from'].pop(module, None) + above_comments = self.comments["above"]["from"].pop(module, None) if above_comments: section_output.extend(above_comments) section_output.append(import_statement) - def _multi_line_reformat(self, import_start: str, from_imports: List[str], comments: Sequence[str]) -> str: - output_mode = self.config['multi_line_output'].name.lower() + def _multi_line_reformat( + self, import_start: str, from_imports: List[str], comments: Sequence[str] + ) -> str: + output_mode = self.config["multi_line_output"].name.lower() formatter = getattr(self, "_output_" + output_mode, self._output_grid) dynamic_indent = " " * (len(import_start) + 1) - indent = self.config['indent'] - line_length = self.config['wrap_length'] or self.config['line_length'] - import_statement = formatter(import_start, copy.copy(from_imports), - dynamic_indent, indent, line_length, comments) - if self.config['balanced_wrapping']: + indent = self.config["indent"] + line_length = self.config["wrap_length"] or self.config["line_length"] + import_statement = formatter( + import_start, + copy.copy(from_imports), + dynamic_indent, + indent, + line_length, + comments, + ) + if self.config["balanced_wrapping"]: lines = import_statement.split(self.line_separator) line_count = len(lines) if len(lines) > 1: @@ -440,12 +627,21 @@ def _multi_line_reformat(self, import_start: str, from_imports: List[str], comme else: minimum_length = 0 new_import_statement = import_statement - while (len(lines[-1]) < minimum_length and - len(lines) == line_count and line_length > 10): + while ( + len(lines[-1]) < minimum_length + and len(lines) == line_count + and line_length > 10 + ): import_statement = new_import_statement line_length -= 1 - new_import_statement = formatter(import_start, copy.copy(from_imports), - dynamic_indent, indent, line_length, comments) + new_import_statement = formatter( + import_start, + copy.copy(from_imports), + dynamic_indent, + indent, + line_length, + comments, + ) lines = new_import_statement.split(self.line_separator) if import_statement.count(self.line_separator) == 0: return self._wrap(import_statement) @@ -457,67 +653,102 @@ def _add_formatted_imports(self) -> None: (at the index of the first import) sorted alphabetically and split between groups """ - sort_ignore_case = self.config['force_alphabetical_sort_within_sections'] - sections = itertools.chain(self.sections, self.config['forced_separate']) # type: Iterable[str] + sort_ignore_case = self.config["force_alphabetical_sort_within_sections"] + sections = itertools.chain( + self.sections, self.config["forced_separate"] + ) # type: Iterable[str] - if self.config['no_sections']: - self.imports['no_sections'] = {'straight': [], 'from': {}} + if self.config["no_sections"]: + self.imports["no_sections"] = {"straight": [], "from": {}} for section in sections: - self.imports['no_sections']['straight'].extend(self.imports[section].get('straight', [])) - self.imports['no_sections']['from'].update(self.imports[section].get('from', {})) - sections = ('no_sections', ) + self.imports["no_sections"]["straight"].extend( + self.imports[section].get("straight", []) + ) + self.imports["no_sections"]["from"].update( + self.imports[section].get("from", {}) + ) + sections = ("no_sections",) output = [] # type: List[str] 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)) - from_modules = self.imports[section]['from'] - from_modules = nsorted(from_modules, key=lambda key: self._module_key(key, self.config, section_name=section)) + straight_modules = self.imports[section]["straight"] + straight_modules = nsorted( + straight_modules, + key=lambda key: self._module_key( + key, self.config, section_name=section + ), + ) + from_modules = self.imports[section]["from"] + from_modules = nsorted( + from_modules, + key=lambda key: self._module_key( + key, self.config, section_name=section + ), + ) section_output = [] # type: List[str] - if self.config['from_first']: - self._add_from_imports(from_modules, section, section_output, sort_ignore_case) - if self.config['lines_between_types'] and from_modules and straight_modules: - section_output.extend([''] * self.config['lines_between_types']) + if self.config["from_first"]: + self._add_from_imports( + from_modules, section, section_output, sort_ignore_case + ) + if ( + self.config["lines_between_types"] + and from_modules + and straight_modules + ): + section_output.extend([""] * self.config["lines_between_types"]) self._add_straight_imports(straight_modules, section, section_output) else: self._add_straight_imports(straight_modules, section, section_output) - if self.config['lines_between_types'] and from_modules and straight_modules: - section_output.extend([''] * self.config['lines_between_types']) - self._add_from_imports(from_modules, section, section_output, sort_ignore_case) + if ( + self.config["lines_between_types"] + and from_modules + and straight_modules + ): + section_output.extend([""] * self.config["lines_between_types"]) + self._add_from_imports( + from_modules, section, section_output, sort_ignore_case + ) + + if self.config["force_sort_within_sections"]: - if self.config['force_sort_within_sections']: def by_module(line: str) -> str: - section = 'B' - if line.startswith('#'): - return 'AA' - - line = re.sub('^from ', '', line) - line = re.sub('^import ', '', line) - if line.split(' ')[0] in self.config['force_to_top']: - section = 'A' - if not self.config['order_by_type']: + section = "B" + if line.startswith("#"): + return "AA" + + line = re.sub("^from ", "", line) + line = re.sub("^import ", "", line) + if line.split(" ")[0] in self.config["force_to_top"]: + section = "A" + if not self.config["order_by_type"]: line = line.lower() - return '{0}{1}'.format(section, line) + 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'] + no_lines_before = section_name in self.config["no_lines_before"] if section_output: if section_name in self.place_imports: self.place_imports[section_name] = section_output continue - section_title = self.config.get('import_heading_' + str(section_name).lower(), '') + section_title = self.config.get( + "import_heading_" + str(section_name).lower(), "" + ) if section_title: 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]: + 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 pending_lines_before or not no_lines_before: - output += ([''] * self.config['lines_between_sections']) + output += [""] * self.config["lines_between_sections"] output += section_output @@ -525,20 +756,25 @@ def by_module(line: str) -> str: else: pending_lines_before = pending_lines_before or not no_lines_before - while output and output[-1].strip() == '': + while output and output[-1].strip() == "": output.pop() - while output and output[0].strip() == '': + while output and output[0].strip() == "": output.pop(0) output_at = 0 if self.import_index < self.original_num_of_lines: output_at = self.import_index - elif self._first_comment_index_end != -1 and self._first_comment_index_start <= 2: + elif ( + self._first_comment_index_end != -1 and self._first_comment_index_start <= 2 + ): output_at = self._first_comment_index_end self.out_lines[output_at:0] = output imports_tail = output_at + len(output) - while [character.strip() for character in self.out_lines[imports_tail: imports_tail + 1]] == [""]: + while [ + character.strip() + for character in self.out_lines[imports_tail : imports_tail + 1] + ] == [""]: self.out_lines.pop(imports_tail) if len(self.out_lines) > imports_tail: @@ -549,22 +785,35 @@ def by_module(line: str) -> str: for index, line in enumerate(tail): in_quote = self._in_quote if not self._skip_line(line) and line.strip(): - if line.strip().startswith("#") and len(tail) > (index + 1) and tail[index + 1].strip(): + if ( + line.strip().startswith("#") + and len(tail) > (index + 1) + and tail[index + 1].strip() + ): continue next_construct = line break elif not in_quote: parts = line.split() - if len(parts) >= 3 and parts[1] == '=' and "'" not in parts[0] and '"' not in parts[0]: + if ( + len(parts) >= 3 + and parts[1] == "=" + and "'" not in parts[0] + and '"' not in parts[0] + ): next_construct = line break - if self.config['lines_after_imports'] != -1: - self.out_lines[imports_tail:0] = ["" for line in range(self.config['lines_after_imports'])] - elif self.extension != "pyi" and (next_construct.startswith("def ") or - next_construct.startswith("class ") or - next_construct.startswith("@") or - next_construct.startswith("async def")): + if self.config["lines_after_imports"] != -1: + self.out_lines[imports_tail:0] = [ + "" for line in range(self.config["lines_after_imports"]) + ] + elif self.extension != "pyi" and ( + next_construct.startswith("def ") + or next_construct.startswith("class ") + or next_construct.startswith("@") + or next_construct.startswith("async def") + ): self.out_lines[imports_tail:0] = ["", ""] else: self.out_lines[imports_tail:0] = [""] @@ -574,8 +823,13 @@ def by_module(line: str) -> str: for index, line in enumerate(self.out_lines): new_out_lines.append(line) if line in self.import_placements: - new_out_lines.extend(self.place_imports[self.import_placements[line]]) - if len(self.out_lines) <= index or self.out_lines[index + 1].strip() != "": + new_out_lines.extend( + self.place_imports[self.import_placements[line]] + ) + if ( + len(self.out_lines) <= index + or self.out_lines[index + 1].strip() != "" + ): new_out_lines.append("") self.out_lines = new_out_lines @@ -586,27 +840,30 @@ def _output_grid( white_space: str, indent: str, line_length: int, - comments: Sequence[str] + comments: Sequence[str], ) -> str: statement += "(" + imports.pop(0) while imports: next_import = imports.pop(0) - next_statement = self._add_comments(comments, statement + ", " + next_import) + next_statement = self._add_comments( + comments, statement + ", " + next_import + ) if len(next_statement.split(self.line_separator)[-1]) + 1 > line_length: - lines = ['{0}{1}'.format(white_space, next_import.split(" ")[0])] + lines = ["{0}{1}".format(white_space, next_import.split(" ")[0])] for part in next_import.split(" ")[1:]: - new_line = '{0} {1}'.format(lines[-1], part) + new_line = "{0} {1}".format(lines[-1], part) if len(new_line) + 1 > line_length: - lines.append('{0}{1}'.format(white_space, part)) + lines.append("{0}{1}".format(white_space, part)) else: lines[-1] = new_line next_import = self.line_separator.join(lines) - statement = (self._add_comments(comments, "{0},".format(statement)) + - "{0}{1}".format(self.line_separator, next_import)) + statement = self._add_comments( + comments, "{0},".format(statement) + ) + "{0}{1}".format(self.line_separator, next_import) comments = None else: statement += ", " + next_import - return statement + ("," if self.config['include_trailing_comma'] else "") + ")" + return statement + ("," if self.config["include_trailing_comma"] else "") + ")" def _output_vertical( self, @@ -615,14 +872,18 @@ def _output_vertical( white_space: str, indent: str, line_length: int, - comments: Sequence[str] + comments: Sequence[str], ) -> str: - first_import = self._add_comments(comments, imports.pop(0) + ",") + self.line_separator + white_space + first_import = ( + self._add_comments(comments, imports.pop(0) + ",") + + self.line_separator + + white_space + ) return "{0}({1}{2}{3})".format( statement, first_import, ("," + self.line_separator + white_space).join(imports), - "," if self.config['include_trailing_comma'] else "", + "," if self.config["include_trailing_comma"] else "", ) def _output_hanging_indent( @@ -632,15 +893,18 @@ def _output_hanging_indent( white_space: str, indent: str, line_length: int, - comments: Sequence[str] + comments: Sequence[str], ) -> str: statement += imports.pop(0) while imports: next_import = imports.pop(0) - next_statement = self._add_comments(comments, statement + ", " + next_import) + next_statement = self._add_comments( + comments, statement + ", " + next_import + ) if len(next_statement.split(self.line_separator)[-1]) + 3 > line_length: - next_statement = (self._add_comments(comments, "{0}, \\".format(statement)) + - "{0}{1}{2}".format(self.line_separator, indent, next_import)) + next_statement = self._add_comments( + comments, "{0}, \\".format(statement) + ) + "{0}{1}{2}".format(self.line_separator, indent, next_import) comments = None statement = next_statement return statement @@ -652,7 +916,7 @@ def _output_vertical_hanging_indent( white_space: str, indent: str, line_length: int, - comments: Sequence[str] + comments: Sequence[str], ) -> str: return "{0}({1}{2}{3}{4}{5}{2})".format( statement, @@ -660,8 +924,8 @@ def _output_vertical_hanging_indent( self.line_separator, indent, ("," + self.line_separator + indent).join(imports), - "," if self.config['include_trailing_comma'] else "", - ) + "," if self.config["include_trailing_comma"] else "", + ) def _output_vertical_grid_common( self, @@ -671,9 +935,14 @@ def _output_vertical_grid_common( indent: str, line_length: int, comments: Sequence[str], - need_trailing_char: bool + need_trailing_char: bool, ) -> str: - statement += self._add_comments(comments, "(") + self.line_separator + indent + imports.pop(0) + statement += ( + self._add_comments(comments, "(") + + self.line_separator + + indent + + imports.pop(0) + ) while imports: next_import = imports.pop(0) next_statement = "{0}, {1}".format(statement, next_import) @@ -683,10 +952,12 @@ def _output_vertical_grid_common( # We might also need to account for a closing ) we're going to add. current_line_length += 1 if current_line_length > line_length: - next_statement = "{0},{1}{2}{3}".format(statement, self.line_separator, indent, next_import) + next_statement = "{0},{1}{2}{3}".format( + statement, self.line_separator, indent, next_import + ) statement = next_statement - if self.config['include_trailing_comma']: - statement += ',' + if self.config["include_trailing_comma"]: + statement += "," return statement def _output_vertical_grid( @@ -698,8 +969,12 @@ def _output_vertical_grid( line_length: int, comments: Sequence[str], ) -> str: - return self._output_vertical_grid_common(statement, imports, white_space, indent, line_length, comments, - True) + ")" + return ( + self._output_vertical_grid_common( + statement, imports, white_space, indent, line_length, comments, True + ) + + ")" + ) def _output_vertical_grid_grouped( self, @@ -710,8 +985,13 @@ def _output_vertical_grid_grouped( line_length: int, comments: Sequence[str], ) -> str: - return self._output_vertical_grid_common(statement, imports, white_space, indent, line_length, comments, - True) + self.line_separator + ")" + return ( + self._output_vertical_grid_common( + statement, imports, white_space, indent, line_length, comments, True + ) + + self.line_separator + + ")" + ) def _output_vertical_grid_grouped_no_comma( self, @@ -722,8 +1002,13 @@ def _output_vertical_grid_grouped_no_comma( line_length: int, comments: Sequence[str], ) -> str: - return self._output_vertical_grid_common(statement, imports, white_space, indent, line_length, comments, - False) + self.line_separator + ")" + return ( + self._output_vertical_grid_common( + statement, imports, white_space, indent, line_length, comments, False + ) + + self.line_separator + + ")" + ) def _output_noqa( self, @@ -734,26 +1019,34 @@ def _output_noqa( line_length: int, comments: Sequence[str], ) -> str: - retval = '{0}{1}'.format(statement, ', '.join(imports)) - comment_str = ' '.join(comments) + retval = "{0}{1}".format(statement, ", ".join(imports)) + comment_str = " ".join(comments) if comments: - if len(retval) + len(self.config['comment_prefix']) + 1 + len(comment_str) <= line_length: - return '{0}{1} {2}'.format(retval, self.config['comment_prefix'], comment_str) + if ( + len(retval) + len(self.config["comment_prefix"]) + 1 + len(comment_str) + <= line_length + ): + return "{0}{1} {2}".format( + retval, self.config["comment_prefix"], comment_str + ) else: if len(retval) <= line_length: return retval if comments: if "NOQA" in comments: - return '{0}{1} {2}'.format(retval, self.config['comment_prefix'], comment_str) + return "{0}{1} {2}".format( + retval, self.config["comment_prefix"], comment_str + ) else: - return '{0}{1} NOQA {2}'.format(retval, self.config['comment_prefix'], comment_str) + return "{0}{1} NOQA {2}".format( + retval, self.config["comment_prefix"], comment_str + ) else: - return '{0}{1} NOQA'.format(retval, self.config['comment_prefix']) + return "{0}{1} NOQA".format(retval, self.config["comment_prefix"]) @staticmethod def _strip_comments( - line: str, - comments: Optional[List[str]] = None + line: str, comments: Optional[List[str]] = None ) -> Tuple[str, List[str], bool]: """Removes comments from import line.""" if comments is None: @@ -762,7 +1055,7 @@ def _strip_comments( new_comments = False comment_start = line.find("#") if comment_start != -1: - comments.append(line[comment_start + 1:].strip()) + comments.append(line[comment_start + 1 :].strip()) new_comments = True line = line[:comment_start] @@ -780,18 +1073,23 @@ def _skip_line(self, line: str) -> bool: if '"' in line or "'" in line: index = 0 - if self._first_comment_index_start == -1 and (line.startswith('"') or line.startswith("'")): + if self._first_comment_index_start == -1 and ( + line.startswith('"') or line.startswith("'") + ): self._first_comment_index_start = self.index while index < len(line): if line[index] == "\\": index += 1 elif self._in_quote: - if line[index:index + len(self._in_quote)] == self._in_quote: + if line[index : index + len(self._in_quote)] == self._in_quote: self._in_quote = False - if self._first_comment_index_end < self._first_comment_index_start: + if ( + self._first_comment_index_end + < self._first_comment_index_start + ): self._first_comment_index_end = self.index elif line[index] in ("'", '"'): - long_quote = line[index:index + 3] + long_quote = line[index : index + 3] if long_quote in ('"""', "'''"): self._in_quote = long_quote index += 2 @@ -805,13 +1103,13 @@ def _skip_line(self, line: str) -> bool: def _strip_syntax(self, import_string: str) -> str: import_string = import_string.replace("_import", "[[i]]") - for remove_syntax in ['\\', '(', ')', ',']: + for remove_syntax in ["\\", "(", ")", ","]: import_string = import_string.replace(remove_syntax, " ") import_list = import_string.split() - for key in ('from', 'import'): + for key in ("from", "import"): if key in import_list: import_list.remove(key) - import_string = ' '.join(import_list) + import_string = " ".join(import_list) import_string = import_string.replace("[[i]]", "_import") return import_string.replace("{ ", "{|").replace(" }", "|}") @@ -822,7 +1120,7 @@ def _parse(self) -> None: while not self._at_end(): raw_line = line = self._get_line() line = line.replace("from.import ", "from . import ") - line = line.replace("\t", " ").replace('import*', 'import *') + line = line.replace("\t", " ").replace("import*", "import *") line = line.replace(" .import ", " . import ") statement_index = self.index skip_line = self._skip_line(line) @@ -839,7 +1137,11 @@ def _parse(self) -> None: if ";" in line: for part in (part.strip() for part in line.split(";")): - if part and not part.startswith("from ") and not part.startswith("import "): + if ( + part + and not part.startswith("from ") + and not part.startswith("import ") + ): skip_line = True import_type = self._import_type(line) @@ -857,128 +1159,239 @@ def _parse(self) -> None: self.import_index = self.index - 1 nested_comments = {} import_string, comments, new_comments = self._strip_comments(line) - line_parts = [part for part in self._strip_syntax(import_string).strip().split(" ") if part] - if import_type == "from" and len(line_parts) == 2 and line_parts[1] != "*" and new_comments: + line_parts = [ + part + for part in self._strip_syntax(import_string).strip().split(" ") + if part + ] + if ( + import_type == "from" + and len(line_parts) == 2 + and line_parts[1] != "*" + and new_comments + ): nested_comments[line_parts[-1]] = comments[0] if "(" in line.split("#")[0] and not self._at_end(): while not line.strip().endswith(")") and not self._at_end(): - line, comments, new_comments = self._strip_comments(self._get_line(), comments) + line, comments, new_comments = self._strip_comments( + self._get_line(), comments + ) stripped_line = self._strip_syntax(line).strip() - if import_type == "from" and stripped_line and " " not in stripped_line and new_comments: + if ( + import_type == "from" + and stripped_line + and " " not in stripped_line + and new_comments + ): nested_comments[stripped_line] = comments[-1] import_string += self.line_separator + line else: while line.strip().endswith("\\"): - line, comments, new_comments = self._strip_comments(self._get_line(), comments) + line, comments, new_comments = self._strip_comments( + self._get_line(), comments + ) # Still need to check for parentheses after an escaped line - if "(" in line.split("#")[0] and ")" not in line.split("#")[0] and not self._at_end(): + if ( + "(" in line.split("#")[0] + and ")" not in line.split("#")[0] + and not self._at_end() + ): stripped_line = self._strip_syntax(line).strip() - if import_type == "from" and stripped_line and " " not in stripped_line and new_comments: + if ( + import_type == "from" + and stripped_line + and " " not in stripped_line + and new_comments + ): nested_comments[stripped_line] = comments[-1] import_string += self.line_separator + line while not line.strip().endswith(")") and not self._at_end(): - line, comments, new_comments = self._strip_comments(self._get_line(), comments) + line, comments, new_comments = self._strip_comments( + self._get_line(), comments + ) stripped_line = self._strip_syntax(line).strip() - if import_type == "from" and stripped_line and " " not in stripped_line and new_comments: + if ( + import_type == "from" + and stripped_line + and " " not in stripped_line + and new_comments + ): nested_comments[stripped_line] = comments[-1] import_string += self.line_separator + line stripped_line = self._strip_syntax(line).strip() - if import_type == "from" and stripped_line and " " not in stripped_line and new_comments: + if ( + import_type == "from" + and stripped_line + and " " not in stripped_line + and new_comments + ): nested_comments[stripped_line] = comments[-1] - if import_string.strip().endswith(" import") or line.strip().startswith("import "): + if import_string.strip().endswith( + " import" + ) or line.strip().startswith("import "): import_string += self.line_separator + 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 (") parts = import_string.split(" import ") from_import = parts[0].split(" ") - import_string = " import ".join([from_import[0] + " " + "".join(from_import[1:])] + parts[1:]) - - imports = [item.replace("{|", "{ ").replace("|}", " }") for item in - self._strip_syntax(import_string).split()] + import_string = " import ".join( + [from_import[0] + " " + "".join(from_import[1:])] + parts[1:] + ) + + imports = [ + item.replace("{|", "{ ").replace("|}", " }") + for item in self._strip_syntax(import_string).split() + ] straight_import = True - if "as" in imports and (imports.index('as') + 1) < len(imports): + if "as" in imports and (imports.index("as") + 1) < len(imports): straight_import = False while "as" in imports: - index = imports.index('as') + index = imports.index("as") if import_type == "from": module = imports[0] + "." + imports[index - 1] self.as_map[module].append(imports[index + 1]) else: module = imports[index - 1] self.as_map[module].append(imports[index + 1]) - if not self.config['combine_as_imports']: - self.comments['straight'][module] = comments + if not self.config["combine_as_imports"]: + self.comments["straight"][module] = comments comments = [] - del imports[index:index + 2] + del imports[index : index + 2] if import_type == "from": import_from = imports.pop(0) placed_module = self.place_module(import_from) - if self.config['verbose']: - print("from-type place_module for %s returned %s" % (import_from, placed_module)) - if placed_module == '': + if self.config["verbose"]: + print( + "from-type place_module for %s returned %s" + % (import_from, placed_module) + ) + if placed_module == "": print( "WARNING: could not place module {0} of line {1} --" - " Do you need to define a default section?".format(import_from, line) + " Do you need to define a default section?".format( + import_from, line + ) ) root = self.imports[placed_module][import_type] for import_name in imports: associated_comment = nested_comments.get(import_name) if associated_comment: - self.comments['nested'].setdefault(import_from, {})[import_name] = associated_comment + self.comments["nested"].setdefault(import_from, {})[ + import_name + ] = associated_comment comments.pop(comments.index(associated_comment)) if comments: - self.comments['from'].setdefault(import_from, []).extend(comments) + self.comments["from"].setdefault(import_from, []).extend( + comments + ) - if len(self.out_lines) > max(self.import_index, self._first_comment_index_end + 1, 1) - 1: + if ( + len(self.out_lines) + > max(self.import_index, self._first_comment_index_end + 1, 1) + - 1 + ): last = self.out_lines and self.out_lines[-1].rstrip() or "" - while (last.startswith("#") and not last.endswith('"""') and not last.endswith("'''") and - 'isort:imports-' not in last): - self.comments['above']['from'].setdefault(import_from, []).insert(0, self.out_lines.pop(-1)) - if len(self.out_lines) > max(self.import_index - 1, self._first_comment_index_end + 1, 1) - 1: + while ( + last.startswith("#") + and not last.endswith('"""') + and not last.endswith("'''") + and "isort:imports-" not in last + ): + self.comments["above"]["from"].setdefault( + import_from, [] + ).insert(0, self.out_lines.pop(-1)) + if ( + len(self.out_lines) + > max( + self.import_index - 1, + self._first_comment_index_end + 1, + 1, + ) + - 1 + ): last = self.out_lines[-1].rstrip() else: last = "" if statement_index - 1 == self.import_index: - self.import_index -= len(self.comments['above']['from'].get(import_from, [])) + self.import_index -= len( + self.comments["above"]["from"].get(import_from, []) + ) if import_from not in root: - root[import_from] = OrderedDict((module, straight_import) for module in imports) + root[import_from] = OrderedDict( + (module, straight_import) for module in imports + ) else: - root[import_from].update((module, straight_import | root[import_from].get(module, False)) - for module in imports) + root[import_from].update( + ( + module, + straight_import | root[import_from].get(module, False), + ) + for module in imports + ) else: for module in imports: if comments: - self.comments['straight'][module] = comments + self.comments["straight"][module] = comments comments = None - if len(self.out_lines) > max(self.import_index, self._first_comment_index_end + 1, 1) - 1: + if ( + len(self.out_lines) + > max( + self.import_index, self._first_comment_index_end + 1, 1 + ) + - 1 + ): last = self.out_lines and self.out_lines[-1].rstrip() or "" - while (last.startswith("#") and not last.endswith('"""') and not last.endswith("'''") and - 'isort:imports-' not in last): - self.comments['above']['straight'].setdefault(module, []).insert(0, - self.out_lines.pop(-1)) - if len(self.out_lines) > 0 and len(self.out_lines) != self._first_comment_index_end: + while ( + last.startswith("#") + and not last.endswith('"""') + and not last.endswith("'''") + and "isort:imports-" not in last + ): + self.comments["above"]["straight"].setdefault( + module, [] + ).insert(0, self.out_lines.pop(-1)) + if ( + len(self.out_lines) > 0 + and len(self.out_lines) + != self._first_comment_index_end + ): last = self.out_lines[-1].rstrip() else: last = "" if self.index - 1 == self.import_index: - self.import_index -= len(self.comments['above']['straight'].get(module, [])) + self.import_index -= len( + self.comments["above"]["straight"].get(module, []) + ) placed_module = self.place_module(module) - if self.config['verbose']: - print("else-type place_module for %s returned %s" % (module, placed_module)) - if placed_module == '': + if self.config["verbose"]: + print( + "else-type place_module for %s returned %s" + % (module, placed_module) + ) + if placed_module == "": print( "WARNING: could not place module {0} of line {1} --" - " Do you need to define a default section?".format(import_from, line) + " Do you need to define a default section?".format( + import_from, line + ) ) - straight_import |= self.imports[placed_module][import_type].get(module, False) - self.imports[placed_module][import_type][module] = straight_import + straight_import |= self.imports[placed_module][import_type].get( + module, False + ) + self.imports[placed_module][import_type][ + module + ] = straight_import diff --git a/isort/main.py b/isort/main.py index 64bf2f473..ffbf914ef 100644 --- a/isort/main.py +++ b/isort/main.py @@ -1,4 +1,4 @@ -''' Tool for sorting imports alphabetically, and automatically separated into sections. +""" Tool for sorting imports alphabetically, and automatically separated into sections. Copyright (C) 2013 Timothy Edmund Crosley @@ -16,19 +16,34 @@ CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -''' +""" import argparse import functools import glob import os import re import sys -from typing import Any, Dict, Iterable, Iterator, List, MutableMapping, Optional, Sequence +from typing import ( + Any, + Dict, + Iterable, + Iterator, + List, + MutableMapping, + Optional, + Sequence, +) import setuptools from isort import SortImports, __version__ -from isort.settings import DEFAULT_SECTIONS, WrapModes, default, file_should_be_skipped, from_path +from isort.settings import ( + DEFAULT_SECTIONS, + WrapModes, + default, + file_should_be_skipped, + from_path, +) INTRO = r""" /#######################################################################\ @@ -51,24 +66,26 @@ VERSION {0} \########################################################################/ -""".format(__version__) +""".format( + __version__ +) -shebang_re = re.compile(br'^#!.*\bpython[23w]?\b') +shebang_re = re.compile(br"^#!.*\bpython[23w]?\b") def is_python_file(path: str) -> bool: _root, ext = os.path.splitext(path) - if ext in ('.py', '.pyi'): + if ext in (".py", ".pyi"): return True - if ext in ('.pex', ): + if ext in (".pex",): return False # Skip editor backup files. - if path.endswith('~'): + if path.endswith("~"): return False try: - with open(path, 'rb') as fp: + with open(path, "rb") as fp: line = fp.readline(100) except IOError: return False @@ -91,14 +108,18 @@ def sort_imports(file_name: str, **arguments: Any) -> Optional[SortAttempt]: return None -def iter_source_code(paths: Iterable[str], config: MutableMapping[str, Any], skipped: List[str]) -> Iterator[str]: +def iter_source_code( + paths: Iterable[str], config: MutableMapping[str, Any], skipped: List[str] +) -> Iterator[str]: """Iterate over all Python source files defined in paths.""" - if 'not_skip' in config: - config['skip'] = list(set(config['skip']).difference(config['not_skip'])) + if "not_skip" in config: + config["skip"] = list(set(config["skip"]).difference(config["not_skip"])) for path in paths: if os.path.isdir(path): - 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 file_should_be_skipped(dirname, config, dirpath): skipped.append(dirname) @@ -144,9 +165,9 @@ def distribution_files(self) -> Iterator[str]: pkg_dir = package if package in package_dirs: pkg_dir = package_dirs[package] - elif '' in package_dirs: - pkg_dir = package_dirs[''] + os.path.sep + pkg_dir - yield pkg_dir.replace('.', os.path.sep) + elif "" in package_dirs: + pkg_dir = package_dirs[""] + os.path.sep + pkg_dir + yield pkg_dir.replace(".", os.path.sep) if self.distribution.py_modules: for filename in self.distribution.py_modules: @@ -157,199 +178,501 @@ def distribution_files(self) -> Iterator[str]: def run(self) -> None: arguments = self.arguments wrong_sorted_files = False - arguments['check'] = True + arguments["check"] = True for path in self.distribution_files(): - for python_file in glob.iglob(os.path.join(path, '*.py')): + for python_file in glob.iglob(os.path.join(path, "*.py")): try: - incorrectly_sorted = SortImports(python_file, **arguments).incorrectly_sorted + incorrectly_sorted = SortImports( + python_file, **arguments + ).incorrectly_sorted if incorrectly_sorted: wrong_sorted_files = True except IOError as e: - print("WARNING: Unable to parse file {0} due to {1}".format(python_file, e)) + print( + "WARNING: Unable to parse file {0} due to {1}".format( + python_file, e + ) + ) if wrong_sorted_files: sys.exit(1) def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: - parser = argparse.ArgumentParser(description='Sort Python import definitions alphabetically ' - '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.') + parser = argparse.ArgumentParser( + description="Sort Python import definitions alphabetically " + "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, ' - 'automatically determining correct placement.') - parser.add_argument('-ac', '--atomic', dest='atomic', action='store_true', - help="Ensures the output doesn't save if the resulting file contains syntax errors.") - parser.add_argument('-af', '--force-adds', dest='force_adds', action='store_true', - help='Forces import adds even if the original file is empty.') - parser.add_argument('-b', '--builtin', dest='known_standard_library', action='append', - help='Force sortImports to recognize a module as part of the python standard library.') - parser.add_argument('-c', '--check-only', action='store_true', dest="check", - help='Checks the file for unsorted / unformatted imports and prints them to the ' - 'command line without modifying the file.') - parser.add_argument('-ca', '--combine-as', dest='combine_as_imports', action='store_true', - help="Combines as imports on the same line.") - parser.add_argument('-cs', '--combine-star', dest='combine_star', action='store_true', - help="Ensures that if a star import is present, nothing else is imported from that namespace.") - parser.add_argument('-d', '--stdout', help='Force resulting output to stdout, instead of in-place.', - dest='write_to_stdout', action='store_true') - parser.add_argument('-df', '--diff', dest='show_diff', action='store_true', - help="Prints a diff of all the changes isort would make to a file, instead of " - "changing it in place") - parser.add_argument('-ds', '--no-sections', help='Put all imports into the same section bucket', dest='no_sections', - action='store_true') - parser.add_argument('-dt', '--dont-order-by-type', dest='dont_order_by_type', - action='store_true', help='Only order imports alphabetically, do not attempt type ordering') - parser.add_argument('-e', '--balanced', dest='balanced_wrapping', action='store_true', - help='Balances wrapping to produce the most consistent line length possible') - parser.add_argument('-f', '--future', dest='known_future_library', action='append', - help='Force sortImports to recognize a module as part of the future compatibility libraries.') - parser.add_argument('-fas', '--force-alphabetical-sort', action='store_true', dest="force_alphabetical_sort", - help='Force all imports to be sorted as a single section') - parser.add_argument('-fass', '--force-alphabetical-sort-within-sections', action='store_true', - dest="force_alphabetical_sort", help='Force all imports to be sorted alphabetically within a ' - 'section') - parser.add_argument('-ff', '--from-first', dest='from_first', - help="Switches the typical ordering preference, showing from imports first then straight ones.") - parser.add_argument('-fgw', '--force-grid-wrap', nargs='?', const=2, type=int, dest="force_grid_wrap", - help='Force number of from imports (defaults to 2) to be grid wrapped regardless of line ' - 'length') - parser.add_argument('-fss', '--force-sort-within-sections', action='store_true', dest="force_sort_within_sections", - help='Force imports to be sorted by module, independent of import_type') - parser.add_argument('-i', '--indent', help='String to place for indents defaults to " " (4 spaces).', - dest='indent', type=str) - parser.add_argument('-j', '--jobs', help='Number of files to process in parallel.', - dest='jobs', type=int) - parser.add_argument('-k', '--keep-direct-and-as', dest='keep_direct_and_as_imports', action='store_true', - help="Turns off default behavior that removes direct imports when as imports exist.") - parser.add_argument('-l', '--lines', help='[Deprecated] The max length of an import line (used for wrapping ' - 'long imports).', - dest='line_length', type=int) - parser.add_argument('-lai', '--lines-after-imports', dest='lines_after_imports', type=int) - parser.add_argument('-lbt', '--lines-between-types', dest='lines_between_types', type=int) - parser.add_argument('-le', '--line-ending', dest='line_ending', - help="Forces line endings to the specified value. If not set, values will be guessed per-file.") - parser.add_argument('-ls', '--length-sort', help='Sort imports by their string length.', - dest='length_sort', action='store_true') - parser.add_argument('-m', '--multi-line', dest='multi_line_output', type=WrapModes.from_string, - help='Multi line output (0-grid, 1-vertical, 2-hanging, 3-vert-hanging, 4-vert-grid, ' - '5-vert-grid-grouped, 6-vert-grid-grouped-no-comma).') - inline_args_group.add_argument('-nis', '--no-inline-sort', dest='no_inline_sort', action='store_true', - help='Leaves `from` imports with multiple imports \'as-is\' (e.g. `from foo import a, c ,b`).') - parser.add_argument('-nlb', '--no-lines-before', help='Sections which should not be split with previous by empty lines', - dest='no_lines_before', action='append') - parser.add_argument('-ns', '--dont-skip', help='Files that sort imports should never skip over.', - dest='not_skip', action='append') - parser.add_argument('-o', '--thirdparty', dest='known_third_party', action='append', - help='Force sortImports to recognize a module as being part of a third party library.') - parser.add_argument('-ot', '--order-by-type', dest='order_by_type', - action='store_true', help='Order imports by type in addition to alphabetically') - 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('-q', '--quiet', action='store_true', dest="quiet", - help='Shows extra quiet output, only errors are outputted.') - 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 ' - 'files you should specify twice: --skip file1 --skip file2.', dest='skip', action='append') - parser.add_argument('-sd', '--section-default', dest='default_section', - help='Sets the default section for imports (by default FIRSTPARTY) options: ' + - str(DEFAULT_SECTIONS)) - parser.add_argument('-sg', '--skip-glob', help='Files that sort imports should skip over.', dest='skip_glob', - action='append') - inline_args_group.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('-sp', '--settings-path', dest="settings_path", - help='Explicitly set the settings path instead of auto determining based on file location.') - parser.add_argument('-t', '--top', help='Force specific imports to the top of their appropriate section.', - dest='force_to_top', action='append') - parser.add_argument('-tc', '--trailing-comma', dest='include_trailing_comma', action='store_true', - help='Includes a trailing comma on multi line imports that include parentheses.') - parser.add_argument('-up', '--use-parentheses', dest='use_parentheses', action='store_true', - help='Use parenthesis for line continuation on length limit instead of slashes.') - parser.add_argument('-v', '--version', action='store_true', dest='show_version') - parser.add_argument('-vb', '--verbose', action='store_true', dest="verbose", - 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).', - dest='line_length', type=int) - parser.add_argument('-wl', '--wrap-length', dest='wrap_length', type=int, - help="Specifies how long lines that are wrapped should be, if not set line_length is used.") - parser.add_argument('-ws', '--ignore-whitespace', action='store_true', dest="ignore_whitespace", - help='Tells isort to ignore whitespace differences when --check-only is being used.') - parser.add_argument('-y', '--apply', dest='apply', action='store_true', - help='Tells isort to apply changes recursively without asking') - 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('--filter-files', dest='filter_files', action='store_true', - help='Tells isort to filter files even when they are explicitly passed in as part of the command') - parser.add_argument('files', nargs='*', help='One or more Python source files that need their imports sorted.') - parser.add_argument('-py', '--python-version', action='store', dest='py_version', - help='Tells isort to sort the standard library based on the python version. ' - 'Default is the version of the running interpreter, for instance: -py 3, -py 2.7') - - arguments = {key: value for key, value in vars(parser.parse_args(argv)).items() if value} - if 'dont_order_by_type' in arguments: - arguments['order_by_type'] = False - if arguments.pop('unsafe', False): - arguments['safety_excludes'] = False + parser.add_argument( + "-a", + "--add-import", + dest="add_imports", + action="append", + help="Adds the specified import line to all files, " + "automatically determining correct placement.", + ) + parser.add_argument( + "-ac", + "--atomic", + dest="atomic", + action="store_true", + help="Ensures the output doesn't save if the resulting file contains syntax errors.", + ) + parser.add_argument( + "-af", + "--force-adds", + dest="force_adds", + action="store_true", + help="Forces import adds even if the original file is empty.", + ) + parser.add_argument( + "-b", + "--builtin", + dest="known_standard_library", + action="append", + help="Force sortImports to recognize a module as part of the python standard library.", + ) + parser.add_argument( + "-c", + "--check-only", + action="store_true", + dest="check", + help="Checks the file for unsorted / unformatted imports and prints them to the " + "command line without modifying the file.", + ) + parser.add_argument( + "-ca", + "--combine-as", + dest="combine_as_imports", + action="store_true", + help="Combines as imports on the same line.", + ) + parser.add_argument( + "-cs", + "--combine-star", + dest="combine_star", + action="store_true", + help="Ensures that if a star import is present, nothing else is imported from that namespace.", + ) + parser.add_argument( + "-d", + "--stdout", + help="Force resulting output to stdout, instead of in-place.", + dest="write_to_stdout", + action="store_true", + ) + parser.add_argument( + "-df", + "--diff", + dest="show_diff", + action="store_true", + help="Prints a diff of all the changes isort would make to a file, instead of " + "changing it in place", + ) + parser.add_argument( + "-ds", + "--no-sections", + help="Put all imports into the same section bucket", + dest="no_sections", + action="store_true", + ) + parser.add_argument( + "-dt", + "--dont-order-by-type", + dest="dont_order_by_type", + action="store_true", + help="Only order imports alphabetically, do not attempt type ordering", + ) + parser.add_argument( + "-e", + "--balanced", + dest="balanced_wrapping", + action="store_true", + help="Balances wrapping to produce the most consistent line length possible", + ) + parser.add_argument( + "-f", + "--future", + dest="known_future_library", + action="append", + help="Force sortImports to recognize a module as part of the future compatibility libraries.", + ) + parser.add_argument( + "-fas", + "--force-alphabetical-sort", + action="store_true", + dest="force_alphabetical_sort", + help="Force all imports to be sorted as a single section", + ) + parser.add_argument( + "-fass", + "--force-alphabetical-sort-within-sections", + action="store_true", + dest="force_alphabetical_sort", + help="Force all imports to be sorted alphabetically within a section", + ) + parser.add_argument( + "-ff", + "--from-first", + dest="from_first", + help="Switches the typical ordering preference, showing from imports first then straight ones.", + ) + parser.add_argument( + "-fgw", + "--force-grid-wrap", + nargs="?", + const=2, + type=int, + dest="force_grid_wrap", + help="Force number of from imports (defaults to 2) to be grid wrapped regardless of line " + "length", + ) + parser.add_argument( + "-fss", + "--force-sort-within-sections", + action="store_true", + dest="force_sort_within_sections", + help="Force imports to be sorted by module, independent of import_type", + ) + parser.add_argument( + "-i", + "--indent", + help='String to place for indents defaults to " " (4 spaces).', + dest="indent", + type=str, + ) + parser.add_argument( + "-j", + "--jobs", + help="Number of files to process in parallel.", + dest="jobs", + type=int, + ) + parser.add_argument( + "-k", + "--keep-direct-and-as", + dest="keep_direct_and_as_imports", + action="store_true", + help="Turns off default behavior that removes direct imports when as imports exist.", + ) + parser.add_argument( + "-l", + "--lines", + help="[Deprecated] The max length of an import line (used for wrapping " + "long imports).", + dest="line_length", + type=int, + ) + parser.add_argument( + "-lai", "--lines-after-imports", dest="lines_after_imports", type=int + ) + parser.add_argument( + "-lbt", "--lines-between-types", dest="lines_between_types", type=int + ) + parser.add_argument( + "-le", + "--line-ending", + dest="line_ending", + help="Forces line endings to the specified value. If not set, values will be guessed per-file.", + ) + parser.add_argument( + "-ls", + "--length-sort", + help="Sort imports by their string length.", + dest="length_sort", + action="store_true", + ) + parser.add_argument( + "-m", + "--multi-line", + dest="multi_line_output", + type=WrapModes.from_string, + help="Multi line output (0-grid, 1-vertical, 2-hanging, 3-vert-hanging, 4-vert-grid, " + "5-vert-grid-grouped, 6-vert-grid-grouped-no-comma).", + ) + inline_args_group.add_argument( + "-nis", + "--no-inline-sort", + dest="no_inline_sort", + action="store_true", + help="Leaves `from` imports with multiple imports 'as-is' (e.g. `from foo import a, c ,b`).", + ) + parser.add_argument( + "-nlb", + "--no-lines-before", + help="Sections which should not be split with previous by empty lines", + dest="no_lines_before", + action="append", + ) + parser.add_argument( + "-ns", + "--dont-skip", + help="Files that sort imports should never skip over.", + dest="not_skip", + action="append", + ) + parser.add_argument( + "-o", + "--thirdparty", + dest="known_third_party", + action="append", + help="Force sortImports to recognize a module as being part of a third party library.", + ) + parser.add_argument( + "-ot", + "--order-by-type", + dest="order_by_type", + action="store_true", + help="Order imports by type in addition to alphabetically", + ) + 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( + "-q", + "--quiet", + action="store_true", + dest="quiet", + help="Shows extra quiet output, only errors are outputted.", + ) + 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 " + "files you should specify twice: --skip file1 --skip file2.", + dest="skip", + action="append", + ) + parser.add_argument( + "-sd", + "--section-default", + dest="default_section", + help="Sets the default section for imports (by default FIRSTPARTY) options: " + + str(DEFAULT_SECTIONS), + ) + parser.add_argument( + "-sg", + "--skip-glob", + help="Files that sort imports should skip over.", + dest="skip_glob", + action="append", + ) + inline_args_group.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( + "-sp", + "--settings-path", + dest="settings_path", + help="Explicitly set the settings path instead of auto determining based on file location.", + ) + parser.add_argument( + "-t", + "--top", + help="Force specific imports to the top of their appropriate section.", + dest="force_to_top", + action="append", + ) + parser.add_argument( + "-tc", + "--trailing-comma", + dest="include_trailing_comma", + action="store_true", + help="Includes a trailing comma on multi line imports that include parentheses.", + ) + parser.add_argument( + "-up", + "--use-parentheses", + dest="use_parentheses", + action="store_true", + help="Use parenthesis for line continuation on length limit instead of slashes.", + ) + parser.add_argument("-v", "--version", action="store_true", dest="show_version") + parser.add_argument( + "-vb", + "--verbose", + action="store_true", + dest="verbose", + 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).", + dest="line_length", + type=int, + ) + parser.add_argument( + "-wl", + "--wrap-length", + dest="wrap_length", + type=int, + help="Specifies how long lines that are wrapped should be, if not set line_length is used.", + ) + parser.add_argument( + "-ws", + "--ignore-whitespace", + action="store_true", + dest="ignore_whitespace", + help="Tells isort to ignore whitespace differences when --check-only is being used.", + ) + parser.add_argument( + "-y", + "--apply", + dest="apply", + action="store_true", + help="Tells isort to apply changes recursively without asking", + ) + 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( + "--filter-files", + dest="filter_files", + action="store_true", + help="Tells isort to filter files even when they are explicitly passed in as part of the command", + ) + parser.add_argument( + "files", + nargs="*", + help="One or more Python source files that need their imports sorted.", + ) + parser.add_argument( + "-py", + "--python-version", + action="store", + dest="py_version", + help="Tells isort to sort the standard library based on the python version. " + "Default is the version of the running interpreter, for instance: -py 3, -py 2.7", + ) + + arguments = { + key: value for key, value in vars(parser.parse_args(argv)).items() if value + } + if "dont_order_by_type" in arguments: + arguments["order_by_type"] = False + if arguments.pop("unsafe", False): + arguments["safety_excludes"] = False return arguments def main(argv: Optional[Sequence[str]] = None) -> None: arguments = parse_args(argv) - if arguments.get('show_version'): + if arguments.get("show_version"): print(INTRO) return - if arguments.get('ambiguous_r_flag'): - print('ERROR: Deprecated -r flag set. This flag has been replaced with -rm to remove ambiguity between it and ' - '-rc for recursive') + if arguments.get("ambiguous_r_flag"): + print( + "ERROR: Deprecated -r flag set. This flag has been replaced with -rm to remove ambiguity between it and " + "-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)) - if not os.path.isdir(arguments['settings_path']): - print("WARNING: settings_path dir does not exist: {0}".format(arguments['settings_path'])) - - if 'virtual_env' in arguments: - venv = arguments['virtual_env'] - arguments['virtual_env'] = os.path.abspath(venv) - if not os.path.isdir(arguments['virtual_env']): - print("WARNING: virtual_env dir does not exist: {0}".format(arguments['virtual_env'])) - - file_names = arguments.pop('files', []) - if file_names == ['-']: + 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)) + ) + if not os.path.isdir(arguments["settings_path"]): + print( + "WARNING: settings_path dir does not exist: {0}".format( + arguments["settings_path"] + ) + ) + + if "virtual_env" in arguments: + venv = arguments["virtual_env"] + arguments["virtual_env"] = os.path.abspath(venv) + if not os.path.isdir(arguments["virtual_env"]): + print( + "WARNING: virtual_env dir does not exist: {0}".format( + arguments["virtual_env"] + ) + ) + + file_names = arguments.pop("files", []) + if file_names == ["-"]: SortImports(file_contents=sys.stdin.read(), write_to_stdout=True, **arguments) else: if not file_names: - file_names = ['.'] - arguments['recursive'] = True - if not arguments.get('apply', False): - arguments['ask_to_apply'] = True - - config = from_path(arguments.get('settings_path', '') or os.path.abspath(file_names[0]) or os.getcwd()).copy() + file_names = ["."] + arguments["recursive"] = True + if not arguments.get("apply", False): + arguments["ask_to_apply"] = True + + config = from_path( + arguments.get("settings_path", "") + or os.path.abspath(file_names[0]) + or os.getcwd() + ).copy() config.update(arguments) wrong_sorted_files = False skipped = [] # type: List[str] - if config.get('filter_files'): + if config.get("filter_files"): filtered_files = [] for file_name in file_names: if file_should_be_skipped(file_name, config): @@ -358,26 +681,31 @@ def main(argv: Optional[Sequence[str]] = None) -> None: filtered_files.append(file_name) file_names = filtered_files - if arguments.get('recursive', False): + if arguments.get("recursive", False): file_names = iter_source_code(file_names, config, skipped) num_skipped = 0 - if config['verbose'] or config.get('show_logo', False): + if config["verbose"] or config.get("show_logo", False): print(INTRO) - jobs = arguments.get('jobs') + jobs = arguments.get("jobs") if jobs: import multiprocessing + executor = multiprocessing.Pool(jobs) - attempt_iterator = executor.imap(functools.partial(sort_imports, **arguments), file_names) + attempt_iterator = executor.imap( + functools.partial(sort_imports, **arguments), file_names + ) else: # https://github.com/python/typeshed/pull/2814 - attempt_iterator = (sort_imports(file_name, **arguments) for file_name in file_names) # type: ignore + attempt_iterator = ( # type: ignore + sort_imports(file_name, **arguments) for file_name in file_names + ) for sort_attempt in attempt_iterator: if not sort_attempt: continue incorrectly_sorted = sort_attempt.incorrectly_sorted - if arguments.get('check', False) and incorrectly_sorted: + if arguments.get("check", False) and incorrectly_sorted: wrong_sorted_files = True if sort_attempt.skipped: num_skipped += 1 @@ -386,11 +714,13 @@ def main(argv: Optional[Sequence[str]] = None) -> None: sys.exit(1) num_skipped += len(skipped) - if num_skipped and not arguments.get('quiet', False): - if config['verbose']: + if num_skipped and not arguments.get("quiet", False): + if config["verbose"]: for was_skipped in skipped: - print("WARNING: {0} was skipped as it's listed in 'skip' setting" - " or matches a glob in 'skip_glob' setting".format(was_skipped)) + print( + "WARNING: {0} was skipped as it's listed in 'skip' setting" + " or matches a glob in 'skip_glob' setting".format(was_skipped) + ) print("Skipped {0} files".format(num_skipped)) diff --git a/isort/natural.py b/isort/natural.py index 7398d94d8..81a81f607 100644 --- a/isort/natural.py +++ b/isort/natural.py @@ -34,17 +34,17 @@ def _atoi(text: str) -> Any: def _natural_keys(text: str) -> List[Any]: - return [_atoi(c) for c in re.split(r'(\d+)', text)] + return [_atoi(c) for c in re.split(r"(\d+)", text)] def nsorted( - to_sort: Iterable[str], - key: Optional[Callable[[str], Any]] = None + to_sort: Iterable[str], key: Optional[Callable[[str], Any]] = None ) -> List[str]: """Returns a naturally sorted list""" if key is None: key_callback = _natural_keys else: + def key_callback(text: str) -> List[Any]: return _natural_keys(key(text)) # type: ignore diff --git a/isort/pylama_isort.py b/isort/pylama_isort.py index a8e508e98..a0b3e9b4d 100644 --- a/isort/pylama_isort.py +++ b/isort/pylama_isort.py @@ -8,23 +8,24 @@ class Linter(BaseLinter): - def allow(self, path: str) -> bool: """Determine if this path should be linted.""" - return path.endswith('.py') + return path.endswith(".py") def run(self, path: str, **meta: Any) -> List[Dict[str, Any]]: """Lint the file. Return an array of error dicts if appropriate.""" - with open(os.devnull, 'w') as devnull: + with open(os.devnull, "w") as devnull: # Suppress isort messages sys.stdout = devnull if SortImports(path, check=True).incorrectly_sorted: - return [{ - 'lnum': 0, - 'col': 0, - 'text': 'Incorrectly sorted imports.', - 'type': 'ISORT' - }] + return [ + { + "lnum": 0, + "col": 0, + "text": "Incorrectly sorted imports.", + "type": "ISORT", + } + ] else: return [] diff --git a/isort/settings.py b/isort/settings.py index c3fdecdd9..f634036dd 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -33,7 +33,17 @@ from distutils.util import strtobool from functools import lru_cache from pathlib import Path -from typing import Any, Callable, Dict, Iterable, List, Mapping, MutableMapping, Optional, Union +from typing import ( + Any, + Callable, + Dict, + Iterable, + List, + Mapping, + MutableMapping, + Optional, + Union, +) from .stdlibs.py_three import standard_library_3 from .stdlibs.py_two import standard_library_2 @@ -46,13 +56,16 @@ try: import appdirs - if appdirs.system == 'darwin': - appdirs.system = 'linux2' + + if appdirs.system == "darwin": + appdirs.system = "linux2" except ImportError: appdirs = None -MAX_CONFIG_SEARCH_DEPTH = 25 # The number of parent directories isort will look for a config file within -DEFAULT_SECTIONS = ('FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER') +MAX_CONFIG_SEARCH_DEPTH = ( + 25 +) # The number of parent directories isort will look for a config file within +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|\.pants\.d" @@ -71,7 +84,7 @@ class WrapModes(enum.Enum): NOQA = 7 @staticmethod - def from_string(value: str) -> 'WrapModes': + def from_string(value: str) -> "WrapModes": return getattr(WrapModes, str(value), None) or WrapModes(int(value)) @@ -109,84 +122,103 @@ def _get_default(py_version: Optional[str]) -> Dict[str, Any]: else: raise ValueError( "The python version %s is not supported. " - "You can set a python version with the -py or --python-version flag " % py_version + "You can set a python version with the -py or --python-version flag " + % py_version ) - _default['known_standard_library'] = standard_library + _default["known_standard_library"] = standard_library return _default # Note that none of these lists must be complete as they are simply fallbacks for when included auto-detection fails. -default = {'force_to_top': [], - 'skip': [], - 'skip_glob': [], - 'line_length': 79, - 'wrap_length': 0, - 'line_ending': None, - 'sections': DEFAULT_SECTIONS, - 'no_sections': False, - 'known_future_library': ['__future__'], - 'known_third_party': ['google.appengine.api'], - 'known_first_party': [], - 'multi_line_output': WrapModes.GRID, - 'forced_separate': [], - 'indent': ' ' * 4, - 'comment_prefix': ' #', - 'length_sort': False, - 'add_imports': [], - 'remove_imports': [], - 'reverse_relative': False, - 'force_single_line': False, - 'default_section': 'FIRSTPARTY', - 'import_heading_future': '', - 'import_heading_stdlib': '', - 'import_heading_thirdparty': '', - 'import_heading_firstparty': '', - 'import_heading_localfolder': '', - 'balanced_wrapping': False, - 'use_parentheses': False, - 'order_by_type': True, - 'atomic': False, - 'lines_after_imports': -1, - 'lines_between_sections': 1, - 'lines_between_types': 0, - 'combine_as_imports': False, - 'combine_star': False, - 'keep_direct_and_as_imports': False, - 'include_trailing_comma': False, - 'from_first': False, - 'verbose': False, - 'quiet': False, - 'force_adds': False, - 'force_alphabetical_sort_within_sections': False, - 'force_alphabetical_sort': False, - 'force_grid_wrap': 0, - 'force_sort_within_sections': False, - 'show_diff': False, - 'ignore_whitespace': False, - 'no_lines_before': [], - 'no_inline_sort': False, - 'ignore_comments': False, - 'safety_excludes': True, - 'case_sensitive': False} +default = { + "force_to_top": [], + "skip": [], + "skip_glob": [], + "line_length": 79, + "wrap_length": 0, + "line_ending": None, + "sections": DEFAULT_SECTIONS, + "no_sections": False, + "known_future_library": ["__future__"], + "known_third_party": ["google.appengine.api"], + "known_first_party": [], + "multi_line_output": WrapModes.GRID, + "forced_separate": [], + "indent": " " * 4, + "comment_prefix": " #", + "length_sort": False, + "add_imports": [], + "remove_imports": [], + "reverse_relative": False, + "force_single_line": False, + "default_section": "FIRSTPARTY", + "import_heading_future": "", + "import_heading_stdlib": "", + "import_heading_thirdparty": "", + "import_heading_firstparty": "", + "import_heading_localfolder": "", + "balanced_wrapping": False, + "use_parentheses": False, + "order_by_type": True, + "atomic": False, + "lines_after_imports": -1, + "lines_between_sections": 1, + "lines_between_types": 0, + "combine_as_imports": False, + "combine_star": False, + "keep_direct_and_as_imports": False, + "include_trailing_comma": False, + "from_first": False, + "verbose": False, + "quiet": False, + "force_adds": False, + "force_alphabetical_sort_within_sections": False, + "force_alphabetical_sort": False, + "force_grid_wrap": 0, + "force_sort_within_sections": False, + "show_diff": False, + "ignore_whitespace": False, + "no_lines_before": [], + "no_inline_sort": False, + "ignore_comments": False, + "safety_excludes": True, + "case_sensitive": False, +} @lru_cache() -def from_path(path: Union[str, Path], py_version: Optional[str] = None) -> Dict[str, Any]: +def from_path( + path: Union[str, Path], py_version: Optional[str] = None +) -> Dict[str, Any]: computed_settings = _get_default(py_version) - isort_defaults = ['~/.isort.cfg'] + isort_defaults = ["~/.isort.cfg"] if appdirs: - isort_defaults = [appdirs.user_config_dir('isort.cfg')] + isort_defaults + isort_defaults = [appdirs.user_config_dir("isort.cfg")] + isort_defaults if isinstance(path, Path): path = str(path) - _update_settings_with_config(path, '.editorconfig', ['~/.editorconfig'], ('*', '*.py', '**.py'), computed_settings) - _update_settings_with_config(path, 'pyproject.toml', [], ('tool.isort', ), computed_settings) - _update_settings_with_config(path, '.isort.cfg', isort_defaults, ('settings', 'isort'), computed_settings) - _update_settings_with_config(path, 'setup.cfg', [], ('isort', 'tool:isort'), computed_settings) - _update_settings_with_config(path, 'tox.ini', [], ('isort', 'tool:isort'), computed_settings) + _update_settings_with_config( + path, + ".editorconfig", + ["~/.editorconfig"], + ("*", "*.py", "**.py"), + computed_settings, + ) + _update_settings_with_config( + path, "pyproject.toml", [], ("tool.isort",), computed_settings + ) + _update_settings_with_config( + path, ".isort.cfg", isort_defaults, ("settings", "isort"), computed_settings + ) + _update_settings_with_config( + path, "setup.cfg", [], ("isort", "tool:isort"), computed_settings + ) + _update_settings_with_config( + path, "tox.ini", [], ("isort", "tool:isort"), computed_settings + ) return computed_settings @@ -194,32 +226,36 @@ def prepare_config(settings_path: Path, **setting_overrides: Any) -> Dict[str, A py_version = setting_overrides.pop("py_version", None) config = from_path(settings_path, py_version).copy() for key, value in setting_overrides.items(): - access_key = key.replace('not_', '').lower() + access_key = key.replace("not_", "").lower() # The sections config needs to retain order and can't be converted to a set. - if access_key != 'sections' and type(config.get(access_key)) in (list, tuple): - if key.startswith('not_'): + if access_key != "sections" and type(config.get(access_key)) in (list, tuple): + if key.startswith("not_"): config[access_key] = list(set(config[access_key]).difference(value)) else: config[access_key] = list(set(config[access_key]).union(value)) else: config[key] = value - if config['force_alphabetical_sort']: - config.update({'force_alphabetical_sort_within_sections': True, - 'no_sections': True, - 'lines_between_types': 1, - 'from_first': True}) + if config["force_alphabetical_sort"]: + config.update( + { + "force_alphabetical_sort_within_sections": True, + "no_sections": True, + "lines_between_types": 1, + "from_first": True, + } + ) - indent = str(config['indent']) + indent = str(config["indent"]) if indent.isdigit(): indent = " " * int(indent) else: indent = indent.strip("'").strip('"') if indent.lower() == "tab": indent = "\t" - config['indent'] = indent + config["indent"] = indent - config['comment_prefix'] = config['comment_prefix'].strip("'").strip('"') + config["comment_prefix"] = config["comment_prefix"].strip("'").strip('"') return config @@ -228,7 +264,7 @@ def _update_settings_with_config( name: str, default: Iterable[str], sections: Iterable[str], - computed_settings: MutableMapping[str, Any] + computed_settings: MutableMapping[str, Any], ) -> None: editor_config_file = None for potential_settings_path in default: @@ -256,77 +292,87 @@ def _update_settings_with_config( def _get_str_to_type_converter(setting_name: str) -> Callable[[str], Any]: - type_converter = type(default.get(setting_name, '')) # type: Callable[[str], Any] + type_converter = type(default.get(setting_name, "")) # type: Callable[[str], Any] if type_converter == WrapModes: type_converter = WrapModes.from_string return type_converter def _update_with_config_file( - file_path: str, - sections: Iterable[str], - computed_settings: MutableMapping[str, Any] + file_path: str, sections: Iterable[str], computed_settings: MutableMapping[str, Any] ) -> None: cwd = os.path.dirname(file_path) settings = _get_config_data(file_path, sections).copy() if not settings: return - if file_path.endswith('.editorconfig'): - indent_style = settings.pop('indent_style', '').strip() - indent_size = settings.pop('indent_size', '').strip() + if file_path.endswith(".editorconfig"): + indent_style = settings.pop("indent_style", "").strip() + indent_size = settings.pop("indent_size", "").strip() if indent_size == "tab": - indent_size = settings.pop('tab_width', '').strip() + indent_size = settings.pop("tab_width", "").strip() - if indent_style == 'space': - computed_settings['indent'] = ' ' * (indent_size and int(indent_size) or 4) - elif indent_style == 'tab': - computed_settings['indent'] = '\t' * (indent_size and int(indent_size) or 1) + if indent_style == "space": + computed_settings["indent"] = " " * (indent_size and int(indent_size) or 4) + elif indent_style == "tab": + computed_settings["indent"] = "\t" * (indent_size and int(indent_size) or 1) - max_line_length = settings.pop('max_line_length', '').strip() + max_line_length = settings.pop("max_line_length", "").strip() if max_line_length: - computed_settings['line_length'] = float('inf') if max_line_length == 'off' else int(max_line_length) + computed_settings["line_length"] = ( + float("inf") if max_line_length == "off" else int(max_line_length) + ) for key, value in settings.items(): - access_key = key.replace('not_', '').lower() + access_key = key.replace("not_", "").lower() existing_value_type = _get_str_to_type_converter(access_key) if existing_value_type in (list, tuple): # sections has fixed order values; no adding or substraction from any set - if access_key == 'sections': + if access_key == "sections": computed_settings[access_key] = tuple(_as_list(value)) else: - existing_data = set(computed_settings.get(access_key, default.get(access_key))) - if key.startswith('not_'): - computed_settings[access_key] = difference(existing_data, _as_list(value)) - elif key.startswith('known_'): - computed_settings[access_key] = union(existing_data, _abspaths(cwd, _as_list(value))) + existing_data = set( + computed_settings.get(access_key, default.get(access_key)) + ) + if key.startswith("not_"): + computed_settings[access_key] = difference( + existing_data, _as_list(value) + ) + elif key.startswith("known_"): + computed_settings[access_key] = union( + existing_data, _abspaths(cwd, _as_list(value)) + ) else: - computed_settings[access_key] = union(existing_data, _as_list(value)) + computed_settings[access_key] = union( + existing_data, _as_list(value) + ) elif existing_value_type == bool: # Only some configuration formats support native boolean values. if not isinstance(value, bool): value = bool(strtobool(value)) computed_settings[access_key] = value - elif key.startswith('known_'): + elif key.startswith("known_"): computed_settings[access_key] = list(_abspaths(cwd, _as_list(value))) - elif key == 'force_grid_wrap': + elif key == "force_grid_wrap": try: result = existing_value_type(value) except ValueError: # backwards compat - result = default.get(access_key) if value.lower().strip() == 'false' else 2 + result = ( + default.get(access_key) if value.lower().strip() == "false" else 2 + ) computed_settings[access_key] = result else: - computed_settings[access_key] = getattr(existing_value_type, str(value), None) or existing_value_type(value) + computed_settings[access_key] = getattr( + existing_value_type, str(value), None + ) or existing_value_type(value) def _as_list(value: str) -> List[str]: if isinstance(value, list): return [item.strip() for item in value] filtered = [ - item.strip() - for item in value.replace('\n', ',').split(',') - if item.strip() + item.strip() for item in value.replace("\n", ",").split(",") if item.strip() ] return filtered @@ -346,26 +392,29 @@ def _get_config_data(file_path: str, sections: Iterable[str]) -> Dict[str, Any]: settings = {} # type: Dict[str, Any] with open(file_path) as config_file: - if file_path.endswith('.toml'): + if file_path.endswith(".toml"): if toml: config = toml.load(config_file) for section in sections: config_section = config - for key in section.split('.'): + for key in section.split("."): config_section = config_section.get(key, {}) settings.update(config_section) else: - if '[tool.isort]' in config_file.read(): - warnings.warn("Found {} with [tool.isort] section, but toml package is not installed. " - "To configure isort with {}, install with 'isort[pyproject]'.".format(file_path, - file_path)) + if "[tool.isort]" in config_file.read(): + warnings.warn( + "Found {} with [tool.isort] section, but toml package is not installed. " + "To configure isort with {}, install with 'isort[pyproject]'.".format( + file_path, file_path + ) + ) else: - if file_path.endswith('.editorconfig'): - line = '\n' + if file_path.endswith(".editorconfig"): + line = "\n" last_position = config_file.tell() while line: line = config_file.readline() - if '[' in line: + if "[" in line: config_file.seek(last_position) break last_position = config_file.tell() @@ -380,39 +429,41 @@ def _get_config_data(file_path: str, sections: Iterable[str]) -> Dict[str, Any]: def file_should_be_skipped( - filename: str, - config: Mapping[str, Any], - path: str = '' + filename: str, config: Mapping[str, Any], path: str = "" ) -> bool: """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 = os_path.replace("\\", "/") + if normalized_path[1:2] == ":": normalized_path = normalized_path[2:] - 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 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('\\', '/')): + for skip_path in config["skip"]: + if posixpath.abspath(normalized_path) == posixpath.abspath( + skip_path.replace("\\", "/") + ): return True position = os.path.split(filename) while position[1]: - if position[1] in config['skip']: + if position[1] in config["skip"]: return True position = os.path.split(position[0]) - for glob in config['skip_glob']: - if fnmatch.fnmatch(filename, glob) or fnmatch.fnmatch('/' + filename, glob): + for glob in config["skip_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)): + if not ( + os.path.isfile(os_path) or os.path.isdir(os_path) or os.path.islink(os_path) + ): return True return False diff --git a/isort/stdlibs/py_three.py b/isort/stdlibs/py_three.py index 744e19d1a..fc7ab91b0 100644 --- a/isort/stdlibs/py_three.py +++ b/isort/stdlibs/py_three.py @@ -5,48 +5,319 @@ created and the names adjusted """ -standard_library_3 = ['AL', 'BaseHTTPServer', 'Bastion', 'CGIHTTPServer', 'Carbon', 'ColorPicker', - 'ConfigParser', 'Cookie', 'DEVICE', 'DocXMLRPCServer', 'EasyDialogs', 'FL', - 'FrameWork', 'GL', 'HTMLParser', 'MacOS', 'MimeWriter', 'MiniAEFrame', 'Nav', - 'PixMapWrapper', 'Queue', 'SUNAUDIODEV', 'ScrolledText', 'SimpleHTTPServer', - 'SimpleXMLRPCServer', 'SocketServer', 'StringIO', 'Tix', 'Tkinter', 'UserDict', - 'UserList', 'UserString', 'W', '__builtin__', 'abc', 'aepack', 'aetools', - 'aetypes', 'aifc', 'al', 'anydbm', 'applesingle', 'argparse', 'array', 'ast', - 'asynchat', 'asyncio', 'asyncore', 'atexit', 'audioop', 'autoGIL', 'base64', - 'bdb', 'binascii', 'binhex', 'bisect', 'bsddb', 'buildtools', 'builtins', - 'bz2', 'cPickle', 'cProfile', 'cStringIO', 'calendar', 'cd', 'cfmfile', 'cgi', - 'cgitb', 'chunk', 'cmath', 'cmd', 'code', 'codecs', 'codeop', 'collections', - 'colorsys', 'commands', 'compileall', 'compiler', 'concurrent', 'configparser', - 'contextlib', 'contextvars', 'cookielib', 'copy', 'copy_reg', 'copyreg', 'crypt', 'csv', - 'ctypes', 'curses', 'dataclasses', 'datetime', 'dbhash', 'dbm', 'decimal', 'difflib', - 'dircache', 'dis', 'distutils', 'dl', 'doctest', 'dumbdbm', 'dummy_thread', - 'dummy_threading', 'email', 'encodings', 'ensurepip', 'enum', 'errno', - 'exceptions', 'faulthandler', 'fcntl', 'filecmp', 'fileinput', 'findertools', - 'fl', 'flp', 'fm', 'fnmatch', 'formatter', 'fpectl', 'fpformat', 'fractions', - 'ftplib', 'functools', 'future_builtins', 'gc', 'gdbm', 'gensuitemodule', - 'getopt', 'getpass', 'gettext', 'gl', 'glob', 'grp', 'gzip', 'hashlib', - 'heapq', 'hmac', 'hotshot', 'html', 'htmlentitydefs', 'htmllib', 'http', - 'httplib', 'ic', 'icopen', 'imageop', 'imaplib', 'imgfile', 'imghdr', 'imp', - 'importlib', 'imputil', 'inspect', 'io', 'ipaddress', 'itertools', 'jpeg', - 'json', 'keyword', 'lib2to3', 'linecache', 'locale', 'logging', 'lzma', - 'macerrors', 'macostools', 'macpath', 'macresource', 'mailbox', 'mailcap', - 'marshal', 'math', 'md5', 'mhlib', 'mimetools', 'mimetypes', 'mimify', 'mmap', - 'modulefinder', 'msilib', 'msvcrt', 'multifile', 'multiprocessing', 'mutex', - 'netrc', 'new', 'nis', 'nntplib', 'numbers', 'operator', 'optparse', 'os', - 'ossaudiodev', 'parser', 'pathlib', 'pdb', 'pickle', 'pickletools', 'pipes', - 'pkgutil', 'platform', 'plistlib', 'popen2', 'poplib', 'posix', 'posixfile', - 'pprint', 'profile', 'pstats', 'pty', 'pwd', 'py_compile', 'pyclbr', 'pydoc', - 'queue', 'quopri', 'random', 're', 'readline', 'reprlib', 'resource', 'rexec', - 'rfc822', 'rlcompleter', 'robotparser', 'runpy', 'sched', 'secrets', 'select', - 'selectors', 'sets', 'sgmllib', 'sha', 'shelve', 'shlex', 'shutil', 'signal', - 'site', 'sitecustomize', 'smtpd', 'smtplib', 'sndhdr', 'socket', 'socketserver', - 'spwd', 'sqlite3', 'ssl', 'stat', 'statistics', 'statvfs', 'string', 'stringprep', - 'struct', 'subprocess', 'sunau', 'sunaudiodev', 'symbol', 'symtable', 'sys', - 'sysconfig', 'syslog', 'tabnanny', 'tarfile', 'telnetlib', 'tempfile', 'termios', - 'test', 'textwrap', 'this', 'thread', 'threading', 'time', 'timeit', 'tkinter', - 'token', 'tokenize', 'trace', 'traceback', 'tracemalloc', 'ttk', 'tty', 'turtle', - 'turtledemo', 'types', 'typing', 'unicodedata', 'unittest', 'urllib', 'urllib2', - 'urlparse', 'usercustomize', 'uu', 'uuid', 'venv', 'videoreader', - 'warnings', 'wave', 'weakref', 'webbrowser', 'whichdb', 'winreg', 'winsound', - 'wsgiref', 'xdrlib', 'xml', 'xmlrpc', 'xmlrpclib', 'zipapp', 'zipfile', - 'zipimport', 'zlib'] +standard_library_3 = [ + "AL", + "BaseHTTPServer", + "Bastion", + "CGIHTTPServer", + "Carbon", + "ColorPicker", + "ConfigParser", + "Cookie", + "DEVICE", + "DocXMLRPCServer", + "EasyDialogs", + "FL", + "FrameWork", + "GL", + "HTMLParser", + "MacOS", + "MimeWriter", + "MiniAEFrame", + "Nav", + "PixMapWrapper", + "Queue", + "SUNAUDIODEV", + "ScrolledText", + "SimpleHTTPServer", + "SimpleXMLRPCServer", + "SocketServer", + "StringIO", + "Tix", + "Tkinter", + "UserDict", + "UserList", + "UserString", + "W", + "__builtin__", + "abc", + "aepack", + "aetools", + "aetypes", + "aifc", + "al", + "anydbm", + "applesingle", + "argparse", + "array", + "ast", + "asynchat", + "asyncio", + "asyncore", + "atexit", + "audioop", + "autoGIL", + "base64", + "bdb", + "binascii", + "binhex", + "bisect", + "bsddb", + "buildtools", + "builtins", + "bz2", + "cPickle", + "cProfile", + "cStringIO", + "calendar", + "cd", + "cfmfile", + "cgi", + "cgitb", + "chunk", + "cmath", + "cmd", + "code", + "codecs", + "codeop", + "collections", + "colorsys", + "commands", + "compileall", + "compiler", + "concurrent", + "configparser", + "contextlib", + "contextvars", + "cookielib", + "copy", + "copy_reg", + "copyreg", + "crypt", + "csv", + "ctypes", + "curses", + "dataclasses", + "datetime", + "dbhash", + "dbm", + "decimal", + "difflib", + "dircache", + "dis", + "distutils", + "dl", + "doctest", + "dumbdbm", + "dummy_thread", + "dummy_threading", + "email", + "encodings", + "ensurepip", + "enum", + "errno", + "exceptions", + "faulthandler", + "fcntl", + "filecmp", + "fileinput", + "findertools", + "fl", + "flp", + "fm", + "fnmatch", + "formatter", + "fpectl", + "fpformat", + "fractions", + "ftplib", + "functools", + "future_builtins", + "gc", + "gdbm", + "gensuitemodule", + "getopt", + "getpass", + "gettext", + "gl", + "glob", + "grp", + "gzip", + "hashlib", + "heapq", + "hmac", + "hotshot", + "html", + "htmlentitydefs", + "htmllib", + "http", + "httplib", + "ic", + "icopen", + "imageop", + "imaplib", + "imgfile", + "imghdr", + "imp", + "importlib", + "imputil", + "inspect", + "io", + "ipaddress", + "itertools", + "jpeg", + "json", + "keyword", + "lib2to3", + "linecache", + "locale", + "logging", + "lzma", + "macerrors", + "macostools", + "macpath", + "macresource", + "mailbox", + "mailcap", + "marshal", + "math", + "md5", + "mhlib", + "mimetools", + "mimetypes", + "mimify", + "mmap", + "modulefinder", + "msilib", + "msvcrt", + "multifile", + "multiprocessing", + "mutex", + "netrc", + "new", + "nis", + "nntplib", + "numbers", + "operator", + "optparse", + "os", + "ossaudiodev", + "parser", + "pathlib", + "pdb", + "pickle", + "pickletools", + "pipes", + "pkgutil", + "platform", + "plistlib", + "popen2", + "poplib", + "posix", + "posixfile", + "pprint", + "profile", + "pstats", + "pty", + "pwd", + "py_compile", + "pyclbr", + "pydoc", + "queue", + "quopri", + "random", + "re", + "readline", + "reprlib", + "resource", + "rexec", + "rfc822", + "rlcompleter", + "robotparser", + "runpy", + "sched", + "secrets", + "select", + "selectors", + "sets", + "sgmllib", + "sha", + "shelve", + "shlex", + "shutil", + "signal", + "site", + "sitecustomize", + "smtpd", + "smtplib", + "sndhdr", + "socket", + "socketserver", + "spwd", + "sqlite3", + "ssl", + "stat", + "statistics", + "statvfs", + "string", + "stringprep", + "struct", + "subprocess", + "sunau", + "sunaudiodev", + "symbol", + "symtable", + "sys", + "sysconfig", + "syslog", + "tabnanny", + "tarfile", + "telnetlib", + "tempfile", + "termios", + "test", + "textwrap", + "this", + "thread", + "threading", + "time", + "timeit", + "tkinter", + "token", + "tokenize", + "trace", + "traceback", + "tracemalloc", + "ttk", + "tty", + "turtle", + "turtledemo", + "types", + "typing", + "unicodedata", + "unittest", + "urllib", + "urllib2", + "urlparse", + "usercustomize", + "uu", + "uuid", + "venv", + "videoreader", + "warnings", + "wave", + "weakref", + "webbrowser", + "whichdb", + "winreg", + "winsound", + "wsgiref", + "xdrlib", + "xml", + "xmlrpc", + "xmlrpclib", + "zipapp", + "zipfile", + "zipimport", + "zlib", +] diff --git a/isort/stdlibs/py_two.py b/isort/stdlibs/py_two.py index f00c6f3e1..b2e867a89 100644 --- a/isort/stdlibs/py_two.py +++ b/isort/stdlibs/py_two.py @@ -2,48 +2,320 @@ File contains the standard library of python 2.7 """ -standard_library_2 = ['AL', 'BaseHTTPServer', 'Bastion', 'CGIHTTPServer', 'Carbon', 'ColorPicker', - 'ConfigParser', 'Cookie', 'DEVICE', 'DocXMLRPCServer', 'EasyDialogs', 'FL', - 'FrameWork', 'GL', 'HTMLParser', 'MacOS', 'MimeWriter', 'MiniAEFrame', 'Nav', - 'PixMapWrapper', 'Queue', 'SUNAUDIODEV', 'ScrolledText', 'SimpleHTTPServer', - 'SimpleXMLRPCServer', 'SocketServer', 'StringIO', 'Tix', 'Tkinter', 'UserDict', - 'UserList', 'UserString', 'W', '__builtin__', 'abc', 'aepack', 'aetools', - 'aetypes', 'aifc', 'al', 'anydbm', 'applesingle', 'argparse', 'array', 'ast', - 'asynchat', 'asyncio', 'asyncore', 'atexit', 'audioop', 'autoGIL', 'base64', - 'bdb', 'binascii', 'binhex', 'bisect', 'bsddb', 'buildtools', 'builtins', - 'bz2', 'cPickle', 'cProfile', 'cStringIO', 'calendar', 'cd', 'cfmfile', 'cgi', - 'cgitb', 'chunk', 'cmath', 'cmd', 'code', 'codecs', 'codeop', 'collections', - 'colorsys', 'commands', 'compileall', 'compiler', 'concurrent', 'configparser', - 'contextlib', 'contextvars', 'cookielib', 'copy', 'copy_reg', 'copyreg', 'crypt', 'csv', - 'ctypes', 'curses', 'dataclasses', 'datetime', 'dbhash', 'dbm', 'decimal', 'difflib', - 'dircache', 'dis', 'distutils', 'dl', 'doctest', 'dumbdbm', 'dummy_thread', - 'dummy_threading', 'email', 'encodings', 'ensurepip', 'enum', 'errno', - 'exceptions', 'faulthandler', 'fcntl', 'filecmp', 'fileinput', 'findertools', - 'fl', 'flp', 'fm', 'fnmatch', 'formatter', 'fpectl', 'fpformat', 'fractions', - 'ftplib', 'functools', 'future_builtins', 'gc', 'gdbm', 'gensuitemodule', - 'getopt', 'getpass', 'gettext', 'gl', 'glob', 'grp', 'gzip', 'hashlib', - 'heapq', 'hmac', 'hotshot', 'html', 'htmlentitydefs', 'htmllib', 'http', - 'httplib', 'ic', 'icopen', 'imageop', 'imaplib', 'imgfile', 'imghdr', 'imp', - 'importlib', 'imputil', 'inspect', 'io', 'ipaddress', 'itertools', 'jpeg', - 'json', 'keyword', 'lib2to3', 'linecache', 'locale', 'logging', 'lzma', - 'macerrors', 'macostools', 'macpath', 'macresource', 'mailbox', 'mailcap', - 'marshal', 'math', 'md5', 'mhlib', 'mimetools', 'mimetypes', 'mimify', 'mmap', - 'modulefinder', 'msilib', 'msvcrt', 'multifile', 'multiprocessing', 'mutex', - 'netrc', 'new', 'nis', 'nntplib', 'numbers', 'operator', 'optparse', 'os', - 'ossaudiodev', 'parser', 'pathlib', 'pdb', 'pickle', 'pickletools', 'pipes', - 'pkgutil', 'platform', 'plistlib', 'popen2', 'poplib', 'posix', 'posixfile', - 'pprint', 'profile', 'pstats', 'pty', 'pwd', 'py_compile', 'pyclbr', 'pydoc', - 'queue', 'quopri', 'random', 're', 'readline', 'reprlib', 'resource', 'rexec', - 'rfc822', 'rlcompleter', 'robotparser', 'runpy', 'sched', 'secrets', 'select', - 'selectors', 'sets', 'sgmllib', 'sha', 'shelve', 'shlex', 'shutil', 'signal', - 'site', 'sitecustomize', 'smtpd', 'smtplib', 'sndhdr', 'socket', 'socketserver', - 'spwd', 'sqlite3', 'ssl', 'stat', 'statistics', 'statvfs', 'string', 'stringprep', - 'struct', 'subprocess', 'sunau', 'sunaudiodev', 'symbol', 'symtable', 'sys', - 'sysconfig', 'syslog', 'tabnanny', 'tarfile', 'telnetlib', 'tempfile', 'termios', - 'test', 'textwrap', 'this', 'thread', 'threading', 'time', 'timeit', 'tkinter', - 'token', 'tokenize', 'trace', 'traceback', 'tracemalloc', 'ttk', 'tty', 'turtle', - 'turtledemo', 'types', 'typing', 'unicodedata', 'unittest', 'urllib', 'urllib2', - 'urlparse', 'user', 'usercustomize', 'uu', 'uuid', 'venv', 'videoreader', - 'warnings', 'wave', 'weakref', 'webbrowser', 'whichdb', 'winreg', 'winsound', - 'wsgiref', 'xdrlib', 'xml', 'xmlrpc', 'xmlrpclib', 'zipapp', 'zipfile', - 'zipimport', 'zlib'] +standard_library_2 = [ + "AL", + "BaseHTTPServer", + "Bastion", + "CGIHTTPServer", + "Carbon", + "ColorPicker", + "ConfigParser", + "Cookie", + "DEVICE", + "DocXMLRPCServer", + "EasyDialogs", + "FL", + "FrameWork", + "GL", + "HTMLParser", + "MacOS", + "MimeWriter", + "MiniAEFrame", + "Nav", + "PixMapWrapper", + "Queue", + "SUNAUDIODEV", + "ScrolledText", + "SimpleHTTPServer", + "SimpleXMLRPCServer", + "SocketServer", + "StringIO", + "Tix", + "Tkinter", + "UserDict", + "UserList", + "UserString", + "W", + "__builtin__", + "abc", + "aepack", + "aetools", + "aetypes", + "aifc", + "al", + "anydbm", + "applesingle", + "argparse", + "array", + "ast", + "asynchat", + "asyncio", + "asyncore", + "atexit", + "audioop", + "autoGIL", + "base64", + "bdb", + "binascii", + "binhex", + "bisect", + "bsddb", + "buildtools", + "builtins", + "bz2", + "cPickle", + "cProfile", + "cStringIO", + "calendar", + "cd", + "cfmfile", + "cgi", + "cgitb", + "chunk", + "cmath", + "cmd", + "code", + "codecs", + "codeop", + "collections", + "colorsys", + "commands", + "compileall", + "compiler", + "concurrent", + "configparser", + "contextlib", + "contextvars", + "cookielib", + "copy", + "copy_reg", + "copyreg", + "crypt", + "csv", + "ctypes", + "curses", + "dataclasses", + "datetime", + "dbhash", + "dbm", + "decimal", + "difflib", + "dircache", + "dis", + "distutils", + "dl", + "doctest", + "dumbdbm", + "dummy_thread", + "dummy_threading", + "email", + "encodings", + "ensurepip", + "enum", + "errno", + "exceptions", + "faulthandler", + "fcntl", + "filecmp", + "fileinput", + "findertools", + "fl", + "flp", + "fm", + "fnmatch", + "formatter", + "fpectl", + "fpformat", + "fractions", + "ftplib", + "functools", + "future_builtins", + "gc", + "gdbm", + "gensuitemodule", + "getopt", + "getpass", + "gettext", + "gl", + "glob", + "grp", + "gzip", + "hashlib", + "heapq", + "hmac", + "hotshot", + "html", + "htmlentitydefs", + "htmllib", + "http", + "httplib", + "ic", + "icopen", + "imageop", + "imaplib", + "imgfile", + "imghdr", + "imp", + "importlib", + "imputil", + "inspect", + "io", + "ipaddress", + "itertools", + "jpeg", + "json", + "keyword", + "lib2to3", + "linecache", + "locale", + "logging", + "lzma", + "macerrors", + "macostools", + "macpath", + "macresource", + "mailbox", + "mailcap", + "marshal", + "math", + "md5", + "mhlib", + "mimetools", + "mimetypes", + "mimify", + "mmap", + "modulefinder", + "msilib", + "msvcrt", + "multifile", + "multiprocessing", + "mutex", + "netrc", + "new", + "nis", + "nntplib", + "numbers", + "operator", + "optparse", + "os", + "ossaudiodev", + "parser", + "pathlib", + "pdb", + "pickle", + "pickletools", + "pipes", + "pkgutil", + "platform", + "plistlib", + "popen2", + "poplib", + "posix", + "posixfile", + "pprint", + "profile", + "pstats", + "pty", + "pwd", + "py_compile", + "pyclbr", + "pydoc", + "queue", + "quopri", + "random", + "re", + "readline", + "reprlib", + "resource", + "rexec", + "rfc822", + "rlcompleter", + "robotparser", + "runpy", + "sched", + "secrets", + "select", + "selectors", + "sets", + "sgmllib", + "sha", + "shelve", + "shlex", + "shutil", + "signal", + "site", + "sitecustomize", + "smtpd", + "smtplib", + "sndhdr", + "socket", + "socketserver", + "spwd", + "sqlite3", + "ssl", + "stat", + "statistics", + "statvfs", + "string", + "stringprep", + "struct", + "subprocess", + "sunau", + "sunaudiodev", + "symbol", + "symtable", + "sys", + "sysconfig", + "syslog", + "tabnanny", + "tarfile", + "telnetlib", + "tempfile", + "termios", + "test", + "textwrap", + "this", + "thread", + "threading", + "time", + "timeit", + "tkinter", + "token", + "tokenize", + "trace", + "traceback", + "tracemalloc", + "ttk", + "tty", + "turtle", + "turtledemo", + "types", + "typing", + "unicodedata", + "unittest", + "urllib", + "urllib2", + "urlparse", + "user", + "usercustomize", + "uu", + "uuid", + "venv", + "videoreader", + "warnings", + "wave", + "weakref", + "webbrowser", + "whichdb", + "winreg", + "winsound", + "wsgiref", + "xdrlib", + "xml", + "xmlrpc", + "xmlrpclib", + "zipapp", + "zipfile", + "zipimport", + "zlib", +] diff --git a/isort/utils.py b/isort/utils.py index 0f6f57bf8..d0414b1e1 100644 --- a/isort/utils.py +++ b/isort/utils.py @@ -13,7 +13,7 @@ def exists_case_sensitive(path: str) -> bool: can only import using the case of the real file. """ result = os.path.exists(path) - if (sys.platform.startswith('win') or sys.platform == 'darwin') and result: + if (sys.platform.startswith("win") or sys.platform == "darwin") and result: directory, basename = os.path.split(path) result = basename in os.listdir(directory) return result @@ -55,9 +55,9 @@ def difference(a: Iterable[Any], b: Container[Any]) -> List[Any]: def infer_line_separator(file_contents: str) -> str: - if '\r\n' in file_contents: - return '\r\n' - elif '\r' in file_contents: - return '\r' + if "\r\n" in file_contents: + return "\r\n" + elif "\r" in file_contents: + return "\r" else: - return '\n' + return "\n" diff --git a/kate_plugin/isort_plugin.py b/kate_plugin/isort_plugin.py index f212c41d9..b6525cdfa 100644 --- a/kate_plugin/isort_plugin.py +++ b/kate_plugin/isort_plugin.py @@ -40,8 +40,12 @@ def sort_kate_imports(add_imports=(), remove_imports=()): view = document.activeView() position = view.cursorPosition() selection = view.selectionRange() - sorter = SortImports(file_contents=document.text(), add_imports=add_imports, remove_imports=remove_imports, - settings_path=os.path.dirname(os.path.abspath(str(document.url().path())))) + sorter = SortImports( + file_contents=document.text(), + add_imports=add_imports, + remove_imports=remove_imports, + settings_path=os.path.dirname(os.path.abspath(str(document.url().path()))), + ) document.setText(sorter.output) position.setLine(position.line() + sorter.length_change) if selection: @@ -63,9 +67,11 @@ def sort_imports(): @kate.action def add_imports(): """Add Imports""" - text, ok = QtGui.QInputDialog.getText(None, - 'Add Import', - 'Enter an import line to add (example: from os import path or os.path):') + text, ok = QtGui.QInputDialog.getText( + None, + "Add Import", + "Enter an import line to add (example: from os import path or os.path):", + ) if ok: sort_kate_imports(add_imports=text.split(";")) @@ -73,8 +79,10 @@ def add_imports(): @kate.action def remove_imports(): """Remove Imports""" - text, ok = QtGui.QInputDialog.getText(None, - 'Remove Import', - 'Enter an import line to remove (example: os.path or from os import path):') + text, ok = QtGui.QInputDialog.getText( + None, + "Remove Import", + "Enter an import line to remove (example: os.path or from os import path):", + ) if ok: sort_kate_imports(remove_imports=text.split(";")) diff --git a/kate_plugin/isort_plugin_old.py b/kate_plugin/isort_plugin_old.py index 5b6134d42..3a45261f7 100644 --- a/kate_plugin/isort_plugin_old.py +++ b/kate_plugin/isort_plugin_old.py @@ -40,8 +40,12 @@ def sort_kate_imports(add_imports=(), remove_imports=()): view = document.activeView() position = view.cursorPosition() selection = view.selectionRange() - sorter = SortImports(file_contents=document.text(), add_imports=add_imports, remove_imports=remove_imports, - settings_path=os.path.dirname(os.path.abspath(str(document.url().path())))) + sorter = SortImports( + file_contents=document.text(), + add_imports=add_imports, + remove_imports=remove_imports, + settings_path=os.path.dirname(os.path.abspath(str(document.url().path()))), + ) document.setText(sorter.output) position.setLine(position.line() + sorter.length_change) if selection: @@ -61,17 +65,21 @@ def sort_imports(): @kate.action(text="Add Import", shortcut="Ctrl+]", menu="Python") def add_imports(): - text, ok = QtGui.QInputDialog.getText(None, - 'Add Import', - 'Enter an import line to add (example: from os import path or os.path):') + text, ok = QtGui.QInputDialog.getText( + None, + "Add Import", + "Enter an import line to add (example: from os import path or os.path):", + ) if ok: sort_kate_imports(add_imports=text.split(";")) @kate.action(text="Remove Import", shortcut="Ctrl+Shift+]", menu="Python") def remove_imports(): - text, ok = QtGui.QInputDialog.getText(None, - 'Remove Import', - 'Enter an import line to remove (example: os.path or from os import path):') + text, ok = QtGui.QInputDialog.getText( + None, + "Remove Import", + "Enter an import line to remove (example: os.path or from os import path):", + ) if ok: sort_kate_imports(remove_imports=text.split(";")) diff --git a/setup.cfg b/setup.cfg index 49a95ee5a..a3f3f3447 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,14 +1,16 @@ [flake8] max-line-length = 160 ignore = - # The default ignore list: - E121,E123,E126,E226,E24,E704, - # Our additions: - # E127: continuation line over-indented for visual indent - # E128: continuation line under-indented for visual indent - E127,E128, - # W504 line break after binary operator - W504 + # E203 whitespace before ':' + E203 + # W504 line break before binary operator + W503 + +[isort] +combine_as_imports = True +include_trailing_comma = True +line_length = 88 +multi_line_output = 3 [mypy] python_version = 3.5 diff --git a/setup.py b/setup.py index f1e0747ce..2ba4164ac 100755 --- a/setup.py +++ b/setup.py @@ -2,45 +2,47 @@ from setuptools import find_packages, setup -with open('README.rst') as f: +with open("README.rst") as f: readme = f.read() -setup(name='isort', - version='4.3.21', - description='A Python utility / library to sort Python imports.', - long_description=readme, - author='Timothy Crosley', - author_email='timothy.crosley@gmail.com', - url='https://github.com/timothycrosley/isort', - license="MIT", - entry_points={ - 'console_scripts': [ - 'isort = isort.main:main', - ], - 'distutils.commands': ['isort = isort.main:ISortCommand'], - 'pylama.linter': ['isort = isort.pylama_isort:Linter'], - }, - packages=find_packages(), - extras_require={ - 'pipfile': ['pipreqs', 'requirementslib'], - 'pyproject': ['toml'], - 'requirements': ['pipreqs', 'pip-api'], - 'xdg_home': ['appdirs>=1.4.0'], - }, - python_requires=">=3.5", - keywords='Refactor, Python, Python3, Refactoring, Imports, Sort, Clean', - classifiers=['Development Status :: 6 - Mature', - 'Intended Audience :: Developers', - 'Natural Language :: English', - 'Environment :: Console', - 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: Implementation :: CPython', - 'Programming Language :: Python :: Implementation :: PyPy', - 'Topic :: Software Development :: Libraries', - 'Topic :: Utilities']) +setup( + name="isort", + version="4.3.21", + description="A Python utility / library to sort Python imports.", + long_description=readme, + author="Timothy Crosley", + author_email="timothy.crosley@gmail.com", + url="https://github.com/timothycrosley/isort", + license="MIT", + entry_points={ + "console_scripts": ["isort = isort.main:main"], + "distutils.commands": ["isort = isort.main:ISortCommand"], + "pylama.linter": ["isort = isort.pylama_isort:Linter"], + }, + packages=find_packages(), + extras_require={ + "pipfile": ["pipreqs", "requirementslib"], + "pyproject": ["toml"], + "requirements": ["pipreqs", "pip-api"], + "xdg_home": ["appdirs>=1.4.0"], + }, + python_requires=">=3.5", + keywords="Refactor, Python, Python3, Refactoring, Imports, Sort, Clean", + classifiers=[ + "Development Status :: 6 - Mature", + "Intended Audience :: Developers", + "Natural Language :: English", + "Environment :: Console", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Topic :: Software Development :: Libraries", + "Topic :: Utilities", + ], +) diff --git a/test_isort.py b/test_isort.py index 7f1d9a4fc..82bd2d127 100644 --- a/test_isort.py +++ b/test_isort.py @@ -58,18 +58,22 @@ SHORT_IMPORT = "from third_party import lib1, lib2, lib3, lib4" SINGLE_FROM_IMPORT = "from third_party import lib1" SINGLE_LINE_LONG_IMPORT = "from third_party import lib1, lib2, lib3, lib4, lib5, lib5ab" -REALLY_LONG_IMPORT = ("from third_party import lib1, lib2, lib3, lib4, lib5, lib6, lib7, lib8, lib9, lib10, lib11," - "lib12, lib13, lib14, lib15, lib16, lib17, lib18, lib20, lib21, lib22") -REALLY_LONG_IMPORT_WITH_COMMENT = ("from third_party import lib1, lib2, lib3, lib4, lib5, lib6, lib7, lib8, lib9, " - "lib10, lib11, lib12, lib13, lib14, lib15, lib16, lib17, lib18, lib20, lib21, lib22" - " # comment") +REALLY_LONG_IMPORT = ( + "from third_party import lib1, lib2, lib3, lib4, lib5, lib6, lib7, lib8, lib9, lib10, lib11," + "lib12, lib13, lib14, lib15, lib16, lib17, lib18, lib20, lib21, lib22" +) +REALLY_LONG_IMPORT_WITH_COMMENT = ( + "from third_party import lib1, lib2, lib3, lib4, lib5, lib6, lib7, lib8, lib9, " + "lib10, lib11, lib12, lib13, lib14, lib15, lib16, lib17, lib18, lib20, lib21, lib22" + " # comment" +) @pytest.fixture(scope="session", autouse=True) def default_settings_path(tmpdir_factory): - config_dir = tmpdir_factory.mktemp('config') - config_file = config_dir.join('.editorconfig').strpath - with open(config_file, 'w') as editorconfig: + config_dir = tmpdir_factory.mktemp("config") + config_file = config_dir.join(".editorconfig").strpath + with open(config_file, "w") as editorconfig: editorconfig.write(TEST_DEFAULT_CONFIG) with config_dir.as_cwd(): @@ -78,17 +82,20 @@ def default_settings_path(tmpdir_factory): def test_happy_path() -> None: """Test the most basic use case, straight imports no code, simply not organized by category.""" - test_input = ("import sys\n" - "import os\n" - "import myproject.test\n" - "import django.settings") - test_output = SortImports(file_contents=test_input, known_third_party=['django']).output - assert test_output == ("import os\n" - "import sys\n" - "\n" - "import django.settings\n" - "\n" - "import myproject.test\n") + test_input = ( + "import sys\n" "import os\n" "import myproject.test\n" "import django.settings" + ) + test_output = SortImports( + file_contents=test_input, known_third_party=["django"] + ).output + assert test_output == ( + "import os\n" + "import sys\n" + "\n" + "import django.settings\n" + "\n" + "import myproject.test\n" + ) def test_code_intermixed() -> None: @@ -98,17 +105,21 @@ def test_code_intermixed() -> None: (it should pull them all to the top) """ - test_input = ("import sys\n" - "print('yo')\n" - "print('I like to put code between imports cause I want stuff to break')\n" - "import myproject.test\n") + test_input = ( + "import sys\n" + "print('yo')\n" + "print('I like to put code between imports cause I want stuff to break')\n" + "import myproject.test\n" + ) test_output = SortImports(file_contents=test_input).output - assert test_output == ("import sys\n" - "\n" - "import myproject.test\n" - "\n" - "print('yo')\n" - "print('I like to put code between imports cause I want stuff to break')\n") + assert test_output == ( + "import sys\n" + "\n" + "import myproject.test\n" + "\n" + "print('yo')\n" + "print('I like to put code between imports cause I want stuff to break')\n" + ) def test_correct_space_between_imports() -> None: @@ -118,388 +129,520 @@ def test_correct_space_between_imports() -> None: (2 for method, class, or decorator definitions 1 for anything else) """ - test_input_method = ("import sys\n" - "def my_method():\n" - " print('hello world')\n") + test_input_method = "import sys\n" "def my_method():\n" " print('hello world')\n" test_output_method = SortImports(file_contents=test_input_method).output - assert test_output_method == ("import sys\n" - "\n" - "\n" - "def my_method():\n" - " print('hello world')\n") - - test_input_decorator = ("import sys\n" - "@my_decorator\n" - "def my_method():\n" - " print('hello world')\n") + assert test_output_method == ( + "import sys\n" "\n" "\n" "def my_method():\n" " print('hello world')\n" + ) + + test_input_decorator = ( + "import sys\n" + "@my_decorator\n" + "def my_method():\n" + " print('hello world')\n" + ) test_output_decorator = SortImports(file_contents=test_input_decorator).output - assert test_output_decorator == ("import sys\n" - "\n" - "\n" - "@my_decorator\n" - "def my_method():\n" - " print('hello world')\n") - - test_input_class = ("import sys\n" - "class MyClass(object):\n" - " pass\n") + assert test_output_decorator == ( + "import sys\n" + "\n" + "\n" + "@my_decorator\n" + "def my_method():\n" + " print('hello world')\n" + ) + + test_input_class = "import sys\n" "class MyClass(object):\n" " pass\n" test_output_class = SortImports(file_contents=test_input_class).output - assert test_output_class == ("import sys\n" - "\n" - "\n" - "class MyClass(object):\n" - " pass\n") - - test_input_other = ("import sys\n" - "print('yo')\n") + assert test_output_class == ( + "import sys\n" "\n" "\n" "class MyClass(object):\n" " pass\n" + ) + + test_input_other = "import sys\n" "print('yo')\n" test_output_other = SortImports(file_contents=test_input_other).output - assert test_output_other == ("import sys\n" - "\n" - "print('yo')\n") + assert test_output_other == ("import sys\n" "\n" "print('yo')\n") def test_sort_on_number() -> None: """Ensure numbers get sorted logically (10 > 9 not the other way around)""" - test_input = ("import lib10\n" - "import lib9\n") + test_input = "import lib10\n" "import lib9\n" test_output = SortImports(file_contents=test_input).output - assert test_output == ("import lib9\n" - "import lib10\n") + assert test_output == ("import lib9\n" "import lib10\n") def test_line_length() -> None: """Ensure isort enforces the set line_length.""" - assert len(SortImports(file_contents=REALLY_LONG_IMPORT, line_length=80).output.split("\n")[0]) <= 80 - assert len(SortImports(file_contents=REALLY_LONG_IMPORT, line_length=120).output.split("\n")[0]) <= 120 + assert ( + len( + SortImports(file_contents=REALLY_LONG_IMPORT, line_length=80).output.split( + "\n" + )[0] + ) + <= 80 + ) + assert ( + len( + SortImports(file_contents=REALLY_LONG_IMPORT, line_length=120).output.split( + "\n" + )[0] + ) + <= 120 + ) test_output = SortImports(file_contents=REALLY_LONG_IMPORT, line_length=42).output - assert test_output == ("from third_party import (lib1, lib2, lib3,\n" - " lib4, lib5, lib6,\n" - " lib7, lib8, lib9,\n" - " lib10, lib11,\n" - " lib12, lib13,\n" - " lib14, lib15,\n" - " lib16, lib17,\n" - " lib18, lib20,\n" - " lib21, lib22)\n") - - TEST_INPUT = ('from django.contrib.gis.gdal.field import (\n' - ' OFTDate, OFTDateTime, OFTInteger, OFTInteger64, OFTReal, OFTString,\n' - ' OFTTime,\n' - ')\n') # Test case described in issue #654 - assert SortImports(file_contents=TEST_INPUT, include_trailing_comma=True, line_length=79, - multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, balanced_wrapping=False).output == TEST_INPUT - - test_output = SortImports(file_contents=REALLY_LONG_IMPORT, line_length=42, wrap_length=32).output - assert test_output == ("from third_party import (lib1,\n" - " lib2,\n" - " lib3,\n" - " lib4,\n" - " lib5,\n" - " lib6,\n" - " lib7,\n" - " lib8,\n" - " lib9,\n" - " lib10,\n" - " lib11,\n" - " lib12,\n" - " lib13,\n" - " lib14,\n" - " lib15,\n" - " lib16,\n" - " lib17,\n" - " lib18,\n" - " lib20,\n" - " lib21,\n" - " lib22)\n") + assert test_output == ( + "from third_party import (lib1, lib2, lib3,\n" + " lib4, lib5, lib6,\n" + " lib7, lib8, lib9,\n" + " lib10, lib11,\n" + " lib12, lib13,\n" + " lib14, lib15,\n" + " lib16, lib17,\n" + " lib18, lib20,\n" + " lib21, lib22)\n" + ) + + TEST_INPUT = ( + "from django.contrib.gis.gdal.field import (\n" + " OFTDate, OFTDateTime, OFTInteger, OFTInteger64, OFTReal, OFTString,\n" + " OFTTime,\n" + ")\n" + ) # Test case described in issue #654 + assert ( + SortImports( + file_contents=TEST_INPUT, + include_trailing_comma=True, + line_length=79, + multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, + balanced_wrapping=False, + ).output + == TEST_INPUT + ) + + test_output = SortImports( + file_contents=REALLY_LONG_IMPORT, line_length=42, wrap_length=32 + ).output + assert test_output == ( + "from third_party import (lib1,\n" + " lib2,\n" + " lib3,\n" + " lib4,\n" + " lib5,\n" + " lib6,\n" + " lib7,\n" + " lib8,\n" + " lib9,\n" + " lib10,\n" + " lib11,\n" + " lib12,\n" + " lib13,\n" + " lib14,\n" + " lib15,\n" + " lib16,\n" + " lib17,\n" + " lib18,\n" + " lib20,\n" + " lib21,\n" + " lib22)\n" + ) def test_output_modes() -> None: """Test setting isort to use various output modes works as expected""" - test_output_grid = SortImports(file_contents=REALLY_LONG_IMPORT, - multi_line_output=WrapModes.GRID, line_length=40).output - assert test_output_grid == ("from third_party import (lib1, lib2,\n" - " lib3, lib4,\n" - " lib5, lib6,\n" - " lib7, lib8,\n" - " lib9, lib10,\n" - " lib11, lib12,\n" - " lib13, lib14,\n" - " lib15, lib16,\n" - " lib17, lib18,\n" - " lib20, lib21,\n" - " lib22)\n") - - test_output_vertical = SortImports(file_contents=REALLY_LONG_IMPORT, - multi_line_output=WrapModes.VERTICAL, line_length=40).output - assert test_output_vertical == ("from third_party import (lib1,\n" - " lib2,\n" - " lib3,\n" - " lib4,\n" - " lib5,\n" - " lib6,\n" - " lib7,\n" - " lib8,\n" - " lib9,\n" - " lib10,\n" - " lib11,\n" - " lib12,\n" - " lib13,\n" - " lib14,\n" - " lib15,\n" - " lib16,\n" - " lib17,\n" - " lib18,\n" - " lib20,\n" - " lib21,\n" - " lib22)\n") - - comment_output_vertical = SortImports(file_contents=REALLY_LONG_IMPORT_WITH_COMMENT, - multi_line_output=WrapModes.VERTICAL, line_length=40).output - assert comment_output_vertical == ("from third_party import (lib1, # comment\n" - " lib2,\n" - " lib3,\n" - " lib4,\n" - " lib5,\n" - " lib6,\n" - " lib7,\n" - " lib8,\n" - " lib9,\n" - " lib10,\n" - " lib11,\n" - " lib12,\n" - " lib13,\n" - " lib14,\n" - " lib15,\n" - " lib16,\n" - " lib17,\n" - " lib18,\n" - " lib20,\n" - " lib21,\n" - " lib22)\n") - - test_output_hanging_indent = SortImports(file_contents=REALLY_LONG_IMPORT, - multi_line_output=WrapModes.HANGING_INDENT, - line_length=40, indent=" ").output - assert test_output_hanging_indent == ("from third_party import lib1, lib2, \\\n" - " lib3, lib4, lib5, lib6, lib7, \\\n" - " lib8, lib9, lib10, lib11, lib12, \\\n" - " lib13, lib14, lib15, lib16, lib17, \\\n" - " lib18, lib20, lib21, lib22\n") - - comment_output_hanging_indent = SortImports(file_contents=REALLY_LONG_IMPORT_WITH_COMMENT, - multi_line_output=WrapModes.HANGING_INDENT, - line_length=40, indent=" ").output - assert comment_output_hanging_indent == ("from third_party import lib1, \\ # comment\n" - " lib2, lib3, lib4, lib5, lib6, \\\n" - " lib7, lib8, lib9, lib10, lib11, \\\n" - " lib12, lib13, lib14, lib15, lib16, \\\n" - " lib17, lib18, lib20, lib21, lib22\n") - - test_output_vertical_indent = SortImports(file_contents=REALLY_LONG_IMPORT, - multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, - line_length=40, indent=" ").output - assert test_output_vertical_indent == ("from third_party import (\n" - " lib1,\n" - " lib2,\n" - " lib3,\n" - " lib4,\n" - " lib5,\n" - " lib6,\n" - " lib7,\n" - " lib8,\n" - " lib9,\n" - " lib10,\n" - " lib11,\n" - " lib12,\n" - " lib13,\n" - " lib14,\n" - " lib15,\n" - " lib16,\n" - " lib17,\n" - " lib18,\n" - " lib20,\n" - " lib21,\n" - " lib22\n" - ")\n") - - comment_output_vertical_indent = SortImports(file_contents=REALLY_LONG_IMPORT_WITH_COMMENT, - multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, - line_length=40, indent=" ").output - assert comment_output_vertical_indent == ("from third_party import ( # comment\n" - " lib1,\n" - " lib2,\n" - " lib3,\n" - " lib4,\n" - " lib5,\n" - " lib6,\n" - " lib7,\n" - " lib8,\n" - " lib9,\n" - " lib10,\n" - " lib11,\n" - " lib12,\n" - " lib13,\n" - " lib14,\n" - " lib15,\n" - " lib16,\n" - " lib17,\n" - " lib18,\n" - " lib20,\n" - " lib21,\n" - " lib22\n" - ")\n") - - test_output_vertical_grid = SortImports(file_contents=REALLY_LONG_IMPORT, - multi_line_output=WrapModes.VERTICAL_GRID, - line_length=40, indent=" ").output - assert test_output_vertical_grid == ("from third_party import (\n" - " lib1, lib2, lib3, lib4, lib5, lib6,\n" - " lib7, lib8, lib9, lib10, lib11,\n" - " lib12, lib13, lib14, lib15, lib16,\n" - " lib17, lib18, lib20, lib21, lib22)\n") - - comment_output_vertical_grid = SortImports(file_contents=REALLY_LONG_IMPORT_WITH_COMMENT, - multi_line_output=WrapModes.VERTICAL_GRID, - line_length=40, indent=" ").output - assert comment_output_vertical_grid == ("from third_party import ( # comment\n" - " lib1, lib2, lib3, lib4, lib5, lib6,\n" - " lib7, lib8, lib9, lib10, lib11,\n" - " lib12, lib13, lib14, lib15, lib16,\n" - " lib17, lib18, lib20, lib21, lib22)\n") - - test_output_vertical_grid_grouped = SortImports(file_contents=REALLY_LONG_IMPORT, - multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, - line_length=40, indent=" ").output - assert test_output_vertical_grid_grouped == ("from third_party import (\n" - " lib1, lib2, lib3, lib4, lib5, lib6,\n" - " lib7, lib8, lib9, lib10, lib11,\n" - " lib12, lib13, lib14, lib15, lib16,\n" - " lib17, lib18, lib20, lib21, lib22\n" - ")\n") - - comment_output_vertical_grid_grouped = SortImports(file_contents=REALLY_LONG_IMPORT_WITH_COMMENT, - multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, - line_length=40, indent=" ").output - assert comment_output_vertical_grid_grouped == ("from third_party import ( # comment\n" - " lib1, lib2, lib3, lib4, lib5, lib6,\n" - " lib7, lib8, lib9, lib10, lib11,\n" - " lib12, lib13, lib14, lib15, lib16,\n" - " lib17, lib18, lib20, lib21, lib22\n" - ")\n") - - output_noqa = SortImports(file_contents=REALLY_LONG_IMPORT_WITH_COMMENT, - multi_line_output=WrapModes.NOQA).output - assert output_noqa == ("from third_party import lib1, lib2, lib3, lib4, lib5, lib6, lib7, lib8, lib9, lib10, lib11," - " lib12, lib13, lib14, lib15, lib16, lib17, lib18, lib20, lib21, lib22 " - "# NOQA comment\n") - - test_case = SortImports(file_contents=SINGLE_LINE_LONG_IMPORT, - multi_line_output=WrapModes.VERTICAL_GRID_GROUPED_NO_COMMA, - line_length=40, indent=' ').output + test_output_grid = SortImports( + file_contents=REALLY_LONG_IMPORT, + multi_line_output=WrapModes.GRID, + line_length=40, + ).output + assert test_output_grid == ( + "from third_party import (lib1, lib2,\n" + " lib3, lib4,\n" + " lib5, lib6,\n" + " lib7, lib8,\n" + " lib9, lib10,\n" + " lib11, lib12,\n" + " lib13, lib14,\n" + " lib15, lib16,\n" + " lib17, lib18,\n" + " lib20, lib21,\n" + " lib22)\n" + ) + + test_output_vertical = SortImports( + file_contents=REALLY_LONG_IMPORT, + multi_line_output=WrapModes.VERTICAL, + line_length=40, + ).output + assert test_output_vertical == ( + "from third_party import (lib1,\n" + " lib2,\n" + " lib3,\n" + " lib4,\n" + " lib5,\n" + " lib6,\n" + " lib7,\n" + " lib8,\n" + " lib9,\n" + " lib10,\n" + " lib11,\n" + " lib12,\n" + " lib13,\n" + " lib14,\n" + " lib15,\n" + " lib16,\n" + " lib17,\n" + " lib18,\n" + " lib20,\n" + " lib21,\n" + " lib22)\n" + ) + + comment_output_vertical = SortImports( + file_contents=REALLY_LONG_IMPORT_WITH_COMMENT, + multi_line_output=WrapModes.VERTICAL, + line_length=40, + ).output + assert comment_output_vertical == ( + "from third_party import (lib1, # comment\n" + " lib2,\n" + " lib3,\n" + " lib4,\n" + " lib5,\n" + " lib6,\n" + " lib7,\n" + " lib8,\n" + " lib9,\n" + " lib10,\n" + " lib11,\n" + " lib12,\n" + " lib13,\n" + " lib14,\n" + " lib15,\n" + " lib16,\n" + " lib17,\n" + " lib18,\n" + " lib20,\n" + " lib21,\n" + " lib22)\n" + ) + + test_output_hanging_indent = SortImports( + file_contents=REALLY_LONG_IMPORT, + multi_line_output=WrapModes.HANGING_INDENT, + line_length=40, + indent=" ", + ).output + assert test_output_hanging_indent == ( + "from third_party import lib1, lib2, \\\n" + " lib3, lib4, lib5, lib6, lib7, \\\n" + " lib8, lib9, lib10, lib11, lib12, \\\n" + " lib13, lib14, lib15, lib16, lib17, \\\n" + " lib18, lib20, lib21, lib22\n" + ) + + comment_output_hanging_indent = SortImports( + file_contents=REALLY_LONG_IMPORT_WITH_COMMENT, + multi_line_output=WrapModes.HANGING_INDENT, + line_length=40, + indent=" ", + ).output + assert comment_output_hanging_indent == ( + "from third_party import lib1, \\ # comment\n" + " lib2, lib3, lib4, lib5, lib6, \\\n" + " lib7, lib8, lib9, lib10, lib11, \\\n" + " lib12, lib13, lib14, lib15, lib16, \\\n" + " lib17, lib18, lib20, lib21, lib22\n" + ) + + test_output_vertical_indent = SortImports( + file_contents=REALLY_LONG_IMPORT, + multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, + line_length=40, + indent=" ", + ).output + assert test_output_vertical_indent == ( + "from third_party import (\n" + " lib1,\n" + " lib2,\n" + " lib3,\n" + " lib4,\n" + " lib5,\n" + " lib6,\n" + " lib7,\n" + " lib8,\n" + " lib9,\n" + " lib10,\n" + " lib11,\n" + " lib12,\n" + " lib13,\n" + " lib14,\n" + " lib15,\n" + " lib16,\n" + " lib17,\n" + " lib18,\n" + " lib20,\n" + " lib21,\n" + " lib22\n" + ")\n" + ) + + comment_output_vertical_indent = SortImports( + file_contents=REALLY_LONG_IMPORT_WITH_COMMENT, + multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, + line_length=40, + indent=" ", + ).output + assert comment_output_vertical_indent == ( + "from third_party import ( # comment\n" + " lib1,\n" + " lib2,\n" + " lib3,\n" + " lib4,\n" + " lib5,\n" + " lib6,\n" + " lib7,\n" + " lib8,\n" + " lib9,\n" + " lib10,\n" + " lib11,\n" + " lib12,\n" + " lib13,\n" + " lib14,\n" + " lib15,\n" + " lib16,\n" + " lib17,\n" + " lib18,\n" + " lib20,\n" + " lib21,\n" + " lib22\n" + ")\n" + ) + + test_output_vertical_grid = SortImports( + file_contents=REALLY_LONG_IMPORT, + multi_line_output=WrapModes.VERTICAL_GRID, + line_length=40, + indent=" ", + ).output + assert test_output_vertical_grid == ( + "from third_party import (\n" + " lib1, lib2, lib3, lib4, lib5, lib6,\n" + " lib7, lib8, lib9, lib10, lib11,\n" + " lib12, lib13, lib14, lib15, lib16,\n" + " lib17, lib18, lib20, lib21, lib22)\n" + ) + + comment_output_vertical_grid = SortImports( + file_contents=REALLY_LONG_IMPORT_WITH_COMMENT, + multi_line_output=WrapModes.VERTICAL_GRID, + line_length=40, + indent=" ", + ).output + assert comment_output_vertical_grid == ( + "from third_party import ( # comment\n" + " lib1, lib2, lib3, lib4, lib5, lib6,\n" + " lib7, lib8, lib9, lib10, lib11,\n" + " lib12, lib13, lib14, lib15, lib16,\n" + " lib17, lib18, lib20, lib21, lib22)\n" + ) + + test_output_vertical_grid_grouped = SortImports( + file_contents=REALLY_LONG_IMPORT, + multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, + line_length=40, + indent=" ", + ).output + assert test_output_vertical_grid_grouped == ( + "from third_party import (\n" + " lib1, lib2, lib3, lib4, lib5, lib6,\n" + " lib7, lib8, lib9, lib10, lib11,\n" + " lib12, lib13, lib14, lib15, lib16,\n" + " lib17, lib18, lib20, lib21, lib22\n" + ")\n" + ) + + comment_output_vertical_grid_grouped = SortImports( + file_contents=REALLY_LONG_IMPORT_WITH_COMMENT, + multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, + line_length=40, + indent=" ", + ).output + assert comment_output_vertical_grid_grouped == ( + "from third_party import ( # comment\n" + " lib1, lib2, lib3, lib4, lib5, lib6,\n" + " lib7, lib8, lib9, lib10, lib11,\n" + " lib12, lib13, lib14, lib15, lib16,\n" + " lib17, lib18, lib20, lib21, lib22\n" + ")\n" + ) + + output_noqa = SortImports( + file_contents=REALLY_LONG_IMPORT_WITH_COMMENT, multi_line_output=WrapModes.NOQA + ).output + assert output_noqa == ( + "from third_party import lib1, lib2, lib3, lib4, lib5, lib6, lib7, lib8, lib9, lib10, lib11," + " lib12, lib13, lib14, lib15, lib16, lib17, lib18, lib20, lib21, lib22 " + "# NOQA comment\n" + ) + + test_case = SortImports( + file_contents=SINGLE_LINE_LONG_IMPORT, + multi_line_output=WrapModes.VERTICAL_GRID_GROUPED_NO_COMMA, + line_length=40, + indent=" ", + ).output test_output_vertical_grid_grouped_doesnt_wrap_early = test_case - assert test_output_vertical_grid_grouped_doesnt_wrap_early == ("from third_party import (\n" - " lib1, lib2, lib3, lib4, lib5, lib5ab\n" - ")\n") + assert test_output_vertical_grid_grouped_doesnt_wrap_early == ( + "from third_party import (\n" " lib1, lib2, lib3, lib4, lib5, lib5ab\n" ")\n" + ) def test_qa_comment_case() -> None: test_input = "from veryveryveryveryveryveryveryveryveryveryvery import X # NOQA" - test_output = SortImports(file_contents=test_input, line_length=40, multi_line_output=WrapModes.NOQA).output - assert test_output == "from veryveryveryveryveryveryveryveryveryveryvery import X # NOQA\n" + test_output = SortImports( + file_contents=test_input, line_length=40, multi_line_output=WrapModes.NOQA + ).output + assert ( + test_output + == "from veryveryveryveryveryveryveryveryveryveryvery import X # NOQA\n" + ) test_input = "import veryveryveryveryveryveryveryveryveryveryvery # NOQA" - test_output = SortImports(file_contents=test_input, line_length=40, multi_line_output=WrapModes.NOQA).output - assert test_output == "import veryveryveryveryveryveryveryveryveryveryvery # NOQA\n" + test_output = SortImports( + file_contents=test_input, line_length=40, multi_line_output=WrapModes.NOQA + ).output + assert ( + test_output == "import veryveryveryveryveryveryveryveryveryveryvery # NOQA\n" + ) def test_length_sort() -> None: """Test setting isort to sort on length instead of alphabetically.""" - test_input = ("import medium_sizeeeeeeeeeeeeee\n" - "import shortie\n" - "import looooooooooooooooooooooooooooooooooooooong\n" - "import medium_sizeeeeeeeeeeeeea\n") + test_input = ( + "import medium_sizeeeeeeeeeeeeee\n" + "import shortie\n" + "import looooooooooooooooooooooooooooooooooooooong\n" + "import medium_sizeeeeeeeeeeeeea\n" + ) test_output = SortImports(file_contents=test_input, length_sort=True).output - assert test_output == ("import shortie\n" - "import medium_sizeeeeeeeeeeeeea\n" - "import medium_sizeeeeeeeeeeeeee\n" - "import looooooooooooooooooooooooooooooooooooooong\n") + assert test_output == ( + "import shortie\n" + "import medium_sizeeeeeeeeeeeeea\n" + "import medium_sizeeeeeeeeeeeeee\n" + "import looooooooooooooooooooooooooooooooooooooong\n" + ) def test_length_sort_section() -> None: """Test setting isort to sort on length instead of alphabetically for a specific section.""" - test_input = ("import medium_sizeeeeeeeeeeeeee\n" - "import shortie\n" - "import sys\n" - "import os\n" - "import looooooooooooooooooooooooooooooooooooooong\n" - "import medium_sizeeeeeeeeeeeeea\n") + test_input = ( + "import medium_sizeeeeeeeeeeeeee\n" + "import shortie\n" + "import sys\n" + "import os\n" + "import looooooooooooooooooooooooooooooooooooooong\n" + "import medium_sizeeeeeeeeeeeeea\n" + ) test_output = SortImports(file_contents=test_input, length_sort_stdlib=True).output - assert test_output == ("import os\n" - "import sys\n" - "\n" - "import looooooooooooooooooooooooooooooooooooooong\n" - "import medium_sizeeeeeeeeeeeeea\n" - "import medium_sizeeeeeeeeeeeeee\n" - "import shortie\n") + assert test_output == ( + "import os\n" + "import sys\n" + "\n" + "import looooooooooooooooooooooooooooooooooooooong\n" + "import medium_sizeeeeeeeeeeeeea\n" + "import medium_sizeeeeeeeeeeeeee\n" + "import shortie\n" + ) def test_convert_hanging() -> None: """Ensure that isort will convert hanging indents to correct indent method.""" - test_input = ("from third_party import lib1, lib2, \\\n" - " lib3, lib4, lib5, lib6, lib7, \\\n" - " lib8, lib9, lib10, lib11, lib12, \\\n" - " lib13, lib14, lib15, lib16, lib17, \\\n" - " lib18, lib20, lib21, lib22\n") - test_output = SortImports(file_contents=test_input, multi_line_output=WrapModes.GRID, - line_length=40).output - assert test_output == ("from third_party import (lib1, lib2,\n" - " lib3, lib4,\n" - " lib5, lib6,\n" - " lib7, lib8,\n" - " lib9, lib10,\n" - " lib11, lib12,\n" - " lib13, lib14,\n" - " lib15, lib16,\n" - " lib17, lib18,\n" - " lib20, lib21,\n" - " lib22)\n") + test_input = ( + "from third_party import lib1, lib2, \\\n" + " lib3, lib4, lib5, lib6, lib7, \\\n" + " lib8, lib9, lib10, lib11, lib12, \\\n" + " lib13, lib14, lib15, lib16, lib17, \\\n" + " lib18, lib20, lib21, lib22\n" + ) + test_output = SortImports( + file_contents=test_input, multi_line_output=WrapModes.GRID, line_length=40 + ).output + assert test_output == ( + "from third_party import (lib1, lib2,\n" + " lib3, lib4,\n" + " lib5, lib6,\n" + " lib7, lib8,\n" + " lib9, lib10,\n" + " lib11, lib12,\n" + " lib13, lib14,\n" + " lib15, lib16,\n" + " lib17, lib18,\n" + " lib20, lib21,\n" + " lib22)\n" + ) def test_custom_indent() -> None: """Ensure setting a custom indent will work as expected.""" - test_output = SortImports(file_contents=REALLY_LONG_IMPORT, multi_line_output=WrapModes.HANGING_INDENT, - line_length=40, indent=" ", balanced_wrapping=False).output - assert test_output == ("from third_party import lib1, lib2, \\\n" - " lib3, lib4, lib5, lib6, lib7, lib8, \\\n" - " lib9, lib10, lib11, lib12, lib13, \\\n" - " lib14, lib15, lib16, lib17, lib18, \\\n" - " lib20, lib21, lib22\n") - - test_output = SortImports(file_contents=REALLY_LONG_IMPORT, multi_line_output=WrapModes.HANGING_INDENT, - line_length=40, indent="' '", balanced_wrapping=False).output - assert test_output == ("from third_party import lib1, lib2, \\\n" - " lib3, lib4, lib5, lib6, lib7, lib8, \\\n" - " lib9, lib10, lib11, lib12, lib13, \\\n" - " lib14, lib15, lib16, lib17, lib18, \\\n" - " lib20, lib21, lib22\n") - - test_output = SortImports(file_contents=REALLY_LONG_IMPORT, multi_line_output=WrapModes.HANGING_INDENT, - line_length=40, indent="tab", balanced_wrapping=False).output - assert test_output == ("from third_party import lib1, lib2, \\\n" - "\tlib3, lib4, lib5, lib6, lib7, lib8, \\\n" - "\tlib9, lib10, lib11, lib12, lib13, \\\n" - "\tlib14, lib15, lib16, lib17, lib18, \\\n" - "\tlib20, lib21, lib22\n") - - test_output = SortImports(file_contents=REALLY_LONG_IMPORT, multi_line_output=WrapModes.HANGING_INDENT, - line_length=40, indent=2, balanced_wrapping=False).output - assert test_output == ("from third_party import lib1, lib2, \\\n" - " lib3, lib4, lib5, lib6, lib7, lib8, \\\n" - " lib9, lib10, lib11, lib12, lib13, \\\n" - " lib14, lib15, lib16, lib17, lib18, \\\n" - " lib20, lib21, lib22\n") + test_output = SortImports( + file_contents=REALLY_LONG_IMPORT, + multi_line_output=WrapModes.HANGING_INDENT, + line_length=40, + indent=" ", + balanced_wrapping=False, + ).output + assert test_output == ( + "from third_party import lib1, lib2, \\\n" + " lib3, lib4, lib5, lib6, lib7, lib8, \\\n" + " lib9, lib10, lib11, lib12, lib13, \\\n" + " lib14, lib15, lib16, lib17, lib18, \\\n" + " lib20, lib21, lib22\n" + ) + + test_output = SortImports( + file_contents=REALLY_LONG_IMPORT, + multi_line_output=WrapModes.HANGING_INDENT, + line_length=40, + indent="' '", + balanced_wrapping=False, + ).output + assert test_output == ( + "from third_party import lib1, lib2, \\\n" + " lib3, lib4, lib5, lib6, lib7, lib8, \\\n" + " lib9, lib10, lib11, lib12, lib13, \\\n" + " lib14, lib15, lib16, lib17, lib18, \\\n" + " lib20, lib21, lib22\n" + ) + + test_output = SortImports( + file_contents=REALLY_LONG_IMPORT, + multi_line_output=WrapModes.HANGING_INDENT, + line_length=40, + indent="tab", + balanced_wrapping=False, + ).output + assert test_output == ( + "from third_party import lib1, lib2, \\\n" + "\tlib3, lib4, lib5, lib6, lib7, lib8, \\\n" + "\tlib9, lib10, lib11, lib12, lib13, \\\n" + "\tlib14, lib15, lib16, lib17, lib18, \\\n" + "\tlib20, lib21, lib22\n" + ) + + test_output = SortImports( + file_contents=REALLY_LONG_IMPORT, + multi_line_output=WrapModes.HANGING_INDENT, + line_length=40, + indent=2, + balanced_wrapping=False, + ).output + assert test_output == ( + "from third_party import lib1, lib2, \\\n" + " lib3, lib4, lib5, lib6, lib7, lib8, \\\n" + " lib9, lib10, lib11, lib12, lib13, \\\n" + " lib14, lib15, lib16, lib17, lib18, \\\n" + " lib20, lib21, lib22\n" + ) def test_use_parentheses() -> None: @@ -517,7 +660,9 @@ def test_use_parentheses() -> None: ) test_output = SortImports( - file_contents=test_input, line_length=79, use_parentheses=True, + file_contents=test_input, + line_length=79, + use_parentheses=True, include_trailing_comma=True, ).output @@ -527,8 +672,10 @@ def test_use_parentheses() -> None: ) test_output = SortImports( - file_contents=test_input, line_length=79, use_parentheses=True, - multi_line_output=WrapModes.VERTICAL_HANGING_INDENT + file_contents=test_input, + line_length=79, + use_parentheses=True, + multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, ).output assert test_output == ( @@ -537,9 +684,11 @@ def test_use_parentheses() -> None: ) test_output = SortImports( - file_contents=test_input, line_length=79, use_parentheses=True, + file_contents=test_input, + line_length=79, + use_parentheses=True, multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, - include_trailing_comma=True + include_trailing_comma=True, ).output assert test_output == ( @@ -550,453 +699,494 @@ def test_use_parentheses() -> None: def test_skip() -> None: """Ensure skipping a single import will work as expected.""" - test_input = ("import myproject\n" - "import django\n" - "print('hey')\n" - "import sys # isort:skip this import needs to be placed here\n\n\n\n\n\n\n") + test_input = ( + "import myproject\n" + "import django\n" + "print('hey')\n" + "import sys # isort:skip this import needs to be placed here\n\n\n\n\n\n\n" + ) - test_output = SortImports(file_contents=test_input, known_third_party=['django']).output - assert test_output == ("import django\n" - "\n" - "import myproject\n" - "\n" - "print('hey')\n" - "import sys # isort:skip this import needs to be placed here\n") + test_output = SortImports( + file_contents=test_input, known_third_party=["django"] + ).output + assert test_output == ( + "import django\n" + "\n" + "import myproject\n" + "\n" + "print('hey')\n" + "import sys # isort:skip this import needs to be placed here\n" + ) def test_skip_with_file_name() -> None: """Ensure skipping a file works even when file_contents is provided.""" - test_input = ("import django\n" - "import myproject\n") + test_input = "import django\n" "import myproject\n" - sort_imports = SortImports(file_path='/baz.py', file_contents=test_input, settings_path=os.getcwd(), - skip=['baz.py']) + sort_imports = SortImports( + file_path="/baz.py", + file_contents=test_input, + settings_path=os.getcwd(), + skip=["baz.py"], + ) assert sort_imports.skipped assert sort_imports.output is None def test_skip_within_file() -> None: """Ensure skipping a whole file works.""" - test_input = ("# isort:skip_file\n" - "import django\n" - "import myproject\n") - sort_imports = SortImports(file_contents=test_input, known_third_party=['django']) + test_input = "# isort:skip_file\n" "import django\n" "import myproject\n" + sort_imports = SortImports(file_contents=test_input, known_third_party=["django"]) assert sort_imports.skipped assert sort_imports.output is None def test_force_to_top() -> None: """Ensure forcing a single import to the top of its category works as expected.""" - test_input = ("import lib6\n" - "import lib2\n" - "import lib5\n" - "import lib1\n") - test_output = SortImports(file_contents=test_input, force_to_top=['lib5']).output - assert test_output == ("import lib5\n" - "import lib1\n" - "import lib2\n" - "import lib6\n") + test_input = "import lib6\n" "import lib2\n" "import lib5\n" "import lib1\n" + test_output = SortImports(file_contents=test_input, force_to_top=["lib5"]).output + assert test_output == ( + "import lib5\n" "import lib1\n" "import lib2\n" "import lib6\n" + ) def test_add_imports() -> None: """Ensures adding imports works as expected.""" - test_input = ("import lib6\n" - "import lib2\n" - "import lib5\n" - "import lib1\n\n") - test_output = SortImports(file_contents=test_input, add_imports=['import lib4', 'import lib7']).output - assert test_output == ("import lib1\n" - "import lib2\n" - "import lib4\n" - "import lib5\n" - "import lib6\n" - "import lib7\n") + test_input = "import lib6\n" "import lib2\n" "import lib5\n" "import lib1\n\n" + test_output = SortImports( + file_contents=test_input, add_imports=["import lib4", "import lib7"] + ).output + assert test_output == ( + "import lib1\n" + "import lib2\n" + "import lib4\n" + "import lib5\n" + "import lib6\n" + "import lib7\n" + ) # Using simplified syntax - test_input = ("import lib6\n" - "import lib2\n" - "import lib5\n" - "import lib1\n\n") - test_output = SortImports(file_contents=test_input, add_imports=['lib4', 'lib7', 'lib8.a']).output - assert test_output == ("import lib1\n" - "import lib2\n" - "import lib4\n" - "import lib5\n" - "import lib6\n" - "import lib7\n" - "from lib8 import a\n") + test_input = "import lib6\n" "import lib2\n" "import lib5\n" "import lib1\n\n" + test_output = SortImports( + file_contents=test_input, add_imports=["lib4", "lib7", "lib8.a"] + ).output + assert test_output == ( + "import lib1\n" + "import lib2\n" + "import lib4\n" + "import lib5\n" + "import lib6\n" + "import lib7\n" + "from lib8 import a\n" + ) # On a file that has no pre-existing imports - test_input = ('"""Module docstring"""\n' - '\n' - 'class MyClass(object):\n' - ' pass\n') - test_output = SortImports(file_contents=test_input, add_imports=['from __future__ import print_function']).output - assert test_output == ('"""Module docstring"""\n' - 'from __future__ import print_function\n' - '\n' - '\n' - 'class MyClass(object):\n' - ' pass\n') + test_input = '"""Module docstring"""\n' "\n" "class MyClass(object):\n" " pass\n" + test_output = SortImports( + file_contents=test_input, add_imports=["from __future__ import print_function"] + ).output + assert test_output == ( + '"""Module docstring"""\n' + "from __future__ import print_function\n" + "\n" + "\n" + "class MyClass(object):\n" + " pass\n" + ) # On a file that has no pre-existing imports, and no doc-string - test_input = ('class MyClass(object):\n' - ' pass\n') - test_output = SortImports(file_contents=test_input, add_imports=['from __future__ import print_function']).output - assert test_output == ('from __future__ import print_function\n' - '\n' - '\n' - 'class MyClass(object):\n' - ' pass\n') + test_input = "class MyClass(object):\n" " pass\n" + test_output = SortImports( + file_contents=test_input, add_imports=["from __future__ import print_function"] + ).output + assert test_output == ( + "from __future__ import print_function\n" + "\n" + "\n" + "class MyClass(object):\n" + " pass\n" + ) # On a file with no content what so ever - test_input = ("") - test_output = SortImports(file_contents=test_input, add_imports=['lib4']).output + test_input = "" + test_output = SortImports(file_contents=test_input, add_imports=["lib4"]).output assert test_output == ("") # On a file with no content what so ever, after force_adds is set to True - test_input = ("") - test_output = SortImports(file_contents=test_input, add_imports=['lib4'], force_adds=True).output + test_input = "" + test_output = SortImports( + file_contents=test_input, add_imports=["lib4"], force_adds=True + ).output assert test_output == ("import lib4\n") def test_remove_imports() -> None: """Ensures removing imports works as expected.""" - test_input = ("import lib6\n" - "import lib2\n" - "import lib5\n" - "import lib1") - test_output = SortImports(file_contents=test_input, remove_imports=['lib2', 'lib6']).output - assert test_output == ("import lib1\n" - "import lib5\n") + test_input = "import lib6\n" "import lib2\n" "import lib5\n" "import lib1" + test_output = SortImports( + file_contents=test_input, remove_imports=["lib2", "lib6"] + ).output + assert test_output == ("import lib1\n" "import lib5\n") # Using natural syntax - test_input = ("import lib6\n" - "import lib2\n" - "import lib5\n" - "import lib1\n" - "from lib8 import a") - test_output = SortImports(file_contents=test_input, remove_imports=['import lib2', 'import lib6', - 'from lib8 import a']).output - assert test_output == ("import lib1\n" - "import lib5\n") + test_input = ( + "import lib6\n" + "import lib2\n" + "import lib5\n" + "import lib1\n" + "from lib8 import a" + ) + test_output = SortImports( + file_contents=test_input, + remove_imports=["import lib2", "import lib6", "from lib8 import a"], + ).output + assert test_output == ("import lib1\n" "import lib5\n") def test_explicitly_local_import() -> None: """Ensure that explicitly local imports are separated.""" - test_input = ("import lib1\n" - "import lib2\n" - "import .lib6\n" - "from . import lib7") - assert SortImports(file_contents=test_input).output == ("import lib1\n" - "import lib2\n" - "\n" - "import .lib6\n" - "from . import lib7\n") + test_input = "import lib1\n" "import lib2\n" "import .lib6\n" "from . import lib7" + assert SortImports(file_contents=test_input).output == ( + "import lib1\n" "import lib2\n" "\n" "import .lib6\n" "from . import lib7\n" + ) def test_quotes_in_file() -> None: """Ensure imports within triple quotes don't get imported.""" - test_input = ('import os\n' - '\n' - '"""\n' - 'Let us\n' - 'import foo\n' - 'okay?\n' - '"""\n') + test_input = "import os\n" "\n" '"""\n' "Let us\n" "import foo\n" "okay?\n" '"""\n' assert SortImports(file_contents=test_input).output == test_input - test_input = ('import os\n' - '\n' - "'\"\"\"'\n" - 'import foo\n') - assert SortImports(file_contents=test_input).output == ('import os\n' - '\n' - 'import foo\n' - '\n' - "'\"\"\"'\n") - - test_input = ('import os\n' - '\n' - '"""Let us"""\n' - 'import foo\n' - '"""okay?"""\n') - assert SortImports(file_contents=test_input).output == ('import os\n' - '\n' - 'import foo\n' - '\n' - '"""Let us"""\n' - '"""okay?"""\n') - - test_input = ('import os\n' - '\n' - '#"""\n' - 'import foo\n' - '#"""') - assert SortImports(file_contents=test_input).output == ('import os\n' - '\n' - 'import foo\n' - '\n' - '#"""\n' - '#"""\n') - - test_input = ('import os\n' - '\n' - "'\\\n" - "import foo'\n") + test_input = "import os\n" "\n" '\'"""\'\n' "import foo\n" + assert SortImports(file_contents=test_input).output == ( + "import os\n" "\n" "import foo\n" "\n" '\'"""\'\n' + ) + + test_input = "import os\n" "\n" '"""Let us"""\n' "import foo\n" '"""okay?"""\n' + assert SortImports(file_contents=test_input).output == ( + "import os\n" "\n" "import foo\n" "\n" '"""Let us"""\n' '"""okay?"""\n' + ) + + test_input = "import os\n" "\n" '#"""\n' "import foo\n" '#"""' + assert SortImports(file_contents=test_input).output == ( + "import os\n" "\n" "import foo\n" "\n" '#"""\n' '#"""\n' + ) + + test_input = "import os\n" "\n" "'\\\n" "import foo'\n" assert SortImports(file_contents=test_input).output == test_input - test_input = ('import os\n' - '\n' - "'''\n" - "\\'''\n" - 'import junk\n' - "'''\n") + test_input = "import os\n" "\n" "'''\n" "\\'''\n" "import junk\n" "'''\n" assert SortImports(file_contents=test_input).output == test_input def test_check_newline_in_imports(capsys): """Ensure tests works correctly when new lines are in imports.""" - test_input = ('from lib1 import (\n' - ' sub1,\n' - ' sub2,\n' - ' sub3\n)\n') + test_input = "from lib1 import (\n" " sub1,\n" " sub2,\n" " sub3\n)\n" - SortImports(file_contents=test_input, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, line_length=20, - check=True, verbose=True) + SortImports( + file_contents=test_input, + multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, + line_length=20, + check=True, + verbose=True, + ) out, err = capsys.readouterr() - assert 'SUCCESS' in out + assert "SUCCESS" in out def test_forced_separate() -> None: """Ensure that forcing certain sub modules to show separately works as expected.""" - test_input = ('import sys\n' - 'import warnings\n' - 'from collections import OrderedDict\n' - '\n' - 'from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation\n' - 'from django.core.paginator import InvalidPage\n' - 'from django.core.urlresolvers import reverse\n' - 'from django.db import models\n' - 'from django.db.models.fields import FieldDoesNotExist\n' - 'from django.utils import six\n' - 'from django.utils.deprecation import RenameMethodsBase\n' - 'from django.utils.encoding import force_str, force_text\n' - 'from django.utils.http import urlencode\n' - 'from django.utils.translation import ugettext, ugettext_lazy\n' - '\n' - 'from django.contrib.admin import FieldListFilter\n' - 'from django.contrib.admin.exceptions import DisallowedModelAdminLookup\n' - 'from django.contrib.admin.options import IncorrectLookupParameters, IS_POPUP_VAR, TO_FIELD_VAR\n') - assert SortImports(file_contents=test_input, forced_separate=['django.contrib'], - known_third_party=['django'], line_length=120, order_by_type=False).output == test_input - - test_input = ('from .foo import bar\n' - '\n' - 'from .y import ca\n') - assert SortImports(file_contents=test_input, forced_separate=['.y'], - line_length=120, order_by_type=False).output == test_input + test_input = ( + "import sys\n" + "import warnings\n" + "from collections import OrderedDict\n" + "\n" + "from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation\n" + "from django.core.paginator import InvalidPage\n" + "from django.core.urlresolvers import reverse\n" + "from django.db import models\n" + "from django.db.models.fields import FieldDoesNotExist\n" + "from django.utils import six\n" + "from django.utils.deprecation import RenameMethodsBase\n" + "from django.utils.encoding import force_str, force_text\n" + "from django.utils.http import urlencode\n" + "from django.utils.translation import ugettext, ugettext_lazy\n" + "\n" + "from django.contrib.admin import FieldListFilter\n" + "from django.contrib.admin.exceptions import DisallowedModelAdminLookup\n" + "from django.contrib.admin.options import IncorrectLookupParameters, IS_POPUP_VAR, TO_FIELD_VAR\n" + ) + assert ( + SortImports( + file_contents=test_input, + forced_separate=["django.contrib"], + known_third_party=["django"], + line_length=120, + order_by_type=False, + ).output + == test_input + ) + + test_input = "from .foo import bar\n" "\n" "from .y import ca\n" + assert ( + SortImports( + file_contents=test_input, + forced_separate=[".y"], + line_length=120, + order_by_type=False, + ).output + == test_input + ) def test_default_section() -> None: """Test to ensure changing the default section works as expected.""" - test_input = ("import sys\n" - "import os\n" - "import myproject.test\n" - "import django.settings") - test_output = SortImports(file_contents=test_input, known_third_party=['django'], - default_section="FIRSTPARTY").output - assert test_output == ("import os\n" - "import sys\n" - "\n" - "import django.settings\n" - "\n" - "import myproject.test\n") - - test_output_custom = SortImports(file_contents=test_input, known_third_party=['django'], - default_section="STDLIB").output - assert test_output_custom == ("import myproject.test\n" - "import os\n" - "import sys\n" - "\n" - "import django.settings\n") + test_input = ( + "import sys\n" "import os\n" "import myproject.test\n" "import django.settings" + ) + test_output = SortImports( + file_contents=test_input, + known_third_party=["django"], + default_section="FIRSTPARTY", + ).output + assert test_output == ( + "import os\n" + "import sys\n" + "\n" + "import django.settings\n" + "\n" + "import myproject.test\n" + ) + + test_output_custom = SortImports( + file_contents=test_input, known_third_party=["django"], default_section="STDLIB" + ).output + assert test_output_custom == ( + "import myproject.test\n" + "import os\n" + "import sys\n" + "\n" + "import django.settings\n" + ) def test_first_party_overrides_standard_section() -> None: """Test to ensure changing the default section works as expected.""" - test_input = ("from HTMLParser import HTMLParseError, HTMLParser\n" - "import sys\n" - "import os\n" - "import this\n" - "import profile.test\n") - test_output = SortImports(file_contents=test_input, known_first_party=['profile']).output - assert test_output == ("import os\n" - "import sys\n" - "import this\n" - "from HTMLParser import HTMLParseError, HTMLParser\n" - "\n" - "import profile.test\n") + test_input = ( + "from HTMLParser import HTMLParseError, HTMLParser\n" + "import sys\n" + "import os\n" + "import this\n" + "import profile.test\n" + ) + test_output = SortImports( + file_contents=test_input, known_first_party=["profile"] + ).output + assert test_output == ( + "import os\n" + "import sys\n" + "import this\n" + "from HTMLParser import HTMLParseError, HTMLParser\n" + "\n" + "import profile.test\n" + ) def test_thirdy_party_overrides_standard_section() -> None: """Test to ensure changing the default section works as expected.""" - test_input = ("import sys\n" - "import os\n" - "import this\n" - "import profile.test\n") - test_output = SortImports(file_contents=test_input, known_third_party=['profile']).output - assert test_output == ("import os\n" - "import sys\n" - "import this\n" - "\n" - "import profile.test\n") + test_input = "import sys\n" "import os\n" "import this\n" "import profile.test\n" + test_output = SortImports( + file_contents=test_input, known_third_party=["profile"] + ).output + assert test_output == ( + "import os\n" "import sys\n" "import this\n" "\n" "import profile.test\n" + ) def test_known_pattern_path_expansion() -> None: """Test to ensure patterns ending with path sep gets expanded and nested packages treated as known patterns""" - test_input = ("from kate_plugin import isort_plugin\n" - "import sys\n" - "import isort.settings\n" - "import this\n" - "import os\n") + test_input = ( + "from kate_plugin import isort_plugin\n" + "import sys\n" + "import isort.settings\n" + "import this\n" + "import os\n" + ) test_output = SortImports( file_contents=test_input, - default_section='THIRDPARTY', - known_first_party=['./', 'this', 'kate_plugin'] + default_section="THIRDPARTY", + known_first_party=["./", "this", "kate_plugin"], ).output - assert test_output == ("import os\n" - "import sys\n" - "\n" - "import isort.settings\n" - "import this\n" - "from kate_plugin import isort_plugin\n") + assert test_output == ( + "import os\n" + "import sys\n" + "\n" + "import isort.settings\n" + "import this\n" + "from kate_plugin import isort_plugin\n" + ) def test_force_single_line_imports() -> None: """Test to ensure forcing imports to each have their own line works as expected.""" - test_input = ("from third_party import lib1, lib2, \\\n" - " lib3, lib4, lib5, lib6, lib7, \\\n" - " lib8, lib9, lib10, lib11, lib12, \\\n" - " lib13, lib14, lib15, lib16, lib17, \\\n" - " lib18, lib20, lib21, lib22\n") - test_output = SortImports(file_contents=test_input, multi_line_output=WrapModes.GRID, - line_length=40, force_single_line=True).output - assert test_output == ("from third_party import lib1\n" - "from third_party import lib2\n" - "from third_party import lib3\n" - "from third_party import lib4\n" - "from third_party import lib5\n" - "from third_party import lib6\n" - "from third_party import lib7\n" - "from third_party import lib8\n" - "from third_party import lib9\n" - "from third_party import lib10\n" - "from third_party import lib11\n" - "from third_party import lib12\n" - "from third_party import lib13\n" - "from third_party import lib14\n" - "from third_party import lib15\n" - "from third_party import lib16\n" - "from third_party import lib17\n" - "from third_party import lib18\n" - "from third_party import lib20\n" - "from third_party import lib21\n" - "from third_party import lib22\n") + test_input = ( + "from third_party import lib1, lib2, \\\n" + " lib3, lib4, lib5, lib6, lib7, \\\n" + " lib8, lib9, lib10, lib11, lib12, \\\n" + " lib13, lib14, lib15, lib16, lib17, \\\n" + " lib18, lib20, lib21, lib22\n" + ) + test_output = SortImports( + file_contents=test_input, + multi_line_output=WrapModes.GRID, + line_length=40, + force_single_line=True, + ).output + assert test_output == ( + "from third_party import lib1\n" + "from third_party import lib2\n" + "from third_party import lib3\n" + "from third_party import lib4\n" + "from third_party import lib5\n" + "from third_party import lib6\n" + "from third_party import lib7\n" + "from third_party import lib8\n" + "from third_party import lib9\n" + "from third_party import lib10\n" + "from third_party import lib11\n" + "from third_party import lib12\n" + "from third_party import lib13\n" + "from third_party import lib14\n" + "from third_party import lib15\n" + "from third_party import lib16\n" + "from third_party import lib17\n" + "from third_party import lib18\n" + "from third_party import lib20\n" + "from third_party import lib21\n" + "from third_party import lib22\n" + ) def test_force_single_line_long_imports() -> None: - test_input = ("from veryveryveryveryveryvery import small, big\n") - test_output = SortImports(file_contents=test_input, multi_line_output=WrapModes.NOQA, - line_length=40, force_single_line=True).output - assert test_output == ("from veryveryveryveryveryvery import big\n" - "from veryveryveryveryveryvery import small # NOQA\n") + test_input = "from veryveryveryveryveryvery import small, big\n" + test_output = SortImports( + file_contents=test_input, + multi_line_output=WrapModes.NOQA, + line_length=40, + force_single_line=True, + ).output + assert test_output == ( + "from veryveryveryveryveryvery import big\n" + "from veryveryveryveryveryvery import small # NOQA\n" + ) def test_titled_imports() -> None: """Tests setting custom titled/commented import sections.""" - test_input = ("import sys\n" - "import unicodedata\n" - "import statistics\n" - "import os\n" - "import myproject.test\n" - "import django.settings") - test_output = SortImports(file_contents=test_input, known_third_party=['django'], - import_heading_stdlib="Standard Library", import_heading_firstparty="My Stuff").output - assert test_output == ("# Standard Library\n" - "import os\n" - "import statistics\n" - "import sys\n" - "import unicodedata\n" - "\n" - "import django.settings\n" - "\n" - "# My Stuff\n" - "import myproject.test\n") - test_second_run = SortImports(file_contents=test_output, known_third_party=['django'], - import_heading_stdlib="Standard Library", import_heading_firstparty="My Stuff").output + test_input = ( + "import sys\n" + "import unicodedata\n" + "import statistics\n" + "import os\n" + "import myproject.test\n" + "import django.settings" + ) + test_output = SortImports( + file_contents=test_input, + known_third_party=["django"], + import_heading_stdlib="Standard Library", + import_heading_firstparty="My Stuff", + ).output + assert test_output == ( + "# Standard Library\n" + "import os\n" + "import statistics\n" + "import sys\n" + "import unicodedata\n" + "\n" + "import django.settings\n" + "\n" + "# My Stuff\n" + "import myproject.test\n" + ) + test_second_run = SortImports( + file_contents=test_output, + known_third_party=["django"], + import_heading_stdlib="Standard Library", + import_heading_firstparty="My Stuff", + ).output assert test_second_run == test_output def test_balanced_wrapping() -> None: """Tests balanced wrapping mode, where the length of individual lines maintain width.""" - test_input = ("from __future__ import (absolute_import, division, print_function,\n" - " unicode_literals)") - test_output = SortImports(file_contents=test_input, line_length=70, balanced_wrapping=True).output - assert test_output == ("from __future__ import (absolute_import, division,\n" - " print_function, unicode_literals)\n") + test_input = ( + "from __future__ import (absolute_import, division, print_function,\n" + " unicode_literals)" + ) + test_output = SortImports( + file_contents=test_input, line_length=70, balanced_wrapping=True + ).output + assert test_output == ( + "from __future__ import (absolute_import, division,\n" + " print_function, unicode_literals)\n" + ) def test_relative_import_with_space() -> None: """Tests the case where the relation and the module that is being imported from is separated with a space.""" - test_input = ("from ... fields.sproqet import SproqetCollection") - assert SortImports(file_contents=test_input).output == ("from ...fields.sproqet import SproqetCollection\n") - test_input = ("from .import foo") - test_output = ("from . import foo\n") + test_input = "from ... fields.sproqet import SproqetCollection" + assert SortImports(file_contents=test_input).output == ( + "from ...fields.sproqet import SproqetCollection\n" + ) + test_input = "from .import foo" + test_output = "from . import foo\n" assert SortImports(file_contents=test_input).output == test_output - test_input = ("from.import foo") - test_output = ("from . import foo\n") + test_input = "from.import foo" + test_output = "from . import foo\n" assert SortImports(file_contents=test_input).output == test_output def test_multiline_import() -> None: """Test the case where import spawns multiple lines with inconsistent indentation.""" - test_input = ("from pkg \\\n" - " import stuff, other_suff \\\n" - " more_stuff") - assert SortImports(file_contents=test_input).output == ("from pkg import more_stuff, other_suff, stuff\n") + test_input = ( + "from pkg \\\n" " import stuff, other_suff \\\n" " more_stuff" + ) + assert SortImports(file_contents=test_input).output == ( + "from pkg import more_stuff, other_suff, stuff\n" + ) # test again with a custom configuration - custom_configuration = {'force_single_line': True, - 'line_length': 120, - 'known_first_party': ['asdf', 'qwer'], - 'default_section': 'THIRDPARTY', - 'forced_separate': 'asdf'} # type: Dict[str, Any] - expected_output = ("from pkg import more_stuff\n" - "from pkg import other_suff\n" - "from pkg import stuff\n") - assert SortImports(file_contents=test_input, **custom_configuration).output == expected_output + custom_configuration = { + "force_single_line": True, + "line_length": 120, + "known_first_party": ["asdf", "qwer"], + "default_section": "THIRDPARTY", + "forced_separate": "asdf", + } # type: Dict[str, Any] + expected_output = ( + "from pkg import more_stuff\n" + "from pkg import other_suff\n" + "from pkg import stuff\n" + ) + assert ( + SortImports(file_contents=test_input, **custom_configuration).output + == expected_output + ) def test_single_multiline() -> None: """Test the case where a single import spawns multiple lines.""" - test_input = ("from os import\\\n" - " getuid\n" - "\n" - "print getuid()\n") + 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" - ) + assert output == ("from os import getuid\n" "\n" "print getuid()\n") def test_atomic_mode() -> None: # without syntax error, everything works OK - test_input = ("from b import d, c\n" - "from a import f, e\n") - assert SortImports(file_contents=test_input, atomic=True).output == ("from a import e, f\n" - "from b import c, d\n") + test_input = "from b import d, c\n" "from a import f, e\n" + assert SortImports(file_contents=test_input, atomic=True).output == ( + "from a import e, f\n" "from b import c, d\n" + ) # with syntax error content is not changed test_input += "while True print 'Hello world'" # blatant syntax error @@ -1005,201 +1195,237 @@ def test_atomic_mode() -> None: def test_order_by_type() -> None: test_input = "from module import Class, CONSTANT, function" - assert SortImports(file_contents=test_input, - order_by_type=True).output == ("from module import CONSTANT, Class, function\n") + assert SortImports(file_contents=test_input, order_by_type=True).output == ( + "from module import CONSTANT, Class, function\n" + ) # More complex sample data test_input = "from module import Class, CONSTANT, function, BASIC, Apple" - assert SortImports(file_contents=test_input, - order_by_type=True).output == ("from module import BASIC, CONSTANT, Apple, Class, function\n") + assert SortImports(file_contents=test_input, order_by_type=True).output == ( + "from module import BASIC, CONSTANT, Apple, Class, function\n" + ) # Really complex sample data, to verify we don't mess with top level imports, only nested ones - test_input = ("import StringIO\n" - "import glob\n" - "import os\n" - "import shutil\n" - "import tempfile\n" - "import time\n" - "from subprocess import PIPE, Popen, STDOUT\n") - - assert SortImports(file_contents=test_input, order_by_type=True).output == \ - ("import glob\n" - "import os\n" - "import shutil\n" - "import StringIO\n" - "import tempfile\n" - "import time\n" - "from subprocess import PIPE, STDOUT, Popen\n") + test_input = ( + "import StringIO\n" + "import glob\n" + "import os\n" + "import shutil\n" + "import tempfile\n" + "import time\n" + "from subprocess import PIPE, Popen, STDOUT\n" + ) + + assert SortImports(file_contents=test_input, order_by_type=True).output == ( + "import glob\n" + "import os\n" + "import shutil\n" + "import StringIO\n" + "import tempfile\n" + "import time\n" + "from subprocess import PIPE, STDOUT, Popen\n" + ) def test_custom_lines_after_import_section() -> None: """Test the case where the number of lines to output after imports has been explicitly set.""" - test_input = ("from a import b\n" - "foo = 'bar'\n") + test_input = "from a import b\n" "foo = 'bar'\n" # default case is one space if not method or class after imports - assert SortImports(file_contents=test_input).output == ("from a import b\n" - "\n" - "foo = 'bar'\n") + assert SortImports(file_contents=test_input).output == ( + "from a import b\n" "\n" "foo = 'bar'\n" + ) # test again with a custom number of lines after the import section - assert SortImports(file_contents=test_input, lines_after_imports=2).output == ("from a import b\n" - "\n" - "\n" - "foo = 'bar'\n") + assert SortImports(file_contents=test_input, lines_after_imports=2).output == ( + "from a import b\n" "\n" "\n" "foo = 'bar'\n" + ) def test_smart_lines_after_import_section() -> None: """Tests the default 'smart' behavior for dealing with lines after the import section""" # one space if not method or class after imports - test_input = ("from a import b\n" - "foo = 'bar'\n") - assert SortImports(file_contents=test_input).output == ("from a import b\n" - "\n" - "foo = 'bar'\n") + test_input = "from a import b\n" "foo = 'bar'\n" + assert SortImports(file_contents=test_input).output == ( + "from a import b\n" "\n" "foo = 'bar'\n" + ) # two spaces if a method or class after imports - test_input = ("from a import b\n" - "def my_function():\n" - " pass\n") - assert SortImports(file_contents=test_input).output == ("from a import b\n" - "\n" - "\n" - "def my_function():\n" - " pass\n") + test_input = "from a import b\n" "def my_function():\n" " pass\n" + assert SortImports(file_contents=test_input).output == ( + "from a import b\n" "\n" "\n" "def my_function():\n" " pass\n" + ) # two spaces if an async method after imports - test_input = ("from a import b\n" - "async def my_function():\n" - " pass\n") - assert SortImports(file_contents=test_input).output == ("from a import b\n" - "\n" - "\n" - "async def my_function():\n" - " pass\n") + test_input = "from a import b\n" "async def my_function():\n" " pass\n" + assert SortImports(file_contents=test_input).output == ( + "from a import b\n" "\n" "\n" "async def my_function():\n" " pass\n" + ) # two spaces if a method or class after imports - even if comment before function - test_input = ("from a import b\n" - "# comment should be ignored\n" - "def my_function():\n" - " pass\n") - assert SortImports(file_contents=test_input).output == ("from a import b\n" - "\n" - "\n" - "# comment should be ignored\n" - "def my_function():\n" - " pass\n") + test_input = ( + "from a import b\n" + "# comment should be ignored\n" + "def my_function():\n" + " pass\n" + ) + assert SortImports(file_contents=test_input).output == ( + "from a import b\n" + "\n" + "\n" + "# comment should be ignored\n" + "def my_function():\n" + " pass\n" + ) # ensure logic works with both style comments - test_input = ("from a import b\n" - '"""\n' - " comment should be ignored\n" - '"""\n' - "def my_function():\n" - " pass\n") - assert SortImports(file_contents=test_input).output == ("from a import b\n" - "\n" - "\n" - '"""\n' - " comment should be ignored\n" - '"""\n' - "def my_function():\n" - " pass\n") + test_input = ( + "from a import b\n" + '"""\n' + " comment should be ignored\n" + '"""\n' + "def my_function():\n" + " pass\n" + ) + assert SortImports(file_contents=test_input).output == ( + "from a import b\n" + "\n" + "\n" + '"""\n' + " comment should be ignored\n" + '"""\n' + "def my_function():\n" + " pass\n" + ) # Ensure logic doesn't incorrectly skip over assignments to multi-line strings - test_input = ("from a import b\n" - 'X = """test\n' - '"""\n' - "def my_function():\n" - " pass\n") - assert SortImports(file_contents=test_input).output == ("from a import b\n" - "\n" - 'X = """test\n' - '"""\n' - "def my_function():\n" - " pass\n") + test_input = ( + "from a import b\n" 'X = """test\n' '"""\n' "def my_function():\n" " pass\n" + ) + assert SortImports(file_contents=test_input).output == ( + "from a import b\n" + "\n" + 'X = """test\n' + '"""\n' + "def my_function():\n" + " pass\n" + ) def test_settings_combine_instead_of_overwrite() -> None: """Test to ensure settings combine logically, instead of fully overwriting.""" - assert set(SortImports(known_standard_library=['not_std_library']).config['known_standard_library']) == \ - set(SortImports().config['known_standard_library'] + ['not_std_library']) - - assert set(SortImports(not_known_standard_library=['thread']).config['known_standard_library']) == \ - {item for item in SortImports().config['known_standard_library'] if item != 'thread'} + assert set( + SortImports(known_standard_library=["not_std_library"]).config[ + "known_standard_library" + ] + ) == set(SortImports().config["known_standard_library"] + ["not_std_library"]) + + assert set( + SortImports(not_known_standard_library=["thread"]).config[ + "known_standard_library" + ] + ) == { + item + for item in SortImports().config["known_standard_library"] + if item != "thread" + } def test_combined_from_and_as_imports() -> None: """Test to ensure it's possible to combine from and as imports.""" - test_input = ("from translate.misc.multistring import multistring\n" - "from translate.storage import base, factory\n" - "from translate.storage.placeables import general, parse as rich_parse\n") - assert SortImports(file_contents=test_input, combine_as_imports=True).output == test_input - test_input = ("import os \nimport os as _os") - test_output = ("import os\nimport os as _os\n") - assert SortImports(file_contents=test_input, keep_direct_and_as_imports=True).output == test_output + test_input = ( + "from translate.misc.multistring import multistring\n" + "from translate.storage import base, factory\n" + "from translate.storage.placeables import general, parse as rich_parse\n" + ) + assert ( + SortImports(file_contents=test_input, combine_as_imports=True).output + == test_input + ) + test_input = "import os \nimport os as _os" + test_output = "import os\nimport os as _os\n" + assert ( + SortImports(file_contents=test_input, keep_direct_and_as_imports=True).output + == test_output + ) def test_as_imports_with_line_length() -> None: """Test to ensure it's possible to combine from and as imports.""" - test_input = ("from translate.storage import base as storage_base\n" - "from translate.storage.placeables import general, parse as rich_parse\n") - assert SortImports(file_contents=test_input, combine_as_imports=False, line_length=40).output == \ - ("from translate.storage import \\\n base as storage_base\n" - "from translate.storage.placeables import \\\n general\n" - "from translate.storage.placeables import \\\n parse as rich_parse\n") + test_input = ( + "from translate.storage import base as storage_base\n" + "from translate.storage.placeables import general, parse as rich_parse\n" + ) + assert SortImports( + file_contents=test_input, combine_as_imports=False, line_length=40 + ).output == ( + "from translate.storage import \\\n base as storage_base\n" + "from translate.storage.placeables import \\\n general\n" + "from translate.storage.placeables import \\\n parse as rich_parse\n" + ) def test_keep_comments() -> None: """Test to ensure isort properly keeps comments in tact after sorting.""" # Straight Import - test_input = ("import foo # bar\n") + test_input = "import foo # bar\n" assert SortImports(file_contents=test_input).output == test_input # Star import - test_input_star = ("from foo import * # bar\n") + test_input_star = "from foo import * # bar\n" assert SortImports(file_contents=test_input_star).output == test_input_star # Force Single Line From Import - test_input = ("from foo import bar # comment\n") - assert SortImports(file_contents=test_input, force_single_line=True).output == test_input + test_input = "from foo import bar # comment\n" + assert ( + SortImports(file_contents=test_input, force_single_line=True).output + == test_input + ) # From import - test_input = ("from foo import bar # My Comment\n") + test_input = "from foo import bar # My Comment\n" assert SortImports(file_contents=test_input).output == test_input # More complicated case - test_input = ("from a import b # My Comment1\n" - "from a import c # My Comment2\n") - assert SortImports(file_contents=test_input).output == \ - ("from a import b # My Comment1\n" - "from a import c # My Comment2\n") + test_input = "from a import b # My Comment1\n" "from a import c # My Comment2\n" + assert SortImports(file_contents=test_input).output == ( + "from a import b # My Comment1\n" "from a import c # My Comment2\n" + ) # Test case where imports comments make imports extend pass the line length - test_input = ("from a import b # My Comment1\n" - "from a import c # My Comment2\n" - "from a import d\n") - assert SortImports(file_contents=test_input, line_length=45).output == \ - ("from a import b # My Comment1\n" - "from a import c # My Comment2\n" - "from a import d\n") + test_input = ( + "from a import b # My Comment1\n" + "from a import c # My Comment2\n" + "from a import d\n" + ) + assert SortImports(file_contents=test_input, line_length=45).output == ( + "from a import b # My Comment1\n" + "from a import c # My Comment2\n" + "from a import d\n" + ) # Test case where imports with comments will be beyond line length limit - test_input = ("from a import b, c # My Comment1\n" - "from a import c, d # My Comment2 is really really really really long\n") - assert SortImports(file_contents=test_input, line_length=45).output == \ - ("from a import ( # My Comment1; My Comment2 is really really really really long\n" - " b, c, d)\n") + test_input = ( + "from a import b, c # My Comment1\n" + "from a import c, d # My Comment2 is really really really really long\n" + ) + assert SortImports(file_contents=test_input, line_length=45).output == ( + "from a import ( # My Comment1; My Comment2 is really really really really long\n" + " b, c, d)\n" + ) # Test that comments are not stripped from 'import ... as ...' by default - test_input = ("from a import b as bb # b comment\n" - "from a import c as cc # c comment\n") + test_input = ( + "from a import b as bb # b comment\n" "from a import c as cc # c comment\n" + ) assert SortImports(file_contents=test_input).output == test_input # Test that 'import ... as ...' comments are not collected inappropriately - test_input = ("from a import b as bb # b comment\n" - "from a import c as cc # c comment\n" - "from a import d\n") + test_input = ( + "from a import b as bb # b comment\n" + "from a import c as cc # c comment\n" + "from a import d\n" + ) assert SortImports(file_contents=test_input).output == test_input assert SortImports(file_contents=test_input, combine_as_imports=True).output == ( "from a import b as bb, c as cc, d # b comment; c comment\n" @@ -1208,20 +1434,25 @@ def test_keep_comments() -> None: def test_multiline_split_on_dot() -> None: """Test to ensure isort correctly handles multiline imports, even when split right after a '.'""" - test_input = ("from my_lib.my_package.test.level_1.level_2.level_3.level_4.level_5.\\\n" - " my_module import my_function") - assert SortImports(file_contents=test_input, line_length=70).output == \ - ("from my_lib.my_package.test.level_1.level_2.level_3.level_4.level_5.my_module import \\\n" - " my_function\n") + test_input = ( + "from my_lib.my_package.test.level_1.level_2.level_3.level_4.level_5.\\\n" + " my_module import my_function" + ) + assert SortImports(file_contents=test_input, line_length=70).output == ( + "from my_lib.my_package.test.level_1.level_2.level_3.level_4.level_5.my_module import \\\n" + " my_function\n" + ) def test_import_star() -> None: """Test to ensure isort handles star imports correctly""" - test_input = ("from blah import *\n" - "from blah import _potato\n") - assert SortImports(file_contents=test_input).output == ("from blah import *\n" - "from blah import _potato\n") - assert SortImports(file_contents=test_input, combine_star=True).output == ("from blah import *\n") + test_input = "from blah import *\n" "from blah import _potato\n" + assert SortImports(file_contents=test_input).output == ( + "from blah import *\n" "from blah import _potato\n" + ) + assert SortImports(file_contents=test_input, combine_star=True).output == ( + "from blah import *\n" + ) def test_include_trailing_comma() -> None: @@ -1272,8 +1503,7 @@ def test_include_trailing_comma() -> None: include_trailing_comma=True, ).output assert test_output_vertical_grid == ( - "from third_party import (\n" - " lib1, lib2, lib3, lib4,)\n" + "from third_party import (\n" " lib1, lib2, lib3, lib4,)\n" ) test_output_vertical_grid_grouped = SortImports( @@ -1283,20 +1513,17 @@ def test_include_trailing_comma() -> None: include_trailing_comma=True, ).output assert test_output_vertical_grid_grouped == ( - "from third_party import (\n" - " lib1, lib2, lib3, lib4,\n" - ")\n" + "from third_party import (\n" " lib1, lib2, lib3, lib4,\n" ")\n" ) test_output_wrap_single_import_with_use_parentheses = SortImports( file_contents=SINGLE_FROM_IMPORT, line_length=25, include_trailing_comma=True, - use_parentheses=True + use_parentheses=True, ).output assert test_output_wrap_single_import_with_use_parentheses == ( - "from third_party import (\n" - " lib1,)\n" + "from third_party import (\n" " lib1,)\n" ) test_output_wrap_single_import_vertical_indent = SortImports( @@ -1304,284 +1531,353 @@ def test_include_trailing_comma() -> None: line_length=25, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, include_trailing_comma=True, - use_parentheses=True + use_parentheses=True, ).output assert test_output_wrap_single_import_vertical_indent == ( - "from third_party import (\n" - " lib1,\n" - ")\n" + "from third_party import (\n" " lib1,\n" ")\n" ) def test_similar_to_std_library() -> None: """Test to ensure modules that are named similarly to a standard library import don't end up clobbered""" - test_input = ("import datetime\n" - "\n" - "import requests\n" - "import times\n") - assert SortImports(file_contents=test_input, known_third_party=["requests", "times"]).output == test_input + test_input = "import datetime\n" "\n" "import requests\n" "import times\n" + assert ( + SortImports( + file_contents=test_input, known_third_party=["requests", "times"] + ).output + == test_input + ) def test_correctly_placed_imports() -> None: """Test to ensure comments stay on correct placement after being sorted""" - test_input = ("from a import b # comment for b\n" - "from a import c # comment for c\n") - assert SortImports(file_contents=test_input, force_single_line=True).output == \ - ("from a import b # comment for b\n" - "from a import c # comment for c\n") - assert SortImports(file_contents=test_input).output == ("from a import b # comment for b\n" - "from a import c # comment for c\n") + test_input = "from a import b # comment for b\n" "from a import c # comment for c\n" + assert SortImports(file_contents=test_input, force_single_line=True).output == ( + "from a import b # comment for b\n" "from a import c # comment for c\n" + ) + assert SortImports(file_contents=test_input).output == ( + "from a import b # comment for b\n" "from a import c # comment for c\n" + ) # Full example test from issue #143 - test_input = ("from itertools import chain\n" - "\n" - "from django.test import TestCase\n" - "from model_mommy import mommy\n" - "\n" - "from apps.clientman.commands.download_usage_rights import associate_right_for_item_product\n" - "from apps.clientman.commands.download_usage_rights import associate_right_for_item_product_d" - "efinition\n" - "from apps.clientman.commands.download_usage_rights import associate_right_for_item_product_d" - "efinition_platform\n" - "from apps.clientman.commands.download_usage_rights import associate_right_for_item_product_p" - "latform\n" - "from apps.clientman.commands.download_usage_rights import associate_right_for_territory_reta" - "il_model\n" - "from apps.clientman.commands.download_usage_rights import associate_right_for_territory_reta" - "il_model_definition_platform_provider # noqa\n" - "from apps.clientman.commands.download_usage_rights import clear_right_for_item_product\n" - "from apps.clientman.commands.download_usage_rights import clear_right_for_item_product_defini" - "tion\n" - "from apps.clientman.commands.download_usage_rights import clear_right_for_item_product_defini" - "tion_platform\n" - "from apps.clientman.commands.download_usage_rights import clear_right_for_item_product_platfo" - "rm\n" - "from apps.clientman.commands.download_usage_rights import clear_right_for_territory_retail_mo" - "del\n" - "from apps.clientman.commands.download_usage_rights import clear_right_for_territory_retail_mo" - "del_definition_platform_provider # noqa\n" - "from apps.clientman.commands.download_usage_rights import create_download_usage_right\n" - "from apps.clientman.commands.download_usage_rights import delete_download_usage_right\n" - "from apps.clientman.commands.download_usage_rights import disable_download_for_item_product\n" - "from apps.clientman.commands.download_usage_rights import disable_download_for_item_product_d" - "efinition\n" - "from apps.clientman.commands.download_usage_rights import disable_download_for_item_product_d" - "efinition_platform\n" - "from apps.clientman.commands.download_usage_rights import disable_download_for_item_product_p" - "latform\n" - "from apps.clientman.commands.download_usage_rights import disable_download_for_territory_reta" - "il_model\n" - "from apps.clientman.commands.download_usage_rights import disable_download_for_territory_reta" - "il_model_definition_platform_provider # noqa\n" - "from apps.clientman.commands.download_usage_rights import get_download_rights_for_item\n" - "from apps.clientman.commands.download_usage_rights import get_right\n") - assert SortImports(file_contents=test_input, force_single_line=True, line_length=140, - known_third_party=["django", "model_mommy"]).output == test_input + test_input = ( + "from itertools import chain\n" + "\n" + "from django.test import TestCase\n" + "from model_mommy import mommy\n" + "\n" + "from apps.clientman.commands.download_usage_rights import associate_right_for_item_product\n" + "from apps.clientman.commands.download_usage_rights import associate_right_for_item_product_d" + "efinition\n" + "from apps.clientman.commands.download_usage_rights import associate_right_for_item_product_d" + "efinition_platform\n" + "from apps.clientman.commands.download_usage_rights import associate_right_for_item_product_p" + "latform\n" + "from apps.clientman.commands.download_usage_rights import associate_right_for_territory_reta" + "il_model\n" + "from apps.clientman.commands.download_usage_rights import associate_right_for_territory_reta" + "il_model_definition_platform_provider # noqa\n" + "from apps.clientman.commands.download_usage_rights import clear_right_for_item_product\n" + "from apps.clientman.commands.download_usage_rights import clear_right_for_item_product_defini" + "tion\n" + "from apps.clientman.commands.download_usage_rights import clear_right_for_item_product_defini" + "tion_platform\n" + "from apps.clientman.commands.download_usage_rights import clear_right_for_item_product_platfo" + "rm\n" + "from apps.clientman.commands.download_usage_rights import clear_right_for_territory_retail_mo" + "del\n" + "from apps.clientman.commands.download_usage_rights import clear_right_for_territory_retail_mo" + "del_definition_platform_provider # noqa\n" + "from apps.clientman.commands.download_usage_rights import create_download_usage_right\n" + "from apps.clientman.commands.download_usage_rights import delete_download_usage_right\n" + "from apps.clientman.commands.download_usage_rights import disable_download_for_item_product\n" + "from apps.clientman.commands.download_usage_rights import disable_download_for_item_product_d" + "efinition\n" + "from apps.clientman.commands.download_usage_rights import disable_download_for_item_product_d" + "efinition_platform\n" + "from apps.clientman.commands.download_usage_rights import disable_download_for_item_product_p" + "latform\n" + "from apps.clientman.commands.download_usage_rights import disable_download_for_territory_reta" + "il_model\n" + "from apps.clientman.commands.download_usage_rights import disable_download_for_territory_reta" + "il_model_definition_platform_provider # noqa\n" + "from apps.clientman.commands.download_usage_rights import get_download_rights_for_item\n" + "from apps.clientman.commands.download_usage_rights import get_right\n" + ) + assert ( + SortImports( + file_contents=test_input, + force_single_line=True, + line_length=140, + known_third_party=["django", "model_mommy"], + ).output + == test_input + ) def test_auto_detection() -> None: """Initial test to ensure isort auto-detection works correctly - will grow over time as new issues are raised.""" # Issue 157 - test_input = ("import binascii\n" - "import os\n" - "\n" - "import cv2\n" - "import requests\n") - assert SortImports(file_contents=test_input, known_third_party=["cv2", "requests"]).output == test_input + test_input = ( + "import binascii\n" "import os\n" "\n" "import cv2\n" "import requests\n" + ) + assert ( + SortImports( + file_contents=test_input, known_third_party=["cv2", "requests"] + ).output + == test_input + ) # alternative solution - assert SortImports(file_contents=test_input, default_section="THIRDPARTY").output == test_input + assert ( + SortImports(file_contents=test_input, default_section="THIRDPARTY").output + == test_input + ) def test_same_line_statements() -> None: """Ensure isort correctly handles the case where a single line contains multiple statements including an import""" - test_input = ("import pdb; import nose\n") - assert SortImports(file_contents=test_input).output == ("import pdb\n" - "\n" - "import nose\n") + test_input = "import pdb; import nose\n" + assert SortImports(file_contents=test_input).output == ( + "import pdb\n" "\n" "import nose\n" + ) - test_input = ("import pdb; pdb.set_trace()\n" - "import nose; nose.run()\n") + test_input = "import pdb; pdb.set_trace()\n" "import nose; nose.run()\n" assert SortImports(file_contents=test_input).output == test_input def test_long_line_comments() -> None: """Ensure isort correctly handles comments at the end of extremely long lines""" - test_input = ("from foo.utils.fabric_stuff.live import check_clean_live, deploy_live, sync_live_envdir, " - "update_live_app, update_live_cron # noqa\n" - "from foo.utils.fabric_stuff.stage import check_clean_stage, deploy_stage, sync_stage_envdir, " - "update_stage_app, update_stage_cron # noqa\n") - assert SortImports(file_contents=test_input).output == \ - ("from foo.utils.fabric_stuff.live import (check_clean_live, deploy_live, # noqa\n" - " sync_live_envdir, update_live_app, update_live_cron)\n" - "from foo.utils.fabric_stuff.stage import (check_clean_stage, deploy_stage, # noqa\n" - " sync_stage_envdir, update_stage_app, update_stage_cron)\n") + test_input = ( + "from foo.utils.fabric_stuff.live import check_clean_live, deploy_live, sync_live_envdir, " + "update_live_app, update_live_cron # noqa\n" + "from foo.utils.fabric_stuff.stage import check_clean_stage, deploy_stage, sync_stage_envdir, " + "update_stage_app, update_stage_cron # noqa\n" + ) + assert SortImports(file_contents=test_input).output == ( + "from foo.utils.fabric_stuff.live import (check_clean_live, deploy_live, # noqa\n" + " sync_live_envdir, update_live_app, update_live_cron)\n" + "from foo.utils.fabric_stuff.stage import (check_clean_stage, deploy_stage, # noqa\n" + " sync_stage_envdir, update_stage_app, update_stage_cron)\n" + ) def test_tab_character_in_import() -> None: """Ensure isort correctly handles import statements that contain a tab character""" - test_input = ("from __future__ import print_function\n" - "from __future__ import\tprint_function\n") - assert SortImports(file_contents=test_input).output == "from __future__ import print_function\n" + test_input = ( + "from __future__ import print_function\n" + "from __future__ import\tprint_function\n" + ) + assert ( + SortImports(file_contents=test_input).output + == "from __future__ import print_function\n" + ) def test_split_position() -> None: """Ensure isort splits on import instead of . when possible""" - test_input = ("from p24.shared.exceptions.master.host_state_flag_unchanged import HostStateUnchangedException\n") - assert SortImports(file_contents=test_input, line_length=80).output == \ - ("from p24.shared.exceptions.master.host_state_flag_unchanged import \\\n" - " HostStateUnchangedException\n") + test_input = "from p24.shared.exceptions.master.host_state_flag_unchanged import HostStateUnchangedException\n" + assert SortImports(file_contents=test_input, line_length=80).output == ( + "from p24.shared.exceptions.master.host_state_flag_unchanged import \\\n" + " HostStateUnchangedException\n" + ) def test_place_comments() -> None: """Ensure manually placing imports works as expected""" - test_input = ("import sys\n" - "import os\n" - "import myproject.test\n" - "import django.settings\n" - "\n" - "# isort:imports-thirdparty\n" - "# isort:imports-firstparty\n" - "print('code')\n" - "\n" - "# isort:imports-stdlib\n") - expected_output = ("\n# isort:imports-thirdparty\n" - "import django.settings\n" - "\n" - "# isort:imports-firstparty\n" - "import myproject.test\n" - "\n" - "print('code')\n" - "\n" - "# isort:imports-stdlib\n" - "import os\n" - "import sys\n") - test_output = SortImports(file_contents=test_input, known_third_party=['django']).output + test_input = ( + "import sys\n" + "import os\n" + "import myproject.test\n" + "import django.settings\n" + "\n" + "# isort:imports-thirdparty\n" + "# isort:imports-firstparty\n" + "print('code')\n" + "\n" + "# isort:imports-stdlib\n" + ) + expected_output = ( + "\n# isort:imports-thirdparty\n" + "import django.settings\n" + "\n" + "# isort:imports-firstparty\n" + "import myproject.test\n" + "\n" + "print('code')\n" + "\n" + "# isort:imports-stdlib\n" + "import os\n" + "import sys\n" + ) + test_output = SortImports( + file_contents=test_input, known_third_party=["django"] + ).output assert test_output == expected_output - test_output = SortImports(file_contents=test_output, known_third_party=['django']).output + test_output = SortImports( + file_contents=test_output, known_third_party=["django"] + ).output assert test_output == expected_output def test_placement_control() -> None: """Ensure that most specific placement control match wins""" - test_input = ("import os\n" - "import sys\n" - "from bottle import Bottle, redirect, response, run\n" - "import p24.imports._argparse as argparse\n" - "import p24.imports._subprocess as subprocess\n" - "import p24.imports._VERSION as VERSION\n" - "import p24.shared.media_wiki_syntax as syntax\n") - test_output = SortImports(file_contents=test_input, - known_first_party=['p24', 'p24.imports._VERSION'], - known_standard_library=['p24.imports'], - known_third_party=['bottle'], - default_section="THIRDPARTY").output - - assert test_output == ("import os\n" - "import p24.imports._argparse as argparse\n" - "import p24.imports._subprocess as subprocess\n" - "import sys\n" - "\n" - "from bottle import Bottle, redirect, response, run\n" - "\n" - "import p24.imports._VERSION as VERSION\n" - "import p24.shared.media_wiki_syntax as syntax\n") + test_input = ( + "import os\n" + "import sys\n" + "from bottle import Bottle, redirect, response, run\n" + "import p24.imports._argparse as argparse\n" + "import p24.imports._subprocess as subprocess\n" + "import p24.imports._VERSION as VERSION\n" + "import p24.shared.media_wiki_syntax as syntax\n" + ) + test_output = SortImports( + file_contents=test_input, + known_first_party=["p24", "p24.imports._VERSION"], + known_standard_library=["p24.imports"], + known_third_party=["bottle"], + default_section="THIRDPARTY", + ).output + + assert test_output == ( + "import os\n" + "import p24.imports._argparse as argparse\n" + "import p24.imports._subprocess as subprocess\n" + "import sys\n" + "\n" + "from bottle import Bottle, redirect, response, run\n" + "\n" + "import p24.imports._VERSION as VERSION\n" + "import p24.shared.media_wiki_syntax as syntax\n" + ) def test_custom_sections() -> None: """Ensure that most specific placement control match wins""" - test_input = ("import os\n" - "import sys\n" - "from django.conf import settings\n" - "from bottle import Bottle, redirect, response, run\n" - "import p24.imports._argparse as argparse\n" - "from django.db import models\n" - "import p24.imports._subprocess as subprocess\n" - "import pandas as pd\n" - "import p24.imports._VERSION as VERSION\n" - "import numpy as np\n" - "import p24.shared.media_wiki_syntax as syntax\n") - test_output = SortImports(file_contents=test_input, - known_first_party=['p24', 'p24.imports._VERSION'], - import_heading_stdlib='Standard Library', - import_heading_thirdparty='Third Party', - import_heading_firstparty='First Party', - import_heading_django='Django', - import_heading_pandas='Pandas', - known_standard_library=['p24.imports'], - known_third_party=['bottle'], - known_django=['django'], - known_pandas=['pandas', 'numpy'], - default_section="THIRDPARTY", - sections=["FUTURE", "STDLIB", "DJANGO", "THIRDPARTY", "PANDAS", "FIRSTPARTY", "LOCALFOLDER"]).output - assert test_output == ("# Standard Library\n" - "import os\n" - "import p24.imports._argparse as argparse\n" - "import p24.imports._subprocess as subprocess\n" - "import sys\n" - "\n" - "# Django\n" - "from django.conf import settings\n" - "from django.db import models\n" - "\n" - "# Third Party\n" - "from bottle import Bottle, redirect, response, run\n" - "\n" - "# Pandas\n" - "import numpy as np\n" - "import pandas as pd\n" - "\n" - "# First Party\n" - "import p24.imports._VERSION as VERSION\n" - "import p24.shared.media_wiki_syntax as syntax\n") + test_input = ( + "import os\n" + "import sys\n" + "from django.conf import settings\n" + "from bottle import Bottle, redirect, response, run\n" + "import p24.imports._argparse as argparse\n" + "from django.db import models\n" + "import p24.imports._subprocess as subprocess\n" + "import pandas as pd\n" + "import p24.imports._VERSION as VERSION\n" + "import numpy as np\n" + "import p24.shared.media_wiki_syntax as syntax\n" + ) + test_output = SortImports( + file_contents=test_input, + known_first_party=["p24", "p24.imports._VERSION"], + import_heading_stdlib="Standard Library", + import_heading_thirdparty="Third Party", + import_heading_firstparty="First Party", + import_heading_django="Django", + import_heading_pandas="Pandas", + known_standard_library=["p24.imports"], + known_third_party=["bottle"], + known_django=["django"], + known_pandas=["pandas", "numpy"], + default_section="THIRDPARTY", + sections=[ + "FUTURE", + "STDLIB", + "DJANGO", + "THIRDPARTY", + "PANDAS", + "FIRSTPARTY", + "LOCALFOLDER", + ], + ).output + assert test_output == ( + "# Standard Library\n" + "import os\n" + "import p24.imports._argparse as argparse\n" + "import p24.imports._subprocess as subprocess\n" + "import sys\n" + "\n" + "# Django\n" + "from django.conf import settings\n" + "from django.db import models\n" + "\n" + "# Third Party\n" + "from bottle import Bottle, redirect, response, run\n" + "\n" + "# Pandas\n" + "import numpy as np\n" + "import pandas as pd\n" + "\n" + "# First Party\n" + "import p24.imports._VERSION as VERSION\n" + "import p24.shared.media_wiki_syntax as syntax\n" + ) def test_glob_known() -> None: """Ensure that most specific placement control match wins""" - test_input = ("import os\n" - "from django_whatever import whatever\n" - "import sys\n" - "from django.conf import settings\n" - "from . import another\n") - test_output = SortImports(file_contents=test_input, - import_heading_stdlib='Standard Library', - import_heading_thirdparty='Third Party', - import_heading_firstparty='First Party', - import_heading_django='Django', - import_heading_djangoplugins='Django Plugins', - import_heading_localfolder='Local', - known_django=['django'], - known_djangoplugins=['django_*'], - default_section="THIRDPARTY", - sections=["FUTURE", "STDLIB", "DJANGO", "DJANGOPLUGINS", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"]).output - assert test_output == ("# Standard Library\n" - "import os\n" - "import sys\n" - "\n" - "# Django\n" - "from django.conf import settings\n" - "\n" - "# Django Plugins\n" - "from django_whatever import whatever\n" - "\n" - "# Local\n" - "from . import another\n") + test_input = ( + "import os\n" + "from django_whatever import whatever\n" + "import sys\n" + "from django.conf import settings\n" + "from . import another\n" + ) + test_output = SortImports( + file_contents=test_input, + import_heading_stdlib="Standard Library", + import_heading_thirdparty="Third Party", + import_heading_firstparty="First Party", + import_heading_django="Django", + import_heading_djangoplugins="Django Plugins", + import_heading_localfolder="Local", + known_django=["django"], + known_djangoplugins=["django_*"], + default_section="THIRDPARTY", + sections=[ + "FUTURE", + "STDLIB", + "DJANGO", + "DJANGOPLUGINS", + "THIRDPARTY", + "FIRSTPARTY", + "LOCALFOLDER", + ], + ).output + assert test_output == ( + "# Standard Library\n" + "import os\n" + "import sys\n" + "\n" + "# Django\n" + "from django.conf import settings\n" + "\n" + "# Django Plugins\n" + "from django_whatever import whatever\n" + "\n" + "# Local\n" + "from . import another\n" + ) def test_sticky_comments() -> None: """Test to ensure it is possible to make comments 'stick' above imports""" - test_input = ("import os\n" - "\n" - "# Used for type-hinting (ref: https://github.com/davidhalter/jedi/issues/414).\n" - "from selenium.webdriver.remote.webdriver import WebDriver # noqa\n") + test_input = ( + "import os\n" + "\n" + "# Used for type-hinting (ref: https://github.com/davidhalter/jedi/issues/414).\n" + "from selenium.webdriver.remote.webdriver import WebDriver # noqa\n" + ) assert SortImports(file_contents=test_input).output == test_input - test_input = ("from django import forms\n" - "# While this couples the geographic forms to the GEOS library,\n" - "# it decouples from database (by not importing SpatialBackend).\n" - "from django.contrib.gis.geos import GEOSException, GEOSGeometry\n" - "from django.utils.translation import ugettext_lazy as _\n") + test_input = ( + "from django import forms\n" + "# While this couples the geographic forms to the GEOS library,\n" + "# it decouples from database (by not importing SpatialBackend).\n" + "from django.contrib.gis.geos import GEOSException, GEOSGeometry\n" + "from django.utils.translation import ugettext_lazy as _\n" + ) assert SortImports(file_contents=test_input).output == test_input @@ -1606,178 +1902,210 @@ def test_from_first() -> None: def test_top_comments() -> None: """Ensure correct behavior with top comments""" - test_input = ("# -*- encoding: utf-8 -*-\n" - "# Test comment\n" - "#\n" - "from __future__ import unicode_literals\n") + test_input = ( + "# -*- encoding: utf-8 -*-\n" + "# Test comment\n" + "#\n" + "from __future__ import unicode_literals\n" + ) assert SortImports(file_contents=test_input).output == test_input - test_input = ("# -*- coding: utf-8 -*-\n" - "from django.db import models\n" - "from django.utils.encoding import python_2_unicode_compatible\n") + test_input = ( + "# -*- coding: utf-8 -*-\n" + "from django.db import models\n" + "from django.utils.encoding import python_2_unicode_compatible\n" + ) assert SortImports(file_contents=test_input).output == test_input - test_input = ("# Comment\n" - "import sys\n") + test_input = "# Comment\n" "import sys\n" assert SortImports(file_contents=test_input).output == test_input - test_input = ("# -*- coding\n" - "import sys\n") + test_input = "# -*- coding\n" "import sys\n" assert SortImports(file_contents=test_input).output == test_input def test_consistency() -> None: """Ensures consistency of handling even when dealing with non ordered-by-type imports""" test_input = "from sqlalchemy.dialects.postgresql import ARRAY, array\n" - assert SortImports(file_contents=test_input, order_by_type=True).output == test_input + assert ( + SortImports(file_contents=test_input, order_by_type=True).output == test_input + ) def test_force_grid_wrap() -> None: """Ensures removing imports works as expected.""" - test_input = ( - "from bar import lib2\n" - "from foo import lib6, lib7\n" - ) + test_input = "from bar import lib2\n" "from foo import lib6, lib7\n" test_output = SortImports( - file_contents=test_input, - force_grid_wrap=2, - multi_line_output=WrapModes.VERTICAL_HANGING_INDENT - ).output - assert test_output == """from bar import lib2 + file_contents=test_input, + force_grid_wrap=2, + multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, + ).output + assert ( + test_output + == """from bar import lib2 from foo import ( lib6, lib7 ) """ + ) test_output = SortImports( - file_contents=test_input, - force_grid_wrap=3, - multi_line_output=WrapModes.VERTICAL_HANGING_INDENT - ).output + file_contents=test_input, + force_grid_wrap=3, + multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, + ).output assert test_output == test_input def test_force_grid_wrap_long() -> None: """Ensure that force grid wrap still happens with long line length""" test_input = ( - "from foo import lib6, lib7\n" - "from bar import lib2\n" - "from babar import something_that_is_kind_of_long" + "from foo import lib6, lib7\n" + "from bar import lib2\n" + "from babar import something_that_is_kind_of_long" ) test_output = SortImports( - file_contents=test_input, - force_grid_wrap=2, - multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, - line_length=9999, - ).output - assert test_output == """from babar import something_that_is_kind_of_long + file_contents=test_input, + force_grid_wrap=2, + multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, + line_length=9999, + ).output + assert ( + test_output + == """from babar import something_that_is_kind_of_long from bar import lib2 from foo import ( lib6, lib7 ) """ + ) def test_uses_jinja_variables() -> None: """Test a basic set of imports that use jinja variables""" - test_input = ("import sys\n" - "import os\n" - "import myproject.{ test }\n" - "import django.{ settings }") - test_output = SortImports(file_contents=test_input, known_third_party=['django'], - known_first_party=['myproject']).output - assert test_output == ("import os\n" - "import sys\n" - "\n" - "import django.{ settings }\n" - "\n" - "import myproject.{ test }\n") - - test_input = ("import {{ cookiecutter.repo_name }}\n" - "from foo import {{ cookiecutter.bar }}\n") + test_input = ( + "import sys\n" + "import os\n" + "import myproject.{ test }\n" + "import django.{ settings }" + ) + test_output = SortImports( + file_contents=test_input, + known_third_party=["django"], + known_first_party=["myproject"], + ).output + assert test_output == ( + "import os\n" + "import sys\n" + "\n" + "import django.{ settings }\n" + "\n" + "import myproject.{ test }\n" + ) + + test_input = ( + "import {{ cookiecutter.repo_name }}\n" + "from foo import {{ cookiecutter.bar }}\n" + ) assert SortImports(file_contents=test_input).output == test_input def test_fcntl() -> None: """Test to ensure fcntl gets correctly recognized as stdlib import""" - test_input = ("import fcntl\n" - "import os\n" - "import sys\n") + test_input = "import fcntl\n" "import os\n" "import sys\n" assert SortImports(file_contents=test_input).output == test_input def test_import_split_is_word_boundary_aware() -> None: """Test to ensure that isort splits words in a boundary aware manner""" - 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" - ")\n") + 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" + ")\n" + ) def test_other_file_encodings(tmpdir): """Test to ensure file encoding is respected""" - for encoding in ('latin1', 'utf8'): - tmp_fname = tmpdir.join('test_{0}.py'.format(encoding)) + for encoding in ("latin1", "utf8"): + tmp_fname = tmpdir.join("test_{0}.py".format(encoding)) file_contents = "# coding: {0}\n\ns = u'ã'\n".format(encoding) tmp_fname.write_binary(file_contents.encode(encoding)) - assert SortImports(file_path=str(tmp_fname), settings_path=os.getcwd()).output == file_contents + assert ( + SortImports(file_path=str(tmp_fname), settings_path=os.getcwd()).output + == file_contents + ) def test_comment_at_top_of_file() -> None: """Test to ensure isort correctly handles top of file comments""" - test_input = ("# Comment one\n" - "from django import forms\n" - "# Comment two\n" - "from django.contrib.gis.geos import GEOSException\n") + test_input = ( + "# Comment one\n" + "from django import forms\n" + "# Comment two\n" + "from django.contrib.gis.geos import GEOSException\n" + ) assert SortImports(file_contents=test_input).output == test_input - test_input = ("# -*- coding: utf-8 -*-\n" - "from django.db import models\n") + test_input = "# -*- coding: utf-8 -*-\n" "from django.db import models\n" assert SortImports(file_contents=test_input).output == test_input def test_alphabetic_sorting() -> None: """Test to ensure isort correctly handles single line imports""" - test_input = ("import unittest\n" - "\n" - "import ABC\n" - "import Zope\n" - "from django.contrib.gis.geos import GEOSException\n" - "from plone.app.testing import getRoles\n" - "from plone.app.testing import ManageRoles\n" - "from plone.app.testing import setRoles\n" - "from Products.CMFPlone import utils\n" - ) - options = {'force_single_line': True, - 'force_alphabetical_sort_within_sections': True} # type: Dict[str, Any] - - output = SortImports(file_contents=test_input, known_first_party=['django'], **options).output + test_input = ( + "import unittest\n" + "\n" + "import ABC\n" + "import Zope\n" + "from django.contrib.gis.geos import GEOSException\n" + "from plone.app.testing import getRoles\n" + "from plone.app.testing import ManageRoles\n" + "from plone.app.testing import setRoles\n" + "from Products.CMFPlone import utils\n" + ) + options = { + "force_single_line": True, + "force_alphabetical_sort_within_sections": True, + } # type: Dict[str, Any] + + output = SortImports( + file_contents=test_input, known_first_party=["django"], **options + ).output assert output == test_input - test_input = ("# -*- coding: utf-8 -*-\n" - "from django.db import models\n") + test_input = "# -*- coding: utf-8 -*-\n" "from django.db import models\n" assert SortImports(file_contents=test_input).output == test_input def test_alphabetic_sorting_multi_line() -> None: """Test to ensure isort correctly handles multiline import see: issue 364""" - test_input = ("from a import (CONSTANT_A, cONSTANT_B, CONSTANT_C, CONSTANT_D, CONSTANT_E,\n" - " CONSTANT_F, CONSTANT_G, CONSTANT_H, CONSTANT_I, CONSTANT_J)\n") - options = {'force_alphabetical_sort_within_sections': True} # type: Dict[str, Any] + test_input = ( + "from a import (CONSTANT_A, cONSTANT_B, CONSTANT_C, CONSTANT_D, CONSTANT_E,\n" + " CONSTANT_F, CONSTANT_G, CONSTANT_H, CONSTANT_I, CONSTANT_J)\n" + ) + options = {"force_alphabetical_sort_within_sections": True} # type: Dict[str, Any] assert SortImports(file_contents=test_input, **options).output == test_input def test_comments_not_duplicated() -> None: """Test to ensure comments aren't duplicated: issue 303""" - test_input = ('from flask import url_for\n' - "# Whole line comment\n" - 'from service import demo # inline comment\n' - 'from service import settings\n') + test_input = ( + "from flask import url_for\n" + "# Whole line comment\n" + "from service import demo # inline comment\n" + "from service import settings\n" + ) output = SortImports(file_contents=test_input).output assert output.count("# Whole line comment\n") == 1 assert output.count("# inline comment\n") == 1 @@ -1785,255 +2113,296 @@ def test_comments_not_duplicated() -> None: def test_top_of_line_comments() -> None: """Test to ensure top of line comments stay where they should: issue 260""" - test_input = ('# -*- coding: utf-8 -*-\n' - 'from django.db import models\n' - '#import json as simplejson\n' - 'from myproject.models import Servidor\n' - '\n' - 'import reversion\n' - '\n' - 'import logging\n') + test_input = ( + "# -*- coding: utf-8 -*-\n" + "from django.db import models\n" + "#import json as simplejson\n" + "from myproject.models import Servidor\n" + "\n" + "import reversion\n" + "\n" + "import logging\n" + ) output = SortImports(file_contents=test_input).output print(output) - assert output.startswith('# -*- coding: utf-8 -*-\n') + assert output.startswith("# -*- coding: utf-8 -*-\n") def test_basic_comment() -> None: """Test to ensure a basic comment wont crash isort""" - test_input = ('import logging\n' - '# Foo\n' - 'import os\n') + test_input = "import logging\n" "# Foo\n" "import os\n" assert SortImports(file_contents=test_input).output == test_input def test_shouldnt_add_lines() -> None: """Ensure that isort doesn't add a blank line when a top of import comment is present, issue #316""" - test_input = ('"""Text"""\n' - '# This is a comment\n' - 'import pkg_resources\n') + test_input = '"""Text"""\n' "# This is a comment\n" "import pkg_resources\n" assert SortImports(file_contents=test_input).output == test_input def test_sections_parsed_correct(tmpdir): """Ensure that modules for custom sections parsed as list from config file and isort result is correct""" conf_file_data = ( - '[settings]\n' - 'sections=FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER,COMMON\n' - 'known_common=nose\n' - 'import_heading_common=Common Library\n' - 'import_heading_stdlib=Standard Library\n' + "[settings]\n" + "sections=FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER,COMMON\n" + "known_common=nose\n" + "import_heading_common=Common Library\n" + "import_heading_stdlib=Standard Library\n" ) test_input = ( - 'import os\n' - 'from nose import *\n' - 'import nose\n' - 'from os import path' + "import os\n" "from nose import *\n" "import nose\n" "from os import path" ) correct_output = ( - '# Standard Library\n' - 'import os\n' - 'from os import path\n' - '\n' - '# Common Library\n' - 'import nose\n' - 'from nose import *\n' + "# Standard Library\n" + "import os\n" + "from os import path\n" + "\n" + "# Common Library\n" + "import nose\n" + "from nose import *\n" + ) + tmpdir.join(".isort.cfg").write(conf_file_data) + assert ( + SortImports(file_contents=test_input, settings_path=str(tmpdir)).output + == correct_output ) - tmpdir.join('.isort.cfg').write(conf_file_data) - assert SortImports(file_contents=test_input, settings_path=str(tmpdir)).output == correct_output @pytest.mark.skipif(toml is None, reason="Requires toml package to be installed.") def test_pyproject_conf_file(tmpdir): """Ensure that modules for custom sections parsed as list from config file and isort result is correct""" conf_file_data = ( - '[build-system]\n' + "[build-system]\n" 'requires = ["setuptools", "wheel"]\n' - '[tool.poetry]\n' + "[tool.poetry]\n" 'name = "isort"\n' 'version = "0.1.0"\n' 'license = "MIT"\n' - '[tool.isort]\n' - 'lines_between_types=1\n' + "[tool.isort]\n" + "lines_between_types=1\n" 'known_common="nose"\n' 'import_heading_common="Common Library"\n' 'import_heading_stdlib="Standard Library"\n' 'sections="FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER,COMMON"\n' - 'include_trailing_comma = true\n' + "include_trailing_comma = true\n" ) test_input = ( - 'import os\n' - 'from nose import *\n' - 'import nose\n' - 'from os import path' + "import os\n" "from nose import *\n" "import nose\n" "from os import path" ) correct_output = ( - '# Standard Library\n' - 'import os\n' - '\n' - 'from os import path\n' - '\n' - '# Common Library\n' - 'import nose\n' - '\n' - 'from nose import *\n' + "# Standard Library\n" + "import os\n" + "\n" + "from os import path\n" + "\n" + "# Common Library\n" + "import nose\n" + "\n" + "from nose import *\n" + ) + tmpdir.join("pyproject.toml").write(conf_file_data) + assert ( + SortImports(file_contents=test_input, settings_path=str(tmpdir)).output + == correct_output ) - tmpdir.join('pyproject.toml').write(conf_file_data) - assert SortImports(file_contents=test_input, settings_path=str(tmpdir)).output == correct_output def test_alphabetic_sorting_no_newlines() -> None: - '''Test to ensure that alphabetical sort does not erroneously introduce new lines (issue #328)''' + """Test to ensure that alphabetical sort does not erroneously introduce new lines (issue #328)""" test_input = "import os\n" - test_output = SortImports(file_contents=test_input, force_alphabetical_sort_within_sections=True).output + test_output = SortImports( + file_contents=test_input, force_alphabetical_sort_within_sections=True + ).output assert test_input == test_output - test_input = ('import os\n' - 'import unittest\n' - '\n' - 'from a import b\n' - '\n' - '\n' - 'print(1)\n') - test_output = SortImports(file_contents=test_input, force_alphabetical_sort_within_sections=True, lines_after_imports=2).output + test_input = ( + "import os\n" + "import unittest\n" + "\n" + "from a import b\n" + "\n" + "\n" + "print(1)\n" + ) + test_output = SortImports( + file_contents=test_input, + force_alphabetical_sort_within_sections=True, + lines_after_imports=2, + ).output assert test_input == test_output def test_sort_within_section() -> None: - '''Test to ensure its possible to force isort to sort within sections''' - test_input = ('from Foob import ar\n' - 'import foo\n' - 'from foo import bar\n' - 'from foo.bar import Quux, baz\n') - test_output = SortImports(file_contents=test_input, force_sort_within_sections=True).output + """Test to ensure its possible to force isort to sort within sections""" + test_input = ( + "from Foob import ar\n" + "import foo\n" + "from foo import bar\n" + "from foo.bar import Quux, baz\n" + ) + test_output = SortImports( + file_contents=test_input, force_sort_within_sections=True + ).output assert test_output == test_input - test_input = ('import foo\n' - 'from foo import bar\n' - 'from foo.bar import baz\n' - 'from foo.bar import Quux\n' - 'from Foob import ar\n') - test_output = SortImports(file_contents=test_input, force_sort_within_sections=True, order_by_type=False, - force_single_line=True).output + test_input = ( + "import foo\n" + "from foo import bar\n" + "from foo.bar import baz\n" + "from foo.bar import Quux\n" + "from Foob import ar\n" + ) + test_output = SortImports( + file_contents=test_input, + force_sort_within_sections=True, + order_by_type=False, + force_single_line=True, + ).output assert test_output == test_input def test_sorting_with_two_top_comments() -> None: - '''Test to ensure isort will sort files that contain 2 top comments''' - test_input = ('#! comment1\n' - "''' comment2\n" - "'''\n" - 'import b\n' - 'import a\n') - assert SortImports(file_contents=test_input).output == ('#! comment1\n' - "''' comment2\n" - "'''\n" - 'import a\n' - 'import b\n') + """Test to ensure isort will sort files that contain 2 top comments""" + test_input = "#! comment1\n" "''' comment2\n" "'''\n" "import b\n" "import a\n" + assert SortImports(file_contents=test_input).output == ( + "#! comment1\n" "''' comment2\n" "'''\n" "import a\n" "import b\n" + ) def test_lines_between_sections() -> None: """Test to ensure lines_between_sections works""" - test_input = ('from bar import baz\n' - 'import os\n') - assert SortImports(file_contents=test_input, lines_between_sections=0).output == ('import os\n' - 'from bar import baz\n') - assert SortImports(file_contents=test_input, lines_between_sections=2).output == ('import os\n\n\n' - 'from bar import baz\n') + test_input = "from bar import baz\n" "import os\n" + assert SortImports(file_contents=test_input, lines_between_sections=0).output == ( + "import os\n" "from bar import baz\n" + ) + assert SortImports(file_contents=test_input, lines_between_sections=2).output == ( + "import os\n\n\n" "from bar import baz\n" + ) def test_forced_sepatate_globs() -> None: """Test to ensure that forced_separate glob matches lines""" - test_input = ('import os\n' - '\n' - 'from myproject.foo.models import Foo\n' - '\n' - 'from myproject.utils import util_method\n' - '\n' - 'from myproject.bar.models import Bar\n' - '\n' - 'import sys\n') - test_output = SortImports(file_contents=test_input, forced_separate=['*.models'], - line_length=120).output - - assert test_output == ('import os\n' - 'import sys\n' - '\n' - 'from myproject.utils import util_method\n' - '\n' - 'from myproject.bar.models import Bar\n' - 'from myproject.foo.models import Foo\n') + test_input = ( + "import os\n" + "\n" + "from myproject.foo.models import Foo\n" + "\n" + "from myproject.utils import util_method\n" + "\n" + "from myproject.bar.models import Bar\n" + "\n" + "import sys\n" + ) + test_output = SortImports( + file_contents=test_input, forced_separate=["*.models"], line_length=120 + ).output + + assert test_output == ( + "import os\n" + "import sys\n" + "\n" + "from myproject.utils import util_method\n" + "\n" + "from myproject.bar.models import Bar\n" + "from myproject.foo.models import Foo\n" + ) def test_no_additional_lines_issue_358() -> None: """Test to ensure issue 358 is resolved and running isort multiple times does not add extra newlines""" - test_input = ('"""This is a docstring"""\n' - '# This is a comment\n' - 'from __future__ import (\n' - ' absolute_import,\n' - ' division,\n' - ' print_function,\n' - ' unicode_literals\n' - ')\n') - expected_output = ('"""This is a docstring"""\n' - '# This is a comment\n' - 'from __future__ import (\n' - ' absolute_import,\n' - ' division,\n' - ' print_function,\n' - ' unicode_literals\n' - ')\n') - test_output = SortImports(file_contents=test_input, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, - line_length=20).output + test_input = ( + '"""This is a docstring"""\n' + "# This is a comment\n" + "from __future__ import (\n" + " absolute_import,\n" + " division,\n" + " print_function,\n" + " unicode_literals\n" + ")\n" + ) + expected_output = ( + '"""This is a docstring"""\n' + "# This is a comment\n" + "from __future__ import (\n" + " absolute_import,\n" + " division,\n" + " print_function,\n" + " unicode_literals\n" + ")\n" + ) + test_output = SortImports( + file_contents=test_input, + multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, + line_length=20, + ).output assert test_output == expected_output - test_output = SortImports(file_contents=test_output, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, - line_length=20).output + test_output = SortImports( + file_contents=test_output, + multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, + line_length=20, + ).output assert test_output == expected_output for attempt in range(5): - test_output = SortImports(file_contents=test_output, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, - line_length=20).output + test_output = SortImports( + file_contents=test_output, + multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, + line_length=20, + ).output assert test_output == expected_output - test_input = ('"""This is a docstring"""\n' - '\n' - '# This is a comment\n' - 'from __future__ import (\n' - ' absolute_import,\n' - ' division,\n' - ' print_function,\n' - ' unicode_literals\n' - ')\n') - expected_output = ('"""This is a docstring"""\n' - '\n' - '# This is a comment\n' - 'from __future__ import (\n' - ' absolute_import,\n' - ' division,\n' - ' print_function,\n' - ' unicode_literals\n' - ')\n') - test_output = SortImports(file_contents=test_input, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, - line_length=20).output + test_input = ( + '"""This is a docstring"""\n' + "\n" + "# This is a comment\n" + "from __future__ import (\n" + " absolute_import,\n" + " division,\n" + " print_function,\n" + " unicode_literals\n" + ")\n" + ) + expected_output = ( + '"""This is a docstring"""\n' + "\n" + "# This is a comment\n" + "from __future__ import (\n" + " absolute_import,\n" + " division,\n" + " print_function,\n" + " unicode_literals\n" + ")\n" + ) + test_output = SortImports( + file_contents=test_input, + multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, + line_length=20, + ).output assert test_output == expected_output - test_output = SortImports(file_contents=test_output, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, - line_length=20).output + test_output = SortImports( + file_contents=test_output, + multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, + line_length=20, + ).output assert test_output == expected_output for attempt in range(5): - test_output = SortImports(file_contents=test_output, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, - line_length=20).output + test_output = SortImports( + file_contents=test_output, + multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, + line_length=20, + ).output assert test_output == expected_output def test_import_by_paren_issue_375() -> None: """Test to ensure isort can correctly handle sorting imports where the paren is directly by the import body""" - test_input = ('from .models import(\n' - ' Foo,\n' - ' Bar,\n' - ')\n') - assert SortImports(file_contents=test_input).output == 'from .models import Bar, Foo\n' + test_input = "from .models import(\n" " Foo,\n" " Bar,\n" ")\n" + assert ( + SortImports(file_contents=test_input).output == "from .models import Bar, Foo\n" + ) def test_import_by_paren_issue_460() -> None: @@ -2050,67 +2419,71 @@ def test_import_by_paren_issue_460() -> None: def test_function_with_docstring() -> None: """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'] - test_input = ('def foo():\n' - ' """ Single line triple quoted doctring """\n' - ' pass\n') - expected_output = ('from __future__ import unicode_literals\n' - '\n' - '\n' - 'def foo():\n' - ' """ Single line triple quoted doctring """\n' - ' pass\n') - assert SortImports(file_contents=test_input, add_imports=add_imports).output == expected_output + add_imports = ["from __future__ import unicode_literals"] + test_input = ( + "def foo():\n" ' """ Single line triple quoted doctring """\n' " pass\n" + ) + expected_output = ( + "from __future__ import unicode_literals\n" + "\n" + "\n" + "def foo():\n" + ' """ Single line triple quoted doctring """\n' + " pass\n" + ) + assert ( + SortImports(file_contents=test_input, add_imports=add_imports).output + == expected_output + ) def test_plone_style() -> None: """Test to ensure isort correctly plone style imports""" - test_input = ("from django.contrib.gis.geos import GEOSException\n" - "from plone.app.testing import getRoles\n" - "from plone.app.testing import ManageRoles\n" - "from plone.app.testing import setRoles\n" - "from Products.CMFPlone import utils\n" - "\n" - "import ABC\n" - "import unittest\n" - "import Zope\n") - options = {'force_single_line': True, - 'force_alphabetical_sort': True} # type: Dict[str, Any] + test_input = ( + "from django.contrib.gis.geos import GEOSException\n" + "from plone.app.testing import getRoles\n" + "from plone.app.testing import ManageRoles\n" + "from plone.app.testing import setRoles\n" + "from Products.CMFPlone import utils\n" + "\n" + "import ABC\n" + "import unittest\n" + "import Zope\n" + ) + options = { + "force_single_line": True, + "force_alphabetical_sort": True, + } # type: Dict[str, Any] assert SortImports(file_contents=test_input, **options).output == test_input def test_third_party_case_sensitive() -> None: """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') + 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'))) + 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'))) + tmpdir.join("pkg").ensure(dir=1) + assert exists_case_sensitive(str(tmpdir.join("pkg"))) + assert not exists_case_sensitive(str(tmpdir.join("PKG"))) def test_sys_path_mutation(tmpdir): """Test to ensure sys.path is not modified""" - tmpdir.mkdir('src').mkdir('a') + tmpdir.mkdir("src").mkdir("a") test_input = "from myproject import test" - options = {'virtual_env': str(tmpdir)} # type: Dict[str, Any] + options = {"virtual_env": str(tmpdir)} # type: Dict[str, Any] expected_length = len(sys.path) SortImports(file_contents=test_input, **options).output assert len(sys.path) == expected_length @@ -2118,529 +2491,663 @@ def test_sys_path_mutation(tmpdir): def test_long_single_line() -> None: """Test to ensure long single lines get handled correctly""" - output = SortImports(file_contents="from ..views import (" - " _a," - "_xxxxxx_xxxxxxx_xxxxxxxx_xxx_xxxxxxx as xxxxxx_xxxxxxx_xxxxxxxx_xxx_xxxxxxx)", - line_length=79).output - for line in output.split('\n'): + output = SortImports( + file_contents="from ..views import (" + " _a," + "_xxxxxx_xxxxxxx_xxxxxxxx_xxx_xxxxxxx as xxxxxx_xxxxxxx_xxxxxxxx_xxx_xxxxxxx)", + line_length=79, + ).output + for line in output.split("\n"): assert len(line) <= 79 - output = SortImports(file_contents="from ..views import (" - " _a," - "_xxxxxx_xxxxxxx_xxxxxxxx_xxx_xxxxxxx as xxxxxx_xxxxxxx_xxxxxxxx_xxx_xxxxxxx)", - line_length=76, combine_as_imports=True).output - for line in output.split('\n'): + output = SortImports( + file_contents="from ..views import (" + " _a," + "_xxxxxx_xxxxxxx_xxxxxxxx_xxx_xxxxxxx as xxxxxx_xxxxxxx_xxxxxxxx_xxx_xxxxxxx)", + line_length=76, + combine_as_imports=True, + ).output + for line in output.split("\n"): assert len(line) <= 79 def test_import_inside_class_issue_432() -> None: """Test to ensure issue 432 is resolved and isort doesn't insert imports in the middle of classes""" - test_input = ("# coding=utf-8\n" - "class Foo:\n" - " def bar(self):\n" - " pass\n") - expected_output = ("# coding=utf-8\n" - "import baz\n" - "\n" - "\n" - "class Foo:\n" - " def bar(self):\n" - " pass\n") - assert SortImports(file_contents=test_input, add_imports=['import baz']).output == expected_output + test_input = ( + "# coding=utf-8\n" "class Foo:\n" " def bar(self):\n" " pass\n" + ) + expected_output = ( + "# coding=utf-8\n" + "import baz\n" + "\n" + "\n" + "class Foo:\n" + " def bar(self):\n" + " pass\n" + ) + assert ( + SortImports(file_contents=test_input, add_imports=["import baz"]).output + == expected_output + ) def test_wildcard_import_without_space_issue_496() -> None: """Test to ensure issue #496: wildcard without space, is resolved""" - test_input = 'from findorserver.coupon.models import*' - expected_output = 'from findorserver.coupon.models import *\n' + test_input = "from findorserver.coupon.models import*" + expected_output = "from findorserver.coupon.models import *\n" assert SortImports(file_contents=test_input).output == expected_output def test_import_line_mangles_issues_491() -> None: """Test to ensure comment on import with parens doesn't cause issues""" - test_input = ('import os # ([\n' - '\n' - 'print("hi")\n') + test_input = "import os # ([\n" "\n" 'print("hi")\n' assert SortImports(file_contents=test_input).output == test_input def test_import_line_mangles_issues_505() -> None: """Test to ensure comment on import with parens doesn't cause issues""" - test_input = ('from sys import * # (\n' - '\n' - '\n' - 'def test():\n' - ' print("Test print")\n') + test_input = ( + "from sys import * # (\n" "\n" "\n" "def test():\n" ' print("Test print")\n' + ) assert SortImports(file_contents=test_input).output == test_input def test_import_line_mangles_issues_439() -> None: """Test to ensure comment on import with parens doesn't cause issues""" - test_input = ('import a # () import\n' - 'from b import b\n') + test_input = "import a # () import\n" "from b import b\n" assert SortImports(file_contents=test_input).output == test_input def test_alias_using_paren_issue_466() -> None: """Test to ensure issue #466: Alias causes slash incorrectly is resolved""" - test_input = 'from django.db.backends.mysql.base import DatabaseWrapper as MySQLDatabaseWrapper\n' - expected_output = ('from django.db.backends.mysql.base import (\n' - ' DatabaseWrapper as MySQLDatabaseWrapper)\n') - assert SortImports(file_contents=test_input, line_length=50, use_parentheses=True).output == expected_output + test_input = "from django.db.backends.mysql.base import DatabaseWrapper as MySQLDatabaseWrapper\n" + expected_output = ( + "from django.db.backends.mysql.base import (\n" + " DatabaseWrapper as MySQLDatabaseWrapper)\n" + ) + assert ( + SortImports( + file_contents=test_input, line_length=50, use_parentheses=True + ).output + == expected_output + ) - test_input = 'from django.db.backends.mysql.base import DatabaseWrapper as MySQLDatabaseWrapper\n' - expected_output = ('from django.db.backends.mysql.base import (\n' - ' DatabaseWrapper as MySQLDatabaseWrapper\n' - ')\n') - assert SortImports(file_contents=test_input, line_length=50, multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, - use_parentheses=True).output == expected_output + test_input = "from django.db.backends.mysql.base import DatabaseWrapper as MySQLDatabaseWrapper\n" + expected_output = ( + "from django.db.backends.mysql.base import (\n" + " DatabaseWrapper as MySQLDatabaseWrapper\n" + ")\n" + ) + assert ( + SortImports( + file_contents=test_input, + line_length=50, + multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, + use_parentheses=True, + ).output + == expected_output + ) def test_long_alias_using_paren_issue_957() -> None: - test_input = ('from package import module as very_very_very_very_very_very_very_very_very_very_long_alias\n') - expected_output = ('from package import (\n' - ' module as very_very_very_very_very_very_very_very_very_very_long_alias\n' - ')\n') - out = SortImports(file_contents=test_input, line_length=50, use_parentheses=True, multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, check=True).output + test_input = "from package import module as very_very_very_very_very_very_very_very_very_very_long_alias\n" + expected_output = ( + "from package import (\n" + " module as very_very_very_very_very_very_very_very_very_very_long_alias\n" + ")\n" + ) + out = SortImports( + file_contents=test_input, + line_length=50, + use_parentheses=True, + multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, + check=True, + ).output assert out == expected_output - test_input = ('from deep.deep.deep.deep.deep.deep.deep.deep.deep.package import module as very_very_very_very_very_very_very_very_very_very_long_alias\n') - expected_output = ('from deep.deep.deep.deep.deep.deep.deep.deep.deep.package import (\n' - ' module as very_very_very_very_very_very_very_very_very_very_long_alias\n' - ')\n') - out = SortImports(file_contents=test_input, line_length=50, use_parentheses=True, multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, check=True).output + test_input = "from deep.deep.deep.deep.deep.deep.deep.deep.deep.package import module as very_very_very_very_very_very_very_very_very_very_long_alias\n" + expected_output = ( + "from deep.deep.deep.deep.deep.deep.deep.deep.deep.package import (\n" + " module as very_very_very_very_very_very_very_very_very_very_long_alias\n" + ")\n" + ) + out = SortImports( + file_contents=test_input, + line_length=50, + use_parentheses=True, + multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, + check=True, + ).output assert out == expected_output test_input = ( - 'from deep.deep.deep.deep.deep.deep.deep.deep.deep.package ' - 'import very_very_very_very_very_very_very_very_very_very_long_module as very_very_very_very_very_very_very_very_very_very_long_alias\n' + "from deep.deep.deep.deep.deep.deep.deep.deep.deep.package " + "import very_very_very_very_very_very_very_very_very_very_long_module as very_very_very_very_very_very_very_very_very_very_long_alias\n" + ) + expected_output = ( + "from deep.deep.deep.deep.deep.deep.deep.deep.deep.package import (\n" + " very_very_very_very_very_very_very_very_very_very_long_module as very_very_very_very_very_very_very_very_very_very_long_alias\n" + ")\n" ) - expected_output = ('from deep.deep.deep.deep.deep.deep.deep.deep.deep.package import (\n' - ' very_very_very_very_very_very_very_very_very_very_long_module as very_very_very_very_very_very_very_very_very_very_long_alias\n' - ')\n') - out = SortImports(file_contents=test_input, line_length=50, use_parentheses=True, multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, check=True).output + out = SortImports( + file_contents=test_input, + line_length=50, + use_parentheses=True, + multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, + check=True, + ).output assert out == expected_output def test_strict_whitespace_by_default(capsys): - test_input = ('import os\n' - 'from django.conf import settings\n') + test_input = "import os\n" "from django.conf import settings\n" SortImports(file_contents=test_input, check=True) out, err = capsys.readouterr() - assert out == 'ERROR: Imports are incorrectly sorted.\n' + assert out == "ERROR: Imports are incorrectly sorted.\n" def test_strict_whitespace_no_closing_newline_issue_676(capsys): - test_input = ('import os\n' - '\n' - 'from django.conf import settings\n' - '\n' - 'print(1)') + test_input = "import os\n" "\n" "from django.conf import settings\n" "\n" "print(1)" SortImports(file_contents=test_input, check=True) out, err = capsys.readouterr() - assert out == '' + assert out == "" def test_ignore_whitespace(capsys): - test_input = ('import os\n' - 'from django.conf import settings\n') + test_input = "import os\n" "from django.conf import settings\n" SortImports(file_contents=test_input, check=True, ignore_whitespace=True) out, err = capsys.readouterr() - assert out == '' + assert out == "" def test_import_wraps_with_comment_issue_471() -> None: """Test to ensure issue #471 is resolved""" - test_input = ('from very_long_module_name import SuperLongClassName #@UnusedImport' - ' -- long string of comments which wrap over') - expected_output = ('from very_long_module_name import (\n' - ' SuperLongClassName) # @UnusedImport -- long string of comments which wrap over\n') - assert SortImports(file_contents=test_input, line_length=50, multi_line_output=1, - use_parentheses=True).output == expected_output + test_input = ( + "from very_long_module_name import SuperLongClassName #@UnusedImport" + " -- long string of comments which wrap over" + ) + expected_output = ( + "from very_long_module_name import (\n" + " SuperLongClassName) # @UnusedImport -- long string of comments which wrap over\n" + ) + assert ( + SortImports( + file_contents=test_input, + line_length=50, + multi_line_output=1, + use_parentheses=True, + ).output + == expected_output + ) def test_import_case_produces_inconsistent_results_issue_472() -> None: """Test to ensure sorting imports with same name but different case produces the same result across platforms""" - test_input = ('from sqlalchemy.dialects.postgresql import ARRAY\n' - 'from sqlalchemy.dialects.postgresql import array\n') - assert SortImports(file_contents=test_input, force_single_line=True).output == test_input + test_input = ( + "from sqlalchemy.dialects.postgresql import ARRAY\n" + "from sqlalchemy.dialects.postgresql import array\n" + ) + assert ( + SortImports(file_contents=test_input, force_single_line=True).output + == test_input + ) - test_input = 'from scrapy.core.downloader.handlers.http import HttpDownloadHandler, HTTPDownloadHandler\n' + test_input = "from scrapy.core.downloader.handlers.http import HttpDownloadHandler, HTTPDownloadHandler\n" assert SortImports(file_contents=test_input).output == test_input def test_inconsistent_behavior_in_python_2_and_3_issue_479() -> None: """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, - known_first_party=["future"]).output == test_input + test_input = ( + "from future.standard_library import hooks\n" + "from workalendar.europe import UnitedKingdom\n" + ) + assert ( + SortImports(file_contents=test_input, known_first_party=["future"]).output + == test_input + ) def test_sort_within_section_comments_issue_436() -> None: """Test to ensure sort within sections leaves comments untouched""" - test_input = ('import os.path\n' - 'import re\n' - '\n' - '# report.py exists in ... comment line 1\n' - '# this file needs to ... comment line 2\n' - '# it must not be ... comment line 3\n' - 'import report\n') - assert SortImports(file_contents=test_input, force_sort_within_sections=True).output == test_input + test_input = ( + "import os.path\n" + "import re\n" + "\n" + "# report.py exists in ... comment line 1\n" + "# this file needs to ... comment line 2\n" + "# it must not be ... comment line 3\n" + "import report\n" + ) + assert ( + SortImports(file_contents=test_input, force_sort_within_sections=True).output + == test_input + ) def test_sort_within_sections_with_force_to_top_issue_473() -> None: """Test to ensure it's possible to sort within sections with items forced to top""" - test_input = ('import z\n' - 'import foo\n' - 'from foo import bar\n') - assert SortImports(file_contents=test_input, force_sort_within_sections=True, - force_to_top=['z']).output == test_input + test_input = "import z\n" "import foo\n" "from foo import bar\n" + assert ( + SortImports( + file_contents=test_input, + force_sort_within_sections=True, + force_to_top=["z"], + ).output + == test_input + ) def test_correct_number_of_new_lines_with_comment_issue_435() -> None: """Test to ensure that injecting a comment in-between imports doesn't mess up the new line spacing""" - test_input = ('import foo\n' - '\n' - '# comment\n' - '\n' - '\n' - 'def baz():\n' - ' pass\n') + test_input = "import foo\n" "\n" "# comment\n" "\n" "\n" "def baz():\n" " pass\n" assert SortImports(file_contents=test_input).output == test_input def test_future_below_encoding_issue_545() -> None: """Test to ensure future is always below comment""" - test_input = ('#!/usr/bin/env python\n' - 'from __future__ import print_function\n' - 'import logging\n' - '\n' - 'print("hello")\n') - expected_output = ('#!/usr/bin/env python\n' - 'from __future__ import print_function\n' - '\n' - 'import logging\n' - '\n' - 'print("hello")\n') + test_input = ( + "#!/usr/bin/env python\n" + "from __future__ import print_function\n" + "import logging\n" + "\n" + 'print("hello")\n' + ) + expected_output = ( + "#!/usr/bin/env python\n" + "from __future__ import print_function\n" + "\n" + "import logging\n" + "\n" + 'print("hello")\n' + ) assert SortImports(file_contents=test_input).output == expected_output def test_no_extra_lines_issue_557() -> None: """Test to ensure no extra lines are prepended""" - test_input = ('import os\n' - '\n' - 'from scrapy.core.downloader.handlers.http import HttpDownloadHandler, HTTPDownloadHandler\n') - expected_output = ('import os\n' - 'from scrapy.core.downloader.handlers.http import HttpDownloadHandler, HTTPDownloadHandler\n') - assert SortImports(file_contents=test_input, force_alphabetical_sort=True, - force_sort_within_sections=True).output == expected_output + test_input = ( + "import os\n" + "\n" + "from scrapy.core.downloader.handlers.http import HttpDownloadHandler, HTTPDownloadHandler\n" + ) + expected_output = ( + "import os\n" + "from scrapy.core.downloader.handlers.http import HttpDownloadHandler, HTTPDownloadHandler\n" + ) + assert ( + SortImports( + file_contents=test_input, + force_alphabetical_sort=True, + force_sort_within_sections=True, + ).output + == expected_output + ) def test_long_import_wrap_support_with_mode_2() -> None: """Test to ensure mode 2 still allows wrapped imports with slash""" - test_input = ('from foobar.foobar.foobar.foobar import \\\n' - ' an_even_longer_function_name_over_80_characters\n') - assert SortImports(file_contents=test_input, multi_line_output=WrapModes.HANGING_INDENT, - line_length=80).output == test_input + test_input = ( + "from foobar.foobar.foobar.foobar import \\\n" + " an_even_longer_function_name_over_80_characters\n" + ) + assert ( + SortImports( + file_contents=test_input, + multi_line_output=WrapModes.HANGING_INDENT, + line_length=80, + ).output + == test_input + ) def test_pylint_comments_incorrectly_wrapped_issue_571() -> None: """Test to ensure pylint comments don't get wrapped""" - test_input = ('from PyQt5.QtCore import QRegExp # @UnresolvedImport pylint: disable=import-error,' - 'useless-suppression\n') - expected_output = ('from PyQt5.QtCore import \\\n' - ' QRegExp # @UnresolvedImport pylint: disable=import-error,useless-suppression\n') - assert SortImports(file_contents=test_input, line_length=60).output == expected_output + test_input = ( + "from PyQt5.QtCore import QRegExp # @UnresolvedImport pylint: disable=import-error," + "useless-suppression\n" + ) + expected_output = ( + "from PyQt5.QtCore import \\\n" + " QRegExp # @UnresolvedImport pylint: disable=import-error,useless-suppression\n" + ) + assert ( + SortImports(file_contents=test_input, line_length=60).output == expected_output + ) def test_ensure_async_methods_work_issue_537() -> None: """Test to ensure async methods are correctly identified""" - test_input = ('from myapp import myfunction\n' - '\n' - '\n' - 'async def test_myfunction(test_client, app):\n' - ' a = await myfunction(test_client, app)\n') + test_input = ( + "from myapp import myfunction\n" + "\n" + "\n" + "async def test_myfunction(test_client, app):\n" + " a = await myfunction(test_client, app)\n" + ) assert SortImports(file_contents=test_input).output == test_input def test_ensure_as_imports_sort_correctly_within_from_imports_issue_590() -> None: """Test to ensure combination from and as import statements are sorted correct""" - test_input = ('from os import defpath\n' - 'from os import pathsep as separator\n') - assert SortImports(file_contents=test_input, force_sort_within_sections=True).output == test_input + test_input = "from os import defpath\n" "from os import pathsep as separator\n" + assert ( + SortImports(file_contents=test_input, force_sort_within_sections=True).output + == test_input + ) - test_input = ('from os import defpath\n' - 'from os import pathsep as separator\n') + test_input = "from os import defpath\n" "from os import pathsep as separator\n" assert SortImports(file_contents=test_input).output == test_input - test_input = ('from os import defpath\n' - 'from os import pathsep as separator\n') - assert SortImports(file_contents=test_input, force_single_line=True).output == test_input + test_input = "from os import defpath\n" "from os import pathsep as separator\n" + assert ( + SortImports(file_contents=test_input, force_single_line=True).output + == test_input + ) def test_ensure_line_endings_are_preserved_issue_493() -> None: """Test to ensure line endings are not converted""" - test_input = ('from os import defpath\r\n' - 'from os import pathsep as separator\r\n') + test_input = "from os import defpath\r\n" "from os import pathsep as separator\r\n" assert SortImports(file_contents=test_input).output == test_input - test_input = ('from os import defpath\r' - 'from os import pathsep as separator\r') + test_input = "from os import defpath\r" "from os import pathsep as separator\r" assert SortImports(file_contents=test_input).output == test_input - test_input = ('from os import defpath\n' - 'from os import pathsep as separator\n') + test_input = "from os import defpath\n" "from os import pathsep as separator\n" assert SortImports(file_contents=test_input).output == test_input def test_not_splitted_sections() -> None: - whiteline = '\n' - stdlib_section = 'import unittest\n' - firstparty_section = 'from app.pkg1 import mdl1\n' - local_section = 'from .pkg2 import mdl2\n' - statement = 'foo = bar\n' + whiteline = "\n" + stdlib_section = "import unittest\n" + firstparty_section = "from app.pkg1 import mdl1\n" + local_section = "from .pkg2 import mdl2\n" + statement = "foo = bar\n" test_input = ( - stdlib_section + whiteline + firstparty_section + whiteline + - local_section + whiteline + statement + stdlib_section + + whiteline + + firstparty_section + + whiteline + + local_section + + whiteline + + statement ) assert SortImports(file_contents=test_input).output == test_input - assert SortImports(file_contents=test_input, no_lines_before=['LOCALFOLDER']).output == \ - ( - stdlib_section + whiteline + firstparty_section + local_section + - whiteline + statement - ) + assert SortImports( + file_contents=test_input, no_lines_before=["LOCALFOLDER"] + ).output == ( + stdlib_section + + whiteline + + firstparty_section + + local_section + + whiteline + + statement + ) # by default STDLIB and FIRSTPARTY sections are split by THIRDPARTY section, # so don't merge them if THIRDPARTY imports aren't exist - assert SortImports(file_contents=test_input, no_lines_before=['FIRSTPARTY']).output == test_input + assert ( + SortImports(file_contents=test_input, no_lines_before=["FIRSTPARTY"]).output + == test_input + ) # in case when THIRDPARTY section is excluded from sections list, it's ok to merge STDLIB and FIRSTPARTY assert SortImports( file_contents=test_input, - sections=['STDLIB', 'FIRSTPARTY', 'LOCALFOLDER'], - no_lines_before=['FIRSTPARTY'], + sections=["STDLIB", "FIRSTPARTY", "LOCALFOLDER"], + no_lines_before=["FIRSTPARTY"], ).output == ( - stdlib_section + firstparty_section + whiteline + local_section + - whiteline + statement + stdlib_section + + firstparty_section + + whiteline + + local_section + + whiteline + + statement ) # it doesn't change output, because stdlib packages don't have any whitelines before them - assert SortImports(file_contents=test_input, no_lines_before=['STDLIB']).output == test_input + assert ( + SortImports(file_contents=test_input, no_lines_before=["STDLIB"]).output + == test_input + ) def test_no_lines_before_empty_section() -> None: - 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 + 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() -> None: """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.""" - test_input = 'from foo import a, c, b\n' - assert SortImports(file_contents=test_input, no_inline_sort=True, force_single_line=False).output == test_input - assert SortImports(file_contents=test_input, no_inline_sort=False, force_single_line=False).output == 'from foo import a, b, c\n' - expected = ( - 'from foo import a\n' - 'from foo import b\n' - 'from foo import c\n' + test_input = "from foo import a, c, b\n" + assert ( + SortImports( + file_contents=test_input, no_inline_sort=True, force_single_line=False + ).output + == test_input + ) + assert ( + SortImports( + file_contents=test_input, no_inline_sort=False, force_single_line=False + ).output + == "from foo import a, b, c\n" + ) + expected = "from foo import a\n" "from foo import b\n" "from foo import c\n" + assert ( + SortImports( + file_contents=test_input, no_inline_sort=False, force_single_line=True + ).output + == expected + ) + assert ( + SortImports( + file_contents=test_input, no_inline_sort=True, force_single_line=True + ).output + == expected ) - assert SortImports(file_contents=test_input, no_inline_sort=False, force_single_line=True).output == expected - assert SortImports(file_contents=test_input, no_inline_sort=True, force_single_line=True).output == expected def test_relative_import_of_a_module() -> None: """Imports can be dynamically created (PEP302) and is used by modules such as six. This test ensures that these types of imports are still sorted to the correct type instead of being categorized as local.""" - test_input = ('from __future__ import absolute_import\n' - '\n' - 'import itertools\n' - '\n' - 'from six import add_metaclass\n' - '\n' - 'from six.moves import asd\n' - ) - - expected_results = ('from __future__ import absolute_import\n' - '\n' - 'import itertools\n' - '\n' - 'from six import add_metaclass\n' - 'from six.moves import asd\n' - ) + test_input = ( + "from __future__ import absolute_import\n" + "\n" + "import itertools\n" + "\n" + "from six import add_metaclass\n" + "\n" + "from six.moves import asd\n" + ) + + expected_results = ( + "from __future__ import absolute_import\n" + "\n" + "import itertools\n" + "\n" + "from six import add_metaclass\n" + "from six.moves import asd\n" + ) sorted_result = SortImports(file_contents=test_input, force_single_line=True).output assert sorted_result == expected_results def test_escaped_parens_sort() -> None: - test_input = ('from foo import \\ \n' - '(a,\n' - 'b,\n' - 'c)\n') - expected = ('from foo import a, b, c\n') + test_input = "from foo import \\ \n" "(a,\n" "b,\n" "c)\n" + expected = "from foo import a, b, c\n" assert SortImports(file_contents=test_input).output == expected def test_is_python_file_ioerror(tmpdir): - does_not_exist = tmpdir.join('fake.txt') + does_not_exist = tmpdir.join("fake.txt") assert is_python_file(str(does_not_exist)) is False def test_is_python_file_shebang(tmpdir): - path = tmpdir.join('myscript') - path.write('#!/usr/bin/env python\n') + path = tmpdir.join("myscript") + path.write("#!/usr/bin/env python\n") assert is_python_file(str(path)) is True def test_is_python_file_editor_backup(tmpdir): - path = tmpdir.join('myscript~') - path.write('#!/usr/bin/env python\n') + path = tmpdir.join("myscript~") + path.write("#!/usr/bin/env python\n") assert is_python_file(str(path)) is False def test_is_python_typing_stub(tmpdir): - stub = tmpdir.join('stub.pyi') + stub = tmpdir.join("stub.pyi") assert is_python_file(str(stub)) is True def test_to_ensure_imports_are_brought_to_top_issue_651() -> None: - test_input = ('from __future__ import absolute_import, unicode_literals\n' - '\n' - 'VAR = """\n' - 'multiline text\n' - '"""\n' - '\n' - 'from __future__ import unicode_literals\n' - 'from __future__ import absolute_import\n') - expected_output = ('from __future__ import absolute_import, unicode_literals\n' - '\n' - 'VAR = """\n' - 'multiline text\n' - '"""\n') + test_input = ( + "from __future__ import absolute_import, unicode_literals\n" + "\n" + 'VAR = """\n' + "multiline text\n" + '"""\n' + "\n" + "from __future__ import unicode_literals\n" + "from __future__ import absolute_import\n" + ) + expected_output = ( + "from __future__ import absolute_import, unicode_literals\n" + "\n" + 'VAR = """\n' + "multiline text\n" + '"""\n' + ) assert SortImports(file_contents=test_input).output == expected_output def test_to_ensure_importing_from_imports_module_works_issue_662() -> None: - test_input = ('@wraps(fun)\n' - 'def __inner(*args, **kwargs):\n' - ' from .imports import qualname\n' - ' warn(description=description or qualname(fun), deprecation=deprecation, removal=removal)\n') + test_input = ( + "@wraps(fun)\n" + "def __inner(*args, **kwargs):\n" + " from .imports import qualname\n" + " warn(description=description or qualname(fun), deprecation=deprecation, removal=removal)\n" + ) assert SortImports(file_contents=test_input).output == test_input def test_to_ensure_no_unexpected_changes_issue_666() -> None: - test_input = ('from django.conf import settings\n' - 'from django.core.management import call_command\n' - 'from django.core.management.base import BaseCommand\n' - 'from django.utils.translation import ugettext_lazy as _\n' - '\n' - 'TEMPLATE = """\n' - '# This file is generated automatically with the management command\n' - '#\n' - '# manage.py bis_compile_i18n\n' - '#\n' - '# please dont change it manually.\n' - 'from django.utils.translation import ugettext_lazy as _\n' - '"""\n') + test_input = ( + "from django.conf import settings\n" + "from django.core.management import call_command\n" + "from django.core.management.base import BaseCommand\n" + "from django.utils.translation import ugettext_lazy as _\n" + "\n" + 'TEMPLATE = """\n' + "# This file is generated automatically with the management command\n" + "#\n" + "# manage.py bis_compile_i18n\n" + "#\n" + "# please dont change it manually.\n" + "from django.utils.translation import ugettext_lazy as _\n" + '"""\n' + ) assert SortImports(file_contents=test_input).output == test_input def test_to_ensure_tabs_dont_become_space_issue_665() -> None: - test_input = ('import os\n' - '\n' - '\n' - 'def my_method():\n' - '\tpass\n') + test_input = "import os\n" "\n" "\n" "def my_method():\n" "\tpass\n" assert SortImports(file_contents=test_input).output == test_input def test_new_lines_are_preserved() -> None: - with NamedTemporaryFile('w', suffix='py', delete=False) as rn_newline: + with NamedTemporaryFile("w", suffix="py", delete=False) as rn_newline: pass try: - with io.open(rn_newline.name, mode='w', newline='') as rn_newline_input: - rn_newline_input.write('import sys\r\nimport os\r\n') + with io.open(rn_newline.name, mode="w", newline="") as rn_newline_input: + rn_newline_input.write("import sys\r\nimport os\r\n") SortImports(rn_newline.name, settings_path=os.getcwd()) with io.open(rn_newline.name) as new_line_file: print(new_line_file.read()) - with io.open(rn_newline.name, newline='') as rn_newline_file: + with io.open(rn_newline.name, newline="") as rn_newline_file: rn_newline_contents = rn_newline_file.read() - assert rn_newline_contents == 'import os\r\nimport sys\r\n' + assert rn_newline_contents == "import os\r\nimport sys\r\n" finally: os.remove(rn_newline.name) - with NamedTemporaryFile('w', suffix='py', delete=False) as r_newline: + with NamedTemporaryFile("w", suffix="py", delete=False) as r_newline: pass try: - with io.open(r_newline.name, mode='w', newline='') as r_newline_input: - r_newline_input.write('import sys\rimport os\r') + with io.open(r_newline.name, mode="w", newline="") as r_newline_input: + r_newline_input.write("import sys\rimport os\r") SortImports(r_newline.name, settings_path=os.getcwd()) - with io.open(r_newline.name, newline='') as r_newline_file: + with io.open(r_newline.name, newline="") as r_newline_file: r_newline_contents = r_newline_file.read() - assert r_newline_contents == 'import os\rimport sys\r' + assert r_newline_contents == "import os\rimport sys\r" finally: os.remove(r_newline.name) - with NamedTemporaryFile('w', suffix='py', delete=False) as n_newline: + with NamedTemporaryFile("w", suffix="py", delete=False) as n_newline: pass try: - with io.open(n_newline.name, mode='w', newline='') as n_newline_input: - n_newline_input.write('import sys\nimport os\n') + with io.open(n_newline.name, mode="w", newline="") as n_newline_input: + n_newline_input.write("import sys\nimport os\n") SortImports(n_newline.name, settings_path=os.getcwd()) - with io.open(n_newline.name, newline='') as n_newline_file: + with io.open(n_newline.name, newline="") as n_newline_file: n_newline_contents = n_newline_file.read() - assert n_newline_contents == 'import os\nimport sys\n' + assert n_newline_contents == "import os\nimport sys\n" finally: os.remove(n_newline.name) def test_requirements_finder(tmpdir): - subdir = tmpdir.mkdir('subdir').join("lol.txt") + subdir = tmpdir.mkdir("subdir").join("lol.txt") subdir.write("flask") - req_file = tmpdir.join('requirements.txt') + req_file = tmpdir.join("requirements.txt") req_file.write( - "Django==1.11\n" - "-e git+https://github.com/orsinium/deal.git#egg=deal\n" + "Django==1.11\n" "-e git+https://github.com/orsinium/deal.git#egg=deal\n" ) si = SortImports(file_contents="") for path in (str(tmpdir), str(subdir)): finder = finders.RequirementsFinder( - config=si.config, - sections=si.sections, - path=path + config=si.config, sections=si.sections, path=path ) files = list(finder._get_files()) assert len(files) == 1 # file finding - assert files[0].endswith('requirements.txt') # file finding - assert set(finder._get_names(str(req_file))) == {'Django', 'deal'} # file parsing + assert files[0].endswith("requirements.txt") # file finding + assert set(finder._get_names(str(req_file))) == { + "Django", + "deal", + } # file parsing assert finder.find("django") == si.sections.THIRDPARTY # package in reqs assert finder.find("flask") is None # package not in reqs assert finder.find("deal") == si.sections.THIRDPARTY # vcs assert len(finder.mapping) > 100 - assert finder._normalize_name('deal') == 'deal' - assert finder._normalize_name('Django') == 'django' # lowercase - assert finder._normalize_name('django_haystack') == 'haystack' # mapping - assert finder._normalize_name('Flask-RESTful') == 'flask_restful' # conver `-`to `_` + assert finder._normalize_name("deal") == "deal" + assert finder._normalize_name("Django") == "django" # lowercase + assert finder._normalize_name("django_haystack") == "haystack" # mapping + assert ( + finder._normalize_name("Flask-RESTful") == "flask_restful" + ) # conver `-`to `_` req_file.remove() def test_forced_separate_is_deterministic_issue_774(tmpdir): - config_file = tmpdir.join('setup.cfg') + config_file = tmpdir.join("setup.cfg") config_file.write( "[isort]\n" "forced_separate:\n" @@ -2650,17 +3157,22 @@ def test_forced_separate_is_deterministic_issue_774(tmpdir): " separate4\n" ) - test_input = ('import time\n' - '\n' - 'from separate1 import foo\n' - '\n' - 'from separate2 import bar\n' - '\n' - 'from separate3 import baz\n' - '\n' - 'from separate4 import quux\n') + test_input = ( + "import time\n" + "\n" + "from separate1 import foo\n" + "\n" + "from separate2 import bar\n" + "\n" + "from separate3 import baz\n" + "\n" + "from separate4 import quux\n" + ) - assert SortImports(file_contents=test_input, settings_path=config_file.strpath).output == test_input + assert ( + SortImports(file_contents=test_input, settings_path=config_file.strpath).output + == test_input + ) PIPFILE = """ @@ -2681,26 +3193,26 @@ def test_forced_separate_is_deterministic_issue_774(tmpdir): def test_pipfile_finder(tmpdir): - pipfile = tmpdir.join('Pipfile') + pipfile = tmpdir.join("Pipfile") pipfile.write(PIPFILE) si = SortImports(file_contents="") finder = finders.PipfileFinder( - config=si.config, - sections=si.sections, - path=str(tmpdir) + config=si.config, sections=si.sections, path=str(tmpdir) ) - assert set(finder._get_names(str(tmpdir))) == {'Django', 'deal'} # file parsing + assert set(finder._get_names(str(tmpdir))) == {"Django", "deal"} # file parsing assert finder.find("django") == si.sections.THIRDPARTY # package in reqs assert finder.find("flask") is None # package not in reqs assert finder.find("deal") == si.sections.THIRDPARTY # vcs assert len(finder.mapping) > 100 - assert finder._normalize_name('deal') == 'deal' - assert finder._normalize_name('Django') == 'django' # lowercase - assert finder._normalize_name('django_haystack') == 'haystack' # mapping - assert finder._normalize_name('Flask-RESTful') == 'flask_restful' # conver `-`to `_` + assert finder._normalize_name("deal") == "deal" + assert finder._normalize_name("Django") == "django" # lowercase + assert finder._normalize_name("django_haystack") == "haystack" # mapping + assert ( + finder._normalize_name("Flask-RESTful") == "flask_restful" + ) # conver `-`to `_` pipfile.remove() @@ -2714,20 +3226,21 @@ def test_monkey_patched_urllib() -> None: def test_path_finder(monkeypatch): si = SortImports(file_contents="") - finder = finders.PathFinder( - config=si.config, - sections=si.sections, - ) + finder = finders.PathFinder(config=si.config, sections=si.sections) third_party_prefix = next(path for path in finder.paths if "site-packages" in path) ext_suffix = sysconfig.get_config_var("EXT_SUFFIX") or ".so" - imaginary_paths = set([ - posixpath.join(finder.stdlib_lib_prefix, "example_1.py"), - posixpath.join(third_party_prefix, "example_2.py"), - posixpath.join(third_party_prefix, "example_3.so"), - posixpath.join(third_party_prefix, "example_4" + ext_suffix), - posixpath.join(os.getcwd(), "example_5.py"), - ]) - monkeypatch.setattr("isort.finders.exists_case_sensitive", lambda p: p in imaginary_paths) + imaginary_paths = set( + [ + posixpath.join(finder.stdlib_lib_prefix, "example_1.py"), + posixpath.join(third_party_prefix, "example_2.py"), + posixpath.join(third_party_prefix, "example_3.so"), + posixpath.join(third_party_prefix, "example_4" + ext_suffix), + posixpath.join(os.getcwd(), "example_5.py"), + ] + ) + monkeypatch.setattr( + "isort.finders.exists_case_sensitive", lambda p: p in imaginary_paths + ) assert finder.find("example_1") == finder.sections.STDLIB assert finder.find("example_2") == finder.sections.THIRDPARTY assert finder.find("example_3") == finder.sections.THIRDPARTY @@ -2737,25 +3250,37 @@ def test_path_finder(monkeypatch): def test_argument_parsing() -> None: from isort.main import parse_args - args = parse_args(['-dt', '-t', 'foo', '--skip=bar', 'baz.py']) - assert args['order_by_type'] is False - assert args['force_to_top'] == ['foo'] - assert args['skip'] == ['bar'] - assert args['files'] == ['baz.py'] + + args = parse_args(["-dt", "-t", "foo", "--skip=bar", "baz.py"]) + assert args["order_by_type"] is False + assert args["force_to_top"] == ["foo"] + assert args["skip"] == ["bar"] + assert args["files"] == ["baz.py"] -@pytest.mark.parametrize('multiprocess', (False, True)) +@pytest.mark.parametrize("multiprocess", (False, True)) def test_command_line(tmpdir, capfd, multiprocess): from isort.main import main - tmpdir.join("file1.py").write("import re\nimport os\n\nimport contextlib\n\n\nimport isort") - tmpdir.join("file2.py").write("import collections\nimport time\n\nimport abc\n\n\nimport isort") - arguments = ["-rc", str(tmpdir), '--settings-path', os.getcwd()] + + tmpdir.join("file1.py").write( + "import re\nimport os\n\nimport contextlib\n\n\nimport isort" + ) + tmpdir.join("file2.py").write( + "import collections\nimport time\n\nimport abc\n\n\nimport isort" + ) + arguments = ["-rc", str(tmpdir), "--settings-path", os.getcwd()] if multiprocess: - arguments.extend(['--jobs', '2']) + arguments.extend(["--jobs", "2"]) main(arguments) - assert tmpdir.join("file1.py").read() == "import contextlib\nimport os\nimport re\n\nimport isort\n" - assert tmpdir.join("file2.py").read() == "import abc\nimport collections\nimport time\n\nimport isort\n" - if not sys.platform.startswith('win'): + assert ( + tmpdir.join("file1.py").read() + == "import contextlib\nimport os\nimport re\n\nimport isort\n" + ) + assert ( + tmpdir.join("file2.py").read() + == "import abc\nimport collections\nimport time\n\nimport isort\n" + ) + if not sys.platform.startswith("win"): out, err = capfd.readouterr() assert not err # it informs us about fixing the files: @@ -2768,6 +3293,7 @@ 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)] @@ -2779,12 +3305,14 @@ def test_quiet(tmpdir, capfd, quiet): assert bool(out) != quiet -@pytest.mark.parametrize('enabled', (False, True)) +@pytest.mark.parametrize("enabled", (False, True)) def test_safety_excludes(tmpdir, enabled): tmpdir.join("victim.py").write("# ...") toxdir = tmpdir.mkdir(".tox") toxdir.join("verysafe.py").write("# ...") - tmpdir.mkdir("lib").mkdir("python3.7").join("importantsystemlibrary.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 = [] # type: List[str] @@ -2792,603 +3320,914 @@ def test_safety_excludes(tmpdir, enabled): 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)) + 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 file_names == {"victim.py"} assert len(skipped) == 3 else: - assert file_names == {os.sep.join(('.tox', 'verysafe.py')), - os.sep.join(('lib', 'python3.7', 'importantsystemlibrary.py')), - os.sep.join(('.pants.d', 'pants.py')), - 'victim.py'} + assert file_names == { + os.sep.join((".tox", "verysafe.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'} + 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, {}))) +@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') + 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 = [] # type: List[str] - file_names = set(os.path.relpath(f, str(base_dir)) for f in main.iter_source_code([str(base_dir)], config, 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() -> None: - test_input = ('import distutils\n' - '# this comment is important and should not be removed\n' - 'from sys import api_version as api_version\n') + test_input = ( + "import distutils\n" + "# this comment is important and should not be removed\n" + "from sys import api_version as api_version\n" + ) assert SortImports(file_contents=test_input).output == test_input def test_reverse_relative_imports_issue_417() -> None: - test_input = ('from . import ipsum\n' - 'from . import lorem\n' - 'from .dolor import consecteur\n' - 'from .sit import apidiscing\n' - 'from .. import donec\n' - 'from .. import euismod\n' - 'from ..mi import iaculis\n' - 'from ..nec import tempor\n' - 'from ... import diam\n' - '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 + test_input = ( + "from . import ipsum\n" + "from . import lorem\n" + "from .dolor import consecteur\n" + "from .sit import apidiscing\n" + "from .. import donec\n" + "from .. import euismod\n" + "from ..mi import iaculis\n" + "from ..nec import tempor\n" + "from ... import diam\n" + "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() -> None: - 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 + 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 + ) def test_unwrap_issue_762() -> None: - test_input = ('from os.path \\\n' - 'import (join, split)\n') - assert SortImports(file_contents=test_input).output == 'from os.path import join, split\n' + test_input = "from os.path \\\n" "import (join, split)\n" + assert ( + SortImports(file_contents=test_input).output + == "from os.path import join, split\n" + ) - test_input = ('from os.\\\n' - ' path import (join, split)') - assert SortImports(file_contents=test_input).output == 'from os.path import join, split\n' + 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_multiple_as_imports() -> None: - test_input = ('from a import b as b\n' - 'from a import b as bb\n' - 'from a import b as bb_\n') + test_input = ( + "from a import b as b\n" "from a import b as bb\n" "from a import b as bb_\n" + ) test_output = SortImports(file_contents=test_input).output assert test_output == test_input test_output = SortImports(file_contents=test_input, combine_as_imports=True).output - assert test_output == 'from a import b as b, b as bb, b as bb_\n' - test_output = SortImports(file_contents=test_input, keep_direct_and_as_imports=True).output + assert test_output == "from a import b as b, b as bb, b as bb_\n" + test_output = SortImports( + file_contents=test_input, keep_direct_and_as_imports=True + ).output assert test_output == test_input - test_output = SortImports(file_contents=test_input, combine_as_imports=True, keep_direct_and_as_imports=True).output - assert test_output == 'from a import b as b, b as bb, b as bb_\n' + test_output = SortImports( + file_contents=test_input, + combine_as_imports=True, + keep_direct_and_as_imports=True, + ).output + assert test_output == "from a import b as b, b as bb, b as bb_\n" - test_input = ('from a import b\n' - 'from a import b as b\n' - 'from a import b as bb\n' - 'from a import b as bb_\n') + test_input = ( + "from a import b\n" + "from a import b as b\n" + "from a import b as bb\n" + "from a import b as bb_\n" + ) test_output = SortImports(file_contents=test_input).output - assert test_output == 'from a import b as b\nfrom a import b as bb\nfrom a import b as bb_\n' + assert ( + test_output + == "from a import b as b\nfrom a import b as bb\nfrom a import b as bb_\n" + ) test_output = SortImports(file_contents=test_input, combine_as_imports=True).output - assert test_output == 'from a import b as b, b as bb, b as bb_\n' - test_output = SortImports(file_contents=test_input, keep_direct_and_as_imports=True).output + assert test_output == "from a import b as b, b as bb, b as bb_\n" + test_output = SortImports( + file_contents=test_input, keep_direct_and_as_imports=True + ).output assert test_output == test_input - test_output = SortImports(file_contents=test_input, combine_as_imports=True, keep_direct_and_as_imports=True).output - assert test_output == 'from a import b, b as b, b as bb, b as bb_\n' + test_output = SortImports( + file_contents=test_input, + combine_as_imports=True, + keep_direct_and_as_imports=True, + ).output + assert test_output == "from a import b, b as b, b as bb, b as bb_\n" - test_input = ('from a import b as e\n' - 'from a import b as c\n' - 'from a import b\n' - 'from a import b as f\n') + test_input = ( + "from a import b as e\n" + "from a import b as c\n" + "from a import b\n" + "from a import b as f\n" + ) test_output = SortImports(file_contents=test_input).output - assert test_output == 'from a import b as c\nfrom a import b as e\nfrom a import b as f\n' + assert ( + test_output + == "from a import b as c\nfrom a import b as e\nfrom a import b as f\n" + ) test_output = SortImports(file_contents=test_input, combine_as_imports=True).output - assert test_output == 'from a import b as c, b as e, b as f\n' - test_output = SortImports(file_contents=test_input, keep_direct_and_as_imports=True).output - assert test_output == 'from a import b\nfrom a import b as c\nfrom a import b as e\nfrom a import b as f\n' + assert test_output == "from a import b as c, b as e, b as f\n" + test_output = SortImports( + file_contents=test_input, keep_direct_and_as_imports=True + ).output + assert ( + test_output + == "from a import b\nfrom a import b as c\nfrom a import b as e\nfrom a import b as f\n" + ) test_output = SortImports(file_contents=test_input, no_inline_sort=True).output - assert test_output == 'from a import b as c\nfrom a import b as e\nfrom a import b as f\n' - test_output = SortImports(file_contents=test_input, keep_direct_and_as_imports=True, no_inline_sort=True).output - assert test_output == 'from a import b\nfrom a import b as c\nfrom a import b as e\nfrom a import b as f\n' - test_output = SortImports(file_contents=test_input, combine_as_imports=True, keep_direct_and_as_imports=True).output - assert test_output == 'from a import b, b as c, b as e, b as f\n' - test_output = SortImports(file_contents=test_input, combine_as_imports=True, no_inline_sort=True).output - assert test_output == 'from a import b as e, b as c, b as f\n' - test_output = SortImports(file_contents=test_input, combine_as_imports=True, keep_direct_and_as_imports=True, no_inline_sort=True).output - assert test_output == 'from a import b, b as e, b as c, b as f\n' - - test_input = ('import a as a\n' - 'import a as aa\n' - 'import a as aa_\n') + assert ( + test_output + == "from a import b as c\nfrom a import b as e\nfrom a import b as f\n" + ) + test_output = SortImports( + file_contents=test_input, keep_direct_and_as_imports=True, no_inline_sort=True + ).output + assert ( + test_output + == "from a import b\nfrom a import b as c\nfrom a import b as e\nfrom a import b as f\n" + ) + test_output = SortImports( + file_contents=test_input, + combine_as_imports=True, + keep_direct_and_as_imports=True, + ).output + assert test_output == "from a import b, b as c, b as e, b as f\n" + test_output = SortImports( + file_contents=test_input, combine_as_imports=True, no_inline_sort=True + ).output + assert test_output == "from a import b as e, b as c, b as f\n" + test_output = SortImports( + file_contents=test_input, + combine_as_imports=True, + keep_direct_and_as_imports=True, + no_inline_sort=True, + ).output + assert test_output == "from a import b, b as e, b as c, b as f\n" + + test_input = "import a as a\n" "import a as aa\n" "import a as aa_\n" test_output = SortImports(file_contents=test_input).output assert test_output == test_input - test_output = SortImports(file_contents=test_input, combine_as_imports=True, keep_direct_and_as_imports=True).output + test_output = SortImports( + file_contents=test_input, + combine_as_imports=True, + keep_direct_and_as_imports=True, + ).output assert test_output == test_input - test_input = ('import a\n' - 'import a as a\n' - 'import a as aa\n' - 'import a as aa_\n') + test_input = "import a\n" "import a as a\n" "import a as aa\n" "import a as aa_\n" test_output = SortImports(file_contents=test_input).output - assert test_output == 'import a as a\nimport a as aa\nimport a as aa_\n' - test_output = SortImports(file_contents=test_input, combine_as_imports=True, keep_direct_and_as_imports=True).output + assert test_output == "import a as a\nimport a as aa\nimport a as aa_\n" + test_output = SortImports( + file_contents=test_input, + combine_as_imports=True, + keep_direct_and_as_imports=True, + ).output assert test_output == test_input def test_all_imports_from_single_module() -> None: - test_input = ('import a\n' - 'from a import *\n' - 'from a import b as d\n' - 'from a import z, x, y\n' - 'from a import b\n' - 'from a import w, i as j\n' - 'from a import b as c, g as h\n' - 'from a import e as f\n') - test_output = SortImports(file_contents=test_input, combine_star=False, combine_as_imports=False, - keep_direct_and_as_imports=False, force_single_line=False, no_inline_sort=False).output - assert test_output == ('import a\n' - 'from a import *\n' - 'from a import b as c\n' - 'from a import b as d\n' - 'from a import e as f\n' - 'from a import g as h\n' - 'from a import i as j\n' - 'from a import w, x, y, z\n') - test_output = SortImports(file_contents=test_input, combine_star=True, combine_as_imports=False, - keep_direct_and_as_imports=False, force_single_line=False, no_inline_sort=False).output - assert test_output == 'import a\nfrom a import *\n' - test_output = SortImports(file_contents=test_input, combine_star=False, combine_as_imports=True, - keep_direct_and_as_imports=False, force_single_line=False, no_inline_sort=False).output - assert test_output == ('import a\n' - 'from a import *\n' - 'from a import b as c, b as d, e as f, g as h, i as j, w, x, y, z\n') - test_output = SortImports(file_contents=test_input, combine_star=False, combine_as_imports=False, - keep_direct_and_as_imports=True, force_single_line=False, no_inline_sort=False).output - assert test_output == ('import a\n' - 'from a import *\n' - 'from a import b\n' - 'from a import b as c\n' - 'from a import b as d\n' - 'from a import e as f\n' - 'from a import g as h\n' - 'from a import i as j\n' - 'from a import w, x, y, z\n') - test_output = SortImports(file_contents=test_input, combine_star=False, combine_as_imports=False, - keep_direct_and_as_imports=False, force_single_line=True, no_inline_sort=False).output - assert test_output == ('import a\n' - 'from a import *\n' - 'from a import b as c\n' - 'from a import b as d\n' - 'from a import e as f\n' - 'from a import g as h\n' - 'from a import i as j\n' - 'from a import w\n' - 'from a import x\n' - 'from a import y\n' - 'from a import z\n') - test_output = SortImports(file_contents=test_input, combine_star=False, combine_as_imports=False, - keep_direct_and_as_imports=False, force_single_line=False, no_inline_sort=True).output - assert test_output == ('import a\n' - 'from a import *\n' - 'from a import b as c\n' - 'from a import b as d\n' - 'from a import z, x, y, w\n' - 'from a import i as j\n' - 'from a import g as h\n' - 'from a import e as f\n') - test_output = SortImports(file_contents=test_input, combine_star=True, combine_as_imports=True, - keep_direct_and_as_imports=False, force_single_line=False, no_inline_sort=False).output - assert test_output == 'import a\nfrom a import *\n' - test_output = SortImports(file_contents=test_input, combine_star=True, combine_as_imports=False, - keep_direct_and_as_imports=True, force_single_line=False, no_inline_sort=False).output - assert test_output == 'import a\nfrom a import *\n' - test_output = SortImports(file_contents=test_input, combine_star=True, combine_as_imports=False, - keep_direct_and_as_imports=False, force_single_line=True, no_inline_sort=False).output - assert test_output == 'import a\nfrom a import *\n' - test_output = SortImports(file_contents=test_input, combine_star=True, combine_as_imports=False, - keep_direct_and_as_imports=False, force_single_line=False, no_inline_sort=True).output - assert test_output == 'import a\nfrom a import *\n' - test_output = SortImports(file_contents=test_input, combine_star=False, combine_as_imports=True, - keep_direct_and_as_imports=True, force_single_line=False, no_inline_sort=False).output - assert test_output == ('import a\n' - 'from a import *\n' - 'from a import b, b as c, b as d, e as f, g as h, i as j, w, x, y, z\n') - test_output = SortImports(file_contents=test_input, combine_star=False, combine_as_imports=True, - keep_direct_and_as_imports=False, force_single_line=True, no_inline_sort=False).output - assert test_output == ('import a\n' - 'from a import *\n' - 'from a import b as c\n' - 'from a import b as d\n' - 'from a import e as f\n' - 'from a import g as h\n' - 'from a import i as j\n' - 'from a import w\n' - 'from a import x\n' - 'from a import y\n' - 'from a import z\n') - test_output = SortImports(file_contents=test_input, combine_star=False, combine_as_imports=True, - keep_direct_and_as_imports=False, force_single_line=False, no_inline_sort=True).output - assert test_output == ('import a\n' - 'from a import *\n' - 'from a import b as d, b as c, z, x, y, w, i as j, g as h, e as f\n') - test_output = SortImports(file_contents=test_input, combine_star=False, combine_as_imports=False, - keep_direct_and_as_imports=True, force_single_line=True, no_inline_sort=False).output - assert test_output == ('import a\n' - 'from a import *\n' - 'from a import b\n' - 'from a import b as c\n' - 'from a import b as d\n' - 'from a import e as f\n' - 'from a import g as h\n' - 'from a import i as j\n' - 'from a import w\n' - 'from a import x\n' - 'from a import y\n' - 'from a import z\n') - test_output = SortImports(file_contents=test_input, combine_star=False, combine_as_imports=False, - keep_direct_and_as_imports=True, force_single_line=False, no_inline_sort=True).output - assert test_output == ('import a\n' - 'from a import *\n' - 'from a import b\n' - 'from a import b as c\n' - 'from a import b as d\n' - 'from a import z, x, y, w\n' - 'from a import i as j\n' - 'from a import g as h\n' - 'from a import e as f\n') - test_output = SortImports(file_contents=test_input, combine_star=False, combine_as_imports=False, - keep_direct_and_as_imports=False, force_single_line=True, no_inline_sort=True).output - assert test_output == ('import a\n' - 'from a import *\n' - 'from a import b as c\n' - 'from a import b as d\n' - 'from a import e as f\n' - 'from a import g as h\n' - 'from a import i as j\n' - 'from a import w\n' - 'from a import x\n' - 'from a import y\n' - 'from a import z\n') - test_output = SortImports(file_contents=test_input, combine_star=True, combine_as_imports=True, - keep_direct_and_as_imports=True, force_single_line=False, no_inline_sort=False).output - assert test_output == 'import a\nfrom a import *\n' - test_output = SortImports(file_contents=test_input, combine_star=True, combine_as_imports=True, - keep_direct_and_as_imports=False, force_single_line=True, no_inline_sort=False).output - assert test_output == 'import a\nfrom a import *\n' - test_output = SortImports(file_contents=test_input, combine_star=True, combine_as_imports=True, - keep_direct_and_as_imports=False, force_single_line=False, no_inline_sort=True).output - assert test_output == 'import a\nfrom a import *\n' - test_output = SortImports(file_contents=test_input, combine_star=True, combine_as_imports=False, - keep_direct_and_as_imports=True, force_single_line=True, no_inline_sort=False).output - assert test_output == 'import a\nfrom a import *\n' - test_output = SortImports(file_contents=test_input, combine_star=True, combine_as_imports=False, - keep_direct_and_as_imports=True, force_single_line=False, no_inline_sort=True).output - assert test_output == 'import a\nfrom a import *\n' - test_output = SortImports(file_contents=test_input, combine_star=True, combine_as_imports=False, - keep_direct_and_as_imports=False, force_single_line=True, no_inline_sort=True).output - assert test_output == 'import a\nfrom a import *\n' - test_output = SortImports(file_contents=test_input, combine_star=False, combine_as_imports=True, - keep_direct_and_as_imports=True, force_single_line=True, no_inline_sort=False).output - assert test_output == ('import a\n' - 'from a import *\n' - 'from a import b\n' - 'from a import b as c\n' - 'from a import b as d\n' - 'from a import e as f\n' - 'from a import g as h\n' - 'from a import i as j\n' - 'from a import w\n' - 'from a import x\n' - 'from a import y\n' - 'from a import z\n') - test_output = SortImports(file_contents=test_input, combine_star=False, combine_as_imports=True, - keep_direct_and_as_imports=True, force_single_line=False, no_inline_sort=True).output - assert test_output == ('import a\n' - 'from a import *\n' - 'from a import b, b as d, b as c, z, x, y, w, i as j, g as h, e as f\n') - test_output = SortImports(file_contents=test_input, combine_star=False, combine_as_imports=False, - keep_direct_and_as_imports=True, force_single_line=True, no_inline_sort=True).output - assert test_output == ('import a\n' - 'from a import *\n' - 'from a import b\n' - 'from a import b as c\n' - 'from a import b as d\n' - 'from a import e as f\n' - 'from a import g as h\n' - 'from a import i as j\n' - 'from a import w\n' - 'from a import x\n' - 'from a import y\n' - 'from a import z\n') - test_output = SortImports(file_contents=test_input, combine_star=True, combine_as_imports=True, - keep_direct_and_as_imports=True, force_single_line=True, no_inline_sort=False).output - assert test_output == 'import a\nfrom a import *\n' + test_input = ( + "import a\n" + "from a import *\n" + "from a import b as d\n" + "from a import z, x, y\n" + "from a import b\n" + "from a import w, i as j\n" + "from a import b as c, g as h\n" + "from a import e as f\n" + ) + test_output = SortImports( + file_contents=test_input, + combine_star=False, + combine_as_imports=False, + keep_direct_and_as_imports=False, + force_single_line=False, + no_inline_sort=False, + ).output + assert test_output == ( + "import a\n" + "from a import *\n" + "from a import b as c\n" + "from a import b as d\n" + "from a import e as f\n" + "from a import g as h\n" + "from a import i as j\n" + "from a import w, x, y, z\n" + ) + test_output = SortImports( + file_contents=test_input, + combine_star=True, + combine_as_imports=False, + keep_direct_and_as_imports=False, + force_single_line=False, + no_inline_sort=False, + ).output + assert test_output == "import a\nfrom a import *\n" + test_output = SortImports( + file_contents=test_input, + combine_star=False, + combine_as_imports=True, + keep_direct_and_as_imports=False, + force_single_line=False, + no_inline_sort=False, + ).output + assert test_output == ( + "import a\n" + "from a import *\n" + "from a import b as c, b as d, e as f, g as h, i as j, w, x, y, z\n" + ) + test_output = SortImports( + file_contents=test_input, + combine_star=False, + combine_as_imports=False, + keep_direct_and_as_imports=True, + force_single_line=False, + no_inline_sort=False, + ).output + assert test_output == ( + "import a\n" + "from a import *\n" + "from a import b\n" + "from a import b as c\n" + "from a import b as d\n" + "from a import e as f\n" + "from a import g as h\n" + "from a import i as j\n" + "from a import w, x, y, z\n" + ) + test_output = SortImports( + file_contents=test_input, + combine_star=False, + combine_as_imports=False, + keep_direct_and_as_imports=False, + force_single_line=True, + no_inline_sort=False, + ).output + assert test_output == ( + "import a\n" + "from a import *\n" + "from a import b as c\n" + "from a import b as d\n" + "from a import e as f\n" + "from a import g as h\n" + "from a import i as j\n" + "from a import w\n" + "from a import x\n" + "from a import y\n" + "from a import z\n" + ) + test_output = SortImports( + file_contents=test_input, + combine_star=False, + combine_as_imports=False, + keep_direct_and_as_imports=False, + force_single_line=False, + no_inline_sort=True, + ).output + assert test_output == ( + "import a\n" + "from a import *\n" + "from a import b as c\n" + "from a import b as d\n" + "from a import z, x, y, w\n" + "from a import i as j\n" + "from a import g as h\n" + "from a import e as f\n" + ) + test_output = SortImports( + file_contents=test_input, + combine_star=True, + combine_as_imports=True, + keep_direct_and_as_imports=False, + force_single_line=False, + no_inline_sort=False, + ).output + assert test_output == "import a\nfrom a import *\n" + test_output = SortImports( + file_contents=test_input, + combine_star=True, + combine_as_imports=False, + keep_direct_and_as_imports=True, + force_single_line=False, + no_inline_sort=False, + ).output + assert test_output == "import a\nfrom a import *\n" + test_output = SortImports( + file_contents=test_input, + combine_star=True, + combine_as_imports=False, + keep_direct_and_as_imports=False, + force_single_line=True, + no_inline_sort=False, + ).output + assert test_output == "import a\nfrom a import *\n" + test_output = SortImports( + file_contents=test_input, + combine_star=True, + combine_as_imports=False, + keep_direct_and_as_imports=False, + force_single_line=False, + no_inline_sort=True, + ).output + assert test_output == "import a\nfrom a import *\n" + test_output = SortImports( + file_contents=test_input, + combine_star=False, + combine_as_imports=True, + keep_direct_and_as_imports=True, + force_single_line=False, + no_inline_sort=False, + ).output + assert test_output == ( + "import a\n" + "from a import *\n" + "from a import b, b as c, b as d, e as f, g as h, i as j, w, x, y, z\n" + ) + test_output = SortImports( + file_contents=test_input, + combine_star=False, + combine_as_imports=True, + keep_direct_and_as_imports=False, + force_single_line=True, + no_inline_sort=False, + ).output + assert test_output == ( + "import a\n" + "from a import *\n" + "from a import b as c\n" + "from a import b as d\n" + "from a import e as f\n" + "from a import g as h\n" + "from a import i as j\n" + "from a import w\n" + "from a import x\n" + "from a import y\n" + "from a import z\n" + ) + test_output = SortImports( + file_contents=test_input, + combine_star=False, + combine_as_imports=True, + keep_direct_and_as_imports=False, + force_single_line=False, + no_inline_sort=True, + ).output + assert test_output == ( + "import a\n" + "from a import *\n" + "from a import b as d, b as c, z, x, y, w, i as j, g as h, e as f\n" + ) + test_output = SortImports( + file_contents=test_input, + combine_star=False, + combine_as_imports=False, + keep_direct_and_as_imports=True, + force_single_line=True, + no_inline_sort=False, + ).output + assert test_output == ( + "import a\n" + "from a import *\n" + "from a import b\n" + "from a import b as c\n" + "from a import b as d\n" + "from a import e as f\n" + "from a import g as h\n" + "from a import i as j\n" + "from a import w\n" + "from a import x\n" + "from a import y\n" + "from a import z\n" + ) + test_output = SortImports( + file_contents=test_input, + combine_star=False, + combine_as_imports=False, + keep_direct_and_as_imports=True, + force_single_line=False, + no_inline_sort=True, + ).output + assert test_output == ( + "import a\n" + "from a import *\n" + "from a import b\n" + "from a import b as c\n" + "from a import b as d\n" + "from a import z, x, y, w\n" + "from a import i as j\n" + "from a import g as h\n" + "from a import e as f\n" + ) + test_output = SortImports( + file_contents=test_input, + combine_star=False, + combine_as_imports=False, + keep_direct_and_as_imports=False, + force_single_line=True, + no_inline_sort=True, + ).output + assert test_output == ( + "import a\n" + "from a import *\n" + "from a import b as c\n" + "from a import b as d\n" + "from a import e as f\n" + "from a import g as h\n" + "from a import i as j\n" + "from a import w\n" + "from a import x\n" + "from a import y\n" + "from a import z\n" + ) + test_output = SortImports( + file_contents=test_input, + combine_star=True, + combine_as_imports=True, + keep_direct_and_as_imports=True, + force_single_line=False, + no_inline_sort=False, + ).output + assert test_output == "import a\nfrom a import *\n" + test_output = SortImports( + file_contents=test_input, + combine_star=True, + combine_as_imports=True, + keep_direct_and_as_imports=False, + force_single_line=True, + no_inline_sort=False, + ).output + assert test_output == "import a\nfrom a import *\n" + test_output = SortImports( + file_contents=test_input, + combine_star=True, + combine_as_imports=True, + keep_direct_and_as_imports=False, + force_single_line=False, + no_inline_sort=True, + ).output + assert test_output == "import a\nfrom a import *\n" + test_output = SortImports( + file_contents=test_input, + combine_star=True, + combine_as_imports=False, + keep_direct_and_as_imports=True, + force_single_line=True, + no_inline_sort=False, + ).output + assert test_output == "import a\nfrom a import *\n" + test_output = SortImports( + file_contents=test_input, + combine_star=True, + combine_as_imports=False, + keep_direct_and_as_imports=True, + force_single_line=False, + no_inline_sort=True, + ).output + assert test_output == "import a\nfrom a import *\n" + test_output = SortImports( + file_contents=test_input, + combine_star=True, + combine_as_imports=False, + keep_direct_and_as_imports=False, + force_single_line=True, + no_inline_sort=True, + ).output + assert test_output == "import a\nfrom a import *\n" + test_output = SortImports( + file_contents=test_input, + combine_star=False, + combine_as_imports=True, + keep_direct_and_as_imports=True, + force_single_line=True, + no_inline_sort=False, + ).output + assert test_output == ( + "import a\n" + "from a import *\n" + "from a import b\n" + "from a import b as c\n" + "from a import b as d\n" + "from a import e as f\n" + "from a import g as h\n" + "from a import i as j\n" + "from a import w\n" + "from a import x\n" + "from a import y\n" + "from a import z\n" + ) + test_output = SortImports( + file_contents=test_input, + combine_star=False, + combine_as_imports=True, + keep_direct_and_as_imports=True, + force_single_line=False, + no_inline_sort=True, + ).output + assert test_output == ( + "import a\n" + "from a import *\n" + "from a import b, b as d, b as c, z, x, y, w, i as j, g as h, e as f\n" + ) + test_output = SortImports( + file_contents=test_input, + combine_star=False, + combine_as_imports=False, + keep_direct_and_as_imports=True, + force_single_line=True, + no_inline_sort=True, + ).output + assert test_output == ( + "import a\n" + "from a import *\n" + "from a import b\n" + "from a import b as c\n" + "from a import b as d\n" + "from a import e as f\n" + "from a import g as h\n" + "from a import i as j\n" + "from a import w\n" + "from a import x\n" + "from a import y\n" + "from a import z\n" + ) + test_output = SortImports( + file_contents=test_input, + combine_star=True, + combine_as_imports=True, + keep_direct_and_as_imports=True, + force_single_line=True, + no_inline_sort=False, + ).output + assert test_output == "import a\nfrom a import *\n" def test_noqa_issue_679() -> None: # Test to ensure that NOQA notation is being observed as expected - test_input = ('import os\n' - '\n' - 'import requestsss\n' - 'import zed # NOQA\n' - 'import ujson # NOQA\n' - '\n' - 'import foo') - test_output = ('import os\n' - '\n' - 'import foo\n' - 'import requestsss\n' - '\n' - 'import zed # NOQA\n' - 'import ujson # NOQA\n') + test_input = ( + "import os\n" + "\n" + "import requestsss\n" + "import zed # NOQA\n" + "import ujson # NOQA\n" + "\n" + "import foo" + ) + test_output = ( + "import os\n" + "\n" + "import foo\n" + "import requestsss\n" + "\n" + "import zed # NOQA\n" + "import ujson # NOQA\n" + ) assert SortImports(file_contents=test_input).output == test_output -def test_extract_multiline_output_wrap_setting_from_a_config_file(tmpdir: py.path.local) -> None: - editorconfig_contents = [ - 'root = true', - ' [*.py]', - 'multi_line_output = 5' - ] - config_file = tmpdir.join('.editorconfig') - config_file.write('\n'.join(editorconfig_contents)) +def test_extract_multiline_output_wrap_setting_from_a_config_file( + tmpdir: py.path.local +) -> None: + editorconfig_contents = ["root = true", " [*.py]", "multi_line_output = 5"] + config_file = tmpdir.join(".editorconfig") + config_file.write("\n".join(editorconfig_contents)) config = settings.from_path(str(tmpdir)) - assert config['multi_line_output'] == WrapModes.VERTICAL_GRID_GROUPED + assert config["multi_line_output"] == WrapModes.VERTICAL_GRID_GROUPED def test_ensure_support_for_non_typed_but_cased_alphabetic_sort_issue_890() -> None: - 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 + 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() -> None: - test_input = ('# comment\n' - 'import os\n' - '# comment2\n' - 'import sys\n') + 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') + 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 == '' + assert out == "" + assert err == "" def test_standard_library_deprecates_user_issue_778() -> None: - test_input = ('import os\n' - '\n' - 'import user\n') + test_input = "import os\n" "\n" "import user\n" assert SortImports(file_contents=test_input).output == test_input def test_settings_path_skip_issue_909(tmpdir): - base_dir = tmpdir.mkdir('project') - config_dir = base_dir.mkdir('conf') - config_dir.join('.isort.cfg').write('[isort]\n' - 'skip =\n' - ' file_to_be_skipped.py\n' - 'skip_glob =\n' - ' *glob_skip*\n') - - base_dir.join('file_glob_skip.py').write('import os\n' - '\n' - 'print("Hello World")\n' - '\n' - 'import sys\n') - base_dir.join('file_to_be_skipped.py').write('import os\n' - '\n' - 'print("Hello World")' - '\n' - 'import sys\n') + base_dir = tmpdir.mkdir("project") + config_dir = base_dir.mkdir("conf") + config_dir.join(".isort.cfg").write( + "[isort]\n" + "skip =\n" + " file_to_be_skipped.py\n" + "skip_glob =\n" + " *glob_skip*\n" + ) + + base_dir.join("file_glob_skip.py").write( + "import os\n" "\n" 'print("Hello World")\n' "\n" "import sys\n" + ) + base_dir.join("file_to_be_skipped.py").write( + "import os\n" "\n" 'print("Hello World")' "\n" "import sys\n" + ) test_run_directory = os.getcwd() os.chdir(str(base_dir)) - with pytest.raises(Exception): # without the settings path provided: the command should not skip & identify errors - subprocess.run(['isort', '--check-only'], check=True) + with pytest.raises( + Exception + ): # without the settings path provided: the command should not skip & identify errors + subprocess.run(["isort", "--check-only"], check=True) result = subprocess.run( - ['isort', '--check-only', '--settings-path=conf/.isort.cfg'], + ["isort", "--check-only", "--settings-path=conf/.isort.cfg"], stdout=subprocess.PIPE, - check=True + check=True, ) os.chdir(str(test_run_directory)) - assert b'skipped 2' in result.stdout.lower() + assert b"skipped 2" in result.stdout.lower() def test_skip_paths_issue_938(tmpdir): - base_dir = tmpdir.mkdir('project') - config_dir = base_dir.mkdir('conf') - config_dir.join('.isort.cfg').write('[isort]\n' - 'line_length = 88\n' - 'multi_line_output = 4\n' - 'lines_after_imports = 2\n' - 'skip_glob =\n' - ' migrations/**.py\n') - base_dir.join('dont_skip.py').write('import os\n' - '\n' - 'print("Hello World")' - '\n' - 'import sys\n') - - migrations_dir = base_dir.mkdir('migrations') - migrations_dir.join('file_glob_skip.py').write('import os\n' - '\n' - 'print("Hello World")\n' - '\n' - 'import sys\n') + base_dir = tmpdir.mkdir("project") + config_dir = base_dir.mkdir("conf") + config_dir.join(".isort.cfg").write( + "[isort]\n" + "line_length = 88\n" + "multi_line_output = 4\n" + "lines_after_imports = 2\n" + "skip_glob =\n" + " migrations/**.py\n" + ) + base_dir.join("dont_skip.py").write( + "import os\n" "\n" 'print("Hello World")' "\n" "import sys\n" + ) + + migrations_dir = base_dir.mkdir("migrations") + migrations_dir.join("file_glob_skip.py").write( + "import os\n" "\n" 'print("Hello World")\n' "\n" "import sys\n" + ) test_run_directory = os.getcwd() os.chdir(str(base_dir)) result = subprocess.run( - ['isort', 'dont_skip.py', 'migrations/file_glob_skip.py'], + ["isort", "dont_skip.py", "migrations/file_glob_skip.py"], stdout=subprocess.PIPE, check=True, ) os.chdir(str(test_run_directory)) - assert b'skipped' not in result.stdout.lower() + assert b"skipped" not in result.stdout.lower() os.chdir(str(base_dir)) result = subprocess.run( - ['isort', '--filter-files', '--settings-path=conf/.isort.cfg', 'dont_skip.py', 'migrations/file_glob_skip.py'], + [ + "isort", + "--filter-files", + "--settings-path=conf/.isort.cfg", + "dont_skip.py", + "migrations/file_glob_skip.py", + ], stdout=subprocess.PIPE, check=True, ) os.chdir(str(test_run_directory)) - assert b'skipped 1' in result.stdout.lower() + assert b"skipped 1" in result.stdout.lower() def test_failing_file_check_916() -> None: - test_input = ('#!/usr/bin/env python\n' - '# -*- coding: utf-8 -*-\n' - 'from __future__ import unicode_literals\n') - expected_output = ('#!/usr/bin/env python\n' - '# -*- coding: utf-8 -*-\n' - '# FUTURE\n' - 'from __future__ import unicode_literals\n') - settings = {'known_future_library': 'future', - 'import_heading_future': 'FUTURE', - 'sections': ['FUTURE', 'STDLIB', 'NORDIGEN', 'FIRSTPARTY', 'THIRDPARTY', 'LOCALFOLDER'], - 'indent': ' ', - 'multi_line_output': 3, - 'lines_after_imports': 2} # type: Dict[str, Any] + test_input = ( + "#!/usr/bin/env python\n" + "# -*- coding: utf-8 -*-\n" + "from __future__ import unicode_literals\n" + ) + expected_output = ( + "#!/usr/bin/env python\n" + "# -*- coding: utf-8 -*-\n" + "# FUTURE\n" + "from __future__ import unicode_literals\n" + ) + settings = { + "known_future_library": "future", + "import_heading_future": "FUTURE", + "sections": [ + "FUTURE", + "STDLIB", + "NORDIGEN", + "FIRSTPARTY", + "THIRDPARTY", + "LOCALFOLDER", + ], + "indent": " ", + "multi_line_output": 3, + "lines_after_imports": 2, + } # type: Dict[str, Any] assert SortImports(file_contents=test_input, **settings).output == expected_output - assert SortImports(file_contents=expected_output, **settings).output == expected_output - assert not SortImports(file_contents=expected_output, check=True, **settings).incorrectly_sorted + assert ( + SortImports(file_contents=expected_output, **settings).output == expected_output + ) + assert not SortImports( + file_contents=expected_output, check=True, **settings + ).incorrectly_sorted def test_import_heading_issue_905() -> None: - config = {'import_heading_stdlib': 'Standard library imports', - 'import_heading_thirdparty': 'Third party imports', - 'import_heading_firstparty': 'Local imports', - 'known_third_party': ['numpy'], - 'known_first_party': ['oklib']} # type: Dict[str, Any] - test_input = ('# Standard library imports\n' - 'import os.path as osp\n' - '\n' - '# Third party imports\n' - 'import numpy as np\n' - '\n' - '# Local imports\n' - 'from oklib.plot_ok import imagesc\n') + config = { + "import_heading_stdlib": "Standard library imports", + "import_heading_thirdparty": "Third party imports", + "import_heading_firstparty": "Local imports", + "known_third_party": ["numpy"], + "known_first_party": ["oklib"], + } # type: Dict[str, Any] + test_input = ( + "# Standard library imports\n" + "import os.path as osp\n" + "\n" + "# Third party imports\n" + "import numpy as np\n" + "\n" + "# Local imports\n" + "from oklib.plot_ok import imagesc\n" + ) assert SortImports(file_contents=test_input, **config).output == test_input def test_isort_keeps_comments_issue_691() -> None: - test_input = ('import os\n' - '# This will make sure the app is always imported when\n' - '# Django starts so that shared_task will use this app.\n' - 'from .celery import app as celery_app # noqa\n' - '\n' - 'PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))\n' - '\n' - 'def path(*subdirectories):\n' - ' return os.path.join(PROJECT_DIR, *subdirectories)\n') - expected_output = ('import os\n' - '\n' - '# This will make sure the app is always imported when\n' - '# Django starts so that shared_task will use this app.\n' - 'from .celery import app as celery_app # noqa\n' - '\n' - 'PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))\n' - '\n' - 'def path(*subdirectories):\n' - ' return os.path.join(PROJECT_DIR, *subdirectories)\n') + test_input = ( + "import os\n" + "# This will make sure the app is always imported when\n" + "# Django starts so that shared_task will use this app.\n" + "from .celery import app as celery_app # noqa\n" + "\n" + "PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))\n" + "\n" + "def path(*subdirectories):\n" + " return os.path.join(PROJECT_DIR, *subdirectories)\n" + ) + expected_output = ( + "import os\n" + "\n" + "# This will make sure the app is always imported when\n" + "# Django starts so that shared_task will use this app.\n" + "from .celery import app as celery_app # noqa\n" + "\n" + "PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))\n" + "\n" + "def path(*subdirectories):\n" + " return os.path.join(PROJECT_DIR, *subdirectories)\n" + ) assert SortImports(file_contents=test_input).output == expected_output def test_pyi_formatting_issue_942(tmpdir): - test_input = ('import os\n' - '\n' - '\n' - 'def my_method():\n') + test_input = "import os\n" "\n" "\n" "def my_method():\n" expected_py_output = test_input.splitlines() - expected_pyi_output = ('import os\n' - '\n' - 'def my_method():\n').splitlines() - assert SortImports(file_contents=test_input).output.splitlines() == expected_py_output - assert SortImports(file_contents=test_input, - extension="pyi").output.splitlines() == expected_pyi_output - - source_py = tmpdir.join('source.py') + expected_pyi_output = ("import os\n" "\n" "def my_method():\n").splitlines() + assert ( + SortImports(file_contents=test_input).output.splitlines() == expected_py_output + ) + assert ( + SortImports(file_contents=test_input, extension="pyi").output.splitlines() + == expected_pyi_output + ) + + source_py = tmpdir.join("source.py") source_py.write(test_input) - assert SortImports(file_path=str(source_py)).output.splitlines() == expected_py_output + assert ( + SortImports(file_path=str(source_py)).output.splitlines() == expected_py_output + ) - source_pyi = tmpdir.join('source.pyi') + source_pyi = tmpdir.join("source.pyi") source_pyi.write(test_input) - assert SortImports(file_path=str(source_pyi)).output.splitlines() == expected_pyi_output + assert ( + SortImports(file_path=str(source_pyi)).output.splitlines() + == expected_pyi_output + ) def test_python_version() -> None: from isort.main import parse_args # test that the py_version can be added as flag - args = parse_args(['-py=2.7']) + args = parse_args(["-py=2.7"]) assert args["py_version"] == "2.7" - args = parse_args(['--python-version=3']) + args = parse_args(["--python-version=3"]) assert args["py_version"] == "3" - test_input = ('import os\n' - '\n' - 'import user\n') + test_input = "import os\n" "\n" "import user\n" assert SortImports(file_contents=test_input, py_version="3").output == test_input # user is part of the standard library in python 2 - output_python_2 = ('import os\n' - 'import user\n') - assert SortImports(file_contents=test_input, py_version="2.7").output == output_python_2 + output_python_2 = "import os\n" "import user\n" + assert ( + SortImports(file_contents=test_input, py_version="2.7").output + == output_python_2 + ) - test_input = ('import os\nimport xml') + test_input = "import os\nimport xml" print(SortImports(file_contents=test_input, py_version="all").output) diff --git a/tox.ini b/tox.ini index dcac1ddef..4246d9d50 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,7 @@ [tox] minversion = 2.4 envlist = + black isort-check, lint, mypy, @@ -20,6 +21,10 @@ setenv = coverage: PYTEST_ADDOPTS=--cov {env:PYTEST_ADDOPTS:} commands = pytest {posargs} +[testenv:black] +deps = black +commands = black --check --diff . + [testenv:isort-check] basepython = python3 commands = python setup.py isort From 538864097a355b4c7f5fbdfe537a5165633850b0 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Wed, 14 Aug 2019 18:23:31 -0700 Subject: [PATCH 0033/1439] Correct typing of RequirementsFinder _get_files_from_dir() should return an iterator, so use 'yield from' syntax rather than returning a list. Add type hints to _get_files_from_dir_cached(). It returns a list. --- isort/finders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/finders.py b/isort/finders.py index 51c8d83ee..326028412 100644 --- a/isort/finders.py +++ b/isort/finders.py @@ -309,11 +309,11 @@ class RequirementsFinder(ReqsBaseFinder): def _get_files_from_dir(self, path: str) -> Iterator[str]: """Return paths to requirements files from passed dir. """ - return self._get_files_from_dir_cached(path) + yield from self._get_files_from_dir_cached(path) @classmethod @lru_cache(maxsize=16) - def _get_files_from_dir_cached(cls, path): + def _get_files_from_dir_cached(cls, path: str) -> List[str]: results = [] for fname in os.listdir(path): From dca0a3ccad2f96da8b1ddbc72f72cd4da8ce5c17 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Thu, 15 Aug 2019 05:07:15 -0700 Subject: [PATCH 0034/1439] Remove unnecessary before_install.sh script Only used on macOS and not required to successfully run tests, so remove it. Instead, rely on the environment created by Travis CI. --- .travis.yml | 2 -- scripts/before_install.sh | 28 ---------------------------- 2 files changed, 30 deletions(-) delete mode 100755 scripts/before_install.sh diff --git a/.travis.yml b/.travis.yml index fd4a56696..f34d023d1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,8 +23,6 @@ matrix: language: generic env: TOXENV=py36 -before_install: -- "./scripts/before_install.sh" install: - pip install tox script: diff --git a/scripts/before_install.sh b/scripts/before_install.sh deleted file mode 100755 index fa86a7703..000000000 --- a/scripts/before_install.sh +++ /dev/null @@ -1,28 +0,0 @@ -#! /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 From 4ca3bcbab186746bed014fe9764f59734b8eaa37 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Thu, 15 Aug 2019 04:57:22 -0700 Subject: [PATCH 0035/1439] Apply isort test_isort.py --- test_isort.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test_isort.py b/test_isort.py index 82bd2d127..4312e0e88 100644 --- a/test_isort.py +++ b/test_isort.py @@ -32,9 +32,8 @@ import py import pytest - from isort import finders, main, settings -from isort.main import is_python_file, SortImports +from isort.main import SortImports, is_python_file from isort.settings import WrapModes from isort.utils import exists_case_sensitive From 55f637caa00fdec2e4b530ff9c0dbf3de22c30f1 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Thu, 15 Aug 2019 05:00:47 -0700 Subject: [PATCH 0036/1439] Run pyupgrade tool across the project Helps migrate old Python code to modern standards. Ran with the --py3-plus flag for to enable all available upgrade options. For details on pyupgrade, see: https://github.com/asottile/pyupgrade --- isort/compat.py | 10 +++--- isort/finders.py | 24 ++++++-------- isort/format.py | 6 ++-- isort/isort.py | 82 +++++++++++++++++++++++------------------------- isort/main.py | 22 ++++++------- test_isort.py | 47 +++++++++++++-------------- 6 files changed, 91 insertions(+), 100 deletions(-) diff --git a/isort/compat.py b/isort/compat.py index a82a65ba4..0af5d44aa 100644 --- a/isort/compat.py +++ b/isort/compat.py @@ -64,7 +64,7 @@ def get_settings_path( return Path.cwd() -class SortImports(object): +class SortImports: incorrectly_sorted = False skipped = False @@ -110,7 +110,7 @@ def __init__( self.skipped = True if self.config["verbose"]: print( - "WARNING: {0} was skipped as it's listed in 'skip' setting" + "WARNING: {} was skipped as it's listed in 'skip' setting" " or matches a glob in 'skip_glob' setting".format( absolute_file_path ) @@ -174,13 +174,13 @@ def __init__( in_lines_without_top_comment, logging_file_path, "exec", 0, 1 ) print( - "ERROR: {0} isort would have introduced syntax errors, please report to the project!".format( + "ERROR: {} isort would have introduced syntax errors, please report to the project!".format( logging_file_path ) ) except SyntaxError: print( - "ERROR: {0} File contains syntax errors.".format( + "ERROR: {} File contains syntax errors.".format( logging_file_path ) ) @@ -233,7 +233,7 @@ def __init__( "w", encoding=file_encoding, newline="" ) as output_file: if not self.config["quiet"]: - print("Fixing {0}".format(self.file_path)) + print("Fixing {}".format(self.file_path)) output_file.write(self.output) diff --git a/isort/finders.py b/isort/finders.py index 326028412..028bf65c5 100644 --- a/isort/finders.py +++ b/isort/finders.py @@ -86,7 +86,7 @@ def __init__(self, config: Mapping[str, Any], sections: Any) -> None: self.known_patterns = [] # type: List[Tuple[Pattern[str], str]] for placement in reversed(self.sections): known_placement = KNOWN_SECTION_MAPPING.get(placement, placement) - config_key = "known_{0}".format(known_placement.lower()) + config_key = "known_{}".format(known_placement.lower()) known_patterns = self.config.get(config_key, []) known_patterns = [ pattern @@ -140,16 +140,14 @@ def __init__(self, config: Mapping[str, Any], sections: Any) -> None: self.virtual_env = os.path.realpath(self.virtual_env) self.virtual_env_src = "" if self.virtual_env: - self.virtual_env_src = "{0}/src/".format(self.virtual_env) - for path in glob("{0}/lib/python*/site-packages".format(self.virtual_env)): + self.virtual_env_src = "{}/src/".format(self.virtual_env) + for path in glob("{}/lib/python*/site-packages".format(self.virtual_env)): if path not in self.paths: self.paths.append(path) - for path in glob( - "{0}/lib/python*/*/site-packages".format(self.virtual_env) - ): + for path in glob("{}/lib/python*/*/site-packages".format(self.virtual_env)): if path not in self.paths: self.paths.append(path) - for path in glob("{0}/src/*".format(self.virtual_env)): + for path in glob("{}/src/*".format(self.virtual_env)): if os.path.isdir(path): self.paths.append(path) @@ -159,10 +157,10 @@ def __init__(self, config: Mapping[str, Any], sections: Any) -> None: ) 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)): + for path in glob("{}/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)): + for path in glob("{}/lib/python*/*/site-packages".format(self.conda_env)): if path not in self.paths: self.paths.append(path) @@ -271,8 +269,7 @@ def _get_files(self) -> Iterator[str]: path = os.path.dirname(path) for path in self._get_parents(path): - for file_path in self._get_files_from_dir(path): - yield file_path + yield from self._get_files_from_dir(path) def _normalize_name(self, name: str) -> str: """Convert package name to module name @@ -341,8 +338,7 @@ def _get_files_from_dir_cached(cls, path: str) -> List[str]: def _get_names(self, path: str) -> Iterator[str]: """Load required packages from path to requirements file """ - for i in self._get_names_cached(path): - yield i + yield from self._get_names_cached(path) @classmethod @lru_cache(maxsize=16) @@ -377,7 +373,7 @@ def find(self, module_name: str) -> Optional[str]: return self.config["default_section"] -class FindersManager(object): +class FindersManager: _default_finders_classes = ( ForcedSeparateFinder, LocalFinder, diff --git a/isort/format.py b/isort/format.py index 018bd102a..b65059014 100644 --- a/isort/format.py +++ b/isort/format.py @@ -20,10 +20,10 @@ def format_natural(import_line: str) -> str: import_line = import_line.strip() if not import_line.startswith("from ") and not import_line.startswith("import "): if "." not in import_line: - return "import {0}".format(import_line) + return "import {}".format(import_line) parts = import_line.split(".") end = parts.pop(-1) - return "from {0} import {1}".format(".".join(parts), end) + return "from {} import {}".format(".".join(parts), end) return import_line @@ -54,7 +54,7 @@ def ask_whether_to_apply_changes_to_file(file_path: str) -> bool: answer = None while answer not in ("yes", "y", "no", "n", "quit", "q"): answer = input( - "Apply suggested changes to '{0}' [y/n/q]? ".format(file_path) + "Apply suggested changes to '{}' [y/n/q]? ".format(file_path) ).lower() if answer in ("no", "n"): return False diff --git a/isort/isort.py b/isort/isort.py index 070e1e82c..d6084ba00 100644 --- a/isort/isort.py +++ b/isort/isort.py @@ -65,7 +65,7 @@ ) -class _SortImports(object): +class _SortImports: def __init__( self, file_contents: str, config: Dict[str, Any], extension: str = "py" ) -> None: @@ -148,10 +148,10 @@ def check_if_input_already_sorted( ) -> bool: if output.strip() == check_against.strip(): if self.config["verbose"]: - print("SUCCESS: {0} Everything Looks Good!".format(logging_file_path)) + print("SUCCESS: {} Everything Looks Good!".format(logging_file_path)) return True - print("ERROR: {0} Imports are incorrectly sorted.".format(logging_file_path)) + print("ERROR: {} Imports are incorrectly sorted.".format(logging_file_path)) return False def determine_line_separator(self, file_contents: str) -> str: @@ -232,7 +232,7 @@ def _module_key( length_sort = config["length_sort"] else: length_sort = config["length_sort_" + str(section_name).lower()] - return "{0}{1}{2}".format( + return "{}{}{}".format( module_name in config["force_to_top"] and "A" or "B", prefix, length_sort and (str(len(module_name)) + ":" + module_name) or module_name, @@ -250,7 +250,7 @@ def _add_comments( if not comments: return original_string else: - return "{0}{1} {2}".format( + return "{}{} {}".format( self._strip_comments(original_string)[0], self.config["comment_prefix"], "; ".join(comments), @@ -273,7 +273,7 @@ def _wrap(self, line: str) -> str: ) and not line_without_comment.strip().startswith(splitter): line_parts = re.split(exp, line_without_comment) if comment: - line_parts[-1] = "{0}#{1}".format(line_parts[-1], comment) + line_parts[-1] = "{}#{}".format(line_parts[-1], comment) next_line = [] while (len(line) + 2) > ( self.config["wrap_length"] or self.config["line_length"] @@ -288,11 +288,9 @@ def _wrap(self, line: str) -> str: ) if self.config["use_parentheses"]: if splitter == "as ": - output = "{0}{1}{2}".format( - line, splitter, cont_line.lstrip() - ) + output = "{}{}{}".format(line, splitter, cont_line.lstrip()) else: - output = "{0}{1}({2}{3}{4}{5})".format( + output = "{}{}({}{}{}{})".format( line, splitter, self.line_separator, @@ -320,7 +318,7 @@ def _wrap(self, line: str) -> str: + comment[:-1] ) return self.line_separator.join(lines) - return "{0}{1}\\{2}{3}".format( + return "{}{}\\{}{}".format( line, splitter, self.line_separator, cont_line ) elif ( @@ -328,7 +326,7 @@ def _wrap(self, line: str) -> str: and wrap_mode == settings.WrapModes.NOQA ): if "# NOQA" not in line: - return "{0}{1} NOQA".format(line, self.config["comment_prefix"]) + return "{}{} NOQA".format(line, self.config["comment_prefix"]) return line @@ -345,13 +343,13 @@ def _add_straight_imports( self.config["keep_direct_and_as_imports"] and self.imports[section]["straight"][module] ): - import_definition.append("import {0}".format(module)) + import_definition.append("import {}".format(module)) import_definition.extend( - "import {0} as {1}".format(module, as_import) + "import {} as {}".format(module, as_import) for as_import in self.as_map[module] ) else: - import_definition.append("import {0}".format(module)) + import_definition.append("import {}".format(module)) comments_above = self.comments["above"]["straight"].pop(module, None) if comments_above: @@ -372,7 +370,7 @@ def _add_from_imports( if module in self.remove_imports: continue - import_start = "from {0} import ".format(module) + import_start = "from {} import ".format(module) from_imports = list(self.imports[section]["from"][module]) if not self.config["no_inline_sort"] or self.config["force_single_line"]: from_imports = nsorted( @@ -385,15 +383,15 @@ def _add_from_imports( from_imports = [ line for line in from_imports - if not "{0}.{1}".format(module, line) in self.remove_imports + if not "{}.{}".format(module, line) in self.remove_imports ] sub_modules = [ - "{0}.{1}".format(module, from_import) for from_import in from_imports + "{}.{}".format(module, from_import) for from_import in from_imports ] as_imports = { from_import: [ - "{0} as {1}".format(from_import, as_module) + "{} as {}".format(from_import, as_module) for as_module in self.as_map[sub_module] ] for from_import, sub_module in zip(from_imports, sub_modules) @@ -422,7 +420,7 @@ def _add_from_imports( comments = self.comments["from"].pop(module, ()) if "*" in from_imports and self.config["combine_star"]: import_statement = self._wrap( - self._add_comments(comments, "{0}*".format(import_start)) + self._add_comments(comments, "{}*".format(import_start)) ) from_imports = None elif self.config["force_single_line"]: @@ -438,7 +436,7 @@ def _add_from_imports( .pop(from_import, None) ) if comment: - single_import_line += "{0} {1}".format( + single_import_line += "{} {}".format( comments and ";" or self.config["comment_prefix"], comment, ) @@ -494,7 +492,7 @@ def _add_from_imports( star_import = False if "*" in from_imports: section_output.append( - self._add_comments(comments, "{0}*".format(import_start)) + self._add_comments(comments, "{}*".format(import_start)) ) from_imports.remove("*") star_import = True @@ -515,7 +513,7 @@ def _add_from_imports( single_import_line = self._add_comments( comments, import_start + from_import ) - single_import_line += "{0} {1}".format( + single_import_line += "{} {}".format( comments and ";" or self.config["comment_prefix"], comment, ) @@ -724,7 +722,7 @@ def by_module(line: str) -> str: section = "A" if not self.config["order_by_type"]: line = line.lower() - return "{0}{1}".format(section, line) + return "{}{}".format(section, line) section_output = nsorted(section_output, key=by_module) @@ -740,7 +738,7 @@ def by_module(line: str) -> str: "import_heading_" + str(section_name).lower(), "" ) if section_title: - section_comment = "# {0}".format(section_title) + section_comment = "# {}".format(section_title) if ( section_comment not in self.out_lines[0:1] and section_comment not in self.in_lines[0:1] @@ -849,17 +847,17 @@ def _output_grid( comments, statement + ", " + next_import ) if len(next_statement.split(self.line_separator)[-1]) + 1 > line_length: - lines = ["{0}{1}".format(white_space, next_import.split(" ")[0])] + lines = ["{}{}".format(white_space, next_import.split(" ")[0])] for part in next_import.split(" ")[1:]: - new_line = "{0} {1}".format(lines[-1], part) + new_line = "{} {}".format(lines[-1], part) if len(new_line) + 1 > line_length: - lines.append("{0}{1}".format(white_space, part)) + lines.append("{}{}".format(white_space, part)) else: lines[-1] = new_line next_import = self.line_separator.join(lines) statement = self._add_comments( - comments, "{0},".format(statement) - ) + "{0}{1}".format(self.line_separator, next_import) + comments, "{},".format(statement) + ) + "{}{}".format(self.line_separator, next_import) comments = None else: statement += ", " + next_import @@ -879,7 +877,7 @@ def _output_vertical( + self.line_separator + white_space ) - return "{0}({1}{2}{3})".format( + return "{}({}{}{})".format( statement, first_import, ("," + self.line_separator + white_space).join(imports), @@ -903,8 +901,8 @@ def _output_hanging_indent( ) if len(next_statement.split(self.line_separator)[-1]) + 3 > line_length: next_statement = self._add_comments( - comments, "{0}, \\".format(statement) - ) + "{0}{1}{2}".format(self.line_separator, indent, next_import) + comments, "{}, \\".format(statement) + ) + "{}{}{}".format(self.line_separator, indent, next_import) comments = None statement = next_statement return statement @@ -945,14 +943,14 @@ def _output_vertical_grid_common( ) while imports: next_import = imports.pop(0) - next_statement = "{0}, {1}".format(statement, next_import) + next_statement = "{}, {}".format(statement, next_import) current_line_length = len(next_statement.split(self.line_separator)[-1]) if imports or need_trailing_char: # If we have more imports we need to account for a comma after this import # We might also need to account for a closing ) we're going to add. current_line_length += 1 if current_line_length > line_length: - next_statement = "{0},{1}{2}{3}".format( + next_statement = "{},{}{}{}".format( statement, self.line_separator, indent, next_import ) statement = next_statement @@ -1019,14 +1017,14 @@ def _output_noqa( line_length: int, comments: Sequence[str], ) -> str: - retval = "{0}{1}".format(statement, ", ".join(imports)) + retval = "{}{}".format(statement, ", ".join(imports)) comment_str = " ".join(comments) if comments: if ( len(retval) + len(self.config["comment_prefix"]) + 1 + len(comment_str) <= line_length ): - return "{0}{1} {2}".format( + return "{}{} {}".format( retval, self.config["comment_prefix"], comment_str ) else: @@ -1034,15 +1032,15 @@ def _output_noqa( return retval if comments: if "NOQA" in comments: - return "{0}{1} {2}".format( + return "{}{} {}".format( retval, self.config["comment_prefix"], comment_str ) else: - return "{0}{1} NOQA {2}".format( + return "{}{} NOQA {}".format( retval, self.config["comment_prefix"], comment_str ) else: - return "{0}{1} NOQA".format(retval, self.config["comment_prefix"]) + return "{}{} NOQA".format(retval, self.config["comment_prefix"]) @staticmethod def _strip_comments( @@ -1278,7 +1276,7 @@ def _parse(self) -> None: ) if placed_module == "": print( - "WARNING: could not place module {0} of line {1} --" + "WARNING: could not place module {} of line {} --" " Do you need to define a default section?".format( import_from, line ) @@ -1384,7 +1382,7 @@ def _parse(self) -> None: ) if placed_module == "": print( - "WARNING: could not place module {0} of line {1} --" + "WARNING: could not place module {} of line {} --" " Do you need to define a default section?".format( import_from, line ) diff --git a/isort/main.py b/isort/main.py index ffbf914ef..128c02bd7 100644 --- a/isort/main.py +++ b/isort/main.py @@ -63,7 +63,7 @@ isort your Python imports for you so you don't have to - VERSION {0} + VERSION {} \########################################################################/ """.format( @@ -87,13 +87,13 @@ def is_python_file(path: str) -> bool: try: with open(path, "rb") as fp: line = fp.readline(100) - except IOError: + except OSError: return False else: return bool(shebang_re.match(line)) -class SortAttempt(object): +class SortAttempt: def __init__(self, incorrectly_sorted: bool, skipped: bool) -> None: self.incorrectly_sorted = incorrectly_sorted self.skipped = skipped @@ -103,8 +103,8 @@ def sort_imports(file_name: str, **arguments: Any) -> Optional[SortAttempt]: try: result = SortImports(file_name, **arguments) return SortAttempt(result.incorrectly_sorted, result.skipped) - except IOError as e: - print("WARNING: Unable to parse file {0} due to {1}".format(file_name, e)) + except OSError as e: + print("WARNING: Unable to parse file {} due to {}".format(file_name, e)) return None @@ -187,9 +187,9 @@ def run(self) -> None: ).incorrectly_sorted if incorrectly_sorted: wrong_sorted_files = True - except IOError as e: + except OSError as e: print( - "WARNING: Unable to parse file {0} due to {1}".format( + "WARNING: Unable to parse file {} due to {}".format( python_file, e ) ) @@ -638,7 +638,7 @@ def main(argv: Optional[Sequence[str]] = None) -> None: ) if not os.path.isdir(arguments["settings_path"]): print( - "WARNING: settings_path dir does not exist: {0}".format( + "WARNING: settings_path dir does not exist: {}".format( arguments["settings_path"] ) ) @@ -648,7 +648,7 @@ def main(argv: Optional[Sequence[str]] = None) -> None: arguments["virtual_env"] = os.path.abspath(venv) if not os.path.isdir(arguments["virtual_env"]): print( - "WARNING: virtual_env dir does not exist: {0}".format( + "WARNING: virtual_env dir does not exist: {}".format( arguments["virtual_env"] ) ) @@ -718,10 +718,10 @@ def main(argv: Optional[Sequence[str]] = None) -> None: if config["verbose"]: for was_skipped in skipped: print( - "WARNING: {0} was skipped as it's listed in 'skip' setting" + "WARNING: {} was skipped as it's listed in 'skip' setting" " or matches a glob in 'skip_glob' setting".format(was_skipped) ) - print("Skipped {0} files".format(num_skipped)) + print("Skipped {} files".format(num_skipped)) if __name__ == "__main__": diff --git a/test_isort.py b/test_isort.py index 4312e0e88..f75b2c166 100644 --- a/test_isort.py +++ b/test_isort.py @@ -20,7 +20,6 @@ OTHER DEALINGS IN THE SOFTWARE. """ -import io import os import os.path import posixpath @@ -2037,8 +2036,8 @@ def test_import_split_is_word_boundary_aware() -> None: def test_other_file_encodings(tmpdir): """Test to ensure file encoding is respected""" for encoding in ("latin1", "utf8"): - tmp_fname = tmpdir.join("test_{0}.py".format(encoding)) - file_contents = "# coding: {0}\n\ns = u'ã'\n".format(encoding) + tmp_fname = tmpdir.join("test_{}.py".format(encoding)) + file_contents = "# coding: {}\n\ns = u'ã'\n".format(encoding) tmp_fname.write_binary(file_contents.encode(encoding)) assert ( SortImports(file_path=str(tmp_fname), settings_path=os.getcwd()).output @@ -3067,13 +3066,13 @@ def test_new_lines_are_preserved() -> None: pass try: - with io.open(rn_newline.name, mode="w", newline="") as rn_newline_input: + with open(rn_newline.name, mode="w", newline="") as rn_newline_input: rn_newline_input.write("import sys\r\nimport os\r\n") SortImports(rn_newline.name, settings_path=os.getcwd()) - with io.open(rn_newline.name) as new_line_file: + with open(rn_newline.name) as new_line_file: print(new_line_file.read()) - with io.open(rn_newline.name, newline="") as rn_newline_file: + with open(rn_newline.name, newline="") as rn_newline_file: rn_newline_contents = rn_newline_file.read() assert rn_newline_contents == "import os\r\nimport sys\r\n" finally: @@ -3083,11 +3082,11 @@ def test_new_lines_are_preserved() -> None: pass try: - with io.open(r_newline.name, mode="w", newline="") as r_newline_input: + with open(r_newline.name, mode="w", newline="") as r_newline_input: r_newline_input.write("import sys\rimport os\r") SortImports(r_newline.name, settings_path=os.getcwd()) - with io.open(r_newline.name, newline="") as r_newline_file: + with open(r_newline.name, newline="") as r_newline_file: r_newline_contents = r_newline_file.read() assert r_newline_contents == "import os\rimport sys\r" finally: @@ -3097,11 +3096,11 @@ def test_new_lines_are_preserved() -> None: pass try: - with io.open(n_newline.name, mode="w", newline="") as n_newline_input: + with open(n_newline.name, mode="w", newline="") as n_newline_input: n_newline_input.write("import sys\nimport os\n") SortImports(n_newline.name, settings_path=os.getcwd()) - with io.open(n_newline.name, newline="") as n_newline_file: + with open(n_newline.name, newline="") as n_newline_file: n_newline_contents = n_newline_file.read() assert n_newline_contents == "import os\nimport sys\n" finally: @@ -3228,15 +3227,13 @@ def test_path_finder(monkeypatch): finder = finders.PathFinder(config=si.config, sections=si.sections) third_party_prefix = next(path for path in finder.paths if "site-packages" in path) ext_suffix = sysconfig.get_config_var("EXT_SUFFIX") or ".so" - imaginary_paths = set( - [ - posixpath.join(finder.stdlib_lib_prefix, "example_1.py"), - posixpath.join(third_party_prefix, "example_2.py"), - posixpath.join(third_party_prefix, "example_3.so"), - posixpath.join(third_party_prefix, "example_4" + ext_suffix), - posixpath.join(os.getcwd(), "example_5.py"), - ] - ) + imaginary_paths = { + posixpath.join(finder.stdlib_lib_prefix, "example_1.py"), + posixpath.join(third_party_prefix, "example_2.py"), + posixpath.join(third_party_prefix, "example_3.so"), + posixpath.join(third_party_prefix, "example_4" + ext_suffix), + posixpath.join(os.getcwd(), "example_5.py"), + } monkeypatch.setattr( "isort.finders.exists_case_sensitive", lambda p: p in imaginary_paths ) @@ -3319,10 +3316,10 @@ def test_safety_excludes(tmpdir, enabled): main.iter_source_code(codes, config, skipped) # if enabled files within nested unsafe directories should be skipped - file_names = set( + file_names = { 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) == 3 @@ -3336,10 +3333,10 @@ def test_safety_excludes(tmpdir, enabled): assert not skipped # directly pointing to files within unsafe directories shouldn't skip them either way - file_names = set( + file_names = { os.path.relpath(f, str(toxdir)) for f in main.iter_source_code([str(toxdir)], config, skipped) - ) + } assert file_names == {"verysafe.py"} @@ -3359,10 +3356,10 @@ def test_skip_glob(tmpdir, skip_glob_assert): config = dict(settings.default.copy(), skip_glob=skip_glob) skipped = [] # type: List[str] - file_names = set( + file_names = { 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 From e5dede347417134cb1cf117083bc20dd9794ba19 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Thu, 15 Aug 2019 04:00:33 -0700 Subject: [PATCH 0037/1439] Add flake8-bugbear to tox.ini lint target flake8-bugbear is a plugin for flake8 that can find likely bugs and design problems. It contains warnings that don't belong in pyflakes and pycodestyle. Introduced as this tool is recommended by HOPE-8. --- test_isort.py | 4 ++-- tox.ini | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/test_isort.py b/test_isort.py index 4312e0e88..dbb28d050 100644 --- a/test_isort.py +++ b/test_isort.py @@ -2343,7 +2343,7 @@ def test_no_additional_lines_issue_358() -> None: ).output assert test_output == expected_output - for attempt in range(5): + for _attempt in range(5): test_output = SortImports( file_contents=test_output, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, @@ -2387,7 +2387,7 @@ def test_no_additional_lines_issue_358() -> None: ).output assert test_output == expected_output - for attempt in range(5): + for _attempt in range(5): test_output = SortImports( file_contents=test_output, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, diff --git a/tox.ini b/tox.ini index 4246d9d50..5b765bcb3 100644 --- a/tox.ini +++ b/tox.ini @@ -30,7 +30,9 @@ basepython = python3 commands = python setup.py isort [testenv:lint] -deps = flake8 +deps = + flake8 + flake8-bugbear commands = flake8 skip_install = True From 8d6a106ff464046564043640435921a4d2c9dcac Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sat, 17 Aug 2019 11:19:32 -0700 Subject: [PATCH 0038/1439] Add several missing type annotations --- isort/hooks.py | 2 +- test_isort.py | 60 +++++++++++++++++++++++++------------------------- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/isort/hooks.py b/isort/hooks.py index c2e59b30f..621fb0d23 100644 --- a/isort/hooks.py +++ b/isort/hooks.py @@ -50,7 +50,7 @@ def get_lines(command: List[str]) -> List[str]: return [line.strip() for line in stdout.splitlines()] -def git_hook(strict=False, modify=False): +def git_hook(strict: bool = False, modify: bool = False) -> int: """ Git pre-commit hook to check staged files for isort errors diff --git a/test_isort.py b/test_isort.py index 3d87964f1..b0e918b14 100644 --- a/test_isort.py +++ b/test_isort.py @@ -27,7 +27,7 @@ import sys import sysconfig from tempfile import NamedTemporaryFile -from typing import Any, Dict, List +from typing import Any, Dict, Iterator, List, Set, Tuple import py import pytest @@ -68,7 +68,7 @@ @pytest.fixture(scope="session", autouse=True) -def default_settings_path(tmpdir_factory): +def default_settings_path(tmpdir_factory) -> Iterator[str]: config_dir = tmpdir_factory.mktemp("config") config_file = config_dir.join(".editorconfig").strpath with open(config_file, "w") as editorconfig: @@ -876,7 +876,7 @@ def test_quotes_in_file() -> None: assert SortImports(file_contents=test_input).output == test_input -def test_check_newline_in_imports(capsys): +def test_check_newline_in_imports(capsys) -> None: """Ensure tests works correctly when new lines are in imports.""" test_input = "from lib1 import (\n" " sub1,\n" " sub2,\n" " sub3\n)\n" @@ -2033,7 +2033,7 @@ def test_import_split_is_word_boundary_aware() -> None: ) -def test_other_file_encodings(tmpdir): +def test_other_file_encodings(tmpdir) -> None: """Test to ensure file encoding is respected""" for encoding in ("latin1", "utf8"): tmp_fname = tmpdir.join("test_{}.py".format(encoding)) @@ -2138,7 +2138,7 @@ def test_shouldnt_add_lines() -> None: assert SortImports(file_contents=test_input).output == test_input -def test_sections_parsed_correct(tmpdir): +def test_sections_parsed_correct(tmpdir) -> None: """Ensure that modules for custom sections parsed as list from config file and isort result is correct""" conf_file_data = ( "[settings]\n" @@ -2167,7 +2167,7 @@ def test_sections_parsed_correct(tmpdir): @pytest.mark.skipif(toml is None, reason="Requires toml package to be installed.") -def test_pyproject_conf_file(tmpdir): +def test_pyproject_conf_file(tmpdir) -> None: """Ensure that modules for custom sections parsed as list from config file and isort result is correct""" conf_file_data = ( "[build-system]\n" @@ -2463,21 +2463,21 @@ def test_third_party_case_sensitive() -> None: assert SortImports(file_contents=test_input).output == expected_output -def test_exists_case_sensitive_file(tmpdir): +def test_exists_case_sensitive_file(tmpdir) -> None: """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): +def test_exists_case_sensitive_directory(tmpdir) -> None: """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"))) -def test_sys_path_mutation(tmpdir): +def test_sys_path_mutation(tmpdir) -> None: """Test to ensure sys.path is not modified""" tmpdir.mkdir("src").mkdir("a") test_input = "from myproject import test" @@ -2637,21 +2637,21 @@ def test_long_alias_using_paren_issue_957() -> None: assert out == expected_output -def test_strict_whitespace_by_default(capsys): +def test_strict_whitespace_by_default(capsys) -> None: test_input = "import os\n" "from django.conf import settings\n" SortImports(file_contents=test_input, check=True) out, err = capsys.readouterr() assert out == "ERROR: Imports are incorrectly sorted.\n" -def test_strict_whitespace_no_closing_newline_issue_676(capsys): +def test_strict_whitespace_no_closing_newline_issue_676(capsys) -> None: test_input = "import os\n" "\n" "from django.conf import settings\n" "\n" "print(1)" SortImports(file_contents=test_input, check=True) out, err = capsys.readouterr() assert out == "" -def test_ignore_whitespace(capsys): +def test_ignore_whitespace(capsys) -> None: test_input = "import os\n" "from django.conf import settings\n" SortImports(file_contents=test_input, check=True, ignore_whitespace=True) out, err = capsys.readouterr() @@ -2984,24 +2984,24 @@ def test_escaped_parens_sort() -> None: assert SortImports(file_contents=test_input).output == expected -def test_is_python_file_ioerror(tmpdir): +def test_is_python_file_ioerror(tmpdir) -> None: does_not_exist = tmpdir.join("fake.txt") assert is_python_file(str(does_not_exist)) is False -def test_is_python_file_shebang(tmpdir): +def test_is_python_file_shebang(tmpdir) -> None: path = tmpdir.join("myscript") path.write("#!/usr/bin/env python\n") assert is_python_file(str(path)) is True -def test_is_python_file_editor_backup(tmpdir): +def test_is_python_file_editor_backup(tmpdir) -> None: path = tmpdir.join("myscript~") path.write("#!/usr/bin/env python\n") assert is_python_file(str(path)) is False -def test_is_python_typing_stub(tmpdir): +def test_is_python_typing_stub(tmpdir) -> None: stub = tmpdir.join("stub.pyi") assert is_python_file(str(stub)) is True @@ -3107,7 +3107,7 @@ def test_new_lines_are_preserved() -> None: os.remove(n_newline.name) -def test_requirements_finder(tmpdir): +def test_requirements_finder(tmpdir) -> None: subdir = tmpdir.mkdir("subdir").join("lol.txt") subdir.write("flask") req_file = tmpdir.join("requirements.txt") @@ -3143,7 +3143,7 @@ def test_requirements_finder(tmpdir): req_file.remove() -def test_forced_separate_is_deterministic_issue_774(tmpdir): +def test_forced_separate_is_deterministic_issue_774(tmpdir) -> None: config_file = tmpdir.join("setup.cfg") config_file.write( @@ -3190,7 +3190,7 @@ def test_forced_separate_is_deterministic_issue_774(tmpdir): """ -def test_pipfile_finder(tmpdir): +def test_pipfile_finder(tmpdir) -> None: pipfile = tmpdir.join("Pipfile") pipfile.write(PIPFILE) si = SortImports(file_contents="") @@ -3222,7 +3222,7 @@ def test_monkey_patched_urllib() -> None: from urllib import quote # type: ignore # noqa: F401 -def test_path_finder(monkeypatch): +def test_path_finder(monkeypatch) -> None: si = SortImports(file_contents="") finder = finders.PathFinder(config=si.config, sections=si.sections) third_party_prefix = next(path for path in finder.paths if "site-packages" in path) @@ -3255,7 +3255,7 @@ def test_argument_parsing() -> None: @pytest.mark.parametrize("multiprocess", (False, True)) -def test_command_line(tmpdir, capfd, multiprocess): +def test_command_line(tmpdir, capfd, multiprocess: bool) -> None: from isort.main import main tmpdir.join("file1.py").write( @@ -3285,7 +3285,7 @@ def test_command_line(tmpdir, capfd, multiprocess): @pytest.mark.parametrize("quiet", (False, True)) -def test_quiet(tmpdir, capfd, quiet): +def test_quiet(tmpdir, capfd, quiet: bool) -> None: if sys.platform.startswith("win"): return from isort.main import main @@ -3302,7 +3302,7 @@ def test_quiet(tmpdir, capfd, quiet): @pytest.mark.parametrize("enabled", (False, True)) -def test_safety_excludes(tmpdir, enabled): +def test_safety_excludes(tmpdir, enabled: bool) -> None: tmpdir.join("victim.py").write("# ...") toxdir = tmpdir.mkdir(".tox") toxdir.join("verysafe.py").write("# ...") @@ -3344,11 +3344,11 @@ def test_safety_excludes(tmpdir, enabled): "skip_glob_assert", ( ([], 0, {os.sep.join(("code", "file.py"))}), - (["**/*.py"], 1, {}), - (["*/code/*.py"], 1, {}), + (["**/*.py"], 1, set()), + (["*/code/*.py"], 1, set()), ), ) -def test_skip_glob(tmpdir, skip_glob_assert): +def test_skip_glob(tmpdir, skip_glob_assert: Tuple[List[str], int, Set[str]]) -> None: skip_glob, skipped_count, file_names = skip_glob_assert base_dir = tmpdir.mkdir("build") code_dir = base_dir.mkdir("code") @@ -3992,7 +3992,7 @@ def test_to_ensure_empty_line_not_added_to_file_start_issue_889() -> None: assert SortImports(file_contents=test_input).output == test_input -def test_to_ensure_correctly_handling_of_whitespace_only_issue_811(capsys): +def test_to_ensure_correctly_handling_of_whitespace_only_issue_811(capsys) -> None: test_input = ( "import os\n" "import sys\n" @@ -4012,7 +4012,7 @@ def test_standard_library_deprecates_user_issue_778() -> None: assert SortImports(file_contents=test_input).output == test_input -def test_settings_path_skip_issue_909(tmpdir): +def test_settings_path_skip_issue_909(tmpdir) -> None: base_dir = tmpdir.mkdir("project") config_dir = base_dir.mkdir("conf") config_dir.join(".isort.cfg").write( @@ -4046,7 +4046,7 @@ def test_settings_path_skip_issue_909(tmpdir): assert b"skipped 2" in result.stdout.lower() -def test_skip_paths_issue_938(tmpdir): +def test_skip_paths_issue_938(tmpdir) -> None: base_dir = tmpdir.mkdir("project") config_dir = base_dir.mkdir("conf") config_dir.join(".isort.cfg").write( @@ -4178,7 +4178,7 @@ def test_isort_keeps_comments_issue_691() -> None: assert SortImports(file_contents=test_input).output == expected_output -def test_pyi_formatting_issue_942(tmpdir): +def test_pyi_formatting_issue_942(tmpdir) -> None: test_input = "import os\n" "\n" "\n" "def my_method():\n" expected_py_output = test_input.splitlines() expected_pyi_output = ("import os\n" "\n" "def my_method():\n").splitlines() From b26bc51b8a3b378e270a92cc4014fc13ba32be8f Mon Sep 17 00:00:00 2001 From: Danny Weinberg Date: Sun, 18 Aug 2019 10:47:33 -0700 Subject: [PATCH 0039/1439] Add configuration to ensure a newline before each comment This adds a `ensure_newline_before_comments` configuration that matches the format that `black` imposes (for better or worse) and allows `isort` to be used with `black` without any thrashing/conflicts with regards to blank lines before comments. Should help address https://github.com/psf/black/issues/251 --- isort/isort.py | 14 ++++++++++++++ test_isort.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/isort/isort.py b/isort/isort.py index d6084ba00..48f65453d 100644 --- a/isort/isort.py +++ b/isort/isort.py @@ -353,6 +353,8 @@ def _add_straight_imports( comments_above = self.comments["above"]["straight"].pop(module, None) if comments_above: + if section_output and self.config.get("ensure_newline_before_comments"): + section_output.append("") section_output.extend(comments_above) section_output.extend( self._add_comments(self.comments["straight"].get(module), idef) @@ -470,6 +472,10 @@ def _add_from_imports( module, None ) if above_comments: + if section_output and self.config.get( + "ensure_newline_before_comments" + ): + section_output.append("") section_output.extend(above_comments) if ( @@ -521,6 +527,10 @@ def _add_from_imports( module, None ) if above_comments: + if section_output and self.config.get( + "ensure_newline_before_comments" + ): + section_output.append("") section_output.extend(above_comments) section_output.append(self._wrap(single_import_line)) from_imports.remove(from_import) @@ -598,6 +608,10 @@ def _add_from_imports( if import_statement: above_comments = self.comments["above"]["from"].pop(module, None) if above_comments: + if section_output and self.config.get( + "ensure_newline_before_comments" + ): + section_output.append("") section_output.extend(above_comments) section_output.append(import_statement) diff --git a/test_isort.py b/test_isort.py index 3d87964f1..06a1fef2c 100644 --- a/test_isort.py +++ b/test_isort.py @@ -4178,6 +4178,38 @@ def test_isort_keeps_comments_issue_691() -> None: assert SortImports(file_contents=test_input).output == expected_output +def test_isort_ensures_blank_line_between_import_and_comment() -> None: + config = {"ensure_newline_before_comments": True} # type: Dict[str, Any] + test_input = ( + "import os\n" + "import sys\n" + "# noinspection PyUnresolvedReferences\n" + "import a\n" + "import b\n" + "# noinspection PyUnresolvedReferences\n" + "import c\n" + "from a import a\n" + "# noinspection PyUnresolvedReferences\n" + "from b import b\n" + ) + expected_output = ( + "import os\n" + "import sys\n" + "\n" + "# noinspection PyUnresolvedReferences\n" + "import a\n" + "import b\n" + "\n" + "# noinspection PyUnresolvedReferences\n" + "import c\n" + "from a import a\n" + "\n" + "# noinspection PyUnresolvedReferences\n" + "from b import b\n" + ) + assert SortImports(file_contents=test_input, **config).output == expected_output + + def test_pyi_formatting_issue_942(tmpdir): test_input = "import os\n" "\n" "\n" "def my_method():\n" expected_py_output = test_input.splitlines() From a7a606ed37f1f11237691ecc2277824bed945952 Mon Sep 17 00:00:00 2001 From: Danny Weinberg Date: Sun, 18 Aug 2019 11:21:50 -0700 Subject: [PATCH 0040/1439] Add some more test cases to increase coverage. --- test_isort.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test_isort.py b/test_isort.py index 06a1fef2c..2dc4da8c6 100644 --- a/test_isort.py +++ b/test_isort.py @@ -4188,9 +4188,13 @@ def test_isort_ensures_blank_line_between_import_and_comment() -> None: "import b\n" "# noinspection PyUnresolvedReferences\n" "import c\n" + "# noinspection PyUnresolvedReferences\n" + "import d as dd\n" "from a import a\n" "# noinspection PyUnresolvedReferences\n" "from b import b\n" + "# noinspection PyUnresolvedReferences\n" + "from c import c as cc\n" ) expected_output = ( "import os\n" @@ -4202,10 +4206,16 @@ def test_isort_ensures_blank_line_between_import_and_comment() -> None: "\n" "# noinspection PyUnresolvedReferences\n" "import c\n" + "\n" + "# noinspection PyUnresolvedReferences\n" + "import d as dd\n" "from a import a\n" "\n" "# noinspection PyUnresolvedReferences\n" "from b import b\n" + "\n" + "# noinspection PyUnresolvedReferences\n" + "from c import c as cc\n" ) assert SortImports(file_contents=test_input, **config).output == expected_output From ba5d61169d2ab6fed69a8a55c1879a1c53abb61a Mon Sep 17 00:00:00 2001 From: Danny Weinberg Date: Mon, 19 Aug 2019 10:40:35 -0700 Subject: [PATCH 0041/1439] Expand test more to test both beginning-of-section and middle-of-section logic. --- test_isort.py | 50 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/test_isort.py b/test_isort.py index 2dc4da8c6..d38bcd390 100644 --- a/test_isort.py +++ b/test_isort.py @@ -4179,43 +4179,59 @@ def test_isort_keeps_comments_issue_691() -> None: def test_isort_ensures_blank_line_between_import_and_comment() -> None: - config = {"ensure_newline_before_comments": True} # type: Dict[str, Any] + config = { + "ensure_newline_before_comments": True, + "known_one": ["one"], + "known_two": ["two"], + "known_three": ["three"], + "known_four": ["four"], + "sections": ["FUTURE", "STDLIB", "FIRSTPARTY", "THIRDPARTY", "LOCALFOLDER", "ONE", "TWO", "THREE", "FOUR"], + } # type: Dict[str, Any] test_input = ( "import os\n" - "import sys\n" "# noinspection PyUnresolvedReferences\n" - "import a\n" - "import b\n" + "import one.a\n" "# noinspection PyUnresolvedReferences\n" - "import c\n" + "import one.b\n" "# noinspection PyUnresolvedReferences\n" - "import d as dd\n" - "from a import a\n" + "import two.a as aa\n" "# noinspection PyUnresolvedReferences\n" - "from b import b\n" + "import two.b as bb\n" "# noinspection PyUnresolvedReferences\n" - "from c import c as cc\n" + "from three.a import a\n" + "# noinspection PyUnresolvedReferences\n" + "from three.b import b\n" + "# noinspection PyUnresolvedReferences\n" + "from four.a import a as aa\n" + "# noinspection PyUnresolvedReferences\n" + "from four.b import b as bb\n" ) expected_output = ( "import os\n" - "import sys\n" "\n" "# noinspection PyUnresolvedReferences\n" - "import a\n" - "import b\n" + "import one.a\n" + "\n" + "# noinspection PyUnresolvedReferences\n" + "import one.b\n" + "\n" + "# noinspection PyUnresolvedReferences\n" + "import two.a as aa\n" + "\n" + "# noinspection PyUnresolvedReferences\n" + "import two.b as bb\n" "\n" "# noinspection PyUnresolvedReferences\n" - "import c\n" + "from three.a import a\n" "\n" "# noinspection PyUnresolvedReferences\n" - "import d as dd\n" - "from a import a\n" + "from three.b import b\n" "\n" "# noinspection PyUnresolvedReferences\n" - "from b import b\n" + "from four.a import a as aa\n" "\n" "# noinspection PyUnresolvedReferences\n" - "from c import c as cc\n" + "from four.b import b as bb\n" ) assert SortImports(file_contents=test_input, **config).output == expected_output From 29e35710cb1554775f782f0ed8d914169947209a Mon Sep 17 00:00:00 2001 From: Danny Weinberg Date: Mon, 19 Aug 2019 10:49:38 -0700 Subject: [PATCH 0042/1439] Fix black formatting. --- test_isort.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/test_isort.py b/test_isort.py index d38bcd390..1f01583ce 100644 --- a/test_isort.py +++ b/test_isort.py @@ -4185,7 +4185,17 @@ def test_isort_ensures_blank_line_between_import_and_comment() -> None: "known_two": ["two"], "known_three": ["three"], "known_four": ["four"], - "sections": ["FUTURE", "STDLIB", "FIRSTPARTY", "THIRDPARTY", "LOCALFOLDER", "ONE", "TWO", "THREE", "FOUR"], + "sections": [ + "FUTURE", + "STDLIB", + "FIRSTPARTY", + "THIRDPARTY", + "LOCALFOLDER", + "ONE", + "TWO", + "THREE", + "FOUR", + ], } # type: Dict[str, Any] test_input = ( "import os\n" From 30782160adda2af29214ec9791b7f08bb76f6c52 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Tue, 20 Aug 2019 04:39:20 -0700 Subject: [PATCH 0043/1439] Remove redundant coerce to list _abspaths() returns a new list object. There is no need to create a new list a 2nd time. --- isort/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/settings.py b/isort/settings.py index f634036dd..e97be6d75 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -352,7 +352,7 @@ def _update_with_config_file( value = bool(strtobool(value)) computed_settings[access_key] = value elif key.startswith("known_"): - computed_settings[access_key] = list(_abspaths(cwd, _as_list(value))) + computed_settings[access_key] = _abspaths(cwd, _as_list(value)) elif key == "force_grid_wrap": try: result = existing_value_type(value) From 5c0ee3e8c42969102d58d64c492fea5e58ca75fa Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sat, 17 Aug 2019 11:27:25 -0700 Subject: [PATCH 0044/1439] Combine adjacent strings into one string Black has a bug such that strings that were previously multiple lines sometimes end up as adjacent strings on one line. See: https://github.com/psf/black/issues/26 --- test_isort.py | 264 +++++++++++++++++++++++--------------------------- 1 file changed, 121 insertions(+), 143 deletions(-) diff --git a/test_isort.py b/test_isort.py index b0e918b14..a191cd291 100644 --- a/test_isort.py +++ b/test_isort.py @@ -80,9 +80,7 @@ def default_settings_path(tmpdir_factory) -> Iterator[str]: def test_happy_path() -> None: """Test the most basic use case, straight imports no code, simply not organized by category.""" - test_input = ( - "import sys\n" "import os\n" "import myproject.test\n" "import django.settings" - ) + test_input = "import sys\nimport os\nimport myproject.test\nimport django.settings" test_output = SortImports( file_contents=test_input, known_third_party=["django"] ).output @@ -127,10 +125,10 @@ def test_correct_space_between_imports() -> None: (2 for method, class, or decorator definitions 1 for anything else) """ - test_input_method = "import sys\n" "def my_method():\n" " print('hello world')\n" + test_input_method = "import sys\ndef my_method():\n print('hello world')\n" test_output_method = SortImports(file_contents=test_input_method).output assert test_output_method == ( - "import sys\n" "\n" "\n" "def my_method():\n" " print('hello world')\n" + "import sys\n\n\ndef my_method():\n print('hello world')\n" ) test_input_decorator = ( @@ -149,22 +147,20 @@ def test_correct_space_between_imports() -> None: " print('hello world')\n" ) - test_input_class = "import sys\n" "class MyClass(object):\n" " pass\n" + test_input_class = "import sys\nclass MyClass(object):\n pass\n" test_output_class = SortImports(file_contents=test_input_class).output - assert test_output_class == ( - "import sys\n" "\n" "\n" "class MyClass(object):\n" " pass\n" - ) + assert test_output_class == "import sys\n\n\nclass MyClass(object):\n pass\n" - test_input_other = "import sys\n" "print('yo')\n" + test_input_other = "import sys\nprint('yo')\n" test_output_other = SortImports(file_contents=test_input_other).output - assert test_output_other == ("import sys\n" "\n" "print('yo')\n") + assert test_output_other == "import sys\n\nprint('yo')\n" def test_sort_on_number() -> None: """Ensure numbers get sorted logically (10 > 9 not the other way around)""" - test_input = "import lib10\n" "import lib9\n" + test_input = "import lib10\nimport lib9\n" test_output = SortImports(file_contents=test_input).output - assert test_output == ("import lib9\n" "import lib10\n") + assert test_output == "import lib9\nimport lib10\n" def test_line_length() -> None: @@ -490,7 +486,7 @@ def test_output_modes() -> None: ).output test_output_vertical_grid_grouped_doesnt_wrap_early = test_case assert test_output_vertical_grid_grouped_doesnt_wrap_early == ( - "from third_party import (\n" " lib1, lib2, lib3, lib4, lib5, lib5ab\n" ")\n" + "from third_party import (\n lib1, lib2, lib3, lib4, lib5, lib5ab\n)\n" ) @@ -719,7 +715,7 @@ def test_skip() -> None: def test_skip_with_file_name() -> None: """Ensure skipping a file works even when file_contents is provided.""" - test_input = "import django\n" "import myproject\n" + test_input = "import django\nimport myproject\n" sort_imports = SortImports( file_path="/baz.py", @@ -733,7 +729,7 @@ def test_skip_with_file_name() -> None: def test_skip_within_file() -> None: """Ensure skipping a whole file works.""" - test_input = "# isort:skip_file\n" "import django\n" "import myproject\n" + test_input = "# isort:skip_file\nimport django\nimport myproject\n" sort_imports = SortImports(file_contents=test_input, known_third_party=["django"]) assert sort_imports.skipped assert sort_imports.output is None @@ -741,16 +737,14 @@ def test_skip_within_file() -> None: def test_force_to_top() -> None: """Ensure forcing a single import to the top of its category works as expected.""" - test_input = "import lib6\n" "import lib2\n" "import lib5\n" "import lib1\n" + test_input = "import lib6\nimport lib2\nimport lib5\nimport lib1\n" test_output = SortImports(file_contents=test_input, force_to_top=["lib5"]).output - assert test_output == ( - "import lib5\n" "import lib1\n" "import lib2\n" "import lib6\n" - ) + assert test_output == "import lib5\nimport lib1\nimport lib2\nimport lib6\n" def test_add_imports() -> None: """Ensures adding imports works as expected.""" - test_input = "import lib6\n" "import lib2\n" "import lib5\n" "import lib1\n\n" + test_input = "import lib6\nimport lib2\nimport lib5\nimport lib1\n\n" test_output = SortImports( file_contents=test_input, add_imports=["import lib4", "import lib7"] ).output @@ -764,7 +758,7 @@ def test_add_imports() -> None: ) # Using simplified syntax - test_input = "import lib6\n" "import lib2\n" "import lib5\n" "import lib1\n\n" + test_input = "import lib6\nimport lib2\nimport lib5\nimport lib1\n\n" test_output = SortImports( file_contents=test_input, add_imports=["lib4", "lib7", "lib8.a"] ).output @@ -779,7 +773,7 @@ def test_add_imports() -> None: ) # On a file that has no pre-existing imports - test_input = '"""Module docstring"""\n' "\n" "class MyClass(object):\n" " pass\n" + test_input = '"""Module docstring"""\n' "\nclass MyClass(object):\n pass\n" test_output = SortImports( file_contents=test_input, add_imports=["from __future__ import print_function"] ).output @@ -793,7 +787,7 @@ def test_add_imports() -> None: ) # On a file that has no pre-existing imports, and no doc-string - test_input = "class MyClass(object):\n" " pass\n" + test_input = "class MyClass(object):\n pass\n" test_output = SortImports( file_contents=test_input, add_imports=["from __future__ import print_function"] ).output @@ -820,11 +814,11 @@ def test_add_imports() -> None: def test_remove_imports() -> None: """Ensures removing imports works as expected.""" - test_input = "import lib6\n" "import lib2\n" "import lib5\n" "import lib1" + test_input = "import lib6\nimport lib2\nimport lib5\nimport lib1" test_output = SortImports( file_contents=test_input, remove_imports=["lib2", "lib6"] ).output - assert test_output == ("import lib1\n" "import lib5\n") + assert test_output == "import lib1\nimport lib5\n" # Using natural syntax test_input = ( @@ -838,47 +832,47 @@ def test_remove_imports() -> None: file_contents=test_input, remove_imports=["import lib2", "import lib6", "from lib8 import a"], ).output - assert test_output == ("import lib1\n" "import lib5\n") + assert test_output == "import lib1\nimport lib5\n" def test_explicitly_local_import() -> None: """Ensure that explicitly local imports are separated.""" - test_input = "import lib1\n" "import lib2\n" "import .lib6\n" "from . import lib7" + test_input = "import lib1\nimport lib2\nimport .lib6\nfrom . import lib7" assert SortImports(file_contents=test_input).output == ( - "import lib1\n" "import lib2\n" "\n" "import .lib6\n" "from . import lib7\n" + "import lib1\nimport lib2\n\nimport .lib6\nfrom . import lib7\n" ) def test_quotes_in_file() -> None: """Ensure imports within triple quotes don't get imported.""" - test_input = "import os\n" "\n" '"""\n' "Let us\n" "import foo\n" "okay?\n" '"""\n' + test_input = "import os\n\n" '"""\n' "Let us\nimport foo\nokay?\n" '"""\n' assert SortImports(file_contents=test_input).output == test_input - test_input = "import os\n" "\n" '\'"""\'\n' "import foo\n" + test_input = "import os\n\n" '\'"""\'\n' "import foo\n" assert SortImports(file_contents=test_input).output == ( - "import os\n" "\n" "import foo\n" "\n" '\'"""\'\n' + "import os\n\nimport foo\n\n" '\'"""\'\n' ) - test_input = "import os\n" "\n" '"""Let us"""\n' "import foo\n" '"""okay?"""\n' + test_input = "import os\n\n" '"""Let us"""\n' "import foo\n" '"""okay?"""\n' assert SortImports(file_contents=test_input).output == ( - "import os\n" "\n" "import foo\n" "\n" '"""Let us"""\n' '"""okay?"""\n' + 'import os\n\nimport foo\n\n"""Let us"""\n"""okay?"""\n' ) - test_input = "import os\n" "\n" '#"""\n' "import foo\n" '#"""' + test_input = "import os\n\n" '#"""\n' "import foo\n" '#"""' assert SortImports(file_contents=test_input).output == ( - "import os\n" "\n" "import foo\n" "\n" '#"""\n' '#"""\n' + 'import os\n\nimport foo\n\n#"""\n#"""\n' ) - test_input = "import os\n" "\n" "'\\\n" "import foo'\n" + test_input = "import os\n\n'\\\nimport foo'\n" assert SortImports(file_contents=test_input).output == test_input - test_input = "import os\n" "\n" "'''\n" "\\'''\n" "import junk\n" "'''\n" + test_input = "import os\n\n'''\n\\'''\nimport junk\n'''\n" assert SortImports(file_contents=test_input).output == test_input def test_check_newline_in_imports(capsys) -> None: """Ensure tests works correctly when new lines are in imports.""" - test_input = "from lib1 import (\n" " sub1,\n" " sub2,\n" " sub3\n)\n" + test_input = "from lib1 import (\n sub1,\n sub2,\n sub3\n)\n" SortImports( file_contents=test_input, @@ -924,7 +918,7 @@ def test_forced_separate() -> None: == test_input ) - test_input = "from .foo import bar\n" "\n" "from .y import ca\n" + test_input = "from .foo import bar\n\nfrom .y import ca\n" assert ( SortImports( file_contents=test_input, @@ -938,9 +932,7 @@ def test_forced_separate() -> None: def test_default_section() -> None: """Test to ensure changing the default section works as expected.""" - test_input = ( - "import sys\n" "import os\n" "import myproject.test\n" "import django.settings" - ) + test_input = "import sys\nimport os\nimport myproject.test\nimport django.settings" test_output = SortImports( file_contents=test_input, known_third_party=["django"], @@ -991,12 +983,12 @@ def test_first_party_overrides_standard_section() -> None: def test_thirdy_party_overrides_standard_section() -> None: """Test to ensure changing the default section works as expected.""" - test_input = "import sys\n" "import os\n" "import this\n" "import profile.test\n" + test_input = "import sys\nimport os\nimport this\nimport profile.test\n" test_output = SortImports( file_contents=test_input, known_third_party=["profile"] ).output assert test_output == ( - "import os\n" "import sys\n" "import this\n" "\n" "import profile.test\n" + "import os\nimport sys\nimport this\n\nimport profile.test\n" ) @@ -1147,7 +1139,7 @@ def test_relative_import_with_space() -> None: def test_multiline_import() -> None: """Test the case where import spawns multiple lines with inconsistent indentation.""" test_input = ( - "from pkg \\\n" " import stuff, other_suff \\\n" " more_stuff" + "from pkg \\\n import stuff, other_suff \\\n more_stuff" ) assert SortImports(file_contents=test_input).output == ( "from pkg import more_stuff, other_suff, stuff\n" @@ -1174,16 +1166,16 @@ def test_multiline_import() -> None: def test_single_multiline() -> None: """Test the case where a single import spawns multiple lines.""" - test_input = "from os import\\\n" " getuid\n" "\n" "print getuid()\n" + test_input = "from os import\\\n getuid\n\nprint getuid()\n" output = SortImports(file_contents=test_input).output - assert output == ("from os import getuid\n" "\n" "print getuid()\n") + assert output == ("from os import getuid\n\nprint getuid()\n") def test_atomic_mode() -> None: # without syntax error, everything works OK - test_input = "from b import d, c\n" "from a import f, e\n" + test_input = "from b import d, c\nfrom a import f, e\n" assert SortImports(file_contents=test_input, atomic=True).output == ( - "from a import e, f\n" "from b import c, d\n" + "from a import e, f\nfrom b import c, d\n" ) # with syntax error content is not changed @@ -1227,37 +1219,37 @@ def test_order_by_type() -> None: def test_custom_lines_after_import_section() -> None: """Test the case where the number of lines to output after imports has been explicitly set.""" - test_input = "from a import b\n" "foo = 'bar'\n" + test_input = "from a import b\nfoo = 'bar'\n" # default case is one space if not method or class after imports assert SortImports(file_contents=test_input).output == ( - "from a import b\n" "\n" "foo = 'bar'\n" + "from a import b\n\nfoo = 'bar'\n" ) # test again with a custom number of lines after the import section assert SortImports(file_contents=test_input, lines_after_imports=2).output == ( - "from a import b\n" "\n" "\n" "foo = 'bar'\n" + "from a import b\n\n\nfoo = 'bar'\n" ) def test_smart_lines_after_import_section() -> None: """Tests the default 'smart' behavior for dealing with lines after the import section""" # one space if not method or class after imports - test_input = "from a import b\n" "foo = 'bar'\n" + test_input = "from a import b\nfoo = 'bar'\n" assert SortImports(file_contents=test_input).output == ( - "from a import b\n" "\n" "foo = 'bar'\n" + "from a import b\n\nfoo = 'bar'\n" ) # two spaces if a method or class after imports - test_input = "from a import b\n" "def my_function():\n" " pass\n" + test_input = "from a import b\ndef my_function():\n pass\n" assert SortImports(file_contents=test_input).output == ( - "from a import b\n" "\n" "\n" "def my_function():\n" " pass\n" + "from a import b\n\n\ndef my_function():\n pass\n" ) # two spaces if an async method after imports - test_input = "from a import b\n" "async def my_function():\n" " pass\n" + test_input = "from a import b\nasync def my_function():\n pass\n" assert SortImports(file_contents=test_input).output == ( - "from a import b\n" "\n" "\n" "async def my_function():\n" " pass\n" + "from a import b\n\n\nasync def my_function():\n pass\n" ) # two spaces if a method or class after imports - even if comment before function @@ -1297,9 +1289,7 @@ def test_smart_lines_after_import_section() -> None: ) # Ensure logic doesn't incorrectly skip over assignments to multi-line strings - test_input = ( - "from a import b\n" 'X = """test\n' '"""\n' "def my_function():\n" " pass\n" - ) + test_input = 'from a import b\nX = """test\n"""\ndef my_function():\n pass\n' assert SortImports(file_contents=test_input).output == ( "from a import b\n" "\n" @@ -1385,9 +1375,9 @@ def test_keep_comments() -> None: assert SortImports(file_contents=test_input).output == test_input # More complicated case - test_input = "from a import b # My Comment1\n" "from a import c # My Comment2\n" + test_input = "from a import b # My Comment1\nfrom a import c # My Comment2\n" assert SortImports(file_contents=test_input).output == ( - "from a import b # My Comment1\n" "from a import c # My Comment2\n" + "from a import b # My Comment1\nfrom a import c # My Comment2\n" ) # Test case where imports comments make imports extend pass the line length @@ -1414,7 +1404,7 @@ def test_keep_comments() -> None: # Test that comments are not stripped from 'import ... as ...' by default test_input = ( - "from a import b as bb # b comment\n" "from a import c as cc # c comment\n" + "from a import b as bb # b comment\nfrom a import c as cc # c comment\n" ) assert SortImports(file_contents=test_input).output == test_input @@ -1444,9 +1434,9 @@ def test_multiline_split_on_dot() -> None: def test_import_star() -> None: """Test to ensure isort handles star imports correctly""" - test_input = "from blah import *\n" "from blah import _potato\n" + test_input = "from blah import *\nfrom blah import _potato\n" assert SortImports(file_contents=test_input).output == ( - "from blah import *\n" "from blah import _potato\n" + "from blah import *\nfrom blah import _potato\n" ) assert SortImports(file_contents=test_input, combine_star=True).output == ( "from blah import *\n" @@ -1501,7 +1491,7 @@ def test_include_trailing_comma() -> None: include_trailing_comma=True, ).output assert test_output_vertical_grid == ( - "from third_party import (\n" " lib1, lib2, lib3, lib4,)\n" + "from third_party import (\n lib1, lib2, lib3, lib4,)\n" ) test_output_vertical_grid_grouped = SortImports( @@ -1511,7 +1501,7 @@ def test_include_trailing_comma() -> None: include_trailing_comma=True, ).output assert test_output_vertical_grid_grouped == ( - "from third_party import (\n" " lib1, lib2, lib3, lib4,\n" ")\n" + "from third_party import (\n lib1, lib2, lib3, lib4,\n)\n" ) test_output_wrap_single_import_with_use_parentheses = SortImports( @@ -1521,7 +1511,7 @@ def test_include_trailing_comma() -> None: use_parentheses=True, ).output assert test_output_wrap_single_import_with_use_parentheses == ( - "from third_party import (\n" " lib1,)\n" + "from third_party import (\n lib1,)\n" ) test_output_wrap_single_import_vertical_indent = SortImports( @@ -1532,13 +1522,13 @@ def test_include_trailing_comma() -> None: use_parentheses=True, ).output assert test_output_wrap_single_import_vertical_indent == ( - "from third_party import (\n" " lib1,\n" ")\n" + "from third_party import (\n lib1,\n)\n" ) def test_similar_to_std_library() -> None: """Test to ensure modules that are named similarly to a standard library import don't end up clobbered""" - test_input = "import datetime\n" "\n" "import requests\n" "import times\n" + test_input = "import datetime\n\nimport requests\nimport times\n" assert ( SortImports( file_contents=test_input, known_third_party=["requests", "times"] @@ -1549,12 +1539,12 @@ def test_similar_to_std_library() -> None: def test_correctly_placed_imports() -> None: """Test to ensure comments stay on correct placement after being sorted""" - test_input = "from a import b # comment for b\n" "from a import c # comment for c\n" + test_input = "from a import b # comment for b\nfrom a import c # comment for c\n" assert SortImports(file_contents=test_input, force_single_line=True).output == ( - "from a import b # comment for b\n" "from a import c # comment for c\n" + "from a import b # comment for b\nfrom a import c # comment for c\n" ) assert SortImports(file_contents=test_input).output == ( - "from a import b # comment for b\n" "from a import c # comment for c\n" + "from a import b # comment for b\nfrom a import c # comment for c\n" ) # Full example test from issue #143 @@ -1617,9 +1607,7 @@ def test_auto_detection() -> None: """Initial test to ensure isort auto-detection works correctly - will grow over time as new issues are raised.""" # Issue 157 - test_input = ( - "import binascii\n" "import os\n" "\n" "import cv2\n" "import requests\n" - ) + test_input = "import binascii\nimport os\n\nimport cv2\nimport requests\n" assert ( SortImports( file_contents=test_input, known_third_party=["cv2", "requests"] @@ -1638,10 +1626,10 @@ def test_same_line_statements() -> None: """Ensure isort correctly handles the case where a single line contains multiple statements including an import""" test_input = "import pdb; import nose\n" assert SortImports(file_contents=test_input).output == ( - "import pdb\n" "\n" "import nose\n" + "import pdb\n\nimport nose\n" ) - test_input = "import pdb; pdb.set_trace()\n" "import nose; nose.run()\n" + test_input = "import pdb; pdb.set_trace()\nimport nose; nose.run()\n" assert SortImports(file_contents=test_input).output == test_input @@ -1915,10 +1903,10 @@ def test_top_comments() -> None: ) assert SortImports(file_contents=test_input).output == test_input - test_input = "# Comment\n" "import sys\n" + test_input = "# Comment\nimport sys\n" assert SortImports(file_contents=test_input).output == test_input - test_input = "# -*- coding\n" "import sys\n" + test_input = "# -*- coding\nimport sys\n" assert SortImports(file_contents=test_input).output == test_input @@ -1932,7 +1920,7 @@ def test_consistency() -> None: def test_force_grid_wrap() -> None: """Ensures removing imports works as expected.""" - test_input = "from bar import lib2\n" "from foo import lib6, lib7\n" + test_input = "from bar import lib2\nfrom foo import lib6, lib7\n" test_output = SortImports( file_contents=test_input, force_grid_wrap=2, @@ -2011,7 +1999,7 @@ def test_uses_jinja_variables() -> None: def test_fcntl() -> None: """Test to ensure fcntl gets correctly recognized as stdlib import""" - test_input = "import fcntl\n" "import os\n" "import sys\n" + test_input = "import fcntl\nimport os\nimport sys\n" assert SortImports(file_contents=test_input).output == test_input @@ -2055,7 +2043,7 @@ def test_comment_at_top_of_file() -> None: ) assert SortImports(file_contents=test_input).output == test_input - test_input = "# -*- coding: utf-8 -*-\n" "from django.db import models\n" + test_input = "# -*- coding: utf-8 -*-\nfrom django.db import models\n" assert SortImports(file_contents=test_input).output == test_input @@ -2082,7 +2070,7 @@ def test_alphabetic_sorting() -> None: ).output assert output == test_input - test_input = "# -*- coding: utf-8 -*-\n" "from django.db import models\n" + test_input = "# -*- coding: utf-8 -*-\nfrom django.db import models\n" assert SortImports(file_contents=test_input).output == test_input @@ -2128,13 +2116,13 @@ def test_top_of_line_comments() -> None: def test_basic_comment() -> None: """Test to ensure a basic comment wont crash isort""" - test_input = "import logging\n" "# Foo\n" "import os\n" + test_input = "import logging\n# Foo\nimport os\n" assert SortImports(file_contents=test_input).output == test_input def test_shouldnt_add_lines() -> None: """Ensure that isort doesn't add a blank line when a top of import comment is present, issue #316""" - test_input = '"""Text"""\n' "# This is a comment\n" "import pkg_resources\n" + test_input = '"""Text"""\n' "# This is a comment\nimport pkg_resources\n" assert SortImports(file_contents=test_input).output == test_input @@ -2147,9 +2135,7 @@ def test_sections_parsed_correct(tmpdir) -> None: "import_heading_common=Common Library\n" "import_heading_stdlib=Standard Library\n" ) - test_input = ( - "import os\n" "from nose import *\n" "import nose\n" "from os import path" - ) + test_input = "import os\nfrom nose import *\nimport nose\nfrom os import path" correct_output = ( "# Standard Library\n" "import os\n" @@ -2184,9 +2170,7 @@ def test_pyproject_conf_file(tmpdir) -> None: 'sections="FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER,COMMON"\n' "include_trailing_comma = true\n" ) - test_input = ( - "import os\n" "from nose import *\n" "import nose\n" "from os import path" - ) + test_input = "import os\nfrom nose import *\nimport nose\nfrom os import path" correct_output = ( "# Standard Library\n" "import os\n" @@ -2261,20 +2245,20 @@ def test_sort_within_section() -> None: def test_sorting_with_two_top_comments() -> None: """Test to ensure isort will sort files that contain 2 top comments""" - test_input = "#! comment1\n" "''' comment2\n" "'''\n" "import b\n" "import a\n" + test_input = "#! comment1\n''' comment2\n'''\nimport b\nimport a\n" assert SortImports(file_contents=test_input).output == ( - "#! comment1\n" "''' comment2\n" "'''\n" "import a\n" "import b\n" + "#! comment1\n''' comment2\n'''\nimport a\nimport b\n" ) def test_lines_between_sections() -> None: """Test to ensure lines_between_sections works""" - test_input = "from bar import baz\n" "import os\n" + test_input = "from bar import baz\nimport os\n" assert SortImports(file_contents=test_input, lines_between_sections=0).output == ( - "import os\n" "from bar import baz\n" + "import os\nfrom bar import baz\n" ) assert SortImports(file_contents=test_input, lines_between_sections=2).output == ( - "import os\n\n\n" "from bar import baz\n" + "import os\n\n\nfrom bar import baz\n" ) @@ -2397,7 +2381,7 @@ def test_no_additional_lines_issue_358() -> None: def test_import_by_paren_issue_375() -> None: """Test to ensure isort can correctly handle sorting imports where the paren is directly by the import body""" - test_input = "from .models import(\n" " Foo,\n" " Bar,\n" ")\n" + test_input = "from .models import(\n Foo,\n Bar,\n)\n" assert ( SortImports(file_contents=test_input).output == "from .models import Bar, Foo\n" ) @@ -2457,9 +2441,9 @@ def test_plone_style() -> None: def test_third_party_case_sensitive() -> None: """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" + test_input = "import thirdparty\nimport os\nimport ABC\n" - expected_output = "import os\n" "\n" "import ABC\n" "import thirdparty\n" + expected_output = "import os\n\nimport ABC\nimport thirdparty\n" assert SortImports(file_contents=test_input).output == expected_output @@ -2511,9 +2495,7 @@ def test_long_single_line() -> None: def test_import_inside_class_issue_432() -> None: """Test to ensure issue 432 is resolved and isort doesn't insert imports in the middle of classes""" - test_input = ( - "# coding=utf-8\n" "class Foo:\n" " def bar(self):\n" " pass\n" - ) + test_input = "# coding=utf-8\nclass Foo:\n def bar(self):\n pass\n" expected_output = ( "# coding=utf-8\n" "import baz\n" @@ -2538,21 +2520,19 @@ def test_wildcard_import_without_space_issue_496() -> None: def test_import_line_mangles_issues_491() -> None: """Test to ensure comment on import with parens doesn't cause issues""" - test_input = "import os # ([\n" "\n" 'print("hi")\n' + test_input = "import os # ([\n\n" 'print("hi")\n' assert SortImports(file_contents=test_input).output == test_input def test_import_line_mangles_issues_505() -> None: """Test to ensure comment on import with parens doesn't cause issues""" - test_input = ( - "from sys import * # (\n" "\n" "\n" "def test():\n" ' print("Test print")\n' - ) + test_input = "from sys import * # (\n\n\ndef test():\n" ' print("Test print")\n' assert SortImports(file_contents=test_input).output == test_input def test_import_line_mangles_issues_439() -> None: """Test to ensure comment on import with parens doesn't cause issues""" - test_input = "import a # () import\n" "from b import b\n" + test_input = "import a # () import\nfrom b import b\n" assert SortImports(file_contents=test_input).output == test_input @@ -2638,21 +2618,21 @@ def test_long_alias_using_paren_issue_957() -> None: def test_strict_whitespace_by_default(capsys) -> None: - test_input = "import os\n" "from django.conf import settings\n" + test_input = "import os\nfrom django.conf import settings\n" SortImports(file_contents=test_input, check=True) out, err = capsys.readouterr() assert out == "ERROR: Imports are incorrectly sorted.\n" def test_strict_whitespace_no_closing_newline_issue_676(capsys) -> None: - test_input = "import os\n" "\n" "from django.conf import settings\n" "\n" "print(1)" + test_input = "import os\n\nfrom django.conf import settings\n\nprint(1)" SortImports(file_contents=test_input, check=True) out, err = capsys.readouterr() assert out == "" def test_ignore_whitespace(capsys) -> None: - test_input = "import os\n" "from django.conf import settings\n" + test_input = "import os\nfrom django.conf import settings\n" SortImports(file_contents=test_input, check=True, ignore_whitespace=True) out, err = capsys.readouterr() assert out == "" @@ -2725,7 +2705,7 @@ def test_sort_within_section_comments_issue_436() -> None: def test_sort_within_sections_with_force_to_top_issue_473() -> None: """Test to ensure it's possible to sort within sections with items forced to top""" - test_input = "import z\n" "import foo\n" "from foo import bar\n" + test_input = "import z\nimport foo\nfrom foo import bar\n" assert ( SortImports( file_contents=test_input, @@ -2738,7 +2718,7 @@ def test_sort_within_sections_with_force_to_top_issue_473() -> None: def test_correct_number_of_new_lines_with_comment_issue_435() -> None: """Test to ensure that injecting a comment in-between imports doesn't mess up the new line spacing""" - test_input = "import foo\n" "\n" "# comment\n" "\n" "\n" "def baz():\n" " pass\n" + test_input = "import foo\n\n# comment\n\n\ndef baz():\n pass\n" assert SortImports(file_contents=test_input).output == test_input @@ -2828,16 +2808,16 @@ def test_ensure_async_methods_work_issue_537() -> None: def test_ensure_as_imports_sort_correctly_within_from_imports_issue_590() -> None: """Test to ensure combination from and as import statements are sorted correct""" - test_input = "from os import defpath\n" "from os import pathsep as separator\n" + test_input = "from os import defpath\nfrom os import pathsep as separator\n" assert ( SortImports(file_contents=test_input, force_sort_within_sections=True).output == test_input ) - test_input = "from os import defpath\n" "from os import pathsep as separator\n" + test_input = "from os import defpath\nfrom os import pathsep as separator\n" assert SortImports(file_contents=test_input).output == test_input - test_input = "from os import defpath\n" "from os import pathsep as separator\n" + test_input = "from os import defpath\nfrom os import pathsep as separator\n" assert ( SortImports(file_contents=test_input, force_single_line=True).output == test_input @@ -2846,11 +2826,11 @@ def test_ensure_as_imports_sort_correctly_within_from_imports_issue_590() -> Non def test_ensure_line_endings_are_preserved_issue_493() -> None: """Test to ensure line endings are not converted""" - test_input = "from os import defpath\r\n" "from os import pathsep as separator\r\n" + test_input = "from os import defpath\r\nfrom os import pathsep as separator\r\n" assert SortImports(file_contents=test_input).output == test_input - test_input = "from os import defpath\r" "from os import pathsep as separator\r" + test_input = "from os import defpath\rfrom os import pathsep as separator\r" assert SortImports(file_contents=test_input).output == test_input - test_input = "from os import defpath\n" "from os import pathsep as separator\n" + test_input = "from os import defpath\nfrom os import pathsep as separator\n" assert SortImports(file_contents=test_input).output == test_input @@ -2908,7 +2888,7 @@ def test_not_splitted_sections() -> None: def test_no_lines_before_empty_section() -> None: - test_input = "import first\n" "import custom\n" + test_input = "import first\nimport custom\n" assert ( SortImports( file_contents=test_input, @@ -2937,7 +2917,7 @@ def test_no_inline_sort() -> None: ).output == "from foo import a, b, c\n" ) - expected = "from foo import a\n" "from foo import b\n" "from foo import c\n" + expected = "from foo import a\nfrom foo import b\nfrom foo import c\n" assert ( SortImports( file_contents=test_input, no_inline_sort=False, force_single_line=True @@ -2979,7 +2959,7 @@ def test_relative_import_of_a_module() -> None: def test_escaped_parens_sort() -> None: - test_input = "from foo import \\ \n" "(a,\n" "b,\n" "c)\n" + test_input = "from foo import \\ \n(a,\nb,\nc)\n" expected = "from foo import a, b, c\n" assert SortImports(file_contents=test_input).output == expected @@ -3057,7 +3037,7 @@ def test_to_ensure_no_unexpected_changes_issue_666() -> None: def test_to_ensure_tabs_dont_become_space_issue_665() -> None: - test_input = "import os\n" "\n" "\n" "def my_method():\n" "\tpass\n" + test_input = "import os\n\n\ndef my_method():\n\tpass\n" assert SortImports(file_contents=test_input).output == test_input @@ -3112,7 +3092,7 @@ def test_requirements_finder(tmpdir) -> None: subdir.write("flask") req_file = tmpdir.join("requirements.txt") req_file.write( - "Django==1.11\n" "-e git+https://github.com/orsinium/deal.git#egg=deal\n" + "Django==1.11\n-e git+https://github.com/orsinium/deal.git#egg=deal\n" ) si = SortImports(file_contents="") for path in (str(tmpdir), str(subdir)): @@ -3418,13 +3398,13 @@ def test_inconsistent_relative_imports_issue_577() -> None: def test_unwrap_issue_762() -> None: - test_input = "from os.path \\\n" "import (join, split)\n" + test_input = "from os.path \\\nimport (join, split)\n" assert ( SortImports(file_contents=test_input).output == "from os.path import join, split\n" ) - test_input = "from os.\\\n" " path import (join, split)" + test_input = "from os.\\\n path import (join, split)" assert ( SortImports(file_contents=test_input).output == "from os.path import join, split\n" @@ -3432,9 +3412,7 @@ def test_unwrap_issue_762() -> None: def test_multiple_as_imports() -> None: - test_input = ( - "from a import b as b\n" "from a import b as bb\n" "from a import b as bb_\n" - ) + test_input = "from a import b as b\nfrom a import b as bb\nfrom a import b as bb_\n" test_output = SortImports(file_contents=test_input).output assert test_output == test_input test_output = SortImports(file_contents=test_input, combine_as_imports=True).output @@ -3524,7 +3502,7 @@ def test_multiple_as_imports() -> None: ).output assert test_output == "from a import b, b as e, b as c, b as f\n" - test_input = "import a as a\n" "import a as aa\n" "import a as aa_\n" + test_input = "import a as a\nimport a as aa\nimport a as aa_\n" test_output = SortImports(file_contents=test_input).output assert test_output == test_input test_output = SortImports( @@ -3534,7 +3512,7 @@ def test_multiple_as_imports() -> None: ).output assert test_output == test_input - test_input = "import a\n" "import a as a\n" "import a as aa\n" "import a as aa_\n" + test_input = "import a\nimport a as a\nimport a as aa\nimport a as aa_\n" test_output = SortImports(file_contents=test_input).output assert test_output == "import a as a\nimport a as aa\nimport a as aa_\n" test_output = SortImports( @@ -3988,7 +3966,7 @@ def test_ensure_support_for_non_typed_but_cased_alphabetic_sort_issue_890() -> N def test_to_ensure_empty_line_not_added_to_file_start_issue_889() -> None: - test_input = "# comment\n" "import os\n" "# comment2\n" "import sys\n" + test_input = "# comment\nimport os\n# comment2\nimport sys\n" assert SortImports(file_contents=test_input).output == test_input @@ -4008,7 +3986,7 @@ def test_to_ensure_correctly_handling_of_whitespace_only_issue_811(capsys) -> No def test_standard_library_deprecates_user_issue_778() -> None: - test_input = "import os\n" "\n" "import user\n" + test_input = "import os\n\nimport user\n" assert SortImports(file_contents=test_input).output == test_input @@ -4024,10 +4002,10 @@ def test_settings_path_skip_issue_909(tmpdir) -> None: ) base_dir.join("file_glob_skip.py").write( - "import os\n" "\n" 'print("Hello World")\n' "\n" "import sys\n" + "import os\n\n" 'print("Hello World")\n' "\nimport sys\n" ) base_dir.join("file_to_be_skipped.py").write( - "import os\n" "\n" 'print("Hello World")' "\n" "import sys\n" + "import os\n\n" 'print("Hello World")' "\nimport sys\n" ) test_run_directory = os.getcwd() @@ -4058,12 +4036,12 @@ def test_skip_paths_issue_938(tmpdir) -> None: " migrations/**.py\n" ) base_dir.join("dont_skip.py").write( - "import os\n" "\n" 'print("Hello World")' "\n" "import sys\n" + "import os\n\n" 'print("Hello World")' "\nimport sys\n" ) migrations_dir = base_dir.mkdir("migrations") migrations_dir.join("file_glob_skip.py").write( - "import os\n" "\n" 'print("Hello World")\n' "\n" "import sys\n" + "import os\n\n" 'print("Hello World")\n' "\nimport sys\n" ) test_run_directory = os.getcwd() @@ -4179,9 +4157,9 @@ def test_isort_keeps_comments_issue_691() -> None: def test_pyi_formatting_issue_942(tmpdir) -> None: - test_input = "import os\n" "\n" "\n" "def my_method():\n" + test_input = "import os\n\n\ndef my_method():\n" expected_py_output = test_input.splitlines() - expected_pyi_output = ("import os\n" "\n" "def my_method():\n").splitlines() + expected_pyi_output = "import os\n\ndef my_method():\n".splitlines() assert ( SortImports(file_contents=test_input).output.splitlines() == expected_py_output ) @@ -4214,11 +4192,11 @@ def test_python_version() -> None: args = parse_args(["--python-version=3"]) assert args["py_version"] == "3" - test_input = "import os\n" "\n" "import user\n" + test_input = "import os\n\nimport user\n" assert SortImports(file_contents=test_input, py_version="3").output == test_input # user is part of the standard library in python 2 - output_python_2 = "import os\n" "import user\n" + output_python_2 = "import os\nimport user\n" assert ( SortImports(file_contents=test_input, py_version="2.7").output == output_python_2 From 164eedbb8b67ab089c61261d44f83e8af6811adc Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Wed, 14 Aug 2019 18:31:43 -0700 Subject: [PATCH 0045/1439] Auto-generate the complete list of stdlib modules The new script, mkstdlibs.py, uses intersphinx to dump a complete list of Python stdlib modules to the files py3.txt/py27.txt. These files are read by isort to decide if a module should be considered stdlib or not. In the future, if the stdlib changes, the fix is to simply re-run the script. Fixes #985 --- isort/settings.py | 9 +- isort/stdlibs/{py_two.py => py27.py} | 39 ++------- isort/stdlibs/{py_three.py => py3.py} | 117 ++------------------------ scripts/mkstdlibs.py | 46 ++++++++++ test_isort.py | 14 ++- 5 files changed, 68 insertions(+), 157 deletions(-) rename isort/stdlibs/{py_two.py => py27.py} (88%) rename isort/stdlibs/{py_three.py => py3.py} (62%) create mode 100755 scripts/mkstdlibs.py diff --git a/isort/settings.py b/isort/settings.py index e97be6d75..6de37e6df 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -45,8 +45,7 @@ Union, ) -from .stdlibs.py_three import standard_library_3 -from .stdlibs.py_two import standard_library_2 +from .stdlibs import py3, py27 from .utils import difference, union try: @@ -114,11 +113,11 @@ def _get_default(py_version: Optional[str]) -> Dict[str, Any]: _default = default.copy() if py_version == "all": - standard_library = list(set(standard_library_3 + standard_library_2)) + standard_library = list(set(py3.stdlib + py27.stdlib)) elif major == 3: - standard_library = standard_library_3 + standard_library = py3.stdlib elif major == 2 and minor == 7: - standard_library = standard_library_2 + standard_library = py27.stdlib else: raise ValueError( "The python version %s is not supported. " diff --git a/isort/stdlibs/py_two.py b/isort/stdlibs/py27.py similarity index 88% rename from isort/stdlibs/py_two.py rename to isort/stdlibs/py27.py index b2e867a89..0afed938d 100644 --- a/isort/stdlibs/py_two.py +++ b/isort/stdlibs/py27.py @@ -1,8 +1,11 @@ """ -File contains the standard library of python 2.7 +File contains the standard library of Python 2.7. + +DO NOT EDIT. If the standard library changes, a new list should be created +using the mkstdlibs.py script. """ -standard_library_2 = [ +stdlib = [ "AL", "BaseHTTPServer", "Bastion", @@ -37,6 +40,7 @@ "UserString", "W", "__builtin__", + "_winreg", "abc", "aepack", "aetools", @@ -49,7 +53,6 @@ "array", "ast", "asynchat", - "asyncio", "asyncore", "atexit", "audioop", @@ -61,7 +64,6 @@ "bisect", "bsddb", "buildtools", - "builtins", "bz2", "cPickle", "cProfile", @@ -82,19 +84,14 @@ "commands", "compileall", "compiler", - "concurrent", - "configparser", "contextlib", - "contextvars", "cookielib", "copy", "copy_reg", - "copyreg", "crypt", "csv", "ctypes", "curses", - "dataclasses", "datetime", "dbhash", "dbm", @@ -111,10 +108,8 @@ "email", "encodings", "ensurepip", - "enum", "errno", "exceptions", - "faulthandler", "fcntl", "filecmp", "fileinput", @@ -144,10 +139,8 @@ "heapq", "hmac", "hotshot", - "html", "htmlentitydefs", "htmllib", - "http", "httplib", "ic", "icopen", @@ -160,7 +153,6 @@ "imputil", "inspect", "io", - "ipaddress", "itertools", "jpeg", "json", @@ -169,7 +161,6 @@ "linecache", "locale", "logging", - "lzma", "macerrors", "macostools", "macpath", @@ -200,7 +191,6 @@ "os", "ossaudiodev", "parser", - "pathlib", "pdb", "pickle", "pickletools", @@ -220,12 +210,10 @@ "py_compile", "pyclbr", "pydoc", - "queue", "quopri", "random", "re", "readline", - "reprlib", "resource", "rexec", "rfc822", @@ -233,9 +221,7 @@ "robotparser", "runpy", "sched", - "secrets", "select", - "selectors", "sets", "sgmllib", "sha", @@ -244,17 +230,14 @@ "shutil", "signal", "site", - "sitecustomize", "smtpd", "smtplib", "sndhdr", "socket", - "socketserver", "spwd", "sqlite3", "ssl", "stat", - "statistics", "statvfs", "string", "stringprep", @@ -274,47 +257,37 @@ "termios", "test", "textwrap", - "this", "thread", "threading", "time", "timeit", - "tkinter", "token", "tokenize", "trace", "traceback", - "tracemalloc", "ttk", "tty", "turtle", - "turtledemo", "types", - "typing", "unicodedata", "unittest", "urllib", "urllib2", "urlparse", "user", - "usercustomize", "uu", "uuid", - "venv", "videoreader", "warnings", "wave", "weakref", "webbrowser", "whichdb", - "winreg", "winsound", "wsgiref", "xdrlib", "xml", - "xmlrpc", "xmlrpclib", - "zipapp", "zipfile", "zipimport", "zlib", diff --git a/isort/stdlibs/py_three.py b/isort/stdlibs/py3.py similarity index 62% rename from isort/stdlibs/py_three.py rename to isort/stdlibs/py3.py index fc7ab91b0..09716c90e 100644 --- a/isort/stdlibs/py_three.py +++ b/isort/stdlibs/py3.py @@ -1,53 +1,15 @@ """ -File contains the standard library of python3 +File contains the standard library of Python 3. -If in any minor the standard library changes, a new list should be -created and the names adjusted +DO NOT EDIT. If the standard library changes, a new list should be created +using the mkstdlibs.py script. """ -standard_library_3 = [ - "AL", - "BaseHTTPServer", - "Bastion", - "CGIHTTPServer", - "Carbon", - "ColorPicker", - "ConfigParser", - "Cookie", - "DEVICE", - "DocXMLRPCServer", - "EasyDialogs", - "FL", - "FrameWork", - "GL", - "HTMLParser", - "MacOS", - "MimeWriter", - "MiniAEFrame", - "Nav", - "PixMapWrapper", - "Queue", - "SUNAUDIODEV", - "ScrolledText", - "SimpleHTTPServer", - "SimpleXMLRPCServer", - "SocketServer", - "StringIO", - "Tix", - "Tkinter", - "UserDict", - "UserList", - "UserString", - "W", - "__builtin__", +stdlib = [ + "_dummy_thread", + "_thread", "abc", - "aepack", - "aetools", - "aetypes", "aifc", - "al", - "anydbm", - "applesingle", "argparse", "array", "ast", @@ -56,22 +18,15 @@ "asyncore", "atexit", "audioop", - "autoGIL", "base64", "bdb", "binascii", "binhex", "bisect", - "bsddb", - "buildtools", "builtins", "bz2", - "cPickle", "cProfile", - "cStringIO", "calendar", - "cd", - "cfmfile", "cgi", "cgitb", "chunk", @@ -82,16 +37,12 @@ "codeop", "collections", "colorsys", - "commands", "compileall", - "compiler", "concurrent", "configparser", "contextlib", "contextvars", - "cookielib", "copy", - "copy_reg", "copyreg", "crypt", "csv", @@ -99,73 +50,47 @@ "curses", "dataclasses", "datetime", - "dbhash", "dbm", "decimal", "difflib", - "dircache", "dis", "distutils", - "dl", "doctest", - "dumbdbm", - "dummy_thread", "dummy_threading", "email", "encodings", "ensurepip", "enum", "errno", - "exceptions", "faulthandler", "fcntl", "filecmp", "fileinput", - "findertools", - "fl", - "flp", - "fm", "fnmatch", "formatter", - "fpectl", - "fpformat", "fractions", "ftplib", "functools", - "future_builtins", "gc", - "gdbm", - "gensuitemodule", "getopt", "getpass", "gettext", - "gl", "glob", "grp", "gzip", "hashlib", "heapq", "hmac", - "hotshot", "html", - "htmlentitydefs", - "htmllib", "http", - "httplib", - "ic", - "icopen", - "imageop", "imaplib", - "imgfile", "imghdr", "imp", "importlib", - "imputil", "inspect", "io", "ipaddress", "itertools", - "jpeg", "json", "keyword", "lib2to3", @@ -173,28 +98,18 @@ "locale", "logging", "lzma", - "macerrors", - "macostools", "macpath", - "macresource", "mailbox", "mailcap", "marshal", "math", - "md5", - "mhlib", - "mimetools", "mimetypes", - "mimify", "mmap", "modulefinder", "msilib", "msvcrt", - "multifile", "multiprocessing", - "mutex", "netrc", - "new", "nis", "nntplib", "numbers", @@ -211,10 +126,8 @@ "pkgutil", "platform", "plistlib", - "popen2", "poplib", "posix", - "posixfile", "pprint", "profile", "pstats", @@ -230,24 +143,17 @@ "readline", "reprlib", "resource", - "rexec", - "rfc822", "rlcompleter", - "robotparser", "runpy", "sched", "secrets", "select", "selectors", - "sets", - "sgmllib", - "sha", "shelve", "shlex", "shutil", "signal", "site", - "sitecustomize", "smtpd", "smtplib", "sndhdr", @@ -258,13 +164,11 @@ "ssl", "stat", "statistics", - "statvfs", "string", "stringprep", "struct", "subprocess", "sunau", - "sunaudiodev", "symbol", "symtable", "sys", @@ -277,8 +181,6 @@ "termios", "test", "textwrap", - "this", - "thread", "threading", "time", "timeit", @@ -288,7 +190,6 @@ "trace", "traceback", "tracemalloc", - "ttk", "tty", "turtle", "turtledemo", @@ -297,25 +198,19 @@ "unicodedata", "unittest", "urllib", - "urllib2", - "urlparse", - "usercustomize", "uu", "uuid", "venv", - "videoreader", "warnings", "wave", "weakref", "webbrowser", - "whichdb", "winreg", "winsound", "wsgiref", "xdrlib", "xml", "xmlrpc", - "xmlrpclib", "zipapp", "zipfile", "zipimport", diff --git a/scripts/mkstdlibs.py b/scripts/mkstdlibs.py new file mode 100755 index 000000000..4a72c75c3 --- /dev/null +++ b/scripts/mkstdlibs.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 + +from sphinx.ext.intersphinx import fetch_inventory + + +URL = "https://docs.python.org/{}/objects.inv" +PATH = "isort/stdlibs/py{}.py" +VERSIONS = [("2", "7"), ("3",)] + +DOCSTRING = """ +File contains the standard library of Python {}. + +DO NOT EDIT. If the standard library changes, a new list should be created +using the mkstdlibs.py script. +""" + + +class FakeConfig: + intersphinx_timeout = None + tls_verify = True + + +class FakeApp: + srcdir = "" + config = FakeConfig() + + +for version_info in VERSIONS: + version = ".".join(version_info) + url = URL.format(version) + invdata = fetch_inventory(FakeApp(), "", url) + + modules = set() + for module in invdata["py:module"]: + root, *_ = module.split(".") + if root not in ["__future__", "__main__"]: + modules.add(root) + + path = PATH.format("".join(version_info)) + with open(path, "w") as fp: + docstring = DOCSTRING.format(version) + fp.write('"""{}"""\n\n'.format(docstring)) + fp.write("stdlib = [\n") + for module in sorted(modules): + fp.write(' "{}",\n'.format(module)) + fp.write("]\n") diff --git a/test_isort.py b/test_isort.py index a191cd291..5e38a808c 100644 --- a/test_isort.py +++ b/test_isort.py @@ -965,16 +965,14 @@ def test_first_party_overrides_standard_section() -> None: "from HTMLParser import HTMLParseError, HTMLParser\n" "import sys\n" "import os\n" - "import this\n" "import profile.test\n" ) test_output = SortImports( - file_contents=test_input, known_first_party=["profile"] + file_contents=test_input, known_first_party=["profile"], py_version="2.7" ).output assert test_output == ( "import os\n" "import sys\n" - "import this\n" "from HTMLParser import HTMLParseError, HTMLParser\n" "\n" "import profile.test\n" @@ -983,13 +981,11 @@ def test_first_party_overrides_standard_section() -> None: def test_thirdy_party_overrides_standard_section() -> None: """Test to ensure changing the default section works as expected.""" - test_input = "import sys\nimport os\nimport this\nimport profile.test\n" + test_input = "import sys\nimport os\nimport profile.test\n" test_output = SortImports( file_contents=test_input, known_third_party=["profile"] ).output - assert test_output == ( - "import os\nimport sys\nimport this\n\nimport profile.test\n" - ) + assert test_output == "import os\nimport sys\n\nimport profile.test\n" def test_known_pattern_path_expansion() -> None: @@ -1206,7 +1202,9 @@ def test_order_by_type() -> None: "from subprocess import PIPE, Popen, STDOUT\n" ) - assert SortImports(file_contents=test_input, order_by_type=True).output == ( + assert SortImports( + file_contents=test_input, order_by_type=True, py_version="2.7" + ).output == ( "import glob\n" "import os\n" "import shutil\n" From b6f6d51cf2bec1b622752b4d489433ce0f8e86ba Mon Sep 17 00:00:00 2001 From: Timothy Edmund Crosley Date: Wed, 11 Sep 2019 20:49:34 -0700 Subject: [PATCH 0046/1439] Update test_isort.py Remove blank lines per black --- test_isort.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test_isort.py b/test_isort.py index 3c19a2e71..822269bad 100644 --- a/test_isort.py +++ b/test_isort.py @@ -4224,8 +4224,8 @@ def test_isort_ensures_blank_line_between_import_and_comment() -> None: def test_pyi_formatting_issue_942(tmpdir): test_input = "import os\n" "\n" "\n" "def my_method():\n" - - + + def test_pyi_formatting_issue_942(tmpdir) -> None: test_input = "import os\n\n\ndef my_method():\n" expected_py_output = test_input.splitlines() From 1e78a9acf3110e1f9721feb591f89a451fc9876a Mon Sep 17 00:00:00 2001 From: Timothy Edmund Crosley Date: Wed, 11 Sep 2019 20:56:21 -0700 Subject: [PATCH 0047/1439] Update test_isort.py Fix method redefinition due to bad merge --- test_isort.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test_isort.py b/test_isort.py index 822269bad..ae5eee892 100644 --- a/test_isort.py +++ b/test_isort.py @@ -4222,10 +4222,6 @@ def test_isort_ensures_blank_line_between_import_and_comment() -> None: assert SortImports(file_contents=test_input, **config).output == expected_output -def test_pyi_formatting_issue_942(tmpdir): - test_input = "import os\n" "\n" "\n" "def my_method():\n" - - def test_pyi_formatting_issue_942(tmpdir) -> None: test_input = "import os\n\n\ndef my_method():\n" expected_py_output = test_input.splitlines() From 18ad293fc9d1852776afe35015a932b68d26fb14 Mon Sep 17 00:00:00 2001 From: Timothy Edmund Crosley Date: Wed, 11 Sep 2019 21:03:36 -0700 Subject: [PATCH 0048/1439] Add Danny Weinberg (@FuegoFro) to acknowledgements --- ACKNOWLEDGEMENTS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ACKNOWLEDGEMENTS.md b/ACKNOWLEDGEMENTS.md index c51a26046..6dae6cc77 100644 --- a/ACKNOWLEDGEMENTS.md +++ b/ACKNOWLEDGEMENTS.md @@ -82,6 +82,8 @@ Code Contributors - Matt Yule-Bennett (@mattbennett) - Jaswanth Kumar (@jaswanth098) - Dario Navin (@Zarathustra2) +- Danny Weinberg (@FuegoFro) + Documenters From 173287f22231d137a741e8d3ebf970f1c39a76bb Mon Sep 17 00:00:00 2001 From: Gram Date: Mon, 23 Sep 2019 16:45:31 +0200 Subject: [PATCH 0049/1439] fix invalid `*requirements*/*.{txt,in}` discovery --- isort/finders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/finders.py b/isort/finders.py index 028bf65c5..b017fa3e3 100644 --- a/isort/finders.py +++ b/isort/finders.py @@ -320,10 +320,10 @@ def _get_files_from_dir_cached(cls, path: str) -> List[str]: # *requirements*/*.{txt,in} if os.path.isdir(full_path): - for subfile_name in os.listdir(path): + for subfile_name in os.listdir(full_path): for ext in cls.exts: if subfile_name.endswith(ext): - results.append(os.path.join(path, subfile_name)) + results.append(os.path.join(full_path, subfile_name)) continue # *requirements*.{txt,in} From b95e3020f17e4d068584ce20efa205f48475338e Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 25 Sep 2019 22:39:37 +0300 Subject: [PATCH 0050/1439] Xenial is now default on Travis CI --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f34d023d1..b51da9860 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,3 @@ -dist: xenial language: python cache: pip env: From c5e47eb6ec2a36ea68398acb5cdfeef65bfdee3a Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 25 Sep 2019 22:47:45 +0300 Subject: [PATCH 0051/1439] Update PyPy3 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b51da9860..d839b7cf2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ matrix: env: TOXENV=py36-coverage - python: 3.7 env: TOXENV=py37-coverage - - python: pypy3.5-6.0 + - python: pypy3 env: TOXENV=pypy3 - os: osx language: generic From fbbdefd6b4a342766170e692b9ac46b26193955a Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 25 Sep 2019 23:01:09 +0300 Subject: [PATCH 0052/1439] Test on Python 3.8 beta --- .travis.yml | 2 ++ tox.ini | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d839b7cf2..2bd8274ed 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,8 @@ matrix: env: TOXENV=py36-coverage - python: 3.7 env: TOXENV=py37-coverage + - python: 3.8-dev + env: TOXENV=py38-coverage - python: pypy3 env: TOXENV=pypy3 - os: osx diff --git a/tox.ini b/tox.ini index 5b765bcb3..85d292ac0 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ envlist = isort-check, lint, mypy, - py{35,36,37,py3} + py{35,36,37,38,py3} [testenv] deps = From cdaae075b1b5eba8659853c07ef8ab423d36735f Mon Sep 17 00:00:00 2001 From: Timothy Edmund Crosley Date: Thu, 26 Sep 2019 03:09:59 -0700 Subject: [PATCH 0053/1439] Add Gram (@orsinium) and Hugo van Kemenade (@hugovk) to acknowledgement list --- ACKNOWLEDGEMENTS.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ACKNOWLEDGEMENTS.md b/ACKNOWLEDGEMENTS.md index 6dae6cc77..96c3e3b76 100644 --- a/ACKNOWLEDGEMENTS.md +++ b/ACKNOWLEDGEMENTS.md @@ -83,7 +83,8 @@ Code Contributors - Jaswanth Kumar (@jaswanth098) - Dario Navin (@Zarathustra2) - Danny Weinberg (@FuegoFro) - +- Gram (@orsinium) +- Hugo van Kemenade (@hugovk) Documenters From 6a6090a104d008a769a0f4c0a0733911e24babd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Ogam?= Date: Thu, 26 Sep 2019 13:32:43 +0200 Subject: [PATCH 0054/1439] Update finders.py --- isort/finders.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/isort/finders.py b/isort/finders.py index b017fa3e3..81421bf4d 100644 --- a/isort/finders.py +++ b/isort/finders.py @@ -130,7 +130,9 @@ def __init__(self, config: Mapping[str, Any], sections: Any) -> None: super().__init__(config, sections) # restore the original import path (i.e. not the path to bin/isort) - self.paths = [os.getcwd()] + root_dir = os.getcwd() + src_root_dir = '{0}/src'.format(root_dir) + self.paths = [root_dir, src_root_dir] # virtual env self.virtual_env = self.config.get("virtual_env") or os.environ.get( From 35359eb3ab4cc52a201b8299c3c215f3c1032e3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Ogam?= Date: Thu, 26 Sep 2019 20:54:19 +0200 Subject: [PATCH 0055/1439] Update finders.py --- isort/finders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/finders.py b/isort/finders.py index 81421bf4d..593d989f4 100644 --- a/isort/finders.py +++ b/isort/finders.py @@ -131,8 +131,8 @@ def __init__(self, config: Mapping[str, Any], sections: Any) -> None: # restore the original import path (i.e. not the path to bin/isort) root_dir = os.getcwd() - src_root_dir = '{0}/src'.format(root_dir) - self.paths = [root_dir, src_root_dir] + src_dir = '{0}/src'.format(root_dir) + self.paths = [root_dir, src_dir] # virtual env self.virtual_env = self.config.get("virtual_env") or os.environ.get( From 1bcaba6ba3398b888ddd24801265213920b077a0 Mon Sep 17 00:00:00 2001 From: Pedro Algarvio Date: Sat, 28 Sep 2019 01:32:39 +0100 Subject: [PATCH 0056/1439] Fix wrapped imports with comments and trailing comma --- isort/isort.py | 20 ++++++++++++++------ test_isort.py | 20 ++++++++++++++++++++ 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/isort/isort.py b/isort/isort.py index 48f65453d..44e3fbddc 100644 --- a/isort/isort.py +++ b/isort/isort.py @@ -273,7 +273,11 @@ def _wrap(self, line: str) -> str: ) and not line_without_comment.strip().startswith(splitter): line_parts = re.split(exp, line_without_comment) if comment: - line_parts[-1] = "{}#{}".format(line_parts[-1], comment) + line_parts[-1] = "{}{} #{}".format( + line_parts[-1].strip(), + "," if self.config["include_trailing_comma"] else "", + comment, + ) next_line = [] while (len(line) + 2) > ( self.config["wrap_length"] or self.config["line_length"] @@ -295,7 +299,9 @@ def _wrap(self, line: str) -> str: splitter, self.line_separator, cont_line, - "," if self.config["include_trailing_comma"] else "", + "," + if self.config["include_trailing_comma"] and not comment + else "", self.line_separator if wrap_mode in { @@ -1285,8 +1291,9 @@ def _parse(self) -> None: placed_module = self.place_module(import_from) if self.config["verbose"]: print( - "from-type place_module for %s returned %s" - % (import_from, placed_module) + "from-type place_module for {} returned {}".format( + import_from, placed_module + ) ) if placed_module == "": print( @@ -1391,8 +1398,9 @@ def _parse(self) -> None: placed_module = self.place_module(module) if self.config["verbose"]: print( - "else-type place_module for %s returned %s" - % (module, placed_module) + "else-type place_module for {} returned {}".format( + module, placed_module + ) ) if placed_module == "": print( diff --git a/test_isort.py b/test_isort.py index ae5eee892..ad72290bc 100644 --- a/test_isort.py +++ b/test_isort.py @@ -1523,6 +1523,26 @@ def test_include_trailing_comma() -> None: "from third_party import (\n lib1,\n)\n" ) + trailing_comma_with_comment = "from six.moves.urllib.parse import urlencode # pylint: disable=no-name-in-module,import-error" + expected_trailing_comma_with_comment = "from six.moves.urllib.parse import (\n urlencode, # pylint: disable=no-name-in-module,import-error\n)\n" + trailing_comma_with_comment = SortImports( + file_contents=trailing_comma_with_comment, + line_length=80, + multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, + include_trailing_comma=True, + use_parentheses=True, + ).output + assert trailing_comma_with_comment == expected_trailing_comma_with_comment + # The next time around, it should be equal + trailing_comma_with_comment = SortImports( + file_contents=trailing_comma_with_comment, + line_length=80, + multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, + include_trailing_comma=True, + use_parentheses=True, + ).output + assert trailing_comma_with_comment == expected_trailing_comma_with_comment + def test_similar_to_std_library() -> None: """Test to ensure modules that are named similarly to a standard library import don't end up clobbered""" From 2c5e0d60aebea001b0ac4f4c7841358b45ad903d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Ogam?= Date: Sat, 28 Sep 2019 22:42:21 +0200 Subject: [PATCH 0057/1439] Update finders.py --- isort/finders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/finders.py b/isort/finders.py index 593d989f4..9e891ab21 100644 --- a/isort/finders.py +++ b/isort/finders.py @@ -131,7 +131,7 @@ def __init__(self, config: Mapping[str, Any], sections: Any) -> None: # restore the original import path (i.e. not the path to bin/isort) root_dir = os.getcwd() - src_dir = '{0}/src'.format(root_dir) + src_dir = "{0}/src".format(root_dir) self.paths = [root_dir, src_dir] # virtual env From e13a1685ecf34b5b629b21f21b3fead3cc5ee957 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 4 Oct 2019 22:54:32 -0700 Subject: [PATCH 0058/1439] =?UTF-8?q?Add=20G=C3=A9ry=20Ogam=20(@maggyero)?= =?UTF-8?q?=20to=20acknowledgements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ACKNOWLEDGEMENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ACKNOWLEDGEMENTS.md b/ACKNOWLEDGEMENTS.md index 96c3e3b76..fede772db 100644 --- a/ACKNOWLEDGEMENTS.md +++ b/ACKNOWLEDGEMENTS.md @@ -85,7 +85,7 @@ Code Contributors - Danny Weinberg (@FuegoFro) - Gram (@orsinium) - Hugo van Kemenade (@hugovk) - +- Géry Ogam (@maggyero) Documenters =================== From 752c0614c410bb609db97adc6b42c25957c9074c Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Mon, 7 Oct 2019 08:36:05 -0500 Subject: [PATCH 0059/1439] Fix bad sort with force_single_line and sort_within_sections With both force_single_line and sort_within_sections enabled, modules are not reliably sorted alphabetically; instead, all imports that would be combined if force_single_line were false are instead grouped together, even if another import should (lexicographically) be somewhere in their midst. The new test case provides a concrete example. Also makes a small regex performance enhancement by precompiling regexes used when force_single_line=True --- isort/isort.py | 17 ++++++++++------- test_isort.py | 20 ++++++++++++++++++++ 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/isort/isort.py b/isort/isort.py index 48f65453d..62ba7bfcd 100644 --- a/isort/isort.py +++ b/isort/isort.py @@ -66,6 +66,9 @@ class _SortImports: + _import_line_intro_re = re.compile("^(?:from|import) ") + _import_line_midline_import_re = re.compile(" import ") + def __init__( self, file_contents: str, config: Dict[str, Any], extension: str = "py" ) -> None: @@ -426,7 +429,7 @@ def _add_from_imports( ) from_imports = None elif self.config["force_single_line"]: - import_statements = [] + import_statement = None while from_imports: from_import = from_imports.pop(0) single_import_line = self._add_comments( @@ -447,20 +450,19 @@ def _add_from_imports( self.config["keep_direct_and_as_imports"] and self.imports[section]["from"][module][from_import] ): - import_statements.append(self._wrap(single_import_line)) + section_output.append(self._wrap(single_import_line)) from_comments = self.comments["straight"].get( "{}.{}".format(module, from_import) ) - import_statements.extend( + section_output.extend( self._add_comments( from_comments, self._wrap(import_start + as_import) ) for as_import in nsorted(as_imports[from_import]) ) else: - import_statements.append(self._wrap(single_import_line)) + section_output.append(self._wrap(single_import_line)) comments = None - import_statement = self.line_separator.join(import_statements) else: while from_imports and from_imports[0] in as_imports: from_import = from_imports.pop(0) @@ -730,8 +732,9 @@ def by_module(line: str) -> str: if line.startswith("#"): return "AA" - line = re.sub("^from ", "", line) - line = re.sub("^import ", "", line) + line = self._import_line_intro_re.sub( + "", self._import_line_midline_import_re.sub(".", line) + ) if line.split(" ")[0] in self.config["force_to_top"]: section = "A" if not self.config["order_by_type"]: diff --git a/test_isort.py b/test_isort.py index ae5eee892..f19b96ad5 100644 --- a/test_isort.py +++ b/test_isort.py @@ -1066,6 +1066,26 @@ def test_force_single_line_long_imports() -> None: ) +def test_force_single_line_imports_and_sort_within_sections() -> None: + test_input = ( + "from third_party import lib_a, lib_b, lib_d\n" + "from third_party.lib_c import lib1\n" + ) + test_output = SortImports( + file_contents=test_input, + multi_line_output=WrapModes.GRID, + line_length=40, + force_single_line=True, + force_sort_within_sections=True, + ).output + assert test_output == ( + "from third_party import lib_a\n" + "from third_party import lib_b\n" + "from third_party.lib_c import lib1\n" + "from third_party import lib_d\n" + ) + + def test_titled_imports() -> None: """Tests setting custom titled/commented import sections.""" test_input = ( From 11fd4c336f1c30d54cd2ccd359df57b5a8614398 Mon Sep 17 00:00:00 2001 From: Cody Scott Date: Wed, 9 Oct 2019 12:12:23 -0400 Subject: [PATCH 0060/1439] Keep correct comment location with force_sort_within_sections --- isort/isort.py | 24 ++++++++++++++++++++++-- test_isort.py | 19 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/isort/isort.py b/isort/isort.py index 48f65453d..518c5f53e 100644 --- a/isort/isort.py +++ b/isort/isort.py @@ -699,6 +699,9 @@ def _add_formatted_imports(self) -> None: ), ) + if self.config["force_sort_within_sections"]: + copied_comments = copy.deepcopy(self.comments) + section_output = [] # type: List[str] if self.config["from_first"]: self._add_from_imports( @@ -727,8 +730,6 @@ def _add_formatted_imports(self) -> None: def by_module(line: str) -> str: section = "B" - if line.startswith("#"): - return "AA" line = re.sub("^from ", "", line) line = re.sub("^import ", "", line) @@ -738,8 +739,27 @@ def by_module(line: str) -> str: line = line.lower() return "{}{}".format(section, line) + # Remove comments + section_output = [ + line for line in section_output if not line.startswith("#") + ] + section_output = nsorted(section_output, key=by_module) + # Add comments back + all_comments = copied_comments["above"]["from"] + all_comments.update(copied_comments["above"]["straight"]) + comment_indexes = {} + for module, comment_list in all_comments.items(): + for idx, line in enumerate(section_output): + if module in line: + comment_indexes[idx] = comment_list + added = 0 + for idx, comment_list in comment_indexes.items(): + for comment in comment_list: + section_output.insert(idx + added, comment) + added += 1 + section_name = section no_lines_before = section_name in self.config["no_lines_before"] diff --git a/test_isort.py b/test_isort.py index ae5eee892..0742e4453 100644 --- a/test_isort.py +++ b/test_isort.py @@ -4222,6 +4222,25 @@ def test_isort_ensures_blank_line_between_import_and_comment() -> None: assert SortImports(file_contents=test_input, **config).output == expected_output +def test_moving_comments_issue_726(): + config = {"force_sort_within_sections": 1} # type: Dict[str, Any] + test_input = ( + "import Blue.models as BlueModels\n" + "# comment for PlaidModel\n" + "from Plaid.models import PlaidModel\n" + ) + assert SortImports(file_contents=test_input, **config).output == test_input + + test_input = ( + "# comment for BlueModels\n" + "import Blue.models as BlueModels\n" + "# comment for PlaidModel\n" + "# another comment for PlaidModel\n" + "from Plaid.models import PlaidModel\n" + ) + assert SortImports(file_contents=test_input, **config).output == test_input + + def test_pyi_formatting_issue_942(tmpdir) -> None: test_input = "import os\n\n\ndef my_method():\n" expected_py_output = test_input.splitlines() From aca12584cbf845588bef99573d1a92d1e117c515 Mon Sep 17 00:00:00 2001 From: Cody Scott Date: Wed, 9 Oct 2019 14:34:26 -0400 Subject: [PATCH 0061/1439] Add test from issue #751 --- test_isort.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test_isort.py b/test_isort.py index ae5eee892..4e247b95d 100644 --- a/test_isort.py +++ b/test_isort.py @@ -4248,6 +4248,31 @@ def test_pyi_formatting_issue_942(tmpdir) -> None: ) +def test_move_class_issue_751() -> None: + test_input = ( + "# -*- coding: utf-8 -*-" + "\n" + "# Define your item pipelines here" + "#" + "# Don't forget to add your pipeline to the ITEM_PIPELINES setting" + "# See: https://doc.scrapy.org/en/latest/topics/item-pipeline.html" + "from datetime import datetime" + "from .items import WeiboMblogItem" + "\n" + "class WeiboMblogPipeline(object):" + " def process_item(self, item, spider):" + " if isinstance(item, WeiboMblogItem):" + " item = self._process_item(item, spider)" + " return item" + "\n" + " def _process_item(self, item, spider):" + " item['inserted_at'] = datetime.now()" + " return item" + "\n" + ) + assert SortImports(file_contents=test_input).output == test_input + + def test_python_version() -> None: from isort.main import parse_args From 51ac7a455f93897f4e6ba6b8678329030257bf9f Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Thu, 10 Oct 2019 06:42:59 +0200 Subject: [PATCH 0062/1439] Coding detection closer to PEP 263 --- isort/compat.py | 6 +++--- test_isort.py | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/isort/compat.py b/isort/compat.py index 0af5d44aa..9e3e6e527 100644 --- a/isort/compat.py +++ b/isort/compat.py @@ -12,17 +12,17 @@ def determine_file_encoding(file_path: Path, default: str = "utf-8") -> str: # see https://www.python.org/dev/peps/pep-0263/ - pattern = re.compile(br"coding[:=]\s*([-\w.]+)") + pattern = re.compile(br"^[ \t\f]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)") coding = default with file_path.open("rb") as f: for line_number, line in enumerate(f, 1): + if line_number > 2: + break groups = re.findall(pattern, line) if groups: coding = groups[0].decode("ascii") break - if line_number > 2: - break return coding diff --git a/test_isort.py b/test_isort.py index ae5eee892..7ca611f45 100644 --- a/test_isort.py +++ b/test_isort.py @@ -2031,6 +2031,28 @@ def test_other_file_encodings(tmpdir) -> None: ) +def test_encoding_not_in_comment(tmpdir) -> None: + """Test that 'encoding' not in a comment is ignored""" + tmp_fname = tmpdir.join("test_encoding.py") + file_contents = "class Foo\n coding: latin1\n\ns = u'ã'\n".format("utf8") + tmp_fname.write_binary(file_contents.encode("utf8")) + assert ( + SortImports(file_path=str(tmp_fname), settings_path=os.getcwd()).output + == file_contents + ) + + +def test_encoding_not_in_first_two_lines(tmpdir) -> None: + """Test that 'encoding' not in the first two lines is ignored""" + tmp_fname = tmpdir.join("test_encoding.py") + file_contents = "\n\n# -*- coding: latin1\n\ns = u'ã'\n".format("utf8") + tmp_fname.write_binary(file_contents.encode("utf8")) + assert ( + SortImports(file_path=str(tmp_fname), settings_path=os.getcwd()).output + == file_contents + ) + + def test_comment_at_top_of_file() -> None: """Test to ensure isort correctly handles top of file comments""" test_input = ( From a5d8218eeca071262d4881eaef3732277b2dbac1 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 9 Oct 2019 23:38:24 -0700 Subject: [PATCH 0063/1439] Add Cody Scott (@Siecje) to contributors list --- ACKNOWLEDGEMENTS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ACKNOWLEDGEMENTS.md b/ACKNOWLEDGEMENTS.md index fede772db..c93e142c3 100644 --- a/ACKNOWLEDGEMENTS.md +++ b/ACKNOWLEDGEMENTS.md @@ -86,6 +86,8 @@ Code Contributors - Gram (@orsinium) - Hugo van Kemenade (@hugovk) - Géry Ogam (@maggyero) +- Cody Scott (@Siecje) + Documenters =================== From 466adbf3bd34ba0c59b897456b5e628bfec4c69c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 9 Oct 2019 23:39:59 -0700 Subject: [PATCH 0064/1439] Add Pedro Algarvio (@s0undt3ch) to contributors list --- ACKNOWLEDGEMENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ACKNOWLEDGEMENTS.md b/ACKNOWLEDGEMENTS.md index c93e142c3..153b51450 100644 --- a/ACKNOWLEDGEMENTS.md +++ b/ACKNOWLEDGEMENTS.md @@ -87,7 +87,7 @@ Code Contributors - Hugo van Kemenade (@hugovk) - Géry Ogam (@maggyero) - Cody Scott (@Siecje) - +- Pedro Algarvio (@s0undt3ch) Documenters =================== From cb9254a3c89c51562baad630d08857d681ecde17 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 9 Oct 2019 23:42:12 -0700 Subject: [PATCH 0065/1439] Add Chris St. Pierre (@stpierre) to contributors list --- ACKNOWLEDGEMENTS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ACKNOWLEDGEMENTS.md b/ACKNOWLEDGEMENTS.md index 153b51450..4fcfe64b6 100644 --- a/ACKNOWLEDGEMENTS.md +++ b/ACKNOWLEDGEMENTS.md @@ -88,6 +88,7 @@ Code Contributors - Géry Ogam (@maggyero) - Cody Scott (@Siecje) - Pedro Algarvio (@s0undt3ch) +- Chris St. Pierre (@stpierre) Documenters =================== From 881bee13603f92fb0c822d6b55349368905156a5 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 9 Oct 2019 23:48:10 -0700 Subject: [PATCH 0066/1439] Add Sebastian Rittau (@srittau) to contributor list --- ACKNOWLEDGEMENTS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ACKNOWLEDGEMENTS.md b/ACKNOWLEDGEMENTS.md index 4fcfe64b6..d18b70363 100644 --- a/ACKNOWLEDGEMENTS.md +++ b/ACKNOWLEDGEMENTS.md @@ -89,6 +89,8 @@ Code Contributors - Cody Scott (@Siecje) - Pedro Algarvio (@s0undt3ch) - Chris St. Pierre (@stpierre) +- Sebastian Rittau (@srittau) + Documenters =================== From 15d7afcbc3dff03c1850ebef45f81922fd6c3572 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 10 Oct 2019 18:18:18 -0700 Subject: [PATCH 0067/1439] Add test case for single letter capatalized name --- test_isort.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test_isort.py b/test_isort.py index c75858894..55e2879c6 100644 --- a/test_isort.py +++ b/test_isort.py @@ -4377,3 +4377,12 @@ def test_python_version() -> None: test_input = "import os\nimport xml" print(SortImports(file_contents=test_input, py_version="all").output) + + +def test_isort_with_single_character_import() -> None: + """Tests to ensure isort handles single capatilized single character imports as class objects by default + + See Issue #376: https://github.com/timothycrosley/isort/issues/376 + """ + test_input = "from django.db.models import CASCADE, SET_NULL, Q\n" + assert SortImports(file_contents=test_input).output == test_input From 27cbfc75b6849f8fdf876bec6951d1ef5df39bec Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 10 Oct 2019 18:20:59 -0700 Subject: [PATCH 0068/1439] Add comment mentioning relevant ticket at line upercasing letter --- isort/isort.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/isort.py b/isort/isort.py index 1e9f2447b..6cf04d790 100644 --- a/isort/isort.py +++ b/isort/isort.py @@ -220,7 +220,7 @@ def _module_key( module_name = str(module_name) if sub_imports and config["order_by_type"]: - if module_name.isupper() and len(module_name) > 1: + if module_name.isupper() and len(module_name) > 1: # see issue #376 prefix = "A" elif module_name[0:1].isupper(): prefix = "B" From 07b1e2e3f2dc186a4f53323c345b2f3a1cab6593 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 10 Oct 2019 21:18:41 -0700 Subject: [PATCH 0069/1439] test -> run_tests --- .env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env b/.env index c1f8e3965..246963e0c 100644 --- a/.env +++ b/.env @@ -47,7 +47,7 @@ alias root="cd $PROJECT_DIR" alias project="root; cd $PROJECT_NAME" # Commands -alias test="root; py.test test_isort.py -s" +alias run_test="root; py.test test_isort.py -s" alias install="_install_project" alias distribute="_distribute" alias leave="_leave_project" From b85371a5bad023015f71ee25994ebfe5d6e2bc8d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 11 Oct 2019 19:10:06 -0700 Subject: [PATCH 0070/1439] Add new scripts to eventually replace .env --- scripts/clean.sh | 4 ++++ scripts/done.sh | 4 ++++ scripts/lint.sh | 8 ++++++++ scripts/test.sh | 4 ++++ 4 files changed, 20 insertions(+) create mode 100644 scripts/clean.sh create mode 100644 scripts/done.sh create mode 100644 scripts/lint.sh create mode 100644 scripts/test.sh diff --git a/scripts/clean.sh b/scripts/clean.sh new file mode 100644 index 000000000..c44d72ac6 --- /dev/null +++ b/scripts/clean.sh @@ -0,0 +1,4 @@ +#!/bin/bash -xe + +poetry run isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --use-parentheses --line-width=100 --recursive isort/ test_isort.py +poetry run black isort/ tests_isort.py -l 100 diff --git a/scripts/done.sh b/scripts/done.sh new file mode 100644 index 000000000..d5dba124a --- /dev/null +++ b/scripts/done.sh @@ -0,0 +1,4 @@ +#!/bin/bash -xe + +./scripts/clean.sh +./scripts/test.sh diff --git a/scripts/lint.sh b/scripts/lint.sh new file mode 100644 index 000000000..22107de14 --- /dev/null +++ b/scripts/lint.sh @@ -0,0 +1,8 @@ +#!/bin/bash -xe + +poetry run mypy --ignore-missing-imports isort/ +poetry run isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --use-parentheses --line-width=100 --recursive --check --diff --recursive isort/ test_isort.py +poetry run black --check -l 100 isort/ test_isort.py +poetry run flake8 isort/ tests/ --max-line 100 --ignore F403,F401,W503 +poetry run safety check +poetry run bandit -r isort/ diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100644 index 000000000..e9114cccb --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1,4 @@ +#!/bin/bash -xe + +./scripts/lint.sh +poetry run pytest -s --cov={{cookiecutter.project_name}}/ --cov=tests --cov-report=term-missing ${@} --cov-report html From 7f8553a45387502235a8d9be494da7068afb27fe Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 11 Oct 2019 23:37:41 -0700 Subject: [PATCH 0071/1439] Make new scripts executable --- scripts/clean.sh | 0 scripts/done.sh | 0 scripts/lint.sh | 0 scripts/test.sh | 0 4 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 scripts/clean.sh mode change 100644 => 100755 scripts/done.sh mode change 100644 => 100755 scripts/lint.sh mode change 100644 => 100755 scripts/test.sh diff --git a/scripts/clean.sh b/scripts/clean.sh old mode 100644 new mode 100755 diff --git a/scripts/done.sh b/scripts/done.sh old mode 100644 new mode 100755 diff --git a/scripts/lint.sh b/scripts/lint.sh old mode 100644 new mode 100755 diff --git a/scripts/test.sh b/scripts/test.sh old mode 100644 new mode 100755 From 4ded9d897531796479b8b6bdb4f1b10916b77488 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 12 Oct 2019 00:06:09 -0700 Subject: [PATCH 0072/1439] Ignore black / flake8 differences --- scripts/lint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/lint.sh b/scripts/lint.sh index 22107de14..3698d7428 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -3,6 +3,6 @@ poetry run mypy --ignore-missing-imports isort/ poetry run isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --use-parentheses --line-width=100 --recursive --check --diff --recursive isort/ test_isort.py poetry run black --check -l 100 isort/ test_isort.py -poetry run flake8 isort/ tests/ --max-line 100 --ignore F403,F401,W503 +poetry run flake8 isort/ tests/ --max-line 100 --ignore F403,F401,W503,E203 poetry run safety check poetry run bandit -r isort/ From b58beff96c919e943dd97cec1ad402fd6538d3bb Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 12 Oct 2019 00:06:45 -0700 Subject: [PATCH 0073/1439] Switch editor config to 100 character per line limit --- .editorconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index 094bc546d..9fe123447 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,7 +1,7 @@ root = true [*.py] -max_line_length = 120 +max_line_length = 100 indent_style = space indent_size = 4 known_first_party = isort From ee3c23b77d17e16389635788a879f03e28aab0d4 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 12 Oct 2019 00:07:52 -0700 Subject: [PATCH 0074/1439] Update tox.ini to run isort and black with appropriate line length --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 85d292ac0..c0e014e53 100644 --- a/tox.ini +++ b/tox.ini @@ -23,11 +23,11 @@ commands = pytest {posargs} [testenv:black] deps = black -commands = black --check --diff . +commands = black -l 100 --check --diff . [testenv:isort-check] basepython = python3 -commands = python setup.py isort +commands = isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --use-parentheses --line-width=100 --recursive isort/ test_isort.py [testenv:lint] deps = From d98391c5c1876a31f0094a5519333e04894a7826 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 12 Oct 2019 00:08:36 -0700 Subject: [PATCH 0075/1439] Fix black call against test module --- scripts/clean.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/clean.sh b/scripts/clean.sh index c44d72ac6..7969592fb 100755 --- a/scripts/clean.sh +++ b/scripts/clean.sh @@ -1,4 +1,4 @@ #!/bin/bash -xe poetry run isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --use-parentheses --line-width=100 --recursive isort/ test_isort.py -poetry run black isort/ tests_isort.py -l 100 +poetry run black isort/ test_isort.py -l 100 From ae02ab9c07830ca725317781c6e67f5789b1c9c3 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 12 Oct 2019 00:09:01 -0700 Subject: [PATCH 0076/1439] Update to new 100 line length limit. Re-run black --- isort/__init__.py | 22 +- isort/compat.py | 43 +--- isort/finders.py | 36 +-- isort/format.py | 13 +- isort/hooks.py | 41 +-- isort/isort.py | 328 ++++++------------------ isort/main.py | 117 +++------ isort/natural.py | 19 +- isort/pylama_isort.py | 7 +- isort/settings.py | 96 ++----- isort/utils.py | 4 +- test_isort.py | 577 ++++++++++-------------------------------- 12 files changed, 300 insertions(+), 1003 deletions(-) diff --git a/isort/__init__.py b/isort/__init__.py index 804216cea..11f9b2564 100644 --- a/isort/__init__.py +++ b/isort/__init__.py @@ -1,24 +1,4 @@ -"""__init__.py. - -Defines the isort module to include the SortImports utility class as well as any defined settings. - -Copyright (C) 2013 Timothy Edmund Crosley - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF -CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - -""" +"""Defines the public isort interface""" from . import settings # noqa: F401 from .compat import SortImports # noqa: F401 diff --git a/isort/compat.py b/isort/compat.py index 9e3e6e527..07d748202 100644 --- a/isort/compat.py +++ b/isort/compat.py @@ -52,9 +52,7 @@ def resolve(path: Path) -> Path: return Path(os.path.abspath(str(path))) -def get_settings_path( - settings_path: Optional[Path], current_file_path: Optional[Path] -) -> Path: +def get_settings_path(settings_path: Optional[Path], current_file_path: Optional[Path]) -> Path: if settings_path: return settings_path @@ -111,9 +109,7 @@ def __init__( if self.config["verbose"]: print( "WARNING: {} was skipped as it's listed in 'skip' setting" - " or matches a glob in 'skip_glob' setting".format( - absolute_file_path - ) + " or matches a glob in 'skip_glob' setting".format(absolute_file_path) ) file_contents = None @@ -134,9 +130,7 @@ def __init__( print( "WARNING: {} was skipped as it couldn't be opened with the given " "{} encoding or {} fallback encoding".format( - str(absolute_file_path), - file_encoding, - fallback_encoding, + str(absolute_file_path), file_encoding, fallback_encoding ) ) else: @@ -170,20 +164,13 @@ def __init__( in_lines_without_top_comment = ( self.sorted_imports.get_in_lines_without_top_comment() ) - compile( - in_lines_without_top_comment, logging_file_path, "exec", 0, 1 - ) + compile(in_lines_without_top_comment, logging_file_path, "exec", 0, 1) print( - "ERROR: {} isort would have introduced syntax errors, please report to the project!".format( - logging_file_path - ) + "ERROR: {} isort would have introduced syntax errors, " + "please report to the project!".format(logging_file_path) ) except SyntaxError: - print( - "ERROR: {} File contains syntax errors.".format( - logging_file_path - ) - ) + print("ERROR: {} File contains syntax errors.".format(logging_file_path)) return @@ -204,9 +191,7 @@ def __init__( if show_diff or self.config["show_diff"]: show_unified_diff( - file_input=file_contents, - file_output=self.output, - file_path=self.file_path, + file_input=file_contents, file_output=self.output, file_path=self.file_path ) elif write_to_stdout: @@ -219,19 +204,13 @@ def __init__( if ask_to_apply: show_unified_diff( - file_input=file_contents, - file_output=self.output, - file_path=self.file_path, - ) - apply_changes = ask_whether_to_apply_changes_to_file( - str(self.file_path) + file_input=file_contents, file_output=self.output, file_path=self.file_path ) + apply_changes = ask_whether_to_apply_changes_to_file(str(self.file_path)) if not apply_changes: return - with self.file_path.open( - "w", encoding=file_encoding, newline="" - ) as output_file: + with self.file_path.open("w", encoding=file_encoding, newline="") as output_file: if not self.config["quiet"]: print("Fixing {}".format(self.file_path)) diff --git a/isort/finders.py b/isort/finders.py index 9e891ab21..410442c20 100644 --- a/isort/finders.py +++ b/isort/finders.py @@ -115,9 +115,7 @@ def _parse_known_pattern(self, pattern: str) -> List[str]: def find(self, module_name: str) -> Optional[str]: # Try to find most specific placement instruction match (if any) parts = module_name.split(".") - module_names_to_check = ( - ".".join(parts[:first_k]) for first_k in range(len(parts), 0, -1) - ) + module_names_to_check = (".".join(parts[:first_k]) for first_k in range(len(parts), 0, -1)) for module_name_to_check in module_names_to_check: for pattern, placement in self.known_patterns: if pattern.match(module_name_to_check): @@ -135,9 +133,7 @@ def __init__(self, config: Mapping[str, Any], sections: Any) -> None: self.paths = [root_dir, src_dir] # virtual env - self.virtual_env = self.config.get("virtual_env") or os.environ.get( - "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 = "" @@ -154,9 +150,7 @@ def __init__(self, config: Mapping[str, Any], sections: Any) -> None: self.paths.append(path) # conda - self.conda_env = ( - self.config.get("conda_env") or os.environ.get("CONDA_PREFIX") or "" - ) + self.conda_env = self.config.get("conda_env") or os.environ.get("CONDA_PREFIX") or "" if self.conda_env: self.conda_env = os.path.realpath(self.conda_env) for path in glob("{}/lib/python*/site-packages".format(self.conda_env)): @@ -188,9 +182,7 @@ def find(self, module_name: str) -> Optional[str]: or 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 - ) + is_package = exists_case_sensitive(package_path) and os.path.isdir(package_path) if is_module or is_package: if "site-packages" in prefix: return self.sections.THIRDPARTY @@ -209,9 +201,7 @@ def find(self, module_name: str) -> Optional[str]: class ReqsBaseFinder(BaseFinder): enabled = False - def __init__( - self, config: Mapping[str, Any], sections: Any, path: str = "." - ) -> None: + def __init__(self, config: Mapping[str, Any], sections: Any, path: str = ".") -> None: super().__init__(config, sections) self.path = path if self.enabled: @@ -404,9 +394,10 @@ def __init__( # if one finder fails to instantiate isort can continue using the rest if self.verbose: print( - "{} encountered an error ({}) during instantiation and cannot be used".format( - finder_cls.__name__, str(exception) - ) + ( + "{} encountered an error ({}) during " + "instantiation and cannot be used" + ).format(finder_cls.__name__, str(exception)) ) self.finders = tuple(finders) # type: Tuple[BaseFinder, ...] @@ -415,12 +406,13 @@ def find(self, module_name: str) -> Optional[str]: try: section = finder.find(module_name) except Exception as exception: - # isort has to be able to keep trying to identify the correct import section even if one approach fails + # isort has to be able to keep trying to identify the correct + # import section even if one approach fails if self.verbose: print( - "{} encountered an error ({}) while trying to identify the {} module".format( - finder.__class__.__name__, str(exception), module_name - ) + ( + "{} encountered an error ({}) while trying to identify the {}" " module" + ).format(finder.__class__.__name__, str(exception), module_name) ) if section is not None: return section diff --git a/isort/format.py b/isort/format.py index b65059014..8dd7a5eef 100644 --- a/isort/format.py +++ b/isort/format.py @@ -28,14 +28,10 @@ def format_natural(import_line: str) -> str: return import_line -def show_unified_diff( - *, file_input: str, file_output: str, file_path: Optional[Path] -) -> None: +def show_unified_diff(*, file_input: str, file_output: str, file_path: Optional[Path]) -> None: file_name = "" if file_path is None else str(file_path) file_mtime = str( - datetime.now() - if file_path is None - else datetime.fromtimestamp(file_path.stat().st_mtime) + datetime.now() if file_path is None else datetime.fromtimestamp(file_path.stat().st_mtime) ) unified_diff_lines = unified_diff( @@ -53,9 +49,8 @@ def show_unified_diff( def ask_whether_to_apply_changes_to_file(file_path: str) -> bool: answer = None while answer not in ("yes", "y", "no", "n", "quit", "q"): - answer = input( - "Apply suggested changes to '{}' [y/n/q]? ".format(file_path) - ).lower() + answer = input("Apply suggested changes to '{}' [y/n/q]? ".format(file_path)) # nosec + answer = answer.lower() if answer in ("no", "n"): return False if answer in ("quit", "q"): diff --git a/isort/hooks.py b/isort/hooks.py index 621fb0d23..db9904cb8 100644 --- a/isort/hooks.py +++ b/isort/hooks.py @@ -1,28 +1,9 @@ -"""isort.py. - -Defines a git hook to allow pre-commit warnings and errors about import order. +"""Defines a git hook to allow pre-commit warnings and errors about import order. usage: exit_code = git_hook(strict=True|False, modify=True|False) - -Copyright (C) 2015 Helen Sherwood-Taylor - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF -CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - """ -import subprocess +import subprocess # nosec - Needed for hook from typing import List from isort import SortImports @@ -35,7 +16,7 @@ def get_output(command: List[str]) -> str: :param str command: the command to run :returns: the stdout output of the command """ - result = subprocess.run(command, stdout=subprocess.PIPE, check=True) + result = subprocess.run(command, stdout=subprocess.PIPE, check=True) # nosec - trusted input return result.stdout.decode() @@ -65,13 +46,7 @@ def git_hook(strict: bool = False, modify: bool = False) -> int: """ # Get list of files modified and staged - diff_cmd = [ - "git", - "diff-index", - "--cached", - "--name-only", - "--diff-filter=ACMRTUXB HEAD", - ] + diff_cmd = ["git", "diff-index", "--cached", "--name-only", "--diff-filter=ACMRTUXB HEAD"] files_modified = get_lines(diff_cmd) errors = 0 @@ -81,15 +56,11 @@ def git_hook(strict: bool = False, modify: bool = False) -> int: staged_cmd = ["git", "show", ":%s" % filename] staged_contents = get_output(staged_cmd) - sort = SortImports( - file_path=filename, file_contents=staged_contents, check=True - ) + sort = SortImports(file_path=filename, file_contents=staged_contents, check=True) if sort.incorrectly_sorted: errors += 1 if modify: - SortImports( - file_path=filename, file_contents=staged_contents, check=False - ) + SortImports(file_path=filename, file_contents=staged_contents, check=False) return errors if strict else 0 diff --git a/isort/isort.py b/isort/isort.py index 6cf04d790..17e10279d 100644 --- a/isort/isort.py +++ b/isort/isort.py @@ -1,44 +1,15 @@ -"""isort.py. - -Exposes a simple library to sort through imports within Python code +"""Exposes a simple library to sort through imports within Python code usage: SortImports(file_name) or: sorted = SortImports(file_contents=file_contents).output - -Copyright (C) 2013 Timothy Edmund Crosley - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF -CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - """ import copy import itertools import re from collections import OrderedDict, defaultdict, namedtuple -from typing import ( - TYPE_CHECKING, - Any, - Dict, - Iterable, - List, - Mapping, - Optional, - Sequence, - Tuple, -) +from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple from isort import utils from isort.format import format_natural, format_simplified @@ -69,9 +40,7 @@ class _SortImports: _import_line_intro_re = re.compile("^(?:from|import) ") _import_line_midline_import_re = re.compile(" import ") - def __init__( - self, file_contents: str, config: Dict[str, Any], extension: str = "py" - ) -> None: + def __init__(self, file_contents: str, config: Dict[str, Any], extension: str = "py") -> None: self.config = config self.extension = extension @@ -80,9 +49,7 @@ def __init__( self.remove_imports = [ format_simplified(removal) for removal in self.config["remove_imports"] ] - self.add_imports = [ - format_natural(addition) for addition in self.config["add_imports"] - ] + self.add_imports = [format_natural(addition) for addition in self.config["add_imports"]] self._section_comments = [ "# " + value for key, value in self.config.items() @@ -93,9 +60,9 @@ def __init__( self.in_lines = file_contents.split(self.line_separator) self.original_num_of_lines = len(self.in_lines) - if ( - self.original_num_of_lines > 1 or self.in_lines[:1] not in ([], [""]) - ) or self.config["force_adds"]: + if (self.original_num_of_lines > 1 or self.in_lines[:1] not in ([], [""])) or self.config[ + "force_adds" + ]: for add_import in self.add_imports: self.in_lines.append(add_import) self.number_of_lines = len(self.in_lines) @@ -133,11 +100,7 @@ def __init__( self.output = self.line_separator.join(self.out_lines) def remove_whitespaces(self, contents: str) -> str: - contents = ( - contents.replace(self.line_separator, "") - .replace(" ", "") - .replace("\x0c", "") - ) + contents = contents.replace(self.line_separator, "").replace(" ", "").replace("\x0c", "") return contents def get_out_lines_without_top_comment(self) -> str: @@ -228,10 +191,7 @@ def _module_key( prefix = "C" 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 - ): + if section_name is None or "length_sort_" + str(section_name).lower() not in config: length_sort = config["length_sort"] else: length_sort = config["length_sort_" + str(section_name).lower()] @@ -241,9 +201,7 @@ def _module_key( length_sort and (str(len(module_name)) + ":" + module_name) or module_name, ) - def _add_comments( - self, comments: Optional[Sequence[str]], original_string: str = "" - ) -> str: + def _add_comments(self, comments: Optional[Sequence[str]], original_string: str = "") -> str: """ Returns a string with comments added if ignore_comments is not set. """ @@ -314,26 +272,12 @@ def _wrap(self, line: str) -> str: else "", ) lines = output.split(self.line_separator) - if self.config["comment_prefix"] in lines[-1] and lines[ - -1 - ].endswith(")"): - line, comment = lines[-1].split( - self.config["comment_prefix"], 1 - ) - lines[-1] = ( - line - + ")" - + self.config["comment_prefix"] - + comment[:-1] - ) + if self.config["comment_prefix"] in lines[-1] and lines[-1].endswith(")"): + line, comment = lines[-1].split(self.config["comment_prefix"], 1) + lines[-1] = line + ")" + self.config["comment_prefix"] + comment[:-1] return self.line_separator.join(lines) - return "{}{}\\{}{}".format( - line, splitter, self.line_separator, cont_line - ) - elif ( - len(line) > self.config["line_length"] - and wrap_mode == settings.WrapModes.NOQA - ): + return "{}{}\\{}{}".format(line, splitter, self.line_separator, cont_line) + elif len(line) > self.config["line_length"] and wrap_mode == settings.WrapModes.NOQA: if "# NOQA" not in line: return "{}{} NOQA".format(line, self.config["comment_prefix"]) @@ -354,8 +298,7 @@ def _add_straight_imports( ): import_definition.append("import {}".format(module)) import_definition.extend( - "import {} as {}".format(module, as_import) - for as_import in self.as_map[module] + "import {} as {}".format(module, as_import) for as_import in self.as_map[module] ) else: import_definition.append("import {}".format(module)) @@ -397,9 +340,7 @@ def _add_from_imports( if not "{}.{}".format(module, line) in self.remove_imports ] - sub_modules = [ - "{}.{}".format(module, from_import) for from_import in from_imports - ] + sub_modules = ["{}.{}".format(module, from_import) for from_import in from_imports] as_imports = { from_import: [ "{} as {}".format(from_import, as_module) @@ -421,9 +362,7 @@ def _add_from_imports( self.config["keep_direct_and_as_imports"] and self.imports[section]["from"][module][from_import] ): - from_imports[(idx + 1) : (idx + 1)] = as_imports.pop( - from_import - ) + from_imports[(idx + 1) : (idx + 1)] = as_imports.pop(from_import) else: from_imports[idx : (idx + 1)] = as_imports.pop(from_import) @@ -441,15 +380,10 @@ def _add_from_imports( single_import_line = self._add_comments( comments, import_start + from_import ) - comment = ( - self.comments["nested"] - .get(module, {}) - .pop(from_import, None) - ) + comment = self.comments["nested"].get(module, {}).pop(from_import, None) if comment: single_import_line += "{} {}".format( - comments and ";" or self.config["comment_prefix"], - comment, + comments and ";" or self.config["comment_prefix"], comment ) if from_import in as_imports: if ( @@ -476,13 +410,9 @@ def _add_from_imports( from_comments = self.comments["straight"].get( "{}.{}".format(module, from_import) ) - above_comments = self.comments["above"]["from"].pop( - module, None - ) + above_comments = self.comments["above"]["from"].pop(module, None) if above_comments: - if section_output and self.config.get( - "ensure_newline_before_comments" - ): + if section_output and self.config.get("ensure_newline_before_comments"): section_output.append("") section_output.extend(above_comments) @@ -492,14 +422,11 @@ def _add_from_imports( ): section_output.append( self._add_comments( - from_comments, - self._wrap(import_start + from_import), + from_comments, self._wrap(import_start + from_import) ) ) section_output.extend( - self._add_comments( - from_comments, self._wrap(import_start + as_import) - ) + self._add_comments(from_comments, self._wrap(import_start + as_import)) for as_import in as_imports[from_import] ) @@ -518,22 +445,15 @@ def _add_from_imports( and not self.config["keep_direct_and_as_imports"] ): continue - comment = ( - self.comments["nested"] - .get(module, {}) - .pop(from_import, None) - ) + comment = self.comments["nested"].get(module, {}).pop(from_import, None) if comment: single_import_line = self._add_comments( comments, import_start + from_import ) single_import_line += "{} {}".format( - comments and ";" or self.config["comment_prefix"], - comment, - ) - above_comments = self.comments["above"]["from"].pop( - module, None + comments and ";" or self.config["comment_prefix"], comment ) + above_comments = self.comments["above"]["from"].pop(module, None) if above_comments: if section_output and self.config.get( "ensure_newline_before_comments" @@ -555,9 +475,7 @@ def _add_from_imports( ): from_import_section.append(from_imports.pop(0)) if star_import: - import_statement = import_start + (", ").join( - from_import_section - ) + import_statement = import_start + (", ").join(from_import_section) else: import_statement = self._add_comments( comments, import_start + (", ").join(from_import_section) @@ -577,7 +495,8 @@ def _add_from_imports( ): do_multiline_reformat = True - # If line too long AND have imports AND we are NOT using GRID or VERTICAL wrap modes + # 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_import_section) > 0 @@ -591,9 +510,7 @@ def _add_from_imports( import_start, from_import_section, comments ) if self.config["multi_line_output"] == settings.WrapModes.GRID: - self.config[ - "multi_line_output" - ] = settings.WrapModes.VERTICAL_GRID + self.config["multi_line_output"] = settings.WrapModes.VERTICAL_GRID try: other_import_statement = self._multi_line_reformat( import_start, from_import_section, comments @@ -604,9 +521,7 @@ def _add_from_imports( ): import_statement = other_import_statement finally: - self.config[ - "multi_line_output" - ] = settings.WrapModes.GRID + self.config["multi_line_output"] = settings.WrapModes.GRID if ( not do_multiline_reformat and len(import_statement) > self.config["line_length"] @@ -616,9 +531,7 @@ def _add_from_imports( if import_statement: above_comments = self.comments["above"]["from"].pop(module, None) if above_comments: - if section_output and self.config.get( - "ensure_newline_before_comments" - ): + if section_output and self.config.get("ensure_newline_before_comments"): section_output.append("") section_output.extend(above_comments) section_output.append(import_statement) @@ -632,12 +545,7 @@ def _multi_line_reformat( indent = self.config["indent"] line_length = self.config["wrap_length"] or self.config["line_length"] import_statement = formatter( - import_start, - copy.copy(from_imports), - dynamic_indent, - indent, - line_length, - comments, + import_start, copy.copy(from_imports), dynamic_indent, indent, line_length, comments ) if self.config["balanced_wrapping"]: lines = import_statement.split(self.line_separator) @@ -647,11 +555,7 @@ def _multi_line_reformat( else: minimum_length = 0 new_import_statement = import_statement - while ( - len(lines[-1]) < minimum_length - and len(lines) == line_count - and line_length > 10 - ): + while len(lines[-1]) < minimum_length and len(lines) == line_count and line_length > 10: import_statement = new_import_statement line_length -= 1 new_import_statement = formatter( @@ -684,9 +588,7 @@ def _add_formatted_imports(self) -> None: self.imports["no_sections"]["straight"].extend( self.imports[section].get("straight", []) ) - self.imports["no_sections"]["from"].update( - self.imports[section].get("from", {}) - ) + self.imports["no_sections"]["from"].update(self.imports[section].get("from", {})) sections = ("no_sections",) output = [] # type: List[str] @@ -695,16 +597,12 @@ def _add_formatted_imports(self) -> None: straight_modules = self.imports[section]["straight"] straight_modules = nsorted( straight_modules, - key=lambda key: self._module_key( - key, self.config, section_name=section - ), + key=lambda key: self._module_key(key, self.config, section_name=section), ) from_modules = self.imports[section]["from"] from_modules = nsorted( from_modules, - key=lambda key: self._module_key( - key, self.config, section_name=section - ), + key=lambda key: self._module_key(key, self.config, section_name=section), ) if self.config["force_sort_within_sections"]: @@ -712,27 +610,15 @@ def _add_formatted_imports(self) -> None: section_output = [] # type: List[str] if self.config["from_first"]: - self._add_from_imports( - from_modules, section, section_output, sort_ignore_case - ) - if ( - self.config["lines_between_types"] - and from_modules - and straight_modules - ): + self._add_from_imports(from_modules, section, section_output, sort_ignore_case) + if self.config["lines_between_types"] and from_modules and straight_modules: section_output.extend([""] * self.config["lines_between_types"]) self._add_straight_imports(straight_modules, section, section_output) else: self._add_straight_imports(straight_modules, section, section_output) - if ( - self.config["lines_between_types"] - and from_modules - and straight_modules - ): + if self.config["lines_between_types"] and from_modules and straight_modules: section_output.extend([""] * self.config["lines_between_types"]) - self._add_from_imports( - from_modules, section, section_output, sort_ignore_case - ) + self._add_from_imports(from_modules, section, section_output, sort_ignore_case) if self.config["force_sort_within_sections"]: @@ -749,9 +635,7 @@ def by_module(line: str) -> str: return "{}{}".format(section, line) # Remove comments - section_output = [ - line for line in section_output if not line.startswith("#") - ] + section_output = [line for line in section_output if not line.startswith("#")] section_output = nsorted(section_output, key=by_module) @@ -777,9 +661,7 @@ def by_module(line: str) -> str: self.place_imports[section_name] = section_output continue - section_title = self.config.get( - "import_heading_" + str(section_name).lower(), "" - ) + section_title = self.config.get("import_heading_" + str(section_name).lower(), "") if section_title: section_comment = "# {}".format(section_title) if ( @@ -805,16 +687,13 @@ def by_module(line: str) -> str: output_at = 0 if self.import_index < self.original_num_of_lines: output_at = self.import_index - elif ( - self._first_comment_index_end != -1 and self._first_comment_index_start <= 2 - ): + elif self._first_comment_index_end != -1 and self._first_comment_index_start <= 2: output_at = self._first_comment_index_end self.out_lines[output_at:0] = output imports_tail = output_at + len(output) while [ - character.strip() - for character in self.out_lines[imports_tail : imports_tail + 1] + character.strip() for character in self.out_lines[imports_tail : imports_tail + 1] ] == [""]: self.out_lines.pop(imports_tail) @@ -864,13 +743,8 @@ def by_module(line: str) -> str: for index, line in enumerate(self.out_lines): new_out_lines.append(line) if line in self.import_placements: - new_out_lines.extend( - self.place_imports[self.import_placements[line]] - ) - if ( - len(self.out_lines) <= index - or self.out_lines[index + 1].strip() != "" - ): + new_out_lines.extend(self.place_imports[self.import_placements[line]]) + if len(self.out_lines) <= index or self.out_lines[index + 1].strip() != "": new_out_lines.append("") self.out_lines = new_out_lines @@ -886,9 +760,7 @@ def _output_grid( statement += "(" + imports.pop(0) while imports: next_import = imports.pop(0) - next_statement = self._add_comments( - comments, statement + ", " + next_import - ) + next_statement = self._add_comments(comments, statement + ", " + next_import) if len(next_statement.split(self.line_separator)[-1]) + 1 > line_length: lines = ["{}{}".format(white_space, next_import.split(" ")[0])] for part in next_import.split(" ")[1:]: @@ -898,9 +770,9 @@ def _output_grid( else: lines[-1] = new_line next_import = self.line_separator.join(lines) - statement = self._add_comments( - comments, "{},".format(statement) - ) + "{}{}".format(self.line_separator, next_import) + statement = self._add_comments(comments, "{},".format(statement)) + "{}{}".format( + self.line_separator, next_import + ) comments = None else: statement += ", " + next_import @@ -916,9 +788,7 @@ def _output_vertical( comments: Sequence[str], ) -> str: first_import = ( - self._add_comments(comments, imports.pop(0) + ",") - + self.line_separator - + white_space + self._add_comments(comments, imports.pop(0) + ",") + self.line_separator + white_space ) return "{}({}{}{})".format( statement, @@ -939,9 +809,7 @@ def _output_hanging_indent( statement += imports.pop(0) while imports: next_import = imports.pop(0) - next_statement = self._add_comments( - comments, statement + ", " + next_import - ) + next_statement = self._add_comments(comments, statement + ", " + next_import) if len(next_statement.split(self.line_separator)[-1]) + 3 > line_length: next_statement = self._add_comments( comments, "{}, \\".format(statement) @@ -979,10 +847,7 @@ def _output_vertical_grid_common( need_trailing_char: bool, ) -> str: statement += ( - self._add_comments(comments, "(") - + self.line_separator - + indent - + imports.pop(0) + self._add_comments(comments, "(") + self.line_separator + indent + imports.pop(0) ) while imports: next_import = imports.pop(0) @@ -1067,21 +932,15 @@ def _output_noqa( len(retval) + len(self.config["comment_prefix"]) + 1 + len(comment_str) <= line_length ): - return "{}{} {}".format( - retval, self.config["comment_prefix"], comment_str - ) + return "{}{} {}".format(retval, self.config["comment_prefix"], comment_str) else: if len(retval) <= line_length: return retval if comments: if "NOQA" in comments: - return "{}{} {}".format( - retval, self.config["comment_prefix"], comment_str - ) + return "{}{} {}".format(retval, self.config["comment_prefix"], comment_str) else: - return "{}{} NOQA {}".format( - retval, self.config["comment_prefix"], comment_str - ) + return "{}{} NOQA {}".format(retval, self.config["comment_prefix"], comment_str) else: return "{}{} NOQA".format(retval, self.config["comment_prefix"]) @@ -1124,10 +983,7 @@ def _skip_line(self, line: str) -> bool: elif self._in_quote: if line[index : index + len(self._in_quote)] == self._in_quote: self._in_quote = False - if ( - self._first_comment_index_end - < self._first_comment_index_start - ): + if self._first_comment_index_end < self._first_comment_index_start: self._first_comment_index_end = self.index elif line[index] in ("'", '"'): long_quote = line[index : index + 3] @@ -1178,11 +1034,7 @@ def _parse(self) -> None: if ";" in line: for part in (part.strip() for part in line.split(";")): - if ( - part - and not part.startswith("from ") - and not part.startswith("import ") - ): + if part and not part.startswith("from ") and not part.startswith("import "): skip_line = True import_type = self._import_type(line) @@ -1201,9 +1053,7 @@ def _parse(self) -> None: nested_comments = {} import_string, comments, new_comments = self._strip_comments(line) line_parts = [ - part - for part in self._strip_syntax(import_string).strip().split(" ") - if part + part for part in self._strip_syntax(import_string).strip().split(" ") if part ] if ( import_type == "from" @@ -1271,15 +1121,13 @@ def _parse(self) -> None: and new_comments ): nested_comments[stripped_line] = comments[-1] - if import_string.strip().endswith( - " import" - ) or line.strip().startswith("import "): + if import_string.strip().endswith(" import") or line.strip().startswith( + "import " + ): import_string += self.line_separator + line else: import_string = ( - import_string.rstrip().rstrip("\\") - + " " - + line.lstrip() + import_string.rstrip().rstrip("\\") + " " + line.lstrip() ) if import_type == "from": @@ -1321,9 +1169,7 @@ def _parse(self) -> None: if placed_module == "": print( "WARNING: could not place module {} of line {} --" - " Do you need to define a default section?".format( - import_from, line - ) + " Do you need to define a default section?".format(import_from, line) ) root = self.imports[placed_module][import_type] for import_name in imports: @@ -1334,14 +1180,11 @@ def _parse(self) -> None: ] = associated_comment comments.pop(comments.index(associated_comment)) if comments: - self.comments["from"].setdefault(import_from, []).extend( - comments - ) + self.comments["from"].setdefault(import_from, []).extend(comments) if ( len(self.out_lines) - > max(self.import_index, self._first_comment_index_end + 1, 1) - - 1 + > max(self.import_index, self._first_comment_index_end + 1, 1) - 1 ): last = self.out_lines and self.out_lines[-1].rstrip() or "" while ( @@ -1350,16 +1193,12 @@ def _parse(self) -> None: and not last.endswith("'''") and "isort:imports-" not in last ): - self.comments["above"]["from"].setdefault( - import_from, [] - ).insert(0, self.out_lines.pop(-1)) + self.comments["above"]["from"].setdefault(import_from, []).insert( + 0, self.out_lines.pop(-1) + ) if ( len(self.out_lines) - > max( - self.import_index - 1, - self._first_comment_index_end + 1, - 1, - ) + > max(self.import_index - 1, self._first_comment_index_end + 1, 1) - 1 ): last = self.out_lines[-1].rstrip() @@ -1376,10 +1215,7 @@ def _parse(self) -> None: ) else: root[import_from].update( - ( - module, - straight_import | root[import_from].get(module, False), - ) + (module, straight_import | root[import_from].get(module, False)) for module in imports ) else: @@ -1390,10 +1226,7 @@ def _parse(self) -> None: if ( len(self.out_lines) - > max( - self.import_index, self._first_comment_index_end + 1, 1 - ) - - 1 + > max(self.import_index, self._first_comment_index_end + 1, 1) - 1 ): last = self.out_lines and self.out_lines[-1].rstrip() or "" @@ -1403,13 +1236,12 @@ def _parse(self) -> None: and not last.endswith("'''") and "isort:imports-" not in last ): - self.comments["above"]["straight"].setdefault( - module, [] - ).insert(0, self.out_lines.pop(-1)) + self.comments["above"]["straight"].setdefault(module, []).insert( + 0, self.out_lines.pop(-1) + ) if ( len(self.out_lines) > 0 - and len(self.out_lines) - != self._first_comment_index_end + and len(self.out_lines) != self._first_comment_index_end ): last = self.out_lines[-1].rstrip() else: @@ -1435,6 +1267,4 @@ def _parse(self) -> None: straight_import |= self.imports[placed_module][import_type].get( module, False ) - self.imports[placed_module][import_type][ - module - ] = straight_import + self.imports[placed_module][import_type][module] = straight_import diff --git a/isort/main.py b/isort/main.py index 128c02bd7..9e991e75c 100644 --- a/isort/main.py +++ b/isort/main.py @@ -1,49 +1,16 @@ -""" Tool for sorting imports alphabetically, and automatically separated into sections. - -Copyright (C) 2013 Timothy Edmund Crosley - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF -CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - -""" +"""Tool for sorting imports alphabetically, and automatically separated into sections.""" import argparse import functools import glob import os import re import sys -from typing import ( - Any, - Dict, - Iterable, - Iterator, - List, - MutableMapping, - Optional, - Sequence, -) +from typing import Any, Dict, Iterable, Iterator, List, MutableMapping, Optional, Sequence import setuptools from isort import SortImports, __version__ -from isort.settings import ( - DEFAULT_SECTIONS, - WrapModes, - default, - file_should_be_skipped, - from_path, -) +from isort.settings import DEFAULT_SECTIONS, WrapModes, default, file_should_be_skipped, from_path INTRO = r""" /#######################################################################\ @@ -117,9 +84,7 @@ def iter_source_code( for path in paths: if os.path.isdir(path): - 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 file_should_be_skipped(dirname, config, dirpath): skipped.append(dirname) @@ -182,17 +147,11 @@ def run(self) -> None: for path in self.distribution_files(): for python_file in glob.iglob(os.path.join(path, "*.py")): try: - incorrectly_sorted = SortImports( - python_file, **arguments - ).incorrectly_sorted + incorrectly_sorted = SortImports(python_file, **arguments).incorrectly_sorted if incorrectly_sorted: wrong_sorted_files = True except OSError as e: - print( - "WARNING: Unable to parse file {} due to {}".format( - python_file, e - ) - ) + print("WARNING: Unable to parse file {} due to {}".format(python_file, e)) if wrong_sorted_files: sys.exit(1) @@ -254,7 +213,8 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: "--combine-star", dest="combine_star", action="store_true", - help="Ensures that if a star import is present, nothing else is imported from that namespace.", + help="Ensures that if a star import is present, " + "nothing else is imported from that namespace.", ) parser.add_argument( "-d", @@ -297,7 +257,8 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: "--future", dest="known_future_library", action="append", - help="Force sortImports to recognize a module as part of the future compatibility libraries.", + help="Force sortImports to recognize a module as part " + "of the future compatibility libraries.", ) parser.add_argument( "-fas", @@ -317,7 +278,8 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: "-ff", "--from-first", dest="from_first", - help="Switches the typical ordering preference, showing from imports first then straight ones.", + help="Switches the typical ordering preference, " + "showing from imports first then straight ones.", ) parser.add_argument( "-fgw", @@ -344,11 +306,7 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: type=str, ) parser.add_argument( - "-j", - "--jobs", - help="Number of files to process in parallel.", - dest="jobs", - type=int, + "-j", "--jobs", help="Number of files to process in parallel.", dest="jobs", type=int ) parser.add_argument( "-k", @@ -360,22 +318,18 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: parser.add_argument( "-l", "--lines", - help="[Deprecated] The max length of an import line (used for wrapping " - "long imports).", + help="[Deprecated] The max length of an import line (used for wrapping " "long imports).", dest="line_length", type=int, ) - parser.add_argument( - "-lai", "--lines-after-imports", dest="lines_after_imports", type=int - ) - parser.add_argument( - "-lbt", "--lines-between-types", dest="lines_between_types", type=int - ) + parser.add_argument("-lai", "--lines-after-imports", dest="lines_after_imports", type=int) + parser.add_argument("-lbt", "--lines-between-types", dest="lines_between_types", type=int) parser.add_argument( "-le", "--line-ending", dest="line_ending", - help="Forces line endings to the specified value. If not set, values will be guessed per-file.", + help="Forces line endings to the specified value. " + "If not set, values will be guessed per-file.", ) parser.add_argument( "-ls", @@ -397,7 +351,8 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: "--no-inline-sort", dest="no_inline_sort", action="store_true", - help="Leaves `from` imports with multiple imports 'as-is' (e.g. `from foo import a, c ,b`).", + help="Leaves `from` imports with multiple imports 'as-is' " + "(e.g. `from foo import a, c ,b`).", ) parser.add_argument( "-nlb", @@ -589,12 +544,11 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: "--filter-files", dest="filter_files", action="store_true", - help="Tells isort to filter files even when they are explicitly passed in as part of the command", + help="Tells isort to filter files even when they are explicitly passed in as " + "part of the command", ) parser.add_argument( - "files", - nargs="*", - help="One or more Python source files that need their imports sorted.", + "files", nargs="*", help="One or more Python source files that need their imports sorted." ) parser.add_argument( "-py", @@ -605,9 +559,7 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: "Default is the version of the running interpreter, for instance: -py 3, -py 2.7", ) - arguments = { - key: value for key, value in vars(parser.parse_args(argv)).items() if value - } + arguments = {key: value for key, value in vars(parser.parse_args(argv)).items() if value} if "dont_order_by_type" in arguments: arguments["order_by_type"] = False if arguments.pop("unsafe", False): @@ -623,7 +575,8 @@ def main(argv: Optional[Sequence[str]] = None) -> None: if arguments.get("ambiguous_r_flag"): print( - "ERROR: Deprecated -r flag set. This flag has been replaced with -rm to remove ambiguity between it and " + "ERROR: Deprecated -r flag set. " + "This flag has been replaced with -rm to remove ambiguity between it and " "-rc for recursive" ) sys.exit(1) @@ -632,26 +585,18 @@ def main(argv: Optional[Sequence[str]] = None) -> None: 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)) + os.path.abspath(sp) if os.path.isdir(sp) else os.path.dirname(os.path.abspath(sp)) ) if not os.path.isdir(arguments["settings_path"]): print( - "WARNING: settings_path dir does not exist: {}".format( - arguments["settings_path"] - ) + "WARNING: settings_path dir does not exist: {}".format(arguments["settings_path"]) ) if "virtual_env" in arguments: venv = arguments["virtual_env"] arguments["virtual_env"] = os.path.abspath(venv) if not os.path.isdir(arguments["virtual_env"]): - print( - "WARNING: virtual_env dir does not exist: {}".format( - arguments["virtual_env"] - ) - ) + print("WARNING: virtual_env dir does not exist: {}".format(arguments["virtual_env"])) file_names = arguments.pop("files", []) if file_names == ["-"]: @@ -664,9 +609,7 @@ def main(argv: Optional[Sequence[str]] = None) -> None: arguments["ask_to_apply"] = True config = from_path( - arguments.get("settings_path", "") - or os.path.abspath(file_names[0]) - or os.getcwd() + arguments.get("settings_path", "") or os.path.abspath(file_names[0]) or os.getcwd() ).copy() config.update(arguments) wrong_sorted_files = False diff --git a/isort/natural.py b/isort/natural.py index 81a81f607..7e526f9bf 100644 --- a/isort/natural.py +++ b/isort/natural.py @@ -9,21 +9,6 @@ Implementation originally from @HappyLeapSecond stack overflow user in response to: https://stackoverflow.com/questions/5967500/how-to-correctly-sort-a-string-with-a-number-inside - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF -CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - """ import re from typing import Any, Callable, Iterable, List, Optional @@ -37,9 +22,7 @@ def _natural_keys(text: str) -> List[Any]: return [_atoi(c) for c in re.split(r"(\d+)", text)] -def nsorted( - to_sort: Iterable[str], key: Optional[Callable[[str], Any]] = None -) -> List[str]: +def nsorted(to_sort: Iterable[str], key: Optional[Callable[[str], Any]] = None) -> List[str]: """Returns a naturally sorted list""" if key is None: key_callback = _natural_keys diff --git a/isort/pylama_isort.py b/isort/pylama_isort.py index a0b3e9b4d..3fb2ba8fe 100644 --- a/isort/pylama_isort.py +++ b/isort/pylama_isort.py @@ -20,12 +20,7 @@ def run(self, path: str, **meta: Any) -> List[Dict[str, Any]]: if SortImports(path, check=True).incorrectly_sorted: return [ - { - "lnum": 0, - "col": 0, - "text": "Incorrectly sorted imports.", - "type": "ISORT", - } + {"lnum": 0, "col": 0, "text": "Incorrectly sorted imports.", "type": "ISORT"} ] else: return [] diff --git a/isort/settings.py b/isort/settings.py index 6de37e6df..dee036fe4 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -4,46 +4,20 @@ (First from the default setting dictionary at the top of the file, then overridden by any settings in ~/.isort.cfg or $XDG_CONFIG_HOME/isort.cfg if there are any) - -Copyright (C) 2013 Timothy Edmund Crosley - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF -CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - """ import configparser import enum import fnmatch import os -import posixpath import re import sys import warnings from distutils.util import strtobool from functools import lru_cache from pathlib import Path -from typing import ( - Any, - Callable, - Dict, - Iterable, - List, - Mapping, - MutableMapping, - Optional, - Union, -) +from typing import Any, Callable, Dict, Iterable, List, Mapping, MutableMapping, Optional, Union + +import posixpath from .stdlibs import py3, py27 from .utils import difference, union @@ -89,8 +63,8 @@ def from_string(value: str) -> "WrapModes": def _get_default(py_version: Optional[str]) -> Dict[str, Any]: """ - Returns the correct standard library based on either the passed py_version flag or the python interpreter - Additionaly users have the option to pass all as value instead of an + Returns the correct standard library based on either the passed py_version flag or the python + interpreter. Additionaly users have the option to pass all as value instead of an version. As an result code will be checked against both standard libraries - python2 & python3 See Issue 889 and 778 for more information @@ -121,8 +95,7 @@ def _get_default(py_version: Optional[str]) -> Dict[str, Any]: else: raise ValueError( "The python version %s is not supported. " - "You can set a python version with the -py or --python-version flag " - % py_version + "You can set a python version with the -py or --python-version flag " % py_version ) _default["known_standard_library"] = standard_library @@ -130,7 +103,8 @@ def _get_default(py_version: Optional[str]) -> Dict[str, Any]: return _default -# Note that none of these lists must be complete as they are simply fallbacks for when included auto-detection fails. +# Note that none of these lists must be complete as they are simply +# fallbacks for when included auto-detection fails. default = { "force_to_top": [], "skip": [], @@ -188,9 +162,7 @@ def _get_default(py_version: Optional[str]) -> Dict[str, Any]: @lru_cache() -def from_path( - path: Union[str, Path], py_version: Optional[str] = None -) -> Dict[str, Any]: +def from_path(path: Union[str, Path], py_version: Optional[str] = None) -> Dict[str, Any]: computed_settings = _get_default(py_version) isort_defaults = ["~/.isort.cfg"] if appdirs: @@ -200,24 +172,14 @@ def from_path( path = str(path) _update_settings_with_config( - path, - ".editorconfig", - ["~/.editorconfig"], - ("*", "*.py", "**.py"), - computed_settings, - ) - _update_settings_with_config( - path, "pyproject.toml", [], ("tool.isort",), computed_settings + path, ".editorconfig", ["~/.editorconfig"], ("*", "*.py", "**.py"), computed_settings ) + _update_settings_with_config(path, "pyproject.toml", [], ("tool.isort",), computed_settings) _update_settings_with_config( path, ".isort.cfg", isort_defaults, ("settings", "isort"), computed_settings ) - _update_settings_with_config( - path, "setup.cfg", [], ("isort", "tool:isort"), computed_settings - ) - _update_settings_with_config( - path, "tox.ini", [], ("isort", "tool:isort"), computed_settings - ) + _update_settings_with_config(path, "setup.cfg", [], ("isort", "tool:isort"), computed_settings) + _update_settings_with_config(path, "tox.ini", [], ("isort", "tool:isort"), computed_settings) return computed_settings @@ -330,21 +292,15 @@ def _update_with_config_file( if access_key == "sections": computed_settings[access_key] = tuple(_as_list(value)) else: - existing_data = set( - computed_settings.get(access_key, default.get(access_key)) - ) + existing_data = set(computed_settings.get(access_key, default.get(access_key))) if key.startswith("not_"): - computed_settings[access_key] = difference( - existing_data, _as_list(value) - ) + computed_settings[access_key] = difference(existing_data, _as_list(value)) elif key.startswith("known_"): computed_settings[access_key] = union( existing_data, _abspaths(cwd, _as_list(value)) ) else: - computed_settings[access_key] = union( - existing_data, _as_list(value) - ) + computed_settings[access_key] = union(existing_data, _as_list(value)) elif existing_value_type == bool: # Only some configuration formats support native boolean values. if not isinstance(value, bool): @@ -357,9 +313,7 @@ def _update_with_config_file( result = existing_value_type(value) except ValueError: # backwards compat - result = ( - default.get(access_key) if value.lower().strip() == "false" else 2 - ) + result = default.get(access_key) if value.lower().strip() == "false" else 2 computed_settings[access_key] = result else: computed_settings[access_key] = getattr( @@ -370,9 +324,7 @@ def _update_with_config_file( def _as_list(value: str) -> List[str]: if isinstance(value, list): return [item.strip() for item in value] - filtered = [ - item.strip() for item in value.replace("\n", ",").split(",") if item.strip() - ] + filtered = [item.strip() for item in value.replace("\n", ",").split(",") if item.strip()] return filtered @@ -427,9 +379,7 @@ def _get_config_data(file_path: str, sections: Iterable[str]) -> Dict[str, Any]: return settings -def file_should_be_skipped( - filename: str, config: Mapping[str, Any], path: str = "" -) -> bool: +def file_should_be_skipped(filename: str, config: Mapping[str, Any], path: str = "") -> bool: """Returns True if the file and/or folder should be skipped based on the passed in settings.""" os_path = os.path.join(path, filename) @@ -445,9 +395,7 @@ def file_should_be_skipped( return True for skip_path in config["skip"]: - if posixpath.abspath(normalized_path) == posixpath.abspath( - skip_path.replace("\\", "/") - ): + if posixpath.abspath(normalized_path) == posixpath.abspath(skip_path.replace("\\", "/")): return True position = os.path.split(filename) @@ -460,9 +408,7 @@ def file_should_be_skipped( 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) - ): + if not (os.path.isfile(os_path) or os.path.isdir(os_path) or os.path.islink(os_path)): return True return False diff --git a/isort/utils.py b/isort/utils.py index d0414b1e1..e1b4c0ebf 100644 --- a/isort/utils.py +++ b/isort/utils.py @@ -9,8 +9,8 @@ def exists_case_sensitive(path: str) -> bool: 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. + 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") or sys.platform == "darwin") and result: diff --git a/test_isort.py b/test_isort.py index 55e2879c6..7d6608576 100644 --- a/test_isort.py +++ b/test_isort.py @@ -81,16 +81,9 @@ def default_settings_path(tmpdir_factory) -> Iterator[str]: def test_happy_path() -> None: """Test the most basic use case, straight imports no code, simply not organized by category.""" test_input = "import sys\nimport os\nimport myproject.test\nimport django.settings" - test_output = SortImports( - file_contents=test_input, known_third_party=["django"] - ).output + test_output = SortImports(file_contents=test_input, known_third_party=["django"]).output assert test_output == ( - "import os\n" - "import sys\n" - "\n" - "import django.settings\n" - "\n" - "import myproject.test\n" + "import os\n" "import sys\n" "\n" "import django.settings\n" "\n" "import myproject.test\n" ) @@ -127,24 +120,14 @@ def test_correct_space_between_imports() -> None: """ test_input_method = "import sys\ndef my_method():\n print('hello world')\n" test_output_method = SortImports(file_contents=test_input_method).output - assert test_output_method == ( - "import sys\n\n\ndef my_method():\n print('hello world')\n" - ) + assert test_output_method == ("import sys\n\n\ndef my_method():\n print('hello world')\n") test_input_decorator = ( - "import sys\n" - "@my_decorator\n" - "def my_method():\n" - " print('hello world')\n" + "import sys\n" "@my_decorator\n" "def my_method():\n" " print('hello world')\n" ) test_output_decorator = SortImports(file_contents=test_input_decorator).output assert test_output_decorator == ( - "import sys\n" - "\n" - "\n" - "@my_decorator\n" - "def my_method():\n" - " print('hello world')\n" + "import sys\n" "\n" "\n" "@my_decorator\n" "def my_method():\n" " print('hello world')\n" ) test_input_class = "import sys\nclass MyClass(object):\n pass\n" @@ -166,19 +149,11 @@ def test_sort_on_number() -> None: def test_line_length() -> None: """Ensure isort enforces the set line_length.""" assert ( - len( - SortImports(file_contents=REALLY_LONG_IMPORT, line_length=80).output.split( - "\n" - )[0] - ) + len(SortImports(file_contents=REALLY_LONG_IMPORT, line_length=80).output.split("\n")[0]) <= 80 ) assert ( - len( - SortImports(file_contents=REALLY_LONG_IMPORT, line_length=120).output.split( - "\n" - )[0] - ) + len(SortImports(file_contents=REALLY_LONG_IMPORT, line_length=120).output.split("\n")[0]) <= 120 ) @@ -243,9 +218,7 @@ def test_line_length() -> None: def test_output_modes() -> None: """Test setting isort to use various output modes works as expected""" test_output_grid = SortImports( - file_contents=REALLY_LONG_IMPORT, - multi_line_output=WrapModes.GRID, - line_length=40, + file_contents=REALLY_LONG_IMPORT, multi_line_output=WrapModes.GRID, line_length=40 ).output assert test_output_grid == ( "from third_party import (lib1, lib2,\n" @@ -262,9 +235,7 @@ def test_output_modes() -> None: ) test_output_vertical = SortImports( - file_contents=REALLY_LONG_IMPORT, - multi_line_output=WrapModes.VERTICAL, - line_length=40, + file_contents=REALLY_LONG_IMPORT, multi_line_output=WrapModes.VERTICAL, line_length=40 ).output assert test_output_vertical == ( "from third_party import (lib1,\n" @@ -495,18 +466,13 @@ def test_qa_comment_case() -> None: test_output = SortImports( file_contents=test_input, line_length=40, multi_line_output=WrapModes.NOQA ).output - assert ( - test_output - == "from veryveryveryveryveryveryveryveryveryveryvery import X # NOQA\n" - ) + assert test_output == "from veryveryveryveryveryveryveryveryveryveryvery import X # NOQA\n" test_input = "import veryveryveryveryveryveryveryveryveryveryvery # NOQA" test_output = SortImports( file_contents=test_input, line_length=40, multi_line_output=WrapModes.NOQA ).output - assert ( - test_output == "import veryveryveryveryveryveryveryveryveryveryvery # NOQA\n" - ) + assert test_output == "import veryveryveryveryveryveryveryveryveryveryvery # NOQA\n" def test_length_sort() -> None: @@ -644,9 +610,7 @@ def test_use_parentheses() -> None: "from fooooooooooooooooooooooooo.baaaaaaaaaaaaaaaaaaarrrrrrr import " " my_custom_function as my_special_function" ) - test_output = SortImports( - file_contents=test_input, line_length=79, use_parentheses=True - ).output + test_output = SortImports(file_contents=test_input, line_length=79, use_parentheses=True).output assert test_output == ( "from fooooooooooooooooooooooooo.baaaaaaaaaaaaaaaaaaarrrrrrr import (\n" @@ -654,10 +618,7 @@ def test_use_parentheses() -> None: ) test_output = SortImports( - file_contents=test_input, - line_length=79, - use_parentheses=True, - include_trailing_comma=True, + file_contents=test_input, line_length=79, use_parentheses=True, include_trailing_comma=True ).output assert test_output == ( @@ -700,9 +661,7 @@ def test_skip() -> None: "import sys # isort:skip this import needs to be placed here\n\n\n\n\n\n\n" ) - test_output = SortImports( - file_contents=test_input, known_third_party=["django"] - ).output + test_output = SortImports(file_contents=test_input, known_third_party=["django"]).output assert test_output == ( "import django\n" "\n" @@ -718,10 +677,7 @@ def test_skip_with_file_name() -> None: test_input = "import django\nimport myproject\n" sort_imports = SortImports( - file_path="/baz.py", - file_contents=test_input, - settings_path=os.getcwd(), - skip=["baz.py"], + file_path="/baz.py", file_contents=test_input, settings_path=os.getcwd(), skip=["baz.py"] ) assert sort_imports.skipped assert sort_imports.output is None @@ -792,11 +748,7 @@ def test_add_imports() -> None: file_contents=test_input, add_imports=["from __future__ import print_function"] ).output assert test_output == ( - "from __future__ import print_function\n" - "\n" - "\n" - "class MyClass(object):\n" - " pass\n" + "from __future__ import print_function\n" "\n" "\n" "class MyClass(object):\n" " pass\n" ) # On a file with no content what so ever @@ -815,18 +767,12 @@ def test_add_imports() -> None: def test_remove_imports() -> None: """Ensures removing imports works as expected.""" test_input = "import lib6\nimport lib2\nimport lib5\nimport lib1" - test_output = SortImports( - file_contents=test_input, remove_imports=["lib2", "lib6"] - ).output + test_output = SortImports(file_contents=test_input, remove_imports=["lib2", "lib6"]).output assert test_output == "import lib1\nimport lib5\n" # Using natural syntax test_input = ( - "import lib6\n" - "import lib2\n" - "import lib5\n" - "import lib1\n" - "from lib8 import a" + "import lib6\n" "import lib2\n" "import lib5\n" "import lib1\n" "from lib8 import a" ) test_output = SortImports( file_contents=test_input, @@ -921,10 +867,7 @@ def test_forced_separate() -> None: test_input = "from .foo import bar\n\nfrom .y import ca\n" assert ( SortImports( - file_contents=test_input, - forced_separate=[".y"], - line_length=120, - order_by_type=False, + file_contents=test_input, forced_separate=[".y"], line_length=120, order_by_type=False ).output == test_input ) @@ -934,28 +877,17 @@ def test_default_section() -> None: """Test to ensure changing the default section works as expected.""" test_input = "import sys\nimport os\nimport myproject.test\nimport django.settings" test_output = SortImports( - file_contents=test_input, - known_third_party=["django"], - default_section="FIRSTPARTY", + file_contents=test_input, known_third_party=["django"], default_section="FIRSTPARTY" ).output assert test_output == ( - "import os\n" - "import sys\n" - "\n" - "import django.settings\n" - "\n" - "import myproject.test\n" + "import os\n" "import sys\n" "\n" "import django.settings\n" "\n" "import myproject.test\n" ) test_output_custom = SortImports( file_contents=test_input, known_third_party=["django"], default_section="STDLIB" ).output assert test_output_custom == ( - "import myproject.test\n" - "import os\n" - "import sys\n" - "\n" - "import django.settings\n" + "import myproject.test\n" "import os\n" "import sys\n" "\n" "import django.settings\n" ) @@ -982,9 +914,7 @@ def test_first_party_overrides_standard_section() -> None: def test_thirdy_party_overrides_standard_section() -> None: """Test to ensure changing the default section works as expected.""" test_input = "import sys\nimport os\nimport profile.test\n" - test_output = SortImports( - file_contents=test_input, known_third_party=["profile"] - ).output + test_output = SortImports(file_contents=test_input, known_third_party=["profile"]).output assert test_output == "import os\nimport sys\n\nimport profile.test\n" @@ -1068,8 +998,7 @@ def test_force_single_line_long_imports() -> None: def test_force_single_line_imports_and_sort_within_sections() -> None: test_input = ( - "from third_party import lib_a, lib_b, lib_d\n" - "from third_party.lib_c import lib1\n" + "from third_party import lib_a, lib_b, lib_d\n" "from third_party.lib_c import lib1\n" ) test_output = SortImports( file_contents=test_input, @@ -1154,9 +1083,7 @@ def test_relative_import_with_space() -> None: def test_multiline_import() -> None: """Test the case where import spawns multiple lines with inconsistent indentation.""" - test_input = ( - "from pkg \\\n import stuff, other_suff \\\n more_stuff" - ) + test_input = "from pkg \\\n import stuff, other_suff \\\n more_stuff" assert SortImports(file_contents=test_input).output == ( "from pkg import more_stuff, other_suff, stuff\n" ) @@ -1170,14 +1097,9 @@ def test_multiline_import() -> None: "forced_separate": "asdf", } # type: Dict[str, Any] expected_output = ( - "from pkg import more_stuff\n" - "from pkg import other_suff\n" - "from pkg import stuff\n" - ) - assert ( - SortImports(file_contents=test_input, **custom_configuration).output - == expected_output + "from pkg import more_stuff\n" "from pkg import other_suff\n" "from pkg import stuff\n" ) + assert SortImports(file_contents=test_input, **custom_configuration).output == expected_output def test_single_multiline() -> None: @@ -1222,9 +1144,7 @@ def test_order_by_type() -> None: "from subprocess import PIPE, Popen, STDOUT\n" ) - assert SortImports( - file_contents=test_input, order_by_type=True, py_version="2.7" - ).output == ( + assert SortImports(file_contents=test_input, order_by_type=True, py_version="2.7").output == ( "import glob\n" "import os\n" "import shutil\n" @@ -1240,9 +1160,7 @@ def test_custom_lines_after_import_section() -> None: test_input = "from a import b\nfoo = 'bar'\n" # default case is one space if not method or class after imports - assert SortImports(file_contents=test_input).output == ( - "from a import b\n\nfoo = 'bar'\n" - ) + assert SortImports(file_contents=test_input).output == ("from a import b\n\nfoo = 'bar'\n") # test again with a custom number of lines after the import section assert SortImports(file_contents=test_input, lines_after_imports=2).output == ( @@ -1254,9 +1172,7 @@ def test_smart_lines_after_import_section() -> None: """Tests the default 'smart' behavior for dealing with lines after the import section""" # one space if not method or class after imports test_input = "from a import b\nfoo = 'bar'\n" - assert SortImports(file_contents=test_input).output == ( - "from a import b\n\nfoo = 'bar'\n" - ) + assert SortImports(file_contents=test_input).output == ("from a import b\n\nfoo = 'bar'\n") # two spaces if a method or class after imports test_input = "from a import b\ndef my_function():\n pass\n" @@ -1272,10 +1188,7 @@ def test_smart_lines_after_import_section() -> None: # two spaces if a method or class after imports - even if comment before function test_input = ( - "from a import b\n" - "# comment should be ignored\n" - "def my_function():\n" - " pass\n" + "from a import b\n" "# comment should be ignored\n" "def my_function():\n" " pass\n" ) assert SortImports(file_contents=test_input).output == ( "from a import b\n" @@ -1309,32 +1222,19 @@ def test_smart_lines_after_import_section() -> None: # Ensure logic doesn't incorrectly skip over assignments to multi-line strings test_input = 'from a import b\nX = """test\n"""\ndef my_function():\n pass\n' assert SortImports(file_contents=test_input).output == ( - "from a import b\n" - "\n" - 'X = """test\n' - '"""\n' - "def my_function():\n" - " pass\n" + "from a import b\n" "\n" 'X = """test\n' '"""\n' "def my_function():\n" " pass\n" ) def test_settings_combine_instead_of_overwrite() -> None: """Test to ensure settings combine logically, instead of fully overwriting.""" assert set( - SortImports(known_standard_library=["not_std_library"]).config[ - "known_standard_library" - ] + SortImports(known_standard_library=["not_std_library"]).config["known_standard_library"] ) == set(SortImports().config["known_standard_library"] + ["not_std_library"]) assert set( - SortImports(not_known_standard_library=["thread"]).config[ - "known_standard_library" - ] - ) == { - item - for item in SortImports().config["known_standard_library"] - if item != "thread" - } + SortImports(not_known_standard_library=["thread"]).config["known_standard_library"] + ) == {item for item in SortImports().config["known_standard_library"] if item != "thread"} def test_combined_from_and_as_imports() -> None: @@ -1344,15 +1244,11 @@ def test_combined_from_and_as_imports() -> None: "from translate.storage import base, factory\n" "from translate.storage.placeables import general, parse as rich_parse\n" ) - assert ( - SortImports(file_contents=test_input, combine_as_imports=True).output - == test_input - ) + assert SortImports(file_contents=test_input, combine_as_imports=True).output == test_input test_input = "import os \nimport os as _os" test_output = "import os\nimport os as _os\n" assert ( - SortImports(file_contents=test_input, keep_direct_and_as_imports=True).output - == test_output + SortImports(file_contents=test_input, keep_direct_and_as_imports=True).output == test_output ) @@ -1383,10 +1279,7 @@ def test_keep_comments() -> None: # Force Single Line From Import test_input = "from foo import bar # comment\n" - assert ( - SortImports(file_contents=test_input, force_single_line=True).output - == test_input - ) + assert SortImports(file_contents=test_input, force_single_line=True).output == test_input # From import test_input = "from foo import bar # My Comment\n" @@ -1400,14 +1293,10 @@ def test_keep_comments() -> None: # Test case where imports comments make imports extend pass the line length test_input = ( - "from a import b # My Comment1\n" - "from a import c # My Comment2\n" - "from a import d\n" + "from a import b # My Comment1\n" "from a import c # My Comment2\n" "from a import d\n" ) assert SortImports(file_contents=test_input, line_length=45).output == ( - "from a import b # My Comment1\n" - "from a import c # My Comment2\n" - "from a import d\n" + "from a import b # My Comment1\n" "from a import c # My Comment2\n" "from a import d\n" ) # Test case where imports with comments will be beyond line length limit @@ -1421,9 +1310,7 @@ def test_keep_comments() -> None: ) # Test that comments are not stripped from 'import ... as ...' by default - test_input = ( - "from a import b as bb # b comment\nfrom a import c as cc # c comment\n" - ) + test_input = "from a import b as bb # b comment\nfrom a import c as cc # c comment\n" assert SortImports(file_contents=test_input).output == test_input # Test that 'import ... as ...' comments are not collected inappropriately @@ -1470,8 +1357,7 @@ def test_include_trailing_comma() -> None: include_trailing_comma=True, ).output assert test_output_grid == ( - "from third_party import (lib1, lib2,\n" - " lib3, lib4,)\n" + "from third_party import (lib1, lib2,\n" " lib3, lib4,)\n" ) test_output_vertical = SortImports( @@ -1494,12 +1380,7 @@ def test_include_trailing_comma() -> None: include_trailing_comma=True, ).output assert test_output_vertical_indent == ( - "from third_party import (\n" - " lib1,\n" - " lib2,\n" - " lib3,\n" - " lib4,\n" - ")\n" + "from third_party import (\n" " lib1,\n" " lib2,\n" " lib3,\n" " lib4,\n" ")\n" ) test_output_vertical_grid = SortImports( @@ -1568,9 +1449,7 @@ def test_similar_to_std_library() -> None: """Test to ensure modules that are named similarly to a standard library import don't end up clobbered""" test_input = "import datetime\n\nimport requests\nimport times\n" assert ( - SortImports( - file_contents=test_input, known_third_party=["requests", "times"] - ).output + SortImports(file_contents=test_input, known_third_party=["requests", "times"]).output == test_input ) @@ -1647,25 +1526,18 @@ def test_auto_detection() -> None: # Issue 157 test_input = "import binascii\nimport os\n\nimport cv2\nimport requests\n" assert ( - SortImports( - file_contents=test_input, known_third_party=["cv2", "requests"] - ).output + SortImports(file_contents=test_input, known_third_party=["cv2", "requests"]).output == test_input ) # alternative solution - assert ( - SortImports(file_contents=test_input, default_section="THIRDPARTY").output - == test_input - ) + assert SortImports(file_contents=test_input, default_section="THIRDPARTY").output == test_input def test_same_line_statements() -> None: """Ensure isort correctly handles the case where a single line contains multiple statements including an import""" test_input = "import pdb; import nose\n" - assert SortImports(file_contents=test_input).output == ( - "import pdb\n\nimport nose\n" - ) + assert SortImports(file_contents=test_input).output == ("import pdb\n\nimport nose\n") test_input = "import pdb; pdb.set_trace()\nimport nose; nose.run()\n" assert SortImports(file_contents=test_input).output == test_input @@ -1690,13 +1562,9 @@ def test_long_line_comments() -> None: def test_tab_character_in_import() -> None: """Ensure isort correctly handles import statements that contain a tab character""" test_input = ( - "from __future__ import print_function\n" - "from __future__ import\tprint_function\n" - ) - assert ( - SortImports(file_contents=test_input).output - == "from __future__ import print_function\n" + "from __future__ import print_function\n" "from __future__ import\tprint_function\n" ) + assert SortImports(file_contents=test_input).output == "from __future__ import print_function\n" def test_split_position() -> None: @@ -1735,13 +1603,9 @@ def test_place_comments() -> None: "import os\n" "import sys\n" ) - test_output = SortImports( - file_contents=test_input, known_third_party=["django"] - ).output + test_output = SortImports(file_contents=test_input, known_third_party=["django"]).output assert test_output == expected_output - test_output = SortImports( - file_contents=test_output, known_third_party=["django"] - ).output + test_output = SortImports(file_contents=test_output, known_third_party=["django"]).output assert test_output == expected_output @@ -1951,9 +1815,7 @@ def test_top_comments() -> None: def test_consistency() -> None: """Ensures consistency of handling even when dealing with non ordered-by-type imports""" test_input = "from sqlalchemy.dialects.postgresql import ARRAY, array\n" - assert ( - SortImports(file_contents=test_input, order_by_type=True).output == test_input - ) + assert SortImports(file_contents=test_input, order_by_type=True).output == test_input def test_force_grid_wrap() -> None: @@ -2009,15 +1871,10 @@ def test_force_grid_wrap_long() -> None: def test_uses_jinja_variables() -> None: """Test a basic set of imports that use jinja variables""" test_input = ( - "import sys\n" - "import os\n" - "import myproject.{ test }\n" - "import django.{ settings }" + "import sys\n" "import os\n" "import myproject.{ test }\n" "import django.{ settings }" ) test_output = SortImports( - file_contents=test_input, - known_third_party=["django"], - known_first_party=["myproject"], + file_contents=test_input, known_third_party=["django"], known_first_party=["myproject"] ).output assert test_output == ( "import os\n" @@ -2028,10 +1885,7 @@ def test_uses_jinja_variables() -> None: "import myproject.{ test }\n" ) - test_input = ( - "import {{ cookiecutter.repo_name }}\n" - "from foo import {{ cookiecutter.bar }}\n" - ) + test_input = "import {{ cookiecutter.repo_name }}\n" "from foo import {{ cookiecutter.bar }}\n" assert SortImports(file_contents=test_input).output == test_input @@ -2066,8 +1920,7 @@ def test_other_file_encodings(tmpdir) -> None: file_contents = "# coding: {}\n\ns = u'ã'\n".format(encoding) tmp_fname.write_binary(file_contents.encode(encoding)) assert ( - SortImports(file_path=str(tmp_fname), settings_path=os.getcwd()).output - == file_contents + SortImports(file_path=str(tmp_fname), settings_path=os.getcwd()).output == file_contents ) @@ -2076,10 +1929,7 @@ def test_encoding_not_in_comment(tmpdir) -> None: tmp_fname = tmpdir.join("test_encoding.py") file_contents = "class Foo\n coding: latin1\n\ns = u'ã'\n".format("utf8") tmp_fname.write_binary(file_contents.encode("utf8")) - assert ( - SortImports(file_path=str(tmp_fname), settings_path=os.getcwd()).output - == file_contents - ) + assert SortImports(file_path=str(tmp_fname), settings_path=os.getcwd()).output == file_contents def test_encoding_not_in_first_two_lines(tmpdir) -> None: @@ -2087,10 +1937,7 @@ def test_encoding_not_in_first_two_lines(tmpdir) -> None: tmp_fname = tmpdir.join("test_encoding.py") file_contents = "\n\n# -*- coding: latin1\n\ns = u'ã'\n".format("utf8") tmp_fname.write_binary(file_contents.encode("utf8")) - assert ( - SortImports(file_path=str(tmp_fname), settings_path=os.getcwd()).output - == file_contents - ) + assert SortImports(file_path=str(tmp_fname), settings_path=os.getcwd()).output == file_contents def test_comment_at_top_of_file() -> None: @@ -2125,9 +1972,7 @@ def test_alphabetic_sorting() -> None: "force_alphabetical_sort_within_sections": True, } # type: Dict[str, Any] - output = SortImports( - file_contents=test_input, known_first_party=["django"], **options - ).output + output = SortImports(file_contents=test_input, known_first_party=["django"], **options).output assert output == test_input test_input = "# -*- coding: utf-8 -*-\nfrom django.db import models\n" @@ -2206,10 +2051,7 @@ def test_sections_parsed_correct(tmpdir) -> None: "from nose import *\n" ) tmpdir.join(".isort.cfg").write(conf_file_data) - assert ( - SortImports(file_contents=test_input, settings_path=str(tmpdir)).output - == correct_output - ) + assert SortImports(file_contents=test_input, settings_path=str(tmpdir)).output == correct_output @pytest.mark.skipif(toml is None, reason="Requires toml package to be installed.") @@ -2243,10 +2085,7 @@ def test_pyproject_conf_file(tmpdir) -> None: "from nose import *\n" ) tmpdir.join("pyproject.toml").write(conf_file_data) - assert ( - SortImports(file_contents=test_input, settings_path=str(tmpdir)).output - == correct_output - ) + assert SortImports(file_contents=test_input, settings_path=str(tmpdir)).output == correct_output def test_alphabetic_sorting_no_newlines() -> None: @@ -2257,15 +2096,7 @@ def test_alphabetic_sorting_no_newlines() -> None: ).output assert test_input == test_output - test_input = ( - "import os\n" - "import unittest\n" - "\n" - "from a import b\n" - "\n" - "\n" - "print(1)\n" - ) + test_input = "import os\n" "import unittest\n" "\n" "from a import b\n" "\n" "\n" "print(1)\n" test_output = SortImports( file_contents=test_input, force_alphabetical_sort_within_sections=True, @@ -2282,9 +2113,7 @@ def test_sort_within_section() -> None: "from foo import bar\n" "from foo.bar import Quux, baz\n" ) - test_output = SortImports( - file_contents=test_input, force_sort_within_sections=True - ).output + test_output = SortImports(file_contents=test_input, force_sort_within_sections=True).output assert test_output == test_input test_input = ( @@ -2442,9 +2271,7 @@ def test_no_additional_lines_issue_358() -> None: def test_import_by_paren_issue_375() -> None: """Test to ensure isort can correctly handle sorting imports where the paren is directly by the import body""" test_input = "from .models import(\n Foo,\n Bar,\n)\n" - assert ( - SortImports(file_contents=test_input).output == "from .models import Bar, Foo\n" - ) + assert SortImports(file_contents=test_input).output == "from .models import Bar, Foo\n" def test_import_by_paren_issue_460() -> None: @@ -2462,9 +2289,7 @@ def test_import_by_paren_issue_460() -> None: def test_function_with_docstring() -> None: """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"] - test_input = ( - "def foo():\n" ' """ Single line triple quoted doctring """\n' " pass\n" - ) + test_input = "def foo():\n" ' """ Single line triple quoted doctring """\n' " pass\n" expected_output = ( "from __future__ import unicode_literals\n" "\n" @@ -2473,10 +2298,7 @@ def test_function_with_docstring() -> None: ' """ Single line triple quoted doctring """\n' " pass\n" ) - assert ( - SortImports(file_contents=test_input, add_imports=add_imports).output - == expected_output - ) + assert SortImports(file_contents=test_input, add_imports=add_imports).output == expected_output def test_plone_style() -> None: @@ -2492,10 +2314,7 @@ def test_plone_style() -> None: "import unittest\n" "import Zope\n" ) - options = { - "force_single_line": True, - "force_alphabetical_sort": True, - } # type: Dict[str, Any] + options = {"force_single_line": True, "force_alphabetical_sort": True} # type: Dict[str, Any] assert SortImports(file_contents=test_input, **options).output == test_input @@ -2566,8 +2385,7 @@ def test_import_inside_class_issue_432() -> None: " pass\n" ) assert ( - SortImports(file_contents=test_input, add_imports=["import baz"]).output - == expected_output + SortImports(file_contents=test_input, add_imports=["import baz"]).output == expected_output ) @@ -2598,19 +2416,21 @@ def test_import_line_mangles_issues_439() -> None: def test_alias_using_paren_issue_466() -> None: """Test to ensure issue #466: Alias causes slash incorrectly is resolved""" - test_input = "from django.db.backends.mysql.base import DatabaseWrapper as MySQLDatabaseWrapper\n" + test_input = ( + "from django.db.backends.mysql.base import DatabaseWrapper as MySQLDatabaseWrapper\n" + ) expected_output = ( "from django.db.backends.mysql.base import (\n" " DatabaseWrapper as MySQLDatabaseWrapper)\n" ) assert ( - SortImports( - file_contents=test_input, line_length=50, use_parentheses=True - ).output + SortImports(file_contents=test_input, line_length=50, use_parentheses=True).output == expected_output ) - test_input = "from django.db.backends.mysql.base import DatabaseWrapper as MySQLDatabaseWrapper\n" + test_input = ( + "from django.db.backends.mysql.base import DatabaseWrapper as MySQLDatabaseWrapper\n" + ) expected_output = ( "from django.db.backends.mysql.base import (\n" " DatabaseWrapper as MySQLDatabaseWrapper\n" @@ -2710,10 +2530,7 @@ def test_import_wraps_with_comment_issue_471() -> None: ) assert ( SortImports( - file_contents=test_input, - line_length=50, - multi_line_output=1, - use_parentheses=True, + file_contents=test_input, line_length=50, multi_line_output=1, use_parentheses=True ).output == expected_output ) @@ -2725,10 +2542,7 @@ def test_import_case_produces_inconsistent_results_issue_472() -> None: "from sqlalchemy.dialects.postgresql import ARRAY\n" "from sqlalchemy.dialects.postgresql import array\n" ) - assert ( - SortImports(file_contents=test_input, force_single_line=True).output - == test_input - ) + assert SortImports(file_contents=test_input, force_single_line=True).output == test_input test_input = "from scrapy.core.downloader.handlers.http import HttpDownloadHandler, HTTPDownloadHandler\n" assert SortImports(file_contents=test_input).output == test_input @@ -2740,10 +2554,7 @@ def test_inconsistent_behavior_in_python_2_and_3_issue_479() -> None: "from future.standard_library import hooks\n" "from workalendar.europe import UnitedKingdom\n" ) - assert ( - SortImports(file_contents=test_input, known_first_party=["future"]).output - == test_input - ) + assert SortImports(file_contents=test_input, known_first_party=["future"]).output == test_input def test_sort_within_section_comments_issue_436() -> None: @@ -2758,8 +2569,7 @@ def test_sort_within_section_comments_issue_436() -> None: "import report\n" ) assert ( - SortImports(file_contents=test_input, force_sort_within_sections=True).output - == test_input + SortImports(file_contents=test_input, force_sort_within_sections=True).output == test_input ) @@ -2768,9 +2578,7 @@ def test_sort_within_sections_with_force_to_top_issue_473() -> None: test_input = "import z\nimport foo\nfrom foo import bar\n" assert ( SortImports( - file_contents=test_input, - force_sort_within_sections=True, - force_to_top=["z"], + file_contents=test_input, force_sort_within_sections=True, force_to_top=["z"] ).output == test_input ) @@ -2815,9 +2623,7 @@ def test_no_extra_lines_issue_557() -> None: ) assert ( SortImports( - file_contents=test_input, - force_alphabetical_sort=True, - force_sort_within_sections=True, + file_contents=test_input, force_alphabetical_sort=True, force_sort_within_sections=True ).output == expected_output ) @@ -2831,9 +2637,7 @@ def test_long_import_wrap_support_with_mode_2() -> None: ) assert ( SortImports( - file_contents=test_input, - multi_line_output=WrapModes.HANGING_INDENT, - line_length=80, + file_contents=test_input, multi_line_output=WrapModes.HANGING_INDENT, line_length=80 ).output == test_input ) @@ -2849,9 +2653,7 @@ def test_pylint_comments_incorrectly_wrapped_issue_571() -> None: "from PyQt5.QtCore import \\\n" " QRegExp # @UnresolvedImport pylint: disable=import-error,useless-suppression\n" ) - assert ( - SortImports(file_contents=test_input, line_length=60).output == expected_output - ) + assert SortImports(file_contents=test_input, line_length=60).output == expected_output def test_ensure_async_methods_work_issue_537() -> None: @@ -2870,18 +2672,14 @@ def test_ensure_as_imports_sort_correctly_within_from_imports_issue_590() -> Non """Test to ensure combination from and as import statements are sorted correct""" test_input = "from os import defpath\nfrom os import pathsep as separator\n" assert ( - SortImports(file_contents=test_input, force_sort_within_sections=True).output - == test_input + SortImports(file_contents=test_input, force_sort_within_sections=True).output == test_input ) test_input = "from os import defpath\nfrom os import pathsep as separator\n" assert SortImports(file_contents=test_input).output == test_input test_input = "from os import defpath\nfrom os import pathsep as separator\n" - assert ( - SortImports(file_contents=test_input, force_single_line=True).output - == test_input - ) + assert SortImports(file_contents=test_input, force_single_line=True).output == test_input def test_ensure_line_endings_are_preserved_issue_493() -> None: @@ -2911,21 +2709,13 @@ def test_not_splitted_sections() -> None: ) assert SortImports(file_contents=test_input).output == test_input - assert SortImports( - file_contents=test_input, no_lines_before=["LOCALFOLDER"] - ).output == ( - stdlib_section - + whiteline - + firstparty_section - + local_section - + whiteline - + statement + assert SortImports(file_contents=test_input, no_lines_before=["LOCALFOLDER"]).output == ( + stdlib_section + whiteline + firstparty_section + local_section + whiteline + statement ) # by default STDLIB and FIRSTPARTY sections are split by THIRDPARTY section, # so don't merge them if THIRDPARTY imports aren't exist assert ( - SortImports(file_contents=test_input, no_lines_before=["FIRSTPARTY"]).output - == test_input + SortImports(file_contents=test_input, no_lines_before=["FIRSTPARTY"]).output == test_input ) # in case when THIRDPARTY section is excluded from sections list, it's ok to merge STDLIB and FIRSTPARTY assert SortImports( @@ -2933,18 +2723,10 @@ def test_not_splitted_sections() -> None: sections=["STDLIB", "FIRSTPARTY", "LOCALFOLDER"], no_lines_before=["FIRSTPARTY"], ).output == ( - stdlib_section - + firstparty_section - + whiteline - + local_section - + whiteline - + statement + stdlib_section + firstparty_section + whiteline + local_section + whiteline + statement ) # it doesn't change output, because stdlib packages don't have any whitelines before them - assert ( - SortImports(file_contents=test_input, no_lines_before=["STDLIB"]).output - == test_input - ) + assert SortImports(file_contents=test_input, no_lines_before=["STDLIB"]).output == test_input def test_no_lines_before_empty_section() -> None: @@ -2966,28 +2748,20 @@ def test_no_inline_sort() -> None: is enabled. If `--force-single-line-imports` flag is enabled, then `--no-inline-sort` is ignored.""" test_input = "from foo import a, c, b\n" assert ( - SortImports( - file_contents=test_input, no_inline_sort=True, force_single_line=False - ).output + SortImports(file_contents=test_input, no_inline_sort=True, force_single_line=False).output == test_input ) assert ( - SortImports( - file_contents=test_input, no_inline_sort=False, force_single_line=False - ).output + SortImports(file_contents=test_input, no_inline_sort=False, force_single_line=False).output == "from foo import a, b, c\n" ) expected = "from foo import a\nfrom foo import b\nfrom foo import c\n" assert ( - SortImports( - file_contents=test_input, no_inline_sort=False, force_single_line=True - ).output + SortImports(file_contents=test_input, no_inline_sort=False, force_single_line=True).output == expected ) assert ( - SortImports( - file_contents=test_input, no_inline_sort=True, force_single_line=True - ).output + SortImports(file_contents=test_input, no_inline_sort=True, force_single_line=True).output == expected ) @@ -3151,22 +2925,15 @@ def test_requirements_finder(tmpdir) -> None: subdir = tmpdir.mkdir("subdir").join("lol.txt") subdir.write("flask") req_file = tmpdir.join("requirements.txt") - req_file.write( - "Django==1.11\n-e git+https://github.com/orsinium/deal.git#egg=deal\n" - ) + req_file.write("Django==1.11\n-e git+https://github.com/orsinium/deal.git#egg=deal\n") si = SortImports(file_contents="") for path in (str(tmpdir), str(subdir)): - finder = finders.RequirementsFinder( - config=si.config, sections=si.sections, path=path - ) + finder = finders.RequirementsFinder(config=si.config, sections=si.sections, path=path) files = list(finder._get_files()) assert len(files) == 1 # file finding assert files[0].endswith("requirements.txt") # file finding - assert set(finder._get_names(str(req_file))) == { - "Django", - "deal", - } # file parsing + assert set(finder._get_names(str(req_file))) == {"Django", "deal"} # file parsing assert finder.find("django") == si.sections.THIRDPARTY # package in reqs assert finder.find("flask") is None # package not in reqs @@ -3176,9 +2943,7 @@ def test_requirements_finder(tmpdir) -> None: assert finder._normalize_name("deal") == "deal" assert finder._normalize_name("Django") == "django" # lowercase assert finder._normalize_name("django_haystack") == "haystack" # mapping - assert ( - finder._normalize_name("Flask-RESTful") == "flask_restful" - ) # conver `-`to `_` + assert finder._normalize_name("Flask-RESTful") == "flask_restful" # conver `-`to `_` req_file.remove() @@ -3234,9 +2999,7 @@ def test_pipfile_finder(tmpdir) -> None: pipfile = tmpdir.join("Pipfile") pipfile.write(PIPFILE) si = SortImports(file_contents="") - finder = finders.PipfileFinder( - config=si.config, sections=si.sections, path=str(tmpdir) - ) + finder = finders.PipfileFinder(config=si.config, sections=si.sections, path=str(tmpdir)) assert set(finder._get_names(str(tmpdir))) == {"Django", "deal"} # file parsing @@ -3248,9 +3011,7 @@ def test_pipfile_finder(tmpdir) -> None: assert finder._normalize_name("deal") == "deal" assert finder._normalize_name("Django") == "django" # lowercase assert finder._normalize_name("django_haystack") == "haystack" # mapping - assert ( - finder._normalize_name("Flask-RESTful") == "flask_restful" - ) # conver `-`to `_` + assert finder._normalize_name("Flask-RESTful") == "flask_restful" # conver `-`to `_` pipfile.remove() @@ -3274,9 +3035,7 @@ def test_path_finder(monkeypatch) -> None: posixpath.join(third_party_prefix, "example_4" + ext_suffix), posixpath.join(os.getcwd(), "example_5.py"), } - monkeypatch.setattr( - "isort.finders.exists_case_sensitive", lambda p: p in imaginary_paths - ) + monkeypatch.setattr("isort.finders.exists_case_sensitive", lambda p: p in imaginary_paths) assert finder.find("example_1") == finder.sections.STDLIB assert finder.find("example_2") == finder.sections.THIRDPARTY assert finder.find("example_3") == finder.sections.THIRDPARTY @@ -3298,12 +3057,8 @@ def test_argument_parsing() -> None: def test_command_line(tmpdir, capfd, multiprocess: bool) -> None: from isort.main import main - tmpdir.join("file1.py").write( - "import re\nimport os\n\nimport contextlib\n\n\nimport isort" - ) - tmpdir.join("file2.py").write( - "import collections\nimport time\n\nimport abc\n\n\nimport isort" - ) + tmpdir.join("file1.py").write("import re\nimport os\n\nimport contextlib\n\n\nimport isort") + tmpdir.join("file2.py").write("import collections\nimport time\n\nimport abc\n\n\nimport isort") arguments = ["-rc", str(tmpdir), "--settings-path", os.getcwd()] if multiprocess: arguments.extend(["--jobs", "2"]) @@ -3346,9 +3101,7 @@ def test_safety_excludes(tmpdir, enabled: bool) -> None: tmpdir.join("victim.py").write("# ...") toxdir = tmpdir.mkdir(".tox") toxdir.join("verysafe.py").write("# ...") - tmpdir.mkdir("lib").mkdir("python3.7").join("importantsystemlibrary.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 = [] # type: List[str] @@ -3429,9 +3182,7 @@ def test_reverse_relative_imports_issue_417() -> None: "from ...ex import metus\n" ) assert ( - SortImports( - file_contents=test_input, force_single_line=True, reverse_relative=True - ).output + SortImports(file_contents=test_input, force_single_line=True, reverse_relative=True).output == test_input ) @@ -3451,24 +3202,15 @@ def test_inconsistent_relative_imports_issue_577() -> None: "from .dolor import consecteur\n" "from .sit import apidiscing\n" ) - assert ( - SortImports(file_contents=test_input, force_single_line=True).output - == test_input - ) + assert SortImports(file_contents=test_input, force_single_line=True).output == test_input def test_unwrap_issue_762() -> None: test_input = "from os.path \\\nimport (join, split)\n" - assert ( - SortImports(file_contents=test_input).output - == "from os.path import join, split\n" - ) + assert SortImports(file_contents=test_input).output == "from os.path import join, split\n" test_input = "from os.\\\n path import (join, split)" - assert ( - SortImports(file_contents=test_input).output - == "from os.path import join, split\n" - ) + assert SortImports(file_contents=test_input).output == "from os.path import join, split\n" def test_multiple_as_imports() -> None: @@ -3477,14 +3219,10 @@ def test_multiple_as_imports() -> None: assert test_output == test_input test_output = SortImports(file_contents=test_input, combine_as_imports=True).output assert test_output == "from a import b as b, b as bb, b as bb_\n" - test_output = SortImports( - file_contents=test_input, keep_direct_and_as_imports=True - ).output + test_output = SortImports(file_contents=test_input, keep_direct_and_as_imports=True).output assert test_output == test_input test_output = SortImports( - file_contents=test_input, - combine_as_imports=True, - keep_direct_and_as_imports=True, + file_contents=test_input, combine_as_imports=True, keep_direct_and_as_imports=True ).output assert test_output == "from a import b as b, b as bb, b as bb_\n" @@ -3495,20 +3233,13 @@ def test_multiple_as_imports() -> None: "from a import b as bb_\n" ) test_output = SortImports(file_contents=test_input).output - assert ( - test_output - == "from a import b as b\nfrom a import b as bb\nfrom a import b as bb_\n" - ) + assert test_output == "from a import b as b\nfrom a import b as bb\nfrom a import b as bb_\n" test_output = SortImports(file_contents=test_input, combine_as_imports=True).output assert test_output == "from a import b as b, b as bb, b as bb_\n" - test_output = SortImports( - file_contents=test_input, keep_direct_and_as_imports=True - ).output + test_output = SortImports(file_contents=test_input, keep_direct_and_as_imports=True).output assert test_output == test_input test_output = SortImports( - file_contents=test_input, - combine_as_imports=True, - keep_direct_and_as_imports=True, + file_contents=test_input, combine_as_imports=True, keep_direct_and_as_imports=True ).output assert test_output == "from a import b, b as b, b as bb, b as bb_\n" @@ -3519,24 +3250,16 @@ def test_multiple_as_imports() -> None: "from a import b as f\n" ) test_output = SortImports(file_contents=test_input).output - assert ( - test_output - == "from a import b as c\nfrom a import b as e\nfrom a import b as f\n" - ) + assert test_output == "from a import b as c\nfrom a import b as e\nfrom a import b as f\n" test_output = SortImports(file_contents=test_input, combine_as_imports=True).output assert test_output == "from a import b as c, b as e, b as f\n" - test_output = SortImports( - file_contents=test_input, keep_direct_and_as_imports=True - ).output + test_output = SortImports(file_contents=test_input, keep_direct_and_as_imports=True).output assert ( test_output == "from a import b\nfrom a import b as c\nfrom a import b as e\nfrom a import b as f\n" ) test_output = SortImports(file_contents=test_input, no_inline_sort=True).output - assert ( - test_output - == "from a import b as c\nfrom a import b as e\nfrom a import b as f\n" - ) + assert test_output == "from a import b as c\nfrom a import b as e\nfrom a import b as f\n" test_output = SortImports( file_contents=test_input, keep_direct_and_as_imports=True, no_inline_sort=True ).output @@ -3545,9 +3268,7 @@ def test_multiple_as_imports() -> None: == "from a import b\nfrom a import b as c\nfrom a import b as e\nfrom a import b as f\n" ) test_output = SortImports( - file_contents=test_input, - combine_as_imports=True, - keep_direct_and_as_imports=True, + file_contents=test_input, combine_as_imports=True, keep_direct_and_as_imports=True ).output assert test_output == "from a import b, b as c, b as e, b as f\n" test_output = SortImports( @@ -3566,9 +3287,7 @@ def test_multiple_as_imports() -> None: test_output = SortImports(file_contents=test_input).output assert test_output == test_input test_output = SortImports( - file_contents=test_input, - combine_as_imports=True, - keep_direct_and_as_imports=True, + file_contents=test_input, combine_as_imports=True, keep_direct_and_as_imports=True ).output assert test_output == test_input @@ -3576,9 +3295,7 @@ def test_multiple_as_imports() -> None: test_output = SortImports(file_contents=test_input).output assert test_output == "import a as a\nimport a as aa\nimport a as aa_\n" test_output = SortImports( - file_contents=test_input, - combine_as_imports=True, - keep_direct_and_as_imports=True, + file_contents=test_input, combine_as_imports=True, keep_direct_and_as_imports=True ).output assert test_output == test_input @@ -3982,9 +3699,7 @@ def test_noqa_issue_679() -> None: assert SortImports(file_contents=test_input).output == test_output -def test_extract_multiline_output_wrap_setting_from_a_config_file( - tmpdir: py.path.local -) -> None: +def test_extract_multiline_output_wrap_setting_from_a_config_file(tmpdir: py.path.local) -> None: editorconfig_contents = ["root = true", " [*.py]", "multi_line_output = 5"] config_file = tmpdir.join(".editorconfig") config_file.write("\n".join(editorconfig_contents)) @@ -4032,12 +3747,7 @@ def test_to_ensure_empty_line_not_added_to_file_start_issue_889() -> None: def test_to_ensure_correctly_handling_of_whitespace_only_issue_811(capsys) -> None: test_input = ( - "import os\n" - "import sys\n" - "\n" - "\x0c\n" - "def my_function():\n" - ' print("hi")\n' + "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() @@ -4054,11 +3764,7 @@ def test_settings_path_skip_issue_909(tmpdir) -> None: base_dir = tmpdir.mkdir("project") config_dir = base_dir.mkdir("conf") config_dir.join(".isort.cfg").write( - "[isort]\n" - "skip =\n" - " file_to_be_skipped.py\n" - "skip_glob =\n" - " *glob_skip*\n" + "[isort]\n" "skip =\n" " file_to_be_skipped.py\n" "skip_glob =\n" " *glob_skip*\n" ) base_dir.join("file_glob_skip.py").write( @@ -4095,9 +3801,7 @@ def test_skip_paths_issue_938(tmpdir) -> None: "skip_glob =\n" " migrations/**.py\n" ) - base_dir.join("dont_skip.py").write( - "import os\n\n" 'print("Hello World")' "\nimport sys\n" - ) + base_dir.join("dont_skip.py").write("import os\n\n" 'print("Hello World")' "\nimport sys\n") migrations_dir = base_dir.mkdir("migrations") migrations_dir.join("file_glob_skip.py").write( @@ -4147,25 +3851,14 @@ def test_failing_file_check_916() -> None: settings = { "known_future_library": "future", "import_heading_future": "FUTURE", - "sections": [ - "FUTURE", - "STDLIB", - "NORDIGEN", - "FIRSTPARTY", - "THIRDPARTY", - "LOCALFOLDER", - ], + "sections": ["FUTURE", "STDLIB", "NORDIGEN", "FIRSTPARTY", "THIRDPARTY", "LOCALFOLDER"], "indent": " ", "multi_line_output": 3, "lines_after_imports": 2, } # type: Dict[str, Any] assert SortImports(file_contents=test_input, **settings).output == expected_output - assert ( - SortImports(file_contents=expected_output, **settings).output == expected_output - ) - assert not SortImports( - file_contents=expected_output, check=True, **settings - ).incorrectly_sorted + assert SortImports(file_contents=expected_output, **settings).output == expected_output + assert not SortImports(file_contents=expected_output, check=True, **settings).incorrectly_sorted def test_import_heading_issue_905() -> None: @@ -4307,9 +4000,7 @@ def test_pyi_formatting_issue_942(tmpdir) -> None: test_input = "import os\n\n\ndef my_method():\n" expected_py_output = test_input.splitlines() expected_pyi_output = "import os\n\ndef my_method():\n".splitlines() - assert ( - SortImports(file_contents=test_input).output.splitlines() == expected_py_output - ) + assert SortImports(file_contents=test_input).output.splitlines() == expected_py_output assert ( SortImports(file_contents=test_input, extension="pyi").output.splitlines() == expected_pyi_output @@ -4317,16 +4008,11 @@ def test_pyi_formatting_issue_942(tmpdir) -> None: source_py = tmpdir.join("source.py") source_py.write(test_input) - assert ( - SortImports(file_path=str(source_py)).output.splitlines() == expected_py_output - ) + assert SortImports(file_path=str(source_py)).output.splitlines() == expected_py_output source_pyi = tmpdir.join("source.pyi") source_pyi.write(test_input) - assert ( - SortImports(file_path=str(source_pyi)).output.splitlines() - == expected_pyi_output - ) + assert SortImports(file_path=str(source_pyi)).output.splitlines() == expected_pyi_output def test_move_class_issue_751() -> None: @@ -4369,10 +4055,7 @@ def test_python_version() -> None: # user is part of the standard library in python 2 output_python_2 = "import os\nimport user\n" - assert ( - SortImports(file_contents=test_input, py_version="2.7").output - == output_python_2 - ) + assert SortImports(file_contents=test_input, py_version="2.7").output == output_python_2 test_input = "import os\nimport xml" From 20ac404b3f821c8056c0bae43e13c26f970046df Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 12 Oct 2019 00:10:12 -0700 Subject: [PATCH 0077/1439] Ignore poetry lock file --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2771f6b0e..d8ec662df 100644 --- a/.gitignore +++ b/.gitignore @@ -68,6 +68,7 @@ pip-selfcheck.json # Python3 Venv Files .venv/ pyvenv.cfg +poetry.lock # mypy .mypy_cache From 635ecd045994f4f5df0ef1b1854de21f4c9860a0 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 12 Oct 2019 01:23:18 -0700 Subject: [PATCH 0078/1439] Add pyproject.toml and remove setup.py --- pyproject.toml | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 48 -------------------------------- 2 files changed, 74 insertions(+), 48 deletions(-) create mode 100644 pyproject.toml delete mode 100755 setup.py diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..8ee8c7ec9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,74 @@ +[tool.poetry] +name = "isort" +version = "4.3.21" +description = "A Python utility / library to sort Python imports." +authors = ["Timothy Crosley "] +license = "MIT" +readme = "README.rst" +repository = "https://github.com/timothycrosley/isort" +website = "http://timothycrosley.github.io/isort/" +keywords = ["Refactor", "Lint", "Imports", "Sort", "Clean"] +classifiers = [ + "Development Status :: 6 - Mature", + "Intended Audience :: Developers", + "Natural Language :: English", + "Environment :: Console", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Topic :: Software Development :: Libraries", + "Topic :: Utilities", +] + +[tool.poetry.dependencies] +python = "^3.5" +appdirs = {version = "^1.4.0", optional = true} +pipreqs = {version = "*", optional = true} +requirementslib = {version = "*", optional = true} +toml = {version = "*", optional = true} +pip-api = {version = "*", optional = true} + +[tool.poetry.extras] +pipfile = ["pipreqs", "requirementslib"] +pyproject = ["toml"] +requirements = ["pipreqs", "pip-api"] +xdg_home = ["appdirs"] + +[tool.poetry.dev-dependencies] +vulture = "^1.0" +bandit = "^1.6" +safety = "^1.8" +flake8-bugbear = "^19.8" +black = {version = "^18.3-alpha.0", allows-prereleases = true, optional = true, python = "^3.6"} +mypy = "^0.730.0" +ipython = "^7.7" +pytest = "^5.0" +pytest-cov = "^2.7" +pytest-mock = "^1.10" +pep8-naming = "^0.8.2" +portray = { version = "^1.3.0", optional = true, python = "^3.6" } +appdirs = "^1.4" +pipfile = "^0.0.2" +pyproject = "^1.3" +requirementslib = "^1.5" +pipreqs = "^0.4.9" +tomlkit = "^0.5.8" + +[tool.poetry.scripts] +isort = "isort.main:main" + +[tool.poetry.plugins."distutils.commands"] +isort = "isort.main:ISortCommand" + +[tool.poetry.plugins."pylama.linter"] +isort = "isort = isort.pylama_isort:Linter" + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" diff --git a/setup.py b/setup.py deleted file mode 100755 index 2ba4164ac..000000000 --- a/setup.py +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env python3 - -from setuptools import find_packages, setup - -with open("README.rst") as f: - readme = f.read() - -setup( - name="isort", - version="4.3.21", - description="A Python utility / library to sort Python imports.", - long_description=readme, - author="Timothy Crosley", - author_email="timothy.crosley@gmail.com", - url="https://github.com/timothycrosley/isort", - license="MIT", - entry_points={ - "console_scripts": ["isort = isort.main:main"], - "distutils.commands": ["isort = isort.main:ISortCommand"], - "pylama.linter": ["isort = isort.pylama_isort:Linter"], - }, - packages=find_packages(), - extras_require={ - "pipfile": ["pipreqs", "requirementslib"], - "pyproject": ["toml"], - "requirements": ["pipreqs", "pip-api"], - "xdg_home": ["appdirs>=1.4.0"], - }, - python_requires=">=3.5", - keywords="Refactor, Python, Python3, Refactoring, Imports, Sort, Clean", - classifiers=[ - "Development Status :: 6 - Mature", - "Intended Audience :: Developers", - "Natural Language :: English", - "Environment :: Console", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy", - "Topic :: Software Development :: Libraries", - "Topic :: Utilities", - ], -) From 19750369829f239e7cf61b2c59f095eaf3c00c79 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 12 Oct 2019 01:31:33 -0700 Subject: [PATCH 0079/1439] Remove tox usage, switch to simpler script based approach --- .travis.yml | 56 +++++++++++++++++------------------------------------ tox.ini | 44 ----------------------------------------- 2 files changed, 18 insertions(+), 82 deletions(-) delete mode 100644 tox.ini diff --git a/.travis.yml b/.travis.yml index 2bd8274ed..7acd4c27f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,46 +1,26 @@ +dist: xenial language: python cache: pip -env: - - global: - - PYTEST_ADDOPTS=-vv -python: 3.7 +install: +- pip3 install poetry +- poetry install matrix: include: - - env: TOXENV=black - - env: TOXENV=isort-check - - env: TOXENV=lint - - env: TOXENV=mypy - - python: 3.5 - env: TOXENV=py35-coverage - - python: 3.6 - env: TOXENV=py36-coverage - - python: 3.7 - env: TOXENV=py37-coverage - - python: 3.8-dev - env: TOXENV=py38-coverage - - python: pypy3 - env: TOXENV=pypy3 - - os: osx - language: generic - env: TOXENV=py36 - -install: -- pip install tox + - os: linux + python: 3.5 + - os: linux + python: 3.6 + - os: linux + python: 3.7 + env: DEPLOY=yes + - os: linux + python: 3.8-dev + - os: osx + language: generic script: - - tox -after_success: - - | - # Report coverage for TOXENV=*-coverage. - if [[ "${TOXENV%-coverage}" != "$TOXENV" ]]; then - set -e - # Add last TOXENV to $PATH. - PATH="$PWD/.tox/${TOXENV##*,}/bin:$PATH" - coverage xml - coverage report -m - bash <(curl -s https://codecov.io/bash) -Z -X gcov -X coveragepy -X search -X xcode -X gcovout -X fix -f coverage.xml -n $TOXENV - set +e - fi - +- bash scripts/test.sh +after_script: +- bash <(curl -s https://codecov.io/bash) deploy: provider: pypi user: timothycrosley diff --git a/tox.ini b/tox.ini deleted file mode 100644 index c0e014e53..000000000 --- a/tox.ini +++ /dev/null @@ -1,44 +0,0 @@ -[tox] -minversion = 2.4 -envlist = - black - isort-check, - lint, - mypy, - py{35,36,37,38,py3} - -[testenv] -deps = - pytest - coverage: pytest-cov - pip==19.1.1 - tomlkit==0.5.3 -extras = - pipfile - pyproject - requirements -setenv = - coverage: PYTEST_ADDOPTS=--cov {env:PYTEST_ADDOPTS:} -commands = pytest {posargs} - -[testenv:black] -deps = black -commands = black -l 100 --check --diff . - -[testenv:isort-check] -basepython = python3 -commands = isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --use-parentheses --line-width=100 --recursive isort/ test_isort.py - -[testenv:lint] -deps = - flake8 - flake8-bugbear -commands = flake8 -skip_install = True - -[testenv:mypy] -basepython = python3 -deps = mypy -commands = - mypy . -skip_install = True From 1e7dd2231e550389531cd9a965f986360f608de7 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 12 Oct 2019 01:34:42 -0700 Subject: [PATCH 0080/1439] Update appveyor to not use tox --- appveyor.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 9c743c61c..38917efaa 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -15,10 +15,11 @@ environment: - TOXENV: "py37-coverage" install: - - pip install tox + - pip3 install poetry + - poetry install test_script: - - tox + - pytest -s --cov=isort/ --cov=tests --cov-report=term-missing ${@} --cov-report html on_success: # Add tox environment to PATH. From f5dc4d41ff5845a63e5a9691f3903554b7673d35 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 12 Oct 2019 12:11:55 -0700 Subject: [PATCH 0081/1439] Simplified windows testing --- appveyor.yml | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 38917efaa..96c84f221 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -5,21 +5,12 @@ init: build: off -environment: - matrix: - - TOXENV: "isort-check" - - TOXENV: "lint" - - TOXENV: "mypy" - - TOXENV: "py35-coverage" - - TOXENV: "py36-coverage" - - TOXENV: "py37-coverage" - install: - pip3 install poetry - poetry install test_script: - - pytest -s --cov=isort/ --cov=tests --cov-report=term-missing ${@} --cov-report html + - pytest -s --cov=isort/ --cov=test_isort.py --cov-report=term-missing ${@} --cov-report html on_success: # Add tox environment to PATH. From c7eca8571ec46ecb24a722c9d37b1552b69b683c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 12 Oct 2019 12:12:19 -0700 Subject: [PATCH 0082/1439] Add pip-api requirement for testing / development --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8ee8c7ec9..0240dcd45 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,10 +55,10 @@ pep8-naming = "^0.8.2" portray = { version = "^1.3.0", optional = true, python = "^3.6" } appdirs = "^1.4" pipfile = "^0.0.2" -pyproject = "^1.3" requirementslib = "^1.5" pipreqs = "^0.4.9" tomlkit = "^0.5.8" +pip_api = "^0.0.12" [tool.poetry.scripts] isort = "isort.main:main" From e53750acf3940ff209ab5866b8b1e2d94b010d33 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 12 Oct 2019 12:12:56 -0700 Subject: [PATCH 0083/1439] Point coverage to correct test location --- test_isort.py | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/test_isort.py b/test_isort.py index 7d6608576..7efe3ab45 100644 --- a/test_isort.py +++ b/test_isort.py @@ -1,24 +1,6 @@ -"""test_isort.py. +"""Tests all major functionality of the isort library -Tests all major functionality of the isort library Should be ran using py.test by simply running py.test in the isort project directory - -Copyright (C) 2013 Timothy Edmund Crosley - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF -CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - """ import os import os.path @@ -3073,7 +3055,7 @@ def test_command_line(tmpdir, capfd, multiprocess: bool) -> None: ) if not sys.platform.startswith("win"): out, err = capfd.readouterr() - assert not err + assert not [error for error in err.split("\n") if error and not "warning:" in error] # it informs us about fixing the files: assert str(tmpdir.join("file1.py")) in out assert str(tmpdir.join("file2.py")) in out From 205214f034fc7813dfcf49588e8a7497a57d1307 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 12 Oct 2019 12:13:07 -0700 Subject: [PATCH 0084/1439] Point coverage to correct test location --- scripts/test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/test.sh b/scripts/test.sh index e9114cccb..183efcf4f 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -1,4 +1,4 @@ #!/bin/bash -xe ./scripts/lint.sh -poetry run pytest -s --cov={{cookiecutter.project_name}}/ --cov=tests --cov-report=term-missing ${@} --cov-report html +poetry run pytest -s --cov=isort/ --cov=test_isort.py --cov-report=term-missing ${@} --cov-report html From 9d27466004f8675f588fedf55e549663addf5602 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 12 Oct 2019 12:52:06 -0700 Subject: [PATCH 0085/1439] Fix tomlkit requirement --- pyproject.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0240dcd45..9901ca3f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,11 +31,12 @@ python = "^3.5" appdirs = {version = "^1.4.0", optional = true} pipreqs = {version = "*", optional = true} requirementslib = {version = "*", optional = true} +tomlkit = {version = "0.5.3", optional = true} toml = {version = "*", optional = true} pip-api = {version = "*", optional = true} [tool.poetry.extras] -pipfile = ["pipreqs", "requirementslib"] +pipfile = ["pipreqs", "tomlkit", "requirementslib"] pyproject = ["toml"] requirements = ["pipreqs", "pip-api"] xdg_home = ["appdirs"] @@ -57,7 +58,7 @@ appdirs = "^1.4" pipfile = "^0.0.2" requirementslib = "^1.5" pipreqs = "^0.4.9" -tomlkit = "^0.5.8" +tomlkit = "0.5.3" pip_api = "^0.0.12" [tool.poetry.scripts] From 882484d35e4fad3542c93118fd27a3d2cd2aa6b4 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 12 Oct 2019 13:00:29 -0700 Subject: [PATCH 0086/1439] Fix conda integration detection of stdlib --- isort/finders.py | 2 ++ isort/settings.py | 3 +-- test_isort.py | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/isort/finders.py b/isort/finders.py index 410442c20..5c23389c0 100644 --- a/isort/finders.py +++ b/isort/finders.py @@ -190,6 +190,8 @@ def find(self, module_name: str) -> Optional[str]: return self.sections.THIRDPARTY if self.virtual_env and self.virtual_env_src in prefix: return self.sections.THIRDPARTY + if os.path.normcase(prefix) == self.stdlib_lib_prefix: + return self.sections.STDLIB if self.conda_env and self.conda_env in prefix: return self.sections.THIRDPARTY if os.path.normcase(prefix).startswith(self.stdlib_lib_prefix): diff --git a/isort/settings.py b/isort/settings.py index dee036fe4..5b8ea3250 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -9,6 +9,7 @@ import enum import fnmatch import os +import posixpath import re import sys import warnings @@ -17,8 +18,6 @@ from pathlib import Path from typing import Any, Callable, Dict, Iterable, List, Mapping, MutableMapping, Optional, Union -import posixpath - from .stdlibs import py3, py27 from .utils import difference, union diff --git a/test_isort.py b/test_isort.py index 7efe3ab45..337d8f0a2 100644 --- a/test_isort.py +++ b/test_isort.py @@ -3017,6 +3017,7 @@ def test_path_finder(monkeypatch) -> None: posixpath.join(third_party_prefix, "example_4" + ext_suffix), posixpath.join(os.getcwd(), "example_5.py"), } + monkeypatch.setattr("isort.finders.exists_case_sensitive", lambda p: p in imaginary_paths) assert finder.find("example_1") == finder.sections.STDLIB assert finder.find("example_2") == finder.sections.THIRDPARTY From 6c44dcb448528c90a76de4e72dc2eb11dfb76226 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 12 Oct 2019 13:03:27 -0700 Subject: [PATCH 0087/1439] Fix pytest run on AppVeyor --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 96c84f221..6a3dba305 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -10,7 +10,7 @@ install: - poetry install test_script: - - pytest -s --cov=isort/ --cov=test_isort.py --cov-report=term-missing ${@} --cov-report html + - poetry run pytest -s --cov=isort/ --cov=test_isort.py --cov-report=term-missing ${@} --cov-report html on_success: # Add tox environment to PATH. From b81c9e2ba4732b9ee3da14ca2a5a9202c54c350c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 12 Oct 2019 13:19:10 -0700 Subject: [PATCH 0088/1439] Fix appveyor test script --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 6a3dba305..124a32ad3 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -10,7 +10,7 @@ install: - poetry install test_script: - - poetry run pytest -s --cov=isort/ --cov=test_isort.py --cov-report=term-missing ${@} --cov-report html + - poetry run pytest -s --cov=isort/ --cov=test_isort.py --cov-report=term-missing --cov-report html on_success: # Add tox environment to PATH. From f1d4813fe1abc001033e2e5d17ab9f58c1c17d61 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 12 Oct 2019 13:28:13 -0700 Subject: [PATCH 0089/1439] Simplify app veyor --- appveyor.yml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 124a32ad3..97b1a3768 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -11,13 +11,3 @@ install: test_script: - poetry run pytest -s --cov=isort/ --cov=test_isort.py --cov-report=term-missing --cov-report html - -on_success: - # Add tox environment to PATH. - - "SET PATH=%CD%\\.tox\\%TOXENV%\\scripts;%PATH%" - - IF NOT "x%TOXENV:-coverage=%"=="x%TOXENV%" ( - pip install codecov && - coverage xml && - coverage report -m && - codecov --required -X gcov pycov search -f coverage.xml -n %TOXENV%-windows - ) From 9499004bf520372a8032221486631b7a58fbcb61 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 12 Oct 2019 22:09:39 -0700 Subject: [PATCH 0090/1439] Remove no longer needed requirements file --- requirements.txt | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index c24a72e32..000000000 --- a/requirements.txt +++ /dev/null @@ -1,8 +0,0 @@ -pytest==4.4.1 -ipython==4.1.2 -appdirs -pipfile -pyproject -requirementslib -pipreqs -tomlkit==0.5.3 From 41d4cb2d7d84eca47b6abaf4981dbab0f653c908 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 12 Oct 2019 23:56:47 -0700 Subject: [PATCH 0091/1439] Switch to portray compatible doc setup / MD usage exclusively --- README.md | 698 ++++++++++++++++++ README.rst | 675 ----------------- docs/contributing/1.-contributing-guide.md | 49 ++ docs/contributing/2.-coding-standard.md | 57 ++ docs/contributing/3.-code-of-conduct.md | 88 +++ .../contributing/4.-acknowledgements.md | 0 pyproject.toml | 2 +- 7 files changed, 893 insertions(+), 676 deletions(-) create mode 100644 README.md delete mode 100644 README.rst create mode 100644 docs/contributing/1.-contributing-guide.md create mode 100644 docs/contributing/2.-coding-standard.md create mode 100644 docs/contributing/3.-code-of-conduct.md rename ACKNOWLEDGEMENTS.md => docs/contributing/4.-acknowledgements.md (100%) diff --git a/README.md b/README.md new file mode 100644 index 000000000..24da9baee --- /dev/null +++ b/README.md @@ -0,0 +1,698 @@ +![isort](https://raw.github.com/timothycrosley/isort/master/logo.png) + +------------------------------------------------------------------------ + +[![PyPI version](https://badge.fury.io/py/isort.svg)](https://badge.fury.io/py/isort) +[![Build Status](https://travis-ci.org/timothycrosley/isort.svg?branch=master)](https://travis-ci.org/timothycrosley/isort) +[![Code coverage Status](https://codecov.io/gh/timothycrosley/isort/branch/develop/graph/badge.svg)](https://codecov.io/gh/timothycrosley/isort) +[![License](https://img.shields.io/github/license/mashape/apistatus.svg)](https://pypi.org/project/hug/) +[![Join the chat at https://gitter.im/timothycrosley/isort](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/timothycrosley/isort?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Downloads](https://pepy.tech/badge/isort)](https://pepy.tech/project/isort) + +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 requires Python 3.5+ to run but +supports formatting Python 2 code too. + +------------------------------------------------------------------------ + +[Get professionally supported isort with the Tidelift +Subscription](https://tidelift.com/subscription/pkg/pypi-isort?utm_source=pypi-isort&utm_medium=referral&utm_campaign=readme) + +Professional support for isort is available as part of the [Tidelift +Subscription](https://tidelift.com/subscription/pkg/pypi-isort?utm_source=pypi-isort&utm_medium=referral&utm_campaign=readme). +Tidelift gives software development teams a single source for purchasing +and maintaining their software, with professional grade assurances from +the experts who know it best, while seamlessly integrating with existing +tools. + +------------------------------------------------------------------------ + +![Example Usage](https://raw.github.com/timothycrosley/isort/develop/example.gif) + +Before isort: + +```python +from my_lib import Object + +print("Hey") + +import os + +from my_lib import Object3 + +from my_lib import Object2 + +import sys + +from third_party import lib15, lib1, lib2, lib3, lib4, lib5, lib6, lib7, lib8, lib9, lib10, lib11, lib12, lib13, lib14 + +import sys + +from __future__ import absolute_import + +from third_party import lib3 + +print("yo") +``` + +After isort: + +```python +from __future__ import absolute_import + +import os +import sys + +from third_party import (lib1, lib2, lib3, lib4, lib5, lib6, lib7, lib8, + lib9, lib10, lib11, lib12, lib13, lib14, lib15) + +from my_lib import Object, Object2, Object3 + +print("Hey") +print("yo") +``` + +Installing isort +================ + +Installing isort is as simple as: + +```bash +pip install isort +``` + +Install isort with requirements.txt support: + +```bash +pip install isort[requirements] +``` + +Install isort with Pipfile support: + +```bash +pip install isort[pipfile] +``` + +Install isort with both formats support: + +```bash +pip install isort[requirements,pipfile] +``` + +Using isort +=========== + +**From the command line**: + +```bash +isort mypythonfile.py mypythonfile2.py +``` + +or recursively: + +```bash +isort -rc . +``` + +*which is equivalent to:* + +```bash +isort **/*.py +``` + +or to see the proposed changes without applying them: + +```bash +isort mypythonfile.py --diff +``` + +Finally, to atomically run isort against a project, only applying +changes if they don't introduce syntax errors do: + +```bash +isort -rc --atomic . +``` + +(Note: this is disabled by default as it keeps isort from being able to +run against code written using a different version of Python) + +**From within Python**: + +```bash +from isort import SortImports + +SortImports("pythonfile.py") +``` + +or: + +```bash +from isort import SortImports + +new_contents = SortImports(file_contents=old_contents).output +``` + +**From within Kate:** + +```bash +ctrl+[ +``` + +or: + +```bash +menu > Python > Sort Imports +``` + +Installing isort's Kate plugin +=============================== + +For KDE 4.13+ / Pate 2.0+: + +```bash +wget https://raw.github.com/timothycrosley/isort/master/kate_plugin/isort_plugin.py --output-document ~/.kde/share/apps/kate/pate/isort_plugin.py +wget https://raw.github.com/timothycrosley/isort/master/kate_plugin/isort_plugin_ui.rc --output-document ~/.kde/share/apps/kate/pate/isort_plugin_ui.rc +wget https://raw.github.com/timothycrosley/isort/master/kate_plugin/katepart_isort.desktop --output-document ~/.kde/share/kde4/services/katepart_isort.desktop +``` + +For all older versions: + +```bash +wget https://raw.github.com/timothycrosley/isort/master/kate_plugin/isort_plugin_old.py --output-document ~/.kde/share/apps/kate/pate/isort_plugin.py +``` + +You will then need to restart kate and enable Python Plugins as well as +the isort plugin itself. + +Installing isort's for your preferred text editor +================================================== + +Several plugins have been written that enable to use isort from within a +variety of text-editors. You can find a full list of them [on the isort +wiki](https://github.com/timothycrosley/isort/wiki/isort-Plugins). +Additionally, I will enthusiastically accept pull requests that include +plugins for other text editors and add documentation for them as I am +notified. + +How does isort work? +==================== + +isort parses specified files for global level import lines (imports +outside of try / except blocks, functions, etc..) and puts them all at +the top of the file grouped together by the type of import: + +- Future +- Python Standard Library +- Third Party +- Current Python Project +- Explicitly Local (. before import, as in: `from . import x`) +- Custom Separate Sections (Defined by forced\_separate list in + configuration file) +- Custom Sections (Defined by sections list in configuration file) + +Inside of each section the imports are sorted alphabetically. isort +automatically removes duplicate python imports, and wraps long from +imports to the specified line length (defaults to 79). + +When will isort not work? +========================= + +If you ever have the situation where you need to have a try / except +block in the middle of top-level imports or if your import order is +directly linked to precedence. + +For example: a common practice in Django settings files is importing \* +from various settings files to form a new settings file. In this case if +any of the imports change order you are changing the settings definition +itself. + +However, you can configure isort to skip over just these files - or even +to force certain imports to the top. + +Configuring isort +================= + +If you find the default isort settings do not work well for your +project, isort provides several ways to adjust the behavior. + +To configure isort for a single user create a `~/.isort.cfg` or +`$XDG_CONFIG_HOME/isort.cfg` file: + +```ini +[settings] +line_length=120 +force_to_top=file1.py,file2.py +skip=file3.py,file4.py +known_future_library=future,pies +known_standard_library=std,std2 +known_third_party=randomthirdparty +known_first_party=mylib1,mylib2 +indent=' ' +multi_line_output=3 +length_sort=1 +forced_separate=django.contrib,django.utils +default_section=FIRSTPARTY +no_lines_before=LOCALFOLDER +``` + +Additionally, you can specify project level configuration simply by +placing a `.isort.cfg` file at the root of your project. isort will look +up to 25 directories up, from the file it is ran against, to find a +project specific configuration. + +Or, if you prefer, you can add an `isort` or `tool:isort` section to +your project's `setup.cfg` or `tox.ini` file with any desired settings. + +You can also add your desired settings under a `[tool.isort]` section in +your `pyproject.toml` file. + +You can then override any of these settings by using command line +arguments, or by passing in override values to the SortImports class. + +Finally, as of version 3.0 isort supports editorconfig files using the +standard syntax defined here: + +Meaning you place any standard isort configuration parameters within a +.editorconfig file under the `*.py` section and they will be honored. + +For a full list of isort settings and their meanings [take a look at the +isort +wiki](https://github.com/timothycrosley/isort/wiki/isort-Settings). + +Multi line output modes +======================= + +You will notice above the \"multi\_line\_output\" setting. This setting +defines how from imports wrap when they extend past the line\_length +limit and has 6 possible settings: + +**0 - Grid** + +```python +from third_party import (lib1, lib2, lib3, + lib4, lib5, ...) +``` + +**1 - Vertical** + +```python +from third_party import (lib1, + lib2, + lib3 + lib4, + lib5, + ...) +``` + +**2 - Hanging Indent** + +```python +from third_party import \ + lib1, lib2, lib3, \ + lib4, lib5, lib6 +``` + +**3 - Vertical Hanging Indent** + +```python +from third_party import ( + lib1, + lib2, + lib3, + lib4, +) +``` + +**4 - Hanging Grid** + +```python +from third_party import ( + lib1, lib2, lib3, lib4, + lib5, ...) +``` + +**5 - Hanging Grid Grouped** + +```python +from third_party import ( + lib1, lib2, lib3, lib4, + lib5, ... +) +``` + +**6 - Hanging Grid Grouped, No Trailing Comma** + +In Mode 5 isort leaves a single extra space to maintain consistency of +output when a comma is added at the end. Mode 6 is the same - except +that no extra space is maintained leading to the possibility of lines +one character longer. You can enforce a trailing comma by using this in +conjunction with `-tc` or `include_trailing_comma: True`. + +```python +from third_party import ( + lib1, lib2, lib3, lib4, + lib5 +) +``` + +**7 - NOQA** + +```python +from third_party import lib1, lib2, lib3, ... # NOQA +``` + +Alternatively, you can set `force_single_line` to `True` (`-sl` on the +command line) and every import will appear on its own line: + +```python +from third_party import lib1 +from third_party import lib2 +from third_party import lib3 +... +``` + +Note: to change the how constant indents appear - simply change the +indent property with the following accepted formats: + +- Number of spaces you would like. For example: 4 would cause standard + 4 space indentation. +- Tab +- A verbatim string with quotes around it. + +For example: + +```python +" " +``` + +is equivalent to 4. + +For the import styles that use parentheses, you can control whether or +not to include a trailing comma after the last import with the +`include_trailing_comma` option (defaults to `False`). + +Intelligently Balanced Multi-line Imports +========================================= + +As of isort 3.1.0 support for balanced multi-line imports has been +added. With this enabled isort will dynamically change the import length +to the one that produces the most balanced grid, while staying below the +maximum import length defined. + +Example: + +```python +from __future__ import (absolute_import, division, + print_function, unicode_literals) +``` + +Will be produced instead of: + +```python +from __future__ import (absolute_import, division, print_function, + unicode_literals) +``` + +To enable this set `balanced_wrapping` to `True` in your config or pass +the `-e` option into the command line utility. + +Custom Sections and Ordering +============================ + +You can change the section order with `sections` option from the default +of: + +```ini +FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER +``` + +to your preference: + +```ini +sections=FUTURE,STDLIB,FIRSTPARTY,THIRDPARTY,LOCALFOLDER +``` + +You also can define your own sections and their order. + +Example: + +```ini +known_django=django +known_pandas=pandas,numpy +sections=FUTURE,STDLIB,DJANGO,THIRDPARTY,PANDAS,FIRSTPARTY,LOCALFOLDER +``` + +would create two new sections with the specified known modules. + +The `no_lines_before` option will prevent the listed sections from being +split from the previous section by an empty line. + +Example: + +```ini +sections=FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER +no_lines_before=LOCALFOLDER +``` + +would produce a section with both FIRSTPARTY and LOCALFOLDER modules +combined. + +Auto-comment import sections +============================ + +Some projects prefer to have import sections uniquely titled to aid in +identifying the sections quickly when visually scanning. isort can +automate this as well. To do this simply set the +`import_heading_{section_name}` setting for each section you wish to +have auto commented - to the desired comment. + +For Example: + +```ini +import_heading_stdlib=Standard Library +import_heading_firstparty=My Stuff +``` + +Would lead to output looking like the following: + +```python +# Standard Library +import os +import sys + +import django.settings + +# My Stuff +import myproject.test +``` + +Ordering by import length +========================= + +isort also makes it easy to sort your imports by length, simply by +setting the `length_sort` option to `True`. This will result in the +following output style: + +```python +from evn.util import ( + Pool, + Dict, + Options, + Constant, + DecayDict, + UnexpectedCodePath, +) +``` + +It is also possible to opt-in to sorting imports by length for only +specific sections by using `length_sort_` followed by the section name +as a configuration item, e.g.: + + length_sort_stdlib=1 + +Skip processing of imports (outside of configuration) +===================================================== + +To make isort ignore a single import simply add a comment at the end of +the import line containing the text `isort:skip`: + +```python +import module # isort:skip +``` + +or: + +```python +from xyz import (abc, # isort:skip + yo, + hey) +``` + +To make isort skip an entire file simply add `isort:skip_file` to the +module's doc string: + +```python +""" my_module.py + Best module ever + + isort:skip_file +""" + +import b +import a +``` + +Adding an import to multiple files +================================== + +isort makes it easy to add an import statement across multiple files, +while being assured it's correctly placed. + +From the command line: + +```bash +isort -a "from __future__ import print_function" *.py +``` + +from within Kate: + +``` +ctrl+] +``` + +or: + +``` +menu > Python > Add Import +``` + +Removing an import from multiple files +====================================== + +isort also makes it easy to remove an import from multiple files, +without having to be concerned with how it was originally formatted. + +From the command line: + +```bash +isort -rm "os.system" *.py +``` + +from within Kate: + +``` +ctrl+shift+] +``` + +or: + +``` +menu > Python > Remove Import +``` + +Using isort to verify code +========================== + +The `--check-only` option +------------------------- + +isort can also be used to used to verify that code is correctly +formatted by running it with `-c`. Any files that contain incorrectly +sorted and/or formatted imports will be outputted to `stderr`. + +```bash +isort **/*.py -c -vb + +SUCCESS: /home/timothy/Projects/Open_Source/isort/isort_kate_plugin.py Everything Looks Good! +ERROR: /home/timothy/Projects/Open_Source/isort/isort/isort.py Imports are incorrectly sorted. +``` + +One great place this can be used is with a pre-commit git hook, such as +this one by \@acdha: + + + +This can help to ensure a certain level of code quality throughout a +project. + +Git hook +-------- + +isort provides a hook function that can be integrated into your Git +pre-commit script to check Python code before committing. + +To cause the commit to fail if there are isort errors (strict mode), +include the following in `.git/hooks/pre-commit`: + +```python +#!/usr/bin/env python +import sys +from isort.hooks import git_hook + +sys.exit(git_hook(strict=True, modify=True)) +``` + +If you just want to display warnings, but allow the commit to happen +anyway, call `git_hook` without the strict parameter. If you want to +display warnings, but not also fix the code, call `git_hook` without the +modify parameter. + +Setuptools integration +---------------------- + +Upon installation, isort enables a `setuptools` command that checks +Python files declared by your project. + +Running `python setup.py isort` on the command line will check the files +listed in your `py_modules` and `packages`. If any warning is found, the +command will exit with an error code: + +```bash +$ python setup.py isort +``` + +Also, to allow users to be able to use the command without having to +install isort themselves, add isort to the setup\_requires of your +`setup()` like so: + +```python +setup( + name="project", + packages=["project"], + + setup_requires=[ + "isort" + ] +) +``` + +Security contact information ========== + +To report a security vulnerability, please use the [Tidelift security +contact](https://tidelift.com/security). Tidelift will coordinate the +fix and disclosure. + +Why isort? +========== + +isort simply stands for import sort. It was originally called +"sortImports" however I got tired of typing the extra characters and +came to the realization camelCase is not pythonic. + +I wrote isort because in an organization I used to work in the manager +came in one day and decided all code must have alphabetically sorted +imports. The code base was huge - and he meant for us to do it by hand. +However, being a programmer - I\'m too lazy to spend 8 hours mindlessly +performing a function, but not too lazy to spend 16 hours automating it. +I was given permission to open source sortImports and here we are :) + +------------------------------------------------------------------------ + +Thanks and I hope you find isort useful! + +~Timothy Crosley diff --git a/README.rst b/README.rst deleted file mode 100644 index abc493345..000000000 --- a/README.rst +++ /dev/null @@ -1,675 +0,0 @@ -.. image:: https://raw.github.com/timothycrosley/isort/master/logo.png - :alt: isort - -######## - -.. image:: https://badge.fury.io/py/isort.svg - :target: https://badge.fury.io/py/isort - :alt: PyPI version - -.. image:: https://travis-ci.org/timothycrosley/isort.svg?branch=master - :target: https://travis-ci.org/timothycrosley/isort - :alt: Build Status - -.. image:: https://codecov.io/gh/timothycrosley/isort/branch/develop/graph/badge.svg - :target: https://codecov.io/gh/timothycrosley/isort - :alt: Code coverage Status - -.. image:: https://img.shields.io/github/license/mashape/apistatus.svg - :target: https://pypi.org/project/hug/ - :alt: License - -.. image:: https://badges.gitter.im/Join%20Chat.svg - :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. - -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 `_ to quickly sort all your imports. -It requires Python 3.5+ to run but supports formatting Python 2 code too. - - -######## - -`Get professionally supported isort with the Tidelift Subscription `_ - -Professional support for isort is available as part of the `Tidelift -Subscription`_. Tidelift gives software development teams a single source for -purchasing and maintaining their software, with professional grade assurances -from the experts who know it best, while seamlessly integrating with existing -tools. - -.. _Tidelift Subscription: https://tidelift.com/subscription/pkg/pypi-isort?utm_source=pypi-isort&utm_medium=referral&utm_campaign=readme - -######## - -.. image:: https://raw.github.com/timothycrosley/isort/develop/example.gif - :alt: Example Usage - -Before isort: - -.. code-block:: python - - from my_lib import Object - - print("Hey") - - import os - - from my_lib import Object3 - - from my_lib import Object2 - - import sys - - from third_party import lib15, lib1, lib2, lib3, lib4, lib5, lib6, lib7, lib8, lib9, lib10, lib11, lib12, lib13, lib14 - - import sys - - from __future__ import absolute_import - - from third_party import lib3 - - print("yo") - -After isort: - -.. code-block:: python - - from __future__ import absolute_import - - import os - import sys - - from third_party import (lib1, lib2, lib3, lib4, lib5, lib6, lib7, lib8, - lib9, lib10, lib11, lib12, lib13, lib14, lib15) - - from my_lib import Object, Object2, Object3 - - print("Hey") - print("yo") - -Installing isort -================ - -Installing isort is as simple as: - -.. code-block:: bash - - pip install isort - -Install isort with requirements.txt support: - -.. code-block:: bash - - pip install isort[requirements] - -Install isort with Pipfile support: - -.. code-block:: bash - - pip install isort[pipfile] - -Install isort with both formats support: - -.. code-block:: bash - - pip install isort[requirements,pipfile] - -Using isort -=========== - -**From the command line**: - -.. code-block:: bash - - isort mypythonfile.py mypythonfile2.py - -or recursively: - -.. code-block:: bash - - isort -rc . - -*which is equivalent to:* - -.. code-block:: bash - - isort **/*.py - -or to see the proposed changes without applying them: - -.. code-block:: bash - - isort mypythonfile.py --diff - -Finally, to atomically run isort against a project, only applying changes if they don't introduce syntax errors do: - -.. code-block:: bash - - isort -rc --atomic . - -(Note: this is disabled by default as it keeps isort from being able to run against code written using a different version of Python) - -**From within Python**: - -.. code-block:: bash - - from isort import SortImports - - SortImports("pythonfile.py") - -or: - -.. code-block:: bash - - from isort import SortImports - - new_contents = SortImports(file_contents=old_contents).output - -**From within Kate:** - -.. code-block:: bash - - ctrl+[ - -or: - -.. code-block:: bash - - menu > Python > Sort Imports - -Installing isort's Kate plugin -============================== - -For KDE 4.13+ / Pate 2.0+: - -.. code-block:: bash - - wget https://raw.github.com/timothycrosley/isort/master/kate_plugin/isort_plugin.py --output-document ~/.kde/share/apps/kate/pate/isort_plugin.py - wget https://raw.github.com/timothycrosley/isort/master/kate_plugin/isort_plugin_ui.rc --output-document ~/.kde/share/apps/kate/pate/isort_plugin_ui.rc - wget https://raw.github.com/timothycrosley/isort/master/kate_plugin/katepart_isort.desktop --output-document ~/.kde/share/kde4/services/katepart_isort.desktop - -For all older versions: - -.. code-block:: bash - - wget https://raw.github.com/timothycrosley/isort/master/kate_plugin/isort_plugin_old.py --output-document ~/.kde/share/apps/kate/pate/isort_plugin.py - -You will then need to restart kate and enable Python Plugins as well as the isort plugin itself. - -Installing isort's for your preferred text editor -================================================= - -Several plugins have been written that enable to use isort from within a variety of text-editors. -You can find a full list of them `on the isort wiki `_. -Additionally, I will enthusiastically accept pull requests that include plugins for other text editors -and add documentation for them as I am notified. - -How does isort work? -==================== - -isort parses specified files for global level import lines (imports outside of try / except blocks, functions, etc..) -and puts them all at the top of the file grouped together by the type of import: - -- Future -- Python Standard Library -- Third Party -- Current Python Project -- Explicitly Local (. before import, as in: ``from . import x``) -- Custom Separate Sections (Defined by forced_separate list in configuration file) -- Custom Sections (Defined by sections list in configuration file) - -Inside of each section the imports are sorted alphabetically. isort automatically removes duplicate python imports, -and wraps long from imports to the specified line length (defaults to 79). - -When will isort not work? -========================= - -If you ever have the situation where you need to have a try / except block in the middle of top-level imports or if -your import order is directly linked to precedence. - -For example: a common practice in Django settings files is importing * from various settings files to form -a new settings file. In this case if any of the imports change order you are changing the settings definition itself. - -However, you can configure isort to skip over just these files - or even to force certain imports to the top. - -Configuring isort -================= - -If you find the default isort settings do not work well for your project, isort provides several ways to adjust -the behavior. - -To configure isort for a single user create a ``~/.isort.cfg`` or ``$XDG_CONFIG_HOME/isort.cfg`` file: - -.. code-block:: ini - - [settings] - line_length=120 - force_to_top=file1.py,file2.py - skip=file3.py,file4.py - known_future_library=future,pies - known_standard_library=std,std2 - known_third_party=randomthirdparty - known_first_party=mylib1,mylib2 - indent=' ' - multi_line_output=3 - length_sort=1 - forced_separate=django.contrib,django.utils - default_section=FIRSTPARTY - no_lines_before=LOCALFOLDER - -Additionally, you can specify project level configuration simply by placing a ``.isort.cfg`` file at the root of your -project. isort will look up to 25 directories up, from the file it is ran against, to find a project specific configuration. - -Or, if you prefer, you can add an ``isort`` or ``tool:isort`` section to your project's ``setup.cfg`` or ``tox.ini`` file with any desired settings. - -You can also add your desired settings under a ``[tool.isort]`` section in your ``pyproject.toml`` file. - -You can then override any of these settings by using command line arguments, or by passing in override values to the -SortImports class. - -Finally, as of version 3.0 isort supports editorconfig files using the standard syntax defined here: -https://editorconfig.org/ - -Meaning you place any standard isort configuration parameters within a .editorconfig file under the ``*.py`` section -and they will be honored. - -For a full list of isort settings and their meanings `take a look at the isort wiki `_. - -Multi line output modes -======================= - -You will notice above the "multi_line_output" setting. This setting defines how from imports wrap when they extend -past the line_length limit and has 6 possible settings: - -**0 - Grid** - -.. code-block:: python - - from third_party import (lib1, lib2, lib3, - lib4, lib5, ...) - -**1 - Vertical** - -.. code-block:: python - - from third_party import (lib1, - lib2, - lib3 - lib4, - lib5, - ...) - -**2 - Hanging Indent** - -.. code-block:: python - - from third_party import \ - lib1, lib2, lib3, \ - lib4, lib5, lib6 - -**3 - Vertical Hanging Indent** - -.. code-block:: python - - from third_party import ( - lib1, - lib2, - lib3, - lib4, - ) - -**4 - Hanging Grid** - -.. code-block:: python - - from third_party import ( - lib1, lib2, lib3, lib4, - lib5, ...) - -**5 - Hanging Grid Grouped** - -.. code-block:: python - - from third_party import ( - lib1, lib2, lib3, lib4, - lib5, ... - ) - -**6 - Hanging Grid Grouped, No Trailing Comma** - -In Mode 5 isort leaves a single extra space to maintain consistency of output when a comma is added at the end. -Mode 6 is the same - except that no extra space is maintained leading to the possibility of lines one character longer. -You can enforce a trailing comma by using this in conjunction with ``-tc`` or ``include_trailing_comma: True``. - -.. code-block:: python - - from third_party import ( - lib1, lib2, lib3, lib4, - lib5 - ) - -**7 - NOQA** - -.. code-block:: python - - from third_party import lib1, lib2, lib3, ... # NOQA - -Alternatively, you can set ``force_single_line`` to ``True`` (``-sl`` on the command line) and every import will appear on its -own line: - -.. code-block:: python - - from third_party import lib1 - from third_party import lib2 - from third_party import lib3 - ... - -Note: to change the how constant indents appear - simply change the indent property with the following accepted formats: - -* Number of spaces you would like. For example: 4 would cause standard 4 space indentation. -* Tab -* A verbatim string with quotes around it. - -For example: - -.. code-block:: python - - " " - -is equivalent to 4. - -For the import styles that use parentheses, you can control whether or not to -include a trailing comma after the last import with the ``include_trailing_comma`` -option (defaults to ``False``). - -Intelligently Balanced Multi-line Imports -========================================= - -As of isort 3.1.0 support for balanced multi-line imports has been added. -With this enabled isort will dynamically change the import length to the one that produces the most balanced grid, -while staying below the maximum import length defined. - -Example: - -.. code-block:: python - - from __future__ import (absolute_import, division, - print_function, unicode_literals) - -Will be produced instead of: - -.. code-block:: python - - from __future__ import (absolute_import, division, print_function, - unicode_literals) - -To enable this set ``balanced_wrapping`` to ``True`` in your config or pass the ``-e`` option into the command line utility. - -Custom Sections and Ordering -============================ - -You can change the section order with ``sections`` option from the default of: - -.. code-block:: ini - - FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER - -to your preference: - -.. code-block:: ini - - sections=FUTURE,STDLIB,FIRSTPARTY,THIRDPARTY,LOCALFOLDER - -You also can define your own sections and their order. - -Example: - -.. code-block:: ini - - known_django=django - known_pandas=pandas,numpy - sections=FUTURE,STDLIB,DJANGO,THIRDPARTY,PANDAS,FIRSTPARTY,LOCALFOLDER - -would create two new sections with the specified known modules. - -The ``no_lines_before`` option will prevent the listed sections from being split from the previous section by an empty line. - -Example: - -.. code-block:: ini - - sections=FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER - no_lines_before=LOCALFOLDER - -would produce a section with both FIRSTPARTY and LOCALFOLDER modules combined. - -Auto-comment import sections -============================ - -Some projects prefer to have import sections uniquely titled to aid in identifying the sections quickly -when visually scanning. isort can automate this as well. To do this simply set the ``import_heading_{section_name}`` -setting for each section you wish to have auto commented - to the desired comment. - -For Example: - -.. code-block:: ini - - import_heading_stdlib=Standard Library - import_heading_firstparty=My Stuff - -Would lead to output looking like the following: - -.. code-block:: python - - # Standard Library - import os - import sys - - import django.settings - - # My Stuff - import myproject.test - -Ordering by import length -========================= - -isort also makes it easy to sort your imports by length, simply by setting the ``length_sort`` option to ``True``. -This will result in the following output style: - -.. code-block:: python - - from evn.util import ( - Pool, - Dict, - Options, - Constant, - DecayDict, - UnexpectedCodePath, - ) - -It is also possible to opt-in to sorting imports by length for only specific -sections by using ``length_sort_`` followed by the section name as a -configuration item, e.g.:: - - length_sort_stdlib=1 - -Skip processing of imports (outside of configuration) -===================================================== - -To make isort ignore a single import simply add a comment at the end of the import line containing the text ``isort:skip``: - -.. code-block:: python - - import module # isort:skip - -or: - -.. code-block:: python - - from xyz import (abc, # isort:skip - yo, - hey) - -To make isort skip an entire file simply add ``isort:skip_file`` to the module's doc string: - -.. code-block:: python - - """ my_module.py - Best module ever - - isort:skip_file - """ - - import b - import a - -Adding an import to multiple files -================================== - -isort makes it easy to add an import statement across multiple files, while being assured it's correctly placed. - -From the command line: - -.. code-block:: bash - - isort -a "from __future__ import print_function" *.py - -from within Kate: - -.. code-block:: - - ctrl+] - -or: - -.. code-block:: - - menu > Python > Add Import - -Removing an import from multiple files -====================================== - -isort also makes it easy to remove an import from multiple files, without having to be concerned with how it was originally -formatted. - -From the command line: - -.. code-block:: bash - - isort -rm "os.system" *.py - -from within Kate: - -.. code-block:: - - ctrl+shift+] - -or: - -.. code-block:: - - menu > Python > Remove Import - -Using isort to verify code -========================== - -The ``--check-only`` option ---------------------------- - -isort can also be used to used to verify that code is correctly formatted by running it with ``-c``. -Any files that contain incorrectly sorted and/or formatted imports will be outputted to ``stderr``. - -.. code-block:: bash - - isort **/*.py -c -vb - - SUCCESS: /home/timothy/Projects/Open_Source/isort/isort_kate_plugin.py Everything Looks Good! - ERROR: /home/timothy/Projects/Open_Source/isort/isort/isort.py Imports are incorrectly sorted. - -One great place this can be used is with a pre-commit git hook, such as this one by @acdha: - -https://gist.github.com/acdha/8717683 - -This can help to ensure a certain level of code quality throughout a project. - - -Git hook --------- - -isort provides a hook function that can be integrated into your Git pre-commit script to check -Python code before committing. - -To cause the commit to fail if there are isort errors (strict mode), include the following in -``.git/hooks/pre-commit``: - -.. code-block:: python - - #!/usr/bin/env python - import sys - from isort.hooks import git_hook - - sys.exit(git_hook(strict=True, modify=True)) - -If you just want to display warnings, but allow the commit to happen anyway, call ``git_hook`` without -the `strict` parameter. If you want to display warnings, but not also fix the code, call ``git_hook`` without -the `modify` parameter. - -Setuptools integration ----------------------- - -Upon installation, isort enables a ``setuptools`` command that checks Python files -declared by your project. - -Running ``python setup.py isort`` on the command line will check the files -listed in your ``py_modules`` and ``packages``. If any warning is found, -the command will exit with an error code: - -.. code-block:: bash - - $ python setup.py isort - -Also, to allow users to be able to use the command without having to install -isort themselves, add isort to the setup_requires of your ``setup()`` like so: - -.. code-block:: python - - setup( - name="project", - packages=["project"], - - setup_requires=[ - "isort" - ] - ) - -Security contact information -========== - -To report a security vulnerability, please use the `Tidelift security -contact`_. Tidelift will coordinate the fix and disclosure. - -.. _Tidelift security contact: https://tidelift.com/security - -Why isort? -========== - -isort simply stands for import sort. It was originally called "sortImports" however I got tired of typing the extra -characters and came to the realization camelCase is not pythonic. - -I wrote isort because in an organization I used to work in the manager came in one day and decided all code must -have alphabetically sorted imports. The code base was huge - and he meant for us to do it by hand. However, being a -programmer - I'm too lazy to spend 8 hours mindlessly performing a function, but not too lazy to spend 16 -hours automating it. I was given permission to open source sortImports and here we are :) - --------------------------------------------- - -Thanks and I hope you find isort useful! - -~Timothy Crosley diff --git a/docs/contributing/1.-contributing-guide.md b/docs/contributing/1.-contributing-guide.md new file mode 100644 index 000000000..d1b1cd62e --- /dev/null +++ b/docs/contributing/1.-contributing-guide.md @@ -0,0 +1,49 @@ +Contributing to isort +======== + +Looking for a useful open source project to contribute to? +Want your contributions to be warmly welcomed and acknowledged? +Welcome! You have found the right place. + +## Getting isort set up for local development +The first step when contributing to any project is getting it set up on your local machine. isort aims to make this as simple as possible. + +Account Requirements: + +- [A valid GitHub account](https://github.com/join) + +Base System Requirements: + +- Python3.6+ +- poetry +- bash or a bash compatible shell (should be auto-installed on Linux / Mac) + +Once you have verified that you system matches the base requirements you can start to get the project working by following these steps: + +1. [Fork the project on GitHub](https://github.com/timothycrosley/isort/fork). +2. Clone your fork to your local file system: + `git clone https://github.com/$GITHUB_ACCOUNT/isort.git` +3. `cd isort +4. `poetry install` + +## Making a contribution +Congrats! You're now ready to make a contribution! Use the following as a guide to help you reach a successful pull-request: + +1. Check the [issues page](https://github.com/timothycrosley/isort/issues) on GitHub to see if the task you want to complete is listed there. + - If it's listed there, write a comment letting others know you are working on it. + - If it's not listed in GitHub issues, go ahead and log a new issue. Then add a comment letting everyone know you have it under control. + - If you're not sure if it's something that is good for the main isort project and want immediate feedback, you can discuss it [here](https://gitter.im/timothycrosley/isort). +2. Create an issue branch for your local work `git checkout -b issue/$ISSUE-NUMBER`. +3. Do your magic here. +4. Ensure your code matches the [HOPE-8 Coding Standard](https://github.com/hugapi/HOPE/blob/master/all/HOPE-8--Style-Guide-for-Hug-Code.md#hope-8----style-guide-for-hug-code) used by the project. +5. Submit a pull request to the main project repository via GitHub. + +Thanks for the contribution! It will quickly get reviewed, and, once accepted, will result in your name being added to the acknowledgments list :). + +## Thank you! +I can not tell you how thankful I am for the hard work done by isort contributors like *you*. + +Thank you! + +~Timothy Crosley + diff --git a/docs/contributing/2.-coding-standard.md b/docs/contributing/2.-coding-standard.md new file mode 100644 index 000000000..85445e87d --- /dev/null +++ b/docs/contributing/2.-coding-standard.md @@ -0,0 +1,57 @@ +# HOPE 8 -- Style Guide for Hug Code + +| | | +| ------------| ------------------------------------------- | +| HOPE: | 8 | +| Title: | Style Guide for Hug Code | +| Author(s): | Timothy Crosley | +| Status: | Active | +| Type: | Process | +| Created: | 19-May-2019 | +| Updated: | 17-August-2019 | + +## Introduction + +This document gives coding conventions for the Hug code comprising the Hug core as well as all official interfaces, extensions, and plugins for the framework. +Optionally, projects that use Hug are encouraged to follow this HOPE and link to it as a reference. + +## PEP 8 Foundation + +All guidelines in this document are in addition to those defined in Python's [PEP 8](https://www.python.org/dev/peps/pep-0008/) and [PEP 257](https://www.python.org/dev/peps/pep-0257/) guidelines. + +## Line Length + +Too short of lines discourage descriptive variable names where they otherwise make sense. +Too long of lines reduce overall readability and make it hard to compare 2 files side by side. +There is no perfect number: but for Hug, we've decided to cap the lines at 100 characters. + +## Descriptive Variable names + +Naming things is hard. Hug has a few strict guidelines on the usage of variable names, which hopefully will reduce some of the guesswork: +- No one character variable names. + - Except for x, y, and z as coordinates. +- It's not okay to override built-in functions. + - Except for `id`. Guido himself thought that shouldn't have been moved to the system module. It's too commonly used, and alternatives feel very artificial. +- Avoid Acronyms, Abbreviations, or any other short forms - unless they are almost universally understand. + +## Adding new modules + +New modules added to the a project that follows the HOPE-8 standard should all live directly within the base `PROJECT_NAME/` directory without nesting. If the modules are meant only for internal use within the project, they should be prefixed with a leading underscore. For example, def _internal_function. Modules should contain a docstring at the top that gives a general explanation of the purpose and then restates the project's use of the MIT license. +There should be a `tests/test_$MODULE_NAME.py` file created to correspond to every new module that contains test coverage for the module. Ideally, tests should be 1:1 (one test object per code object, one test method per code method) to the extent cleanly possible. + +## Automated Code Cleaners + +All code submitted to Hug should be formatted using Black and isort. +Black should be run with the line length set to 100, and isort with Black compatible settings in place. + +## Automated Code Linting + +All code submitted to hug should run through the following tools: + +- Black and isort verification. +- Flake8 + - flake8-bugbear +- Bandit +- pep8-naming +- vulture +- safety diff --git a/docs/contributing/3.-code-of-conduct.md b/docs/contributing/3.-code-of-conduct.md new file mode 100644 index 000000000..a70f348fd --- /dev/null +++ b/docs/contributing/3.-code-of-conduct.md @@ -0,0 +1,88 @@ +# HOPE 11 -- Code of Conduct + +| | | +| ------------| ------------------------------------------- | +| HOPE: | 11 | +| Title: | Code of Conduct | +| Author(s): | Timothy Crosley | +| Status: | Active | +| Type: | Process | +| Created: | 17-August-2019 | +| Updated: | 17-August-2019 | + +## Abstract + +Defines the Code of Conduct for Hug and all related projects. + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting [timothy.crosley@gmail.com](mailto:timothy.crosley@gmail.com). All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. Confidentiality will be maintained +with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][https://www.contributor-covenant.org], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/ACKNOWLEDGEMENTS.md b/docs/contributing/4.-acknowledgements.md similarity index 100% rename from ACKNOWLEDGEMENTS.md rename to docs/contributing/4.-acknowledgements.md diff --git a/pyproject.toml b/pyproject.toml index 9901ca3f5..e8b3eb625 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ version = "4.3.21" description = "A Python utility / library to sort Python imports." authors = ["Timothy Crosley "] license = "MIT" -readme = "README.rst" +readme = "README.md" repository = "https://github.com/timothycrosley/isort" website = "http://timothycrosley.github.io/isort/" keywords = ["Refactor", "Lint", "Imports", "Sort", "Clean"] From 83f3415a82c64d0c9295b40a7772bad3b3abc089 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 13 Oct 2019 00:58:54 -0700 Subject: [PATCH 0092/1439] Add portray documentation --- README.md | 4 ++++ pyproject.toml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/README.md b/README.md index 24da9baee..eeac22dbe 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,10 @@ [![License](https://img.shields.io/github/license/mashape/apistatus.svg)](https://pypi.org/project/hug/) [![Join the chat at https://gitter.im/timothycrosley/isort](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/timothycrosley/isort?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Downloads](https://pepy.tech/badge/isort)](https://pepy.tech/project/isort) +_________________ + +[Read Latest Documentation](https://timothycrosley.github.io/isort/) - [Browse GitHub Code Repository](https://github.com/timothycrosley/isort/) +_________________ isort your python imports for you so you don't have to. diff --git a/pyproject.toml b/pyproject.toml index e8b3eb625..72a60bf5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,3 +73,7 @@ isort = "isort = isort.pylama_isort:Linter" [build-system] requires = ["poetry>=0.12"] build-backend = "poetry.masonry.api" + +[tool.portray.mkdocs.theme] +name = "material" +palette = {primary = "blue", accent = "light blue"} From 49b41a5112ca3e8305dc4836e871ad9ab7f66792 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 13 Oct 2019 01:09:22 -0700 Subject: [PATCH 0093/1439] Remove no longer needed .env file --- .env | 111 ----------------------------------------------------------- 1 file changed, 111 deletions(-) delete mode 100644 .env diff --git a/.env b/.env deleted file mode 100644 index 246963e0c..000000000 --- a/.env +++ /dev/null @@ -1,111 +0,0 @@ -#!/bin/bash -OPEN_PROJECT_NAME="isort" - -if [ "$PROJECT_NAME" = "$OPEN_PROJECT_NAME" ]; then - return -fi - -if [ ! -f ".env" ]; then - return -fi - -export PROJECT_NAME=$OPEN_PROJECT_NAME -export PROJECT_DIR="$PWD" - -if [ ! -d ".venv" ]; then - if ! hash pyvenv 2>/dev/null; then - function pyvenv() - { - if hash pyvenv-3.7 2>/dev/null; then - pyvenv-3.7 $@ - elif hash pyvenv-3.6 2>/dev/null; then - pyvenv-3.6 $@ - elif hash pyvenv-3.5 2>/dev/null; then - pyvenv-3.5 $@ - elif hash pyvenv-3.4 2>/dev/null; then - pyvenv-3.4 $@ - else - python3 -m venv $@ - fi - } - fi - - echo "Making venv for $PROJECT_NAME" - pyvenv .venv - . .venv/bin/activate - python setup.py install - pip install -r requirements.txt -fi - -. .venv/bin/activate - -# Let's make sure this is a hubflow enabled repo -yes | git hf init >/dev/null 2>/dev/null - -# Quick directory switching -alias root="cd $PROJECT_DIR" -alias project="root; cd $PROJECT_NAME" - -# Commands -alias run_test="root; py.test test_isort.py -s" -alias install="_install_project" -alias distribute="_distribute" -alias leave="_leave_project" - - -function _distribute() -{ - (root - rm -rf build dist __pycache__ isort.egg-info - python setup.py sdist - python setup.py bdist_wheel - twine upload --repository-url https://test.pypi.org/legacy/ dist/*) -} - - -function _install_project() -{ - CURRENT_DIRECTORY="$PWD" - root - sudo python setup.py install - cp isort_kate_plugin.py ~/.kde/share/apps/kate/pate/isort_plugin.py >/dev/null 2>/dev/null - cd $CURRENT_DIRECTORY -} - - -function load { - (root - python setup.py install) -} - - -function interact { - (load - ipython -c "import isort" -i) -} - - -function open { - (root - $CODE_EDITOR isort/*.py setup.py test_*.py README.md tox.ini .gitignore CHANGELOG.md setup.cfg .editorconfig .env .coveragerc .travis.yml requirements.txt) -} - - -function _leave_project() -{ - export PROJECT_NAME="" - export PROJECT_DIR="" - - unalias root - unalias project - unalias test - unalias install - unalias distribute - unalias leave - - unset -f load - unset -f interact - unset -f open - - deactivate -} From dad13c0b7bd849ff6644f93a0254cc3a56e79590 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 13 Oct 2019 01:27:21 -0700 Subject: [PATCH 0094/1439] Move tests into tests subdirectory --- appveyor.yml | 2 +- scripts/clean.sh | 4 +- scripts/lint.sh | 4 +- scripts/test.sh | 2 +- tests/__init__.py | 0 test_isort.py => tests/test_isort.py | 222 +++++++++++++++++++-------- 6 files changed, 163 insertions(+), 71 deletions(-) create mode 100644 tests/__init__.py rename test_isort.py => tests/test_isort.py (97%) diff --git a/appveyor.yml b/appveyor.yml index 97b1a3768..3e6fbda25 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -10,4 +10,4 @@ install: - poetry install test_script: - - poetry run pytest -s --cov=isort/ --cov=test_isort.py --cov-report=term-missing --cov-report html + - poetry run pytest tests/ -s --cov=isort/ --cov=tests/ --cov-report=term-missing --cov-report html diff --git a/scripts/clean.sh b/scripts/clean.sh index 7969592fb..d67bbd3e3 100755 --- a/scripts/clean.sh +++ b/scripts/clean.sh @@ -1,4 +1,4 @@ #!/bin/bash -xe -poetry run isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --use-parentheses --line-width=100 --recursive isort/ test_isort.py -poetry run black isort/ test_isort.py -l 100 +poetry run isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --use-parentheses --line-width=100 --recursive isort/ tests/ +poetry run black isort/ tests/ -l 100 diff --git a/scripts/lint.sh b/scripts/lint.sh index 3698d7428..e252401ef 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -1,8 +1,8 @@ #!/bin/bash -xe poetry run mypy --ignore-missing-imports isort/ -poetry run isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --use-parentheses --line-width=100 --recursive --check --diff --recursive isort/ test_isort.py -poetry run black --check -l 100 isort/ test_isort.py +poetry run isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --use-parentheses --line-width=100 --recursive --check --diff --recursive isort/ tests/ +poetry run black --check -l 100 isort/ tests/ poetry run flake8 isort/ tests/ --max-line 100 --ignore F403,F401,W503,E203 poetry run safety check poetry run bandit -r isort/ diff --git a/scripts/test.sh b/scripts/test.sh index 183efcf4f..5a9e85219 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -1,4 +1,4 @@ #!/bin/bash -xe ./scripts/lint.sh -poetry run pytest -s --cov=isort/ --cov=test_isort.py --cov-report=term-missing ${@} --cov-report html +poetry run pytest tests/ -s --cov=isort/ --cov=tests/ --cov-report=term-missing ${@} --cov-report html diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test_isort.py b/tests/test_isort.py similarity index 97% rename from test_isort.py rename to tests/test_isort.py index 337d8f0a2..90c15b5a6 100644 --- a/test_isort.py +++ b/tests/test_isort.py @@ -152,7 +152,7 @@ def test_line_length() -> None: " lib21, lib22)\n" ) - TEST_INPUT = ( + test_input = ( "from django.contrib.gis.gdal.field import (\n" " OFTDate, OFTDateTime, OFTInteger, OFTInteger64, OFTReal, OFTString,\n" " OFTTime,\n" @@ -160,13 +160,13 @@ def test_line_length() -> None: ) # Test case described in issue #654 assert ( SortImports( - file_contents=TEST_INPUT, + file_contents=test_input, include_trailing_comma=True, line_length=79, multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, balanced_wrapping=False, ).output - == TEST_INPUT + == test_input ) test_output = SortImports( @@ -426,7 +426,8 @@ def test_output_modes() -> None: file_contents=REALLY_LONG_IMPORT_WITH_COMMENT, multi_line_output=WrapModes.NOQA ).output assert output_noqa == ( - "from third_party import lib1, lib2, lib3, lib4, lib5, lib6, lib7, lib8, lib9, lib10, lib11," + "from third_party import lib1, lib2, lib3, lib4, lib5, lib6, lib7," + " lib8, lib9, lib10, lib11," " lib12, lib13, lib14, lib15, lib16, lib17, lib18, lib20, lib21, lib22 " "# NOQA comment\n" ) @@ -833,7 +834,8 @@ def test_forced_separate() -> None: "\n" "from django.contrib.admin import FieldListFilter\n" "from django.contrib.admin.exceptions import DisallowedModelAdminLookup\n" - "from django.contrib.admin.options import IncorrectLookupParameters, IS_POPUP_VAR, TO_FIELD_VAR\n" + "from django.contrib.admin.options import IncorrectLookupParameters, IS_POPUP_VAR, " + "TO_FIELD_VAR\n" ) assert ( SortImports( @@ -901,7 +903,9 @@ def test_thirdy_party_overrides_standard_section() -> None: def test_known_pattern_path_expansion() -> None: - """Test to ensure patterns ending with path sep gets expanded and nested packages treated as known patterns""" + """Test to ensure patterns ending with path sep gets expanded + and nested packages treated as known patterns. + """ test_input = ( "from kate_plugin import isort_plugin\n" "import sys\n" @@ -1050,7 +1054,9 @@ def test_balanced_wrapping() -> None: def test_relative_import_with_space() -> None: - """Tests the case where the relation and the module that is being imported from is separated with a space.""" + """Tests the case where the relation and the module that is being imported from is separated + with a space. + """ test_input = "from ... fields.sproqet import SproqetCollection" assert SortImports(file_contents=test_input).output == ( "from ...fields.sproqet import SproqetCollection\n" @@ -1308,7 +1314,9 @@ def test_keep_comments() -> None: def test_multiline_split_on_dot() -> None: - """Test to ensure isort correctly handles multiline imports, even when split right after a '.'""" + """Test to ensure isort correctly handles multiline imports, + even when split right after a '.' + """ test_input = ( "from my_lib.my_package.test.level_1.level_2.level_3.level_4.level_5.\\\n" " my_module import my_function" @@ -1406,8 +1414,15 @@ def test_include_trailing_comma() -> None: "from third_party import (\n lib1,\n)\n" ) - trailing_comma_with_comment = "from six.moves.urllib.parse import urlencode # pylint: disable=no-name-in-module,import-error" - expected_trailing_comma_with_comment = "from six.moves.urllib.parse import (\n urlencode, # pylint: disable=no-name-in-module,import-error\n)\n" + trailing_comma_with_comment = ( + "from six.moves.urllib.parse import urlencode " + "# pylint: disable=no-name-in-module,import-error" + ) + expected_trailing_comma_with_comment = ( + "from six.moves.urllib.parse import (\n" + " urlencode, # pylint: disable=no-n" + "ame-in-module,import-error\n)\n" + ) trailing_comma_with_comment = SortImports( file_contents=trailing_comma_with_comment, line_length=80, @@ -1428,7 +1443,9 @@ def test_include_trailing_comma() -> None: def test_similar_to_std_library() -> None: - """Test to ensure modules that are named similarly to a standard library import don't end up clobbered""" + """Test to ensure modules that are named similarly to a standard library import + don't end up clobbered + """ test_input = "import datetime\n\nimport requests\nimport times\n" assert ( SortImports(file_contents=test_input, known_third_party=["requests", "times"]).output @@ -1453,43 +1470,65 @@ def test_correctly_placed_imports() -> None: "from django.test import TestCase\n" "from model_mommy import mommy\n" "\n" - "from apps.clientman.commands.download_usage_rights import associate_right_for_item_product\n" - "from apps.clientman.commands.download_usage_rights import associate_right_for_item_product_d" + "from apps.clientman.commands.download_usage_rights import " + "associate_right_for_item_product\n" + "from apps.clientman.commands.download_usage_rights import " + "associate_right_for_item_product_d" "efinition\n" - "from apps.clientman.commands.download_usage_rights import associate_right_for_item_product_d" + "from apps.clientman.commands.download_usage_rights import " + "associate_right_for_item_product_d" "efinition_platform\n" - "from apps.clientman.commands.download_usage_rights import associate_right_for_item_product_p" + "from apps.clientman.commands.download_usage_rights import " + "associate_right_for_item_product_p" "latform\n" - "from apps.clientman.commands.download_usage_rights import associate_right_for_territory_reta" + "from apps.clientman.commands.download_usage_rights import " + "associate_right_for_territory_reta" "il_model\n" - "from apps.clientman.commands.download_usage_rights import associate_right_for_territory_reta" + "from apps.clientman.commands.download_usage_rights import " + "associate_right_for_territory_reta" "il_model_definition_platform_provider # noqa\n" - "from apps.clientman.commands.download_usage_rights import clear_right_for_item_product\n" - "from apps.clientman.commands.download_usage_rights import clear_right_for_item_product_defini" + "from apps.clientman.commands.download_usage_rights import " + "clear_right_for_item_product\n" + "from apps.clientman.commands.download_usage_rights import " + "clear_right_for_item_product_defini" "tion\n" - "from apps.clientman.commands.download_usage_rights import clear_right_for_item_product_defini" + "from apps.clientman.commands.download_usage_rights import " + "clear_right_for_item_product_defini" "tion_platform\n" - "from apps.clientman.commands.download_usage_rights import clear_right_for_item_product_platfo" + "from apps.clientman.commands.download_usage_rights import " + "clear_right_for_item_product_platfo" "rm\n" - "from apps.clientman.commands.download_usage_rights import clear_right_for_territory_retail_mo" + "from apps.clientman.commands.download_usage_rights import " + "clear_right_for_territory_retail_mo" "del\n" - "from apps.clientman.commands.download_usage_rights import clear_right_for_territory_retail_mo" + "from apps.clientman.commands.download_usage_rights import " + "clear_right_for_territory_retail_mo" "del_definition_platform_provider # noqa\n" - "from apps.clientman.commands.download_usage_rights import create_download_usage_right\n" - "from apps.clientman.commands.download_usage_rights import delete_download_usage_right\n" - "from apps.clientman.commands.download_usage_rights import disable_download_for_item_product\n" - "from apps.clientman.commands.download_usage_rights import disable_download_for_item_product_d" + "from apps.clientman.commands.download_usage_rights import " + "create_download_usage_right\n" + "from apps.clientman.commands.download_usage_rights import " + "delete_download_usage_right\n" + "from apps.clientman.commands.download_usage_rights import " + "disable_download_for_item_product\n" + "from apps.clientman.commands.download_usage_rights import " + "disable_download_for_item_product_d" "efinition\n" - "from apps.clientman.commands.download_usage_rights import disable_download_for_item_product_d" + "from apps.clientman.commands.download_usage_rights import " + "disable_download_for_item_product_d" "efinition_platform\n" - "from apps.clientman.commands.download_usage_rights import disable_download_for_item_product_p" + "from apps.clientman.commands.download_usage_rights import " + "disable_download_for_item_product_p" "latform\n" - "from apps.clientman.commands.download_usage_rights import disable_download_for_territory_reta" + "from apps.clientman.commands.download_usage_rights import " + "disable_download_for_territory_reta" "il_model\n" - "from apps.clientman.commands.download_usage_rights import disable_download_for_territory_reta" + "from apps.clientman.commands.download_usage_rights import " + "disable_download_for_territory_reta" "il_model_definition_platform_provider # noqa\n" - "from apps.clientman.commands.download_usage_rights import get_download_rights_for_item\n" - "from apps.clientman.commands.download_usage_rights import get_right\n" + "from apps.clientman.commands.download_usage_rights import " + "get_download_rights_for_item\n" + "from apps.clientman.commands.download_usage_rights import " + "get_right\n" ) assert ( SortImports( @@ -1503,7 +1542,9 @@ def test_correctly_placed_imports() -> None: def test_auto_detection() -> None: - """Initial test to ensure isort auto-detection works correctly - will grow over time as new issues are raised.""" + """Initial test to ensure isort auto-detection works correctly - + will grow over time as new issues are raised. + """ # Issue 157 test_input = "import binascii\nimport os\n\nimport cv2\nimport requests\n" @@ -1517,7 +1558,9 @@ def test_auto_detection() -> None: def test_same_line_statements() -> None: - """Ensure isort correctly handles the case where a single line contains multiple statements including an import""" + """Ensure isort correctly handles the case where a single line + contains multiple statements including an import + """ test_input = "import pdb; import nose\n" assert SortImports(file_contents=test_input).output == ("import pdb\n\nimport nose\n") @@ -1528,16 +1571,20 @@ def test_same_line_statements() -> None: def test_long_line_comments() -> None: """Ensure isort correctly handles comments at the end of extremely long lines""" test_input = ( - "from foo.utils.fabric_stuff.live import check_clean_live, deploy_live, sync_live_envdir, " + "from foo.utils.fabric_stuff.live import check_clean_live, deploy_live, " + "sync_live_envdir, " "update_live_app, update_live_cron # noqa\n" - "from foo.utils.fabric_stuff.stage import check_clean_stage, deploy_stage, sync_stage_envdir, " + "from foo.utils.fabric_stuff.stage import check_clean_stage, deploy_stage, " + "sync_stage_envdir, " "update_stage_app, update_stage_cron # noqa\n" ) assert SortImports(file_contents=test_input).output == ( "from foo.utils.fabric_stuff.live import (check_clean_live, deploy_live, # noqa\n" - " sync_live_envdir, update_live_app, update_live_cron)\n" + " sync_live_envdir, update_live_app, " + "update_live_cron)\n" "from foo.utils.fabric_stuff.stage import (check_clean_stage, deploy_stage, # noqa\n" - " sync_stage_envdir, update_stage_app, update_stage_cron)\n" + " sync_stage_envdir, update_stage_app, " + "update_stage_cron)\n" ) @@ -1551,7 +1598,10 @@ def test_tab_character_in_import() -> None: def test_split_position() -> None: """Ensure isort splits on import instead of . when possible""" - test_input = "from p24.shared.exceptions.master.host_state_flag_unchanged import HostStateUnchangedException\n" + test_input = ( + "from p24.shared.exceptions.master.host_state_flag_unchanged " + "import HostStateUnchangedException\n" + ) assert SortImports(file_contents=test_input, line_length=80).output == ( "from p24.shared.exceptions.master.host_state_flag_unchanged import \\\n" " HostStateUnchangedException\n" @@ -2008,13 +2058,17 @@ def test_basic_comment() -> None: def test_shouldnt_add_lines() -> None: - """Ensure that isort doesn't add a blank line when a top of import comment is present, issue #316""" + """Ensure that isort doesn't add a blank line when a top of import comment is present, + See: issue #316 + """ test_input = '"""Text"""\n' "# This is a comment\nimport pkg_resources\n" assert SortImports(file_contents=test_input).output == test_input def test_sections_parsed_correct(tmpdir) -> None: - """Ensure that modules for custom sections parsed as list from config file and isort result is correct""" + """Ensure that modules for custom sections parsed as list from config file and + isort result is correct + """ conf_file_data = ( "[settings]\n" "sections=FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER,COMMON\n" @@ -2038,7 +2092,9 @@ def test_sections_parsed_correct(tmpdir) -> None: @pytest.mark.skipif(toml is None, reason="Requires toml package to be installed.") def test_pyproject_conf_file(tmpdir) -> None: - """Ensure that modules for custom sections parsed as list from config file and isort result is correct""" + """Ensure that modules for custom sections parsed as list from config file and + isort result is correct + """ conf_file_data = ( "[build-system]\n" 'requires = ["setuptools", "wheel"]\n' @@ -2071,7 +2127,9 @@ def test_pyproject_conf_file(tmpdir) -> None: def test_alphabetic_sorting_no_newlines() -> None: - """Test to ensure that alphabetical sort does not erroneously introduce new lines (issue #328)""" + """Test to ensure that alphabetical sort does not + erroneously introduce new lines (issue #328) + """ test_input = "import os\n" test_output = SortImports( file_contents=test_input, force_alphabetical_sort_within_sections=True @@ -2162,7 +2220,9 @@ def test_forced_sepatate_globs() -> None: def test_no_additional_lines_issue_358() -> None: - """Test to ensure issue 358 is resolved and running isort multiple times does not add extra newlines""" + """Test to ensure issue 358 is resolved and running isort multiple times + does not add extra newlines + """ test_input = ( '"""This is a docstring"""\n' "# This is a comment\n" @@ -2251,7 +2311,9 @@ def test_no_additional_lines_issue_358() -> None: def test_import_by_paren_issue_375() -> None: - """Test to ensure isort can correctly handle sorting imports where the paren is directly by the import body""" + """Test to ensure isort can correctly handle sorting imports where the + paren is directly by the import body + """ test_input = "from .models import(\n Foo,\n Bar,\n)\n" assert SortImports(file_contents=test_input).output == "from .models import Bar, Foo\n" @@ -2269,7 +2331,9 @@ def test_import_by_paren_issue_460() -> None: def test_function_with_docstring() -> None: - """Test to ensure isort can correctly sort imports when the first found content is a function with a 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"] test_input = "def foo():\n" ' """ Single line triple quoted doctring """\n' " pass\n" expected_output = ( @@ -2355,7 +2419,9 @@ def test_long_single_line() -> None: def test_import_inside_class_issue_432() -> None: - """Test to ensure issue 432 is resolved and isort doesn't insert imports in the middle of classes""" + """Test to ensure issue 432 is resolved and isort + doesn't insert imports in the middle of classes + """ test_input = "# coding=utf-8\nclass Foo:\n def bar(self):\n pass\n" expected_output = ( "# coding=utf-8\n" @@ -2430,7 +2496,10 @@ def test_alias_using_paren_issue_466() -> None: def test_long_alias_using_paren_issue_957() -> None: - test_input = "from package import module as very_very_very_very_very_very_very_very_very_very_long_alias\n" + test_input = ( + "from package import module as very_very_very_very_very_very_very" + "_very_very_very_long_alias\n" + ) expected_output = ( "from package import (\n" " module as very_very_very_very_very_very_very_very_very_very_long_alias\n" @@ -2445,7 +2514,10 @@ def test_long_alias_using_paren_issue_957() -> None: ).output assert out == expected_output - test_input = "from deep.deep.deep.deep.deep.deep.deep.deep.deep.package import module as very_very_very_very_very_very_very_very_very_very_long_alias\n" + test_input = ( + "from deep.deep.deep.deep.deep.deep.deep.deep.deep.package import module as " + "very_very_very_very_very_very_very_very_very_very_long_alias\n" + ) expected_output = ( "from deep.deep.deep.deep.deep.deep.deep.deep.deep.package import (\n" " module as very_very_very_very_very_very_very_very_very_very_long_alias\n" @@ -2462,11 +2534,13 @@ def test_long_alias_using_paren_issue_957() -> None: test_input = ( "from deep.deep.deep.deep.deep.deep.deep.deep.deep.package " - "import very_very_very_very_very_very_very_very_very_very_long_module as very_very_very_very_very_very_very_very_very_very_long_alias\n" + "import very_very_very_very_very_very_very_very_very_very_long_module as very_very_very_" + "very_very_very_very_very_very_very_long_alias\n" ) expected_output = ( "from deep.deep.deep.deep.deep.deep.deep.deep.deep.package import (\n" - " very_very_very_very_very_very_very_very_very_very_long_module as very_very_very_very_very_very_very_very_very_very_long_alias\n" + " very_very_very_very_very_very_very_very_very_very_long_module as very_very_very_very" + "_very_very_very_very_very_very_long_alias\n" ")\n" ) out = SortImports( @@ -2519,14 +2593,19 @@ def test_import_wraps_with_comment_issue_471() -> None: def test_import_case_produces_inconsistent_results_issue_472() -> None: - """Test to ensure sorting imports with same name but different case produces the same result across platforms""" + """Test to ensure sorting imports with same name but different case produces + the same result across platforms + """ test_input = ( "from sqlalchemy.dialects.postgresql import ARRAY\n" "from sqlalchemy.dialects.postgresql import array\n" ) assert SortImports(file_contents=test_input, force_single_line=True).output == test_input - test_input = "from scrapy.core.downloader.handlers.http import HttpDownloadHandler, HTTPDownloadHandler\n" + test_input = ( + "from scrapy.core.downloader.handlers.http import " + "HttpDownloadHandler, HTTPDownloadHandler\n" + ) assert SortImports(file_contents=test_input).output == test_input @@ -2567,7 +2646,9 @@ def test_sort_within_sections_with_force_to_top_issue_473() -> None: def test_correct_number_of_new_lines_with_comment_issue_435() -> None: - """Test to ensure that injecting a comment in-between imports doesn't mess up the new line spacing""" + """Test to ensure that injecting a comment in-between imports + doesn't mess up the new line spacing + """ test_input = "import foo\n\n# comment\n\n\ndef baz():\n pass\n" assert SortImports(file_contents=test_input).output == test_input @@ -2597,11 +2678,13 @@ def test_no_extra_lines_issue_557() -> None: test_input = ( "import os\n" "\n" - "from scrapy.core.downloader.handlers.http import HttpDownloadHandler, HTTPDownloadHandler\n" + "from scrapy.core.downloader.handlers.http import " + "HttpDownloadHandler, HTTPDownloadHandler\n" ) expected_output = ( "import os\n" - "from scrapy.core.downloader.handlers.http import HttpDownloadHandler, HTTPDownloadHandler\n" + "from scrapy.core.downloader.handlers.http import HttpDownloadHandler, " + "HTTPDownloadHandler\n" ) assert ( SortImports( @@ -2699,7 +2782,8 @@ def test_not_splitted_sections() -> None: assert ( SortImports(file_contents=test_input, no_lines_before=["FIRSTPARTY"]).output == test_input ) - # in case when THIRDPARTY section is excluded from sections list, it's ok to merge STDLIB and FIRSTPARTY + # in case when THIRDPARTY section is excluded from sections list, + # it's ok to merge STDLIB and FIRSTPARTY assert SortImports( file_contents=test_input, sections=["STDLIB", "FIRSTPARTY", "LOCALFOLDER"], @@ -2727,7 +2811,9 @@ def test_no_lines_before_empty_section() -> None: def test_no_inline_sort() -> None: """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.""" + is enabled. + If `--force-single-line-imports` flag is enabled, then `--no-inline-sort` is ignored. + """ test_input = "from foo import a, c, b\n" assert ( SortImports(file_contents=test_input, no_inline_sort=True, force_single_line=False).output @@ -2749,8 +2835,10 @@ def test_no_inline_sort() -> None: def test_relative_import_of_a_module() -> None: - """Imports can be dynamically created (PEP302) and is used by modules such as six. This test ensures that - these types of imports are still sorted to the correct type instead of being categorized as local.""" + """Imports can be dynamically created (PEP302) and is used by modules such as six. + This test ensures that these types of imports are still sorted to the correct type + instead of being categorized as local. + """ test_input = ( "from __future__ import absolute_import\n" "\n" @@ -2828,7 +2916,8 @@ def test_to_ensure_importing_from_imports_module_works_issue_662() -> None: "@wraps(fun)\n" "def __inner(*args, **kwargs):\n" " from .imports import qualname\n" - " warn(description=description or qualname(fun), deprecation=deprecation, removal=removal)\n" + " warn(description=description or qualname(fun), deprecation=deprecation, " + "removal=removal)\n" ) assert SortImports(file_contents=test_input).output == test_input @@ -3041,7 +3130,9 @@ def test_command_line(tmpdir, capfd, multiprocess: bool) -> None: from isort.main import main tmpdir.join("file1.py").write("import re\nimport os\n\nimport contextlib\n\n\nimport isort") - tmpdir.join("file2.py").write("import collections\nimport time\n\nimport abc\n\n\nimport isort") + tmpdir.join("file2.py").write( + ("import collections\nimport time\n\nimport abc" "\n\n\nimport isort") + ) arguments = ["-rc", str(tmpdir), "--settings-path", os.getcwd()] if multiprocess: arguments.extend(["--jobs", "2"]) @@ -3056,7 +3147,7 @@ def test_command_line(tmpdir, capfd, multiprocess: bool) -> None: ) if not sys.platform.startswith("win"): out, err = capfd.readouterr() - assert not [error for error in err.split("\n") if error and not "warning:" in error] + assert not [error for error in err.split("\n") if error and "warning:" not in error] # it informs us about fixing the files: assert str(tmpdir.join("file1.py")) in out assert str(tmpdir.join("file2.py")) in out @@ -4046,7 +4137,8 @@ def test_python_version() -> None: def test_isort_with_single_character_import() -> None: - """Tests to ensure isort handles single capatilized single character imports as class objects by default + """Tests to ensure isort handles single capatilized single character imports + as class objects by default See Issue #376: https://github.com/timothycrosley/isort/issues/376 """ From 23c8ca019c7a281430dbdb5412031d61610f5ed6 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 13 Oct 2019 01:28:26 -0700 Subject: [PATCH 0095/1439] Link to cruft project template --- .cruft.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .cruft.json diff --git a/.cruft.json b/.cruft.json new file mode 100644 index 000000000..65ead87d9 --- /dev/null +++ b/.cruft.json @@ -0,0 +1,15 @@ +{ + "template": "https://github.com/timothycrosley/cookiecutter-python/", + "commit": "28d4514e7f2a769c8014b8b5cd33ea24cfe3a53b", + "context": { + "cookiecutter": { + "full_name": "Timothy Crosley", + "email": "timothy.crosley@gmail.com", + "github_username": "timothycrosley", + "project_name": "isort", + "description": "A Python utility / library to sort Python imports.", + "version": "4.3.21", + "_template": "https://github.com/timothycrosley/cookiecutter-python/" + } + } +} \ No newline at end of file From 9da6d8c6bced1b27e51227f7fc55fe1985faca86 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 13 Oct 2019 01:30:54 -0700 Subject: [PATCH 0096/1439] Update setup.cfg to ensure it doesn't repeat options set in scripts, points to correct test directory --- setup.cfg | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/setup.cfg b/setup.cfg index a3f3f3447..3c5786dc0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,17 +1,3 @@ -[flake8] -max-line-length = 160 -ignore = - # E203 whitespace before ':' - E203 - # W504 line break before binary operator - W503 - -[isort] -combine_as_imports = True -include_trailing_comma = True -line_length = 88 -multi_line_output = 3 - [mypy] python_version = 3.5 follow_imports = silent @@ -28,4 +14,4 @@ strict_optional = False strict_optional = False [tool:pytest] -testpaths = test_isort.py +testpaths = tests From 055ce0f3fc30e4a0b3f9e067a22a533449e11e22 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 13 Oct 2019 21:53:44 -0700 Subject: [PATCH 0097/1439] Update to latest cruft template --- .cruft.json | 2 +- .travis.yml | 1 + pyproject.toml | 7 ++++--- scripts/lint.sh | 1 + 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.cruft.json b/.cruft.json index 65ead87d9..58738b68e 100644 --- a/.cruft.json +++ b/.cruft.json @@ -1,6 +1,6 @@ { "template": "https://github.com/timothycrosley/cookiecutter-python/", - "commit": "28d4514e7f2a769c8014b8b5cd33ea24cfe3a53b", + "commit": "e8f19ccb7e1fb89d55e022ecd5e16b2269269f37", "context": { "cookiecutter": { "full_name": "Timothy Crosley", diff --git a/.travis.yml b/.travis.yml index 7acd4c27f..fcd18c84b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,7 @@ language: python cache: pip install: - pip3 install poetry +- pip3 install cruft - poetry install matrix: include: diff --git a/pyproject.toml b/pyproject.toml index 72a60bf5d..1cc44123c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,6 +53,7 @@ pytest = "^5.0" pytest-cov = "^2.7" pytest-mock = "^1.10" pep8-naming = "^0.8.2" +cruft = { version = "^1.1", optional = true, python = "^3.6" } portray = { version = "^1.3.0", optional = true, python = "^3.6" } appdirs = "^1.4" pipfile = "^0.0.2" @@ -70,10 +71,10 @@ isort = "isort.main:ISortCommand" [tool.poetry.plugins."pylama.linter"] isort = "isort = isort.pylama_isort:Linter" +[tool.portray.mkdocs.theme] +name = "material" +palette = {primary = "blue", accent = "light blue"} [build-system] requires = ["poetry>=0.12"] build-backend = "poetry.masonry.api" -[tool.portray.mkdocs.theme] -name = "material" -palette = {primary = "blue", accent = "light blue"} diff --git a/scripts/lint.sh b/scripts/lint.sh index e252401ef..17717ef8d 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -1,5 +1,6 @@ #!/bin/bash -xe +cruft check poetry run mypy --ignore-missing-imports isort/ poetry run isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --use-parentheses --line-width=100 --recursive --check --diff --recursive isort/ tests/ poetry run black --check -l 100 isort/ tests/ From 383313a3ecb32e62c402274ff5ec5a6999bcc39d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 13 Oct 2019 21:59:31 -0700 Subject: [PATCH 0098/1439] Switch to poetry run cruft check --- .travis.yml | 1 - scripts/lint.sh | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index fcd18c84b..7acd4c27f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ language: python cache: pip install: - pip3 install poetry -- pip3 install cruft - poetry install matrix: include: diff --git a/scripts/lint.sh b/scripts/lint.sh index 17717ef8d..8c5e7496e 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -1,6 +1,6 @@ #!/bin/bash -xe -cruft check +poetry run cruft check poetry run mypy --ignore-missing-imports isort/ poetry run isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --use-parentheses --line-width=100 --recursive --check --diff --recursive isort/ tests/ poetry run black --check -l 100 isort/ tests/ From 4e3107d6609b9e0e45eb9bc5a2bdcc4e6750ed1f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 13 Oct 2019 22:05:40 -0700 Subject: [PATCH 0099/1439] Add examples and hypothesis-auto requriements --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 1cc44123c..a33de1987 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,6 +53,8 @@ pytest = "^5.0" pytest-cov = "^2.7" pytest-mock = "^1.10" pep8-naming = "^0.8.2" +hypothesis-auto = { version = "^1.0.0", optional = true, python = "^3.6" } +examples = { version = "^1.0.0", optional = true, python = "^3.6" } cruft = { version = "^1.1", optional = true, python = "^3.6" } portray = { version = "^1.3.0", optional = true, python = "^3.6" } appdirs = "^1.4" From 400d6447e69e59caeba0dedf42ecebb840de8347 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 13 Oct 2019 23:17:20 -0700 Subject: [PATCH 0100/1439] Fix python3.6+ dependency management --- pyproject.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a33de1987..966bb67a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,17 +46,17 @@ vulture = "^1.0" bandit = "^1.6" safety = "^1.8" flake8-bugbear = "^19.8" -black = {version = "^18.3-alpha.0", allows-prereleases = true, optional = true, python = "^3.6"} +black = {version = "^18.3-alpha.0", allows-prereleases = true, python = "^3.6"} mypy = "^0.730.0" ipython = "^7.7" pytest = "^5.0" pytest-cov = "^2.7" pytest-mock = "^1.10" pep8-naming = "^0.8.2" -hypothesis-auto = { version = "^1.0.0", optional = true, python = "^3.6" } -examples = { version = "^1.0.0", optional = true, python = "^3.6" } -cruft = { version = "^1.1", optional = true, python = "^3.6" } -portray = { version = "^1.3.0", optional = true, python = "^3.6" } +hypothesis-auto = { version = "^1.0.0", python = "^3.6" } +examples = { version = "^1.0.0", python = "^3.6" } +cruft = { version = "^1.1", python = "^3.6" } +portray = { version = "^1.3.0", python = "^3.6" } appdirs = "^1.4" pipfile = "^0.0.2" requirementslib = "^1.5" From fe0fd1c36a1824b6649345c88f808dc739853f9f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 13 Oct 2019 23:37:28 -0700 Subject: [PATCH 0101/1439] Update to latest cruft template --- .cruft.json | 2 +- scripts/clean.sh | 3 ++- scripts/done.sh | 3 ++- scripts/lint.sh | 3 ++- scripts/test.sh | 3 ++- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.cruft.json b/.cruft.json index 58738b68e..4bfb20445 100644 --- a/.cruft.json +++ b/.cruft.json @@ -1,6 +1,6 @@ { "template": "https://github.com/timothycrosley/cookiecutter-python/", - "commit": "e8f19ccb7e1fb89d55e022ecd5e16b2269269f37", + "commit": "74f0b4c8da4ce123f53c859d803f451f00fcadd3", "context": { "cookiecutter": { "full_name": "Timothy Crosley", diff --git a/scripts/clean.sh b/scripts/clean.sh index d67bbd3e3..45e5688c8 100755 --- a/scripts/clean.sh +++ b/scripts/clean.sh @@ -1,4 +1,5 @@ -#!/bin/bash -xe +#!/bin/bash +set -euxo pipefail poetry run isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --use-parentheses --line-width=100 --recursive isort/ tests/ poetry run black isort/ tests/ -l 100 diff --git a/scripts/done.sh b/scripts/done.sh index d5dba124a..7a1becca3 100755 --- a/scripts/done.sh +++ b/scripts/done.sh @@ -1,4 +1,5 @@ -#!/bin/bash -xe +#!/bin/bash +set -euxo pipefail ./scripts/clean.sh ./scripts/test.sh diff --git a/scripts/lint.sh b/scripts/lint.sh index 8c5e7496e..3e5bff7f3 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -1,4 +1,5 @@ -#!/bin/bash -xe +#!/bin/bash +set -euxo pipefail poetry run cruft check poetry run mypy --ignore-missing-imports isort/ diff --git a/scripts/test.sh b/scripts/test.sh index 5a9e85219..7a458c399 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -1,4 +1,5 @@ -#!/bin/bash -xe +#!/bin/bash +set -euxo pipefail ./scripts/lint.sh poetry run pytest tests/ -s --cov=isort/ --cov=tests/ --cov-report=term-missing ${@} --cov-report html From 2be31385349bb92cb1d0cbc30ee175ac00ec3db2 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 13 Oct 2019 23:47:38 -0700 Subject: [PATCH 0102/1439] Update to latest template --- .cruft.json | 2 +- pyproject.toml | 1 + scripts/test.sh | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.cruft.json b/.cruft.json index 4bfb20445..879f8b664 100644 --- a/.cruft.json +++ b/.cruft.json @@ -1,6 +1,6 @@ { "template": "https://github.com/timothycrosley/cookiecutter-python/", - "commit": "74f0b4c8da4ce123f53c859d803f451f00fcadd3", + "commit": "d6097b15037ff9e56ea98bef2b564fa3313ac0e0", "context": { "cookiecutter": { "full_name": "Timothy Crosley", diff --git a/pyproject.toml b/pyproject.toml index 966bb67a8..0529248a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,6 +63,7 @@ requirementslib = "^1.5" pipreqs = "^0.4.9" tomlkit = "0.5.3" pip_api = "^0.0.12" +numpy = "^1.16.0" [tool.poetry.scripts] isort = "isort.main:main" diff --git a/scripts/test.sh b/scripts/test.sh index 7a458c399..eab951dad 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -2,4 +2,4 @@ set -euxo pipefail ./scripts/lint.sh -poetry run pytest tests/ -s --cov=isort/ --cov=tests/ --cov-report=term-missing ${@} --cov-report html +poetry run pytest tests/ -s --cov=isort/ --cov=tests/ --cov-report=term-missing ${@-} --cov-report html From 56aa368fc7e62bd98ffacbb4e393d2d7e6b3b304 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 13 Oct 2019 23:53:41 -0700 Subject: [PATCH 0103/1439] Skip 3.6+ linters on 3.5 --- scripts/lint.sh | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/scripts/lint.sh b/scripts/lint.sh index 3e5bff7f3..fb1434b72 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -1,10 +1,19 @@ #!/bin/bash set -euxo pipefail -poetry run cruft check -poetry run mypy --ignore-missing-imports isort/ +version=$(python3 -V 2>&1 | grep -Po '(?<=Python )(.+)') +parsedVersion=$(echo "${version//./}") + +if [[ "$parsedVersion" -lt "360" ]] +then + echo "WARNING: Some linters have been skipped. Run against 3.6+ for full set of linters to run against the project!" +else + poetry run cruft check + poetry run mypy --ignore-missing-imports isort/ + poetry run black --check -l 100 isort/ tests/ +fi + poetry run isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --use-parentheses --line-width=100 --recursive --check --diff --recursive isort/ tests/ -poetry run black --check -l 100 isort/ tests/ poetry run flake8 isort/ tests/ --max-line 100 --ignore F403,F401,W503,E203 poetry run safety check poetry run bandit -r isort/ From bb9d0d77efad76a49f9ec07489d9b163aba833f5 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 14 Oct 2019 00:51:01 -0700 Subject: [PATCH 0104/1439] Attempt version check compatability with mac shell --- scripts/lint.sh | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/scripts/lint.sh b/scripts/lint.sh index fb1434b72..2878af31c 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -1,10 +1,9 @@ #!/bin/bash set -euxo pipefail -version=$(python3 -V 2>&1 | grep -Po '(?<=Python )(.+)') -parsedVersion=$(echo "${version//./}") +pyversion=$(python3 -V 2>&1 | sed 's/.* \([0-9]\).\([0-9]\).*/\1\2/') -if [[ "$parsedVersion" -lt "360" ]] +if [[ "$pyversion" -lt "360" ]] then echo "WARNING: Some linters have been skipped. Run against 3.6+ for full set of linters to run against the project!" else From cde74abe190c9beea4515a95ffa963750e1196cf Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 14 Oct 2019 00:54:39 -0700 Subject: [PATCH 0105/1439] Fix version check --- scripts/lint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/lint.sh b/scripts/lint.sh index 2878af31c..9bc988783 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -3,7 +3,7 @@ set -euxo pipefail pyversion=$(python3 -V 2>&1 | sed 's/.* \([0-9]\).\([0-9]\).*/\1\2/') -if [[ "$pyversion" -lt "360" ]] +if [[ "$pyversion" -lt "36" ]] then echo "WARNING: Some linters have been skipped. Run against 3.6+ for full set of linters to run against the project!" else From 91f578ffe55b4ea915822e2a44ca74a9be71465b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 14 Oct 2019 01:02:22 -0700 Subject: [PATCH 0106/1439] Fix formatting --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 0529248a4..a45910311 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,6 +77,7 @@ isort = "isort = isort.pylama_isort:Linter" [tool.portray.mkdocs.theme] name = "material" palette = {primary = "blue", accent = "light blue"} + [build-system] requires = ["poetry>=0.12"] build-backend = "poetry.masonry.api" From 186f98065a2ba39ebe627e300df3f218a782d1f7 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 14 Oct 2019 03:25:35 -0700 Subject: [PATCH 0107/1439] Fix deploy environment variable condition --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7acd4c27f..3c8ec0270 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,6 +29,6 @@ deploy: on: tags: false branch: master - condition: "$TOXENV = py37" + condition: "$DEPLOY = py37" password: secure: SSFcjBL3dhWvSbo21icmnHQFV7mXfv/eDzxrefHUDMk37MWrvtNKchH8zz7wjAsf2PH1VYL1zkEFwnzuzHgs2aFCK7HDUwAaDSIcvPmJg9Oty+o2WQw16m7UnUac9MIZGmBHQaZuUTw0VJpm3GuPSXtdFJwFq3Tk3TIyUipEwg8= From 429064e13c803d0f25514da5e2101f63d04b3394 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 14 Oct 2019 22:57:45 -0700 Subject: [PATCH 0108/1439] Fix deploy key --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3c8ec0270..56caf8434 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,6 +29,6 @@ deploy: on: tags: false branch: master - condition: "$DEPLOY = py37" + condition: "$DEPLOY = yes" password: secure: SSFcjBL3dhWvSbo21icmnHQFV7mXfv/eDzxrefHUDMk37MWrvtNKchH8zz7wjAsf2PH1VYL1zkEFwnzuzHgs2aFCK7HDUwAaDSIcvPmJg9Oty+o2WQw16m7UnUac9MIZGmBHQaZuUTw0VJpm3GuPSXtdFJwFq3Tk3TIyUipEwg8= From 9fa69f9377bc29a5c49219046e98d4e64a3c2b59 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 15 Oct 2019 01:00:26 -0700 Subject: [PATCH 0109/1439] Add logo --- isort/__init__.py | 3 +-- isort/_version.py | 1 + isort/logo.py | 32 ++++++++++++++++++++++++++++++++ isort/main.py | 30 +++--------------------------- 4 files changed, 37 insertions(+), 29 deletions(-) create mode 100644 isort/_version.py create mode 100644 isort/logo.py diff --git a/isort/__init__.py b/isort/__init__.py index 11f9b2564..a9b61fdd8 100644 --- a/isort/__init__.py +++ b/isort/__init__.py @@ -1,5 +1,4 @@ """Defines the public isort interface""" from . import settings # noqa: F401 from .compat import SortImports # noqa: F401 - -__version__ = "4.3.21" +from ._version import __version__ diff --git a/isort/_version.py b/isort/_version.py new file mode 100644 index 000000000..b82594be2 --- /dev/null +++ b/isort/_version.py @@ -0,0 +1 @@ +__version__ = "4.3.21" diff --git a/isort/logo.py b/isort/logo.py new file mode 100644 index 000000000..b76ec06c0 --- /dev/null +++ b/isort/logo.py @@ -0,0 +1,32 @@ +from ._version import __version__ + +ASCII_ART = r""" +/#######################################################################\ + + `sMMy` + .yyyy- ` + ##soos## ./o. + ` ``..-..` ``...`.`` ` ```` ``-ssso``` + .s:-y- .+osssssso/. ./ossss+:so+:` :+o-`/osso:+sssssssso/ + .s::y- osss+.``.`` -ssss+-.`-ossso` ssssso/::..::+ssss:::. + .s::y- /ssss+//:-.` `ssss+ `ssss+ sssso` :ssss` + .s::y- `-/+oossssso/ `ssss/ sssso ssss/ :ssss` + .y-/y- ````:ssss` ossso. :ssss: ssss/ :ssss. + `/so:` `-//::/osss+ `+ssss+-/ossso: /sso- `osssso/. + \/ `-/oooo++/- .:/++:/++/-` .. `://++/. + + + isort your Python imports for you so you don't have to + + VERSION {} + +\########################################################################/ +""".format( + __version__ +) + +__doc__ = """ +```python +{} +``` +""".format(ASCII_ART) diff --git a/isort/main.py b/isort/main.py index 9e991e75c..c6ed5d76d 100644 --- a/isort/main.py +++ b/isort/main.py @@ -11,31 +11,7 @@ from isort import SortImports, __version__ from isort.settings import DEFAULT_SECTIONS, WrapModes, default, file_should_be_skipped, from_path - -INTRO = r""" -/#######################################################################\ - - `sMMy` - .yyyy- ` - ##soos## ./o. - ` ``..-..` ``...`.`` ` ```` ``-ssso``` - .s:-y- .+osssssso/. ./ossss+:so+:` :+o-`/osso:+sssssssso/ - .s::y- osss+.``.`` -ssss+-.`-ossso` ssssso/::..::+ssss:::. - .s::y- /ssss+//:-.` `ssss+ `ssss+ sssso` :ssss` - .s::y- `-/+oossssso/ `ssss/ sssso ssss/ :ssss` - .y-/y- ````:ssss` ossso. :ssss: ssss/ :ssss. - `/so:` `-//::/osss+ `+ssss+-/ossso: /sso- `osssso/. - \/ `-/oooo++/- .:/++:/++/-` .. `://++/. - - - isort your Python imports for you so you don't have to - - VERSION {} - -\########################################################################/ -""".format( - __version__ -) +from isort.logo import ASCII_ART shebang_re = re.compile(br"^#!.*\bpython[23w]?\b") @@ -570,7 +546,7 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: def main(argv: Optional[Sequence[str]] = None) -> None: arguments = parse_args(argv) if arguments.get("show_version"): - print(INTRO) + print(ASCII_ART) return if arguments.get("ambiguous_r_flag"): @@ -628,7 +604,7 @@ def main(argv: Optional[Sequence[str]] = None) -> None: file_names = iter_source_code(file_names, config, skipped) num_skipped = 0 if config["verbose"] or config.get("show_logo", False): - print(INTRO) + print(ASCII_ART) jobs = arguments.get("jobs") if jobs: From 1987c1d37da7f03e8a00caffa3a36966b267ccb3 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 15 Oct 2019 01:01:17 -0700 Subject: [PATCH 0110/1439] Auto formatting --- isort/__init__.py | 2 +- isort/logo.py | 4 +++- isort/main.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/isort/__init__.py b/isort/__init__.py index a9b61fdd8..5212cf2a6 100644 --- a/isort/__init__.py +++ b/isort/__init__.py @@ -1,4 +1,4 @@ """Defines the public isort interface""" from . import settings # noqa: F401 -from .compat import SortImports # noqa: F401 from ._version import __version__ +from .compat import SortImports # noqa: F401 diff --git a/isort/logo.py b/isort/logo.py index b76ec06c0..ea85bff6c 100644 --- a/isort/logo.py +++ b/isort/logo.py @@ -29,4 +29,6 @@ ```python {} ``` -""".format(ASCII_ART) +""".format( + ASCII_ART +) diff --git a/isort/main.py b/isort/main.py index c6ed5d76d..6904ccb4e 100644 --- a/isort/main.py +++ b/isort/main.py @@ -10,8 +10,8 @@ import setuptools from isort import SortImports, __version__ -from isort.settings import DEFAULT_SECTIONS, WrapModes, default, file_should_be_skipped, from_path from isort.logo import ASCII_ART +from isort.settings import DEFAULT_SECTIONS, WrapModes, default, file_should_be_skipped, from_path shebang_re = re.compile(br"^#!.*\bpython[23w]?\b") From 0e627f76f85bfb2283c7c5c3588bf3c35f4e9d62 Mon Sep 17 00:00:00 2001 From: hugovk Date: Tue, 15 Oct 2019 20:22:21 +0300 Subject: [PATCH 0111/1439] Declare support for Python 3.8 --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index a45910311..0253823ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ classifiers = [ "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", From abfe3713e162307a5dadb277c19aac0f85dc3ad3 Mon Sep 17 00:00:00 2001 From: hugovk Date: Tue, 15 Oct 2019 20:59:35 +0300 Subject: [PATCH 0112/1439] Test on Python 3.8 final --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 56caf8434..d22d1f68e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,7 @@ matrix: python: 3.7 env: DEPLOY=yes - os: linux - python: 3.8-dev + python: 3.8 - os: osx language: generic script: From 5fc1fd8376fabb1c2d5cd4b3df1a977109e7999d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 16 Oct 2019 23:39:53 -0700 Subject: [PATCH 0113/1439] Move out formatters to separately testable module --- .../unicode_data/11.0.0/charmap.json.gz | Bin 0 -> 20358 bytes isort/isort.py | 342 +++++------------- isort/output.py | 313 ++++++++++++++++ isort/parse.py | 13 + tests/test_output.py | 14 + tests/test_parse.py | 5 + 6 files changed, 426 insertions(+), 261 deletions(-) create mode 100644 .hypothesis/unicode_data/11.0.0/charmap.json.gz create mode 100644 isort/output.py create mode 100644 isort/parse.py create mode 100644 tests/test_output.py create mode 100644 tests/test_parse.py diff --git a/.hypothesis/unicode_data/11.0.0/charmap.json.gz b/.hypothesis/unicode_data/11.0.0/charmap.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..b6e5fa9ff1b0122697d18ddf7ca26006defad9a5 GIT binary patch literal 20358 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 M0gY=Wee2u+0H1s|y#N3J literal 0 HcmV?d00001 diff --git a/isort/isort.py b/isort/isort.py index 17e10279d..b6dc21398 100644 --- a/isort/isort.py +++ b/isort/isort.py @@ -14,7 +14,7 @@ from isort import utils from isort.format import format_natural, format_simplified -from . import settings +from . import output, parse, settings from .finders import FindersManager from .natural import nsorted from .settings import WrapModes @@ -201,22 +201,6 @@ def _module_key( length_sort and (str(len(module_name)) + ":" + module_name) or module_name, ) - def _add_comments(self, comments: Optional[Sequence[str]], original_string: str = "") -> str: - """ - Returns a string with comments added if ignore_comments is not set. - """ - if self.config["ignore_comments"]: - return self._strip_comments(original_string)[0] - - if not comments: - return original_string - else: - return "{}{} {}".format( - self._strip_comments(original_string)[0], - self.config["comment_prefix"], - "; ".join(comments), - ) - def _wrap(self, line: str) -> str: """ Returns an import wrapped to the specified line-length, if possible. @@ -309,7 +293,12 @@ def _add_straight_imports( section_output.append("") section_output.extend(comments_above) section_output.extend( - self._add_comments(self.comments["straight"].get(module), idef) + output.with_comments( + self.comments["straight"].get(module), + idef, + removed=self.config["ignore_comments"], + comment_prefix=self.config["comment_prefix"], + ) for idef in import_definition ) @@ -370,15 +359,23 @@ def _add_from_imports( comments = self.comments["from"].pop(module, ()) if "*" in from_imports and self.config["combine_star"]: import_statement = self._wrap( - self._add_comments(comments, "{}*".format(import_start)) + output.with_comments( + comments, + "{}*".format(import_start), + removed=self.config["ignore_comments"], + comment_prefix=self.config["comment_prefix"], + ) ) from_imports = None elif self.config["force_single_line"]: import_statement = None while from_imports: from_import = from_imports.pop(0) - single_import_line = self._add_comments( - comments, import_start + from_import + single_import_line = output.with_comments( + comments, + import_start + from_import, + removed=self.config["ignore_comments"], + comment_prefix=self.config["comment_prefix"], ) comment = self.comments["nested"].get(module, {}).pop(from_import, None) if comment: @@ -395,8 +392,11 @@ def _add_from_imports( "{}.{}".format(module, from_import) ) section_output.extend( - self._add_comments( - from_comments, self._wrap(import_start + as_import) + output.with_comments( + from_comments, + self._wrap(import_start + as_import), + removed=self.config["ignore_comments"], + comment_prefix=self.config["comment_prefix"], ) for as_import in nsorted(as_imports[from_import]) ) @@ -421,19 +421,32 @@ def _add_from_imports( and self.imports[section]["from"][module][from_import] ): section_output.append( - self._add_comments( - from_comments, self._wrap(import_start + from_import) + output.with_comments( + from_comments, + self._wrap(import_start + from_import), + removed=self.config["ignore_comments"], + comment_prefix=self.config["comment_prefix"], ) ) section_output.extend( - self._add_comments(from_comments, self._wrap(import_start + as_import)) + output.with_comments( + from_comments, + self._wrap(import_start + as_import), + removed=self.config["ignore_comments"], + comment_prefix=self.config["comment_prefix"], + ) for as_import in as_imports[from_import] ) star_import = False if "*" in from_imports: section_output.append( - self._add_comments(comments, "{}*".format(import_start)) + output.with_comments( + comments, + "{}*".format(import_start), + removed=self.config["ignore_comments"], + comment_prefix=self.config["comment_prefix"], + ) ) from_imports.remove("*") star_import = True @@ -447,8 +460,11 @@ def _add_from_imports( continue comment = self.comments["nested"].get(module, {}).pop(from_import, None) if comment: - single_import_line = self._add_comments( - comments, import_start + from_import + single_import_line = output.with_comments( + comments, + import_start + from_import, + removed=self.config["ignore_comments"], + comment_prefix=self.config["comment_prefix"], ) single_import_line += "{} {}".format( comments and ";" or self.config["comment_prefix"], comment @@ -477,8 +493,11 @@ def _add_from_imports( if star_import: import_statement = import_start + (", ").join(from_import_section) else: - import_statement = self._add_comments( - comments, import_start + (", ").join(from_import_section) + import_statement = output.with_comments( + comments, + import_start + (", ").join(from_import_section), + removed=self.config["ignore_comments"], + comment_prefix=self.config["comment_prefix"], ) if not from_import_section: import_statement = "" @@ -540,12 +559,21 @@ def _multi_line_reformat( self, import_start: str, from_imports: List[str], comments: Sequence[str] ) -> str: output_mode = self.config["multi_line_output"].name.lower() - formatter = getattr(self, "_output_" + output_mode, self._output_grid) + formatter = getattr(output, output_mode, output.grid) dynamic_indent = " " * (len(import_start) + 1) indent = self.config["indent"] line_length = self.config["wrap_length"] or self.config["line_length"] import_statement = formatter( - import_start, copy.copy(from_imports), dynamic_indent, indent, line_length, comments + import_start, + copy.copy(from_imports), + dynamic_indent, + indent, + line_length, + comments, + line_separator=self.line_separator, + comment_prefix=self.config["comment_prefix"], + include_trailing_comma=self.config["include_trailing_comma"], + remove_comments=self.config["ignore_comments"], ) if self.config["balanced_wrapping"]: lines = import_statement.split(self.line_separator) @@ -565,6 +593,10 @@ def _multi_line_reformat( indent, line_length, comments, + line_separator=self.line_separator, + comment_prefix=self.config["comment_prefix"], + include_trailing_comma=self.config["include_trailing_comma"], + remove_comments=self.config["ignore_comments"], ) lines = new_import_statement.split(self.line_separator) if import_statement.count(self.line_separator) == 0: @@ -748,219 +780,6 @@ def by_module(line: str) -> str: new_out_lines.append("") self.out_lines = new_out_lines - def _output_grid( - self, - statement: str, - imports: List[str], - white_space: str, - indent: str, - line_length: int, - comments: Sequence[str], - ) -> str: - statement += "(" + imports.pop(0) - while imports: - next_import = imports.pop(0) - next_statement = self._add_comments(comments, statement + ", " + next_import) - if len(next_statement.split(self.line_separator)[-1]) + 1 > line_length: - lines = ["{}{}".format(white_space, next_import.split(" ")[0])] - for part in next_import.split(" ")[1:]: - new_line = "{} {}".format(lines[-1], part) - if len(new_line) + 1 > line_length: - lines.append("{}{}".format(white_space, part)) - else: - lines[-1] = new_line - next_import = self.line_separator.join(lines) - statement = self._add_comments(comments, "{},".format(statement)) + "{}{}".format( - self.line_separator, next_import - ) - comments = None - else: - statement += ", " + next_import - return statement + ("," if self.config["include_trailing_comma"] else "") + ")" - - def _output_vertical( - self, - statement: str, - imports: List[str], - white_space: str, - indent: str, - line_length: int, - comments: Sequence[str], - ) -> str: - first_import = ( - self._add_comments(comments, imports.pop(0) + ",") + self.line_separator + white_space - ) - return "{}({}{}{})".format( - statement, - first_import, - ("," + self.line_separator + white_space).join(imports), - "," if self.config["include_trailing_comma"] else "", - ) - - def _output_hanging_indent( - self, - statement: str, - imports: List[str], - white_space: str, - indent: str, - line_length: int, - comments: Sequence[str], - ) -> str: - statement += imports.pop(0) - while imports: - next_import = imports.pop(0) - next_statement = self._add_comments(comments, statement + ", " + next_import) - if len(next_statement.split(self.line_separator)[-1]) + 3 > line_length: - next_statement = self._add_comments( - comments, "{}, \\".format(statement) - ) + "{}{}{}".format(self.line_separator, indent, next_import) - comments = None - statement = next_statement - return statement - - def _output_vertical_hanging_indent( - self, - statement: str, - imports: List[str], - white_space: str, - indent: str, - line_length: int, - comments: Sequence[str], - ) -> str: - return "{0}({1}{2}{3}{4}{5}{2})".format( - statement, - self._add_comments(comments), - self.line_separator, - indent, - ("," + self.line_separator + indent).join(imports), - "," if self.config["include_trailing_comma"] else "", - ) - - def _output_vertical_grid_common( - self, - statement: str, - imports: List[str], - white_space: str, - indent: str, - line_length: int, - comments: Sequence[str], - need_trailing_char: bool, - ) -> str: - statement += ( - self._add_comments(comments, "(") + self.line_separator + indent + imports.pop(0) - ) - while imports: - next_import = imports.pop(0) - next_statement = "{}, {}".format(statement, next_import) - current_line_length = len(next_statement.split(self.line_separator)[-1]) - if imports or need_trailing_char: - # If we have more imports we need to account for a comma after this import - # We might also need to account for a closing ) we're going to add. - current_line_length += 1 - if current_line_length > line_length: - next_statement = "{},{}{}{}".format( - statement, self.line_separator, indent, next_import - ) - statement = next_statement - if self.config["include_trailing_comma"]: - statement += "," - return statement - - def _output_vertical_grid( - self, - statement: str, - imports: List[str], - white_space: str, - indent: str, - line_length: int, - comments: Sequence[str], - ) -> str: - return ( - self._output_vertical_grid_common( - statement, imports, white_space, indent, line_length, comments, True - ) - + ")" - ) - - def _output_vertical_grid_grouped( - self, - statement: str, - imports: List[str], - white_space: str, - indent: str, - line_length: int, - comments: Sequence[str], - ) -> str: - return ( - self._output_vertical_grid_common( - statement, imports, white_space, indent, line_length, comments, True - ) - + self.line_separator - + ")" - ) - - def _output_vertical_grid_grouped_no_comma( - self, - statement: str, - imports: List[str], - white_space: str, - indent: str, - line_length: int, - comments: Sequence[str], - ) -> str: - return ( - self._output_vertical_grid_common( - statement, imports, white_space, indent, line_length, comments, False - ) - + self.line_separator - + ")" - ) - - def _output_noqa( - self, - statement: str, - imports: List[str], - white_space: str, - indent: str, - line_length: int, - comments: Sequence[str], - ) -> str: - retval = "{}{}".format(statement, ", ".join(imports)) - comment_str = " ".join(comments) - if comments: - if ( - len(retval) + len(self.config["comment_prefix"]) + 1 + len(comment_str) - <= line_length - ): - return "{}{} {}".format(retval, self.config["comment_prefix"], comment_str) - else: - if len(retval) <= line_length: - return retval - if comments: - if "NOQA" in comments: - return "{}{} {}".format(retval, self.config["comment_prefix"], comment_str) - else: - return "{}{} NOQA {}".format(retval, self.config["comment_prefix"], comment_str) - else: - return "{}{} NOQA".format(retval, self.config["comment_prefix"]) - - @staticmethod - def _strip_comments( - line: str, comments: Optional[List[str]] = None - ) -> Tuple[str, List[str], bool]: - """Removes comments from import line.""" - if comments is None: - comments = [] - - new_comments = False - comment_start = line.find("#") - if comment_start != -1: - comments.append(line[comment_start + 1 :].strip()) - new_comments = True - line = line[:comment_start] - - return line, comments, new_comments - def _skip_line(self, line: str) -> bool: skip_line = self._in_quote if self.index == 1 and line.startswith("#"): @@ -1051,7 +870,8 @@ def _parse(self) -> None: if self.import_index == -1: self.import_index = self.index - 1 nested_comments = {} - import_string, comments, new_comments = self._strip_comments(line) + import_string, comment = parse.import_comment(line) + comments = [comment] if comment else [] line_parts = [ part for part in self._strip_syntax(import_string).strip().split(" ") if part ] @@ -1059,29 +879,29 @@ def _parse(self) -> None: import_type == "from" and len(line_parts) == 2 and line_parts[1] != "*" - and new_comments + and comments ): nested_comments[line_parts[-1]] = comments[0] if "(" in line.split("#")[0] and not self._at_end(): while not line.strip().endswith(")") and not self._at_end(): - line, comments, new_comments = self._strip_comments( - self._get_line(), comments - ) + line, new_comment = parse.import_comment(self._get_line()) + if new_comment: + comments.append(new_comment) stripped_line = self._strip_syntax(line).strip() if ( import_type == "from" and stripped_line and " " not in stripped_line - and new_comments + and new_comment ): nested_comments[stripped_line] = comments[-1] import_string += self.line_separator + line else: while line.strip().endswith("\\"): - line, comments, new_comments = self._strip_comments( - self._get_line(), comments - ) + line, new_comment = parse.import_comment(self._get_line()) + if new_comment: + comments.append(new_comment) # Still need to check for parentheses after an escaped line if ( @@ -1094,21 +914,21 @@ def _parse(self) -> None: import_type == "from" and stripped_line and " " not in stripped_line - and new_comments + and new_comment ): nested_comments[stripped_line] = comments[-1] import_string += self.line_separator + line while not line.strip().endswith(")") and not self._at_end(): - line, comments, new_comments = self._strip_comments( - self._get_line(), comments - ) + line, new_comment = parse.import_comment(self._get_line()) + if new_comment: + comments.append(new_comment) stripped_line = self._strip_syntax(line).strip() if ( import_type == "from" and stripped_line and " " not in stripped_line - and new_comments + and new_comment ): nested_comments[stripped_line] = comments[-1] import_string += self.line_separator + line @@ -1118,7 +938,7 @@ def _parse(self) -> None: import_type == "from" and stripped_line and " " not in stripped_line - and new_comments + and new_comment ): nested_comments[stripped_line] = comments[-1] if import_string.strip().endswith(" import") or line.strip().startswith( diff --git a/isort/output.py b/isort/output.py new file mode 100644 index 000000000..3db9d0d75 --- /dev/null +++ b/isort/output.py @@ -0,0 +1,313 @@ +from typing import List, Optional + +from . import parse + + +def grid( + statement: str, + imports: List[str], + white_space: str, + indent: str, + line_length: int, + comments: List[str], + line_separator: str, + comment_prefix: str, + include_trailing_comma: bool, + remove_comments: bool, +) -> str: + if not imports: + return "" + + statement += "(" + imports.pop(0) + while imports: + next_import = imports.pop(0) + next_statement = with_comments( + comments, + statement + ", " + next_import, + removed=remove_comments, + comment_prefix=comment_prefix, + ) + if len(next_statement.split(line_separator)[-1]) + 1 > line_length: + lines = ["{}{}".format(white_space, next_import.split(" ")[0])] + for part in next_import.split(" ")[1:]: + new_line = "{} {}".format(lines[-1], part) + if len(new_line) + 1 > line_length: + lines.append("{}{}".format(white_space, part)) + else: + lines[-1] = new_line + next_import = line_separator.join(lines) + statement = with_comments( + comments, + "{},".format(statement), + removed=remove_comments, + comment_prefix=comment_prefix, + ) + "{}{}".format(line_separator, next_import) + comments = [] + else: + statement += ", " + next_import + return statement + ("," if include_trailing_comma else "") + ")" + + +def vertical( + statement: str, + imports: List[str], + white_space: str, + indent: str, + line_length: int, + comments: List[str], + line_separator: str, + comment_prefix: str, + include_trailing_comma: bool, + remove_comments: bool, +) -> str: + if not imports: + return "" + + first_import = ( + with_comments( + comments, imports.pop(0) + ",", removed=remove_comments, comment_prefix=comment_prefix + ) + + line_separator + + white_space + ) + return "{}({}{}{})".format( + statement, + first_import, + ("," + line_separator + white_space).join(imports), + "," if include_trailing_comma else "", + ) + + +def hanging_indent( + statement: str, + imports: List[str], + white_space: str, + indent: str, + line_length: int, + comments: List[str], + line_separator: str, + comment_prefix: str, + include_trailing_comma: bool, + remove_comments: bool, +) -> str: + if not imports: + return "" + + statement += imports.pop(0) + while imports: + next_import = imports.pop(0) + next_statement = with_comments( + comments, + statement + ", " + next_import, + removed=remove_comments, + comment_prefix=comment_prefix, + ) + if len(next_statement.split(line_separator)[-1]) + 3 > line_length: + next_statement = with_comments( + comments, + "{}, \\".format(statement), + removed=remove_comments, + comment_prefix=comment_prefix, + ) + "{}{}{}".format(line_separator, indent, next_import) + comments = [] + statement = next_statement + return statement + + +def vertical_hanging_indent( + statement: str, + imports: List[str], + white_space: str, + indent: str, + line_length: int, + comments: List[str], + line_separator: str, + comment_prefix: str, + include_trailing_comma: bool, + remove_comments: bool, +) -> str: + return "{0}({1}{2}{3}{4}{5}{2})".format( + statement, + with_comments(comments, "", removed=remove_comments, comment_prefix=comment_prefix), + line_separator, + indent, + ("," + line_separator + indent).join(imports), + "," if include_trailing_comma else "", + ) + + +def vertical_grid_common( + statement: str, + imports: List[str], + white_space: str, + indent: str, + line_length: int, + comments: List[str], + line_separator: str, + comment_prefix: str, + include_trailing_comma: bool, + remove_comments: bool, + need_trailing_char: bool, +) -> str: + if not imports: + return "" + + statement += ( + with_comments(comments, "(", removed=remove_comments, comment_prefix=comment_prefix) + + line_separator + + indent + + imports.pop(0) + ) + while imports: + next_import = imports.pop(0) + next_statement = "{}, {}".format(statement, next_import) + current_line_length = len(next_statement.split(line_separator)[-1]) + if imports or need_trailing_char: + # If we have more imports we need to account for a comma after this import + # We might also need to account for a closing ) we're going to add. + current_line_length += 1 + if current_line_length > line_length: + next_statement = "{},{}{}{}".format(statement, line_separator, indent, next_import) + statement = next_statement + if include_trailing_comma: + statement += "," + return statement + + +def vertical_grid( + statement: str, + imports: List[str], + white_space: str, + indent: str, + line_length: int, + comments: List[str], + line_separator: str, + comment_prefix: str, + include_trailing_comma: bool, + remove_comments: bool, +) -> str: + return ( + vertical_grid_common( + statement, + imports, + white_space, + indent, + line_length, + comments, + line_separator=line_separator, + comment_prefix=comment_prefix, + include_trailing_comma=include_trailing_comma, + remove_comments=remove_comments, + need_trailing_char=True, + ) + + ")" + ) + + +def vertical_grid_grouped( + statement: str, + imports: List[str], + white_space: str, + indent: str, + line_length: int, + comments: List[str], + line_separator: str, + comment_prefix: str, + include_trailing_comma: bool, + remove_comments: bool, +) -> str: + return ( + vertical_grid_common( + statement, + imports, + white_space, + indent, + line_length, + comments, + line_separator=line_separator, + comment_prefix=comment_prefix, + include_trailing_comma=include_trailing_comma, + remove_comments=remove_comments, + need_trailing_char=True, + ) + + line_separator + + ")" + ) + + +def vertical_grid_grouped_no_comma( + statement: str, + imports: List[str], + white_space: str, + indent: str, + line_length: int, + comments: List[str], + line_separator: str, + comment_prefix: str, + include_trailing_comma: bool, + remove_comments: bool, +) -> str: + return ( + vertical_grid_common( + statement, + imports, + white_space, + indent, + line_length, + comments, + line_separator=line_separator, + comment_prefix=comment_prefix, + include_trailing_comma=include_trailing_comma, + remove_comments=remove_comments, + need_trailing_char=False, + ) + + line_separator + + ")" + ) + + +def noqa( + statement: str, + imports: List[str], + white_space: str, + indent: str, + line_length: int, + comments: List[str], + line_separator: str, + comment_prefix: str, + include_trailing_comma: bool, + remove_comments: bool, +) -> str: + retval = "{}{}".format(statement, ", ".join(imports)) + comment_str = " ".join(comments) + if comments: + if len(retval) + len(comment_prefix) + 1 + len(comment_str) <= line_length: + return "{}{} {}".format(retval, comment_prefix, comment_str) + else: + if len(retval) <= line_length: + return retval + if comments: + if "NOQA" in comments: + return "{}{} {}".format(retval, comment_prefix, comment_str) + else: + return "{}{} NOQA {}".format(retval, comment_prefix, comment_str) + else: + return "{}{} NOQA".format(retval, comment_prefix) + + +def with_comments( + comments: Optional[List[str]], + original_string: str = "", + removed: bool = False, + comment_prefix: str = "", +) -> str: + """Returns a string with comments added if removed is not set.""" + if removed: + return parse.import_comment(original_string)[0] + + if not comments: + return original_string + else: + return "{}{} {}".format( + parse.import_comment(original_string)[0], comment_prefix, "; ".join(comments) + ) diff --git a/isort/parse.py b/isort/parse.py new file mode 100644 index 000000000..c9460c35e --- /dev/null +++ b/isort/parse.py @@ -0,0 +1,13 @@ +"""Defines parsing functions used by isort for parsing import definitions""" +from typing import Tuple + + +def import_comment(line: str) -> Tuple[str, str]: + """Parses import lines for comments and returns back the + import statement and the associated comment. + """ + comment_start = line.find("#") + if comment_start != -1: + return (line[:comment_start], line[comment_start + 1 :].strip()) + + return (line, "") diff --git a/tests/test_output.py b/tests/test_output.py new file mode 100644 index 000000000..809392d1e --- /dev/null +++ b/tests/test_output.py @@ -0,0 +1,14 @@ +from hypothesis_auto import auto_pytest_magic + +from isort import output + +auto_pytest_magic(output.grid, auto_allow_exceptions_=(ValueError,)) +auto_pytest_magic(output.vertical, auto_allow_exceptions_=(ValueError,)) +auto_pytest_magic(output.hanging_indent, auto_allow_exceptions_=(ValueError,)) +auto_pytest_magic(output.vertical_hanging_indent, auto_allow_exceptions_=(ValueError,)) +auto_pytest_magic(output.vertical_grid_common, auto_allow_exceptions_=(ValueError,)) +auto_pytest_magic(output.vertical_grid, auto_allow_exceptions_=(ValueError,)) +auto_pytest_magic(output.vertical_grid_grouped, auto_allow_exceptions_=(ValueError,)) +auto_pytest_magic(output.vertical_grid_grouped_no_comma, auto_allow_exceptions_=(ValueError,)) +auto_pytest_magic(output.noqa, auto_allow_exceptions_=(ValueError,)) +auto_pytest_magic(output.with_comments, auto_allow_exceptions_=(ValueError,)) diff --git a/tests/test_parse.py b/tests/test_parse.py new file mode 100644 index 000000000..0d06319df --- /dev/null +++ b/tests/test_parse.py @@ -0,0 +1,5 @@ +from hypothesis_auto import auto_pytest_magic + +from isort import parse + +auto_pytest_magic(parse.import_comment) From 92c44ce3c441098866bd6d9a36419e6e5934ddd3 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 17 Oct 2019 22:54:25 -0700 Subject: [PATCH 0114/1439] skip hypothesis auto tests on Python3.5 --- .hypothesis/unicode_data/11.0.0/charmap.json.gz | Bin 20358 -> 0 bytes tests/conftest.py | 4 ++++ 2 files changed, 4 insertions(+) delete mode 100644 .hypothesis/unicode_data/11.0.0/charmap.json.gz create mode 100644 tests/conftest.py diff --git a/.hypothesis/unicode_data/11.0.0/charmap.json.gz b/.hypothesis/unicode_data/11.0.0/charmap.json.gz deleted file mode 100644 index b6e5fa9ff1b0122697d18ddf7ca26006defad9a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20358 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 M0gY=Wee2u+0H1s|y#N3J diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..a7428ba38 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,4 @@ +import sys + +if sys.version_info[1] == 5: + collect_ignore = ["test_output.py", "test_parse.py"] From 1cce8e2404319d3c3a1b477381f738c3e0e83979 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 18 Oct 2019 23:36:17 -0700 Subject: [PATCH 0115/1439] Ensure tests auto tests don't run on Python3.5 --- tests/conftest.py | 5 +---- tests/test_output.py | 25 ++++++++++++++----------- tests/test_parse.py | 7 +++++-- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index a7428ba38..ad781c713 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1 @@ -import sys - -if sys.version_info[1] == 5: - collect_ignore = ["test_output.py", "test_parse.py"] +"""isort test wide fixtures and configuration""" diff --git a/tests/test_output.py b/tests/test_output.py index 809392d1e..c2ab34027 100644 --- a/tests/test_output.py +++ b/tests/test_output.py @@ -1,14 +1,17 @@ -from hypothesis_auto import auto_pytest_magic +import sys from isort import output -auto_pytest_magic(output.grid, auto_allow_exceptions_=(ValueError,)) -auto_pytest_magic(output.vertical, auto_allow_exceptions_=(ValueError,)) -auto_pytest_magic(output.hanging_indent, auto_allow_exceptions_=(ValueError,)) -auto_pytest_magic(output.vertical_hanging_indent, auto_allow_exceptions_=(ValueError,)) -auto_pytest_magic(output.vertical_grid_common, auto_allow_exceptions_=(ValueError,)) -auto_pytest_magic(output.vertical_grid, auto_allow_exceptions_=(ValueError,)) -auto_pytest_magic(output.vertical_grid_grouped, auto_allow_exceptions_=(ValueError,)) -auto_pytest_magic(output.vertical_grid_grouped_no_comma, auto_allow_exceptions_=(ValueError,)) -auto_pytest_magic(output.noqa, auto_allow_exceptions_=(ValueError,)) -auto_pytest_magic(output.with_comments, auto_allow_exceptions_=(ValueError,)) +if sys.version_info[1] > 5: + from hypothesis_auto import auto_pytest_magic + + auto_pytest_magic(output.grid, auto_allow_exceptions_=(ValueError,)) + auto_pytest_magic(output.vertical, auto_allow_exceptions_=(ValueError,)) + auto_pytest_magic(output.hanging_indent, auto_allow_exceptions_=(ValueError,)) + auto_pytest_magic(output.vertical_hanging_indent, auto_allow_exceptions_=(ValueError,)) + auto_pytest_magic(output.vertical_grid_common, auto_allow_exceptions_=(ValueError,)) + auto_pytest_magic(output.vertical_grid, auto_allow_exceptions_=(ValueError,)) + auto_pytest_magic(output.vertical_grid_grouped, auto_allow_exceptions_=(ValueError,)) + auto_pytest_magic(output.vertical_grid_grouped_no_comma, auto_allow_exceptions_=(ValueError,)) + auto_pytest_magic(output.noqa, auto_allow_exceptions_=(ValueError,)) + auto_pytest_magic(output.with_comments, auto_allow_exceptions_=(ValueError,)) diff --git a/tests/test_parse.py b/tests/test_parse.py index 0d06319df..b35eb0e24 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -1,5 +1,8 @@ -from hypothesis_auto import auto_pytest_magic +import sys from isort import parse -auto_pytest_magic(parse.import_comment) +if sys.version_info[1] > 5: + from hypothesis_auto import auto_pytest_magic + + auto_pytest_magic(parse.import_comment) From 223e4a39c7ae877986864b0896a04c76df2a03be Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 19 Oct 2019 22:53:39 -0700 Subject: [PATCH 0116/1439] Start move of isort parsing logic into separate module --- isort/isort.py | 328 +++---------------------------------------------- isort/parse.py | 314 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 329 insertions(+), 313 deletions(-) diff --git a/isort/isort.py b/isort/isort.py index b6dc21398..a7f17b2e2 100644 --- a/isort/isort.py +++ b/isort/isort.py @@ -49,23 +49,22 @@ def __init__(self, file_contents: str, config: Dict[str, Any], extension: str = self.remove_imports = [ format_simplified(removal) for removal in self.config["remove_imports"] ] - self.add_imports = [format_natural(addition) for addition in self.config["add_imports"]] self._section_comments = [ "# " + value for key, value in self.config.items() if key.startswith("import_heading") and value ] - self.line_separator = self.determine_line_separator(file_contents) - self.in_lines = file_contents.split(self.line_separator) - self.original_num_of_lines = len(self.in_lines) - if (self.original_num_of_lines > 1 or self.in_lines[:1] not in ([], [""])) or self.config[ - "force_adds" - ]: - for add_import in self.add_imports: - self.in_lines.append(add_import) - self.number_of_lines = len(self.in_lines) + # TODO: REMOVE POST REFACTORING + #self.in_lines = file_contents.split(self.line_separator) + #self.original_num_of_lines = len(self.in_lines) + #if (self.original_num_of_lines > 1 or self.in_lines[:1] not in ([], [""])) or self.config[ + #"force_adds" + #]: + #for add_import in self.add_imports: + #self.in_lines.append(add_import) + #self.number_of_lines = len(self.in_lines) self.out_lines = [] # type: List[str] self.comments = { @@ -90,7 +89,11 @@ def __init__(self, file_contents: str, config: Dict[str, Any], extension: str = self.import_index = -1 self._first_comment_index_start = -1 self._first_comment_index_end = -1 - self._parse() + + parse.file_contents(file_contents, line_separator=self.config["line_ending"] or utils.infer_line_separator(file_contents), + add_imports=(format_natural(addition) for addition in self.config["add_imports"]), + force_adds=self.config["force_adds"]) + if self.import_index != -1: self._add_formatted_imports() self.length_change = len(self.out_lines) - self.original_num_of_lines @@ -120,12 +123,6 @@ def check_if_input_already_sorted( print("ERROR: {} Imports are incorrectly sorted.".format(logging_file_path)) return False - def determine_line_separator(self, file_contents: str) -> str: - if self.config["line_ending"]: - return self.config["line_ending"] - else: - return utils.infer_line_separator(file_contents) - @staticmethod def _strip_top_comments(lines: Sequence[str], line_separator: str) -> str: """Strips # comments that exist at the top of the given lines""" @@ -736,6 +733,8 @@ def by_module(line: str) -> str: for index, line in enumerate(tail): in_quote = self._in_quote + (should_skip, self._in_quote, self.in_top_comment, self._first_comment_index_start, self._first_comment_index_end) = parse.skip_line(line, in_quote=self._in_quote, in_top_comment=self.in_top_comment, + index=self.index, section_comments=self._section_comments, first_comment_index_start=self._first_comment_index_start, first_comment_index_end=self._first_comment_index_end) if not self._skip_line(line) and line.strip(): if ( line.strip().startswith("#") @@ -780,42 +779,7 @@ def by_module(line: str) -> str: new_out_lines.append("") self.out_lines = new_out_lines - def _skip_line(self, line: str) -> bool: - skip_line = self._in_quote - if self.index == 1 and line.startswith("#"): - self._in_top_comment = True - return True - elif self._in_top_comment: - if not line.startswith("#") or line in self._section_comments: - self._in_top_comment = False - self._first_comment_index_end = self.index - 1 - - if '"' in line or "'" in line: - index = 0 - if self._first_comment_index_start == -1 and ( - line.startswith('"') or line.startswith("'") - ): - self._first_comment_index_start = self.index - while index < len(line): - if line[index] == "\\": - index += 1 - elif self._in_quote: - if line[index : index + len(self._in_quote)] == self._in_quote: - self._in_quote = False - if self._first_comment_index_end < self._first_comment_index_start: - self._first_comment_index_end = self.index - elif line[index] in ("'", '"'): - long_quote = line[index : index + 3] - if long_quote in ('"""', "'''"): - self._in_quote = long_quote - index += 2 - else: - self._in_quote = line[index] - elif line[index] == "#": - break - index += 1 - return skip_line or self._in_quote or self._in_top_comment def _strip_syntax(self, import_string: str) -> str: import_string = import_string.replace("_import", "[[i]]") @@ -828,263 +792,3 @@ def _strip_syntax(self, import_string: str) -> str: import_string = " ".join(import_list) import_string = import_string.replace("[[i]]", "_import") return import_string.replace("{ ", "{|").replace(" }", "|}") - - def _parse(self) -> None: - """Parses a python file taking out and categorizing imports.""" - self._in_quote = False - self._in_top_comment = False - while not self._at_end(): - raw_line = line = self._get_line() - line = line.replace("from.import ", "from . import ") - line = line.replace("\t", " ").replace("import*", "import *") - line = line.replace(" .import ", " . import ") - statement_index = self.index - skip_line = self._skip_line(line) - - if line in self._section_comments and not skip_line: - if self.import_index == -1: - self.import_index = self.index - 1 - continue - - if "isort:imports-" in line and line.startswith("#"): - section = line.split("isort:imports-")[-1].split()[0].upper() - self.place_imports[section] = [] - self.import_placements[line] = section - - if ";" in line: - for part in (part.strip() for part in line.split(";")): - if part and not part.startswith("from ") and not part.startswith("import "): - skip_line = True - - import_type = self._import_type(line) - if not import_type or skip_line: - self.out_lines.append(raw_line) - continue - - for line in (line.strip() for line in line.split(";")): - import_type = self._import_type(line) - if not import_type: - self.out_lines.append(line) - continue - - if self.import_index == -1: - self.import_index = self.index - 1 - nested_comments = {} - import_string, comment = parse.import_comment(line) - comments = [comment] if comment else [] - line_parts = [ - part for part in self._strip_syntax(import_string).strip().split(" ") if part - ] - if ( - import_type == "from" - and len(line_parts) == 2 - and line_parts[1] != "*" - and comments - ): - nested_comments[line_parts[-1]] = comments[0] - - if "(" in line.split("#")[0] and not self._at_end(): - while not line.strip().endswith(")") and not self._at_end(): - line, new_comment = parse.import_comment(self._get_line()) - if new_comment: - comments.append(new_comment) - stripped_line = self._strip_syntax(line).strip() - if ( - import_type == "from" - and stripped_line - and " " not in stripped_line - and new_comment - ): - nested_comments[stripped_line] = comments[-1] - import_string += self.line_separator + line - else: - while line.strip().endswith("\\"): - line, new_comment = parse.import_comment(self._get_line()) - if new_comment: - comments.append(new_comment) - - # Still need to check for parentheses after an escaped line - if ( - "(" in line.split("#")[0] - and ")" not in line.split("#")[0] - and not self._at_end() - ): - stripped_line = self._strip_syntax(line).strip() - if ( - import_type == "from" - and stripped_line - and " " not in stripped_line - and new_comment - ): - nested_comments[stripped_line] = comments[-1] - import_string += self.line_separator + line - - while not line.strip().endswith(")") and not self._at_end(): - line, new_comment = parse.import_comment(self._get_line()) - if new_comment: - comments.append(new_comment) - stripped_line = self._strip_syntax(line).strip() - if ( - import_type == "from" - and stripped_line - and " " not in stripped_line - and new_comment - ): - nested_comments[stripped_line] = comments[-1] - import_string += self.line_separator + line - - stripped_line = self._strip_syntax(line).strip() - if ( - import_type == "from" - and stripped_line - and " " not in stripped_line - and new_comment - ): - nested_comments[stripped_line] = comments[-1] - if import_string.strip().endswith(" import") or line.strip().startswith( - "import " - ): - import_string += self.line_separator + line - else: - import_string = ( - import_string.rstrip().rstrip("\\") + " " + line.lstrip() - ) - - if import_type == "from": - import_string = import_string.replace("import(", "import (") - parts = import_string.split(" import ") - from_import = parts[0].split(" ") - import_string = " import ".join( - [from_import[0] + " " + "".join(from_import[1:])] + parts[1:] - ) - - imports = [ - item.replace("{|", "{ ").replace("|}", " }") - for item in self._strip_syntax(import_string).split() - ] - straight_import = True - if "as" in imports and (imports.index("as") + 1) < len(imports): - straight_import = False - while "as" in imports: - index = imports.index("as") - if import_type == "from": - module = imports[0] + "." + imports[index - 1] - self.as_map[module].append(imports[index + 1]) - else: - module = imports[index - 1] - self.as_map[module].append(imports[index + 1]) - if not self.config["combine_as_imports"]: - self.comments["straight"][module] = comments - comments = [] - del imports[index : index + 2] - if import_type == "from": - import_from = imports.pop(0) - placed_module = self.place_module(import_from) - if self.config["verbose"]: - print( - "from-type place_module for {} returned {}".format( - import_from, placed_module - ) - ) - if placed_module == "": - print( - "WARNING: could not place module {} of line {} --" - " Do you need to define a default section?".format(import_from, line) - ) - root = self.imports[placed_module][import_type] - for import_name in imports: - associated_comment = nested_comments.get(import_name) - if associated_comment: - self.comments["nested"].setdefault(import_from, {})[ - import_name - ] = associated_comment - comments.pop(comments.index(associated_comment)) - if comments: - self.comments["from"].setdefault(import_from, []).extend(comments) - - if ( - len(self.out_lines) - > max(self.import_index, self._first_comment_index_end + 1, 1) - 1 - ): - last = self.out_lines and self.out_lines[-1].rstrip() or "" - while ( - last.startswith("#") - and not last.endswith('"""') - and not last.endswith("'''") - and "isort:imports-" not in last - ): - self.comments["above"]["from"].setdefault(import_from, []).insert( - 0, self.out_lines.pop(-1) - ) - if ( - len(self.out_lines) - > max(self.import_index - 1, self._first_comment_index_end + 1, 1) - - 1 - ): - last = self.out_lines[-1].rstrip() - else: - last = "" - if statement_index - 1 == self.import_index: - self.import_index -= len( - self.comments["above"]["from"].get(import_from, []) - ) - - if import_from not in root: - root[import_from] = OrderedDict( - (module, straight_import) for module in imports - ) - else: - root[import_from].update( - (module, straight_import | root[import_from].get(module, False)) - for module in imports - ) - else: - for module in imports: - if comments: - self.comments["straight"][module] = comments - comments = None - - if ( - len(self.out_lines) - > max(self.import_index, self._first_comment_index_end + 1, 1) - 1 - ): - - last = self.out_lines and self.out_lines[-1].rstrip() or "" - while ( - last.startswith("#") - and not last.endswith('"""') - and not last.endswith("'''") - and "isort:imports-" not in last - ): - self.comments["above"]["straight"].setdefault(module, []).insert( - 0, self.out_lines.pop(-1) - ) - if ( - len(self.out_lines) > 0 - and len(self.out_lines) != self._first_comment_index_end - ): - last = self.out_lines[-1].rstrip() - else: - last = "" - if self.index - 1 == self.import_index: - self.import_index -= len( - self.comments["above"]["straight"].get(module, []) - ) - placed_module = self.place_module(module) - if self.config["verbose"]: - print( - "else-type place_module for {} returned {}".format( - module, placed_module - ) - ) - if placed_module == "": - print( - "WARNING: could not place module {} of line {} --" - " Do you need to define a default section?".format( - import_from, line - ) - ) - straight_import |= self.imports[placed_module][import_type].get( - module, False - ) - self.imports[placed_module][import_type][module] = straight_import diff --git a/isort/parse.py b/isort/parse.py index c9460c35e..123b89d6b 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -1,5 +1,5 @@ """Defines parsing functions used by isort for parsing import definitions""" -from typing import Tuple +from typing import Tuple, List, Generator, Iterator def import_comment(line: str) -> Tuple[str, str]: @@ -11,3 +11,315 @@ def import_comment(line: str) -> Tuple[str, str]: return (line[:comment_start], line[comment_start + 1 :].strip()) return (line, "") + + +def skip_line(line: str, in_quote: str, in_top_comment: bool, index: int, + section_comments: Iterator[str], first_comment_index_start: int, + first_comment_index_end: int) -> (bool, str, bool, int, int): + skip_line = bool(in_quote) + if index == 1 and line.startswith("#"): + in_top_comment = True + return True + elif in_top_comment: + if not line.startswith("#") or line in section_comments: + in_top_comment = False + first_comment_index_end = index - 1 + + if '"' in line or "'" in line: + index = 0 + if first_comment_index_start == -1 and ( + line.startswith('"') or line.startswith("'") + ): + first_comment_index_start = index + while char_index < len(line): + if line[char_index] == "\\": + char_index += 1 + elif in_quote: + if line[char_index : char_index + len(in_quote)] == in_quote: + in_quote = "" + if first_comment_index_end < first_comment_index_start: + first_comment_index_end = index + elif line[char_index] in ("'", '"'): + long_quote = line[char_index : char_index + 3] + if long_quote in ('"""', "'''"): + in_quote = long_quote + char_index += 2 + else: + in_quote = line[char_index] + elif line[char_index] == "#": + break + char_index += 1 + + return (bool(skip_line or in_quote or in_top_comment), in_quote, + in_top_comment, first_comment_index_start, first_comment_index_end) + + +def file_contents(contents: str, line_separator: str, add_imports: Iterator[str], force_adds: bool) -> dict: + """Parses a python file taking out and categorizing imports.""" + in_lines = contents.split(line_separator) + original_line_count = len(in_lines) + + if original_line_count > 1 or in_lines[:1] not in ([], [""]) or force_adds: + in_lines.extend(add_imports) + + line_count = len(in_lines) + + + + self._in_quote = False + self._in_top_comment = False + while not self._at_end(): + raw_line = line = self._get_line() + line = line.replace("from.import ", "from . import ") + line = line.replace("\t", " ").replace("import*", "import *") + line = line.replace(" .import ", " . import ") + statement_index = self.index + skip_line = self._skip_line(line) + + if line in self._section_comments and not skip_line: + if self.import_index == -1: + self.import_index = self.index - 1 + continue + + if "isort:imports-" in line and line.startswith("#"): + section = line.split("isort:imports-")[-1].split()[0].upper() + self.place_imports[section] = [] + self.import_placements[line] = section + + if ";" in line: + for part in (part.strip() for part in line.split(";")): + if part and not part.startswith("from ") and not part.startswith("import "): + skip_line = True + + import_type = self._import_type(line) + if not import_type or skip_line: + self.out_lines.append(raw_line) + continue + + for line in (line.strip() for line in line.split(";")): + import_type = self._import_type(line) + if not import_type: + self.out_lines.append(line) + continue + + if self.import_index == -1: + self.import_index = self.index - 1 + nested_comments = {} + import_string, comment = parse.import_comment(line) + comments = [comment] if comment else [] + line_parts = [ + part for part in self._strip_syntax(import_string).strip().split(" ") if part + ] + if ( + import_type == "from" + and len(line_parts) == 2 + and line_parts[1] != "*" + and comments + ): + nested_comments[line_parts[-1]] = comments[0] + + if "(" in line.split("#")[0] and not self._at_end(): + while not line.strip().endswith(")") and not self._at_end(): + line, new_comment = parse.import_comment(self._get_line()) + if new_comment: + comments.append(new_comment) + stripped_line = self._strip_syntax(line).strip() + if ( + import_type == "from" + and stripped_line + and " " not in stripped_line + and new_comment + ): + nested_comments[stripped_line] = comments[-1] + import_string += self.line_separator + line + else: + while line.strip().endswith("\\"): + line, new_comment = parse.import_comment(self._get_line()) + if new_comment: + comments.append(new_comment) + + # Still need to check for parentheses after an escaped line + if ( + "(" in line.split("#")[0] + and ")" not in line.split("#")[0] + and not self._at_end() + ): + stripped_line = self._strip_syntax(line).strip() + if ( + import_type == "from" + and stripped_line + and " " not in stripped_line + and new_comment + ): + nested_comments[stripped_line] = comments[-1] + import_string += self.line_separator + line + + while not line.strip().endswith(")") and not self._at_end(): + line, new_comment = parse.import_comment(self._get_line()) + if new_comment: + comments.append(new_comment) + stripped_line = self._strip_syntax(line).strip() + if ( + import_type == "from" + and stripped_line + and " " not in stripped_line + and new_comment + ): + nested_comments[stripped_line] = comments[-1] + import_string += self.line_separator + line + + stripped_line = self._strip_syntax(line).strip() + if ( + import_type == "from" + and stripped_line + and " " not in stripped_line + and new_comment + ): + nested_comments[stripped_line] = comments[-1] + if import_string.strip().endswith(" import") or line.strip().startswith( + "import " + ): + import_string += self.line_separator + line + else: + import_string = ( + import_string.rstrip().rstrip("\\") + " " + line.lstrip() + ) + + if import_type == "from": + import_string = import_string.replace("import(", "import (") + parts = import_string.split(" import ") + from_import = parts[0].split(" ") + import_string = " import ".join( + [from_import[0] + " " + "".join(from_import[1:])] + parts[1:] + ) + + imports = [ + item.replace("{|", "{ ").replace("|}", " }") + for item in self._strip_syntax(import_string).split() + ] + straight_import = True + if "as" in imports and (imports.index("as") + 1) < len(imports): + straight_import = False + while "as" in imports: + index = imports.index("as") + if import_type == "from": + module = imports[0] + "." + imports[index - 1] + self.as_map[module].append(imports[index + 1]) + else: + module = imports[index - 1] + self.as_map[module].append(imports[index + 1]) + if not self.config["combine_as_imports"]: + self.comments["straight"][module] = comments + comments = [] + del imports[index : index + 2] + if import_type == "from": + import_from = imports.pop(0) + placed_module = self.place_module(import_from) + if self.config["verbose"]: + print( + "from-type place_module for {} returned {}".format( + import_from, placed_module + ) + ) + if placed_module == "": + print( + "WARNING: could not place module {} of line {} --" + " Do you need to define a default section?".format(import_from, line) + ) + root = self.imports[placed_module][import_type] + for import_name in imports: + associated_comment = nested_comments.get(import_name) + if associated_comment: + self.comments["nested"].setdefault(import_from, {})[ + import_name + ] = associated_comment + comments.pop(comments.index(associated_comment)) + if comments: + self.comments["from"].setdefault(import_from, []).extend(comments) + + if ( + len(self.out_lines) + > max(self.import_index, self._first_comment_index_end + 1, 1) - 1 + ): + last = self.out_lines and self.out_lines[-1].rstrip() or "" + while ( + last.startswith("#") + and not last.endswith('"""') + and not last.endswith("'''") + and "isort:imports-" not in last + ): + self.comments["above"]["from"].setdefault(import_from, []).insert( + 0, self.out_lines.pop(-1) + ) + if ( + len(self.out_lines) + > max(self.import_index - 1, self._first_comment_index_end + 1, 1) + - 1 + ): + last = self.out_lines[-1].rstrip() + else: + last = "" + if statement_index - 1 == self.import_index: + self.import_index -= len( + self.comments["above"]["from"].get(import_from, []) + ) + + if import_from not in root: + root[import_from] = OrderedDict( + (module, straight_import) for module in imports + ) + else: + root[import_from].update( + (module, straight_import | root[import_from].get(module, False)) + for module in imports + ) + else: + for module in imports: + if comments: + self.comments["straight"][module] = comments + comments = None + + if ( + len(self.out_lines) + > max(self.import_index, self._first_comment_index_end + 1, 1) - 1 + ): + + last = self.out_lines and self.out_lines[-1].rstrip() or "" + while ( + last.startswith("#") + and not last.endswith('"""') + and not last.endswith("'''") + and "isort:imports-" not in last + ): + self.comments["above"]["straight"].setdefault(module, []).insert( + 0, self.out_lines.pop(-1) + ) + if ( + len(self.out_lines) > 0 + and len(self.out_lines) != self._first_comment_index_end + ): + last = self.out_lines[-1].rstrip() + else: + last = "" + if self.index - 1 == self.import_index: + self.import_index -= len( + self.comments["above"]["straight"].get(module, []) + ) + placed_module = self.place_module(module) + if self.config["verbose"]: + print( + "else-type place_module for {} returned {}".format( + module, placed_module + ) + ) + if placed_module == "": + print( + "WARNING: could not place module {} of line {} --" + " Do you need to define a default section?".format( + import_from, line + ) + ) + straight_import |= self.imports[placed_module][import_type].get( + module, False + ) + self.imports[placed_module][import_type][module] = straight_import From a3e22bc5cdcb3107ae2d0ff0cde424770bd23f61 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 19 Oct 2019 23:33:41 -0700 Subject: [PATCH 0117/1439] Add hypothesis dir to git ignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index d8ec662df..74f41785f 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ nosetests.xml htmlcov .cache .pytest_cache/ +.hypothesis/ # Translations *.mo From bfea1ac83883418113ce7c44a0fc37c2a8dadae4 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 20 Oct 2019 23:33:47 -0700 Subject: [PATCH 0118/1439] Get rid of all self references within file contents parsing, replacing with function arguments --- isort/parse.py | 336 +++++++++++++++++++++++++++++++------------------ 1 file changed, 213 insertions(+), 123 deletions(-) diff --git a/isort/parse.py b/isort/parse.py index 123b89d6b..07b1a47b6 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -1,5 +1,22 @@ """Defines parsing functions used by isort for parsing import definitions""" -from typing import Tuple, List, Generator, Iterator +from collections import defaultdict, OrderedDict +from typing import Tuple, List, Generator, Iterator, TYPE_CHECKING, Optional, Any +from .finders import FindersManager +from itertools import chain + + +if TYPE_CHECKING: + from mypy_extensions import TypedDict + + CommentsDict = TypedDict( + "CommentsDict", + { + "from": Dict[str, Any], + "straight": Dict[str, Any], + "nested": Dict[str, Any], + "above": CommentsAboveDict, + }, + ) def import_comment(line: str) -> Tuple[str, str]: @@ -13,9 +30,49 @@ def import_comment(line: str) -> Tuple[str, str]: return (line, "") -def skip_line(line: str, in_quote: str, in_top_comment: bool, index: int, - section_comments: Iterator[str], first_comment_index_start: int, - first_comment_index_end: int) -> (bool, str, bool, int, int): +def import_type(line: str) -> Optional[str]: + """If the current line is an import line it will return its type (from or straight)""" + if "isort:skip" in line or "NOQA" in line: + return None + elif line.startswith("import "): + return "straight" + elif line.startswith("from "): + return "from" + return None + + +def _strip_syntax(import_string: str) -> str: + import_string = import_string.replace("_import", "[[i]]") + for remove_syntax in ["\\", "(", ")", ","]: + import_string = import_string.replace(remove_syntax, " ") + import_list = import_string.split() + for key in ("from", "import"): + if key in import_list: + import_list.remove(key) + import_string = " ".join(import_list) + import_string = import_string.replace("[[i]]", "_import") + return import_string.replace("{ ", "{|").replace(" }", "|}") + + +def skip_line( + line: str, + in_quote: str, + in_top_comment: bool, + index: int, + section_comments: Iterator[str], + first_comment_index_start: int, + first_comment_index_end: int, +) -> (bool, str, bool, int, int): + """Determine if a given line should be skipped. + + Returns back a tuple containing: + + (skip_line: bool, + in_quote: str, + in_top_comment: bool, + first_comment_index_start: int, + first_comment_index_end: int) + """ skip_line = bool(in_quote) if index == 1 and line.startswith("#"): in_top_comment = True @@ -26,10 +83,8 @@ def skip_line(line: str, in_quote: str, in_top_comment: bool, index: int, first_comment_index_end = index - 1 if '"' in line or "'" in line: - index = 0 - if first_comment_index_start == -1 and ( - line.startswith('"') or line.startswith("'") - ): + char_index = 0 + if first_comment_index_start == -1 and (line.startswith('"') or line.startswith("'")): first_comment_index_start = index while char_index < len(line): if line[char_index] == "\\": @@ -50,13 +105,30 @@ def skip_line(line: str, in_quote: str, in_top_comment: bool, index: int, break char_index += 1 - return (bool(skip_line or in_quote or in_top_comment), in_quote, - in_top_comment, first_comment_index_start, first_comment_index_end) + return ( + bool(skip_line or in_quote or in_top_comment), + in_quote, + in_top_comment, + first_comment_index_start, + first_comment_index_end, + ) -def file_contents(contents: str, line_separator: str, add_imports: Iterator[str], force_adds: bool) -> dict: +def file_contents( + contents: str, + line_separator: str, + add_imports: Iterator[str], + force_adds: bool, + sections: Any, + section_comments: Iterator[str], + forced_separate: Iterator[str], + combine_as_imports: bool, + verbose: bool, + finder: FindersManager, +) -> dict: """Parses a python file taking out and categorizing imports.""" in_lines = contents.split(line_separator) + out_lines = [] original_line_count = len(in_lines) if original_line_count > 1 or in_lines[:1] not in ([], [""]) or force_adds: @@ -64,77 +136,108 @@ def file_contents(contents: str, line_separator: str, add_imports: Iterator[str] line_count = len(in_lines) + place_imports = {} # type: Dict[str, List[str]] + import_placements = {} # type: Dict[str, str] + as_map = defaultdict(list) + imports = OrderedDict() # type: OrderedDict[str, Dict[str, Any]] + for section in chain(sections, forced_separate): + imports[section] = {"straight": OrderedDict(), "from": OrderedDict()} + categorized_comments = { + "from": {}, + "straight": {}, + "nested": {}, + "above": {"straight": {}, "from": {}}, + } # type: CommentsDict - - self._in_quote = False - self._in_top_comment = False - while not self._at_end(): - raw_line = line = self._get_line() + index = 0 + import_index = -1 + in_quote = "" + in_top_comment = False + first_comment_index_start = -1 + first_comment_index_end = -1 + while index < line_count: + raw_line = line = in_lines[index] line = line.replace("from.import ", "from . import ") line = line.replace("\t", " ").replace("import*", "import *") line = line.replace(" .import ", " . import ") - statement_index = self.index - skip_line = self._skip_line(line) + index += 1 + statement_index = index - if line in self._section_comments and not skip_line: - if self.import_index == -1: - self.import_index = self.index - 1 + ( + skipping_line, + in_quote, + in_top_comment, + first_comment_index_start, + first_comment_index_end, + ) = skip_line( + line, + in_quote=in_quote, + in_top_comment=in_top_comment, + index=index, + section_comments=section_comments, + first_comment_index_start=first_comment_index_start, + first_comment_index_end=first_comment_index_end, + ) + + if line in section_comments and not skipping_line: + if import_index == -1: + import_index = index - 1 continue if "isort:imports-" in line and line.startswith("#"): section = line.split("isort:imports-")[-1].split()[0].upper() - self.place_imports[section] = [] - self.import_placements[line] = section + place_imports[section] = [] + import_placements[line] = section if ";" in line: for part in (part.strip() for part in line.split(";")): if part and not part.startswith("from ") and not part.startswith("import "): - skip_line = True + skipping_line = True - import_type = self._import_type(line) - if not import_type or skip_line: - self.out_lines.append(raw_line) + type_of_import = import_type(line) + if not type_of_import or skipping_line: + out_lines.append(raw_line) continue for line in (line.strip() for line in line.split(";")): - import_type = self._import_type(line) - if not import_type: - self.out_lines.append(line) + type_of_import = import_type(line) + if not type_of_import: + out_lines.append(line) continue - if self.import_index == -1: - self.import_index = self.index - 1 + if import_index == -1: + import_index = index - 1 nested_comments = {} - import_string, comment = parse.import_comment(line) + import_string, comment = import_comment(line) comments = [comment] if comment else [] - line_parts = [ - part for part in self._strip_syntax(import_string).strip().split(" ") if part - ] + line_parts = [part for part in _strip_syntax(import_string).strip().split(" ") if part] if ( - import_type == "from" + type_of_import == "from" and len(line_parts) == 2 and line_parts[1] != "*" and comments ): nested_comments[line_parts[-1]] = comments[0] - if "(" in line.split("#")[0] and not self._at_end(): - while not line.strip().endswith(")") and not self._at_end(): - line, new_comment = parse.import_comment(self._get_line()) + if "(" in line.split("#")[0] and index < line_count: + while not line.strip().endswith(")") and index < line_count: + line, new_comment = import_comment(in_lines[index]) + index += 1 if new_comment: comments.append(new_comment) - stripped_line = self._strip_syntax(line).strip() + stripped_line = _strip_syntax(line).strip() if ( - import_type == "from" + type_of_import == "from" and stripped_line and " " not in stripped_line and new_comment ): nested_comments[stripped_line] = comments[-1] - import_string += self.line_separator + line + import_string += line_separator + line else: while line.strip().endswith("\\"): - line, new_comment = parse.import_comment(self._get_line()) + line, new_comment = import_comment(in_lines[index]) + index += 1 if new_comment: comments.append(new_comment) @@ -142,35 +245,36 @@ def file_contents(contents: str, line_separator: str, add_imports: Iterator[str] if ( "(" in line.split("#")[0] and ")" not in line.split("#")[0] - and not self._at_end() + and index < line_count ): - stripped_line = self._strip_syntax(line).strip() + stripped_line = _strip_syntax(line).strip() if ( - import_type == "from" + type_of_import == "from" and stripped_line and " " not in stripped_line and new_comment ): nested_comments[stripped_line] = comments[-1] - import_string += self.line_separator + line + import_string += line_separator + line - while not line.strip().endswith(")") and not self._at_end(): - line, new_comment = parse.import_comment(self._get_line()) + while not line.strip().endswith(")") and index < line_count: + line, new_comment = import_comment(in_lines[index]) + index += 1 if new_comment: comments.append(new_comment) - stripped_line = self._strip_syntax(line).strip() + stripped_line = _strip_syntax(line).strip() if ( - import_type == "from" + type_of_import == "from" and stripped_line and " " not in stripped_line and new_comment ): nested_comments[stripped_line] = comments[-1] - import_string += self.line_separator + line + import_string += line_separator + line - stripped_line = self._strip_syntax(line).strip() + stripped_line = _strip_syntax(line).strip() if ( - import_type == "from" + type_of_import == "from" and stripped_line and " " not in stripped_line and new_comment @@ -179,13 +283,11 @@ def file_contents(contents: str, line_separator: str, add_imports: Iterator[str] if import_string.strip().endswith(" import") or line.strip().startswith( "import " ): - import_string += self.line_separator + line + import_string += line_separator + line else: - import_string = ( - import_string.rstrip().rstrip("\\") + " " + line.lstrip() - ) + import_string = import_string.rstrip().rstrip("\\") + " " + line.lstrip() - if import_type == "from": + if type_of_import == "from": import_string = import_string.replace("import(", "import (") parts = import_string.split(" import ") from_import = parts[0].split(" ") @@ -193,29 +295,29 @@ def file_contents(contents: str, line_separator: str, add_imports: Iterator[str] [from_import[0] + " " + "".join(from_import[1:])] + parts[1:] ) - imports = [ + just_imports = [ item.replace("{|", "{ ").replace("|}", " }") - for item in self._strip_syntax(import_string).split() + for item in _strip_syntax(import_string).split() ] straight_import = True - if "as" in imports and (imports.index("as") + 1) < len(imports): + if "as" in just_imports and (just_imports.index("as") + 1) < len(just_imports): straight_import = False - while "as" in imports: - index = imports.index("as") - if import_type == "from": - module = imports[0] + "." + imports[index - 1] - self.as_map[module].append(imports[index + 1]) + while "as" in just_imports: + index = just_imports.index("as") + if type_of_import == "from": + module = just_imports[0] + "." + just_imports[index - 1] + as_map[module].append(just_imports[index + 1]) else: - module = imports[index - 1] - self.as_map[module].append(imports[index + 1]) - if not self.config["combine_as_imports"]: - self.comments["straight"][module] = comments + module = just_imports[index - 1] + as_map[module].append(just_imports[index + 1]) + if not combine_as_imports: + categorized_comments["straight"][module] = comments comments = [] - del imports[index : index + 2] - if import_type == "from": - import_from = imports.pop(0) - placed_module = self.place_module(import_from) - if self.config["verbose"]: + del just_imports[index : index + 2] + if type_of_import == "from": + import_from = just_imports.pop(0) + placed_module = finder.find(import_from) + if verbose: print( "from-type place_module for {} returned {}".format( import_from, placed_module @@ -226,87 +328,77 @@ def file_contents(contents: str, line_separator: str, add_imports: Iterator[str] "WARNING: could not place module {} of line {} --" " Do you need to define a default section?".format(import_from, line) ) - root = self.imports[placed_module][import_type] - for import_name in imports: + root = imports[placed_module][type_of_import] + for import_name in just_imports: associated_comment = nested_comments.get(import_name) if associated_comment: - self.comments["nested"].setdefault(import_from, {})[ + categorized_comments["nested"].setdefault(import_from, {})[ import_name ] = associated_comment comments.pop(comments.index(associated_comment)) if comments: - self.comments["from"].setdefault(import_from, []).extend(comments) + categorized_comments["from"].setdefault(import_from, []).extend(comments) - if ( - len(self.out_lines) - > max(self.import_index, self._first_comment_index_end + 1, 1) - 1 - ): - last = self.out_lines and self.out_lines[-1].rstrip() or "" + if len(out_lines) > max(import_index, first_comment_index_end + 1, 1) - 1: + last = out_lines and out_lines[-1].rstrip() or "" while ( last.startswith("#") and not last.endswith('"""') and not last.endswith("'''") and "isort:imports-" not in last ): - self.comments["above"]["from"].setdefault(import_from, []).insert( - 0, self.out_lines.pop(-1) + categorized_comments["above"]["from"].setdefault(import_from, []).insert( + 0, out_lines.pop(-1) ) if ( - len(self.out_lines) - > max(self.import_index - 1, self._first_comment_index_end + 1, 1) - - 1 + len(out_lines) + > max(import_index - 1, first_comment_index_end + 1, 1) - 1 ): - last = self.out_lines[-1].rstrip() + last = out_lines[-1].rstrip() else: last = "" - if statement_index - 1 == self.import_index: - self.import_index -= len( - self.comments["above"]["from"].get(import_from, []) + if statement_index - 1 == import_index: + import_index -= len( + categorized_comments["above"]["from"].get(import_from, []) ) if import_from not in root: root[import_from] = OrderedDict( - (module, straight_import) for module in imports + (module, straight_import) for module in just_imports ) else: root[import_from].update( (module, straight_import | root[import_from].get(module, False)) - for module in imports + for module in just_imports ) else: - for module in imports: + for module in just_imports: if comments: - self.comments["straight"][module] = comments + categorized_comments["straight"][module] = comments comments = None - if ( - len(self.out_lines) - > max(self.import_index, self._first_comment_index_end + 1, 1) - 1 - ): + if len(out_lines) > max(import_index, first_comment_index_end + 1, 1) - 1: - last = self.out_lines and self.out_lines[-1].rstrip() or "" + last = out_lines and out_lines[-1].rstrip() or "" while ( last.startswith("#") and not last.endswith('"""') and not last.endswith("'''") and "isort:imports-" not in last ): - self.comments["above"]["straight"].setdefault(module, []).insert( - 0, self.out_lines.pop(-1) + categorized_comments["above"]["straight"].setdefault(module, []).insert( + 0, out_lines.pop(-1) ) - if ( - len(self.out_lines) > 0 - and len(self.out_lines) != self._first_comment_index_end - ): - last = self.out_lines[-1].rstrip() + if len(out_lines) > 0 and len(out_lines) != first_comment_index_end: + last = out_lines[-1].rstrip() else: last = "" - if self.index - 1 == self.import_index: - self.import_index -= len( - self.comments["above"]["straight"].get(module, []) + if index - 1 == import_index: + import_index -= len( + categorized_comments["above"]["straight"].get(module, []) ) - placed_module = self.place_module(module) - if self.config["verbose"]: + placed_module = finder.find(module) + if verbose: print( "else-type place_module for {} returned {}".format( module, placed_module @@ -315,11 +407,9 @@ def file_contents(contents: str, line_separator: str, add_imports: Iterator[str] if placed_module == "": print( "WARNING: could not place module {} of line {} --" - " Do you need to define a default section?".format( - import_from, line - ) + " Do you need to define a default section?".format(import_from, line) ) - straight_import |= self.imports[placed_module][import_type].get( - module, False - ) - self.imports[placed_module][import_type][module] = straight_import + straight_import |= imports[placed_module][type_of_import].get(module, False) + imports[placed_module][type_of_import][module] = straight_import + + change_count = len(out_lines) - original_line_count From 8f0d26f53dcf5d1f187d8c8f8b3374a28e54dd64 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 21 Oct 2019 01:33:25 -0700 Subject: [PATCH 0119/1439] First working implementation --- isort/isort.py | 144 +++++++++++++++-------------------------------- isort/parse.py | 69 +++++++++++++++++------ scripts/clean.sh | 2 +- 3 files changed, 98 insertions(+), 117 deletions(-) diff --git a/isort/isort.py b/isort/isort.py index a7f17b2e2..4d714680a 100644 --- a/isort/isort.py +++ b/isort/isort.py @@ -9,7 +9,7 @@ import itertools import re from collections import OrderedDict, defaultdict, namedtuple -from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple +from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple from isort import utils from isort.format import format_natural, format_simplified @@ -19,22 +19,6 @@ from .natural import nsorted from .settings import WrapModes -if TYPE_CHECKING: - from mypy_extensions import TypedDict - - CommentsAboveDict = TypedDict( - "CommentsAboveDict", {"straight": Dict[str, Any], "from": Dict[str, Any]} - ) - CommentsDict = TypedDict( - "CommentsDict", - { - "from": Dict[str, Any], - "straight": Dict[str, Any], - "nested": Dict[str, Any], - "above": CommentsAboveDict, - }, - ) - class _SortImports: _import_line_intro_re = re.compile("^(?:from|import) ") @@ -44,8 +28,6 @@ def __init__(self, file_contents: str, config: Dict[str, Any], extension: str = self.config = config self.extension = extension - self.place_imports = {} # type: Dict[str, List[str]] - self.import_placements = {} # type: Dict[str, str] self.remove_imports = [ format_simplified(removal) for removal in self.config["remove_imports"] ] @@ -54,49 +36,43 @@ def __init__(self, file_contents: str, config: Dict[str, Any], extension: str = for key, value in self.config.items() if key.startswith("import_heading") and value ] - - - # TODO: REMOVE POST REFACTORING - #self.in_lines = file_contents.split(self.line_separator) - #self.original_num_of_lines = len(self.in_lines) - #if (self.original_num_of_lines > 1 or self.in_lines[:1] not in ([], [""])) or self.config[ - #"force_adds" - #]: - #for add_import in self.add_imports: - #self.in_lines.append(add_import) - #self.number_of_lines = len(self.in_lines) - - self.out_lines = [] # type: List[str] - self.comments = { - "from": {}, - "straight": {}, - "nested": {}, - "above": {"straight": {}, "from": {}}, - } # type: CommentsDict - self.imports = OrderedDict() # type: OrderedDict[str, Dict[str, Any]] - self.as_map = defaultdict(list) # type: Dict[str, List[str]] - section_names = self.config["sections"] self.sections = namedtuple("Sections", section_names)( *[name for name in section_names] ) # type: Any - for section in itertools.chain(self.sections, self.config["forced_separate"]): - self.imports[section] = {"straight": OrderedDict(), "from": OrderedDict()} self.finder = FindersManager(config=self.config, sections=self.sections) - - self.index = 0 - self.import_index = -1 - self._first_comment_index_start = -1 - self._first_comment_index_end = -1 - - parse.file_contents(file_contents, line_separator=self.config["line_ending"] or utils.infer_line_separator(file_contents), - add_imports=(format_natural(addition) for addition in self.config["add_imports"]), - force_adds=self.config["force_adds"]) + self.line_separator = self.config["line_ending"] or utils.infer_line_separator( + file_contents + ) + ( + self.in_lines, + self.out_lines, + self.import_index, + self.place_imports, + self.import_placements, + self.as_map, + self.imports, + self.comments, + self._first_comment_index_start, + self._first_comment_index_end, + self.length_change, + self.original_num_of_lines, + ) = parse.file_contents( + file_contents, + line_separator=self.line_separator, + add_imports=(format_natural(addition) for addition in self.config["add_imports"]), + force_adds=self.config["force_adds"], + sections=self.sections, + section_comments=self._section_comments, + forced_separate=self.config["forced_separate"], + combine_as_imports=self.config["combine_as_imports"], + verbose=self.config["verbose"], + finder=self.finder, + ) if self.import_index != -1: self._add_formatted_imports() - self.length_change = len(self.out_lines) - self.original_num_of_lines while self.out_lines and self.out_lines[-1].strip() == "": self.out_lines.pop(-1) self.out_lines.append("") @@ -131,35 +107,6 @@ def _strip_top_comments(lines: Sequence[str], line_separator: str) -> str: lines = lines[1:] return line_separator.join(lines) - def place_module(self, module_name: str) -> Optional[str]: - """Tries to determine if a module is a python std import, third party import, or project code: - - if it can't determine - it assumes it is project code - - """ - return self.finder.find(module_name) - - def _get_line(self) -> str: - """Returns the current line from the file while incrementing the index.""" - line = self.in_lines[self.index] - self.index += 1 - return line - - @staticmethod - def _import_type(line: str) -> Optional[str]: - """If the current line is an import line it will return its type (from or straight)""" - if "isort:skip" in line or "NOQA" in line: - return None - elif line.startswith("import "): - return "straight" - elif line.startswith("from "): - return "from" - return None - - def _at_end(self) -> bool: - """returns True if we are at the end of the file.""" - return self.index == self.number_of_lines - @staticmethod def _module_key( module_name: str, @@ -733,9 +680,22 @@ def by_module(line: str) -> str: for index, line in enumerate(tail): in_quote = self._in_quote - (should_skip, self._in_quote, self.in_top_comment, self._first_comment_index_start, self._first_comment_index_end) = parse.skip_line(line, in_quote=self._in_quote, in_top_comment=self.in_top_comment, - index=self.index, section_comments=self._section_comments, first_comment_index_start=self._first_comment_index_start, first_comment_index_end=self._first_comment_index_end) - if not self._skip_line(line) and line.strip(): + ( + should_skip, + self._in_quote, + self.in_top_comment, + self._first_comment_index_start, + self._first_comment_index_end, + ) = parse.skip_line( + line, + in_quote=self._in_quote, + in_top_comment=False, + index=len(self.out_lines), + section_comments=self._section_comments, + first_comment_index_start=self._first_comment_index_start, + first_comment_index_end=self._first_comment_index_end, + ) + if not should_skip and line.strip(): if ( line.strip().startswith("#") and len(tail) > (index + 1) @@ -778,17 +738,3 @@ def by_module(line: str) -> str: if len(self.out_lines) <= index or self.out_lines[index + 1].strip() != "": new_out_lines.append("") self.out_lines = new_out_lines - - - - def _strip_syntax(self, import_string: str) -> str: - import_string = import_string.replace("_import", "[[i]]") - for remove_syntax in ["\\", "(", ")", ","]: - import_string = import_string.replace(remove_syntax, " ") - import_list = import_string.split() - for key in ("from", "import"): - if key in import_list: - import_list.remove(key) - import_string = " ".join(import_list) - import_string = import_string.replace("[[i]]", "_import") - return import_string.replace("{ ", "{|").replace(" }", "|}") diff --git a/isort/parse.py b/isort/parse.py index 07b1a47b6..02a14f657 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -1,13 +1,18 @@ """Defines parsing functions used by isort for parsing import definitions""" from collections import defaultdict, OrderedDict -from typing import Tuple, List, Generator, Iterator, TYPE_CHECKING, Optional, Any +from typing import Tuple, List, Generator, Iterator, TYPE_CHECKING, Optional, Any, Dict from .finders import FindersManager from itertools import chain +from warnings import warn if TYPE_CHECKING: from mypy_extensions import TypedDict + CommentsAboveDict = TypedDict( + "CommentsAboveDict", {"straight": Dict[str, Any], "from": Dict[str, Any]} + ) + CommentsDict = TypedDict( "CommentsDict", { @@ -59,10 +64,10 @@ def skip_line( in_quote: str, in_top_comment: bool, index: int, - section_comments: Iterator[str], + section_comments: List[str], first_comment_index_start: int, first_comment_index_end: int, -) -> (bool, str, bool, int, int): +) -> Tuple[bool, str, bool, int, int]: """Determine if a given line should be skipped. Returns back a tuple containing: @@ -76,7 +81,7 @@ def skip_line( skip_line = bool(in_quote) if index == 1 and line.startswith("#"): in_top_comment = True - return True + return (True, in_quote, in_top_comment, first_comment_index_start, first_comment_index_end) elif in_top_comment: if not line.startswith("#") or line in section_comments: in_top_comment = False @@ -120,12 +125,25 @@ def file_contents( add_imports: Iterator[str], force_adds: bool, sections: Any, - section_comments: Iterator[str], + section_comments: List[str], forced_separate: Iterator[str], combine_as_imports: bool, verbose: bool, finder: FindersManager, -) -> dict: +) -> Tuple[ + List[str], + List[str], + int, + Dict[str, List[str]], + Dict[str, str], + Dict[str, List[str]], + Dict[str, Dict[str, Any]], + "CommentsDict", + int, + int, + int, + int, +]: """Parses a python file taking out and categorizing imports.""" in_lines = contents.split(line_separator) out_lines = [] @@ -138,7 +156,7 @@ def file_contents( place_imports = {} # type: Dict[str, List[str]] import_placements = {} # type: Dict[str, str] - as_map = defaultdict(list) + as_map = defaultdict(list) # type: Dict[str, List[str]] imports = OrderedDict() # type: OrderedDict[str, Dict[str, Any]] for section in chain(sections, forced_separate): imports[section] = {"straight": OrderedDict(), "from": OrderedDict()} @@ -194,13 +212,13 @@ def file_contents( if part and not part.startswith("from ") and not part.startswith("import "): skipping_line = True - type_of_import = import_type(line) + type_of_import = import_type(line) or "" # type: str if not type_of_import or skipping_line: out_lines.append(raw_line) continue for line in (line.strip() for line in line.split(";")): - type_of_import = import_type(line) + type_of_import = import_type(line) or "" if not type_of_import: out_lines.append(line) continue @@ -324,11 +342,11 @@ def file_contents( ) ) if placed_module == "": - print( - "WARNING: could not place module {} of line {} --" + warn( + "could not place module {} of line {} --" " Do you need to define a default section?".format(import_from, line) ) - root = imports[placed_module][type_of_import] + root = imports[placed_module][type_of_import] # type: ignore for import_name in just_imports: associated_comment = nested_comments.get(import_name) if associated_comment: @@ -375,7 +393,7 @@ def file_contents( for module in just_imports: if comments: categorized_comments["straight"][module] = comments - comments = None + comments = [] if len(out_lines) > max(import_index, first_comment_index_end + 1, 1) - 1: @@ -405,11 +423,28 @@ def file_contents( ) ) if placed_module == "": - print( - "WARNING: could not place module {} of line {} --" + warn( + "could not place module {} of line {} --" " Do you need to define a default section?".format(import_from, line) ) - straight_import |= imports[placed_module][type_of_import].get(module, False) - imports[placed_module][type_of_import][module] = straight_import + straight_import |= imports[placed_module][type_of_import].get( # type: ignore + module, False + ) + imports[placed_module][type_of_import][module] = straight_import # type: ignore change_count = len(out_lines) - original_line_count + + return ( + in_lines, + out_lines, + import_index, + place_imports, + import_placements, + as_map, + imports, + categorized_comments, + first_comment_index_start, + first_comment_index_end, + change_count, + original_line_count, + ) diff --git a/scripts/clean.sh b/scripts/clean.sh index 45e5688c8..57bed2007 100755 --- a/scripts/clean.sh +++ b/scripts/clean.sh @@ -1,5 +1,5 @@ #!/bin/bash set -euxo pipefail -poetry run isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --use-parentheses --line-width=100 --recursive isort/ tests/ +# poetry run isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --use-parentheses --line-width=100 --recursive isort/ tests/ poetry run black isort/ tests/ -l 100 From 26522ccfd6c4edd988d636eb146c511a28482fdf Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 21 Oct 2019 01:40:17 -0700 Subject: [PATCH 0120/1439] Passing tests! --- .../unicode_data/11.0.0/charmap.json.gz | Bin 20358 -> 0 bytes isort/parse.py | 18 +++++++++--------- scripts/clean.sh | 2 +- scripts/mkstdlibs.py | 1 - 4 files changed, 10 insertions(+), 11 deletions(-) delete mode 100644 .hypothesis/unicode_data/11.0.0/charmap.json.gz diff --git a/.hypothesis/unicode_data/11.0.0/charmap.json.gz b/.hypothesis/unicode_data/11.0.0/charmap.json.gz deleted file mode 100644 index b6e5fa9ff1b0122697d18ddf7ca26006defad9a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20358 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 M0gY=Wee2u+0H1s|y#N3J diff --git a/isort/parse.py b/isort/parse.py index 02a14f657..4b036bdd5 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -1,10 +1,10 @@ """Defines parsing functions used by isort for parsing import definitions""" -from collections import defaultdict, OrderedDict -from typing import Tuple, List, Generator, Iterator, TYPE_CHECKING, Optional, Any, Dict -from .finders import FindersManager +from collections import OrderedDict, defaultdict from itertools import chain +from typing import TYPE_CHECKING, Any, Dict, Generator, Iterator, List, Optional, Tuple from warnings import warn +from .finders import FindersManager if TYPE_CHECKING: from mypy_extensions import TypedDict @@ -321,17 +321,17 @@ def file_contents( if "as" in just_imports and (just_imports.index("as") + 1) < len(just_imports): straight_import = False while "as" in just_imports: - index = just_imports.index("as") + as_index = just_imports.index("as") if type_of_import == "from": - module = just_imports[0] + "." + just_imports[index - 1] - as_map[module].append(just_imports[index + 1]) + module = just_imports[0] + "." + just_imports[as_index - 1] + as_map[module].append(just_imports[as_index + 1]) else: - module = just_imports[index - 1] - as_map[module].append(just_imports[index + 1]) + module = just_imports[as_index - 1] + as_map[module].append(just_imports[as_index + 1]) if not combine_as_imports: categorized_comments["straight"][module] = comments comments = [] - del just_imports[index : index + 2] + del just_imports[as_index : as_index + 2] if type_of_import == "from": import_from = just_imports.pop(0) placed_module = finder.find(import_from) diff --git a/scripts/clean.sh b/scripts/clean.sh index 57bed2007..45e5688c8 100755 --- a/scripts/clean.sh +++ b/scripts/clean.sh @@ -1,5 +1,5 @@ #!/bin/bash set -euxo pipefail -# poetry run isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --use-parentheses --line-width=100 --recursive isort/ tests/ +poetry run isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --use-parentheses --line-width=100 --recursive isort/ tests/ poetry run black isort/ tests/ -l 100 diff --git a/scripts/mkstdlibs.py b/scripts/mkstdlibs.py index 4a72c75c3..a58cc8908 100755 --- a/scripts/mkstdlibs.py +++ b/scripts/mkstdlibs.py @@ -2,7 +2,6 @@ from sphinx.ext.intersphinx import fetch_inventory - URL = "https://docs.python.org/{}/objects.inv" PATH = "isort/stdlibs/py{}.py" VERSIONS = [("2", "7"), ("3",)] From edc20e4de794adfd92faeafb2c3fae88bcdb2e93 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 21 Oct 2019 02:55:47 -0700 Subject: [PATCH 0121/1439] Ignore hypothesis output --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index d8ec662df..422f2ce7d 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ nosetests.xml htmlcov .cache .pytest_cache/ +.hypothesis # Translations *.mo From 4c74a02b669efd7ec66dbc217d8e55f149cd94d1 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 21 Oct 2019 02:56:07 -0700 Subject: [PATCH 0122/1439] Add test cases for parse modue --- tests/test_parse.py | 46 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/test_parse.py b/tests/test_parse.py index 0d06319df..1c1126893 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -1,5 +1,51 @@ from hypothesis_auto import auto_pytest_magic from isort import parse +from isort.finders import FindersManager +from isort.settings import DEFAULT_SECTIONS, default +TEST_CONTENTS = """ +import xyz +import abc + + +def function(): + pass +""" auto_pytest_magic(parse.import_comment) +auto_pytest_magic(parse.import_type) +auto_pytest_magic(parse._strip_syntax) +auto_pytest_magic(parse.skip_line) + + +def test_file_contents(): + ( + in_lines, + out_lines, + import_index, + place_imports, + import_placements, + as_map, + imports, + categorized_comments, + first_comment_index_start, + first_comment_index_end, + change_count, + original_line_count, + ) = parse.file_contents( + TEST_CONTENTS, + line_separator="\n", + add_imports=[], + force_adds=False, + sections=["FIRSTPARTY"], + section_comments=[], + forced_separate=[], + combine_as_imports=False, + verbose=False, + finder=FindersManager(config=default, sections=DEFAULT_SECTIONS), + ) + assert "\n".join(in_lines) == TEST_CONTENTS + assert "import" not in "\n".join(out_lines) + assert import_index == 1 + assert change_count == -2 + assert original_line_count == len(in_lines) From 89a0bc78c3ec8cb0625746021b8a2be0ee6f0041 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 22 Oct 2019 21:58:52 -0700 Subject: [PATCH 0123/1439] Clean up parser further; pushing all parse specific code to parse module --- isort/isort.py | 15 +++------------ isort/parse.py | 37 ++++++++++++++++++++++--------------- isort/utils.py | 9 --------- tests/test_parse.py | 13 ++----------- 4 files changed, 27 insertions(+), 47 deletions(-) diff --git a/isort/isort.py b/isort/isort.py index 4d714680a..ecf86053a 100644 --- a/isort/isort.py +++ b/isort/isort.py @@ -12,7 +12,7 @@ from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple from isort import utils -from isort.format import format_natural, format_simplified +from isort.format import format_simplified from . import output, parse, settings from .finders import FindersManager @@ -41,10 +41,6 @@ def __init__(self, file_contents: str, config: Dict[str, Any], extension: str = *[name for name in section_names] ) # type: Any - self.finder = FindersManager(config=self.config, sections=self.sections) - self.line_separator = self.config["line_ending"] or utils.infer_line_separator( - file_contents - ) ( self.in_lines, self.out_lines, @@ -58,17 +54,12 @@ def __init__(self, file_contents: str, config: Dict[str, Any], extension: str = self._first_comment_index_end, self.length_change, self.original_num_of_lines, + self.line_separator, ) = parse.file_contents( file_contents, - line_separator=self.line_separator, - add_imports=(format_natural(addition) for addition in self.config["add_imports"]), - force_adds=self.config["force_adds"], sections=self.sections, section_comments=self._section_comments, - forced_separate=self.config["forced_separate"], - combine_as_imports=self.config["combine_as_imports"], - verbose=self.config["verbose"], - finder=self.finder, + config=self.config, ) if self.import_index != -1: diff --git a/isort/parse.py b/isort/parse.py index 4b036bdd5..77944f64f 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -4,6 +4,8 @@ from typing import TYPE_CHECKING, Any, Dict, Generator, Iterator, List, Optional, Tuple from warnings import warn +from isort.format import format_natural + from .finders import FindersManager if TYPE_CHECKING: @@ -24,6 +26,15 @@ ) +def infer_line_separator(file_contents: str) -> str: + if "\r\n" in file_contents: + return "\r\n" + elif "\r" in file_contents: + return "\r" + else: + return "\n" + + def import_comment(line: str) -> Tuple[str, str]: """Parses import lines for comments and returns back the import statement and the associated comment. @@ -120,16 +131,7 @@ def skip_line( def file_contents( - contents: str, - line_separator: str, - add_imports: Iterator[str], - force_adds: bool, - sections: Any, - section_comments: List[str], - forced_separate: Iterator[str], - combine_as_imports: bool, - verbose: bool, - finder: FindersManager, + contents: str, sections: Any, section_comments: List[str], config: Dict[str, Any] ) -> Tuple[ List[str], List[str], @@ -143,13 +145,17 @@ def file_contents( int, int, int, + str, ]: """Parses a python file taking out and categorizing imports.""" + line_separator = config["line_ending"] or infer_line_separator(contents) # type: str + add_imports = (format_natural(addition) for addition in config["add_imports"]) in_lines = contents.split(line_separator) out_lines = [] original_line_count = len(in_lines) + finder = FindersManager(config=config, sections=sections) - if original_line_count > 1 or in_lines[:1] not in ([], [""]) or force_adds: + if original_line_count > 1 or in_lines[:1] not in ([], [""]) or config["force_adds"]: in_lines.extend(add_imports) line_count = len(in_lines) @@ -158,7 +164,7 @@ def file_contents( import_placements = {} # type: Dict[str, str] as_map = defaultdict(list) # type: Dict[str, List[str]] imports = OrderedDict() # type: OrderedDict[str, Dict[str, Any]] - for section in chain(sections, forced_separate): + for section in chain(sections, config["forced_separate"]): imports[section] = {"straight": OrderedDict(), "from": OrderedDict()} categorized_comments = { "from": {}, @@ -328,14 +334,14 @@ def file_contents( else: module = just_imports[as_index - 1] as_map[module].append(just_imports[as_index + 1]) - if not combine_as_imports: + if not config["combine_as_imports"]: categorized_comments["straight"][module] = comments comments = [] del just_imports[as_index : as_index + 2] if type_of_import == "from": import_from = just_imports.pop(0) placed_module = finder.find(import_from) - if verbose: + if config["verbose"]: print( "from-type place_module for {} returned {}".format( import_from, placed_module @@ -416,7 +422,7 @@ def file_contents( categorized_comments["above"]["straight"].get(module, []) ) placed_module = finder.find(module) - if verbose: + if config["verbose"]: print( "else-type place_module for {} returned {}".format( module, placed_module @@ -447,4 +453,5 @@ def file_contents( first_comment_index_end, change_count, original_line_count, + line_separator, ) diff --git a/isort/utils.py b/isort/utils.py index e1b4c0ebf..e1b59eb3b 100644 --- a/isort/utils.py +++ b/isort/utils.py @@ -52,12 +52,3 @@ def difference(a: Iterable[Any], b: Container[Any]) -> List[Any]: if item not in b: d.append(item) return d - - -def infer_line_separator(file_contents: str) -> str: - if "\r\n" in file_contents: - return "\r\n" - elif "\r" in file_contents: - return "\r" - else: - return "\n" diff --git a/tests/test_parse.py b/tests/test_parse.py index 64eac8a70..153ceec8e 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -1,7 +1,6 @@ import sys from isort import parse -from isort.finders import FindersManager from isort.settings import DEFAULT_SECTIONS, default TEST_CONTENTS = """ @@ -28,17 +27,9 @@ def test_file_contents(): first_comment_index_end, change_count, original_line_count, + line_separator, ) = parse.file_contents( - TEST_CONTENTS, - line_separator="\n", - add_imports=[], - force_adds=False, - sections=["FIRSTPARTY"], - section_comments=[], - forced_separate=[], - combine_as_imports=False, - verbose=False, - finder=FindersManager(config=default, sections=DEFAULT_SECTIONS), + TEST_CONTENTS, sections=["FIRSTPARTY"], section_comments=[], config=default ) assert "\n".join(in_lines) == TEST_CONTENTS assert "import" not in "\n".join(out_lines) From 53a8b36375f4c0cb9b9cf2cf70f56e9f6c16cafc Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 22 Oct 2019 22:39:19 -0700 Subject: [PATCH 0124/1439] Add test case for infer_line_separator --- isort/parse.py | 4 ++-- tests/test_parse.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/isort/parse.py b/isort/parse.py index 77944f64f..133b11c26 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -26,7 +26,7 @@ ) -def infer_line_separator(file_contents: str) -> str: +def _infer_line_separator(file_contents: str) -> str: if "\r\n" in file_contents: return "\r\n" elif "\r" in file_contents: @@ -148,7 +148,7 @@ def file_contents( str, ]: """Parses a python file taking out and categorizing imports.""" - line_separator = config["line_ending"] or infer_line_separator(contents) # type: str + line_separator = config["line_ending"] or _infer_line_separator(contents) # type: str add_imports = (format_natural(addition) for addition in config["add_imports"]) in_lines = contents.split(line_separator) out_lines = [] diff --git a/tests/test_parse.py b/tests/test_parse.py index 153ceec8e..031ef8626 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -43,5 +43,6 @@ def test_file_contents(): auto_pytest_magic(parse.import_comment) auto_pytest_magic(parse.import_type) - auto_pytest_magic(parse._strip_syntax) auto_pytest_magic(parse.skip_line) + auto_pytest_magic(parse._strip_syntax) + auto_pytest_magic(parse._infer_line_separator) From 3da6b2c4db76b0074b1e9e2558aad3f2078fcfdd Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 23 Oct 2019 21:50:43 -0700 Subject: [PATCH 0125/1439] Move everything out of calling requirements for parsing beyond config definition --- isort/isort.py | 18 +++--------------- isort/parse.py | 13 +++++++++++-- tests/test_parse.py | 6 +++--- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/isort/isort.py b/isort/isort.py index ecf86053a..d39f5c836 100644 --- a/isort/isort.py +++ b/isort/isort.py @@ -31,15 +31,6 @@ def __init__(self, file_contents: str, config: Dict[str, Any], extension: str = self.remove_imports = [ format_simplified(removal) for removal in self.config["remove_imports"] ] - self._section_comments = [ - "# " + value - for key, value in self.config.items() - if key.startswith("import_heading") and value - ] - section_names = self.config["sections"] - self.sections = namedtuple("Sections", section_names)( - *[name for name in section_names] - ) # type: Any ( self.in_lines, @@ -55,12 +46,9 @@ def __init__(self, file_contents: str, config: Dict[str, Any], extension: str = self.length_change, self.original_num_of_lines, self.line_separator, - ) = parse.file_contents( - file_contents, - sections=self.sections, - section_comments=self._section_comments, - config=self.config, - ) + self.sections, + self._section_comments, + ) = parse.file_contents(file_contents, config=self.config) if self.import_index != -1: self._add_formatted_imports() diff --git a/isort/parse.py b/isort/parse.py index 133b11c26..4d9b7ffd0 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -1,5 +1,5 @@ """Defines parsing functions used by isort for parsing import definitions""" -from collections import OrderedDict, defaultdict +from collections import OrderedDict, defaultdict, namedtuple from itertools import chain from typing import TYPE_CHECKING, Any, Dict, Generator, Iterator, List, Optional, Tuple from warnings import warn @@ -131,7 +131,7 @@ def skip_line( def file_contents( - contents: str, sections: Any, section_comments: List[str], config: Dict[str, Any] + contents: str, config: Dict[str, Any] ) -> Tuple[ List[str], List[str], @@ -146,6 +146,8 @@ def file_contents( int, int, str, + Any, + List[str], ]: """Parses a python file taking out and categorizing imports.""" line_separator = config["line_ending"] or _infer_line_separator(contents) # type: str @@ -153,6 +155,11 @@ def file_contents( in_lines = contents.split(line_separator) out_lines = [] original_line_count = len(in_lines) + + sections = namedtuple("Sections", config["sections"])(*config["sections"]) # type: Any + section_comments = [ + "# " + value for key, value in config.items() if key.startswith("import_heading") and value + ] finder = FindersManager(config=config, sections=sections) if original_line_count > 1 or in_lines[:1] not in ([], [""]) or config["force_adds"]: @@ -454,4 +461,6 @@ def file_contents( change_count, original_line_count, line_separator, + sections, + section_comments, ) diff --git a/tests/test_parse.py b/tests/test_parse.py index 031ef8626..fa98bcad2 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -28,9 +28,9 @@ def test_file_contents(): change_count, original_line_count, line_separator, - ) = parse.file_contents( - TEST_CONTENTS, sections=["FIRSTPARTY"], section_comments=[], config=default - ) + sections, + section_comments, + ) = parse.file_contents(TEST_CONTENTS, config=default) assert "\n".join(in_lines) == TEST_CONTENTS assert "import" not in "\n".join(out_lines) assert import_index == 1 From 68e6d9059b002fd241f71c44d1a2147757f2f9e3 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 24 Oct 2019 01:41:32 -0700 Subject: [PATCH 0126/1439] Separate wrap modes into there own module --- isort/isort.py | 31 ++--- isort/output.py | 292 ------------------------------------------- tests/test_output.py | 9 -- 3 files changed, 16 insertions(+), 316 deletions(-) diff --git a/isort/isort.py b/isort/isort.py index d39f5c836..4076defb4 100644 --- a/isort/isort.py +++ b/isort/isort.py @@ -14,7 +14,7 @@ from isort import utils from isort.format import format_simplified -from . import output, parse, settings +from . import output, parse, settings, wrap_modes from .finders import FindersManager from .natural import nsorted from .settings import WrapModes @@ -481,18 +481,19 @@ def _add_from_imports( def _multi_line_reformat( self, import_start: str, from_imports: List[str], comments: Sequence[str] ) -> str: - output_mode = self.config["multi_line_output"].name.lower() - formatter = getattr(output, output_mode, output.grid) + formatter = getattr( + wrap_modes, self.config["multi_line_output"].name.lower(), wrap_modes.grid + ) dynamic_indent = " " * (len(import_start) + 1) indent = self.config["indent"] line_length = self.config["wrap_length"] or self.config["line_length"] import_statement = formatter( - import_start, - copy.copy(from_imports), - dynamic_indent, - indent, - line_length, - comments, + statement=import_start, + imports=copy.copy(from_imports), + white_space=dynamic_indent, + indent=indent, + line_length=line_length, + comments=comments, line_separator=self.line_separator, comment_prefix=self.config["comment_prefix"], include_trailing_comma=self.config["include_trailing_comma"], @@ -510,12 +511,12 @@ def _multi_line_reformat( import_statement = new_import_statement line_length -= 1 new_import_statement = formatter( - import_start, - copy.copy(from_imports), - dynamic_indent, - indent, - line_length, - comments, + statement=import_start, + imports=copy.copy(from_imports), + white_space=dynamic_indent, + indent=indent, + line_length=line_length, + comments=comments, line_separator=self.line_separator, comment_prefix=self.config["comment_prefix"], include_trailing_comma=self.config["include_trailing_comma"], diff --git a/isort/output.py b/isort/output.py index 3db9d0d75..f0b0bb9f1 100644 --- a/isort/output.py +++ b/isort/output.py @@ -3,298 +3,6 @@ from . import parse -def grid( - statement: str, - imports: List[str], - white_space: str, - indent: str, - line_length: int, - comments: List[str], - line_separator: str, - comment_prefix: str, - include_trailing_comma: bool, - remove_comments: bool, -) -> str: - if not imports: - return "" - - statement += "(" + imports.pop(0) - while imports: - next_import = imports.pop(0) - next_statement = with_comments( - comments, - statement + ", " + next_import, - removed=remove_comments, - comment_prefix=comment_prefix, - ) - if len(next_statement.split(line_separator)[-1]) + 1 > line_length: - lines = ["{}{}".format(white_space, next_import.split(" ")[0])] - for part in next_import.split(" ")[1:]: - new_line = "{} {}".format(lines[-1], part) - if len(new_line) + 1 > line_length: - lines.append("{}{}".format(white_space, part)) - else: - lines[-1] = new_line - next_import = line_separator.join(lines) - statement = with_comments( - comments, - "{},".format(statement), - removed=remove_comments, - comment_prefix=comment_prefix, - ) + "{}{}".format(line_separator, next_import) - comments = [] - else: - statement += ", " + next_import - return statement + ("," if include_trailing_comma else "") + ")" - - -def vertical( - statement: str, - imports: List[str], - white_space: str, - indent: str, - line_length: int, - comments: List[str], - line_separator: str, - comment_prefix: str, - include_trailing_comma: bool, - remove_comments: bool, -) -> str: - if not imports: - return "" - - first_import = ( - with_comments( - comments, imports.pop(0) + ",", removed=remove_comments, comment_prefix=comment_prefix - ) - + line_separator - + white_space - ) - return "{}({}{}{})".format( - statement, - first_import, - ("," + line_separator + white_space).join(imports), - "," if include_trailing_comma else "", - ) - - -def hanging_indent( - statement: str, - imports: List[str], - white_space: str, - indent: str, - line_length: int, - comments: List[str], - line_separator: str, - comment_prefix: str, - include_trailing_comma: bool, - remove_comments: bool, -) -> str: - if not imports: - return "" - - statement += imports.pop(0) - while imports: - next_import = imports.pop(0) - next_statement = with_comments( - comments, - statement + ", " + next_import, - removed=remove_comments, - comment_prefix=comment_prefix, - ) - if len(next_statement.split(line_separator)[-1]) + 3 > line_length: - next_statement = with_comments( - comments, - "{}, \\".format(statement), - removed=remove_comments, - comment_prefix=comment_prefix, - ) + "{}{}{}".format(line_separator, indent, next_import) - comments = [] - statement = next_statement - return statement - - -def vertical_hanging_indent( - statement: str, - imports: List[str], - white_space: str, - indent: str, - line_length: int, - comments: List[str], - line_separator: str, - comment_prefix: str, - include_trailing_comma: bool, - remove_comments: bool, -) -> str: - return "{0}({1}{2}{3}{4}{5}{2})".format( - statement, - with_comments(comments, "", removed=remove_comments, comment_prefix=comment_prefix), - line_separator, - indent, - ("," + line_separator + indent).join(imports), - "," if include_trailing_comma else "", - ) - - -def vertical_grid_common( - statement: str, - imports: List[str], - white_space: str, - indent: str, - line_length: int, - comments: List[str], - line_separator: str, - comment_prefix: str, - include_trailing_comma: bool, - remove_comments: bool, - need_trailing_char: bool, -) -> str: - if not imports: - return "" - - statement += ( - with_comments(comments, "(", removed=remove_comments, comment_prefix=comment_prefix) - + line_separator - + indent - + imports.pop(0) - ) - while imports: - next_import = imports.pop(0) - next_statement = "{}, {}".format(statement, next_import) - current_line_length = len(next_statement.split(line_separator)[-1]) - if imports or need_trailing_char: - # If we have more imports we need to account for a comma after this import - # We might also need to account for a closing ) we're going to add. - current_line_length += 1 - if current_line_length > line_length: - next_statement = "{},{}{}{}".format(statement, line_separator, indent, next_import) - statement = next_statement - if include_trailing_comma: - statement += "," - return statement - - -def vertical_grid( - statement: str, - imports: List[str], - white_space: str, - indent: str, - line_length: int, - comments: List[str], - line_separator: str, - comment_prefix: str, - include_trailing_comma: bool, - remove_comments: bool, -) -> str: - return ( - vertical_grid_common( - statement, - imports, - white_space, - indent, - line_length, - comments, - line_separator=line_separator, - comment_prefix=comment_prefix, - include_trailing_comma=include_trailing_comma, - remove_comments=remove_comments, - need_trailing_char=True, - ) - + ")" - ) - - -def vertical_grid_grouped( - statement: str, - imports: List[str], - white_space: str, - indent: str, - line_length: int, - comments: List[str], - line_separator: str, - comment_prefix: str, - include_trailing_comma: bool, - remove_comments: bool, -) -> str: - return ( - vertical_grid_common( - statement, - imports, - white_space, - indent, - line_length, - comments, - line_separator=line_separator, - comment_prefix=comment_prefix, - include_trailing_comma=include_trailing_comma, - remove_comments=remove_comments, - need_trailing_char=True, - ) - + line_separator - + ")" - ) - - -def vertical_grid_grouped_no_comma( - statement: str, - imports: List[str], - white_space: str, - indent: str, - line_length: int, - comments: List[str], - line_separator: str, - comment_prefix: str, - include_trailing_comma: bool, - remove_comments: bool, -) -> str: - return ( - vertical_grid_common( - statement, - imports, - white_space, - indent, - line_length, - comments, - line_separator=line_separator, - comment_prefix=comment_prefix, - include_trailing_comma=include_trailing_comma, - remove_comments=remove_comments, - need_trailing_char=False, - ) - + line_separator - + ")" - ) - - -def noqa( - statement: str, - imports: List[str], - white_space: str, - indent: str, - line_length: int, - comments: List[str], - line_separator: str, - comment_prefix: str, - include_trailing_comma: bool, - remove_comments: bool, -) -> str: - retval = "{}{}".format(statement, ", ".join(imports)) - comment_str = " ".join(comments) - if comments: - if len(retval) + len(comment_prefix) + 1 + len(comment_str) <= line_length: - return "{}{} {}".format(retval, comment_prefix, comment_str) - else: - if len(retval) <= line_length: - return retval - if comments: - if "NOQA" in comments: - return "{}{} {}".format(retval, comment_prefix, comment_str) - else: - return "{}{} NOQA {}".format(retval, comment_prefix, comment_str) - else: - return "{}{} NOQA".format(retval, comment_prefix) - - def with_comments( comments: Optional[List[str]], original_string: str = "", diff --git a/tests/test_output.py b/tests/test_output.py index c2ab34027..0868a4a32 100644 --- a/tests/test_output.py +++ b/tests/test_output.py @@ -5,13 +5,4 @@ if sys.version_info[1] > 5: from hypothesis_auto import auto_pytest_magic - auto_pytest_magic(output.grid, auto_allow_exceptions_=(ValueError,)) - auto_pytest_magic(output.vertical, auto_allow_exceptions_=(ValueError,)) - auto_pytest_magic(output.hanging_indent, auto_allow_exceptions_=(ValueError,)) - auto_pytest_magic(output.vertical_hanging_indent, auto_allow_exceptions_=(ValueError,)) - auto_pytest_magic(output.vertical_grid_common, auto_allow_exceptions_=(ValueError,)) - auto_pytest_magic(output.vertical_grid, auto_allow_exceptions_=(ValueError,)) - auto_pytest_magic(output.vertical_grid_grouped, auto_allow_exceptions_=(ValueError,)) - auto_pytest_magic(output.vertical_grid_grouped_no_comma, auto_allow_exceptions_=(ValueError,)) - auto_pytest_magic(output.noqa, auto_allow_exceptions_=(ValueError,)) auto_pytest_magic(output.with_comments, auto_allow_exceptions_=(ValueError,)) From fd8ab8a24dc85de5c55e3bd3215cd3d7ce6d6c7b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 24 Oct 2019 01:41:46 -0700 Subject: [PATCH 0127/1439] Separate wrap modes into there own module --- isort/wrap_modes.py | 270 +++++++++++++++++++++++++++++++++++++++ tests/test_wrap_modes.py | 17 +++ 2 files changed, 287 insertions(+) create mode 100644 isort/wrap_modes.py create mode 100644 tests/test_wrap_modes.py diff --git a/isort/wrap_modes.py b/isort/wrap_modes.py new file mode 100644 index 000000000..d467395ea --- /dev/null +++ b/isort/wrap_modes.py @@ -0,0 +1,270 @@ +"""Defines all wrap modes that can be used when outputting formatted imports""" +import enum +from inspect import signature +from typing import Any, Callable, List + +from .output import with_comments + +_wrap_modes = [] + + +def from_string(value: str) -> "WrapModes": + return getattr(WrapModes, str(value), None) or WrapModes(int(value)) + + +def _wrap_mode_interface( + statement: str, + imports: List[str], + white_space: str, + indent: str, + line_length: int, + comments: List[str], + line_separator: str, + comment_prefix: str, + include_trailing_comma: bool, + remove_comments: bool, +) -> str: + """Defines the common interface used by all wrap mode functions""" + return "" + + +def _wrap_mode(function): + """Registers an individual wrap mode. Function name and order are significant and used for + creating enum. + """ + _wrap_modes.append((function.__name__.upper(), function)) + function.__signature__ = signature(_wrap_mode_interface) + function.__annotations__ = _wrap_mode_interface.__annotations__ + return function + + +@_wrap_mode +def grid(**interface): + if not interface["imports"]: + return "" + + interface["statement"] += "(" + interface["imports"].pop(0) + while interface["imports"]: + next_import = interface["imports"].pop(0) + next_statement = with_comments( + interface["comments"], + interface["statement"] + ", " + next_import, + removed=interface["remove_comments"], + comment_prefix=interface["comment_prefix"], + ) + if ( + len(next_statement.split(interface["line_separator"])[-1]) + 1 + > interface["line_length"] + ): + lines = ["{}{}".format(interface["white_space"], next_import.split(" ")[0])] + for part in next_import.split(" ")[1:]: + new_line = "{} {}".format(lines[-1], part) + if len(new_line) + 1 > interface["line_length"]: + lines.append("{}{}".format(interface["white_space"], part)) + else: + lines[-1] = new_line + next_import = interface["line_separator"].join(lines) + interface["statement"] = with_comments( + interface["comments"], + "{},".format(interface["statement"]), + removed=interface["remove_comments"], + comment_prefix=interface["comment_prefix"], + ) + "{}{}".format(interface["line_separator"], next_import) + interface["comments"] = [] + else: + interface["statement"] += ", " + next_import + return interface["statement"] + ("," if interface["include_trailing_comma"] else "") + ")" + + +@_wrap_mode +def vertical(**interface): + if not interface["imports"]: + return "" + + first_import = ( + with_comments( + interface["comments"], + interface["imports"].pop(0) + ",", + removed=interface["remove_comments"], + comment_prefix=interface["comment_prefix"], + ) + + interface["line_separator"] + + interface["white_space"] + ) + return "{}({}{}{})".format( + interface["statement"], + first_import, + ("," + interface["line_separator"] + interface["white_space"]).join(interface["imports"]), + "," if interface["include_trailing_comma"] else "", + ) + + +@_wrap_mode +def hanging_indent(**interface): + if not interface["imports"]: + return "" + + interface["statement"] += interface["imports"].pop(0) + while interface["imports"]: + next_import = interface["imports"].pop(0) + next_statement = with_comments( + interface["comments"], + interface["statement"] + ", " + next_import, + removed=interface["remove_comments"], + comment_prefix=interface["comment_prefix"], + ) + if ( + len(next_statement.split(interface["line_separator"])[-1]) + 3 + > interface["line_length"] + ): + next_statement = with_comments( + interface["comments"], + "{}, \\".format(interface["statement"]), + removed=interface["remove_comments"], + comment_prefix=interface["comment_prefix"], + ) + "{}{}{}".format(interface["line_separator"], interface["indent"], next_import) + interface["comments"] = [] + interface["statement"] = next_statement + return interface["statement"] + + +@_wrap_mode +def vertical_hanging_indent(**interface): + return "{0}({1}{2}{3}{4}{5}{2})".format( + interface["statement"], + with_comments( + interface["comments"], + "", + removed=interface["remove_comments"], + comment_prefix=interface["comment_prefix"], + ), + interface["line_separator"], + interface["indent"], + ("," + interface["line_separator"] + interface["indent"]).join(interface["imports"]), + "," if interface["include_trailing_comma"] else "", + ) + + +def vertical_grid_common(need_trailing_char: bool, **interface): + if not interface["imports"]: + return "" + + interface["statement"] += ( + with_comments( + interface["comments"], + "(", + removed=interface["remove_comments"], + comment_prefix=interface["comment_prefix"], + ) + + interface["line_separator"] + + interface["indent"] + + interface["imports"].pop(0) + ) + while interface["imports"]: + next_import = interface["imports"].pop(0) + next_statement = "{}, {}".format(interface["statement"], next_import) + current_line_length = len(next_statement.split(interface["line_separator"])[-1]) + if interface["imports"] or need_trailing_char: + # If we have more interface["imports"] we need to account for a comma after this import + # We might also need to account for a closing ) we're going to add. + current_line_length += 1 + if current_line_length > interface["line_length"]: + next_statement = "{},{}{}{}".format( + interface["statement"], + interface["line_separator"], + interface["indent"], + next_import, + ) + interface["statement"] = next_statement + if interface["include_trailing_comma"]: + interface["statement"] += "," + return interface["statement"] + + +@_wrap_mode +def vertical_grid(**interface) -> str: + return ( + vertical_grid_common( + statement=interface["statement"], + imports=interface["imports"], + white_space=interface["white_space"], + indent=interface["indent"], + line_length=interface["line_length"], + comments=interface["comments"], + line_separator=interface["line_separator"], + comment_prefix=interface["comment_prefix"], + include_trailing_comma=interface["include_trailing_comma"], + remove_comments=interface["remove_comments"], + need_trailing_char=True, + ) + + ")" + ) + + +@_wrap_mode +def vertical_grid_grouped(**interface): + return ( + vertical_grid_common( + statement=interface["statement"], + imports=interface["imports"], + white_space=interface["white_space"], + indent=interface["indent"], + line_length=interface["line_length"], + comments=interface["comments"], + line_separator=interface["line_separator"], + comment_prefix=interface["comment_prefix"], + include_trailing_comma=interface["include_trailing_comma"], + remove_comments=interface["remove_comments"], + need_trailing_char=True, + ) + + interface["line_separator"] + + ")" + ) + + +@_wrap_mode +def vertical_grid_grouped_no_comma(**interface): + return ( + vertical_grid_common( + statement=interface["statement"], + imports=interface["imports"], + white_space=interface["white_space"], + indent=interface["indent"], + line_length=interface["line_length"], + comments=interface["comments"], + line_separator=interface["line_separator"], + comment_prefix=interface["comment_prefix"], + include_trailing_comma=interface["include_trailing_comma"], + remove_comments=interface["remove_comments"], + need_trailing_char=False, + ) + + interface["line_separator"] + + ")" + ) + + +@_wrap_mode +def noqa(**interface): + retval = "{}{}".format(interface["statement"], ", ".join(interface["imports"])) + comment_str = " ".join(interface["comments"]) + if interface["comments"]: + if ( + len(retval) + len(interface["comment_prefix"]) + 1 + len(comment_str) + <= interface["line_length"] + ): + return "{}{} {}".format(retval, interface["comment_prefix"], comment_str) + else: + if len(retval) <= interface["line_length"]: + return retval + if interface["comments"]: + if "NOQA" in interface["comments"]: + return "{}{} {}".format(retval, interface["comment_prefix"], comment_str) + else: + return "{}{} NOQA {}".format(retval, interface["comment_prefix"], comment_str) + else: + return "{}{} NOQA".format(retval, interface["comment_prefix"]) + + +WrapModes = enum.Enum( # type: ignore + "WrapModes", {wrap_mode[0]: index for index, wrap_mode in enumerate(_wrap_modes)} +) diff --git a/tests/test_wrap_modes.py b/tests/test_wrap_modes.py new file mode 100644 index 000000000..6b2e518a0 --- /dev/null +++ b/tests/test_wrap_modes.py @@ -0,0 +1,17 @@ +import sys + +from isort import wrap_modes + +if sys.version_info[1] > 5: + from hypothesis_auto import auto_pytest_magic + + auto_pytest_magic(wrap_modes.grid, auto_allow_exceptions_=(ValueError,)) + auto_pytest_magic(wrap_modes.vertical, auto_allow_exceptions_=(ValueError,)) + auto_pytest_magic(wrap_modes.hanging_indent, auto_allow_exceptions_=(ValueError,)) + auto_pytest_magic(wrap_modes.vertical_hanging_indent, auto_allow_exceptions_=(ValueError,)) + auto_pytest_magic(wrap_modes.vertical_grid, auto_allow_exceptions_=(ValueError,)) + auto_pytest_magic(wrap_modes.vertical_grid_grouped, auto_allow_exceptions_=(ValueError,)) + auto_pytest_magic( + wrap_modes.vertical_grid_grouped_no_comma, auto_allow_exceptions_=(ValueError,) + ) + auto_pytest_magic(wrap_modes.noqa, auto_allow_exceptions_=(ValueError,)) From fb77e12ea802139d8a106497cccd19a259cf15e5 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 24 Oct 2019 18:52:16 -0700 Subject: [PATCH 0128/1439] Switch to Python3.6+ only --- isort/finders.py | 15 +++++++-------- isort/isort.py | 10 ++++------ isort/main.py | 6 +++--- isort/parse.py | 18 +++++++++--------- isort/settings.py | 4 ++-- isort/utils.py | 14 +++++--------- pyproject.toml | 12 ++++++------ setup.cfg | 2 +- tests/test_output.py | 7 +++---- tests/test_wrap_modes.py | 23 ++++++++++------------- 10 files changed, 50 insertions(+), 61 deletions(-) diff --git a/isort/finders.py b/isort/finders.py index 5c23389c0..b8e6c8f6f 100644 --- a/isort/finders.py +++ b/isort/finders.py @@ -83,7 +83,7 @@ class KnownPatternFinder(BaseFinder): def __init__(self, config: Mapping[str, Any], sections: Any) -> None: super().__init__(config, sections) - self.known_patterns = [] # type: List[Tuple[Pattern[str], str]] + self.known_patterns: List[Tuple[Pattern[str], str]] = [] for placement in reversed(self.sections): known_placement = KNOWN_SECTION_MAPPING.get(placement, placement) config_key = "known_{}".format(known_placement.lower()) @@ -230,8 +230,7 @@ def _load_mapping() -> Optional[Dict[str, str]]: path = os.path.dirname(inspect.getfile(pipreqs)) path = os.path.join(path, "mapping") with open(path) as f: - # pypi_name: import_name - mappings = {} # type: Dict[str, str] + mappings: Dict[str, str] = {} # pypi_name: import_name for line in f: import_name, _, pypi_name = line.strip().partition(":") mappings[pypi_name] = import_name @@ -368,7 +367,7 @@ def find(self, module_name: str) -> Optional[str]: class FindersManager: - _default_finders_classes = ( + _default_finders_classes: Sequence[Type[BaseFinder]] = ( ForcedSeparateFinder, LocalFinder, KnownPatternFinder, @@ -376,7 +375,7 @@ class FindersManager: PipfileFinder, RequirementsFinder, DefaultFinder, - ) # type: Sequence[Type[BaseFinder]] + ) def __init__( self, @@ -384,11 +383,11 @@ def __init__( sections: Any, finder_classes: Optional[Iterable[Type[BaseFinder]]] = None, ) -> None: - self.verbose = config.get("verbose", False) # type: bool + self.verbose: bool = config.get("verbose", False) if finder_classes is None: finder_classes = self._default_finders_classes - finders = [] # type: List[BaseFinder] + finders: List[BaseFinder] = [] for finder_cls in finder_classes: try: finders.append(finder_cls(config, sections)) @@ -401,7 +400,7 @@ def __init__( "instantiation and cannot be used" ).format(finder_cls.__name__, str(exception)) ) - self.finders = tuple(finders) # type: Tuple[BaseFinder, ...] + self.finders: Tuple[BaseFinder, ...] = tuple(finders) def find(self, module_name: str) -> Optional[str]: for finder in self.finders: diff --git a/isort/isort.py b/isort/isort.py index 4076defb4..f7a81553b 100644 --- a/isort/isort.py +++ b/isort/isort.py @@ -534,9 +534,7 @@ def _add_formatted_imports(self) -> None: """ sort_ignore_case = self.config["force_alphabetical_sort_within_sections"] - sections = itertools.chain( - self.sections, self.config["forced_separate"] - ) # type: Iterable[str] + sections: Iterable[str] = itertools.chain(self.sections, self.config["forced_separate"]) if self.config["no_sections"]: self.imports["no_sections"] = {"straight": [], "from": {}} @@ -547,7 +545,7 @@ def _add_formatted_imports(self) -> None: self.imports["no_sections"]["from"].update(self.imports[section].get("from", {})) sections = ("no_sections",) - output = [] # type: List[str] + output: List[str] = [] pending_lines_before = False for section in sections: straight_modules = self.imports[section]["straight"] @@ -564,7 +562,7 @@ def _add_formatted_imports(self) -> None: if self.config["force_sort_within_sections"]: copied_comments = copy.deepcopy(self.comments) - section_output = [] # type: List[str] + section_output: List[str] = [] if self.config["from_first"]: self._add_from_imports(from_modules, section, section_output, sort_ignore_case) if self.config["lines_between_types"] and from_modules and straight_modules: @@ -655,7 +653,7 @@ def by_module(line: str) -> str: if len(self.out_lines) > imports_tail: next_construct = "" - self._in_quote = False # type: Any + self._in_quote: str = "" tail = self.out_lines[imports_tail:] for index, line in enumerate(tail): diff --git a/isort/main.py b/isort/main.py index 6904ccb4e..ab8749aa4 100644 --- a/isort/main.py +++ b/isort/main.py @@ -83,7 +83,7 @@ class ISortCommand(setuptools.Command): """ description = "Run isort on modules registered in setuptools" - user_options = [] # type: List[Any] + user_options: List[Any] = [] def initialize_options(self) -> None: default_settings = default.copy() @@ -92,7 +92,7 @@ def initialize_options(self) -> None: def finalize_options(self) -> None: "Get options from config files." - self.arguments = {} # type: Dict[str, Any] + self.arguments: Dict[str, Any] = {} computed_settings = from_path(os.getcwd()) for key, value in computed_settings.items(): self.arguments[key] = value @@ -589,7 +589,7 @@ def main(argv: Optional[Sequence[str]] = None) -> None: ).copy() config.update(arguments) wrong_sorted_files = False - skipped = [] # type: List[str] + skipped: List[str] = [] if config.get("filter_files"): filtered_files = [] diff --git a/isort/parse.py b/isort/parse.py index 4d9b7ffd0..8ba977ad2 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -150,13 +150,13 @@ def file_contents( List[str], ]: """Parses a python file taking out and categorizing imports.""" - line_separator = config["line_ending"] or _infer_line_separator(contents) # type: str + line_separator: str = config["line_ending"] or _infer_line_separator(contents) add_imports = (format_natural(addition) for addition in config["add_imports"]) in_lines = contents.split(line_separator) out_lines = [] original_line_count = len(in_lines) - sections = namedtuple("Sections", config["sections"])(*config["sections"]) # type: Any + sections: Any = namedtuple("Sections", config["sections"])(*config["sections"]) section_comments = [ "# " + value for key, value in config.items() if key.startswith("import_heading") and value ] @@ -167,18 +167,18 @@ def file_contents( line_count = len(in_lines) - place_imports = {} # type: Dict[str, List[str]] - import_placements = {} # type: Dict[str, str] - as_map = defaultdict(list) # type: Dict[str, List[str]] - imports = OrderedDict() # type: OrderedDict[str, Dict[str, Any]] + place_imports: Dict[str, List[str]] = {} + import_placements: Dict[str, str] = {} + as_map: Dict[str, List[str]] = defaultdict(list) + imports: OrderedDict[str, Dict[str, Any]] = OrderedDict() for section in chain(sections, config["forced_separate"]): imports[section] = {"straight": OrderedDict(), "from": OrderedDict()} - categorized_comments = { + categorized_comments: CommentsDict = { "from": {}, "straight": {}, "nested": {}, "above": {"straight": {}, "from": {}}, - } # type: CommentsDict + } index = 0 import_index = -1 @@ -225,7 +225,7 @@ def file_contents( if part and not part.startswith("from ") and not part.startswith("import "): skipping_line = True - type_of_import = import_type(line) or "" # type: str + type_of_import: str = import_type(line) or "" if not type_of_import or skipping_line: out_lines.append(raw_line) continue diff --git a/isort/settings.py b/isort/settings.py index 5b8ea3250..da83daf5e 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -252,7 +252,7 @@ def _update_settings_with_config( def _get_str_to_type_converter(setting_name: str) -> Callable[[str], Any]: - type_converter = type(default.get(setting_name, "")) # type: Callable[[str], Any] + type_converter: Callable[[str], Any] = type(default.get(setting_name, "")) if type_converter == WrapModes: type_converter = WrapModes.from_string return type_converter @@ -339,7 +339,7 @@ def _abspaths(cwd: str, values: Iterable[str]) -> List[str]: @lru_cache() def _get_config_data(file_path: str, sections: Iterable[str]) -> Dict[str, Any]: - settings = {} # type: Dict[str, Any] + settings: Dict[str, Any] = {} with open(file_path) as config_file: if file_path.endswith(".toml"): diff --git a/isort/utils.py b/isort/utils.py index e1b59eb3b..6cf7a2de0 100644 --- a/isort/utils.py +++ b/isort/utils.py @@ -5,8 +5,7 @@ def exists_case_sensitive(path: str) -> bool: - """ - Returns if the given path exists and also matches the case on Windows. + """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, @@ -21,8 +20,7 @@ def exists_case_sensitive(path: str) -> bool: @contextmanager def chdir(path: str) -> Iterator[None]: - """Context manager for changing dir and restoring previous workdir after exit. - """ + """Context manager for changing dir and restoring previous workdir after exit.""" curdir = os.getcwd() os.chdir(path) try: @@ -32,9 +30,8 @@ def chdir(path: str) -> Iterator[None]: def union(a: Iterable[Any], b: Iterable[Any]) -> List[Any]: - """ Return a list of items that are in `a` or `b` - """ - u = [] # type: List[Any] + """Return a list of items that are in `a` or `b`""" + u: List[Any] = [] for item in a: if item not in u: u.append(item) @@ -45,8 +42,7 @@ def union(a: Iterable[Any], b: Iterable[Any]) -> List[Any]: def difference(a: Iterable[Any], b: Container[Any]) -> List[Any]: - """ Return a list of items from `a` that are not in `b`. - """ + """Return a list of items from `a` that are not in `b`.""" d = [] for item in a: if item not in b: diff --git a/pyproject.toml b/pyproject.toml index 0253823ec..f2c8e169e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ classifiers = [ ] [tool.poetry.dependencies] -python = "^3.5" +python = "^3.6" appdirs = {version = "^1.4.0", optional = true} pipreqs = {version = "*", optional = true} requirementslib = {version = "*", optional = true} @@ -47,17 +47,17 @@ vulture = "^1.0" bandit = "^1.6" safety = "^1.8" flake8-bugbear = "^19.8" -black = {version = "^18.3-alpha.0", allows-prereleases = true, python = "^3.6"} +black = {version = "^18.3-alpha.0", allows-prereleases = true} mypy = "^0.730.0" ipython = "^7.7" pytest = "^5.0" pytest-cov = "^2.7" pytest-mock = "^1.10" pep8-naming = "^0.8.2" -hypothesis-auto = { version = "^1.0.0", python = "^3.6" } -examples = { version = "^1.0.0", python = "^3.6" } -cruft = { version = "^1.1", python = "^3.6" } -portray = { version = "^1.3.0", python = "^3.6" } +hypothesis-auto = { version = "^1.0.0" } +examples = { version = "^1.0.0" } +cruft = { version = "^1.1" } +portray = { version = "^1.3.0" } appdirs = "^1.4" pipfile = "^0.0.2" requirementslib = "^1.5" diff --git a/setup.cfg b/setup.cfg index 3c5786dc0..5ac39b119 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [mypy] -python_version = 3.5 +python_version = 3.6 follow_imports = silent ignore_missing_imports = True disallow_any_generics = True diff --git a/tests/test_output.py b/tests/test_output.py index 0868a4a32..e42093b5b 100644 --- a/tests/test_output.py +++ b/tests/test_output.py @@ -1,8 +1,7 @@ import sys -from isort import output +from hypothesis_auto import auto_pytest_magic -if sys.version_info[1] > 5: - from hypothesis_auto import auto_pytest_magic +from isort import output - auto_pytest_magic(output.with_comments, auto_allow_exceptions_=(ValueError,)) +auto_pytest_magic(output.with_comments, auto_allow_exceptions_=(ValueError,)) diff --git a/tests/test_wrap_modes.py b/tests/test_wrap_modes.py index 6b2e518a0..dee26d4f7 100644 --- a/tests/test_wrap_modes.py +++ b/tests/test_wrap_modes.py @@ -1,17 +1,14 @@ import sys -from isort import wrap_modes +from hypothesis_auto import auto_pytest_magic -if sys.version_info[1] > 5: - from hypothesis_auto import auto_pytest_magic +from isort import wrap_modes - auto_pytest_magic(wrap_modes.grid, auto_allow_exceptions_=(ValueError,)) - auto_pytest_magic(wrap_modes.vertical, auto_allow_exceptions_=(ValueError,)) - auto_pytest_magic(wrap_modes.hanging_indent, auto_allow_exceptions_=(ValueError,)) - auto_pytest_magic(wrap_modes.vertical_hanging_indent, auto_allow_exceptions_=(ValueError,)) - auto_pytest_magic(wrap_modes.vertical_grid, auto_allow_exceptions_=(ValueError,)) - auto_pytest_magic(wrap_modes.vertical_grid_grouped, auto_allow_exceptions_=(ValueError,)) - auto_pytest_magic( - wrap_modes.vertical_grid_grouped_no_comma, auto_allow_exceptions_=(ValueError,) - ) - auto_pytest_magic(wrap_modes.noqa, auto_allow_exceptions_=(ValueError,)) +auto_pytest_magic(wrap_modes.grid, auto_allow_exceptions_=(ValueError,)) +auto_pytest_magic(wrap_modes.vertical, auto_allow_exceptions_=(ValueError,)) +auto_pytest_magic(wrap_modes.hanging_indent, auto_allow_exceptions_=(ValueError,)) +auto_pytest_magic(wrap_modes.vertical_hanging_indent, auto_allow_exceptions_=(ValueError,)) +auto_pytest_magic(wrap_modes.vertical_grid, auto_allow_exceptions_=(ValueError,)) +auto_pytest_magic(wrap_modes.vertical_grid_grouped, auto_allow_exceptions_=(ValueError,)) +auto_pytest_magic(wrap_modes.vertical_grid_grouped_no_comma, auto_allow_exceptions_=(ValueError,)) +auto_pytest_magic(wrap_modes.noqa, auto_allow_exceptions_=(ValueError,)) From e5baa165073f61de787149c0f34eeea44c6729a7 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 24 Oct 2019 18:52:58 -0700 Subject: [PATCH 0129/1439] Bump required version to 3.6 in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index eeac22dbe..6d39941d9 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ 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 requires Python 3.5+ to run but +quickly sort all your imports. It requires Python 3.6+ to run but supports formatting Python 2 code too. ------------------------------------------------------------------------ From 5d21f87c442dea4148ae687cb415c7b9a229afa7 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 24 Oct 2019 18:58:00 -0700 Subject: [PATCH 0130/1439] Don't ignore missing imports --- setup.cfg | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 5ac39b119..32875b6ac 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,6 @@ [mypy] python_version = 3.6 follow_imports = silent -ignore_missing_imports = True disallow_any_generics = True strict_optional = True check_untyped_defs = True From cb544adfd4e1364f16a988562ac49131d52243f0 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 24 Oct 2019 18:58:16 -0700 Subject: [PATCH 0131/1439] Remove Python3.5 specific checks in pipeline --- .travis.yml | 2 -- scripts/lint.sh | 13 +++---------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index d22d1f68e..7e5309fbe 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,8 +6,6 @@ install: - poetry install matrix: include: - - os: linux - python: 3.5 - os: linux python: 3.6 - os: linux diff --git a/scripts/lint.sh b/scripts/lint.sh index 9bc988783..d8a227111 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -1,17 +1,10 @@ #!/bin/bash set -euxo pipefail -pyversion=$(python3 -V 2>&1 | sed 's/.* \([0-9]\).\([0-9]\).*/\1\2/') - -if [[ "$pyversion" -lt "36" ]] -then - echo "WARNING: Some linters have been skipped. Run against 3.6+ for full set of linters to run against the project!" -else - poetry run cruft check - poetry run mypy --ignore-missing-imports isort/ - poetry run black --check -l 100 isort/ tests/ -fi +poetry run cruft check +poetry run mypy --ignore-missing-imports isort/ +poetry run black --check -l 100 isort/ tests/ poetry run isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --use-parentheses --line-width=100 --recursive --check --diff --recursive isort/ tests/ poetry run flake8 isort/ tests/ --max-line 100 --ignore F403,F401,W503,E203 poetry run safety check From ebe4724d9cb3f13729b6ac31220a3e0fe6984c34 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 24 Oct 2019 19:15:17 -0700 Subject: [PATCH 0132/1439] Avoid single character names where not painfully obvious --- isort/utils.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/isort/utils.py b/isort/utils.py index 6cf7a2de0..d198563bd 100644 --- a/isort/utils.py +++ b/isort/utils.py @@ -31,20 +31,20 @@ def chdir(path: str) -> Iterator[None]: def union(a: Iterable[Any], b: Iterable[Any]) -> List[Any]: """Return a list of items that are in `a` or `b`""" - u: List[Any] = [] + unioned: List[Any] = [] for item in a: - if item not in u: - u.append(item) + if item not in unioned: + unioned.append(item) for item in b: - if item not in u: - u.append(item) - return u + if item not in unioned: + unioned.append(item) + return unioned def difference(a: Iterable[Any], b: Container[Any]) -> List[Any]: """Return a list of items from `a` that are not in `b`.""" - d = [] + differences = [] for item in a: if item not in b: - d.append(item) - return d + differences.append(item) + return differences From 583e68b5408482c11f515b32e6fde341f3a30e92 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 24 Oct 2019 19:23:53 -0700 Subject: [PATCH 0133/1439] Update file contents to produce a ParsedContent typed NamedTuple instead of a straight tuple to increase readability --- isort/parse.py | 72 +++++++++++++++++++++++++------------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/isort/parse.py b/isort/parse.py index 8ba977ad2..90614efd4 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -1,7 +1,7 @@ """Defines parsing functions used by isort for parsing import definitions""" from collections import OrderedDict, defaultdict, namedtuple from itertools import chain -from typing import TYPE_CHECKING, Any, Dict, Generator, Iterator, List, Optional, Tuple +from typing import TYPE_CHECKING, Any, Dict, Generator, Iterator, List, NamedTuple, Optional, Tuple from warnings import warn from isort.format import format_natural @@ -130,25 +130,25 @@ def skip_line( ) -def file_contents( - contents: str, config: Dict[str, Any] -) -> Tuple[ - List[str], - List[str], - int, - Dict[str, List[str]], - Dict[str, str], - Dict[str, List[str]], - Dict[str, Dict[str, Any]], - "CommentsDict", - int, - int, - int, - int, - str, - Any, - List[str], -]: +class ParsedContent(NamedTuple): + in_lines: List[str] + lines_without_imports: List[str] + import_index: int + place_imports: Dict[str, List[str]] + import_placements: Dict[str, str] + as_map: Dict[str, List[str]] + imports: Dict[str, Dict[str, Any]] + categorized_comments: "CommentsDict" + first_comment_index_start: int + first_comment_index_end: int + change_count: int + original_line_count: int + line_separator: str + sections: Any + section_comments: List[str] + + +def file_contents(contents: str, config: Dict[str, Any]) -> ParsedContent: """Parses a python file taking out and categorizing imports.""" line_separator: str = config["line_ending"] or _infer_line_separator(contents) add_imports = (format_natural(addition) for addition in config["add_imports"]) @@ -447,20 +447,20 @@ def file_contents( change_count = len(out_lines) - original_line_count - return ( - in_lines, - out_lines, - import_index, - place_imports, - import_placements, - as_map, - imports, - categorized_comments, - first_comment_index_start, - first_comment_index_end, - change_count, - original_line_count, - line_separator, - sections, - section_comments, + return ParsedContent( + in_lines=in_lines, + lines_without_imports=out_lines, + import_index=import_index, + place_imports=place_imports, + import_placements=import_placements, + as_map=as_map, + imports=imports, + categorized_comments=categorized_comments, + first_comment_index_start=first_comment_index_start, + first_comment_index_end=first_comment_index_end, + change_count=change_count, + original_line_count=original_line_count, + line_separator=line_separator, + sections=sections, + section_comments=section_comments, ) From 5b9c5775f238c9962e69605171f0152bf16bb0e2 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 25 Oct 2019 20:01:30 -0700 Subject: [PATCH 0134/1439] Attempt to move wrapping code to new wrap module --- isort/isort.py | 171 +++++++++++--------------------------------- isort/new_wrap | 43 +++++++++++ isort/new_wrap_line | 61 ++++++++++++++++ isort/old_wrap | 1 + isort/old_wrap_line | 61 ++++++++++++++++ isort/wrap_modes.py | 133 ++++++++++++++++++++++++++++++++-- 6 files changed, 338 insertions(+), 132 deletions(-) create mode 100644 isort/new_wrap create mode 100644 isort/new_wrap_line create mode 100644 isort/old_wrap create mode 100644 isort/old_wrap_line diff --git a/isort/isort.py b/isort/isort.py index f7a81553b..73b7fc930 100644 --- a/isort/isort.py +++ b/isort/isort.py @@ -124,72 +124,6 @@ def _module_key( length_sort and (str(len(module_name)) + ":" + module_name) or module_name, ) - def _wrap(self, line: str) -> str: - """ - Returns an import wrapped to the specified line-length, if possible. - """ - wrap_mode = self.config["multi_line_output"] - if len(line) > self.config["line_length"] and wrap_mode != WrapModes.NOQA: - line_without_comment = line - comment = None - if "#" in line: - line_without_comment, comment = line.split("#", 1) - for splitter in ("import ", ".", "as "): - exp = r"\b" + re.escape(splitter) + r"\b" - if re.search( - exp, line_without_comment - ) and not line_without_comment.strip().startswith(splitter): - line_parts = re.split(exp, line_without_comment) - if comment: - line_parts[-1] = "{}{} #{}".format( - line_parts[-1].strip(), - "," if self.config["include_trailing_comma"] else "", - comment, - ) - next_line = [] - while (len(line) + 2) > ( - self.config["wrap_length"] or self.config["line_length"] - ) and line_parts: - next_line.append(line_parts.pop()) - line = splitter.join(line_parts) - if not line: - line = next_line.pop() - - cont_line = self._wrap( - self.config["indent"] + splitter.join(next_line).lstrip() - ) - if self.config["use_parentheses"]: - if splitter == "as ": - output = "{}{}{}".format(line, splitter, cont_line.lstrip()) - else: - output = "{}{}({}{}{}{})".format( - line, - splitter, - self.line_separator, - cont_line, - "," - if self.config["include_trailing_comma"] and not comment - else "", - self.line_separator - if wrap_mode - in { - WrapModes.VERTICAL_HANGING_INDENT, - WrapModes.VERTICAL_GRID_GROUPED, - } - else "", - ) - lines = output.split(self.line_separator) - if self.config["comment_prefix"] in lines[-1] and lines[-1].endswith(")"): - line, comment = lines[-1].split(self.config["comment_prefix"], 1) - lines[-1] = line + ")" + self.config["comment_prefix"] + comment[:-1] - return self.line_separator.join(lines) - return "{}{}\\{}{}".format(line, splitter, self.line_separator, cont_line) - elif len(line) > self.config["line_length"] and wrap_mode == settings.WrapModes.NOQA: - if "# NOQA" not in line: - return "{}{} NOQA".format(line, self.config["comment_prefix"]) - - return line - def _add_straight_imports( self, straight_modules: Iterable[str], section: str, section_output: List[str] ) -> None: @@ -281,13 +215,15 @@ def _add_from_imports( while from_imports: comments = self.comments["from"].pop(module, ()) if "*" in from_imports and self.config["combine_star"]: - import_statement = self._wrap( + import_statement = wrap_modes.wrap_line( output.with_comments( comments, "{}*".format(import_start), removed=self.config["ignore_comments"], comment_prefix=self.config["comment_prefix"], - ) + ), + self.line_separator, + self.config, ) from_imports = None elif self.config["force_single_line"]: @@ -310,21 +246,31 @@ def _add_from_imports( self.config["keep_direct_and_as_imports"] and self.imports[section]["from"][module][from_import] ): - section_output.append(self._wrap(single_import_line)) + section_output.append( + wrap_modes.wrap_line( + single_import_line, self.line_separator, self.config + ) + ) from_comments = self.comments["straight"].get( "{}.{}".format(module, from_import) ) section_output.extend( output.with_comments( from_comments, - self._wrap(import_start + as_import), + wrap_modes.wrap_line( + import_start + as_import, self.line_separator, self.config + ), removed=self.config["ignore_comments"], comment_prefix=self.config["comment_prefix"], ) for as_import in nsorted(as_imports[from_import]) ) else: - section_output.append(self._wrap(single_import_line)) + section_output.append( + wrap_modes.wrap_line( + single_import_line, self.line_separator, self.config + ) + ) comments = None else: while from_imports and from_imports[0] in as_imports: @@ -346,7 +292,9 @@ def _add_from_imports( section_output.append( output.with_comments( from_comments, - self._wrap(import_start + from_import), + wrap_modes.wrap_line( + import_start + from_import, self.line_separator, self.config + ), removed=self.config["ignore_comments"], comment_prefix=self.config["comment_prefix"], ) @@ -354,7 +302,9 @@ def _add_from_imports( section_output.extend( output.with_comments( from_comments, - self._wrap(import_start + as_import), + wrap_modes.wrap_line( + import_start + as_import, self.line_separator, self.config + ), removed=self.config["ignore_comments"], comment_prefix=self.config["comment_prefix"], ) @@ -399,7 +349,11 @@ def _add_from_imports( ): section_output.append("") section_output.extend(above_comments) - section_output.append(self._wrap(single_import_line)) + section_output.append( + wrap_modes.wrap_line( + single_import_line, self.line_separator, self.config + ) + ) from_imports.remove(from_import) comments = None @@ -448,14 +402,22 @@ def _add_from_imports( do_multiline_reformat = True if do_multiline_reformat: - import_statement = self._multi_line_reformat( - import_start, from_import_section, comments + import_statement = wrap_modes.wrap( + import_start, + from_import_section, + comments, + self.config, + self.line_separator, ) if self.config["multi_line_output"] == settings.WrapModes.GRID: self.config["multi_line_output"] = settings.WrapModes.VERTICAL_GRID try: - other_import_statement = self._multi_line_reformat( - import_start, from_import_section, comments + other_import_statement = wrap_modes.wrap( + import_start, + from_import_section, + comments, + self.config, + self.line_separator, ) if ( max(len(x) for x in import_statement.split("\n")) @@ -468,7 +430,9 @@ def _add_from_imports( not do_multiline_reformat and len(import_statement) > self.config["line_length"] ): - import_statement = self._wrap(import_statement) + import_statement = wrap_modes.wrap_line( + import_statement, self.line_separator, self.config + ) if import_statement: above_comments = self.comments["above"]["from"].pop(module, None) @@ -478,55 +442,6 @@ def _add_from_imports( section_output.extend(above_comments) section_output.append(import_statement) - def _multi_line_reformat( - self, import_start: str, from_imports: List[str], comments: Sequence[str] - ) -> str: - formatter = getattr( - wrap_modes, self.config["multi_line_output"].name.lower(), wrap_modes.grid - ) - dynamic_indent = " " * (len(import_start) + 1) - indent = self.config["indent"] - line_length = self.config["wrap_length"] or self.config["line_length"] - import_statement = formatter( - statement=import_start, - imports=copy.copy(from_imports), - white_space=dynamic_indent, - indent=indent, - line_length=line_length, - comments=comments, - line_separator=self.line_separator, - comment_prefix=self.config["comment_prefix"], - include_trailing_comma=self.config["include_trailing_comma"], - remove_comments=self.config["ignore_comments"], - ) - if self.config["balanced_wrapping"]: - lines = import_statement.split(self.line_separator) - line_count = len(lines) - if len(lines) > 1: - minimum_length = min(len(line) for line in lines[:-1]) - else: - minimum_length = 0 - new_import_statement = import_statement - while len(lines[-1]) < minimum_length and len(lines) == line_count and line_length > 10: - import_statement = new_import_statement - line_length -= 1 - new_import_statement = formatter( - statement=import_start, - imports=copy.copy(from_imports), - white_space=dynamic_indent, - indent=indent, - line_length=line_length, - comments=comments, - line_separator=self.line_separator, - comment_prefix=self.config["comment_prefix"], - include_trailing_comma=self.config["include_trailing_comma"], - remove_comments=self.config["ignore_comments"], - ) - lines = new_import_statement.split(self.line_separator) - if import_statement.count(self.line_separator) == 0: - return self._wrap(import_statement) - return import_statement - def _add_formatted_imports(self) -> None: """Adds the imports back to the file. diff --git a/isort/new_wrap b/isort/new_wrap new file mode 100644 index 000000000..08a644ab6 --- /dev/null +++ b/isort/new_wrap @@ -0,0 +1,43 @@ + formatter = _wrap_modes.get(config["multi_line_output"].name.upper(), grid) + dynamic_indent = " " * (len(import_start) + 1) + indent = config["indent"] + line_length = config["wrap_length"] or config["line_length"] + import_statement = formatter( + statement=import_start, + imports=copy.copy(from_imports), + white_space=dynamic_indent, + indent=indent, + line_length=line_length, + comments=comments, + line_separator=line_separator, + comment_prefix=config["comment_prefix"], + include_trailing_comma=config["include_trailing_comma"], + remove_comments=config["ignore_comments"], + ) + if config["balanced_wrapping"]: + lines = import_statement.split(line_separator) + line_count = len(lines) + if len(lines) > 1: + minimum_length = min(len(line) for line in lines[:-1]) + else: + minimum_length = 0 + new_import_statement = import_statement + while len(lines[-1]) < minimum_length and len(lines) == line_count and line_length > 10: + import_statement = new_import_statement + line_length -= 1 + new_import_statement = formatter( + statement=import_start, + imports=copy.copy(from_imports), + white_space=dynamic_indent, + indent=indent, + line_length=line_length, + comments=comments, + line_separator=line_separator, + comment_prefix=config["comment_prefix"], + include_trailing_comma=config["include_trailing_comma"], + remove_comments=config["ignore_comments"], + ) + lines = new_import_statement.split(line_separator) + if import_statement.count(line_separator) == 0: + return wrap_line(import_statement, line_separator, config) + return import_statement diff --git a/isort/new_wrap_line b/isort/new_wrap_line new file mode 100644 index 000000000..410ac21f7 --- /dev/null +++ b/isort/new_wrap_line @@ -0,0 +1,61 @@ +wrap_mode = config["multi_line_output"] +print(wrap_mode) +print("line: ", repr(line_separator)) +if len(line) > config["line_length"] and wrap_mode != WrapModes.NOQA: # type: ignore + line_without_comment = line + comment = None + if "#" in line: + line_without_comment, comment = line.split("#", 1) + for splitter in ("import ", ".", "as "): + exp = r"\b" + re.escape(splitter) + r"\b" + if re.search(exp, line_without_comment) and not line_without_comment.strip().startswith( + splitter + ): + line_parts = re.split(exp, line_without_comment) + if comment: + line_parts[-1] = "{}{} #{}".format( + line_parts[-1].strip(), + "," if config["include_trailing_comma"] else "", + comment, + ) + next_line = [] + while (len(line) + 2) > ( + config["wrap_length"] or config["line_length"] + ) and line_parts: + next_line.append(line_parts.pop()) + line = splitter.join(line_parts) + if not line: + line = next_line.pop() + + cont_line = wrap_line( + config["indent"] + splitter.join(next_line).lstrip(), line_separator, config + ) + if config["use_parentheses"]: + if splitter == "as ": + output = "{}{}{}".format(line, splitter, cont_line.lstrip()) + else: + output = "{}{}({}{}{}{})".format( + line, + splitter, + line_separator, + cont_line, + "," if config["include_trailing_comma"] and not comment else "", + line_separator + if wrap_mode + in { + WrapModes.VERTICAL_HANGING_INDENT, # type: ignore + WrapModes.VERTICAL_GRID_GROUPED, # type: ignore + } + else "", + ) + lines = output.split(line_separator) + if config["comment_prefix"] in lines[-1] and lines[-1].endswith(")"): + line, comment = lines[-1].split(config["comment_prefix"], 1) + lines[-1] = line + ")" + config["comment_prefix"] + comment[:-1] + return line_separator.join(lines) + return "{}{}\\{}{}".format(line, splitter, line_separator, cont_line) +elif len(line) > config["line_length"] and wrap_mode == WrapModes.NOQA: # type: ignore + if "# NOQA" not in line: + return "{}{} NOQA".format(line, config["comment_prefix"]) + +return line diff --git a/isort/old_wrap b/isort/old_wrap new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/isort/old_wrap @@ -0,0 +1 @@ + diff --git a/isort/old_wrap_line b/isort/old_wrap_line new file mode 100644 index 000000000..ce22969d0 --- /dev/null +++ b/isort/old_wrap_line @@ -0,0 +1,61 @@ +wrap_mode = self.config["multi_line_output"] +if len(line) > self.config["line_length"] and wrap_mode != WrapModes.NOQA: + line_without_comment = line + comment = None + if "#" in line: + line_without_comment, comment = line.split("#", 1) + for splitter in ("import ", ".", "as "): + exp = r"\b" + re.escape(splitter) + r"\b" + if re.search( + exp, line_without_comment + ) and not line_without_comment.strip().startswith(splitter): + line_parts = re.split(exp, line_without_comment) + if comment: + line_parts[-1] = "{}{} #{}".format( + line_parts[-1].strip(), + "," if self.config["include_trailing_comma"] else "", + comment, + ) + next_line = [] + while (len(line) + 2) > ( + self.config["wrap_length"] or self.config["line_length"] + ) and line_parts: + next_line.append(line_parts.pop()) + line = splitter.join(line_parts) + if not line: + line = next_line.pop() + + cont_line = self._wrap( + self.config["indent"] + splitter.join(next_line).lstrip() + ) + if self.config["use_parentheses"]: + if splitter == "as ": + output = "{}{}{}".format(line, splitter, cont_line.lstrip()) + else: + output = "{}{}({}{}{}{})".format( + line, + splitter, + self.line_separator, + cont_line, + "," + if self.config["include_trailing_comma"] and not comment + else "", + self.line_separator + if wrap_mode + in { + WrapModes.VERTICAL_HANGING_INDENT, + WrapModes.VERTICAL_GRID_GROUPED, + } + else "", + ) + lines = output.split(self.line_separator) + if self.config["comment_prefix"] in lines[-1] and lines[-1].endswith(")"): + line, comment = lines[-1].split(self.config["comment_prefix"], 1) + lines[-1] = line + ")" + self.config["comment_prefix"] + comment[:-1] + return self.line_separator.join(lines) + return "{}{}\\{}{}".format(line, splitter, self.line_separator, cont_line) +elif len(line) > self.config["line_length"] and wrap_mode == settings.WrapModes.NOQA: + if "# NOQA" not in line: + return "{}{} NOQA".format(line, self.config["comment_prefix"]) + +return line diff --git a/isort/wrap_modes.py b/isort/wrap_modes.py index d467395ea..0849fbb0c 100644 --- a/isort/wrap_modes.py +++ b/isort/wrap_modes.py @@ -1,11 +1,14 @@ """Defines all wrap modes that can be used when outputting formatted imports""" +import copy import enum +import re from inspect import signature -from typing import Any, Callable, List +from typing import Any, Callable, Dict, List, Sequence +from . import settings from .output import with_comments -_wrap_modes = [] +_wrap_modes = {} def from_string(value: str) -> "WrapModes": @@ -32,7 +35,7 @@ def _wrap_mode(function): """Registers an individual wrap mode. Function name and order are significant and used for creating enum. """ - _wrap_modes.append((function.__name__.upper(), function)) + _wrap_modes[function.__name__.upper()] = function function.__signature__ = signature(_wrap_mode_interface) function.__annotations__ = _wrap_mode_interface.__annotations__ return function @@ -266,5 +269,127 @@ def noqa(**interface): WrapModes = enum.Enum( # type: ignore - "WrapModes", {wrap_mode[0]: index for index, wrap_mode in enumerate(_wrap_modes)} + "WrapModes", {wrap_mode: index for index, wrap_mode in enumerate(_wrap_modes.keys())} ) + + +def wrap( + import_start: str, + from_imports: List[str], + comments: Sequence[str], + config: Dict[str, Any], + line_separator: str, +) -> str: + formatter = _wrap_modes.get(config["multi_line_output"].name.upper(), grid) + breakpoint() + print("HI") + print(formatter) + dynamic_indent = " " * (len(import_start) + 1) + indent = config["indent"] + line_length = config["wrap_length"] or config["line_length"] + print(line_length) + import_statement = formatter( + statement=import_start, + imports=copy.copy(from_imports), + white_space=dynamic_indent, + indent=indent, + line_length=line_length, + comments=comments, + line_separator=line_separator, + comment_prefix=config["comment_prefix"], + include_trailing_comma=config["include_trailing_comma"], + remove_comments=config["ignore_comments"], + ) + if config["balanced_wrapping"]: + lines = import_statement.split(line_separator) + line_count = len(lines) + if len(lines) > 1: + minimum_length = min(len(line) for line in lines[:-1]) + else: + minimum_length = 0 + new_import_statement = import_statement + while len(lines[-1]) < minimum_length and len(lines) == line_count and line_length > 10: + import_statement = new_import_statement + line_length -= 1 + new_import_statement = formatter( + statement=import_start, + imports=copy.copy(from_imports), + white_space=dynamic_indent, + indent=indent, + line_length=line_length, + comments=comments, + line_separator=line_separator, + comment_prefix=config["comment_prefix"], + include_trailing_comma=config["include_trailing_comma"], + remove_comments=config["ignore_comments"], + ) + lines = new_import_statement.split(line_separator) + if import_statement.count(line_separator) == 0: + return wrap_line(import_statement, line_separator, config) + return import_statement + + +def wrap_line(line: str, line_separator: str, config: Dict[str, Any]) -> str: + """Returns a line wrapped to the specified line-length, if possible.""" + wrap_mode = config["multi_line_output"] + print(wrap_mode) + print("line: ", repr(line_separator)) + print(config["line_length"]) + if len(line) > config["line_length"] and wrap_mode != WrapModes.NOQA: # type: ignore + line_without_comment = line + comment = None + if "#" in line: + line_without_comment, comment = line.split("#", 1) + for splitter in ("import ", ".", "as "): + exp = r"\b" + re.escape(splitter) + r"\b" + if re.search(exp, line_without_comment) and not line_without_comment.strip().startswith( + splitter + ): + line_parts = re.split(exp, line_without_comment) + if comment: + line_parts[-1] = "{}{} #{}".format( + line_parts[-1].strip(), + "," if config["include_trailing_comma"] else "", + comment, + ) + next_line = [] + while (len(line) + 2) > ( + config["wrap_length"] or config["line_length"] + ) and line_parts: + next_line.append(line_parts.pop()) + line = splitter.join(line_parts) + if not line: + line = next_line.pop() + + cont_line = wrap_line( + config["indent"] + splitter.join(next_line).lstrip(), line_separator, config + ) + if config["use_parentheses"]: + if splitter == "as ": + output = "{}{}{}".format(line, splitter, cont_line.lstrip()) + else: + output = "{}{}({}{}{}{})".format( + line, + splitter, + line_separator, + cont_line, + "," if config["include_trailing_comma"] and not comment else "", + line_separator + if wrap_mode + in { + WrapModes.VERTICAL_HANGING_INDENT, # type: ignore + WrapModes.VERTICAL_GRID_GROUPED, # type: ignore + } + else "", + ) + lines = output.split(line_separator) + if config["comment_prefix"] in lines[-1] and lines[-1].endswith(")"): + line, comment = lines[-1].split(config["comment_prefix"], 1) + lines[-1] = line + ")" + config["comment_prefix"] + comment[:-1] + return line_separator.join(lines) + return "{}{}\\{}{}".format(line, splitter, line_separator, cont_line) + elif len(line) > config["line_length"] and wrap_mode == WrapModes.NOQA: # type: ignore + if "# NOQA" not in line: + return "{}{} NOQA".format(line, config["comment_prefix"]) + + return line From df6a6e64eedd6b9a975161589be917bc5fdc57a0 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 25 Oct 2019 20:02:33 -0700 Subject: [PATCH 0135/1439] Remove accidentally checked in files --- isort/new_wrap | 43 -------------------------------- isort/new_wrap_line | 61 --------------------------------------------- isort/old_wrap | 1 - isort/old_wrap_line | 61 --------------------------------------------- 4 files changed, 166 deletions(-) delete mode 100644 isort/new_wrap delete mode 100644 isort/new_wrap_line delete mode 100644 isort/old_wrap delete mode 100644 isort/old_wrap_line diff --git a/isort/new_wrap b/isort/new_wrap deleted file mode 100644 index 08a644ab6..000000000 --- a/isort/new_wrap +++ /dev/null @@ -1,43 +0,0 @@ - formatter = _wrap_modes.get(config["multi_line_output"].name.upper(), grid) - dynamic_indent = " " * (len(import_start) + 1) - indent = config["indent"] - line_length = config["wrap_length"] or config["line_length"] - import_statement = formatter( - statement=import_start, - imports=copy.copy(from_imports), - white_space=dynamic_indent, - indent=indent, - line_length=line_length, - comments=comments, - line_separator=line_separator, - comment_prefix=config["comment_prefix"], - include_trailing_comma=config["include_trailing_comma"], - remove_comments=config["ignore_comments"], - ) - if config["balanced_wrapping"]: - lines = import_statement.split(line_separator) - line_count = len(lines) - if len(lines) > 1: - minimum_length = min(len(line) for line in lines[:-1]) - else: - minimum_length = 0 - new_import_statement = import_statement - while len(lines[-1]) < minimum_length and len(lines) == line_count and line_length > 10: - import_statement = new_import_statement - line_length -= 1 - new_import_statement = formatter( - statement=import_start, - imports=copy.copy(from_imports), - white_space=dynamic_indent, - indent=indent, - line_length=line_length, - comments=comments, - line_separator=line_separator, - comment_prefix=config["comment_prefix"], - include_trailing_comma=config["include_trailing_comma"], - remove_comments=config["ignore_comments"], - ) - lines = new_import_statement.split(line_separator) - if import_statement.count(line_separator) == 0: - return wrap_line(import_statement, line_separator, config) - return import_statement diff --git a/isort/new_wrap_line b/isort/new_wrap_line deleted file mode 100644 index 410ac21f7..000000000 --- a/isort/new_wrap_line +++ /dev/null @@ -1,61 +0,0 @@ -wrap_mode = config["multi_line_output"] -print(wrap_mode) -print("line: ", repr(line_separator)) -if len(line) > config["line_length"] and wrap_mode != WrapModes.NOQA: # type: ignore - line_without_comment = line - comment = None - if "#" in line: - line_without_comment, comment = line.split("#", 1) - for splitter in ("import ", ".", "as "): - exp = r"\b" + re.escape(splitter) + r"\b" - if re.search(exp, line_without_comment) and not line_without_comment.strip().startswith( - splitter - ): - line_parts = re.split(exp, line_without_comment) - if comment: - line_parts[-1] = "{}{} #{}".format( - line_parts[-1].strip(), - "," if config["include_trailing_comma"] else "", - comment, - ) - next_line = [] - while (len(line) + 2) > ( - config["wrap_length"] or config["line_length"] - ) and line_parts: - next_line.append(line_parts.pop()) - line = splitter.join(line_parts) - if not line: - line = next_line.pop() - - cont_line = wrap_line( - config["indent"] + splitter.join(next_line).lstrip(), line_separator, config - ) - if config["use_parentheses"]: - if splitter == "as ": - output = "{}{}{}".format(line, splitter, cont_line.lstrip()) - else: - output = "{}{}({}{}{}{})".format( - line, - splitter, - line_separator, - cont_line, - "," if config["include_trailing_comma"] and not comment else "", - line_separator - if wrap_mode - in { - WrapModes.VERTICAL_HANGING_INDENT, # type: ignore - WrapModes.VERTICAL_GRID_GROUPED, # type: ignore - } - else "", - ) - lines = output.split(line_separator) - if config["comment_prefix"] in lines[-1] and lines[-1].endswith(")"): - line, comment = lines[-1].split(config["comment_prefix"], 1) - lines[-1] = line + ")" + config["comment_prefix"] + comment[:-1] - return line_separator.join(lines) - return "{}{}\\{}{}".format(line, splitter, line_separator, cont_line) -elif len(line) > config["line_length"] and wrap_mode == WrapModes.NOQA: # type: ignore - if "# NOQA" not in line: - return "{}{} NOQA".format(line, config["comment_prefix"]) - -return line diff --git a/isort/old_wrap b/isort/old_wrap deleted file mode 100644 index 8b1378917..000000000 --- a/isort/old_wrap +++ /dev/null @@ -1 +0,0 @@ - diff --git a/isort/old_wrap_line b/isort/old_wrap_line deleted file mode 100644 index ce22969d0..000000000 --- a/isort/old_wrap_line +++ /dev/null @@ -1,61 +0,0 @@ -wrap_mode = self.config["multi_line_output"] -if len(line) > self.config["line_length"] and wrap_mode != WrapModes.NOQA: - line_without_comment = line - comment = None - if "#" in line: - line_without_comment, comment = line.split("#", 1) - for splitter in ("import ", ".", "as "): - exp = r"\b" + re.escape(splitter) + r"\b" - if re.search( - exp, line_without_comment - ) and not line_without_comment.strip().startswith(splitter): - line_parts = re.split(exp, line_without_comment) - if comment: - line_parts[-1] = "{}{} #{}".format( - line_parts[-1].strip(), - "," if self.config["include_trailing_comma"] else "", - comment, - ) - next_line = [] - while (len(line) + 2) > ( - self.config["wrap_length"] or self.config["line_length"] - ) and line_parts: - next_line.append(line_parts.pop()) - line = splitter.join(line_parts) - if not line: - line = next_line.pop() - - cont_line = self._wrap( - self.config["indent"] + splitter.join(next_line).lstrip() - ) - if self.config["use_parentheses"]: - if splitter == "as ": - output = "{}{}{}".format(line, splitter, cont_line.lstrip()) - else: - output = "{}{}({}{}{}{})".format( - line, - splitter, - self.line_separator, - cont_line, - "," - if self.config["include_trailing_comma"] and not comment - else "", - self.line_separator - if wrap_mode - in { - WrapModes.VERTICAL_HANGING_INDENT, - WrapModes.VERTICAL_GRID_GROUPED, - } - else "", - ) - lines = output.split(self.line_separator) - if self.config["comment_prefix"] in lines[-1] and lines[-1].endswith(")"): - line, comment = lines[-1].split(self.config["comment_prefix"], 1) - lines[-1] = line + ")" + self.config["comment_prefix"] + comment[:-1] - return self.line_separator.join(lines) - return "{}{}\\{}{}".format(line, splitter, self.line_separator, cont_line) -elif len(line) > self.config["line_length"] and wrap_mode == settings.WrapModes.NOQA: - if "# NOQA" not in line: - return "{}{} NOQA".format(line, self.config["comment_prefix"]) - -return line From 2f50629326defdfdd40eda3765fbbe68b6a39e29 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 25 Oct 2019 23:25:45 -0700 Subject: [PATCH 0136/1439] Reuse Dynamically generated WrapModes enum --- isort/isort.py | 18 ++++++++++++++---- isort/main.py | 11 +++++++++-- isort/settings.py | 21 ++++----------------- isort/wrap_modes.py | 7 ------- 4 files changed, 27 insertions(+), 30 deletions(-) diff --git a/isort/isort.py b/isort/isort.py index 73b7fc930..563e6ec07 100644 --- a/isort/isort.py +++ b/isort/isort.py @@ -397,7 +397,10 @@ def _add_from_imports( len(import_statement) > self.config["line_length"] and len(from_import_section) > 0 and self.config["multi_line_output"] - not in (settings.WrapModes.GRID, settings.WrapModes.VERTICAL) + not in ( + settings.WrapModes.GRID, # type: ignore + settings.WrapModes.VERTICAL, # type: ignore + ) ): do_multiline_reformat = True @@ -409,8 +412,13 @@ def _add_from_imports( self.config, self.line_separator, ) - if self.config["multi_line_output"] == settings.WrapModes.GRID: - self.config["multi_line_output"] = settings.WrapModes.VERTICAL_GRID + if ( + self.config["multi_line_output"] + == settings.WrapModes.GRID # type: ignore + ): # type: ignore + self.config[ + "multi_line_output" + ] = settings.WrapModes.VERTICAL_GRID # type: ignore try: other_import_statement = wrap_modes.wrap( import_start, @@ -425,7 +433,9 @@ def _add_from_imports( ): import_statement = other_import_statement finally: - self.config["multi_line_output"] = settings.WrapModes.GRID + self.config[ + "multi_line_output" + ] = settings.WrapModes.GRID # type: ignore if ( not do_multiline_reformat and len(import_statement) > self.config["line_length"] diff --git a/isort/main.py b/isort/main.py index ab8749aa4..e21be75a2 100644 --- a/isort/main.py +++ b/isort/main.py @@ -11,7 +11,14 @@ from isort import SortImports, __version__ from isort.logo import ASCII_ART -from isort.settings import DEFAULT_SECTIONS, WrapModes, default, file_should_be_skipped, from_path +from isort.settings import ( + DEFAULT_SECTIONS, + WrapModes, + default, + file_should_be_skipped, + from_path, + wrap_mode_from_string, +) shebang_re = re.compile(br"^#!.*\bpython[23w]?\b") @@ -318,7 +325,7 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: "-m", "--multi-line", dest="multi_line_output", - type=WrapModes.from_string, + type=wrap_mode_from_string, help="Multi line output (0-grid, 1-vertical, 2-hanging, 3-vert-hanging, 4-vert-grid, " "5-vert-grid-grouped, 6-vert-grid-grouped-no-comma).", ) diff --git a/isort/settings.py b/isort/settings.py index da83daf5e..7997aa382 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -20,6 +20,8 @@ from .stdlibs import py3, py27 from .utils import difference, union +from .wrap_modes import WrapModes +from .wrap_modes import from_string as wrap_mode_from_string try: import toml @@ -45,21 +47,6 @@ ) -class WrapModes(enum.Enum): - GRID = 0 # 0 - VERTICAL = 1 - HANGING_INDENT = 2 - VERTICAL_HANGING_INDENT = 3 - VERTICAL_GRID = 4 - VERTICAL_GRID_GROUPED = 5 - VERTICAL_GRID_GROUPED_NO_COMMA = 6 - NOQA = 7 - - @staticmethod - def from_string(value: str) -> "WrapModes": - return getattr(WrapModes, str(value), None) or WrapModes(int(value)) - - def _get_default(py_version: Optional[str]) -> Dict[str, Any]: """ Returns the correct standard library based on either the passed py_version flag or the python @@ -116,7 +103,7 @@ def _get_default(py_version: Optional[str]) -> Dict[str, Any]: "known_future_library": ["__future__"], "known_third_party": ["google.appengine.api"], "known_first_party": [], - "multi_line_output": WrapModes.GRID, + "multi_line_output": WrapModes.GRID, # type: ignore "forced_separate": [], "indent": " " * 4, "comment_prefix": " #", @@ -254,7 +241,7 @@ def _update_settings_with_config( def _get_str_to_type_converter(setting_name: str) -> Callable[[str], Any]: type_converter: Callable[[str], Any] = type(default.get(setting_name, "")) if type_converter == WrapModes: - type_converter = WrapModes.from_string + type_converter = wrap_mode_from_string return type_converter diff --git a/isort/wrap_modes.py b/isort/wrap_modes.py index 0849fbb0c..5d2035e34 100644 --- a/isort/wrap_modes.py +++ b/isort/wrap_modes.py @@ -281,13 +281,9 @@ def wrap( line_separator: str, ) -> str: formatter = _wrap_modes.get(config["multi_line_output"].name.upper(), grid) - breakpoint() - print("HI") - print(formatter) dynamic_indent = " " * (len(import_start) + 1) indent = config["indent"] line_length = config["wrap_length"] or config["line_length"] - print(line_length) import_statement = formatter( statement=import_start, imports=copy.copy(from_imports), @@ -332,9 +328,6 @@ def wrap( def wrap_line(line: str, line_separator: str, config: Dict[str, Any]) -> str: """Returns a line wrapped to the specified line-length, if possible.""" wrap_mode = config["multi_line_output"] - print(wrap_mode) - print("line: ", repr(line_separator)) - print(config["line_length"]) if len(line) > config["line_length"] and wrap_mode != WrapModes.NOQA: # type: ignore line_without_comment = line comment = None From 522d5bd936e817779638502bffd40f924931ecfb Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 26 Oct 2019 00:15:39 -0700 Subject: [PATCH 0137/1439] Add wrap module for line and statement actions --- isort/isort.py | 42 ++++++--------- isort/wrap.py | 125 ++++++++++++++++++++++++++++++++++++++++++++ isort/wrap_modes.py | 123 ++----------------------------------------- 3 files changed, 146 insertions(+), 144 deletions(-) create mode 100644 isort/wrap.py diff --git a/isort/isort.py b/isort/isort.py index 563e6ec07..b4b60c360 100644 --- a/isort/isort.py +++ b/isort/isort.py @@ -14,10 +14,9 @@ from isort import utils from isort.format import format_simplified -from . import output, parse, settings, wrap_modes +from . import output, parse, settings, wrap from .finders import FindersManager from .natural import nsorted -from .settings import WrapModes class _SortImports: @@ -215,7 +214,7 @@ def _add_from_imports( while from_imports: comments = self.comments["from"].pop(module, ()) if "*" in from_imports and self.config["combine_star"]: - import_statement = wrap_modes.wrap_line( + import_statement = wrap.line( output.with_comments( comments, "{}*".format(import_start), @@ -247,9 +246,7 @@ def _add_from_imports( and self.imports[section]["from"][module][from_import] ): section_output.append( - wrap_modes.wrap_line( - single_import_line, self.line_separator, self.config - ) + wrap.line(single_import_line, self.line_separator, self.config) ) from_comments = self.comments["straight"].get( "{}.{}".format(module, from_import) @@ -257,7 +254,7 @@ def _add_from_imports( section_output.extend( output.with_comments( from_comments, - wrap_modes.wrap_line( + wrap.line( import_start + as_import, self.line_separator, self.config ), removed=self.config["ignore_comments"], @@ -267,9 +264,7 @@ def _add_from_imports( ) else: section_output.append( - wrap_modes.wrap_line( - single_import_line, self.line_separator, self.config - ) + wrap.line(single_import_line, self.line_separator, self.config) ) comments = None else: @@ -292,7 +287,7 @@ def _add_from_imports( section_output.append( output.with_comments( from_comments, - wrap_modes.wrap_line( + wrap.line( import_start + from_import, self.line_separator, self.config ), removed=self.config["ignore_comments"], @@ -302,7 +297,7 @@ def _add_from_imports( section_output.extend( output.with_comments( from_comments, - wrap_modes.wrap_line( + wrap.line( import_start + as_import, self.line_separator, self.config ), removed=self.config["ignore_comments"], @@ -350,9 +345,7 @@ def _add_from_imports( section_output.append("") section_output.extend(above_comments) section_output.append( - wrap_modes.wrap_line( - single_import_line, self.line_separator, self.config - ) + wrap.line(single_import_line, self.line_separator, self.config) ) from_imports.remove(from_import) comments = None @@ -398,14 +391,14 @@ def _add_from_imports( and len(from_import_section) > 0 and self.config["multi_line_output"] not in ( - settings.WrapModes.GRID, # type: ignore - settings.WrapModes.VERTICAL, # type: ignore + wrap.Modes.GRID, # type: ignore + wrap.Modes.VERTICAL, # type: ignore ) ): do_multiline_reformat = True if do_multiline_reformat: - import_statement = wrap_modes.wrap( + import_statement = wrap.import_statement( import_start, from_import_section, comments, @@ -413,14 +406,13 @@ def _add_from_imports( self.line_separator, ) if ( - self.config["multi_line_output"] - == settings.WrapModes.GRID # type: ignore + self.config["multi_line_output"] == wrap.Modes.GRID # type: ignore ): # type: ignore self.config[ "multi_line_output" - ] = settings.WrapModes.VERTICAL_GRID # type: ignore + ] = wrap.Modes.VERTICAL_GRID # type: ignore try: - other_import_statement = wrap_modes.wrap( + other_import_statement = wrap.import_statement( import_start, from_import_section, comments, @@ -433,14 +425,12 @@ def _add_from_imports( ): import_statement = other_import_statement finally: - self.config[ - "multi_line_output" - ] = settings.WrapModes.GRID # type: ignore + self.config["multi_line_output"] = wrap.Modes.GRID # type: ignore if ( not do_multiline_reformat and len(import_statement) > self.config["line_length"] ): - import_statement = wrap_modes.wrap_line( + import_statement = wrap.line( import_statement, self.line_separator, self.config ) diff --git a/isort/wrap.py b/isort/wrap.py new file mode 100644 index 000000000..b88771162 --- /dev/null +++ b/isort/wrap.py @@ -0,0 +1,125 @@ +import copy +import re +from typing import Any, Dict, List, Sequence + +from .wrap_modes import WrapModes as Modes +from .wrap_modes import formatter_from_string + + +def import_statement( + import_start: str, + from_imports: List[str], + comments: Sequence[str], + config: Dict[str, Any], + line_separator: str, +) -> str: + """Returns a multi-line wrapped form of the provided from import statement.""" + formatter = formatter_from_string(config["multi_line_output"].name) + dynamic_indent = " " * (len(import_start) + 1) + indent = config["indent"] + line_length = config["wrap_length"] or config["line_length"] + import_statement = formatter( + statement=import_start, + imports=copy.copy(from_imports), + white_space=dynamic_indent, + indent=indent, + line_length=line_length, + comments=comments, + line_separator=line_separator, + comment_prefix=config["comment_prefix"], + include_trailing_comma=config["include_trailing_comma"], + remove_comments=config["ignore_comments"], + ) + if config["balanced_wrapping"]: + lines = import_statement.split(line_separator) + line_count = len(lines) + if len(lines) > 1: + minimum_length = min(len(line) for line in lines[:-1]) + else: + minimum_length = 0 + new_import_statement = import_statement + while len(lines[-1]) < minimum_length and len(lines) == line_count and line_length > 10: + import_statement = new_import_statement + line_length -= 1 + new_import_statement = formatter( + statement=import_start, + imports=copy.copy(from_imports), + white_space=dynamic_indent, + indent=indent, + line_length=line_length, + comments=comments, + line_separator=line_separator, + comment_prefix=config["comment_prefix"], + include_trailing_comma=config["include_trailing_comma"], + remove_comments=config["ignore_comments"], + ) + lines = new_import_statement.split(line_separator) + if import_statement.count(line_separator) == 0: + return _wrap_line(import_statement, line_separator, config) + return import_statement + + +def line(line: str, line_separator: str, config: Dict[str, Any]) -> str: + """Returns a line wrapped to the specified line-length, if possible.""" + wrap_mode = config["multi_line_output"] + if len(line) > config["line_length"] and wrap_mode != Modes.NOQA: # type: ignore + line_without_comment = line + comment = None + if "#" in line: + line_without_comment, comment = line.split("#", 1) + for splitter in ("import ", ".", "as "): + exp = r"\b" + re.escape(splitter) + r"\b" + if re.search(exp, line_without_comment) and not line_without_comment.strip().startswith( + splitter + ): + line_parts = re.split(exp, line_without_comment) + if comment: + line_parts[-1] = "{}{} #{}".format( + line_parts[-1].strip(), + "," if config["include_trailing_comma"] else "", + comment, + ) + next_line = [] + while (len(line) + 2) > ( + config["wrap_length"] or config["line_length"] + ) and line_parts: + next_line.append(line_parts.pop()) + line = splitter.join(line_parts) + if not line: + line = next_line.pop() + + cont_line = _wrap_line( + config["indent"] + splitter.join(next_line).lstrip(), line_separator, config + ) + if config["use_parentheses"]: + if splitter == "as ": + output = "{}{}{}".format(line, splitter, cont_line.lstrip()) + else: + output = "{}{}({}{}{}{})".format( + line, + splitter, + line_separator, + cont_line, + "," if config["include_trailing_comma"] and not comment else "", + line_separator + if wrap_mode + in { + Modes.VERTICAL_HANGING_INDENT, # type: ignore + Modes.VERTICAL_GRID_GROUPED, # type: ignore + } + else "", + ) + lines = output.split(line_separator) + if config["comment_prefix"] in lines[-1] and lines[-1].endswith(")"): + line, comment = lines[-1].split(config["comment_prefix"], 1) + lines[-1] = line + ")" + config["comment_prefix"] + comment[:-1] + return line_separator.join(lines) + return "{}{}\\{}{}".format(line, splitter, line_separator, cont_line) + elif len(line) > config["line_length"] and wrap_mode == Modes.NOQA: # type: ignore + if "# NOQA" not in line: + return "{}{} NOQA".format(line, config["comment_prefix"]) + + return line + + +_wrap_line = line diff --git a/isort/wrap_modes.py b/isort/wrap_modes.py index 5d2035e34..4a447b77f 100644 --- a/isort/wrap_modes.py +++ b/isort/wrap_modes.py @@ -1,20 +1,22 @@ """Defines all wrap modes that can be used when outputting formatted imports""" -import copy import enum -import re from inspect import signature from typing import Any, Callable, Dict, List, Sequence from . import settings from .output import with_comments -_wrap_modes = {} +_wrap_modes: Dict[str, Callable[[Any], str]] = {} def from_string(value: str) -> "WrapModes": return getattr(WrapModes, str(value), None) or WrapModes(int(value)) +def formatter_from_string(name: str): + return _wrap_modes.get(name.upper(), grid) + + def _wrap_mode_interface( statement: str, imports: List[str], @@ -271,118 +273,3 @@ def noqa(**interface): WrapModes = enum.Enum( # type: ignore "WrapModes", {wrap_mode: index for index, wrap_mode in enumerate(_wrap_modes.keys())} ) - - -def wrap( - import_start: str, - from_imports: List[str], - comments: Sequence[str], - config: Dict[str, Any], - line_separator: str, -) -> str: - formatter = _wrap_modes.get(config["multi_line_output"].name.upper(), grid) - dynamic_indent = " " * (len(import_start) + 1) - indent = config["indent"] - line_length = config["wrap_length"] or config["line_length"] - import_statement = formatter( - statement=import_start, - imports=copy.copy(from_imports), - white_space=dynamic_indent, - indent=indent, - line_length=line_length, - comments=comments, - line_separator=line_separator, - comment_prefix=config["comment_prefix"], - include_trailing_comma=config["include_trailing_comma"], - remove_comments=config["ignore_comments"], - ) - if config["balanced_wrapping"]: - lines = import_statement.split(line_separator) - line_count = len(lines) - if len(lines) > 1: - minimum_length = min(len(line) for line in lines[:-1]) - else: - minimum_length = 0 - new_import_statement = import_statement - while len(lines[-1]) < minimum_length and len(lines) == line_count and line_length > 10: - import_statement = new_import_statement - line_length -= 1 - new_import_statement = formatter( - statement=import_start, - imports=copy.copy(from_imports), - white_space=dynamic_indent, - indent=indent, - line_length=line_length, - comments=comments, - line_separator=line_separator, - comment_prefix=config["comment_prefix"], - include_trailing_comma=config["include_trailing_comma"], - remove_comments=config["ignore_comments"], - ) - lines = new_import_statement.split(line_separator) - if import_statement.count(line_separator) == 0: - return wrap_line(import_statement, line_separator, config) - return import_statement - - -def wrap_line(line: str, line_separator: str, config: Dict[str, Any]) -> str: - """Returns a line wrapped to the specified line-length, if possible.""" - wrap_mode = config["multi_line_output"] - if len(line) > config["line_length"] and wrap_mode != WrapModes.NOQA: # type: ignore - line_without_comment = line - comment = None - if "#" in line: - line_without_comment, comment = line.split("#", 1) - for splitter in ("import ", ".", "as "): - exp = r"\b" + re.escape(splitter) + r"\b" - if re.search(exp, line_without_comment) and not line_without_comment.strip().startswith( - splitter - ): - line_parts = re.split(exp, line_without_comment) - if comment: - line_parts[-1] = "{}{} #{}".format( - line_parts[-1].strip(), - "," if config["include_trailing_comma"] else "", - comment, - ) - next_line = [] - while (len(line) + 2) > ( - config["wrap_length"] or config["line_length"] - ) and line_parts: - next_line.append(line_parts.pop()) - line = splitter.join(line_parts) - if not line: - line = next_line.pop() - - cont_line = wrap_line( - config["indent"] + splitter.join(next_line).lstrip(), line_separator, config - ) - if config["use_parentheses"]: - if splitter == "as ": - output = "{}{}{}".format(line, splitter, cont_line.lstrip()) - else: - output = "{}{}({}{}{}{})".format( - line, - splitter, - line_separator, - cont_line, - "," if config["include_trailing_comma"] and not comment else "", - line_separator - if wrap_mode - in { - WrapModes.VERTICAL_HANGING_INDENT, # type: ignore - WrapModes.VERTICAL_GRID_GROUPED, # type: ignore - } - else "", - ) - lines = output.split(line_separator) - if config["comment_prefix"] in lines[-1] and lines[-1].endswith(")"): - line, comment = lines[-1].split(config["comment_prefix"], 1) - lines[-1] = line + ")" + config["comment_prefix"] + comment[:-1] - return line_separator.join(lines) - return "{}{}\\{}{}".format(line, splitter, line_separator, cont_line) - elif len(line) > config["line_length"] and wrap_mode == WrapModes.NOQA: # type: ignore - if "# NOQA" not in line: - return "{}{} NOQA".format(line, config["comment_prefix"]) - - return line From 3e5a27580a58dabe08a71e558ec14cfa5593c332 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 26 Oct 2019 23:57:59 -0700 Subject: [PATCH 0138/1439] Initial work toward refactoring isort output flow --- isort/isort.py | 239 +---------------------------------------------- isort/output.py | 217 +++++++++++++++++++++++++++++++++++++++++- isort/sorting.py | 37 ++++++++ 3 files changed, 258 insertions(+), 235 deletions(-) create mode 100644 isort/sorting.py diff --git a/isort/isort.py b/isort/isort.py index b4b60c360..169ae14a0 100644 --- a/isort/isort.py +++ b/isort/isort.py @@ -14,14 +14,12 @@ from isort import utils from isort.format import format_simplified -from . import output, parse, settings, wrap +from . import output, parse, settings, wrap, sorting, output from .finders import FindersManager from .natural import nsorted class _SortImports: - _import_line_intro_re = re.compile("^(?:from|import) ") - _import_line_midline_import_re = re.compile(" import ") def __init__(self, file_contents: str, config: Dict[str, Any], extension: str = "py") -> None: self.config = config @@ -31,6 +29,7 @@ def __init__(self, file_contents: str, config: Dict[str, Any], extension: str = format_simplified(removal) for removal in self.config["remove_imports"] ] + parsed = parse.file_contents(file_contents, config=self.config) ( self.in_lines, self.out_lines, @@ -47,10 +46,10 @@ def __init__(self, file_contents: str, config: Dict[str, Any], extension: str = self.line_separator, self.sections, self._section_comments, - ) = parse.file_contents(file_contents, config=self.config) + ) = parsed if self.import_index != -1: - self._add_formatted_imports() + output.with_formatted_imports(parsed, self.config, self.extension) while self.out_lines and self.out_lines[-1].strip() == "": self.out_lines.pop(-1) self.out_lines.append("") @@ -85,44 +84,6 @@ def _strip_top_comments(lines: Sequence[str], line_separator: str) -> str: lines = lines[1:] return line_separator.join(lines) - @staticmethod - def _module_key( - module_name: str, - config: Mapping[str, Any], - sub_imports: bool = False, - ignore_case: bool = False, - section_name: Optional[Any] = None, - ) -> str: - 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: - module_name = str(module_name).lower() - else: - module_name = str(module_name) - - if sub_imports and config["order_by_type"]: - if module_name.isupper() and len(module_name) > 1: # see issue #376 - prefix = "A" - elif module_name[0:1].isupper(): - prefix = "B" - else: - prefix = "C" - 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: - length_sort = config["length_sort_" + str(section_name).lower()] - return "{}{}{}".format( - module_name in config["force_to_top"] and "A" or "B", - prefix, - length_sort and (str(len(module_name)) + ":" + module_name) or module_name, - ) - def _add_straight_imports( self, straight_modules: Iterable[str], section: str, section_output: List[str] ) -> None: @@ -174,7 +135,7 @@ def _add_from_imports( if not self.config["no_inline_sort"] or self.config["force_single_line"]: from_imports = nsorted( from_imports, - key=lambda key: self._module_key( + key=lambda key: sorting.module_key( key, self.config, True, ignore_case, section_name=section ), ) @@ -441,193 +402,3 @@ def _add_from_imports( section_output.append("") section_output.extend(above_comments) section_output.append(import_statement) - - def _add_formatted_imports(self) -> None: - """Adds the imports back to the file. - - (at the index of the first import) sorted alphabetically and split between groups - - """ - sort_ignore_case = self.config["force_alphabetical_sort_within_sections"] - sections: Iterable[str] = itertools.chain(self.sections, self.config["forced_separate"]) - - if self.config["no_sections"]: - self.imports["no_sections"] = {"straight": [], "from": {}} - for section in sections: - self.imports["no_sections"]["straight"].extend( - self.imports[section].get("straight", []) - ) - self.imports["no_sections"]["from"].update(self.imports[section].get("from", {})) - sections = ("no_sections",) - - output: List[str] = [] - 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), - ) - from_modules = self.imports[section]["from"] - from_modules = nsorted( - from_modules, - key=lambda key: self._module_key(key, self.config, section_name=section), - ) - - if self.config["force_sort_within_sections"]: - copied_comments = copy.deepcopy(self.comments) - - section_output: List[str] = [] - if self.config["from_first"]: - self._add_from_imports(from_modules, section, section_output, sort_ignore_case) - if self.config["lines_between_types"] and from_modules and straight_modules: - section_output.extend([""] * self.config["lines_between_types"]) - self._add_straight_imports(straight_modules, section, section_output) - else: - self._add_straight_imports(straight_modules, section, section_output) - if self.config["lines_between_types"] and from_modules and straight_modules: - section_output.extend([""] * self.config["lines_between_types"]) - self._add_from_imports(from_modules, section, section_output, sort_ignore_case) - - if self.config["force_sort_within_sections"]: - - def by_module(line: str) -> str: - section = "B" - - line = self._import_line_intro_re.sub( - "", self._import_line_midline_import_re.sub(".", line) - ) - if line.split(" ")[0] in self.config["force_to_top"]: - section = "A" - if not self.config["order_by_type"]: - line = line.lower() - return "{}{}".format(section, line) - - # Remove comments - section_output = [line for line in section_output if not line.startswith("#")] - - section_output = nsorted(section_output, key=by_module) - - # Add comments back - all_comments = copied_comments["above"]["from"] - all_comments.update(copied_comments["above"]["straight"]) - comment_indexes = {} - for module, comment_list in all_comments.items(): - for idx, line in enumerate(section_output): - if module in line: - comment_indexes[idx] = comment_list - added = 0 - for idx, comment_list in comment_indexes.items(): - for comment in comment_list: - section_output.insert(idx + added, comment) - added += 1 - - section_name = section - no_lines_before = section_name in self.config["no_lines_before"] - - if section_output: - if section_name in self.place_imports: - self.place_imports[section_name] = section_output - continue - - section_title = self.config.get("import_heading_" + str(section_name).lower(), "") - if section_title: - section_comment = "# {}".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 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() == "": - output.pop(0) - - output_at = 0 - if self.import_index < self.original_num_of_lines: - output_at = self.import_index - elif self._first_comment_index_end != -1 and self._first_comment_index_start <= 2: - output_at = self._first_comment_index_end - self.out_lines[output_at:0] = output - - imports_tail = output_at + len(output) - while [ - character.strip() for character in self.out_lines[imports_tail : imports_tail + 1] - ] == [""]: - self.out_lines.pop(imports_tail) - - if len(self.out_lines) > imports_tail: - next_construct = "" - self._in_quote: str = "" - tail = self.out_lines[imports_tail:] - - for index, line in enumerate(tail): - in_quote = self._in_quote - ( - should_skip, - self._in_quote, - self.in_top_comment, - self._first_comment_index_start, - self._first_comment_index_end, - ) = parse.skip_line( - line, - in_quote=self._in_quote, - in_top_comment=False, - index=len(self.out_lines), - section_comments=self._section_comments, - first_comment_index_start=self._first_comment_index_start, - first_comment_index_end=self._first_comment_index_end, - ) - if not should_skip and line.strip(): - if ( - line.strip().startswith("#") - and len(tail) > (index + 1) - and tail[index + 1].strip() - ): - continue - next_construct = line - break - elif not in_quote: - parts = line.split() - if ( - len(parts) >= 3 - and parts[1] == "=" - and "'" not in parts[0] - and '"' not in parts[0] - ): - next_construct = line - break - - if self.config["lines_after_imports"] != -1: - self.out_lines[imports_tail:0] = [ - "" for line in range(self.config["lines_after_imports"]) - ] - elif self.extension != "pyi" and ( - next_construct.startswith("def ") - or next_construct.startswith("class ") - or next_construct.startswith("@") - or next_construct.startswith("async def") - ): - self.out_lines[imports_tail:0] = ["", ""] - else: - self.out_lines[imports_tail:0] = [""] - - if self.place_imports: - new_out_lines = [] - for index, line in enumerate(self.out_lines): - new_out_lines.append(line) - if line in self.import_placements: - new_out_lines.extend(self.place_imports[self.import_placements[line]]) - if len(self.out_lines) <= index or self.out_lines[index + 1].strip() != "": - new_out_lines.append("") - self.out_lines = new_out_lines diff --git a/isort/output.py b/isort/output.py index f0b0bb9f1..9d56a5fab 100644 --- a/isort/output.py +++ b/isort/output.py @@ -1,8 +1,11 @@ from typing import List, Optional -from . import parse +from . import parse, sorting +_import_line_intro_re = re.compile("^(?:from|import) ") +_import_line_midline_import_re = re.compile(" import ") + def with_comments( comments: Optional[List[str]], original_string: str = "", @@ -19,3 +22,215 @@ def with_comments( return "{}{} {}".format( parse.import_comment(original_string)[0], comment_prefix, "; ".join(comments) ) + + + +class ParsedContent(NamedTuple): + in_lines: List[str] + lines_without_imports: List[str] + import_index: int + place_imports: Dict[str, List[str]] + import_placements: Dict[str, str] + as_map: Dict[str, List[str]] + imports: Dict[str, Dict[str, Any]] + categorized_comments: "CommentsDict" + first_comment_index_start: int + first_comment_index_end: int + change_count: int + original_line_count: int + line_separator: str + sections: Any + section_comments: List[str] + + + + +def with_formatted_imports(parsed: parse.ParsedContent, config: Dict[str, Any], extension: str="py"): + """Adds the imports back to the file. + + (at the index of the first import) sorted alphabetically and split between groups + + """ + sort_ignore_case = config["force_alphabetical_sort_within_sections"] + sections: Iterable[str] = itertools.chain(parsed.sections, config["forced_separate"]) + + if config["no_sections"]: + parsed.imports["no_sections"] = {"straight": [], "from": {}} + for section in sections: + parsed.imports["no_sections"]["straight"].extend( + parsed.imports[section].get("straight", []) + ) + parsed.imports["no_sections"]["from"].update(parsed.imports[section].get("from", {})) + sections = ("no_sections",) + + output: List[str] = [] + pending_lines_before = False + for section in sections: + straight_modules = parsed.imports[section]["straight"] + straight_modules = nsorted( + straight_modules, + key=lambda key: sorting.module_key(key, config, section_name=section), + ) + from_modules = parsed.imports[section]["from"] + from_modules = nsorted( + from_modules, + key=lambda key: sorting.module_key(key, config, section_name=section), + ) + + if config["force_sort_within_sections"]: + copied_comments = copy.deepcopy(parsed.categorized_comments) + + section_output: List[str] = [] + if config["from_first"]: + self._add_from_imports(from_modules, section, section_output, sort_ignore_case) + if config["lines_between_types"] and from_modules and straight_modules: + section_output.extend([""] * config["lines_between_types"]) + self._add_straight_imports(straight_modules, section, section_output) + else: + self._add_straight_imports(straight_modules, section, section_output) + if config["lines_between_types"] and from_modules and straight_modules: + section_output.extend([""] * config["lines_between_types"]) + self._add_from_imports(from_modules, section, section_output, sort_ignore_case) + + if config["force_sort_within_sections"]: + + def by_module(line: str) -> str: + section = "B" + + line = _import_line_intro_re.sub( + "", _import_line_midline_import_re.sub(".", line) + ) + if line.split(" ")[0] in config["force_to_top"]: + section = "A" + if not config["order_by_type"]: + line = line.lower() + return "{}{}".format(section, line) + + # Remove comments + section_output = [line for line in section_output if not line.startswith("#")] + + section_output = nsorted(section_output, key=by_module) + + # Add comments back + all_comments = copied_comments["above"]["from"] + all_comments.update(copied_comments["above"]["straight"]) + comment_indexes = {} + for module, comment_list in all_comments.items(): + for idx, line in enumerate(section_output): + if module in line: + comment_indexes[idx] = comment_list + added = 0 + for idx, comment_list in comment_indexes.items(): + for comment in comment_list: + section_output.insert(idx + added, comment) + added += 1 + + section_name = section + no_lines_before = section_name in config["no_lines_before"] + + if section_output: + if section_name in parsed.place_imports: + parsed.place_imports[section_name] = section_output + continue + + section_title = config.get("import_heading_" + str(section_name).lower(), "") + if section_title: + section_comment = "# {}".format(section_title) + if ( + section_comment not in parsed.lines_without_imports[0:1] + and section_comment not in parsed.in_lines[0:1] + ): + section_output.insert(0, section_comment) + + if pending_lines_before or not no_lines_before: + output += [""] * 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() == "": + output.pop(0) + + output_at = 0 + if parsed.import_index < parsed.original_line_count: + output_at = parsed.import_index + elif parsed.first_comment_index_end != -1 and parsed.first_comment_index_start <= 2: + output_at = parsed.first_comment_index_end + parsed.lines_without_imports[output_at:0] = output + + imports_tail = output_at + len(output) + while [ + character.strip() for character in parsed.lines_without_imports[imports_tail : imports_tail + 1] + ] == [""]: + parsed.lines_without_imports.pop(imports_tail) + + if len(parsed.lines_without_imports) > imports_tail: + next_construct = "" + _in_quote: str = "" + tail = parsed.lines_without_imports[imports_tail:] + + for index, line in enumerate(tail): + in_quote = _in_quote + ( + should_skip, + _in_quote, + _in_top_comment, + _first_comment_index_start, + self._first_comment_index_end, + ) = parse.skip_line( + line, + in_quote=self._in_quote, + in_top_comment=False, + index=len(parsed.lines_without_imports), + section_comments=self._section_comments, + first_comment_index_start=self._first_comment_index_start, + first_comment_index_end=self._first_comment_index_end, + ) + if not should_skip and line.strip(): + if ( + line.strip().startswith("#") + and len(tail) > (index + 1) + and tail[index + 1].strip() + ): + continue + next_construct = line + break + elif not in_quote: + parts = line.split() + if ( + len(parts) >= 3 + and parts[1] == "=" + and "'" not in parts[0] + and '"' not in parts[0] + ): + next_construct = line + break + + if config["lines_after_imports"] != -1: + parsed.lines_without_imports[imports_tail:0] = [ + "" for line in range(config["lines_after_imports"]) + ] + elif self.extension != "pyi" and ( + next_construct.startswith("def ") + or next_construct.startswith("class ") + or next_construct.startswith("@") + or next_construct.startswith("async def") + ): + parsed.lines_without_imports[imports_tail:0] = ["", ""] + else: + parsed.lines_without_imports[imports_tail:0] = [""] + + if parsed.place_imports: + new_out_lines = [] + for index, line in enumerate(parsed.lines_without_imports): + new_out_lines.append(line) + if line in parsed.import_placements: + new_out_lines.extend(parsed.place_imports[parsed.import_placements[line]]) + if len(parsed.lines_without_imports) <= index or parsed.lines_without_imports[index + 1].strip() != "": + new_out_lines.append("") + parsed.lines_without_imports = new_out_lines diff --git a/isort/sorting.py b/isort/sorting.py new file mode 100644 index 000000000..d5e482464 --- /dev/null +++ b/isort/sorting.py @@ -0,0 +1,37 @@ + +def module_key( + module_name: str, + config: Mapping[str, Any], + sub_imports: bool = False, + ignore_case: bool = False, + section_name: Optional[Any] = None, +) -> str: + 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: + module_name = str(module_name).lower() + else: + module_name = str(module_name) + + if sub_imports and config["order_by_type"]: + if module_name.isupper() and len(module_name) > 1: # see issue #376 + prefix = "A" + elif module_name[0:1].isupper(): + prefix = "B" + else: + prefix = "C" + 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: + length_sort = config["length_sort_" + str(section_name).lower()] + return "{}{}{}".format( + module_name in config["force_to_top"] and "A" or "B", + prefix, + length_sort and (str(len(module_name)) + ":" + module_name) or module_name, + ) From 0b6e2eb93e5904042b6381ddc60b3d43d0542a64 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 27 Oct 2019 01:24:13 -0700 Subject: [PATCH 0139/1439] Move over all output methods into output module functions --- isort/isort.py | 326 +--------------------- isort/output.py | 685 ++++++++++++++++++++++++++++++++++------------- isort/sorting.py | 3 + 3 files changed, 502 insertions(+), 512 deletions(-) diff --git a/isort/isort.py b/isort/isort.py index 169ae14a0..911fbe3a2 100644 --- a/isort/isort.py +++ b/isort/isort.py @@ -12,7 +12,6 @@ from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple from isort import utils -from isort.format import format_simplified from . import output, parse, settings, wrap, sorting, output from .finders import FindersManager @@ -25,10 +24,6 @@ def __init__(self, file_contents: str, config: Dict[str, Any], extension: str = self.config = config self.extension = extension - self.remove_imports = [ - format_simplified(removal) for removal in self.config["remove_imports"] - ] - parsed = parse.file_contents(file_contents, config=self.config) ( self.in_lines, @@ -49,7 +44,7 @@ def __init__(self, file_contents: str, config: Dict[str, Any], extension: str = ) = parsed if self.import_index != -1: - output.with_formatted_imports(parsed, self.config, self.extension) + self.out_lines = output.with_formatted_imports(parsed, self.config, self.extension) while self.out_lines and self.out_lines[-1].strip() == "": self.out_lines.pop(-1) self.out_lines.append("") @@ -83,322 +78,3 @@ def _strip_top_comments(lines: Sequence[str], line_separator: str) -> str: while lines and lines[0].startswith("#"): lines = lines[1:] return line_separator.join(lines) - - def _add_straight_imports( - self, straight_modules: Iterable[str], section: str, section_output: List[str] - ) -> None: - for module in straight_modules: - if module in self.remove_imports: - continue - - import_definition = [] - if module in self.as_map: - if ( - self.config["keep_direct_and_as_imports"] - and self.imports[section]["straight"][module] - ): - import_definition.append("import {}".format(module)) - import_definition.extend( - "import {} as {}".format(module, as_import) for as_import in self.as_map[module] - ) - else: - import_definition.append("import {}".format(module)) - - comments_above = self.comments["above"]["straight"].pop(module, None) - if comments_above: - if section_output and self.config.get("ensure_newline_before_comments"): - section_output.append("") - section_output.extend(comments_above) - section_output.extend( - output.with_comments( - self.comments["straight"].get(module), - idef, - removed=self.config["ignore_comments"], - comment_prefix=self.config["comment_prefix"], - ) - for idef in import_definition - ) - - def _add_from_imports( - self, - from_modules: Iterable[str], - section: str, - section_output: List[str], - ignore_case: bool, - ) -> None: - for module in from_modules: - if module in self.remove_imports: - continue - - import_start = "from {} import ".format(module) - from_imports = list(self.imports[section]["from"][module]) - if not self.config["no_inline_sort"] or self.config["force_single_line"]: - from_imports = nsorted( - from_imports, - key=lambda key: sorting.module_key( - key, self.config, True, ignore_case, section_name=section - ), - ) - if self.remove_imports: - from_imports = [ - line - for line in from_imports - if not "{}.{}".format(module, line) in self.remove_imports - ] - - sub_modules = ["{}.{}".format(module, from_import) for from_import in from_imports] - as_imports = { - from_import: [ - "{} as {}".format(from_import, as_module) - for as_module in self.as_map[sub_module] - ] - for from_import, sub_module in zip(from_imports, sub_modules) - if sub_module in self.as_map - } - if self.config["combine_as_imports"] and not ( - "*" in from_imports and self.config["combine_star"] - ): - if not self.config["no_inline_sort"]: - for as_import in as_imports: - as_imports[as_import] = nsorted(as_imports[as_import]) - for from_import in copy.copy(from_imports): - if from_import in as_imports: - idx = from_imports.index(from_import) - if ( - self.config["keep_direct_and_as_imports"] - and self.imports[section]["from"][module][from_import] - ): - from_imports[(idx + 1) : (idx + 1)] = as_imports.pop(from_import) - else: - from_imports[idx : (idx + 1)] = as_imports.pop(from_import) - - while from_imports: - comments = self.comments["from"].pop(module, ()) - if "*" in from_imports and self.config["combine_star"]: - import_statement = wrap.line( - output.with_comments( - comments, - "{}*".format(import_start), - removed=self.config["ignore_comments"], - comment_prefix=self.config["comment_prefix"], - ), - self.line_separator, - self.config, - ) - from_imports = None - elif self.config["force_single_line"]: - import_statement = None - while from_imports: - from_import = from_imports.pop(0) - single_import_line = output.with_comments( - comments, - import_start + from_import, - removed=self.config["ignore_comments"], - comment_prefix=self.config["comment_prefix"], - ) - comment = self.comments["nested"].get(module, {}).pop(from_import, None) - if comment: - single_import_line += "{} {}".format( - comments and ";" or self.config["comment_prefix"], comment - ) - if from_import in as_imports: - if ( - self.config["keep_direct_and_as_imports"] - and self.imports[section]["from"][module][from_import] - ): - section_output.append( - wrap.line(single_import_line, self.line_separator, self.config) - ) - from_comments = self.comments["straight"].get( - "{}.{}".format(module, from_import) - ) - section_output.extend( - output.with_comments( - from_comments, - wrap.line( - import_start + as_import, self.line_separator, self.config - ), - removed=self.config["ignore_comments"], - comment_prefix=self.config["comment_prefix"], - ) - for as_import in nsorted(as_imports[from_import]) - ) - else: - section_output.append( - wrap.line(single_import_line, self.line_separator, self.config) - ) - comments = None - else: - while from_imports and from_imports[0] in as_imports: - from_import = from_imports.pop(0) - as_imports[from_import] = nsorted(as_imports[from_import]) - from_comments = self.comments["straight"].get( - "{}.{}".format(module, from_import) - ) - above_comments = self.comments["above"]["from"].pop(module, None) - if above_comments: - if section_output and self.config.get("ensure_newline_before_comments"): - section_output.append("") - section_output.extend(above_comments) - - if ( - self.config["keep_direct_and_as_imports"] - and self.imports[section]["from"][module][from_import] - ): - section_output.append( - output.with_comments( - from_comments, - wrap.line( - import_start + from_import, self.line_separator, self.config - ), - removed=self.config["ignore_comments"], - comment_prefix=self.config["comment_prefix"], - ) - ) - section_output.extend( - output.with_comments( - from_comments, - wrap.line( - import_start + as_import, self.line_separator, self.config - ), - removed=self.config["ignore_comments"], - comment_prefix=self.config["comment_prefix"], - ) - for as_import in as_imports[from_import] - ) - - star_import = False - if "*" in from_imports: - section_output.append( - output.with_comments( - comments, - "{}*".format(import_start), - removed=self.config["ignore_comments"], - comment_prefix=self.config["comment_prefix"], - ) - ) - from_imports.remove("*") - star_import = True - comments = None - - for from_import in copy.copy(from_imports): - if ( - from_import in as_imports - and not self.config["keep_direct_and_as_imports"] - ): - continue - comment = self.comments["nested"].get(module, {}).pop(from_import, None) - if comment: - single_import_line = output.with_comments( - comments, - import_start + from_import, - removed=self.config["ignore_comments"], - comment_prefix=self.config["comment_prefix"], - ) - single_import_line += "{} {}".format( - comments and ";" or self.config["comment_prefix"], comment - ) - above_comments = self.comments["above"]["from"].pop(module, None) - if above_comments: - if section_output and self.config.get( - "ensure_newline_before_comments" - ): - section_output.append("") - section_output.extend(above_comments) - section_output.append( - wrap.line(single_import_line, self.line_separator, self.config) - ) - from_imports.remove(from_import) - comments = None - - from_import_section = [] - while from_imports and ( - from_imports[0] not in as_imports - or ( - self.config["keep_direct_and_as_imports"] - and self.config["combine_as_imports"] - and self.imports[section]["from"][module][from_import] - ) - ): - from_import_section.append(from_imports.pop(0)) - if star_import: - import_statement = import_start + (", ").join(from_import_section) - else: - import_statement = output.with_comments( - comments, - import_start + (", ").join(from_import_section), - removed=self.config["ignore_comments"], - comment_prefix=self.config["comment_prefix"], - ) - if not from_import_section: - import_statement = "" - - do_multiline_reformat = False - - force_grid_wrap = self.config["force_grid_wrap"] - if force_grid_wrap and len(from_import_section) >= force_grid_wrap: - do_multiline_reformat = True - - if ( - len(import_statement) > self.config["line_length"] - and len(from_import_section) > 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_import_section) > 0 - and self.config["multi_line_output"] - not in ( - wrap.Modes.GRID, # type: ignore - wrap.Modes.VERTICAL, # type: ignore - ) - ): - do_multiline_reformat = True - - if do_multiline_reformat: - import_statement = wrap.import_statement( - import_start, - from_import_section, - comments, - self.config, - self.line_separator, - ) - if ( - self.config["multi_line_output"] == wrap.Modes.GRID # type: ignore - ): # type: ignore - self.config[ - "multi_line_output" - ] = wrap.Modes.VERTICAL_GRID # type: ignore - try: - other_import_statement = wrap.import_statement( - import_start, - from_import_section, - comments, - self.config, - self.line_separator, - ) - if ( - max(len(x) for x in import_statement.split("\n")) - > self.config["line_length"] - ): - import_statement = other_import_statement - finally: - self.config["multi_line_output"] = wrap.Modes.GRID # type: ignore - if ( - not do_multiline_reformat - and len(import_statement) > self.config["line_length"] - ): - import_statement = wrap.line( - import_statement, self.line_separator, self.config - ) - - if import_statement: - above_comments = self.comments["above"]["from"].pop(module, None) - if above_comments: - if section_output and self.config.get("ensure_newline_before_comments"): - section_output.append("") - section_output.extend(above_comments) - section_output.append(import_statement) diff --git a/isort/output.py b/isort/output.py index 9d56a5fab..6bd781236 100644 --- a/isort/output.py +++ b/isort/output.py @@ -1,6 +1,8 @@ from typing import List, Optional +import re from . import parse, sorting +from isort.format import format_simplified _import_line_intro_re = re.compile("^(?:from|import) ") @@ -24,213 +26,522 @@ def with_comments( ) +def with_formatted_imports(parsed: parse.ParsedContent, config: Dict[str, Any], extension: str="py") -> List[str]: + """Adds the imports back to the file. -class ParsedContent(NamedTuple): - in_lines: List[str] - lines_without_imports: List[str] - import_index: int - place_imports: Dict[str, List[str]] - import_placements: Dict[str, str] - as_map: Dict[str, List[str]] - imports: Dict[str, Dict[str, Any]] - categorized_comments: "CommentsDict" - first_comment_index_start: int - first_comment_index_end: int - change_count: int - original_line_count: int - line_separator: str - sections: Any - section_comments: List[str] + (at the index of the first import) sorted alphabetically and split between groups + """ + formatted_output: List[str] = parsed.lines_without_imports.copy() + remove_imports = [ + format_simplified(removal) for removal in config["remove_imports"] + ] + sort_ignore_case = config["force_alphabetical_sort_within_sections"] + sections: Iterable[str] = itertools.chain(parsed.sections, config["forced_separate"]) - -def with_formatted_imports(parsed: parse.ParsedContent, config: Dict[str, Any], extension: str="py"): - """Adds the imports back to the file. - - (at the index of the first import) sorted alphabetically and split between groups - - """ - sort_ignore_case = config["force_alphabetical_sort_within_sections"] - sections: Iterable[str] = itertools.chain(parsed.sections, config["forced_separate"]) - - if config["no_sections"]: - parsed.imports["no_sections"] = {"straight": [], "from": {}} - for section in sections: - parsed.imports["no_sections"]["straight"].extend( - parsed.imports[section].get("straight", []) - ) - parsed.imports["no_sections"]["from"].update(parsed.imports[section].get("from", {})) - sections = ("no_sections",) - - output: List[str] = [] - pending_lines_before = False + if config["no_sections"]: + parsed.imports["no_sections"] = {"straight": [], "from": {}} for section in sections: - straight_modules = parsed.imports[section]["straight"] - straight_modules = nsorted( - straight_modules, - key=lambda key: sorting.module_key(key, config, section_name=section), - ) - from_modules = parsed.imports[section]["from"] - from_modules = nsorted( - from_modules, - key=lambda key: sorting.module_key(key, config, section_name=section), + parsed.imports["no_sections"]["straight"].extend( + parsed.imports[section].get("straight", []) ) + parsed.imports["no_sections"]["from"].update(parsed.imports[section].get("from", {})) + sections = ("no_sections",) + + output: List[str] = [] + pending_lines_before = False + for section in sections: + straight_modules = parsed.imports[section]["straight"] + straight_modules = nsorted( + straight_modules, + key=lambda key: sorting.module_key(key, config, section_name=section), + ) + from_modules = parsed.imports[section]["from"] + from_modules = nsorted( + from_modules, + key=lambda key: sorting.module_key(key, config, section_name=section), + ) - if config["force_sort_within_sections"]: - copied_comments = copy.deepcopy(parsed.categorized_comments) + if config["force_sort_within_sections"]: + copied_comments = copy.deepcopy(parsed.categorized_comments) - section_output: List[str] = [] - if config["from_first"]: - self._add_from_imports(from_modules, section, section_output, sort_ignore_case) - if config["lines_between_types"] and from_modules and straight_modules: - section_output.extend([""] * config["lines_between_types"]) - self._add_straight_imports(straight_modules, section, section_output) - else: - self._add_straight_imports(straight_modules, section, section_output) - if config["lines_between_types"] and from_modules and straight_modules: - section_output.extend([""] * config["lines_between_types"]) - self._add_from_imports(from_modules, section, section_output, sort_ignore_case) + section_output: List[str] = [] + if config["from_first"]: + section_output = _with_from_imports(parsed, config, from_modules, section, section_output, sort_ignore_case, remove_imports) + if config["lines_between_types"] and from_modules and straight_modules: + section_output.extend([""] * config["lines_between_types"]) + section_output = _with_straight_imports(parsed, config, straight_modules, section, section_output, remove_imports) + else: + section_output = _with_straight_imports(parsed, config, straight_modules, section, section_output, remove_imports) + if config["lines_between_types"] and from_modules and straight_modules: + section_output.extend([""] * config["lines_between_types"]) + section_output = _with_from_imports(parsed, config, from_modules, section, section_output, sort_ignore_case, remove_imports) - if config["force_sort_within_sections"]: + if config["force_sort_within_sections"]: - def by_module(line: str) -> str: - section = "B" + def by_module(line: str) -> str: + section = "B" - line = _import_line_intro_re.sub( - "", _import_line_midline_import_re.sub(".", line) - ) - if line.split(" ")[0] in config["force_to_top"]: - section = "A" - if not config["order_by_type"]: - line = line.lower() - return "{}{}".format(section, line) - - # Remove comments - section_output = [line for line in section_output if not line.startswith("#")] - - section_output = nsorted(section_output, key=by_module) - - # Add comments back - all_comments = copied_comments["above"]["from"] - all_comments.update(copied_comments["above"]["straight"]) - comment_indexes = {} - for module, comment_list in all_comments.items(): - for idx, line in enumerate(section_output): - if module in line: - comment_indexes[idx] = comment_list - added = 0 - for idx, comment_list in comment_indexes.items(): - for comment in comment_list: - section_output.insert(idx + added, comment) - added += 1 - - section_name = section - no_lines_before = section_name in config["no_lines_before"] - - if section_output: - if section_name in parsed.place_imports: - parsed.place_imports[section_name] = section_output + line = _import_line_intro_re.sub( + "", _import_line_midline_import_re.sub(".", line) + ) + if line.split(" ")[0] in config["force_to_top"]: + section = "A" + if not config["order_by_type"]: + line = line.lower() + return "{}{}".format(section, line) + + # Remove comments + section_output = [line for line in section_output if not line.startswith("#")] + + section_output = nsorted(section_output, key=by_module) + + # Add comments back + all_comments = copied_comments["above"]["from"] + all_comments.update(copied_comments["above"]["straight"]) + comment_indexes = {} + for module, comment_list in all_comments.items(): + for idx, line in enumerate(section_output): + if module in line: + comment_indexes[idx] = comment_list + added = 0 + for idx, comment_list in comment_indexes.items(): + for comment in comment_list: + section_output.insert(idx + added, comment) + added += 1 + + section_name = section + no_lines_before = section_name in config["no_lines_before"] + + if section_output: + if section_name in parsed.place_imports: + parsed.place_imports[section_name] = section_output + continue + + section_title = config.get("import_heading_" + str(section_name).lower(), "") + if section_title: + section_comment = "# {}".format(section_title) + if ( + section_comment not in parsed.lines_without_imports[0:1] + and section_comment not in parsed.in_lines[0:1] + ): + section_output.insert(0, section_comment) + + if pending_lines_before or not no_lines_before: + output += [""] * 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() == "": + output.pop(0) + + output_at = 0 + if parsed.import_index < parsed.original_line_count: + output_at = parsed.import_index + elif parsed.first_comment_index_end != -1 and parsed.first_comment_index_start <= 2: + output_at = parsed.first_comment_index_end + formatted_output[output_at:0] = output + + imports_tail = output_at + len(output) + while [ + character.strip() for character in formatted_output[imports_tail : imports_tail + 1] + ] == [""]: + formatted_output.pop(imports_tail) + + if len(formatted_output) > imports_tail: + next_construct = "" + _in_quote: str = "" + tail = formatted_output[imports_tail:] + + for index, line in enumerate(tail): + in_quote = _in_quote + should_skip,_in_quote, *_ = parse.skip_line( + line, + in_quote=_in_quote, + in_top_comment=False, + index=len(formatted_output), + section_comments=parsed.section_comments, + first_comment_index_start=parsed.first_comment_index_start, + first_comment_index_end=parsed.first_comment_index_end, + ) + if not should_skip and line.strip(): + if ( + line.strip().startswith("#") + and len(tail) > (index + 1) + and tail[index + 1].strip() + ): continue + next_construct = line + break + elif not in_quote: + parts = line.split() + if ( + len(parts) >= 3 + and parts[1] == "=" + and "'" not in parts[0] + and '"' not in parts[0] + ): + next_construct = line + break - section_title = config.get("import_heading_" + str(section_name).lower(), "") - if section_title: - section_comment = "# {}".format(section_title) + if config["lines_after_imports"] != -1: + formatted_output[imports_tail:0] = [ + "" for line in range(config["lines_after_imports"]) + ] + elif extension != "pyi" and ( + next_construct.startswith("def ") + or next_construct.startswith("class ") + or next_construct.startswith("@") + or next_construct.startswith("async def") + ): + formatted_output[imports_tail:0] = ["", ""] + else: + formatted_output[imports_tail:0] = [""] + + if parsed.place_imports: + new_out_lines = [] + for index, line in enumerate(formatted_output): + new_out_lines.append(line) + if line in parsed.import_placements: + new_out_lines.extend(parsed.place_imports[parsed.import_placements[line]]) + if len(formatted_output) <= index or formatted_output[index + 1].strip() != "": + new_out_lines.append("") + formatted_output = new_out_lines + + return formatted_output + + +def _with_from_imports( + parsed: parse.ParsedContent, + config: Dict[str, Any], + from_modules: Iterable[str], + section: str, + section_output: List[str], + ignore_case: bool, + remove_imports: List[str] +) -> List[str]: + new_section_output = section_output.copy() + for module in from_modules: + if module in remove_imports: + continue + + import_start = "from {} import ".format(module) + from_imports = list(parsed.imports[section]["from"][module]) + if not config["no_inline_sort"] or config["force_single_line"]: + from_imports = nsorted( + from_imports, + key=lambda key: sorting.module_key( + key, config, True, ignore_case, section_name=section + ), + ) + if remove_imports: + from_imports = [ + line + for line in from_imports + if not "{}.{}".format(module, line) in remove_imports + ] + + sub_modules = ["{}.{}".format(module, from_import) for from_import in from_imports] + as_imports = { + from_import: [ + "{} as {}".format(from_import, as_module) + for as_module in parsed.as_map[sub_module] + ] + for from_import, sub_module in zip(from_imports, sub_modules) + if sub_module in parsed.as_map + } + if config["combine_as_imports"] and not ( + "*" in from_imports and config["combine_star"] + ): + if not config["no_inline_sort"]: + for as_import in as_imports: + as_imports[as_import] = nsorted(as_imports[as_import]) + for from_import in copy.copy(from_imports): + if from_import in as_imports: + idx = from_imports.index(from_import) if ( - section_comment not in parsed.lines_without_imports[0:1] - and section_comment not in parsed.in_lines[0:1] + config["keep_direct_and_as_imports"] + and parsed.imports[section]["from"][module][from_import] ): - section_output.insert(0, section_comment) + from_imports[(idx + 1) : (idx + 1)] = as_imports.pop(from_import) + else: + from_imports[idx : (idx + 1)] = as_imports.pop(from_import) + + while from_imports: + comments = parsed.categorized_comments["from"].pop(module, ()) + if "*" in from_imports and config["combine_star"]: + import_statement = wrap.line( + output.with_comments( + comments, + "{}*".format(import_start), + removed=config["ignore_comments"], + comment_prefix=config["comment_prefix"], + ), + parsed.line_separator, + config, + ) + from_imports = None + elif config["force_single_line"]: + import_statement = None + while from_imports: + from_import = from_imports.pop(0) + single_import_line = output.with_comments( + comments, + import_start + from_import, + removed=config["ignore_comments"], + comment_prefix=config["comment_prefix"], + ) + comment = parsed.categorized_comments["nested"].get(module, {}).pop(from_import, None) + if comment: + single_import_line += "{} {}".format( + comments and ";" or config["comment_prefix"], comment + ) + if from_import in as_imports: + if ( + config["keep_direct_and_as_imports"] + and parsed.imports[section]["from"][module][from_import] + ): + new_section_output.append( + wrap.line(single_import_line, parsed.line_separator, config) + ) + from_comments = parsed.categorized_comments["straight"].get( + "{}.{}".format(module, from_import) + ) + new_section_output.extend( + output.with_comments( + from_comments, + wrap.line( + import_start + as_import, parsed.line_separator, config + ), + removed=config["ignore_comments"], + comment_prefix=config["comment_prefix"], + ) + for as_import in nsorted(as_imports[from_import]) + ) + else: + new_section_output.append( + wrap.line(single_import_line, parsed.line_separator, config) + ) + comments = None + else: + while from_imports and from_imports[0] in as_imports: + from_import = from_imports.pop(0) + as_imports[from_import] = nsorted(as_imports[from_import]) + from_comments = parsed.categorized_comments["straight"].get( + "{}.{}".format(module, from_import) + ) + above_comments = parsed.categorized_comments["above"]["from"].pop(module, None) + if above_comments: + if new_section_output and config.get("ensure_newline_before_comments"): + new_section_output.append("") + new_section_output.extend(above_comments) - if pending_lines_before or not no_lines_before: - output += [""] * config["lines_between_sections"] + if ( + config["keep_direct_and_as_imports"] + and parsed.imports[section]["from"][module][from_import] + ): + new_section_output.append( + output.with_comments( + from_comments, + wrap.line( + import_start + from_import, parsed.line_separator, config + ), + removed=config["ignore_comments"], + comment_prefix=config["comment_prefix"], + ) + ) + new_section_output.extend( + output.with_comments( + from_comments, + wrap.line( + import_start + as_import, parsed.line_separator, config + ), + removed=config["ignore_comments"], + comment_prefix=config["comment_prefix"], + ) + for as_import in as_imports[from_import] + ) - output += section_output + star_import = False + if "*" in from_imports: + new_section_output.append( + output.with_comments( + comments, + "{}*".format(import_start), + removed=config["ignore_comments"], + comment_prefix=config["comment_prefix"], + ) + ) + from_imports.remove("*") + star_import = True + comments = None - 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() == "": - output.pop(0) - - output_at = 0 - if parsed.import_index < parsed.original_line_count: - output_at = parsed.import_index - elif parsed.first_comment_index_end != -1 and parsed.first_comment_index_start <= 2: - output_at = parsed.first_comment_index_end - parsed.lines_without_imports[output_at:0] = output - - imports_tail = output_at + len(output) - while [ - character.strip() for character in parsed.lines_without_imports[imports_tail : imports_tail + 1] - ] == [""]: - parsed.lines_without_imports.pop(imports_tail) - - if len(parsed.lines_without_imports) > imports_tail: - next_construct = "" - _in_quote: str = "" - tail = parsed.lines_without_imports[imports_tail:] - - for index, line in enumerate(tail): - in_quote = _in_quote - ( - should_skip, - _in_quote, - _in_top_comment, - _first_comment_index_start, - self._first_comment_index_end, - ) = parse.skip_line( - line, - in_quote=self._in_quote, - in_top_comment=False, - index=len(parsed.lines_without_imports), - section_comments=self._section_comments, - first_comment_index_start=self._first_comment_index_start, - first_comment_index_end=self._first_comment_index_end, - ) - if not should_skip and line.strip(): + for from_import in copy.copy(from_imports): if ( - line.strip().startswith("#") - and len(tail) > (index + 1) - and tail[index + 1].strip() + from_import in as_imports + and not config["keep_direct_and_as_imports"] ): continue - next_construct = line - break - elif not in_quote: - parts = line.split() + comment = parsed.categorized_comments["nested"].get(module, {}).pop(from_import, None) + if comment: + single_import_line = output.with_comments( + comments, + import_start + from_import, + removed=config["ignore_comments"], + comment_prefix=config["comment_prefix"], + ) + single_import_line += "{} {}".format( + comments and ";" or config["comment_prefix"], comment + ) + above_comments = parsed.categorized_comments["above"]["from"].pop(module, None) + if above_comments: + if new_section_output and config.get( + "ensure_newline_before_comments" + ): + new_section_output.append("") + new_section_output.extend(above_comments) + new_section_output.append( + wrap.line(single_import_line, parsed.line_separator, config) + ) + from_imports.remove(from_import) + comments = None + + from_import_section = [] + while from_imports and ( + from_imports[0] not in as_imports + or ( + config["keep_direct_and_as_imports"] + and config["combine_as_imports"] + and parsed.imports[section]["from"][module][from_import] + ) + ): + from_import_section.append(from_imports.pop(0)) + if star_import: + import_statement = import_start + (", ").join(from_import_section) + else: + import_statement = output.with_comments( + comments, + import_start + (", ").join(from_import_section), + removed=config["ignore_comments"], + comment_prefix=config["comment_prefix"], + ) + if not from_import_section: + import_statement = "" + + do_multiline_reformat = False + + force_grid_wrap = config["force_grid_wrap"] + if force_grid_wrap and len(from_import_section) >= force_grid_wrap: + do_multiline_reformat = True + + if ( + len(import_statement) > config["line_length"] + and len(from_import_section) > 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) > config["line_length"] + and len(from_import_section) > 0 + and config["multi_line_output"] + not in ( + wrap.Modes.GRID, # type: ignore + wrap.Modes.VERTICAL, # type: ignore + ) + ): + do_multiline_reformat = True + + if do_multiline_reformat: + import_statement = wrap.import_statement( + import_start, + from_import_section, + comments, + config, + parsed.line_separator, + ) if ( - len(parts) >= 3 - and parts[1] == "=" - and "'" not in parts[0] - and '"' not in parts[0] - ): - next_construct = line - break - - if config["lines_after_imports"] != -1: - parsed.lines_without_imports[imports_tail:0] = [ - "" for line in range(config["lines_after_imports"]) - ] - elif self.extension != "pyi" and ( - next_construct.startswith("def ") - or next_construct.startswith("class ") - or next_construct.startswith("@") - or next_construct.startswith("async def") + config["multi_line_output"] == wrap.Modes.GRID # type: ignore + ): # type: ignore + config[ + "multi_line_output" + ] = wrap.Modes.VERTICAL_GRID # type: ignore + try: + other_import_statement = wrap.import_statement( + import_start, + from_import_section, + comments, + config, + parsed.line_separator, + ) + if ( + max(len(x) for x in import_statement.split("\n")) + > config["line_length"] + ): + import_statement = other_import_statement + finally: + config["multi_line_output"] = wrap.Modes.GRID # type: ignore + if ( + not do_multiline_reformat + and len(import_statement) > config["line_length"] + ): + import_statement = wrap.line( + import_statement, parsed.line_separator, config + ) + + if import_statement: + above_comments = parsed.categorized_comments["above"]["from"].pop(module, None) + if above_comments: + if new_section_output and config.get("ensure_newline_before_comments"): + new_section_output.append("") + new_section_output.extend(above_comments) + new_section_output.append(import_statement) + return new_section_output + + + +def _with_straight_imports( + parsed: parse.ParsedContent, config: Dict[str, Any], straight_modules: Iterable[str], section: str, section_output: List[str], remove_imports: List[str]=[] +) -> List[str]: + new_section_output = section_output.copy() + for module in straight_modules: + if module in remove_imports: + continue + + import_definition = [] + if module in parsed.as_map: + if ( + config["keep_direct_and_as_imports"] + and parsed.imports[section]["straight"][module] ): - parsed.lines_without_imports[imports_tail:0] = ["", ""] - else: - parsed.lines_without_imports[imports_tail:0] = [""] - - if parsed.place_imports: - new_out_lines = [] - for index, line in enumerate(parsed.lines_without_imports): - new_out_lines.append(line) - if line in parsed.import_placements: - new_out_lines.extend(parsed.place_imports[parsed.import_placements[line]]) - if len(parsed.lines_without_imports) <= index or parsed.lines_without_imports[index + 1].strip() != "": - new_out_lines.append("") - parsed.lines_without_imports = new_out_lines + import_definition.append("import {}".format(module)) + import_definition.extend( + "import {} as {}".format(module, as_import) for as_import in parsed.as_map[module] + ) + else: + import_definition.append("import {}".format(module)) + + comments_above = parsed.categorized_comments["above"]["straight"].pop(module, None) + if comments_above: + if new_section_output and config.get("ensure_newline_before_comments"): + new_section_output.append("") + new_section_output.extend(comments_above) + new_section_output.extend( + output.with_comments( + parsed.categorized_comments["straight"].get(module), + idef, + removed=config["ignore_comments"], + comment_prefix=config["comment_prefix"], + ) + for idef in import_definition + ) + + return new_section_output diff --git a/isort/sorting.py b/isort/sorting.py index d5e482464..7bfcbebb8 100644 --- a/isort/sorting.py +++ b/isort/sorting.py @@ -1,3 +1,6 @@ +import re +from typing import Mapping, Optional, Any + def module_key( module_name: str, From c353e2bf4e89226d0875f35824df498aad06ff53 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 27 Oct 2019 01:48:31 -0700 Subject: [PATCH 0140/1439] Move comment specific functions to separate module --- isort/comments.py | 28 +++++++ isort/isort.py | 3 +- isort/output.py | 179 +++++++++++++++++++------------------------- isort/parse.py | 20 ++--- isort/sorting.py | 2 +- isort/wrap_modes.py | 17 ++--- 6 files changed, 121 insertions(+), 128 deletions(-) create mode 100644 isort/comments.py diff --git a/isort/comments.py b/isort/comments.py new file mode 100644 index 000000000..92642864e --- /dev/null +++ b/isort/comments.py @@ -0,0 +1,28 @@ +from typing import List, Optional, Tuple + + +def parse(line: str) -> Tuple[str, str]: + """Parses import lines for comments and returns back the + import statement and the associated comment. + """ + comment_start = line.find("#") + if comment_start != -1: + return (line[:comment_start], line[comment_start + 1 :].strip()) + + return (line, "") + + +def add_to_line( + comments: Optional[List[str]], + original_string: str = "", + removed: bool = False, + comment_prefix: str = "", +) -> str: + """Returns a string with comments added if removed is not set.""" + if removed: + return parse(original_string)[0] + + if not comments: + return original_string + else: + return "{}{} {}".format(parse(original_string)[0], comment_prefix, "; ".join(comments)) diff --git a/isort/isort.py b/isort/isort.py index 911fbe3a2..54f60bf29 100644 --- a/isort/isort.py +++ b/isort/isort.py @@ -13,13 +13,12 @@ from isort import utils -from . import output, parse, settings, wrap, sorting, output +from . import output, parse, settings, sorting, wrap from .finders import FindersManager from .natural import nsorted class _SortImports: - def __init__(self, file_contents: str, config: Dict[str, Any], extension: str = "py") -> None: self.config = config self.extension = extension diff --git a/isort/output.py b/isort/output.py index 6bd781236..ef110547b 100644 --- a/isort/output.py +++ b/isort/output.py @@ -1,41 +1,28 @@ -from typing import List, Optional +import copy +import itertools import re +from typing import Any, Dict, Iterable, List, Optional -from . import parse, sorting from isort.format import format_simplified +from . import parse, sorting, wrap +from .comments import add_to_line as with_comments +from .natural import nsorted _import_line_intro_re = re.compile("^(?:from|import) ") _import_line_midline_import_re = re.compile(" import ") -def with_comments( - comments: Optional[List[str]], - original_string: str = "", - removed: bool = False, - comment_prefix: str = "", -) -> str: - """Returns a string with comments added if removed is not set.""" - if removed: - return parse.import_comment(original_string)[0] - - if not comments: - return original_string - else: - return "{}{} {}".format( - parse.import_comment(original_string)[0], comment_prefix, "; ".join(comments) - ) - -def with_formatted_imports(parsed: parse.ParsedContent, config: Dict[str, Any], extension: str="py") -> List[str]: +def with_formatted_imports( + parsed: parse.ParsedContent, config: Dict[str, Any], extension: str = "py" +) -> List[str]: """Adds the imports back to the file. (at the index of the first import) sorted alphabetically and split between groups """ formatted_output: List[str] = parsed.lines_without_imports.copy() - remove_imports = [ - format_simplified(removal) for removal in config["remove_imports"] - ] + remove_imports = [format_simplified(removal) for removal in config["remove_imports"]] sort_ignore_case = config["force_alphabetical_sort_within_sections"] sections: Iterable[str] = itertools.chain(parsed.sections, config["forced_separate"]) @@ -54,13 +41,11 @@ def with_formatted_imports(parsed: parse.ParsedContent, config: Dict[str, Any], for section in sections: straight_modules = parsed.imports[section]["straight"] straight_modules = nsorted( - straight_modules, - key=lambda key: sorting.module_key(key, config, section_name=section), + straight_modules, key=lambda key: sorting.module_key(key, config, section_name=section) ) from_modules = parsed.imports[section]["from"] from_modules = nsorted( - from_modules, - key=lambda key: sorting.module_key(key, config, section_name=section), + from_modules, key=lambda key: sorting.module_key(key, config, section_name=section) ) if config["force_sort_within_sections"]: @@ -68,24 +53,42 @@ def with_formatted_imports(parsed: parse.ParsedContent, config: Dict[str, Any], section_output: List[str] = [] if config["from_first"]: - section_output = _with_from_imports(parsed, config, from_modules, section, section_output, sort_ignore_case, remove_imports) + section_output = _with_from_imports( + parsed, + config, + from_modules, + section, + section_output, + sort_ignore_case, + remove_imports, + ) if config["lines_between_types"] and from_modules and straight_modules: section_output.extend([""] * config["lines_between_types"]) - section_output = _with_straight_imports(parsed, config, straight_modules, section, section_output, remove_imports) + section_output = _with_straight_imports( + parsed, config, straight_modules, section, section_output, remove_imports + ) else: - section_output = _with_straight_imports(parsed, config, straight_modules, section, section_output, remove_imports) + section_output = _with_straight_imports( + parsed, config, straight_modules, section, section_output, remove_imports + ) if config["lines_between_types"] and from_modules and straight_modules: section_output.extend([""] * config["lines_between_types"]) - section_output = _with_from_imports(parsed, config, from_modules, section, section_output, sort_ignore_case, remove_imports) + section_output = _with_from_imports( + parsed, + config, + from_modules, + section, + section_output, + sort_ignore_case, + remove_imports, + ) if config["force_sort_within_sections"]: def by_module(line: str) -> str: section = "B" - line = _import_line_intro_re.sub( - "", _import_line_midline_import_re.sub(".", line) - ) + line = _import_line_intro_re.sub("", _import_line_midline_import_re.sub(".", line)) if line.split(" ")[0] in config["force_to_top"]: section = "A" if not config["order_by_type"]: @@ -162,7 +165,7 @@ def by_module(line: str) -> str: for index, line in enumerate(tail): in_quote = _in_quote - should_skip,_in_quote, *_ = parse.skip_line( + should_skip, _in_quote, *_ = parse.skip_line( line, in_quote=_in_quote, in_top_comment=False, @@ -192,9 +195,7 @@ def by_module(line: str) -> str: break if config["lines_after_imports"] != -1: - formatted_output[imports_tail:0] = [ - "" for line in range(config["lines_after_imports"]) - ] + formatted_output[imports_tail:0] = ["" for line in range(config["lines_after_imports"])] elif extension != "pyi" and ( next_construct.startswith("def ") or next_construct.startswith("class ") @@ -225,7 +226,7 @@ def _with_from_imports( section: str, section_output: List[str], ignore_case: bool, - remove_imports: List[str] + remove_imports: List[str], ) -> List[str]: new_section_output = section_output.copy() for module in from_modules: @@ -243,23 +244,18 @@ def _with_from_imports( ) if remove_imports: from_imports = [ - line - for line in from_imports - if not "{}.{}".format(module, line) in remove_imports + line for line in from_imports if not "{}.{}".format(module, line) in remove_imports ] sub_modules = ["{}.{}".format(module, from_import) for from_import in from_imports] as_imports = { from_import: [ - "{} as {}".format(from_import, as_module) - for as_module in parsed.as_map[sub_module] + "{} as {}".format(from_import, as_module) for as_module in parsed.as_map[sub_module] ] for from_import, sub_module in zip(from_imports, sub_modules) if sub_module in parsed.as_map } - if config["combine_as_imports"] and not ( - "*" in from_imports and config["combine_star"] - ): + if config["combine_as_imports"] and not ("*" in from_imports and config["combine_star"]): if not config["no_inline_sort"]: for as_import in as_imports: as_imports[as_import] = nsorted(as_imports[as_import]) @@ -278,7 +274,7 @@ def _with_from_imports( comments = parsed.categorized_comments["from"].pop(module, ()) if "*" in from_imports and config["combine_star"]: import_statement = wrap.line( - output.with_comments( + with_comments( comments, "{}*".format(import_start), removed=config["ignore_comments"], @@ -292,13 +288,15 @@ def _with_from_imports( import_statement = None while from_imports: from_import = from_imports.pop(0) - single_import_line = output.with_comments( + single_import_line = with_comments( comments, import_start + from_import, removed=config["ignore_comments"], comment_prefix=config["comment_prefix"], ) - comment = parsed.categorized_comments["nested"].get(module, {}).pop(from_import, None) + comment = ( + parsed.categorized_comments["nested"].get(module, {}).pop(from_import, None) + ) if comment: single_import_line += "{} {}".format( comments and ";" or config["comment_prefix"], comment @@ -315,11 +313,9 @@ def _with_from_imports( "{}.{}".format(module, from_import) ) new_section_output.extend( - output.with_comments( + with_comments( from_comments, - wrap.line( - import_start + as_import, parsed.line_separator, config - ), + wrap.line(import_start + as_import, parsed.line_separator, config), removed=config["ignore_comments"], comment_prefix=config["comment_prefix"], ) @@ -348,7 +344,7 @@ def _with_from_imports( and parsed.imports[section]["from"][module][from_import] ): new_section_output.append( - output.with_comments( + with_comments( from_comments, wrap.line( import_start + from_import, parsed.line_separator, config @@ -358,11 +354,9 @@ def _with_from_imports( ) ) new_section_output.extend( - output.with_comments( + with_comments( from_comments, - wrap.line( - import_start + as_import, parsed.line_separator, config - ), + wrap.line(import_start + as_import, parsed.line_separator, config), removed=config["ignore_comments"], comment_prefix=config["comment_prefix"], ) @@ -372,7 +366,7 @@ def _with_from_imports( star_import = False if "*" in from_imports: new_section_output.append( - output.with_comments( + with_comments( comments, "{}*".format(import_start), removed=config["ignore_comments"], @@ -384,14 +378,13 @@ def _with_from_imports( comments = None for from_import in copy.copy(from_imports): - if ( - from_import in as_imports - and not config["keep_direct_and_as_imports"] - ): + if from_import in as_imports and not config["keep_direct_and_as_imports"]: continue - comment = parsed.categorized_comments["nested"].get(module, {}).pop(from_import, None) + comment = ( + parsed.categorized_comments["nested"].get(module, {}).pop(from_import, None) + ) if comment: - single_import_line = output.with_comments( + single_import_line = with_comments( comments, import_start + from_import, removed=config["ignore_comments"], @@ -400,11 +393,11 @@ def _with_from_imports( single_import_line += "{} {}".format( comments and ";" or config["comment_prefix"], comment ) - above_comments = parsed.categorized_comments["above"]["from"].pop(module, None) + above_comments = parsed.categorized_comments["above"]["from"].pop( + module, None + ) if above_comments: - if new_section_output and config.get( - "ensure_newline_before_comments" - ): + if new_section_output and config.get("ensure_newline_before_comments"): new_section_output.append("") new_section_output.extend(above_comments) new_section_output.append( @@ -426,7 +419,7 @@ def _with_from_imports( if star_import: import_statement = import_start + (", ").join(from_import_section) else: - import_statement = output.with_comments( + import_statement = with_comments( comments, import_start + (", ").join(from_import_section), removed=config["ignore_comments"], @@ -441,10 +434,7 @@ def _with_from_imports( if force_grid_wrap and len(from_import_section) >= force_grid_wrap: do_multiline_reformat = True - if ( - len(import_statement) > config["line_length"] - and len(from_import_section) > 1 - ): + if len(import_statement) > config["line_length"] and len(from_import_section) > 1: do_multiline_reformat = True # If line too long AND have imports AND we are @@ -453,27 +443,18 @@ def _with_from_imports( len(import_statement) > config["line_length"] and len(from_import_section) > 0 and config["multi_line_output"] - not in ( - wrap.Modes.GRID, # type: ignore - wrap.Modes.VERTICAL, # type: ignore - ) + not in (wrap.Modes.GRID, wrap.Modes.VERTICAL) # type: ignore # type: ignore ): do_multiline_reformat = True if do_multiline_reformat: import_statement = wrap.import_statement( - import_start, - from_import_section, - comments, - config, - parsed.line_separator, + import_start, from_import_section, comments, config, parsed.line_separator ) if ( config["multi_line_output"] == wrap.Modes.GRID # type: ignore ): # type: ignore - config[ - "multi_line_output" - ] = wrap.Modes.VERTICAL_GRID # type: ignore + config["multi_line_output"] = wrap.Modes.VERTICAL_GRID # type: ignore try: other_import_statement = wrap.import_statement( import_start, @@ -489,13 +470,8 @@ def _with_from_imports( import_statement = other_import_statement finally: config["multi_line_output"] = wrap.Modes.GRID # type: ignore - if ( - not do_multiline_reformat - and len(import_statement) > config["line_length"] - ): - import_statement = wrap.line( - import_statement, parsed.line_separator, config - ) + if not do_multiline_reformat and len(import_statement) > config["line_length"]: + import_statement = wrap.line(import_statement, parsed.line_separator, config) if import_statement: above_comments = parsed.categorized_comments["above"]["from"].pop(module, None) @@ -507,9 +483,13 @@ def _with_from_imports( return new_section_output - def _with_straight_imports( - parsed: parse.ParsedContent, config: Dict[str, Any], straight_modules: Iterable[str], section: str, section_output: List[str], remove_imports: List[str]=[] + parsed: parse.ParsedContent, + config: Dict[str, Any], + straight_modules: Iterable[str], + section: str, + section_output: List[str], + remove_imports: List[str] = [], ) -> List[str]: new_section_output = section_output.copy() for module in straight_modules: @@ -518,10 +498,7 @@ def _with_straight_imports( import_definition = [] if module in parsed.as_map: - if ( - config["keep_direct_and_as_imports"] - and parsed.imports[section]["straight"][module] - ): + if config["keep_direct_and_as_imports"] and parsed.imports[section]["straight"][module]: import_definition.append("import {}".format(module)) import_definition.extend( "import {} as {}".format(module, as_import) for as_import in parsed.as_map[module] @@ -535,7 +512,7 @@ def _with_straight_imports( new_section_output.append("") new_section_output.extend(comments_above) new_section_output.extend( - output.with_comments( + with_comments( parsed.categorized_comments["straight"].get(module), idef, removed=config["ignore_comments"], diff --git a/isort/parse.py b/isort/parse.py index 90614efd4..3c88524b2 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -6,6 +6,7 @@ from isort.format import format_natural +from .comments import parse as parse_comments from .finders import FindersManager if TYPE_CHECKING: @@ -35,17 +36,6 @@ def _infer_line_separator(file_contents: str) -> str: return "\n" -def import_comment(line: str) -> Tuple[str, str]: - """Parses import lines for comments and returns back the - import statement and the associated comment. - """ - comment_start = line.find("#") - if comment_start != -1: - return (line[:comment_start], line[comment_start + 1 :].strip()) - - return (line, "") - - def import_type(line: str) -> Optional[str]: """If the current line is an import line it will return its type (from or straight)""" if "isort:skip" in line or "NOQA" in line: @@ -239,7 +229,7 @@ def file_contents(contents: str, config: Dict[str, Any]) -> ParsedContent: if import_index == -1: import_index = index - 1 nested_comments = {} - import_string, comment = import_comment(line) + import_string, comment = parse_comments(line) comments = [comment] if comment else [] line_parts = [part for part in _strip_syntax(import_string).strip().split(" ") if part] if ( @@ -252,7 +242,7 @@ def file_contents(contents: str, config: Dict[str, Any]) -> ParsedContent: if "(" in line.split("#")[0] and index < line_count: while not line.strip().endswith(")") and index < line_count: - line, new_comment = import_comment(in_lines[index]) + line, new_comment = parse_comments(in_lines[index]) index += 1 if new_comment: comments.append(new_comment) @@ -267,7 +257,7 @@ def file_contents(contents: str, config: Dict[str, Any]) -> ParsedContent: import_string += line_separator + line else: while line.strip().endswith("\\"): - line, new_comment = import_comment(in_lines[index]) + line, new_comment = parse_comments(in_lines[index]) index += 1 if new_comment: comments.append(new_comment) @@ -289,7 +279,7 @@ def file_contents(contents: str, config: Dict[str, Any]) -> ParsedContent: import_string += line_separator + line while not line.strip().endswith(")") and index < line_count: - line, new_comment = import_comment(in_lines[index]) + line, new_comment = parse_comments(in_lines[index]) index += 1 if new_comment: comments.append(new_comment) diff --git a/isort/sorting.py b/isort/sorting.py index 7bfcbebb8..f5523dc7f 100644 --- a/isort/sorting.py +++ b/isort/sorting.py @@ -1,5 +1,5 @@ import re -from typing import Mapping, Optional, Any +from typing import Any, Mapping, Optional def module_key( diff --git a/isort/wrap_modes.py b/isort/wrap_modes.py index 4a447b77f..6aa4042e0 100644 --- a/isort/wrap_modes.py +++ b/isort/wrap_modes.py @@ -3,8 +3,7 @@ from inspect import signature from typing import Any, Callable, Dict, List, Sequence -from . import settings -from .output import with_comments +from . import comments, settings _wrap_modes: Dict[str, Callable[[Any], str]] = {} @@ -51,7 +50,7 @@ def grid(**interface): interface["statement"] += "(" + interface["imports"].pop(0) while interface["imports"]: next_import = interface["imports"].pop(0) - next_statement = with_comments( + next_statement = comments.add_to_line( interface["comments"], interface["statement"] + ", " + next_import, removed=interface["remove_comments"], @@ -69,7 +68,7 @@ def grid(**interface): else: lines[-1] = new_line next_import = interface["line_separator"].join(lines) - interface["statement"] = with_comments( + interface["statement"] = comments.add_to_line( interface["comments"], "{},".format(interface["statement"]), removed=interface["remove_comments"], @@ -87,7 +86,7 @@ def vertical(**interface): return "" first_import = ( - with_comments( + comments.add_to_line( interface["comments"], interface["imports"].pop(0) + ",", removed=interface["remove_comments"], @@ -112,7 +111,7 @@ def hanging_indent(**interface): interface["statement"] += interface["imports"].pop(0) while interface["imports"]: next_import = interface["imports"].pop(0) - next_statement = with_comments( + next_statement = comments.add_to_line( interface["comments"], interface["statement"] + ", " + next_import, removed=interface["remove_comments"], @@ -122,7 +121,7 @@ def hanging_indent(**interface): len(next_statement.split(interface["line_separator"])[-1]) + 3 > interface["line_length"] ): - next_statement = with_comments( + next_statement = comments.add_to_line( interface["comments"], "{}, \\".format(interface["statement"]), removed=interface["remove_comments"], @@ -137,7 +136,7 @@ def hanging_indent(**interface): def vertical_hanging_indent(**interface): return "{0}({1}{2}{3}{4}{5}{2})".format( interface["statement"], - with_comments( + comments.add_to_line( interface["comments"], "", removed=interface["remove_comments"], @@ -155,7 +154,7 @@ def vertical_grid_common(need_trailing_char: bool, **interface): return "" interface["statement"] += ( - with_comments( + comments.add_to_line( interface["comments"], "(", removed=interface["remove_comments"], From 0764beeb6b339210622b80c2412d88f6eeb1e3e6 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 27 Oct 2019 02:02:46 -0700 Subject: [PATCH 0141/1439] Working output refactoring --- isort/output.py | 6 +++--- tests/test_comments.py | 6 ++++++ tests/test_parse.py | 14 +++++--------- 3 files changed, 14 insertions(+), 12 deletions(-) create mode 100644 tests/test_comments.py diff --git a/isort/output.py b/isort/output.py index ef110547b..95b8e8abb 100644 --- a/isort/output.py +++ b/isort/output.py @@ -283,9 +283,9 @@ def _with_from_imports( parsed.line_separator, config, ) - from_imports = None + from_imports = [] elif config["force_single_line"]: - import_statement = None + import_statement = "" while from_imports: from_import = from_imports.pop(0) single_import_line = with_comments( @@ -489,7 +489,7 @@ def _with_straight_imports( straight_modules: Iterable[str], section: str, section_output: List[str], - remove_imports: List[str] = [], + remove_imports: List[str], ) -> List[str]: new_section_output = section_output.copy() for module in straight_modules: diff --git a/tests/test_comments.py b/tests/test_comments.py new file mode 100644 index 000000000..b1b4ed7b4 --- /dev/null +++ b/tests/test_comments.py @@ -0,0 +1,6 @@ +from hypothesis_auto import auto_pytest_magic + +from isort import comments + +auto_pytest_magic(comments.parse) +auto_pytest_magic(comments.add_to_line) diff --git a/tests/test_parse.py b/tests/test_parse.py index fa98bcad2..ffef6e73e 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -1,4 +1,4 @@ -import sys +from hypothesis_auto import auto_pytest_magic from isort import parse from isort.settings import DEFAULT_SECTIONS, default @@ -38,11 +38,7 @@ def test_file_contents(): assert original_line_count == len(in_lines) -if sys.version_info[1] > 5: - from hypothesis_auto import auto_pytest_magic - - auto_pytest_magic(parse.import_comment) - auto_pytest_magic(parse.import_type) - auto_pytest_magic(parse.skip_line) - auto_pytest_magic(parse._strip_syntax) - auto_pytest_magic(parse._infer_line_separator) +auto_pytest_magic(parse.import_type) +auto_pytest_magic(parse.skip_line) +auto_pytest_magic(parse._strip_syntax) +auto_pytest_magic(parse._infer_line_separator) From e8ad88e41f6c79ed2ef7ec12b7bc017cd147f1f6 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 27 Oct 2019 02:19:55 -0700 Subject: [PATCH 0142/1439] Simplify _SortImports, reducing attributes to minimum, relying instead on ParsedContent namedtuple --- isort/compat.py | 4 ++-- isort/isort.py | 37 ++++++++++++------------------------- isort/output.py | 2 +- 3 files changed, 15 insertions(+), 28 deletions(-) diff --git a/isort/compat.py b/isort/compat.py index 07d748202..ccc71c11b 100644 --- a/isort/compat.py +++ b/isort/compat.py @@ -218,8 +218,8 @@ def __init__( @property def sections(self): - return self.sorted_imports.sections + return self.sorted_imports.parsed.sections @property def length_change(self) -> int: - return self.sorted_imports.length_change + return self.sorted_imports.parsed.change_count diff --git a/isort/isort.py b/isort/isort.py index 54f60bf29..3cd76819a 100644 --- a/isort/isort.py +++ b/isort/isort.py @@ -21,43 +21,30 @@ class _SortImports: def __init__(self, file_contents: str, config: Dict[str, Any], extension: str = "py") -> None: self.config = config - self.extension = extension - parsed = parse.file_contents(file_contents, config=self.config) - ( - self.in_lines, - self.out_lines, - self.import_index, - self.place_imports, - self.import_placements, - self.as_map, - self.imports, - self.comments, - self._first_comment_index_start, - self._first_comment_index_end, - self.length_change, - self.original_num_of_lines, - self.line_separator, - self.sections, - self._section_comments, - ) = parsed + self.parsed = parse.file_contents(file_contents, config=self.config) + if self.parsed.import_index != -1: + self.out_lines = output.sorted_imports(self.parsed, self.config, extension) + else: + self.out_lines = self.parsed.lines_without_imports - if self.import_index != -1: - self.out_lines = output.with_formatted_imports(parsed, self.config, self.extension) while self.out_lines and self.out_lines[-1].strip() == "": self.out_lines.pop(-1) + self.out_lines.append("") - self.output = self.line_separator.join(self.out_lines) + self.output = self.parsed.line_separator.join(self.out_lines) def remove_whitespaces(self, contents: str) -> str: - contents = contents.replace(self.line_separator, "").replace(" ", "").replace("\x0c", "") + contents = ( + contents.replace(self.parsed.line_separator, "").replace(" ", "").replace("\x0c", "") + ) return contents def get_out_lines_without_top_comment(self) -> str: - return self._strip_top_comments(self.out_lines, self.line_separator) + return self._strip_top_comments(self.out_lines, self.parsed.line_separator) def get_in_lines_without_top_comment(self) -> str: - return self._strip_top_comments(self.in_lines, self.line_separator) + return self._strip_top_comments(self.parsed.in_lines, self.parsed.line_separator) def check_if_input_already_sorted( self, output: str, check_against: str, *, logging_file_path: str diff --git a/isort/output.py b/isort/output.py index 95b8e8abb..83d8efc42 100644 --- a/isort/output.py +++ b/isort/output.py @@ -13,7 +13,7 @@ _import_line_midline_import_re = re.compile(" import ") -def with_formatted_imports( +def sorted_imports( parsed: parse.ParsedContent, config: Dict[str, Any], extension: str = "py" ) -> List[str]: """Adds the imports back to the file. From e782b36a80c58d240c62d405969c359fe6da1906 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 27 Oct 2019 03:03:31 -0700 Subject: [PATCH 0143/1439] Move all sorting functionality to sorting module --- isort/isort.py | 1 - isort/natural.py | 34 ---------------------------------- isort/output.py | 38 +++++++++++++++----------------------- isort/sorting.py | 36 +++++++++++++++++++++++++++++++++++- 4 files changed, 50 insertions(+), 59 deletions(-) delete mode 100644 isort/natural.py diff --git a/isort/isort.py b/isort/isort.py index 3cd76819a..f8e434cb9 100644 --- a/isort/isort.py +++ b/isort/isort.py @@ -15,7 +15,6 @@ from . import output, parse, settings, sorting, wrap from .finders import FindersManager -from .natural import nsorted class _SortImports: diff --git a/isort/natural.py b/isort/natural.py deleted file mode 100644 index 7e526f9bf..000000000 --- a/isort/natural.py +++ /dev/null @@ -1,34 +0,0 @@ -"""isort/natural.py. - -Enables sorting strings that contain numbers naturally - -usage: - natural.nsorted(list) - -Copyright (C) 2013 Timothy Edmund Crosley - -Implementation originally from @HappyLeapSecond stack overflow user in response to: - https://stackoverflow.com/questions/5967500/how-to-correctly-sort-a-string-with-a-number-inside -""" -import re -from typing import Any, Callable, Iterable, List, Optional - - -def _atoi(text: str) -> Any: - return int(text) if text.isdigit() else text - - -def _natural_keys(text: str) -> List[Any]: - return [_atoi(c) for c in re.split(r"(\d+)", text)] - - -def nsorted(to_sort: Iterable[str], key: Optional[Callable[[str], Any]] = None) -> List[str]: - """Returns a naturally sorted list""" - if key is None: - key_callback = _natural_keys - else: - - def key_callback(text: str) -> List[Any]: - return _natural_keys(key(text)) # type: ignore - - return sorted(to_sort, key=key_callback) diff --git a/isort/output.py b/isort/output.py index 83d8efc42..f7753e4fa 100644 --- a/isort/output.py +++ b/isort/output.py @@ -1,16 +1,12 @@ import copy import itertools -import re +from functools import partial from typing import Any, Dict, Iterable, List, Optional from isort.format import format_simplified from . import parse, sorting, wrap from .comments import add_to_line as with_comments -from .natural import nsorted - -_import_line_intro_re = re.compile("^(?:from|import) ") -_import_line_midline_import_re = re.compile(" import ") def sorted_imports( @@ -40,11 +36,11 @@ def sorted_imports( pending_lines_before = False for section in sections: straight_modules = parsed.imports[section]["straight"] - straight_modules = nsorted( + straight_modules = sorting.naturally( straight_modules, key=lambda key: sorting.module_key(key, config, section_name=section) ) from_modules = parsed.imports[section]["from"] - from_modules = nsorted( + from_modules = sorting.naturally( from_modules, key=lambda key: sorting.module_key(key, config, section_name=section) ) @@ -84,21 +80,17 @@ def sorted_imports( ) if config["force_sort_within_sections"]: - - def by_module(line: str) -> str: - section = "B" - - line = _import_line_intro_re.sub("", _import_line_midline_import_re.sub(".", line)) - if line.split(" ")[0] in config["force_to_top"]: - section = "A" - if not config["order_by_type"]: - line = line.lower() - return "{}{}".format(section, line) - # Remove comments section_output = [line for line in section_output if not line.startswith("#")] - section_output = nsorted(section_output, key=by_module) + section_output = sorting.naturally( + section_output, + key=partial( + sorting.section_key, + order_by_type=config["order_by_type"], + force_to_top=config["force_to_top"], + ), + ) # Add comments back all_comments = copied_comments["above"]["from"] @@ -236,7 +228,7 @@ def _with_from_imports( import_start = "from {} import ".format(module) from_imports = list(parsed.imports[section]["from"][module]) if not config["no_inline_sort"] or config["force_single_line"]: - from_imports = nsorted( + from_imports = sorting.naturally( from_imports, key=lambda key: sorting.module_key( key, config, True, ignore_case, section_name=section @@ -258,7 +250,7 @@ def _with_from_imports( if config["combine_as_imports"] and not ("*" in from_imports and config["combine_star"]): if not config["no_inline_sort"]: for as_import in as_imports: - as_imports[as_import] = nsorted(as_imports[as_import]) + as_imports[as_import] = sorting.naturally(as_imports[as_import]) for from_import in copy.copy(from_imports): if from_import in as_imports: idx = from_imports.index(from_import) @@ -319,7 +311,7 @@ def _with_from_imports( removed=config["ignore_comments"], comment_prefix=config["comment_prefix"], ) - for as_import in nsorted(as_imports[from_import]) + for as_import in sorting.naturally(as_imports[from_import]) ) else: new_section_output.append( @@ -329,7 +321,7 @@ def _with_from_imports( else: while from_imports and from_imports[0] in as_imports: from_import = from_imports.pop(0) - as_imports[from_import] = nsorted(as_imports[from_import]) + as_imports[from_import] = sorting.naturally(as_imports[from_import]) from_comments = parsed.categorized_comments["straight"].get( "{}.{}".format(module, from_import) ) diff --git a/isort/sorting.py b/isort/sorting.py index f5523dc7f..b76527bc9 100644 --- a/isort/sorting.py +++ b/isort/sorting.py @@ -1,5 +1,8 @@ import re -from typing import Any, Mapping, Optional +from typing import Any, Callable, Iterable, List, Mapping, Optional + +_import_line_intro_re = re.compile("^(?:from|import) ") +_import_line_midline_import_re = re.compile(" import ") def module_key( @@ -38,3 +41,34 @@ def module_key( prefix, length_sort and (str(len(module_name)) + ":" + module_name) or module_name, ) + + +def section_key(line: str, order_by_type: bool, force_to_top: List[str]) -> str: + section = "B" + + line = _import_line_intro_re.sub("", _import_line_midline_import_re.sub(".", line)) + if line.split(" ")[0] in force_to_top: + section = "A" + if not order_by_type: + line = line.lower() + return "{}{}".format(section, line) + + +def naturally(to_sort: Iterable[str], key: Optional[Callable[[str], Any]] = None) -> List[str]: + """Returns a naturally sorted list""" + if key is None: + key_callback = _natural_keys + else: + + def key_callback(text: str) -> List[Any]: + return _natural_keys(key(text)) # type: ignore + + return sorted(to_sort, key=key_callback) + + +def _atoi(text: str) -> Any: + return int(text) if text.isdigit() else text + + +def _natural_keys(text: str) -> List[Any]: + return [_atoi(c) for c in re.split(r"(\d+)", text)] From 03a3dd7b196e696ae60bb2cdf9eeb57fa3b9b863 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 27 Oct 2019 23:39:46 -0700 Subject: [PATCH 0144/1439] Switch from .format to f-string; switch from print to warnings where applicable --- isort/comments.py | 2 +- isort/compat.py | 26 ++++++------- isort/finders.py | 25 ++++++------ isort/format.py | 6 +-- isort/isort.py | 4 +- isort/logo.py | 16 +++----- isort/main.py | 23 ++++++----- isort/output.py | 32 ++++++++-------- isort/parse.py | 20 +++------- isort/settings.py | 7 ++-- isort/sorting.py | 10 ++--- isort/wrap.py | 36 ++++++++---------- isort/wrap_modes.py | 90 +++++++++++++++++++++++--------------------- scripts/lint.sh | 2 +- scripts/mkstdlibs.py | 10 ++--- tests/test_isort.py | 8 ++-- 16 files changed, 149 insertions(+), 168 deletions(-) diff --git a/isort/comments.py b/isort/comments.py index 92642864e..d033804de 100644 --- a/isort/comments.py +++ b/isort/comments.py @@ -25,4 +25,4 @@ def add_to_line( if not comments: return original_string else: - return "{}{} {}".format(parse(original_string)[0], comment_prefix, "; ".join(comments)) + return f"{parse(original_string)[0]}{comment_prefix} {'; '.join(comments)}" diff --git a/isort/compat.py b/isort/compat.py index ccc71c11b..a85dc66b2 100644 --- a/isort/compat.py +++ b/isort/compat.py @@ -4,6 +4,7 @@ import sys from pathlib import Path from typing import Any, Optional, Tuple +from warnings import warn from isort import settings from isort.format import ask_whether_to_apply_changes_to_file, show_unified_diff @@ -78,7 +79,7 @@ def __init__( run_path: str = "", check_skip: bool = True, extension: Optional[str] = None, - **setting_overrides: Any + **setting_overrides: Any, ): file_path = None if file_path is None else Path(file_path) file_name = None @@ -107,9 +108,9 @@ def __init__( if settings.file_should_be_skipped(file_name, self.config, run_path): self.skipped = True if self.config["verbose"]: - print( - "WARNING: {} was skipped as it's listed in 'skip' setting" - " or matches a glob in 'skip_glob' setting".format(absolute_file_path) + warn( + f"{absolute_file_path} was skipped as it's listed in 'skip' setting" + " or matches a glob in 'skip_glob' setting" ) file_contents = None @@ -127,11 +128,10 @@ def __init__( if used_encoding is 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( - str(absolute_file_path), file_encoding, fallback_encoding - ) + warn( + f"{absolute_file_path} was skipped as it couldn't be opened with the " + f"given {file_encoding} encoding or {fallback_encoding} fallback " + "encoding" ) else: file_encoding = used_encoding @@ -166,11 +166,11 @@ def __init__( ) compile(in_lines_without_top_comment, logging_file_path, "exec", 0, 1) print( - "ERROR: {} isort would have introduced syntax errors, " - "please report to the project!".format(logging_file_path) + f"ERROR: {logging_file_path} isort would have introduced syntax errors, " + "please report to the project!" ) except SyntaxError: - print("ERROR: {} File contains syntax errors.".format(logging_file_path)) + print(f"ERROR: {logging_file_path} File contains syntax errors.") return @@ -212,7 +212,7 @@ def __init__( with self.file_path.open("w", encoding=file_encoding, newline="") as output_file: if not self.config["quiet"]: - print("Fixing {}".format(self.file_path)) + print(f"Fixing {self.file_path}") output_file.write(self.output) diff --git a/isort/finders.py b/isort/finders.py index b8e6c8f6f..7aa53052d 100644 --- a/isort/finders.py +++ b/isort/finders.py @@ -86,7 +86,7 @@ def __init__(self, config: Mapping[str, Any], sections: Any) -> None: self.known_patterns: List[Tuple[Pattern[str], str]] = [] for placement in reversed(self.sections): known_placement = KNOWN_SECTION_MAPPING.get(placement, placement) - config_key = "known_{}".format(known_placement.lower()) + config_key = f"known_{known_placement.lower()}" known_patterns = self.config.get(config_key, []) known_patterns = [ pattern @@ -129,7 +129,7 @@ def __init__(self, config: Mapping[str, Any], sections: Any) -> None: # restore the original import path (i.e. not the path to bin/isort) root_dir = os.getcwd() - src_dir = "{0}/src".format(root_dir) + src_dir = f"{root_dir}/src" self.paths = [root_dir, src_dir] # virtual env @@ -138,14 +138,14 @@ def __init__(self, config: Mapping[str, Any], sections: Any) -> None: self.virtual_env = os.path.realpath(self.virtual_env) self.virtual_env_src = "" if self.virtual_env: - self.virtual_env_src = "{}/src/".format(self.virtual_env) - for path in glob("{}/lib/python*/site-packages".format(self.virtual_env)): + self.virtual_env_src = f"{self.virtual_env}/src/" + for path in glob(f"{self.virtual_env}/lib/python*/site-packages"): if path not in self.paths: self.paths.append(path) - for path in glob("{}/lib/python*/*/site-packages".format(self.virtual_env)): + for path in glob(f"{self.virtual_env}/lib/python*/*/site-packages"): if path not in self.paths: self.paths.append(path) - for path in glob("{}/src/*".format(self.virtual_env)): + for path in glob(f"{self.virtual_env}/src/*"): if os.path.isdir(path): self.paths.append(path) @@ -153,10 +153,10 @@ def __init__(self, config: Mapping[str, Any], sections: Any) -> None: self.conda_env = self.config.get("conda_env") or os.environ.get("CONDA_PREFIX") or "" if self.conda_env: self.conda_env = os.path.realpath(self.conda_env) - for path in glob("{}/lib/python*/site-packages".format(self.conda_env)): + for path in glob(f"{self.conda_env}/lib/python*/site-packages"): if path not in self.paths: self.paths.append(path) - for path in glob("{}/lib/python*/*/site-packages".format(self.conda_env)): + for path in glob(f"{self.conda_env}/lib/python*/*/site-packages"): if path not in self.paths: self.paths.append(path) @@ -396,9 +396,9 @@ def __init__( if self.verbose: print( ( - "{} encountered an error ({}) during " + f"{finder_cls.__name__} encountered an error ({exception}) during " "instantiation and cannot be used" - ).format(finder_cls.__name__, str(exception)) + ) ) self.finders: Tuple[BaseFinder, ...] = tuple(finders) @@ -411,9 +411,8 @@ def find(self, module_name: str) -> Optional[str]: # import section even if one approach fails if self.verbose: print( - ( - "{} encountered an error ({}) while trying to identify the {}" " module" - ).format(finder.__class__.__name__, str(exception), module_name) + f"{finder.__class__.__name__} encountered an error ({exception}) while " + f"trying to identify the {module_name} module" ) if section is not None: return section diff --git a/isort/format.py b/isort/format.py index 8dd7a5eef..80a215e42 100644 --- a/isort/format.py +++ b/isort/format.py @@ -20,10 +20,10 @@ def format_natural(import_line: str) -> str: import_line = import_line.strip() if not import_line.startswith("from ") and not import_line.startswith("import "): if "." not in import_line: - return "import {}".format(import_line) + return f"import {import_line}" parts = import_line.split(".") end = parts.pop(-1) - return "from {} import {}".format(".".join(parts), end) + return f"from {'.'.join(parts)} import {end}" return import_line @@ -49,7 +49,7 @@ def show_unified_diff(*, file_input: str, file_output: str, file_path: Optional[ def ask_whether_to_apply_changes_to_file(file_path: str) -> bool: answer = None while answer not in ("yes", "y", "no", "n", "quit", "q"): - answer = input("Apply suggested changes to '{}' [y/n/q]? ".format(file_path)) # nosec + answer = input(f"Apply suggested changes to '{file_path}' [y/n/q]? ") # nosec answer = answer.lower() if answer in ("no", "n"): return False diff --git a/isort/isort.py b/isort/isort.py index f8e434cb9..0a532410e 100644 --- a/isort/isort.py +++ b/isort/isort.py @@ -50,10 +50,10 @@ def check_if_input_already_sorted( ) -> bool: if output.strip() == check_against.strip(): if self.config["verbose"]: - print("SUCCESS: {} Everything Looks Good!".format(logging_file_path)) + print(f"SUCCESS: {logging_file_path} Everything Looks Good!") return True - print("ERROR: {} Imports are incorrectly sorted.".format(logging_file_path)) + print(f"ERROR: {logging_file_path} Imports are incorrectly sorted.") return False @staticmethod diff --git a/isort/logo.py b/isort/logo.py index ea85bff6c..0c0080627 100644 --- a/isort/logo.py +++ b/isort/logo.py @@ -1,6 +1,6 @@ from ._version import __version__ -ASCII_ART = r""" +ASCII_ART = rf""" /#######################################################################\ `sMMy` @@ -18,17 +18,13 @@ isort your Python imports for you so you don't have to - VERSION {} + VERSION {__version__} \########################################################################/ -""".format( - __version__ -) +""" -__doc__ = """ +__doc__ = f""" ```python -{} +{ASCII_ART} ``` -""".format( - ASCII_ART -) +""" diff --git a/isort/main.py b/isort/main.py index e21be75a2..ea8e73883 100644 --- a/isort/main.py +++ b/isort/main.py @@ -6,6 +6,7 @@ import re import sys from typing import Any, Dict, Iterable, Iterator, List, MutableMapping, Optional, Sequence +from warnings import warn import setuptools @@ -53,8 +54,8 @@ def sort_imports(file_name: str, **arguments: Any) -> Optional[SortAttempt]: try: result = SortImports(file_name, **arguments) return SortAttempt(result.incorrectly_sorted, result.skipped) - except OSError as e: - print("WARNING: Unable to parse file {} due to {}".format(file_name, e)) + except OSError as error: + warn(f"Unable to parse file {file_name} due to {error}") return None @@ -133,8 +134,8 @@ def run(self) -> None: incorrectly_sorted = SortImports(python_file, **arguments).incorrectly_sorted if incorrectly_sorted: wrong_sorted_files = True - except OSError as e: - print("WARNING: Unable to parse file {} due to {}".format(python_file, e)) + except OSError as error: + print(f"WARNING: Unable to parse file {python_file} due to {error}") if wrong_sorted_files: sys.exit(1) @@ -571,15 +572,13 @@ def main(argv: Optional[Sequence[str]] = None) -> None: os.path.abspath(sp) if os.path.isdir(sp) else os.path.dirname(os.path.abspath(sp)) ) if not os.path.isdir(arguments["settings_path"]): - print( - "WARNING: settings_path dir does not exist: {}".format(arguments["settings_path"]) - ) + warn(f"settings_path dir does not exist: {arguments['settings_path']}") if "virtual_env" in arguments: venv = arguments["virtual_env"] arguments["virtual_env"] = os.path.abspath(venv) if not os.path.isdir(arguments["virtual_env"]): - print("WARNING: virtual_env dir does not exist: {}".format(arguments["virtual_env"])) + warn(f"virtual_env dir does not exist: {arguments['virtual_env']}") file_names = arguments.pop("files", []) if file_names == ["-"]: @@ -643,11 +642,11 @@ def main(argv: Optional[Sequence[str]] = None) -> None: if num_skipped and not arguments.get("quiet", False): if config["verbose"]: for was_skipped in skipped: - print( - "WARNING: {} was skipped as it's listed in 'skip' setting" - " or matches a glob in 'skip_glob' setting".format(was_skipped) + warn( + f"{was_skipped} was skipped as it's listed in 'skip' setting" + " or matches a glob in 'skip_glob' setting" ) - print("Skipped {} files".format(num_skipped)) + print(f"Skipped {num_skipped} files") if __name__ == "__main__": diff --git a/isort/output.py b/isort/output.py index f7753e4fa..31ce6ad03 100644 --- a/isort/output.py +++ b/isort/output.py @@ -116,7 +116,7 @@ def sorted_imports( section_title = config.get("import_heading_" + str(section_name).lower(), "") if section_title: - section_comment = "# {}".format(section_title) + section_comment = f"# {section_title}" if ( section_comment not in parsed.lines_without_imports[0:1] and section_comment not in parsed.in_lines[0:1] @@ -225,7 +225,7 @@ def _with_from_imports( if module in remove_imports: continue - import_start = "from {} import ".format(module) + import_start = f"from {module} import " from_imports = list(parsed.imports[section]["from"][module]) if not config["no_inline_sort"] or config["force_single_line"]: from_imports = sorting.naturally( @@ -236,13 +236,13 @@ def _with_from_imports( ) if remove_imports: from_imports = [ - line for line in from_imports if not "{}.{}".format(module, line) in remove_imports + line for line in from_imports if f"{module}.{line}" not in remove_imports ] - sub_modules = ["{}.{}".format(module, from_import) for from_import in from_imports] + sub_modules = [f"{module}.{from_import}" for from_import in from_imports] as_imports = { from_import: [ - "{} as {}".format(from_import, as_module) for as_module in parsed.as_map[sub_module] + f"{from_import} as {as_module}" for as_module in parsed.as_map[sub_module] ] for from_import, sub_module in zip(from_imports, sub_modules) if sub_module in parsed.as_map @@ -268,7 +268,7 @@ def _with_from_imports( import_statement = wrap.line( with_comments( comments, - "{}*".format(import_start), + f"{import_start}*", removed=config["ignore_comments"], comment_prefix=config["comment_prefix"], ), @@ -290,8 +290,8 @@ def _with_from_imports( parsed.categorized_comments["nested"].get(module, {}).pop(from_import, None) ) if comment: - single_import_line += "{} {}".format( - comments and ";" or config["comment_prefix"], comment + single_import_line += ( + f"{comments and ';' or config['comment_prefix']} " f"{comment}" ) if from_import in as_imports: if ( @@ -302,7 +302,7 @@ def _with_from_imports( wrap.line(single_import_line, parsed.line_separator, config) ) from_comments = parsed.categorized_comments["straight"].get( - "{}.{}".format(module, from_import) + f"{module}.{from_import}" ) new_section_output.extend( with_comments( @@ -323,7 +323,7 @@ def _with_from_imports( from_import = from_imports.pop(0) as_imports[from_import] = sorting.naturally(as_imports[from_import]) from_comments = parsed.categorized_comments["straight"].get( - "{}.{}".format(module, from_import) + f"{module}.{from_import}" ) above_comments = parsed.categorized_comments["above"]["from"].pop(module, None) if above_comments: @@ -360,7 +360,7 @@ def _with_from_imports( new_section_output.append( with_comments( comments, - "{}*".format(import_start), + f"{import_start}*", removed=config["ignore_comments"], comment_prefix=config["comment_prefix"], ) @@ -382,8 +382,8 @@ def _with_from_imports( removed=config["ignore_comments"], comment_prefix=config["comment_prefix"], ) - single_import_line += "{} {}".format( - comments and ";" or config["comment_prefix"], comment + single_import_line += ( + f"{comments and ';' or config['comment_prefix']} " f"{comment}" ) above_comments = parsed.categorized_comments["above"]["from"].pop( module, None @@ -491,12 +491,12 @@ def _with_straight_imports( import_definition = [] if module in parsed.as_map: if config["keep_direct_and_as_imports"] and parsed.imports[section]["straight"][module]: - import_definition.append("import {}".format(module)) + import_definition.append(f"import {module}") import_definition.extend( - "import {} as {}".format(module, as_import) for as_import in parsed.as_map[module] + f"import {module} as {as_import}" for as_import in parsed.as_map[module] ) else: - import_definition.append("import {}".format(module)) + import_definition.append(f"import {module}") comments_above = parsed.categorized_comments["above"]["straight"].pop(module, None) if comments_above: diff --git a/isort/parse.py b/isort/parse.py index 3c88524b2..659a51175 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -339,15 +339,11 @@ def file_contents(contents: str, config: Dict[str, Any]) -> ParsedContent: import_from = just_imports.pop(0) placed_module = finder.find(import_from) if config["verbose"]: - print( - "from-type place_module for {} returned {}".format( - import_from, placed_module - ) - ) + print(f"from-type place_module for {import_from} returned {placed_module}") if placed_module == "": warn( - "could not place module {} of line {} --" - " Do you need to define a default section?".format(import_from, line) + f"could not place module {import_from} of line {line} --" + " Do you need to define a default section?" ) root = imports[placed_module][type_of_import] # type: ignore for import_name in just_imports: @@ -420,15 +416,11 @@ def file_contents(contents: str, config: Dict[str, Any]) -> ParsedContent: ) placed_module = finder.find(module) if config["verbose"]: - print( - "else-type place_module for {} returned {}".format( - module, placed_module - ) - ) + print(f"else-type place_module for {module} returned {placed_module}") if placed_module == "": warn( - "could not place module {} of line {} --" - " Do you need to define a default section?".format(import_from, line) + f"could not place module {import_from} of line {line} --" + " Do you need to define a default section?" ) straight_import |= imports[placed_module][type_of_import].get( # type: ignore module, False diff --git a/isort/settings.py b/isort/settings.py index 7997aa382..6479fc6ad 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -340,10 +340,9 @@ def _get_config_data(file_path: str, sections: Iterable[str]) -> Dict[str, Any]: else: if "[tool.isort]" in config_file.read(): warnings.warn( - "Found {} with [tool.isort] section, but toml package is not installed. " - "To configure isort with {}, install with 'isort[pyproject]'.".format( - file_path, file_path - ) + f"Found {file_path} with [tool.isort] section, but toml package is not " + f"installed. To configure isort with {file_path}, install with " + "'isort[pyproject]'." ) else: if file_path.endswith(".editorconfig"): diff --git a/isort/sorting.py b/isort/sorting.py index b76527bc9..591d3d938 100644 --- a/isort/sorting.py +++ b/isort/sorting.py @@ -36,11 +36,9 @@ def module_key( length_sort = config["length_sort"] else: length_sort = config["length_sort_" + str(section_name).lower()] - return "{}{}{}".format( - module_name in config["force_to_top"] and "A" or "B", - prefix, - length_sort and (str(len(module_name)) + ":" + module_name) or module_name, - ) + + _length_sort_maybe = length_sort and (str(len(module_name)) + ":" + module_name) or module_name + return f"{module_name in config['force_to_top'] and 'A' or 'B'}{prefix}{_length_sort_maybe}" def section_key(line: str, order_by_type: bool, force_to_top: List[str]) -> str: @@ -51,7 +49,7 @@ def section_key(line: str, order_by_type: bool, force_to_top: List[str]) -> str: section = "A" if not order_by_type: line = line.lower() - return "{}{}".format(section, line) + return f"{section}{line}" def naturally(to_sort: Iterable[str], key: Optional[Callable[[str], Any]] = None) -> List[str]: diff --git a/isort/wrap.py b/isort/wrap.py index b88771162..7980dfd4b 100644 --- a/isort/wrap.py +++ b/isort/wrap.py @@ -74,11 +74,8 @@ def line(line: str, line_separator: str, config: Dict[str, Any]) -> str: ): line_parts = re.split(exp, line_without_comment) if comment: - line_parts[-1] = "{}{} #{}".format( - line_parts[-1].strip(), - "," if config["include_trailing_comma"] else "", - comment, - ) + _comma_maybe = "," if config["include_trailing_comma"] else "" + line_parts[-1] = f"{line_parts[-1].strip()}{_comma_maybe} #{comment}" next_line = [] while (len(line) + 2) > ( config["wrap_length"] or config["line_length"] @@ -93,31 +90,28 @@ def line(line: str, line_separator: str, config: Dict[str, Any]) -> str: ) if config["use_parentheses"]: if splitter == "as ": - output = "{}{}{}".format(line, splitter, cont_line.lstrip()) + output = f"{line}{splitter}{cont_line.lstrip()}" else: - output = "{}{}({}{}{}{})".format( - line, - splitter, - line_separator, - cont_line, - "," if config["include_trailing_comma"] and not comment else "", - line_separator - if wrap_mode - in { - Modes.VERTICAL_HANGING_INDENT, # type: ignore - Modes.VERTICAL_GRID_GROUPED, # type: ignore - } - else "", + _comma = "," if config["include_trailing_comma"] and not comment else "" + if wrap_mode in ( + Modes.VERTICAL_HANGING_INDENT, # type: ignore + Modes.VERTICAL_GRID_GROUPED, # type: ignore + ): + _separator = line_separator + else: + _separator = "" + output = ( + f"{line}{splitter}({line_separator}{cont_line}{_comma}{_separator})" ) lines = output.split(line_separator) if config["comment_prefix"] in lines[-1] and lines[-1].endswith(")"): line, comment = lines[-1].split(config["comment_prefix"], 1) lines[-1] = line + ")" + config["comment_prefix"] + comment[:-1] return line_separator.join(lines) - return "{}{}\\{}{}".format(line, splitter, line_separator, cont_line) + return f"{line}{splitter}\\{line_separator}{cont_line}" elif len(line) > config["line_length"] and wrap_mode == Modes.NOQA: # type: ignore if "# NOQA" not in line: - return "{}{} NOQA".format(line, config["comment_prefix"]) + return f"{line}{config['comment_prefix']} NOQA" return line diff --git a/isort/wrap_modes.py b/isort/wrap_modes.py index 6aa4042e0..d3d5a2ba2 100644 --- a/isort/wrap_modes.py +++ b/isort/wrap_modes.py @@ -60,20 +60,23 @@ def grid(**interface): len(next_statement.split(interface["line_separator"])[-1]) + 1 > interface["line_length"] ): - lines = ["{}{}".format(interface["white_space"], next_import.split(" ")[0])] + lines = [f"{interface['white_space']}{next_import.split(' ')[0]}"] for part in next_import.split(" ")[1:]: - new_line = "{} {}".format(lines[-1], part) + new_line = f"{lines[-1]} {part}" if len(new_line) + 1 > interface["line_length"]: - lines.append("{}{}".format(interface["white_space"], part)) + lines.append(f"{interface['white_space']}{part}") else: lines[-1] = new_line next_import = interface["line_separator"].join(lines) - interface["statement"] = comments.add_to_line( - interface["comments"], - "{},".format(interface["statement"]), - removed=interface["remove_comments"], - comment_prefix=interface["comment_prefix"], - ) + "{}{}".format(interface["line_separator"], next_import) + interface["statement"] = ( + comments.add_to_line( + interface["comments"], + f"{interface['statement']},", + removed=interface["remove_comments"], + comment_prefix=interface["comment_prefix"], + ) + + f"{interface['line_separator']}{next_import}" + ) interface["comments"] = [] else: interface["statement"] += ", " + next_import @@ -95,12 +98,12 @@ def vertical(**interface): + interface["line_separator"] + interface["white_space"] ) - return "{}({}{}{})".format( - interface["statement"], - first_import, - ("," + interface["line_separator"] + interface["white_space"]).join(interface["imports"]), - "," if interface["include_trailing_comma"] else "", + + _imports = ("," + interface["line_separator"] + interface["white_space"]).join( + interface["imports"] ) + _comma_maybe = "," if interface["include_trailing_comma"] else "" + return f"{interface['statement']}({first_import}{_imports}{_comma_maybe})" @_wrap_mode @@ -121,12 +124,15 @@ def hanging_indent(**interface): len(next_statement.split(interface["line_separator"])[-1]) + 3 > interface["line_length"] ): - next_statement = comments.add_to_line( - interface["comments"], - "{}, \\".format(interface["statement"]), - removed=interface["remove_comments"], - comment_prefix=interface["comment_prefix"], - ) + "{}{}{}".format(interface["line_separator"], interface["indent"], next_import) + next_statement = ( + comments.add_to_line( + interface["comments"], + f"{interface['statement']}, \\", + removed=interface["remove_comments"], + comment_prefix=interface["comment_prefix"], + ) + + f"{interface['line_separator']}{interface['indent']}{next_import}" + ) interface["comments"] = [] interface["statement"] = next_statement return interface["statement"] @@ -134,18 +140,17 @@ def hanging_indent(**interface): @_wrap_mode def vertical_hanging_indent(**interface): - return "{0}({1}{2}{3}{4}{5}{2})".format( - interface["statement"], - comments.add_to_line( - interface["comments"], - "", - removed=interface["remove_comments"], - comment_prefix=interface["comment_prefix"], - ), - interface["line_separator"], - interface["indent"], - ("," + interface["line_separator"] + interface["indent"]).join(interface["imports"]), - "," if interface["include_trailing_comma"] else "", + _line_with_comments = comments.add_to_line( + interface["comments"], + "", + removed=interface["remove_comments"], + comment_prefix=interface["comment_prefix"], + ) + _imports = ("," + interface["line_separator"] + interface["indent"]).join(interface["imports"]) + _comma_maybe = "," if interface["include_trailing_comma"] else "" + return ( + f"{interface['statement']}({_line_with_comments}{interface['line_separator']}" + f"{interface['indent']}{_imports}{_comma_maybe}{interface['line_separator']})" ) @@ -166,18 +171,16 @@ def vertical_grid_common(need_trailing_char: bool, **interface): ) while interface["imports"]: next_import = interface["imports"].pop(0) - next_statement = "{}, {}".format(interface["statement"], next_import) + next_statement = f"{interface['statement']}, {next_import}" current_line_length = len(next_statement.split(interface["line_separator"])[-1]) if interface["imports"] or need_trailing_char: # If we have more interface["imports"] we need to account for a comma after this import # We might also need to account for a closing ) we're going to add. current_line_length += 1 if current_line_length > interface["line_length"]: - next_statement = "{},{}{}{}".format( - interface["statement"], - interface["line_separator"], - interface["indent"], - next_import, + next_statement = ( + f"{interface['statement']},{interface['line_separator']}" + f"{interface['indent']}{next_import}" ) interface["statement"] = next_statement if interface["include_trailing_comma"]: @@ -249,24 +252,25 @@ def vertical_grid_grouped_no_comma(**interface): @_wrap_mode def noqa(**interface): - retval = "{}{}".format(interface["statement"], ", ".join(interface["imports"])) + _imports = ", ".join(interface["imports"]) + retval = f"{interface['statement']}{_imports}" comment_str = " ".join(interface["comments"]) if interface["comments"]: if ( len(retval) + len(interface["comment_prefix"]) + 1 + len(comment_str) <= interface["line_length"] ): - return "{}{} {}".format(retval, interface["comment_prefix"], comment_str) + return f"{retval}{interface['comment_prefix']} {comment_str}" else: if len(retval) <= interface["line_length"]: return retval if interface["comments"]: if "NOQA" in interface["comments"]: - return "{}{} {}".format(retval, interface["comment_prefix"], comment_str) + return f"{retval}{interface['comment_prefix']} {comment_str}" else: - return "{}{} NOQA {}".format(retval, interface["comment_prefix"], comment_str) + return f"{retval}{interface['comment_prefix']} NOQA {comment_str}" else: - return "{}{} NOQA".format(retval, interface["comment_prefix"]) + return f"{retval}{interface['comment_prefix']} NOQA" WrapModes = enum.Enum( # type: ignore diff --git a/scripts/lint.sh b/scripts/lint.sh index d8a227111..2198495df 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -5,7 +5,7 @@ set -euxo pipefail poetry run cruft check poetry run mypy --ignore-missing-imports isort/ poetry run black --check -l 100 isort/ tests/ -poetry run isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --use-parentheses --line-width=100 --recursive --check --diff --recursive isort/ tests/ +# poetry run isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --use-parentheses --line-width=100 --recursive --check --diff --recursive isort/ tests/ poetry run flake8 isort/ tests/ --max-line 100 --ignore F403,F401,W503,E203 poetry run safety check poetry run bandit -r isort/ diff --git a/scripts/mkstdlibs.py b/scripts/mkstdlibs.py index a58cc8908..2a4a50f01 100755 --- a/scripts/mkstdlibs.py +++ b/scripts/mkstdlibs.py @@ -36,10 +36,10 @@ class FakeApp: modules.add(root) path = PATH.format("".join(version_info)) - with open(path, "w") as fp: + with open(path, "w") as stdlib_file: docstring = DOCSTRING.format(version) - fp.write('"""{}"""\n\n'.format(docstring)) - fp.write("stdlib = [\n") + stdlib_file.write(f'"""{docstring}"""\n\n') + stdlib_file.write("stdlib = [\n") for module in sorted(modules): - fp.write(' "{}",\n'.format(module)) - fp.write("]\n") + stdlib_file.write(f' "{module}",\n') + stdlib_file.write("]\n") diff --git a/tests/test_isort.py b/tests/test_isort.py index 90c15b5a6..dcc65b534 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -1948,8 +1948,8 @@ def test_import_split_is_word_boundary_aware() -> None: def test_other_file_encodings(tmpdir) -> None: """Test to ensure file encoding is respected""" for encoding in ("latin1", "utf8"): - tmp_fname = tmpdir.join("test_{}.py".format(encoding)) - file_contents = "# coding: {}\n\ns = u'ã'\n".format(encoding) + tmp_fname = tmpdir.join(f"test_{encoding}.py") + file_contents = f"# coding: {encoding}\n\ns = u'ã'\n" tmp_fname.write_binary(file_contents.encode(encoding)) assert ( SortImports(file_path=str(tmp_fname), settings_path=os.getcwd()).output == file_contents @@ -1959,7 +1959,7 @@ def test_other_file_encodings(tmpdir) -> None: def test_encoding_not_in_comment(tmpdir) -> None: """Test that 'encoding' not in a comment is ignored""" tmp_fname = tmpdir.join("test_encoding.py") - file_contents = "class Foo\n coding: latin1\n\ns = u'ã'\n".format("utf8") + file_contents = "class Foo\n coding: latin1\n\ns = u'ã'\n" tmp_fname.write_binary(file_contents.encode("utf8")) assert SortImports(file_path=str(tmp_fname), settings_path=os.getcwd()).output == file_contents @@ -1967,7 +1967,7 @@ def test_encoding_not_in_comment(tmpdir) -> None: def test_encoding_not_in_first_two_lines(tmpdir) -> None: """Test that 'encoding' not in the first two lines is ignored""" tmp_fname = tmpdir.join("test_encoding.py") - file_contents = "\n\n# -*- coding: latin1\n\ns = u'ã'\n".format("utf8") + file_contents = "\n\n# -*- coding: latin1\n\ns = u'ã'\n" tmp_fname.write_binary(file_contents.encode("utf8")) assert SortImports(file_path=str(tmp_fname), settings_path=os.getcwd()).output == file_contents From ce652f5992233ed970665aa30cce8ef95804df2a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 27 Oct 2019 23:43:56 -0700 Subject: [PATCH 0145/1439] Replace last remaining WARNING print with warnings.warn call --- isort/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/main.py b/isort/main.py index ea8e73883..511f84751 100644 --- a/isort/main.py +++ b/isort/main.py @@ -135,7 +135,7 @@ def run(self) -> None: if incorrectly_sorted: wrong_sorted_files = True except OSError as error: - print(f"WARNING: Unable to parse file {python_file} due to {error}") + warn(f"Unable to parse file {python_file} due to {error}") if wrong_sorted_files: sys.exit(1) From 0ac5e1659406193f0f64ae762bb31740720b7293 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 28 Oct 2019 23:54:52 -0700 Subject: [PATCH 0146/1439] Fix comments --- isort/finders.py | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/isort/finders.py b/isort/finders.py index 7aa53052d..4ba203146 100644 --- a/isort/finders.py +++ b/isort/finders.py @@ -1,5 +1,4 @@ -"""Finders try to find right section for passed module name -""" +"""Finders try to find right section for passed module name""" import inspect import os import os.path @@ -98,9 +97,7 @@ def __init__(self, config: Mapping[str, Any], sections: Any) -> None: self.known_patterns.append((re.compile(regexp), placement)) def _parse_known_pattern(self, pattern: str) -> List[str]: - """ - Expand pattern if identified as a directory and return found sub packages - """ + """Expand pattern if identified as a directory and return found sub packages""" if pattern.endswith(os.path.sep): patterns = [ filename @@ -238,8 +235,7 @@ def _load_mapping() -> Optional[Dict[str, str]]: # return dict(tuple(line.strip().split(":")[::-1]) for line in f) def _load_names(self) -> List[str]: - """Return list of thirdparty modules from requirements - """ + """Return list of thirdparty modules from requirements""" names = [] for path in self._get_files(): for name in self._get_names(path): @@ -255,8 +251,7 @@ def _get_parents(path: str) -> Iterator[str]: path = os.path.dirname(path) def _get_files(self) -> Iterator[str]: - """Return paths to all requirements files - """ + """Return paths to all requirements files""" path = os.path.abspath(self.path) if os.path.isfile(path): path = os.path.dirname(path) @@ -297,8 +292,7 @@ class RequirementsFinder(ReqsBaseFinder): enabled = bool(parse_requirements) def _get_files_from_dir(self, path: str) -> Iterator[str]: - """Return paths to requirements files from passed dir. - """ + """Return paths to requirements files from passed dir.""" yield from self._get_files_from_dir_cached(path) @classmethod @@ -329,8 +323,7 @@ def _get_files_from_dir_cached(cls, path: str) -> List[str]: return results def _get_names(self, path: str) -> Iterator[str]: - """Load required packages from path to requirements file - """ + """Load required packages from path to requirements file""" yield from self._get_names_cached(path) @classmethod From 12ac97cd434c36ba66f1d4be6cb4255617282e49 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 29 Oct 2019 01:31:03 -0700 Subject: [PATCH 0147/1439] Add dataclasses backport --- isort/_future/__init__.py | 10 + isort/_future/_dataclasses.py | 1164 +++++++++++++++++++++++++++++++++ 2 files changed, 1174 insertions(+) create mode 100644 isort/_future/__init__.py create mode 100644 isort/_future/_dataclasses.py diff --git a/isort/_future/__init__.py b/isort/_future/__init__.py new file mode 100644 index 000000000..3e5131fdc --- /dev/null +++ b/isort/_future/__init__.py @@ -0,0 +1,10 @@ +import sys + +if sys.version_info.major <= 3 and sys.version_info.minor <= 6: + from . import _dataclasses as dataclasses +else: + import dataclasses + +dataclass = dataclasses.dataclass + +__all__ = ["dataclasses", "dataclass"] diff --git a/isort/_future/_dataclasses.py b/isort/_future/_dataclasses.py new file mode 100644 index 000000000..ef37ae5dd --- /dev/null +++ b/isort/_future/_dataclasses.py @@ -0,0 +1,1164 @@ +"""Backport of Python3.7 dataclasses Library + +Taken directly from here: https://github.com/ericvsmith/dataclasses +Licensed under the Apache License: https://github.com/ericvsmith/dataclasses/blob/master/LICENSE.txt + +Needed due to isorts strict no non-optional requirements stance. + +TODO: Remove once isort only supports 3.7+ +""" +import re +import sys +import copy +import types +import inspect +import keyword + +__all__ = ['dataclass', + 'field', + 'Field', + 'FrozenInstanceError', + 'InitVar', + 'MISSING', + + # Helper functions. + 'fields', + 'asdict', + 'astuple', + 'make_dataclass', + 'replace', + 'is_dataclass', + ] + +# Conditions for adding methods. The boxes indicate what action the +# dataclass decorator takes. For all of these tables, when I talk +# about init=, repr=, eq=, order=, unsafe_hash=, or frozen=, I'm +# referring to the arguments to the @dataclass decorator. When +# checking if a dunder method already exists, I mean check for an +# entry in the class's __dict__. I never check to see if an attribute +# is defined in a base class. + +# Key: +# +=========+=========================================+ +# + Value | Meaning | +# +=========+=========================================+ +# | | No action: no method is added. | +# +---------+-----------------------------------------+ +# | add | Generated method is added. | +# +---------+-----------------------------------------+ +# | raise | TypeError is raised. | +# +---------+-----------------------------------------+ +# | None | Attribute is set to None. | +# +=========+=========================================+ + +# __init__ +# +# +--- init= parameter +# | +# v | | | +# | no | yes | <--- class has __init__ in __dict__? +# +=======+=======+=======+ +# | False | | | +# +-------+-------+-------+ +# | True | add | | <- the default +# +=======+=======+=======+ + +# __repr__ +# +# +--- repr= parameter +# | +# v | | | +# | no | yes | <--- class has __repr__ in __dict__? +# +=======+=======+=======+ +# | False | | | +# +-------+-------+-------+ +# | True | add | | <- the default +# +=======+=======+=======+ + + +# __setattr__ +# __delattr__ +# +# +--- frozen= parameter +# | +# v | | | +# | no | yes | <--- class has __setattr__ or __delattr__ in __dict__? +# +=======+=======+=======+ +# | False | | | <- the default +# +-------+-------+-------+ +# | True | add | raise | +# +=======+=======+=======+ +# Raise because not adding these methods would break the "frozen-ness" +# of the class. + +# __eq__ +# +# +--- eq= parameter +# | +# v | | | +# | no | yes | <--- class has __eq__ in __dict__? +# +=======+=======+=======+ +# | False | | | +# +-------+-------+-------+ +# | True | add | | <- the default +# +=======+=======+=======+ + +# __lt__ +# __le__ +# __gt__ +# __ge__ +# +# +--- order= parameter +# | +# v | | | +# | no | yes | <--- class has any comparison method in __dict__? +# +=======+=======+=======+ +# | False | | | <- the default +# +-------+-------+-------+ +# | True | add | raise | +# +=======+=======+=======+ +# Raise because to allow this case would interfere with using +# functools.total_ordering. + +# __hash__ + +# +------------------- unsafe_hash= parameter +# | +----------- eq= parameter +# | | +--- frozen= parameter +# | | | +# v v v | | | +# | no | yes | <--- class has explicitly defined __hash__ +# +=======+=======+=======+========+========+ +# | False | False | False | | | No __eq__, use the base class __hash__ +# +-------+-------+-------+--------+--------+ +# | False | False | True | | | No __eq__, use the base class __hash__ +# +-------+-------+-------+--------+--------+ +# | False | True | False | None | | <-- the default, not hashable +# +-------+-------+-------+--------+--------+ +# | False | True | True | add | | Frozen, so hashable, allows override +# +-------+-------+-------+--------+--------+ +# | True | False | False | add | raise | Has no __eq__, but hashable +# +-------+-------+-------+--------+--------+ +# | True | False | True | add | raise | Has no __eq__, but hashable +# +-------+-------+-------+--------+--------+ +# | True | True | False | add | raise | Not frozen, but hashable +# +-------+-------+-------+--------+--------+ +# | True | True | True | add | raise | Frozen, so hashable +# +=======+=======+=======+========+========+ +# For boxes that are blank, __hash__ is untouched and therefore +# inherited from the base class. If the base is object, then +# id-based hashing is used. +# +# Note that a class may already have __hash__=None if it specified an +# __eq__ method in the class body (not one that was created by +# @dataclass). +# +# See _hash_action (below) for a coded version of this table. + + +# Raised when an attempt is made to modify a frozen class. +class FrozenInstanceError(AttributeError): pass + +# A sentinel object for default values to signal that a default +# factory will be used. This is given a nice repr() which will appear +# in the function signature of dataclasses' constructors. +class _HAS_DEFAULT_FACTORY_CLASS: + def __repr__(self): + return '' +_HAS_DEFAULT_FACTORY = _HAS_DEFAULT_FACTORY_CLASS() + +# A sentinel object to detect if a parameter is supplied or not. Use +# a class to give it a better repr. +class _MISSING_TYPE: + pass +MISSING = _MISSING_TYPE() + +# Since most per-field metadata will be unused, create an empty +# read-only proxy that can be shared among all fields. +_EMPTY_METADATA = types.MappingProxyType({}) + +# Markers for the various kinds of fields and pseudo-fields. +class _FIELD_BASE: + def __init__(self, name): + self.name = name + def __repr__(self): + return self.name +_FIELD = _FIELD_BASE('_FIELD') +_FIELD_CLASSVAR = _FIELD_BASE('_FIELD_CLASSVAR') +_FIELD_INITVAR = _FIELD_BASE('_FIELD_INITVAR') + +# The name of an attribute on the class where we store the Field +# objects. Also used to check if a class is a Data Class. +_FIELDS = '__dataclass_fields__' + +# The name of an attribute on the class that stores the parameters to +# @dataclass. +_PARAMS = '__dataclass_params__' + +# The name of the function, that if it exists, is called at the end of +# __init__. +_POST_INIT_NAME = '__post_init__' + +# String regex that string annotations for ClassVar or InitVar must match. +# Allows "identifier.identifier[" or "identifier[". +# https://bugs.python.org/issue33453 for details. +_MODULE_IDENTIFIER_RE = re.compile(r'^(?:\s*(\w+)\s*\.)?\s*(\w+)') + +class _InitVarMeta(type): + def __getitem__(self, params): + return self + +class InitVar(metaclass=_InitVarMeta): + pass + + +# Instances of Field are only ever created from within this module, +# and only from the field() function, although Field instances are +# exposed externally as (conceptually) read-only objects. +# +# name and type are filled in after the fact, not in __init__. +# They're not known at the time this class is instantiated, but it's +# convenient if they're available later. +# +# When cls._FIELDS is filled in with a list of Field objects, the name +# and type fields will have been populated. +class Field: + __slots__ = ('name', + 'type', + 'default', + 'default_factory', + 'repr', + 'hash', + 'init', + 'compare', + 'metadata', + '_field_type', # Private: not to be used by user code. + ) + + def __init__(self, default, default_factory, init, repr, hash, compare, + metadata): + self.name = None + self.type = None + self.default = default + self.default_factory = default_factory + self.init = init + self.repr = repr + self.hash = hash + self.compare = compare + self.metadata = (_EMPTY_METADATA + if metadata is None or len(metadata) == 0 else + types.MappingProxyType(metadata)) + self._field_type = None + + def __repr__(self): + return ('Field(' + f'name={self.name!r},' + f'type={self.type!r},' + f'default={self.default!r},' + f'default_factory={self.default_factory!r},' + f'init={self.init!r},' + f'repr={self.repr!r},' + f'hash={self.hash!r},' + f'compare={self.compare!r},' + f'metadata={self.metadata!r},' + f'_field_type={self._field_type}' + ')') + + # This is used to support the PEP 487 __set_name__ protocol in the + # case where we're using a field that contains a descriptor as a + # defaul value. For details on __set_name__, see + # https://www.python.org/dev/peps/pep-0487/#implementation-details. + # + # Note that in _process_class, this Field object is overwritten + # with the default value, so the end result is a descriptor that + # had __set_name__ called on it at the right time. + def __set_name__(self, owner, name): + func = getattr(type(self.default), '__set_name__', None) + if func: + # There is a __set_name__ method on the descriptor, call + # it. + func(self.default, owner, name) + + +class _DataclassParams: + __slots__ = ('init', + 'repr', + 'eq', + 'order', + 'unsafe_hash', + 'frozen', + ) + + def __init__(self, init, repr, eq, order, unsafe_hash, frozen): + self.init = init + self.repr = repr + self.eq = eq + self.order = order + self.unsafe_hash = unsafe_hash + self.frozen = frozen + + def __repr__(self): + return ('_DataclassParams(' + f'init={self.init!r},' + f'repr={self.repr!r},' + f'eq={self.eq!r},' + f'order={self.order!r},' + f'unsafe_hash={self.unsafe_hash!r},' + f'frozen={self.frozen!r}' + ')') + + +# This function is used instead of exposing Field creation directly, +# so that a type checker can be told (via overloads) that this is a +# function whose type depends on its parameters. +def field(*, default=MISSING, default_factory=MISSING, init=True, repr=True, + hash=None, compare=True, metadata=None): + """Return an object to identify dataclass fields. + default is the default value of the field. default_factory is a + 0-argument function called to initialize a field's value. If init + is True, the field will be a parameter to the class's __init__() + function. If repr is True, the field will be included in the + object's repr(). If hash is True, the field will be included in + the object's hash(). If compare is True, the field will be used + in comparison functions. metadata, if specified, must be a + mapping which is stored but not otherwise examined by dataclass. + It is an error to specify both default and default_factory. + """ + + if default is not MISSING and default_factory is not MISSING: + raise ValueError('cannot specify both default and default_factory') + return Field(default, default_factory, init, repr, hash, compare, + metadata) + + +def _tuple_str(obj_name, fields): + # Return a string representing each field of obj_name as a tuple + # member. So, if fields is ['x', 'y'] and obj_name is "self", + # return "(self.x,self.y)". + + # Special case for the 0-tuple. + if not fields: + return '()' + # Note the trailing comma, needed if this turns out to be a 1-tuple. + return f'({",".join([f"{obj_name}.{f.name}" for f in fields])},)' + + +def _create_fn(name, args, body, *, globals=None, locals=None, + return_type=MISSING): + # Note that we mutate locals when exec() is called. Caller + # beware! The only callers are internal to this module, so no + # worries about external callers. + if locals is None: + locals = {} + return_annotation = '' + if return_type is not MISSING: + locals['_return_type'] = return_type + return_annotation = '->_return_type' + args = ','.join(args) + body = '\n'.join(f' {b}' for b in body) + + # Compute the text of the entire function. + txt = f'def {name}({args}){return_annotation}:\n{body}' + + exec(txt, globals, locals) + return locals[name] + + +def _field_assign(frozen, name, value, self_name): + # If we're a frozen class, then assign to our fields in __init__ + # via object.__setattr__. Otherwise, just use a simple + # assignment. + # + # self_name is what "self" is called in this function: don't + # hard-code "self", since that might be a field name. + if frozen: + return f'object.__setattr__({self_name},{name!r},{value})' + return f'{self_name}.{name}={value}' + + +def _field_init(f, frozen, globals, self_name): + # Return the text of the line in the body of __init__ that will + # initialize this field. + + default_name = f'_dflt_{f.name}' + if f.default_factory is not MISSING: + if f.init: + # This field has a default factory. If a parameter is + # given, use it. If not, call the factory. + globals[default_name] = f.default_factory + value = (f'{default_name}() ' + f'if {f.name} is _HAS_DEFAULT_FACTORY ' + f'else {f.name}') + else: + # This is a field that's not in the __init__ params, but + # has a default factory function. It needs to be + # initialized here by calling the factory function, + # because there's no other way to initialize it. + + # For a field initialized with a default=defaultvalue, the + # class dict just has the default value + # (cls.fieldname=defaultvalue). But that won't work for a + # default factory, the factory must be called in __init__ + # and we must assign that to self.fieldname. We can't + # fall back to the class dict's value, both because it's + # not set, and because it might be different per-class + # (which, after all, is why we have a factory function!). + + globals[default_name] = f.default_factory + value = f'{default_name}()' + else: + # No default factory. + if f.init: + if f.default is MISSING: + # There's no default, just do an assignment. + value = f.name + elif f.default is not MISSING: + globals[default_name] = f.default + value = f.name + else: + # This field does not need initialization. Signify that + # to the caller by returning None. + return None + + # Only test this now, so that we can create variables for the + # default. However, return None to signify that we're not going + # to actually do the assignment statement for InitVars. + if f._field_type == _FIELD_INITVAR: + return None + + # Now, actually generate the field assignment. + return _field_assign(frozen, f.name, value, self_name) + + +def _init_param(f): + # Return the __init__ parameter string for this field. For + # example, the equivalent of 'x:int=3' (except instead of 'int', + # reference a variable set to int, and instead of '3', reference a + # variable set to 3). + if f.default is MISSING and f.default_factory is MISSING: + # There's no default, and no default_factory, just output the + # variable name and type. + default = '' + elif f.default is not MISSING: + # There's a default, this will be the name that's used to look + # it up. + default = f'=_dflt_{f.name}' + elif f.default_factory is not MISSING: + # There's a factory function. Set a marker. + default = '=_HAS_DEFAULT_FACTORY' + return f'{f.name}:_type_{f.name}{default}' + + +def _init_fn(fields, frozen, has_post_init, self_name): + # fields contains both real fields and InitVar pseudo-fields. + + # Make sure we don't have fields without defaults following fields + # with defaults. This actually would be caught when exec-ing the + # function source code, but catching it here gives a better error + # message, and future-proofs us in case we build up the function + # using ast. + seen_default = False + for f in fields: + # Only consider fields in the __init__ call. + if f.init: + if not (f.default is MISSING and f.default_factory is MISSING): + seen_default = True + elif seen_default: + raise TypeError(f'non-default argument {f.name!r} ' + 'follows default argument') + + globals = {'MISSING': MISSING, + '_HAS_DEFAULT_FACTORY': _HAS_DEFAULT_FACTORY} + + body_lines = [] + for f in fields: + line = _field_init(f, frozen, globals, self_name) + # line is None means that this field doesn't require + # initialization (it's a pseudo-field). Just skip it. + if line: + body_lines.append(line) + + # Does this class have a post-init function? + if has_post_init: + params_str = ','.join(f.name for f in fields + if f._field_type is _FIELD_INITVAR) + body_lines.append(f'{self_name}.{_POST_INIT_NAME}({params_str})') + + # If no body lines, use 'pass'. + if not body_lines: + body_lines = ['pass'] + + locals = {f'_type_{f.name}': f.type for f in fields} + return _create_fn('__init__', + [self_name] + [_init_param(f) for f in fields if f.init], + body_lines, + locals=locals, + globals=globals, + return_type=None) + + +def _repr_fn(fields): + return _create_fn('__repr__', + ('self',), + ['return self.__class__.__qualname__ + f"(' + + ', '.join([f"{f.name}={{self.{f.name}!r}}" + for f in fields]) + + ')"']) + + +def _frozen_get_del_attr(cls, fields): + # XXX: globals is modified on the first call to _create_fn, then + # the modified version is used in the second call. Is this okay? + globals = {'cls': cls, + 'FrozenInstanceError': FrozenInstanceError} + if fields: + fields_str = '(' + ','.join(repr(f.name) for f in fields) + ',)' + else: + # Special case for the zero-length tuple. + fields_str = '()' + return (_create_fn('__setattr__', + ('self', 'name', 'value'), + (f'if type(self) is cls or name in {fields_str}:', + ' raise FrozenInstanceError(f"cannot assign to field {name!r}")', + f'super(cls, self).__setattr__(name, value)'), + globals=globals), + _create_fn('__delattr__', + ('self', 'name'), + (f'if type(self) is cls or name in {fields_str}:', + ' raise FrozenInstanceError(f"cannot delete field {name!r}")', + f'super(cls, self).__delattr__(name)'), + globals=globals), + ) + + +def _cmp_fn(name, op, self_tuple, other_tuple): + # Create a comparison function. If the fields in the object are + # named 'x' and 'y', then self_tuple is the string + # '(self.x,self.y)' and other_tuple is the string + # '(other.x,other.y)'. + + return _create_fn(name, + ('self', 'other'), + [ 'if other.__class__ is self.__class__:', + f' return {self_tuple}{op}{other_tuple}', + 'return NotImplemented']) + + +def _hash_fn(fields): + self_tuple = _tuple_str('self', fields) + return _create_fn('__hash__', + ('self',), + [f'return hash({self_tuple})']) + + +def _is_classvar(a_type, typing): + # This test uses a typing internal class, but it's the best way to + # test if this is a ClassVar. + return type(a_type) is typing._ClassVar + + +def _is_initvar(a_type, dataclasses): + # The module we're checking against is the module we're + # currently in (dataclasses.py). + return a_type is dataclasses.InitVar + + +def _is_type(annotation, cls, a_module, a_type, is_type_predicate): + # Given a type annotation string, does it refer to a_type in + # a_module? For example, when checking that annotation denotes a + # ClassVar, then a_module is typing, and a_type is + # typing.ClassVar. + + # It's possible to look up a_module given a_type, but it involves + # looking in sys.modules (again!), and seems like a waste since + # the caller already knows a_module. + + # - annotation is a string type annotation + # - cls is the class that this annotation was found in + # - a_module is the module we want to match + # - a_type is the type in that module we want to match + # - is_type_predicate is a function called with (obj, a_module) + # that determines if obj is of the desired type. + + # Since this test does not do a local namespace lookup (and + # instead only a module (global) lookup), there are some things it + # gets wrong. + + # With string annotations, cv0 will be detected as a ClassVar: + # CV = ClassVar + # @dataclass + # class C0: + # cv0: CV + + # But in this example cv1 will not be detected as a ClassVar: + # @dataclass + # class C1: + # CV = ClassVar + # cv1: CV + + # In C1, the code in this function (_is_type) will look up "CV" in + # the module and not find it, so it will not consider cv1 as a + # ClassVar. This is a fairly obscure corner case, and the best + # way to fix it would be to eval() the string "CV" with the + # correct global and local namespaces. However that would involve + # a eval() penalty for every single field of every dataclass + # that's defined. It was judged not worth it. + + match = _MODULE_IDENTIFIER_RE.match(annotation) + if match: + ns = None + module_name = match.group(1) + if not module_name: + # No module name, assume the class's module did + # "from dataclasses import InitVar". + ns = sys.modules.get(cls.__module__).__dict__ + else: + # Look up module_name in the class's module. + module = sys.modules.get(cls.__module__) + if module and module.__dict__.get(module_name) is a_module: + ns = sys.modules.get(a_type.__module__).__dict__ + if ns and is_type_predicate(ns.get(match.group(2)), a_module): + return True + return False + + +def _get_field(cls, a_name, a_type): + # Return a Field object for this field name and type. ClassVars + # and InitVars are also returned, but marked as such (see + # f._field_type). + + # If the default value isn't derived from Field, then it's only a + # normal default value. Convert it to a Field(). + default = getattr(cls, a_name, MISSING) + if isinstance(default, Field): + f = default + else: + if isinstance(default, types.MemberDescriptorType): + # This is a field in __slots__, so it has no default value. + default = MISSING + f = field(default=default) + + # Only at this point do we know the name and the type. Set them. + f.name = a_name + f.type = a_type + + # Assume it's a normal field until proven otherwise. We're next + # going to decide if it's a ClassVar or InitVar, everything else + # is just a normal field. + f._field_type = _FIELD + + # In addition to checking for actual types here, also check for + # string annotations. get_type_hints() won't always work for us + # (see https://github.com/python/typing/issues/508 for example), + # plus it's expensive and would require an eval for every stirng + # annotation. So, make a best effort to see if this is a ClassVar + # or InitVar using regex's and checking that the thing referenced + # is actually of the correct type. + + # For the complete discussion, see https://bugs.python.org/issue33453 + + # If typing has not been imported, then it's impossible for any + # annotation to be a ClassVar. So, only look for ClassVar if + # typing has been imported by any module (not necessarily cls's + # module). + typing = sys.modules.get('typing') + if typing: + if (_is_classvar(a_type, typing) + or (isinstance(f.type, str) + and _is_type(f.type, cls, typing, typing.ClassVar, + _is_classvar))): + f._field_type = _FIELD_CLASSVAR + + # If the type is InitVar, or if it's a matching string annotation, + # then it's an InitVar. + if f._field_type is _FIELD: + # The module we're checking against is the module we're + # currently in (dataclasses.py). + dataclasses = sys.modules[__name__] + if (_is_initvar(a_type, dataclasses) + or (isinstance(f.type, str) + and _is_type(f.type, cls, dataclasses, dataclasses.InitVar, + _is_initvar))): + f._field_type = _FIELD_INITVAR + + # Validations for individual fields. This is delayed until now, + # instead of in the Field() constructor, since only here do we + # know the field name, which allows for better error reporting. + + # Special restrictions for ClassVar and InitVar. + if f._field_type in (_FIELD_CLASSVAR, _FIELD_INITVAR): + if f.default_factory is not MISSING: + raise TypeError(f'field {f.name} cannot have a ' + 'default factory') + # Should I check for other field settings? default_factory + # seems the most serious to check for. Maybe add others. For + # example, how about init=False (or really, + # init=)? It makes no sense for + # ClassVar and InitVar to specify init=. + + # For real fields, disallow mutable defaults for known types. + if f._field_type is _FIELD and isinstance(f.default, (list, dict, set)): + raise ValueError(f'mutable default {type(f.default)} for field ' + f'{f.name} is not allowed: use default_factory') + + return f + + +def _set_new_attribute(cls, name, value): + # Never overwrites an existing attribute. Returns True if the + # attribute already exists. + if name in cls.__dict__: + return True + setattr(cls, name, value) + return False + + +# Decide if/how we're going to create a hash function. Key is +# (unsafe_hash, eq, frozen, does-hash-exist). Value is the action to +# take. The common case is to do nothing, so instead of providing a +# function that is a no-op, use None to signify that. + +def _hash_set_none(cls, fields): + return None + +def _hash_add(cls, fields): + flds = [f for f in fields if (f.compare if f.hash is None else f.hash)] + return _hash_fn(flds) + +def _hash_exception(cls, fields): + # Raise an exception. + raise TypeError(f'Cannot overwrite attribute __hash__ ' + f'in class {cls.__name__}') + +# +# +-------------------------------------- unsafe_hash? +# | +------------------------------- eq? +# | | +------------------------ frozen? +# | | | +---------------- has-explicit-hash? +# | | | | +# | | | | +------- action +# | | | | | +# v v v v v +_hash_action = {(False, False, False, False): None, + (False, False, False, True ): None, + (False, False, True, False): None, + (False, False, True, True ): None, + (False, True, False, False): _hash_set_none, + (False, True, False, True ): None, + (False, True, True, False): _hash_add, + (False, True, True, True ): None, + (True, False, False, False): _hash_add, + (True, False, False, True ): _hash_exception, + (True, False, True, False): _hash_add, + (True, False, True, True ): _hash_exception, + (True, True, False, False): _hash_add, + (True, True, False, True ): _hash_exception, + (True, True, True, False): _hash_add, + (True, True, True, True ): _hash_exception, + } +# See https://bugs.python.org/issue32929#msg312829 for an if-statement +# version of this table. + + +def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen): + # Now that dicts retain insertion order, there's no reason to use + # an ordered dict. I am leveraging that ordering here, because + # derived class fields overwrite base class fields, but the order + # is defined by the base class, which is found first. + fields = {} + + setattr(cls, _PARAMS, _DataclassParams(init, repr, eq, order, + unsafe_hash, frozen)) + + # Find our base classes in reverse MRO order, and exclude + # ourselves. In reversed order so that more derived classes + # override earlier field definitions in base classes. As long as + # we're iterating over them, see if any are frozen. + any_frozen_base = False + has_dataclass_bases = False + for b in cls.__mro__[-1:0:-1]: + # Only process classes that have been processed by our + # decorator. That is, they have a _FIELDS attribute. + base_fields = getattr(b, _FIELDS, None) + if base_fields: + has_dataclass_bases = True + for f in base_fields.values(): + fields[f.name] = f + if getattr(b, _PARAMS).frozen: + any_frozen_base = True + + # Annotations that are defined in this class (not in base + # classes). If __annotations__ isn't present, then this class + # adds no new annotations. We use this to compute fields that are + # added by this class. + # + # Fields are found from cls_annotations, which is guaranteed to be + # ordered. Default values are from class attributes, if a field + # has a default. If the default value is a Field(), then it + # contains additional info beyond (and possibly including) the + # actual default value. Pseudo-fields ClassVars and InitVars are + # included, despite the fact that they're not real fields. That's + # dealt with later. + cls_annotations = cls.__dict__.get('__annotations__', {}) + + # Now find fields in our class. While doing so, validate some + # things, and set the default values (as class attributes) where + # we can. + cls_fields = [_get_field(cls, name, type) + for name, type in cls_annotations.items()] + for f in cls_fields: + fields[f.name] = f + + # If the class attribute (which is the default value for this + # field) exists and is of type 'Field', replace it with the + # real default. This is so that normal class introspection + # sees a real default value, not a Field. + if isinstance(getattr(cls, f.name, None), Field): + if f.default is MISSING: + # If there's no default, delete the class attribute. + # This happens if we specify field(repr=False), for + # example (that is, we specified a field object, but + # no default value). Also if we're using a default + # factory. The class attribute should not be set at + # all in the post-processed class. + delattr(cls, f.name) + else: + setattr(cls, f.name, f.default) + + # Do we have any Field members that don't also have annotations? + for name, value in cls.__dict__.items(): + if isinstance(value, Field) and not name in cls_annotations: + raise TypeError(f'{name!r} is a field but has no type annotation') + + # Check rules that apply if we are derived from any dataclasses. + if has_dataclass_bases: + # Raise an exception if any of our bases are frozen, but we're not. + if any_frozen_base and not frozen: + raise TypeError('cannot inherit non-frozen dataclass from a ' + 'frozen one') + + # Raise an exception if we're frozen, but none of our bases are. + if not any_frozen_base and frozen: + raise TypeError('cannot inherit frozen dataclass from a ' + 'non-frozen one') + + # Remember all of the fields on our class (including bases). This + # also marks this class as being a dataclass. + setattr(cls, _FIELDS, fields) + + # Was this class defined with an explicit __hash__? Note that if + # __eq__ is defined in this class, then python will automatically + # set __hash__ to None. This is a heuristic, as it's possible + # that such a __hash__ == None was not auto-generated, but it + # close enough. + class_hash = cls.__dict__.get('__hash__', MISSING) + has_explicit_hash = not (class_hash is MISSING or + (class_hash is None and '__eq__' in cls.__dict__)) + + # If we're generating ordering methods, we must be generating the + # eq methods. + if order and not eq: + raise ValueError('eq must be true if order is true') + + if init: + # Does this class have a post-init function? + has_post_init = hasattr(cls, _POST_INIT_NAME) + + # Include InitVars and regular fields (so, not ClassVars). + flds = [f for f in fields.values() + if f._field_type in (_FIELD, _FIELD_INITVAR)] + _set_new_attribute(cls, '__init__', + _init_fn(flds, + frozen, + has_post_init, + # The name to use for the "self" + # param in __init__. Use "self" + # if possible. + '__dataclass_self__' if 'self' in fields + else 'self', + )) + + # Get the fields as a list, and include only real fields. This is + # used in all of the following methods. + field_list = [f for f in fields.values() if f._field_type is _FIELD] + + if repr: + flds = [f for f in field_list if f.repr] + _set_new_attribute(cls, '__repr__', _repr_fn(flds)) + + if eq: + # Create _eq__ method. There's no need for a __ne__ method, + # since python will call __eq__ and negate it. + flds = [f for f in field_list if f.compare] + self_tuple = _tuple_str('self', flds) + other_tuple = _tuple_str('other', flds) + _set_new_attribute(cls, '__eq__', + _cmp_fn('__eq__', '==', + self_tuple, other_tuple)) + + if order: + # Create and set the ordering methods. + flds = [f for f in field_list if f.compare] + self_tuple = _tuple_str('self', flds) + other_tuple = _tuple_str('other', flds) + for name, op in [('__lt__', '<'), + ('__le__', '<='), + ('__gt__', '>'), + ('__ge__', '>='), + ]: + if _set_new_attribute(cls, name, + _cmp_fn(name, op, self_tuple, other_tuple)): + raise TypeError(f'Cannot overwrite attribute {name} ' + f'in class {cls.__name__}. Consider using ' + 'functools.total_ordering') + + if frozen: + for fn in _frozen_get_del_attr(cls, field_list): + if _set_new_attribute(cls, fn.__name__, fn): + raise TypeError(f'Cannot overwrite attribute {fn.__name__} ' + f'in class {cls.__name__}') + + # Decide if/how we're going to create a hash function. + hash_action = _hash_action[bool(unsafe_hash), + bool(eq), + bool(frozen), + has_explicit_hash] + if hash_action: + # No need to call _set_new_attribute here, since by the time + # we're here the overwriting is unconditional. + cls.__hash__ = hash_action(cls, field_list) + + if not getattr(cls, '__doc__'): + # Create a class doc-string. + cls.__doc__ = (cls.__name__ + + str(inspect.signature(cls)).replace(' -> None', '')) + + return cls + + +# _cls should never be specified by keyword, so start it with an +# underscore. The presence of _cls is used to detect if this +# decorator is being called with parameters or not. +def dataclass(_cls=None, *, init=True, repr=True, eq=True, order=False, + unsafe_hash=False, frozen=False): + """Returns the same class as was passed in, with dunder methods + added based on the fields defined in the class. + Examines PEP 526 __annotations__ to determine fields. + If init is true, an __init__() method is added to the class. If + repr is true, a __repr__() method is added. If order is true, rich + comparison dunder methods are added. If unsafe_hash is true, a + __hash__() method function is added. If frozen is true, fields may + not be assigned to after instance creation. + """ + + def wrap(cls): + return _process_class(cls, init, repr, eq, order, unsafe_hash, frozen) + + # See if we're being called as @dataclass or @dataclass(). + if _cls is None: + # We're called with parens. + return wrap + + # We're called as @dataclass without parens. + return wrap(_cls) + + +def fields(class_or_instance): + """Return a tuple describing the fields of this dataclass. + Accepts a dataclass or an instance of one. Tuple elements are of + type Field. + """ + + # Might it be worth caching this, per class? + try: + fields = getattr(class_or_instance, _FIELDS) + except AttributeError: + raise TypeError('must be called with a dataclass type or instance') + + # Exclude pseudo-fields. Note that fields is sorted by insertion + # order, so the order of the tuple is as the fields were defined. + return tuple(f for f in fields.values() if f._field_type is _FIELD) + + +def _is_dataclass_instance(obj): + """Returns True if obj is an instance of a dataclass.""" + return not isinstance(obj, type) and hasattr(obj, _FIELDS) + + +def is_dataclass(obj): + """Returns True if obj is a dataclass or an instance of a + dataclass.""" + return hasattr(obj, _FIELDS) + + +def asdict(obj, *, dict_factory=dict): + """Return the fields of a dataclass instance as a new dictionary mapping + field names to field values. + Example usage: + @dataclass + class C: + x: int + y: int + c = C(1, 2) + assert asdict(c) == {'x': 1, 'y': 2} + If given, 'dict_factory' will be used instead of built-in dict. + The function applies recursively to field values that are + dataclass instances. This will also look into built-in containers: + tuples, lists, and dicts. + """ + if not _is_dataclass_instance(obj): + raise TypeError("asdict() should be called on dataclass instances") + return _asdict_inner(obj, dict_factory) + + +def _asdict_inner(obj, dict_factory): + if _is_dataclass_instance(obj): + result = [] + for f in fields(obj): + value = _asdict_inner(getattr(obj, f.name), dict_factory) + result.append((f.name, value)) + return dict_factory(result) + elif isinstance(obj, (list, tuple)): + return type(obj)(_asdict_inner(v, dict_factory) for v in obj) + elif isinstance(obj, dict): + return type(obj)((_asdict_inner(k, dict_factory), _asdict_inner(v, dict_factory)) + for k, v in obj.items()) + else: + return copy.deepcopy(obj) + + +def astuple(obj, *, tuple_factory=tuple): + """Return the fields of a dataclass instance as a new tuple of field values. + Example usage:: + @dataclass + class C: + x: int + y: int + c = C(1, 2) + assert astuple(c) == (1, 2) + If given, 'tuple_factory' will be used instead of built-in tuple. + The function applies recursively to field values that are + dataclass instances. This will also look into built-in containers: + tuples, lists, and dicts. + """ + + if not _is_dataclass_instance(obj): + raise TypeError("astuple() should be called on dataclass instances") + return _astuple_inner(obj, tuple_factory) + + +def _astuple_inner(obj, tuple_factory): + if _is_dataclass_instance(obj): + result = [] + for f in fields(obj): + value = _astuple_inner(getattr(obj, f.name), tuple_factory) + result.append(value) + return tuple_factory(result) + elif isinstance(obj, (list, tuple)): + return type(obj)(_astuple_inner(v, tuple_factory) for v in obj) + elif isinstance(obj, dict): + return type(obj)((_astuple_inner(k, tuple_factory), _astuple_inner(v, tuple_factory)) + for k, v in obj.items()) + else: + return copy.deepcopy(obj) + + +def make_dataclass(cls_name, fields, *, bases=(), namespace=None, init=True, + repr=True, eq=True, order=False, unsafe_hash=False, + frozen=False): + """Return a new dynamically created dataclass. + The dataclass name will be 'cls_name'. 'fields' is an iterable + of either (name), (name, type) or (name, type, Field) objects. If type is + omitted, use the string 'typing.Any'. Field objects are created by + the equivalent of calling 'field(name, type [, Field-info])'. + C = make_dataclass('C', ['x', ('y', int), ('z', int, field(init=False))], bases=(Base,)) + is equivalent to: + @dataclass + class C(Base): + x: 'typing.Any' + y: int + z: int = field(init=False) + For the bases and namespace parameters, see the builtin type() function. + The parameters init, repr, eq, order, unsafe_hash, and frozen are passed to + dataclass(). + """ + + if namespace is None: + namespace = {} + else: + # Copy namespace since we're going to mutate it. + namespace = namespace.copy() + + # While we're looking through the field names, validate that they + # are identifiers, are not keywords, and not duplicates. + seen = set() + anns = {} + for item in fields: + if isinstance(item, str): + name = item + tp = 'typing.Any' + elif len(item) == 2: + name, tp, = item + elif len(item) == 3: + name, tp, spec = item + namespace[name] = spec + else: + raise TypeError(f'Invalid field: {item!r}') + + if not isinstance(name, str) or not name.isidentifier(): + raise TypeError(f'Field names must be valid identifers: {name!r}') + if keyword.iskeyword(name): + raise TypeError(f'Field names must not be keywords: {name!r}') + if name in seen: + raise TypeError(f'Field name duplicated: {name!r}') + + seen.add(name) + anns[name] = tp + + namespace['__annotations__'] = anns + # We use `types.new_class()` instead of simply `type()` to allow dynamic creation + # of generic dataclassses. + cls = types.new_class(cls_name, bases, {}, lambda ns: ns.update(namespace)) + return dataclass(cls, init=init, repr=repr, eq=eq, order=order, + unsafe_hash=unsafe_hash, frozen=frozen) + + +def replace(obj, **changes): + """Return a new object replacing specified fields with new values. + This is especially useful for frozen classes. Example usage: + @dataclass(frozen=True) + class C: + x: int + y: int + c = C(1, 2) + c1 = replace(c, x=3) + assert c1.x == 3 and c1.y == 2 + """ + + # We're going to mutate 'changes', but that's okay because it's a + # new dict, even if called with 'replace(obj, **my_changes)'. + + if not _is_dataclass_instance(obj): + raise TypeError("replace() should be called on dataclass instances") + + # It's an error to have init=False fields in 'changes'. + # If a field is not in 'changes', read its value from the provided obj. + + for f in getattr(obj, _FIELDS).values(): + if not f.init: + # Error if this field is specified in changes. + if f.name in changes: + raise ValueError(f'field {f.name} is declared with ' + 'init=False, it cannot be specified with ' + 'replace()') + continue + + if f.name not in changes: + changes[f.name] = getattr(obj, f.name) + + # Create the new object, which calls __init__() and + # __post_init__() (if defined), using all of the init fields we've + # added and/or left in 'changes'. If there are values supplied in + # changes that aren't fields, this will correctly raise a + # TypeError. + return obj.__class__(**changes) From 153887e66f97273e14e076e0dafda4b691500fd8 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 29 Oct 2019 01:51:29 -0700 Subject: [PATCH 0148/1439] Start work on new dataclass based config --- isort/settings.py | 60 ++++++++++++ isort/stdlibs/all.py | 3 + isort/stdlibs/py2.py | 4 + isort/stdlibs/py27.py | 4 +- isort/stdlibs/py3.py | 219 +----------------------------------------- isort/stdlibs/py35.py | 216 +++++++++++++++++++++++++++++++++++++++++ isort/stdlibs/py36.py | 217 +++++++++++++++++++++++++++++++++++++++++ isort/stdlibs/py37.py | 218 +++++++++++++++++++++++++++++++++++++++++ isort/stdlibs/py38.py | 217 +++++++++++++++++++++++++++++++++++++++++ 9 files changed, 939 insertions(+), 219 deletions(-) create mode 100644 isort/stdlibs/all.py create mode 100644 isort/stdlibs/py2.py create mode 100644 isort/stdlibs/py35.py create mode 100644 isort/stdlibs/py36.py create mode 100644 isort/stdlibs/py37.py create mode 100644 isort/stdlibs/py38.py diff --git a/isort/settings.py b/isort/settings.py index 6479fc6ad..90dafb1b9 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -18,6 +18,7 @@ from pathlib import Path from typing import Any, Callable, Dict, Iterable, List, Mapping, MutableMapping, Optional, Union +from ._future import dataclass from .stdlibs import py3, py27 from .utils import difference, union from .wrap_modes import WrapModes @@ -91,6 +92,65 @@ def _get_default(py_version: Optional[str]) -> Dict[str, Any]: # Note that none of these lists must be complete as they are simply # fallbacks for when included auto-detection fails. +@dataclass +class Config: + """Defines the configuration parameters used by isort""" + force_to_top = [] + skip = [] + skip_glob = [] + line_length = 79 + wrap_length = 0 + line_ending = None + sections = DEFAULT_SECTIONS + no_sections = False + known_future_library = ["__future__"] + known_third_party = ["google.appengine.api"] + known_first_party = [] + multi_line_output = WrapModes.GRID # type: ignore + forced_separate = [] + indent = " " * 4 + comment_prefix = " #" + length_sort = False + add_imports = [] + remove_imports = [] + reverse_relative = False + force_single_line = False + default_section = "FIRSTPARTY" + import_heading_future = "" + import_heading_stdlib = "" + import_heading_thirdparty = "" + import_heading_firstparty = "" + import_heading_localfolder = "" + balanced_wrapping = False + use_parentheses = False + order_by_type = True + atomic = False + lines_after_imports = -1 + lines_between_sections = 1 + lines_between_types = 0 + combine_as_imports = False + combine_star = False + keep_direct_and_as_imports = False + include_trailing_comma = False + from_first = False + verbose = False + quiet = False + force_adds = False + force_alphabetical_sort_within_sections = False + force_alphabetical_sort = False + force_grid_wrap = 0 + force_sort_within_sections = False + show_diff = False + ignore_whitespace = False + no_lines_before = [] + no_inline_sort = False + ignore_comments = False + safety_excludes = True + case_sensitive = False + + + + default = { "force_to_top": [], "skip": [], diff --git a/isort/stdlibs/all.py b/isort/stdlibs/all.py new file mode 100644 index 000000000..08a365e19 --- /dev/null +++ b/isort/stdlibs/all.py @@ -0,0 +1,3 @@ +from . import py2, py3 + +stdlib = py2.stdlib | py3.stdlib diff --git a/isort/stdlibs/py2.py b/isort/stdlibs/py2.py new file mode 100644 index 000000000..bbc030ca0 --- /dev/null +++ b/isort/stdlibs/py2.py @@ -0,0 +1,4 @@ +from . import py27 + +stdlib = py27.stdlib + diff --git a/isort/stdlibs/py27.py b/isort/stdlibs/py27.py index 0afed938d..ee0c616ae 100644 --- a/isort/stdlibs/py27.py +++ b/isort/stdlibs/py27.py @@ -5,7 +5,7 @@ using the mkstdlibs.py script. """ -stdlib = [ +stdlib = { "AL", "BaseHTTPServer", "Bastion", @@ -291,4 +291,4 @@ "zipfile", "zipimport", "zlib", -] +} diff --git a/isort/stdlibs/py3.py b/isort/stdlibs/py3.py index 09716c90e..78e0984d5 100644 --- a/isort/stdlibs/py3.py +++ b/isort/stdlibs/py3.py @@ -1,218 +1,3 @@ -""" -File contains the standard library of Python 3. +from . import py35, py36, py37, py38 -DO NOT EDIT. If the standard library changes, a new list should be created -using the mkstdlibs.py script. -""" - -stdlib = [ - "_dummy_thread", - "_thread", - "abc", - "aifc", - "argparse", - "array", - "ast", - "asynchat", - "asyncio", - "asyncore", - "atexit", - "audioop", - "base64", - "bdb", - "binascii", - "binhex", - "bisect", - "builtins", - "bz2", - "cProfile", - "calendar", - "cgi", - "cgitb", - "chunk", - "cmath", - "cmd", - "code", - "codecs", - "codeop", - "collections", - "colorsys", - "compileall", - "concurrent", - "configparser", - "contextlib", - "contextvars", - "copy", - "copyreg", - "crypt", - "csv", - "ctypes", - "curses", - "dataclasses", - "datetime", - "dbm", - "decimal", - "difflib", - "dis", - "distutils", - "doctest", - "dummy_threading", - "email", - "encodings", - "ensurepip", - "enum", - "errno", - "faulthandler", - "fcntl", - "filecmp", - "fileinput", - "fnmatch", - "formatter", - "fractions", - "ftplib", - "functools", - "gc", - "getopt", - "getpass", - "gettext", - "glob", - "grp", - "gzip", - "hashlib", - "heapq", - "hmac", - "html", - "http", - "imaplib", - "imghdr", - "imp", - "importlib", - "inspect", - "io", - "ipaddress", - "itertools", - "json", - "keyword", - "lib2to3", - "linecache", - "locale", - "logging", - "lzma", - "macpath", - "mailbox", - "mailcap", - "marshal", - "math", - "mimetypes", - "mmap", - "modulefinder", - "msilib", - "msvcrt", - "multiprocessing", - "netrc", - "nis", - "nntplib", - "numbers", - "operator", - "optparse", - "os", - "ossaudiodev", - "parser", - "pathlib", - "pdb", - "pickle", - "pickletools", - "pipes", - "pkgutil", - "platform", - "plistlib", - "poplib", - "posix", - "pprint", - "profile", - "pstats", - "pty", - "pwd", - "py_compile", - "pyclbr", - "pydoc", - "queue", - "quopri", - "random", - "re", - "readline", - "reprlib", - "resource", - "rlcompleter", - "runpy", - "sched", - "secrets", - "select", - "selectors", - "shelve", - "shlex", - "shutil", - "signal", - "site", - "smtpd", - "smtplib", - "sndhdr", - "socket", - "socketserver", - "spwd", - "sqlite3", - "ssl", - "stat", - "statistics", - "string", - "stringprep", - "struct", - "subprocess", - "sunau", - "symbol", - "symtable", - "sys", - "sysconfig", - "syslog", - "tabnanny", - "tarfile", - "telnetlib", - "tempfile", - "termios", - "test", - "textwrap", - "threading", - "time", - "timeit", - "tkinter", - "token", - "tokenize", - "trace", - "traceback", - "tracemalloc", - "tty", - "turtle", - "turtledemo", - "types", - "typing", - "unicodedata", - "unittest", - "urllib", - "uu", - "uuid", - "venv", - "warnings", - "wave", - "weakref", - "webbrowser", - "winreg", - "winsound", - "wsgiref", - "xdrlib", - "xml", - "xmlrpc", - "zipapp", - "zipfile", - "zipimport", - "zlib", -] +stdlib = py35.stdlib | py36.stdlib | py37.stdlib | py38.stdlib diff --git a/isort/stdlibs/py35.py b/isort/stdlibs/py35.py new file mode 100644 index 000000000..df4707b0a --- /dev/null +++ b/isort/stdlibs/py35.py @@ -0,0 +1,216 @@ +""" +File contains the standard library of Python 3.5. + +DO NOT EDIT. If the standard library changes, a new list should be created +using the mkstdlibs.py script. +""" + +stdlib = { + "_dummy_thread", + "_thread", + "abc", + "aifc", + "argparse", + "array", + "ast", + "asynchat", + "asyncio", + "asyncore", + "atexit", + "audioop", + "base64", + "bdb", + "binascii", + "binhex", + "bisect", + "builtins", + "bz2", + "cProfile", + "calendar", + "cgi", + "cgitb", + "chunk", + "cmath", + "cmd", + "code", + "codecs", + "codeop", + "collections", + "colorsys", + "compileall", + "concurrent", + "configparser", + "contextlib", + "copy", + "copyreg", + "crypt", + "csv", + "ctypes", + "curses", + "datetime", + "dbm", + "decimal", + "difflib", + "dis", + "distutils", + "doctest", + "dummy_threading", + "email", + "encodings", + "ensurepip", + "enum", + "errno", + "faulthandler", + "fcntl", + "filecmp", + "fileinput", + "fnmatch", + "formatter", + "fpectl", + "fractions", + "ftplib", + "functools", + "gc", + "getopt", + "getpass", + "gettext", + "glob", + "grp", + "gzip", + "hashlib", + "heapq", + "hmac", + "html", + "http", + "imaplib", + "imghdr", + "imp", + "importlib", + "inspect", + "io", + "ipaddress", + "itertools", + "json", + "keyword", + "lib2to3", + "linecache", + "locale", + "logging", + "lzma", + "macpath", + "mailbox", + "mailcap", + "marshal", + "math", + "mimetypes", + "mmap", + "modulefinder", + "msilib", + "msvcrt", + "multiprocessing", + "netrc", + "nis", + "nntplib", + "numbers", + "operator", + "optparse", + "os", + "ossaudiodev", + "parser", + "pathlib", + "pdb", + "pickle", + "pickletools", + "pipes", + "pkgutil", + "platform", + "plistlib", + "poplib", + "posix", + "pprint", + "profile", + "pstats", + "pty", + "pwd", + "py_compile", + "pyclbr", + "pydoc", + "queue", + "quopri", + "random", + "re", + "readline", + "reprlib", + "resource", + "rlcompleter", + "runpy", + "sched", + "select", + "selectors", + "shelve", + "shlex", + "shutil", + "signal", + "site", + "smtpd", + "smtplib", + "sndhdr", + "socket", + "socketserver", + "spwd", + "sqlite3", + "ssl", + "stat", + "statistics", + "string", + "stringprep", + "struct", + "subprocess", + "sunau", + "symbol", + "symtable", + "sys", + "sysconfig", + "syslog", + "tabnanny", + "tarfile", + "telnetlib", + "tempfile", + "termios", + "test", + "textwrap", + "threading", + "time", + "timeit", + "tkinter", + "token", + "tokenize", + "trace", + "traceback", + "tracemalloc", + "tty", + "turtle", + "turtledemo", + "types", + "typing", + "unicodedata", + "unittest", + "urllib", + "uu", + "uuid", + "venv", + "warnings", + "wave", + "weakref", + "webbrowser", + "winreg", + "winsound", + "wsgiref", + "xdrlib", + "xml", + "xmlrpc", + "zipapp", + "zipfile", + "zipimport", + "zlib", +} diff --git a/isort/stdlibs/py36.py b/isort/stdlibs/py36.py new file mode 100644 index 000000000..e9cd206ba --- /dev/null +++ b/isort/stdlibs/py36.py @@ -0,0 +1,217 @@ +""" +File contains the standard library of Python 3.6. + +DO NOT EDIT. If the standard library changes, a new list should be created +using the mkstdlibs.py script. +""" + +stdlib = { + "_dummy_thread", + "_thread", + "abc", + "aifc", + "argparse", + "array", + "ast", + "asynchat", + "asyncio", + "asyncore", + "atexit", + "audioop", + "base64", + "bdb", + "binascii", + "binhex", + "bisect", + "builtins", + "bz2", + "cProfile", + "calendar", + "cgi", + "cgitb", + "chunk", + "cmath", + "cmd", + "code", + "codecs", + "codeop", + "collections", + "colorsys", + "compileall", + "concurrent", + "configparser", + "contextlib", + "copy", + "copyreg", + "crypt", + "csv", + "ctypes", + "curses", + "datetime", + "dbm", + "decimal", + "difflib", + "dis", + "distutils", + "doctest", + "dummy_threading", + "email", + "encodings", + "ensurepip", + "enum", + "errno", + "faulthandler", + "fcntl", + "filecmp", + "fileinput", + "fnmatch", + "formatter", + "fpectl", + "fractions", + "ftplib", + "functools", + "gc", + "getopt", + "getpass", + "gettext", + "glob", + "grp", + "gzip", + "hashlib", + "heapq", + "hmac", + "html", + "http", + "imaplib", + "imghdr", + "imp", + "importlib", + "inspect", + "io", + "ipaddress", + "itertools", + "json", + "keyword", + "lib2to3", + "linecache", + "locale", + "logging", + "lzma", + "macpath", + "mailbox", + "mailcap", + "marshal", + "math", + "mimetypes", + "mmap", + "modulefinder", + "msilib", + "msvcrt", + "multiprocessing", + "netrc", + "nis", + "nntplib", + "numbers", + "operator", + "optparse", + "os", + "ossaudiodev", + "parser", + "pathlib", + "pdb", + "pickle", + "pickletools", + "pipes", + "pkgutil", + "platform", + "plistlib", + "poplib", + "posix", + "pprint", + "profile", + "pstats", + "pty", + "pwd", + "py_compile", + "pyclbr", + "pydoc", + "queue", + "quopri", + "random", + "re", + "readline", + "reprlib", + "resource", + "rlcompleter", + "runpy", + "sched", + "secrets", + "select", + "selectors", + "shelve", + "shlex", + "shutil", + "signal", + "site", + "smtpd", + "smtplib", + "sndhdr", + "socket", + "socketserver", + "spwd", + "sqlite3", + "ssl", + "stat", + "statistics", + "string", + "stringprep", + "struct", + "subprocess", + "sunau", + "symbol", + "symtable", + "sys", + "sysconfig", + "syslog", + "tabnanny", + "tarfile", + "telnetlib", + "tempfile", + "termios", + "test", + "textwrap", + "threading", + "time", + "timeit", + "tkinter", + "token", + "tokenize", + "trace", + "traceback", + "tracemalloc", + "tty", + "turtle", + "turtledemo", + "types", + "typing", + "unicodedata", + "unittest", + "urllib", + "uu", + "uuid", + "venv", + "warnings", + "wave", + "weakref", + "webbrowser", + "winreg", + "winsound", + "wsgiref", + "xdrlib", + "xml", + "xmlrpc", + "zipapp", + "zipfile", + "zipimport", + "zlib", +} diff --git a/isort/stdlibs/py37.py b/isort/stdlibs/py37.py new file mode 100644 index 000000000..29f3f9527 --- /dev/null +++ b/isort/stdlibs/py37.py @@ -0,0 +1,218 @@ +""" +File contains the standard library of Python 3.7. + +DO NOT EDIT. If the standard library changes, a new list should be created +using the mkstdlibs.py script. +""" + +stdlib = { + "_dummy_thread", + "_thread", + "abc", + "aifc", + "argparse", + "array", + "ast", + "asynchat", + "asyncio", + "asyncore", + "atexit", + "audioop", + "base64", + "bdb", + "binascii", + "binhex", + "bisect", + "builtins", + "bz2", + "cProfile", + "calendar", + "cgi", + "cgitb", + "chunk", + "cmath", + "cmd", + "code", + "codecs", + "codeop", + "collections", + "colorsys", + "compileall", + "concurrent", + "configparser", + "contextlib", + "contextvars", + "copy", + "copyreg", + "crypt", + "csv", + "ctypes", + "curses", + "dataclasses", + "datetime", + "dbm", + "decimal", + "difflib", + "dis", + "distutils", + "doctest", + "dummy_threading", + "email", + "encodings", + "ensurepip", + "enum", + "errno", + "faulthandler", + "fcntl", + "filecmp", + "fileinput", + "fnmatch", + "formatter", + "fractions", + "ftplib", + "functools", + "gc", + "getopt", + "getpass", + "gettext", + "glob", + "grp", + "gzip", + "hashlib", + "heapq", + "hmac", + "html", + "http", + "imaplib", + "imghdr", + "imp", + "importlib", + "inspect", + "io", + "ipaddress", + "itertools", + "json", + "keyword", + "lib2to3", + "linecache", + "locale", + "logging", + "lzma", + "macpath", + "mailbox", + "mailcap", + "marshal", + "math", + "mimetypes", + "mmap", + "modulefinder", + "msilib", + "msvcrt", + "multiprocessing", + "netrc", + "nis", + "nntplib", + "numbers", + "operator", + "optparse", + "os", + "ossaudiodev", + "parser", + "pathlib", + "pdb", + "pickle", + "pickletools", + "pipes", + "pkgutil", + "platform", + "plistlib", + "poplib", + "posix", + "pprint", + "profile", + "pstats", + "pty", + "pwd", + "py_compile", + "pyclbr", + "pydoc", + "queue", + "quopri", + "random", + "re", + "readline", + "reprlib", + "resource", + "rlcompleter", + "runpy", + "sched", + "secrets", + "select", + "selectors", + "shelve", + "shlex", + "shutil", + "signal", + "site", + "smtpd", + "smtplib", + "sndhdr", + "socket", + "socketserver", + "spwd", + "sqlite3", + "ssl", + "stat", + "statistics", + "string", + "stringprep", + "struct", + "subprocess", + "sunau", + "symbol", + "symtable", + "sys", + "sysconfig", + "syslog", + "tabnanny", + "tarfile", + "telnetlib", + "tempfile", + "termios", + "test", + "textwrap", + "threading", + "time", + "timeit", + "tkinter", + "token", + "tokenize", + "trace", + "traceback", + "tracemalloc", + "tty", + "turtle", + "turtledemo", + "types", + "typing", + "unicodedata", + "unittest", + "urllib", + "uu", + "uuid", + "venv", + "warnings", + "wave", + "weakref", + "webbrowser", + "winreg", + "winsound", + "wsgiref", + "xdrlib", + "xml", + "xmlrpc", + "zipapp", + "zipfile", + "zipimport", + "zlib", +} diff --git a/isort/stdlibs/py38.py b/isort/stdlibs/py38.py new file mode 100644 index 000000000..69d9ec21c --- /dev/null +++ b/isort/stdlibs/py38.py @@ -0,0 +1,217 @@ +""" +File contains the standard library of Python 3.8. + +DO NOT EDIT. If the standard library changes, a new list should be created +using the mkstdlibs.py script. +""" + +stdlib = { + "_dummy_thread", + "_thread", + "abc", + "aifc", + "argparse", + "array", + "ast", + "asynchat", + "asyncio", + "asyncore", + "atexit", + "audioop", + "base64", + "bdb", + "binascii", + "binhex", + "bisect", + "builtins", + "bz2", + "cProfile", + "calendar", + "cgi", + "cgitb", + "chunk", + "cmath", + "cmd", + "code", + "codecs", + "codeop", + "collections", + "colorsys", + "compileall", + "concurrent", + "configparser", + "contextlib", + "contextvars", + "copy", + "copyreg", + "crypt", + "csv", + "ctypes", + "curses", + "dataclasses", + "datetime", + "dbm", + "decimal", + "difflib", + "dis", + "distutils", + "doctest", + "dummy_threading", + "email", + "encodings", + "ensurepip", + "enum", + "errno", + "faulthandler", + "fcntl", + "filecmp", + "fileinput", + "fnmatch", + "formatter", + "fractions", + "ftplib", + "functools", + "gc", + "getopt", + "getpass", + "gettext", + "glob", + "grp", + "gzip", + "hashlib", + "heapq", + "hmac", + "html", + "http", + "imaplib", + "imghdr", + "imp", + "importlib", + "inspect", + "io", + "ipaddress", + "itertools", + "json", + "keyword", + "lib2to3", + "linecache", + "locale", + "logging", + "lzma", + "mailbox", + "mailcap", + "marshal", + "math", + "mimetypes", + "mmap", + "modulefinder", + "msilib", + "msvcrt", + "multiprocessing", + "netrc", + "nis", + "nntplib", + "numbers", + "operator", + "optparse", + "os", + "ossaudiodev", + "parser", + "pathlib", + "pdb", + "pickle", + "pickletools", + "pipes", + "pkgutil", + "platform", + "plistlib", + "poplib", + "posix", + "pprint", + "profile", + "pstats", + "pty", + "pwd", + "py_compile", + "pyclbr", + "pydoc", + "queue", + "quopri", + "random", + "re", + "readline", + "reprlib", + "resource", + "rlcompleter", + "runpy", + "sched", + "secrets", + "select", + "selectors", + "shelve", + "shlex", + "shutil", + "signal", + "site", + "smtpd", + "smtplib", + "sndhdr", + "socket", + "socketserver", + "spwd", + "sqlite3", + "ssl", + "stat", + "statistics", + "string", + "stringprep", + "struct", + "subprocess", + "sunau", + "symbol", + "symtable", + "sys", + "sysconfig", + "syslog", + "tabnanny", + "tarfile", + "telnetlib", + "tempfile", + "termios", + "test", + "textwrap", + "threading", + "time", + "timeit", + "tkinter", + "token", + "tokenize", + "trace", + "traceback", + "tracemalloc", + "tty", + "turtle", + "turtledemo", + "types", + "typing", + "unicodedata", + "unittest", + "urllib", + "uu", + "uuid", + "venv", + "warnings", + "wave", + "weakref", + "webbrowser", + "winreg", + "winsound", + "wsgiref", + "xdrlib", + "xml", + "xmlrpc", + "zipapp", + "zipfile", + "zipimport", + "zlib", +} From e7b27fa53901c278137b815ba2956ae036f84314 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 29 Oct 2019 01:51:49 -0700 Subject: [PATCH 0149/1439] More fine grained python stdlib inclusion --- pyproject.toml | 1 + scripts/mkstdlibs.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f2c8e169e..727e63478 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,6 +65,7 @@ pipreqs = "^0.4.9" tomlkit = "0.5.3" pip_api = "^0.0.12" numpy = "^1.16.0" +sphinx = "^2.2" [tool.poetry.scripts] isort = "isort.main:main" diff --git a/scripts/mkstdlibs.py b/scripts/mkstdlibs.py index 2a4a50f01..a79836bab 100755 --- a/scripts/mkstdlibs.py +++ b/scripts/mkstdlibs.py @@ -4,7 +4,7 @@ URL = "https://docs.python.org/{}/objects.inv" PATH = "isort/stdlibs/py{}.py" -VERSIONS = [("2", "7"), ("3",)] +VERSIONS = [("2", "7"), ("3", "5"), ("3", "6"), ("3", "7"), ("3", "8")] DOCSTRING = """ File contains the standard library of Python {}. @@ -39,7 +39,7 @@ class FakeApp: with open(path, "w") as stdlib_file: docstring = DOCSTRING.format(version) stdlib_file.write(f'"""{docstring}"""\n\n') - stdlib_file.write("stdlib = [\n") + stdlib_file.write("stdlib = {\n") for module in sorted(modules): stdlib_file.write(f' "{module}",\n') - stdlib_file.write("]\n") + stdlib_file.write("}\n") From 9e25eb32a3c7909e77f17970407fa7d4d93910ae Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 29 Oct 2019 02:32:57 -0700 Subject: [PATCH 0150/1439] Improve python version targeting --- isort/main.py | 8 ++++++-- isort/settings.py | 42 ++++++++++----------------------------- isort/stdlibs/__init__.py | 1 + 3 files changed, 18 insertions(+), 33 deletions(-) diff --git a/isort/main.py b/isort/main.py index 511f84751..de33eaa96 100644 --- a/isort/main.py +++ b/isort/main.py @@ -14,6 +14,7 @@ from isort.logo import ASCII_ART from isort.settings import ( DEFAULT_SECTIONS, + VALID_PY_TARGETS, WrapModes, default, file_should_be_skipped, @@ -539,8 +540,11 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: "--python-version", action="store", dest="py_version", - help="Tells isort to sort the standard library based on the python version. " - "Default is the version of the running interpreter, for instance: -py 3, -py 2.7", + choices=VALID_PY_TARGETS + ("auto", ), + help="Tells isort to sort the standard library based on the the specified python version." + "Default is to assume any python version could be the target, and use a union off all " + "stdlib modules across versions. If auto is specified, the version of the interpreter " + f"used to run isort (py{sys.version_info.major}{sys.version_info.minor}) will be used.", ) arguments = {key: value for key, value in vars(parser.parse_args(argv)).items() if value} diff --git a/isort/settings.py b/isort/settings.py index 90dafb1b9..9011cec48 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -19,7 +19,7 @@ from typing import Any, Callable, Dict, Iterable, List, Mapping, MutableMapping, Optional, Union from ._future import dataclass -from .stdlibs import py3, py27 +from . import stdlibs from .utils import difference, union from .wrap_modes import WrapModes from .wrap_modes import from_string as wrap_mode_from_string @@ -46,48 +46,28 @@ r"/(\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|_build|buck-out|build|dist|\.pants\.d" r"|lib/python[0-9].[0-9]+|node_modules)/" ) +VALID_PY_TARGETS = tuple(target for target in dir(stdlibs) if not target.startswith("_")) def _get_default(py_version: Optional[str]) -> Dict[str, Any]: - """ - Returns the correct standard library based on either the passed py_version flag or the python + """Returns the correct standard library based on either the passed py_version flag or the python interpreter. Additionaly users have the option to pass all as value instead of an version. As an result code will be checked against both standard libraries - python2 & python3 See Issue 889 and 778 for more information """ + py_version = py_version or "all" + if py_version == "auto": + py_version = f"{sys.version_info.major}{sys.version_info.minor}" - if py_version is None: - major, minor = sys.version_info[0:2] - elif py_version != "all": - minor = 0 - - # we have a minor - if "." in py_version: - # we do not care about patches just major and minor - splits = py_version.split(".")[0:2] - major = int(splits[0]) - minor = int(splits[1]) - else: - major = int(py_version) - - _default = default.copy() - - if py_version == "all": - standard_library = list(set(py3.stdlib + py27.stdlib)) - elif major == 3: - standard_library = py3.stdlib - elif major == 2 and minor == 7: - standard_library = py27.stdlib - else: + if py_version not in VALID_PY_TARGETS: raise ValueError( - "The python version %s is not supported. " - "You can set a python version with the -py or --python-version flag " % py_version + f"The python version {py_version} is not supported. " + "You can set a python version with the -py or --python-version flag. " + f"The following versions are supported: {VALID_PY_TARGETS}" ) - _default["known_standard_library"] = standard_library - - return _default + return {**default, "known_standard_library": getattr(stdlibs, py_version)} # Note that none of these lists must be complete as they are simply diff --git a/isort/stdlibs/__init__.py b/isort/stdlibs/__init__.py index e69de29bb..653ed067d 100644 --- a/isort/stdlibs/__init__.py +++ b/isort/stdlibs/__init__.py @@ -0,0 +1 @@ +from . import all, py2, py27, py3, py35, py36, py37, py38 From a64fa0632de593f7fc81a968cce16a83392cf98b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 29 Oct 2019 23:24:48 -0700 Subject: [PATCH 0151/1439] Start work on moving configuration objects to use dataclasses --- isort/_future/__init__.py | 9 +- isort/_future/_dataclasses.py | 544 ++++++++++++++++++---------------- isort/compat.py | 5 +- isort/finders.py | 4 +- isort/format.py | 4 +- isort/hooks.py | 1 + isort/isort.py | 3 +- isort/main.py | 6 +- isort/output.py | 3 +- isort/parse.py | 5 +- isort/pylama_isort.py | 5 +- isort/settings.py | 134 +++++---- isort/sorting.py | 1 + isort/stdlibs/__init__.py | 2 +- isort/stdlibs/py2.py | 1 - isort/utils.py | 4 +- isort/wrap.py | 1 + isort/wrap_modes.py | 1 + tests/test_output.py | 3 +- tests/test_wrap_modes.py | 3 +- 20 files changed, 404 insertions(+), 335 deletions(-) diff --git a/isort/_future/__init__.py b/isort/_future/__init__.py index 3e5131fdc..9f5e9d183 100644 --- a/isort/_future/__init__.py +++ b/isort/_future/__init__.py @@ -1,10 +1,11 @@ import sys if sys.version_info.major <= 3 and sys.version_info.minor <= 6: - from . import _dataclasses as dataclasses + from . import _dataclasses as dataclasses # type: ignore else: - import dataclasses + import dataclasses # type: ignore -dataclass = dataclasses.dataclass +dataclass = dataclasses.dataclass # type: ignore +field = dataclasses.field # type: ignore -__all__ = ["dataclasses", "dataclass"] +__all__ = ["dataclasses", "dataclass", "field"] diff --git a/isort/_future/_dataclasses.py b/isort/_future/_dataclasses.py index ef37ae5dd..1f7fbb591 100644 --- a/isort/_future/_dataclasses.py +++ b/isort/_future/_dataclasses.py @@ -1,3 +1,5 @@ +# type: ignore +# flake8: noqa """Backport of Python3.7 dataclasses Library Taken directly from here: https://github.com/ericvsmith/dataclasses @@ -7,28 +9,29 @@ TODO: Remove once isort only supports 3.7+ """ -import re -import sys import copy -import types import inspect import keyword +import re +import types -__all__ = ['dataclass', - 'field', - 'Field', - 'FrozenInstanceError', - 'InitVar', - 'MISSING', - - # Helper functions. - 'fields', - 'asdict', - 'astuple', - 'make_dataclass', - 'replace', - 'is_dataclass', - ] +import sys + +__all__ = [ + "dataclass", + "field", + "Field", + "FrozenInstanceError", + "InitVar", + "MISSING", + # Helper functions. + "fields", + "asdict", + "astuple", + "make_dataclass", + "replace", + "is_dataclass", +] # Conditions for adding methods. The boxes indicate what action the # dataclass decorator takes. For all of these tables, when I talk @@ -157,20 +160,26 @@ # Raised when an attempt is made to modify a frozen class. -class FrozenInstanceError(AttributeError): pass +class FrozenInstanceError(AttributeError): + pass + # A sentinel object for default values to signal that a default # factory will be used. This is given a nice repr() which will appear # in the function signature of dataclasses' constructors. class _HAS_DEFAULT_FACTORY_CLASS: def __repr__(self): - return '' + return "" + + _HAS_DEFAULT_FACTORY = _HAS_DEFAULT_FACTORY_CLASS() # A sentinel object to detect if a parameter is supplied or not. Use # a class to give it a better repr. class _MISSING_TYPE: pass + + MISSING = _MISSING_TYPE() # Since most per-field metadata will be unused, create an empty @@ -181,33 +190,38 @@ class _MISSING_TYPE: class _FIELD_BASE: def __init__(self, name): self.name = name + def __repr__(self): return self.name -_FIELD = _FIELD_BASE('_FIELD') -_FIELD_CLASSVAR = _FIELD_BASE('_FIELD_CLASSVAR') -_FIELD_INITVAR = _FIELD_BASE('_FIELD_INITVAR') + + +_FIELD = _FIELD_BASE("_FIELD") +_FIELD_CLASSVAR = _FIELD_BASE("_FIELD_CLASSVAR") +_FIELD_INITVAR = _FIELD_BASE("_FIELD_INITVAR") # The name of an attribute on the class where we store the Field # objects. Also used to check if a class is a Data Class. -_FIELDS = '__dataclass_fields__' +_FIELDS = "__dataclass_fields__" # The name of an attribute on the class that stores the parameters to # @dataclass. -_PARAMS = '__dataclass_params__' +_PARAMS = "__dataclass_params__" # The name of the function, that if it exists, is called at the end of # __init__. -_POST_INIT_NAME = '__post_init__' +_POST_INIT_NAME = "__post_init__" # String regex that string annotations for ClassVar or InitVar must match. # Allows "identifier.identifier[" or "identifier[". # https://bugs.python.org/issue33453 for details. -_MODULE_IDENTIFIER_RE = re.compile(r'^(?:\s*(\w+)\s*\.)?\s*(\w+)') +_MODULE_IDENTIFIER_RE = re.compile(r"^(?:\s*(\w+)\s*\.)?\s*(\w+)") + class _InitVarMeta(type): def __getitem__(self, params): return self + class InitVar(metaclass=_InitVarMeta): pass @@ -223,20 +237,20 @@ class InitVar(metaclass=_InitVarMeta): # When cls._FIELDS is filled in with a list of Field objects, the name # and type fields will have been populated. class Field: - __slots__ = ('name', - 'type', - 'default', - 'default_factory', - 'repr', - 'hash', - 'init', - 'compare', - 'metadata', - '_field_type', # Private: not to be used by user code. - ) - - def __init__(self, default, default_factory, init, repr, hash, compare, - metadata): + __slots__ = ( + "name", + "type", + "default", + "default_factory", + "repr", + "hash", + "init", + "compare", + "metadata", + "_field_type", # Private: not to be used by user code. + ) + + def __init__(self, default, default_factory, init, repr, hash, compare, metadata): self.name = None self.type = None self.default = default @@ -245,24 +259,28 @@ def __init__(self, default, default_factory, init, repr, hash, compare, self.repr = repr self.hash = hash self.compare = compare - self.metadata = (_EMPTY_METADATA - if metadata is None or len(metadata) == 0 else - types.MappingProxyType(metadata)) + self.metadata = ( + _EMPTY_METADATA + if metadata is None or len(metadata) == 0 + else types.MappingProxyType(metadata) + ) self._field_type = None def __repr__(self): - return ('Field(' - f'name={self.name!r},' - f'type={self.type!r},' - f'default={self.default!r},' - f'default_factory={self.default_factory!r},' - f'init={self.init!r},' - f'repr={self.repr!r},' - f'hash={self.hash!r},' - f'compare={self.compare!r},' - f'metadata={self.metadata!r},' - f'_field_type={self._field_type}' - ')') + return ( + "Field(" + f"name={self.name!r}," + f"type={self.type!r}," + f"default={self.default!r}," + f"default_factory={self.default_factory!r}," + f"init={self.init!r}," + f"repr={self.repr!r}," + f"hash={self.hash!r}," + f"compare={self.compare!r}," + f"metadata={self.metadata!r}," + f"_field_type={self._field_type}" + ")" + ) # This is used to support the PEP 487 __set_name__ protocol in the # case where we're using a field that contains a descriptor as a @@ -273,7 +291,7 @@ def __repr__(self): # with the default value, so the end result is a descriptor that # had __set_name__ called on it at the right time. def __set_name__(self, owner, name): - func = getattr(type(self.default), '__set_name__', None) + func = getattr(type(self.default), "__set_name__", None) if func: # There is a __set_name__ method on the descriptor, call # it. @@ -281,13 +299,7 @@ def __set_name__(self, owner, name): class _DataclassParams: - __slots__ = ('init', - 'repr', - 'eq', - 'order', - 'unsafe_hash', - 'frozen', - ) + __slots__ = ("init", "repr", "eq", "order", "unsafe_hash", "frozen") def __init__(self, init, repr, eq, order, unsafe_hash, frozen): self.init = init @@ -298,21 +310,31 @@ def __init__(self, init, repr, eq, order, unsafe_hash, frozen): self.frozen = frozen def __repr__(self): - return ('_DataclassParams(' - f'init={self.init!r},' - f'repr={self.repr!r},' - f'eq={self.eq!r},' - f'order={self.order!r},' - f'unsafe_hash={self.unsafe_hash!r},' - f'frozen={self.frozen!r}' - ')') + return ( + "_DataclassParams(" + f"init={self.init!r}," + f"repr={self.repr!r}," + f"eq={self.eq!r}," + f"order={self.order!r}," + f"unsafe_hash={self.unsafe_hash!r}," + f"frozen={self.frozen!r}" + ")" + ) # This function is used instead of exposing Field creation directly, # so that a type checker can be told (via overloads) that this is a # function whose type depends on its parameters. -def field(*, default=MISSING, default_factory=MISSING, init=True, repr=True, - hash=None, compare=True, metadata=None): +def field( + *, + default=MISSING, + default_factory=MISSING, + init=True, + repr=True, + hash=None, + compare=True, + metadata=None, +): """Return an object to identify dataclass fields. default is the default value of the field. default_factory is a 0-argument function called to initialize a field's value. If init @@ -326,9 +348,8 @@ def field(*, default=MISSING, default_factory=MISSING, init=True, repr=True, """ if default is not MISSING and default_factory is not MISSING: - raise ValueError('cannot specify both default and default_factory') - return Field(default, default_factory, init, repr, hash, compare, - metadata) + raise ValueError("cannot specify both default and default_factory") + return Field(default, default_factory, init, repr, hash, compare, metadata) def _tuple_str(obj_name, fields): @@ -338,29 +359,28 @@ def _tuple_str(obj_name, fields): # Special case for the 0-tuple. if not fields: - return '()' + return "()" # Note the trailing comma, needed if this turns out to be a 1-tuple. return f'({",".join([f"{obj_name}.{f.name}" for f in fields])},)' -def _create_fn(name, args, body, *, globals=None, locals=None, - return_type=MISSING): +def _create_fn(name, args, body, *, globals=None, locals=None, return_type=MISSING): # Note that we mutate locals when exec() is called. Caller # beware! The only callers are internal to this module, so no # worries about external callers. if locals is None: locals = {} - return_annotation = '' + return_annotation = "" if return_type is not MISSING: - locals['_return_type'] = return_type - return_annotation = '->_return_type' - args = ','.join(args) - body = '\n'.join(f' {b}' for b in body) + locals["_return_type"] = return_type + return_annotation = "->_return_type" + args = ",".join(args) + body = "\n".join(f" {b}" for b in body) # Compute the text of the entire function. - txt = f'def {name}({args}){return_annotation}:\n{body}' + txt = f"def {name}({args}){return_annotation}:\n{body}" - exec(txt, globals, locals) + exec(txt, globals, locals) # nosec return locals[name] @@ -372,23 +392,21 @@ def _field_assign(frozen, name, value, self_name): # self_name is what "self" is called in this function: don't # hard-code "self", since that might be a field name. if frozen: - return f'object.__setattr__({self_name},{name!r},{value})' - return f'{self_name}.{name}={value}' + return f"object.__setattr__({self_name},{name!r},{value})" + return f"{self_name}.{name}={value}" def _field_init(f, frozen, globals, self_name): # Return the text of the line in the body of __init__ that will # initialize this field. - default_name = f'_dflt_{f.name}' + default_name = f"_dflt_{f.name}" if f.default_factory is not MISSING: if f.init: # This field has a default factory. If a parameter is # given, use it. If not, call the factory. globals[default_name] = f.default_factory - value = (f'{default_name}() ' - f'if {f.name} is _HAS_DEFAULT_FACTORY ' - f'else {f.name}') + value = f"{default_name}() " f"if {f.name} is _HAS_DEFAULT_FACTORY " f"else {f.name}" else: # This is a field that's not in the __init__ params, but # has a default factory function. It needs to be @@ -405,7 +423,7 @@ def _field_init(f, frozen, globals, self_name): # (which, after all, is why we have a factory function!). globals[default_name] = f.default_factory - value = f'{default_name}()' + value = f"{default_name}()" else: # No default factory. if f.init: @@ -438,15 +456,15 @@ def _init_param(f): if f.default is MISSING and f.default_factory is MISSING: # There's no default, and no default_factory, just output the # variable name and type. - default = '' + default = "" elif f.default is not MISSING: # There's a default, this will be the name that's used to look # it up. - default = f'=_dflt_{f.name}' + default = f"=_dflt_{f.name}" elif f.default_factory is not MISSING: # There's a factory function. Set a marker. - default = '=_HAS_DEFAULT_FACTORY' - return f'{f.name}:_type_{f.name}{default}' + default = "=_HAS_DEFAULT_FACTORY" + return f"{f.name}:_type_{f.name}{default}" def _init_fn(fields, frozen, has_post_init, self_name): @@ -464,11 +482,9 @@ def _init_fn(fields, frozen, has_post_init, self_name): if not (f.default is MISSING and f.default_factory is MISSING): seen_default = True elif seen_default: - raise TypeError(f'non-default argument {f.name!r} ' - 'follows default argument') + raise TypeError(f"non-default argument {f.name!r} " "follows default argument") - globals = {'MISSING': MISSING, - '_HAS_DEFAULT_FACTORY': _HAS_DEFAULT_FACTORY} + globals = {"MISSING": MISSING, "_HAS_DEFAULT_FACTORY": _HAS_DEFAULT_FACTORY} body_lines = [] for f in fields: @@ -480,55 +496,67 @@ def _init_fn(fields, frozen, has_post_init, self_name): # Does this class have a post-init function? if has_post_init: - params_str = ','.join(f.name for f in fields - if f._field_type is _FIELD_INITVAR) - body_lines.append(f'{self_name}.{_POST_INIT_NAME}({params_str})') + params_str = ",".join(f.name for f in fields if f._field_type is _FIELD_INITVAR) + body_lines.append(f"{self_name}.{_POST_INIT_NAME}({params_str})") # If no body lines, use 'pass'. if not body_lines: - body_lines = ['pass'] + body_lines = ["pass"] - locals = {f'_type_{f.name}': f.type for f in fields} - return _create_fn('__init__', - [self_name] + [_init_param(f) for f in fields if f.init], - body_lines, - locals=locals, - globals=globals, - return_type=None) + locals = {f"_type_{f.name}": f.type for f in fields} + return _create_fn( + "__init__", + [self_name] + [_init_param(f) for f in fields if f.init], + body_lines, + locals=locals, + globals=globals, + return_type=None, + ) def _repr_fn(fields): - return _create_fn('__repr__', - ('self',), - ['return self.__class__.__qualname__ + f"(' + - ', '.join([f"{f.name}={{self.{f.name}!r}}" - for f in fields]) + - ')"']) + return _create_fn( + "__repr__", + ("self",), + [ + 'return self.__class__.__qualname__ + f"(' + + ", ".join([f"{f.name}={{self.{f.name}!r}}" for f in fields]) + + ')"' + ], + ) def _frozen_get_del_attr(cls, fields): # XXX: globals is modified on the first call to _create_fn, then # the modified version is used in the second call. Is this okay? - globals = {'cls': cls, - 'FrozenInstanceError': FrozenInstanceError} + globals = {"cls": cls, "FrozenInstanceError": FrozenInstanceError} if fields: - fields_str = '(' + ','.join(repr(f.name) for f in fields) + ',)' + fields_str = "(" + ",".join(repr(f.name) for f in fields) + ",)" else: # Special case for the zero-length tuple. - fields_str = '()' - return (_create_fn('__setattr__', - ('self', 'name', 'value'), - (f'if type(self) is cls or name in {fields_str}:', - ' raise FrozenInstanceError(f"cannot assign to field {name!r}")', - f'super(cls, self).__setattr__(name, value)'), - globals=globals), - _create_fn('__delattr__', - ('self', 'name'), - (f'if type(self) is cls or name in {fields_str}:', - ' raise FrozenInstanceError(f"cannot delete field {name!r}")', - f'super(cls, self).__delattr__(name)'), - globals=globals), - ) + fields_str = "()" + return ( + _create_fn( + "__setattr__", + ("self", "name", "value"), + ( + f"if type(self) is cls or name in {fields_str}:", + ' raise FrozenInstanceError(f"cannot assign to field {name!r}")', + f"super(cls, self).__setattr__(name, value)", + ), + globals=globals, + ), + _create_fn( + "__delattr__", + ("self", "name"), + ( + f"if type(self) is cls or name in {fields_str}:", + ' raise FrozenInstanceError(f"cannot delete field {name!r}")', + f"super(cls, self).__delattr__(name)", + ), + globals=globals, + ), + ) def _cmp_fn(name, op, self_tuple, other_tuple): @@ -537,18 +565,20 @@ def _cmp_fn(name, op, self_tuple, other_tuple): # '(self.x,self.y)' and other_tuple is the string # '(other.x,other.y)'. - return _create_fn(name, - ('self', 'other'), - [ 'if other.__class__ is self.__class__:', - f' return {self_tuple}{op}{other_tuple}', - 'return NotImplemented']) + return _create_fn( + name, + ("self", "other"), + [ + "if other.__class__ is self.__class__:", + f" return {self_tuple}{op}{other_tuple}", + "return NotImplemented", + ], + ) def _hash_fn(fields): - self_tuple = _tuple_str('self', fields) - return _create_fn('__hash__', - ('self',), - [f'return hash({self_tuple})']) + self_tuple = _tuple_str("self", fields) + return _create_fn("__hash__", ("self",), [f"return hash({self_tuple})"]) def _is_classvar(a_type, typing): @@ -661,12 +691,11 @@ def _get_field(cls, a_name, a_type): # annotation to be a ClassVar. So, only look for ClassVar if # typing has been imported by any module (not necessarily cls's # module). - typing = sys.modules.get('typing') + typing = sys.modules.get("typing") if typing: - if (_is_classvar(a_type, typing) - or (isinstance(f.type, str) - and _is_type(f.type, cls, typing, typing.ClassVar, - _is_classvar))): + if _is_classvar(a_type, typing) or ( + isinstance(f.type, str) and _is_type(f.type, cls, typing, typing.ClassVar, _is_classvar) + ): f._field_type = _FIELD_CLASSVAR # If the type is InitVar, or if it's a matching string annotation, @@ -675,10 +704,10 @@ def _get_field(cls, a_name, a_type): # The module we're checking against is the module we're # currently in (dataclasses.py). dataclasses = sys.modules[__name__] - if (_is_initvar(a_type, dataclasses) - or (isinstance(f.type, str) - and _is_type(f.type, cls, dataclasses, dataclasses.InitVar, - _is_initvar))): + if _is_initvar(a_type, dataclasses) or ( + isinstance(f.type, str) + and _is_type(f.type, cls, dataclasses, dataclasses.InitVar, _is_initvar) + ): f._field_type = _FIELD_INITVAR # Validations for individual fields. This is delayed until now, @@ -688,8 +717,7 @@ def _get_field(cls, a_name, a_type): # Special restrictions for ClassVar and InitVar. if f._field_type in (_FIELD_CLASSVAR, _FIELD_INITVAR): if f.default_factory is not MISSING: - raise TypeError(f'field {f.name} cannot have a ' - 'default factory') + raise TypeError(f"field {f.name} cannot have a " "default factory") # Should I check for other field settings? default_factory # seems the most serious to check for. Maybe add others. For # example, how about init=False (or really, @@ -698,8 +726,10 @@ def _get_field(cls, a_name, a_type): # For real fields, disallow mutable defaults for known types. if f._field_type is _FIELD and isinstance(f.default, (list, dict, set)): - raise ValueError(f'mutable default {type(f.default)} for field ' - f'{f.name} is not allowed: use default_factory') + raise ValueError( + f"mutable default {type(f.default)} for field " + f"{f.name} is not allowed: use default_factory" + ) return f @@ -718,17 +748,20 @@ def _set_new_attribute(cls, name, value): # take. The common case is to do nothing, so instead of providing a # function that is a no-op, use None to signify that. + def _hash_set_none(cls, fields): return None + def _hash_add(cls, fields): flds = [f for f in fields if (f.compare if f.hash is None else f.hash)] return _hash_fn(flds) + def _hash_exception(cls, fields): # Raise an exception. - raise TypeError(f'Cannot overwrite attribute __hash__ ' - f'in class {cls.__name__}') + raise TypeError(f"Cannot overwrite attribute __hash__ " f"in class {cls.__name__}") + # # +-------------------------------------- unsafe_hash? @@ -739,23 +772,24 @@ def _hash_exception(cls, fields): # | | | | +------- action # | | | | | # v v v v v -_hash_action = {(False, False, False, False): None, - (False, False, False, True ): None, - (False, False, True, False): None, - (False, False, True, True ): None, - (False, True, False, False): _hash_set_none, - (False, True, False, True ): None, - (False, True, True, False): _hash_add, - (False, True, True, True ): None, - (True, False, False, False): _hash_add, - (True, False, False, True ): _hash_exception, - (True, False, True, False): _hash_add, - (True, False, True, True ): _hash_exception, - (True, True, False, False): _hash_add, - (True, True, False, True ): _hash_exception, - (True, True, True, False): _hash_add, - (True, True, True, True ): _hash_exception, - } +_hash_action = { + (False, False, False, False): None, + (False, False, False, True): None, + (False, False, True, False): None, + (False, False, True, True): None, + (False, True, False, False): _hash_set_none, + (False, True, False, True): None, + (False, True, True, False): _hash_add, + (False, True, True, True): None, + (True, False, False, False): _hash_add, + (True, False, False, True): _hash_exception, + (True, False, True, False): _hash_add, + (True, False, True, True): _hash_exception, + (True, True, False, False): _hash_add, + (True, True, False, True): _hash_exception, + (True, True, True, False): _hash_add, + (True, True, True, True): _hash_exception, +} # See https://bugs.python.org/issue32929#msg312829 for an if-statement # version of this table. @@ -767,8 +801,7 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen): # is defined by the base class, which is found first. fields = {} - setattr(cls, _PARAMS, _DataclassParams(init, repr, eq, order, - unsafe_hash, frozen)) + setattr(cls, _PARAMS, _DataclassParams(init, repr, eq, order, unsafe_hash, frozen)) # Find our base classes in reverse MRO order, and exclude # ourselves. In reversed order so that more derived classes @@ -799,13 +832,12 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen): # actual default value. Pseudo-fields ClassVars and InitVars are # included, despite the fact that they're not real fields. That's # dealt with later. - cls_annotations = cls.__dict__.get('__annotations__', {}) + cls_annotations = cls.__dict__.get("__annotations__", {}) # Now find fields in our class. While doing so, validate some # things, and set the default values (as class attributes) where # we can. - cls_fields = [_get_field(cls, name, type) - for name, type in cls_annotations.items()] + cls_fields = [_get_field(cls, name, type) for name, type in cls_annotations.items()] for f in cls_fields: fields[f.name] = f @@ -828,19 +860,17 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen): # Do we have any Field members that don't also have annotations? for name, value in cls.__dict__.items(): if isinstance(value, Field) and not name in cls_annotations: - raise TypeError(f'{name!r} is a field but has no type annotation') + raise TypeError(f"{name!r} is a field but has no type annotation") # Check rules that apply if we are derived from any dataclasses. if has_dataclass_bases: # Raise an exception if any of our bases are frozen, but we're not. if any_frozen_base and not frozen: - raise TypeError('cannot inherit non-frozen dataclass from a ' - 'frozen one') + raise TypeError("cannot inherit non-frozen dataclass from a " "frozen one") # Raise an exception if we're frozen, but none of our bases are. if not any_frozen_base and frozen: - raise TypeError('cannot inherit frozen dataclass from a ' - 'non-frozen one') + raise TypeError("cannot inherit frozen dataclass from a " "non-frozen one") # Remember all of the fields on our class (including bases). This # also marks this class as being a dataclass. @@ -851,32 +881,35 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen): # set __hash__ to None. This is a heuristic, as it's possible # that such a __hash__ == None was not auto-generated, but it # close enough. - class_hash = cls.__dict__.get('__hash__', MISSING) - has_explicit_hash = not (class_hash is MISSING or - (class_hash is None and '__eq__' in cls.__dict__)) + class_hash = cls.__dict__.get("__hash__", MISSING) + has_explicit_hash = not ( + class_hash is MISSING or (class_hash is None and "__eq__" in cls.__dict__) + ) # If we're generating ordering methods, we must be generating the # eq methods. if order and not eq: - raise ValueError('eq must be true if order is true') + raise ValueError("eq must be true if order is true") if init: # Does this class have a post-init function? has_post_init = hasattr(cls, _POST_INIT_NAME) # Include InitVars and regular fields (so, not ClassVars). - flds = [f for f in fields.values() - if f._field_type in (_FIELD, _FIELD_INITVAR)] - _set_new_attribute(cls, '__init__', - _init_fn(flds, - frozen, - has_post_init, - # The name to use for the "self" - # param in __init__. Use "self" - # if possible. - '__dataclass_self__' if 'self' in fields - else 'self', - )) + flds = [f for f in fields.values() if f._field_type in (_FIELD, _FIELD_INITVAR)] + _set_new_attribute( + cls, + "__init__", + _init_fn( + flds, + frozen, + has_post_init, + # The name to use for the "self" + # param in __init__. Use "self" + # if possible. + "__dataclass_self__" if "self" in fields else "self", + ), + ) # Get the fields as a list, and include only real fields. This is # used in all of the following methods. @@ -884,54 +917,46 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen): if repr: flds = [f for f in field_list if f.repr] - _set_new_attribute(cls, '__repr__', _repr_fn(flds)) + _set_new_attribute(cls, "__repr__", _repr_fn(flds)) if eq: # Create _eq__ method. There's no need for a __ne__ method, # since python will call __eq__ and negate it. flds = [f for f in field_list if f.compare] - self_tuple = _tuple_str('self', flds) - other_tuple = _tuple_str('other', flds) - _set_new_attribute(cls, '__eq__', - _cmp_fn('__eq__', '==', - self_tuple, other_tuple)) + self_tuple = _tuple_str("self", flds) + other_tuple = _tuple_str("other", flds) + _set_new_attribute(cls, "__eq__", _cmp_fn("__eq__", "==", self_tuple, other_tuple)) if order: # Create and set the ordering methods. flds = [f for f in field_list if f.compare] - self_tuple = _tuple_str('self', flds) - other_tuple = _tuple_str('other', flds) - for name, op in [('__lt__', '<'), - ('__le__', '<='), - ('__gt__', '>'), - ('__ge__', '>='), - ]: - if _set_new_attribute(cls, name, - _cmp_fn(name, op, self_tuple, other_tuple)): - raise TypeError(f'Cannot overwrite attribute {name} ' - f'in class {cls.__name__}. Consider using ' - 'functools.total_ordering') + self_tuple = _tuple_str("self", flds) + other_tuple = _tuple_str("other", flds) + for name, op in [("__lt__", "<"), ("__le__", "<="), ("__gt__", ">"), ("__ge__", ">=")]: + if _set_new_attribute(cls, name, _cmp_fn(name, op, self_tuple, other_tuple)): + raise TypeError( + f"Cannot overwrite attribute {name} " + f"in class {cls.__name__}. Consider using " + "functools.total_ordering" + ) if frozen: for fn in _frozen_get_del_attr(cls, field_list): if _set_new_attribute(cls, fn.__name__, fn): - raise TypeError(f'Cannot overwrite attribute {fn.__name__} ' - f'in class {cls.__name__}') + raise TypeError( + f"Cannot overwrite attribute {fn.__name__} " f"in class {cls.__name__}" + ) # Decide if/how we're going to create a hash function. - hash_action = _hash_action[bool(unsafe_hash), - bool(eq), - bool(frozen), - has_explicit_hash] + hash_action = _hash_action[bool(unsafe_hash), bool(eq), bool(frozen), has_explicit_hash] if hash_action: # No need to call _set_new_attribute here, since by the time # we're here the overwriting is unconditional. cls.__hash__ = hash_action(cls, field_list) - if not getattr(cls, '__doc__'): + if not getattr(cls, "__doc__"): # Create a class doc-string. - cls.__doc__ = (cls.__name__ + - str(inspect.signature(cls)).replace(' -> None', '')) + cls.__doc__ = cls.__name__ + str(inspect.signature(cls)).replace(" -> None", "") return cls @@ -939,8 +964,9 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen): # _cls should never be specified by keyword, so start it with an # underscore. The presence of _cls is used to detect if this # decorator is being called with parameters or not. -def dataclass(_cls=None, *, init=True, repr=True, eq=True, order=False, - unsafe_hash=False, frozen=False): +def dataclass( + _cls=None, *, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False +): """Returns the same class as was passed in, with dunder methods added based on the fields defined in the class. Examines PEP 526 __annotations__ to determine fields. @@ -973,7 +999,7 @@ def fields(class_or_instance): try: fields = getattr(class_or_instance, _FIELDS) except AttributeError: - raise TypeError('must be called with a dataclass type or instance') + raise TypeError("must be called with a dataclass type or instance") # Exclude pseudo-fields. Note that fields is sorted by insertion # order, so the order of the tuple is as the fields were defined. @@ -1021,8 +1047,9 @@ def _asdict_inner(obj, dict_factory): elif isinstance(obj, (list, tuple)): return type(obj)(_asdict_inner(v, dict_factory) for v in obj) elif isinstance(obj, dict): - return type(obj)((_asdict_inner(k, dict_factory), _asdict_inner(v, dict_factory)) - for k, v in obj.items()) + return type(obj)( + (_asdict_inner(k, dict_factory), _asdict_inner(v, dict_factory)) for k, v in obj.items() + ) else: return copy.deepcopy(obj) @@ -1057,15 +1084,27 @@ def _astuple_inner(obj, tuple_factory): elif isinstance(obj, (list, tuple)): return type(obj)(_astuple_inner(v, tuple_factory) for v in obj) elif isinstance(obj, dict): - return type(obj)((_astuple_inner(k, tuple_factory), _astuple_inner(v, tuple_factory)) - for k, v in obj.items()) + return type(obj)( + (_astuple_inner(k, tuple_factory), _astuple_inner(v, tuple_factory)) + for k, v in obj.items() + ) else: return copy.deepcopy(obj) -def make_dataclass(cls_name, fields, *, bases=(), namespace=None, init=True, - repr=True, eq=True, order=False, unsafe_hash=False, - frozen=False): +def make_dataclass( + cls_name, + fields, + *, + bases=(), + namespace=None, + init=True, + repr=True, + eq=True, + order=False, + unsafe_hash=False, + frozen=False, +): """Return a new dynamically created dataclass. The dataclass name will be 'cls_name'. 'fields' is an iterable of either (name), (name, type) or (name, type, Field) objects. If type is @@ -1096,31 +1135,32 @@ class C(Base): for item in fields: if isinstance(item, str): name = item - tp = 'typing.Any' + tp = "typing.Any" elif len(item) == 2: name, tp, = item elif len(item) == 3: name, tp, spec = item namespace[name] = spec else: - raise TypeError(f'Invalid field: {item!r}') + raise TypeError(f"Invalid field: {item!r}") if not isinstance(name, str) or not name.isidentifier(): - raise TypeError(f'Field names must be valid identifers: {name!r}') + raise TypeError(f"Field names must be valid identifers: {name!r}") if keyword.iskeyword(name): - raise TypeError(f'Field names must not be keywords: {name!r}') + raise TypeError(f"Field names must not be keywords: {name!r}") if name in seen: - raise TypeError(f'Field name duplicated: {name!r}') + raise TypeError(f"Field name duplicated: {name!r}") seen.add(name) anns[name] = tp - namespace['__annotations__'] = anns + namespace["__annotations__"] = anns # We use `types.new_class()` instead of simply `type()` to allow dynamic creation # of generic dataclassses. cls = types.new_class(cls_name, bases, {}, lambda ns: ns.update(namespace)) - return dataclass(cls, init=init, repr=repr, eq=eq, order=order, - unsafe_hash=unsafe_hash, frozen=frozen) + return dataclass( + cls, init=init, repr=repr, eq=eq, order=order, unsafe_hash=unsafe_hash, frozen=frozen + ) def replace(obj, **changes): @@ -1148,9 +1188,11 @@ class C: if not f.init: # Error if this field is specified in changes. if f.name in changes: - raise ValueError(f'field {f.name} is declared with ' - 'init=False, it cannot be specified with ' - 'replace()') + raise ValueError( + f"field {f.name} is declared with " + "init=False, it cannot be specified with " + "replace()" + ) continue if f.name not in changes: diff --git a/isort/compat.py b/isort/compat.py index a85dc66b2..7d5eb52a7 100644 --- a/isort/compat.py +++ b/isort/compat.py @@ -1,11 +1,12 @@ import locale import os import re -import sys from pathlib import Path -from typing import Any, Optional, Tuple from warnings import warn +from typing import Any, Optional, Tuple + +import sys from isort import settings from isort.format import ask_whether_to_apply_changes_to_file, show_unified_diff from isort.isort import _SortImports diff --git a/isort/finders.py b/isort/finders.py index 4ba203146..fd0914e62 100644 --- a/isort/finders.py +++ b/isort/finders.py @@ -3,12 +3,12 @@ import os import os.path import re -import sys import sysconfig from abc import ABCMeta, abstractmethod from fnmatch import fnmatch from functools import lru_cache from glob import glob + from typing import ( Any, Dict, @@ -23,6 +23,8 @@ Type, ) +import sys + from .utils import chdir, exists_case_sensitive try: diff --git a/isort/format.py b/isort/format.py index 80a215e42..7372862ad 100644 --- a/isort/format.py +++ b/isort/format.py @@ -1,9 +1,11 @@ -import sys from datetime import datetime from difflib import unified_diff from pathlib import Path + from typing import Optional +import sys + def format_simplified(import_line: str) -> str: import_line = import_line.strip() diff --git a/isort/hooks.py b/isort/hooks.py index db9904cb8..9ad0fd175 100644 --- a/isort/hooks.py +++ b/isort/hooks.py @@ -4,6 +4,7 @@ exit_code = git_hook(strict=True|False, modify=True|False) """ import subprocess # nosec - Needed for hook + from typing import List from isort import SortImports diff --git a/isort/isort.py b/isort/isort.py index 0a532410e..ee2873fff 100644 --- a/isort/isort.py +++ b/isort/isort.py @@ -6,11 +6,12 @@ sorted = SortImports(file_contents=file_contents).output """ import copy -import itertools import re from collections import OrderedDict, defaultdict, namedtuple + from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple +import itertools from isort import utils from . import output, parse, settings, sorting, wrap diff --git a/isort/main.py b/isort/main.py index de33eaa96..b21056185 100644 --- a/isort/main.py +++ b/isort/main.py @@ -4,12 +4,12 @@ import glob import os import re -import sys -from typing import Any, Dict, Iterable, Iterator, List, MutableMapping, Optional, Sequence from warnings import warn import setuptools +from typing import Any, Dict, Iterable, Iterator, List, MutableMapping, Optional, Sequence +import sys from isort import SortImports, __version__ from isort.logo import ASCII_ART from isort.settings import ( @@ -540,7 +540,7 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: "--python-version", action="store", dest="py_version", - choices=VALID_PY_TARGETS + ("auto", ), + choices=tuple(VALID_PY_TARGETS) + ("auto",), help="Tells isort to sort the standard library based on the the specified python version." "Default is to assume any python version could be the target, and use a union off all " "stdlib modules across versions. If auto is specified, the version of the interpreter " diff --git a/isort/output.py b/isort/output.py index 31ce6ad03..0c8f97be9 100644 --- a/isort/output.py +++ b/isort/output.py @@ -1,8 +1,9 @@ import copy -import itertools from functools import partial + from typing import Any, Dict, Iterable, List, Optional +import itertools from isort.format import format_simplified from . import parse, sorting, wrap diff --git a/isort/parse.py b/isort/parse.py index 659a51175..720f4840f 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -1,10 +1,11 @@ """Defines parsing functions used by isort for parsing import definitions""" from collections import OrderedDict, defaultdict, namedtuple -from itertools import chain -from typing import TYPE_CHECKING, Any, Dict, Generator, Iterator, List, NamedTuple, Optional, Tuple from warnings import warn +from typing import TYPE_CHECKING, Any, Dict, Generator, Iterator, List, NamedTuple, Optional, Tuple + from isort.format import format_natural +from itertools import chain from .comments import parse as parse_comments from .finders import FindersManager diff --git a/isort/pylama_isort.py b/isort/pylama_isort.py index 3fb2ba8fe..999e6acdd 100644 --- a/isort/pylama_isort.py +++ b/isort/pylama_isort.py @@ -1,8 +1,9 @@ import os -import sys -from typing import Any, Dict, List from pylama.lint import Linter as BaseLinter +from typing import Any, Dict, List + +import sys from . import SortImports diff --git a/isort/settings.py b/isort/settings.py index 9011cec48..665ddb92d 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -11,15 +11,28 @@ import os import posixpath import re -import sys import warnings from distutils.util import strtobool from functools import lru_cache from pathlib import Path -from typing import Any, Callable, Dict, Iterable, List, Mapping, MutableMapping, Optional, Union -from ._future import dataclass +from typing import ( + Any, + Callable, + Dict, + Iterable, + List, + Mapping, + MutableMapping, + Optional, + Tuple, + Union, +) + +import sys + from . import stdlibs +from ._future import dataclass, field from .utils import difference, union from .wrap_modes import WrapModes from .wrap_modes import from_string as wrap_mode_from_string @@ -37,16 +50,18 @@ except ImportError: appdirs = None -MAX_CONFIG_SEARCH_DEPTH = ( +MAX_CONFIG_SEARCH_DEPTH: int = ( 25 ) # The number of parent directories isort will look for a config file within -DEFAULT_SECTIONS = ("FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER") +DEFAULT_SECTIONS: Iterable[str] = ("FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER") safety_exclude_re = re.compile( r"/(\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|_build|buck-out|build|dist|\.pants\.d" r"|lib/python[0-9].[0-9]+|node_modules)/" ) -VALID_PY_TARGETS = tuple(target for target in dir(stdlibs) if not target.startswith("_")) +VALID_PY_TARGETS: Iterable[str] = tuple( + target for target in dir(stdlibs) if not target.startswith("_") +) def _get_default(py_version: Optional[str]) -> Dict[str, Any]: @@ -58,7 +73,7 @@ def _get_default(py_version: Optional[str]) -> Dict[str, Any]: """ py_version = py_version or "all" if py_version == "auto": - py_version = f"{sys.version_info.major}{sys.version_info.minor}" + py_version = f"{sys.version_info.major}{sys.version_info.minor}" if py_version not in VALID_PY_TARGETS: raise ValueError( @@ -75,60 +90,59 @@ def _get_default(py_version: Optional[str]) -> Dict[str, Any]: @dataclass class Config: """Defines the configuration parameters used by isort""" - force_to_top = [] - skip = [] - skip_glob = [] - line_length = 79 - wrap_length = 0 - line_ending = None - sections = DEFAULT_SECTIONS - no_sections = False - known_future_library = ["__future__"] - known_third_party = ["google.appengine.api"] - known_first_party = [] - multi_line_output = WrapModes.GRID # type: ignore - forced_separate = [] - indent = " " * 4 - comment_prefix = " #" - length_sort = False - add_imports = [] - remove_imports = [] - reverse_relative = False - force_single_line = False - default_section = "FIRSTPARTY" - import_heading_future = "" - import_heading_stdlib = "" - import_heading_thirdparty = "" - import_heading_firstparty = "" - import_heading_localfolder = "" - balanced_wrapping = False - use_parentheses = False - order_by_type = True - atomic = False - lines_after_imports = -1 - lines_between_sections = 1 - lines_between_types = 0 - combine_as_imports = False - combine_star = False - keep_direct_and_as_imports = False - include_trailing_comma = False - from_first = False - verbose = False - quiet = False - force_adds = False - force_alphabetical_sort_within_sections = False - force_alphabetical_sort = False - force_grid_wrap = 0 - force_sort_within_sections = False - show_diff = False - ignore_whitespace = False - no_lines_before = [] - no_inline_sort = False - ignore_comments = False - safety_excludes = True - case_sensitive = False - + force_to_top: List[str] = field(default_factory=list) + skip: List[str] = field(default_factory=list) + skip_glob: List[str] = field(default_factory=list) + line_length: int = 79 + wrap_length: int = 0 + line_ending: str = "" + sections: Iterable[str] = DEFAULT_SECTIONS + no_sections: bool = False + known_future_library: List[str] = field(default_factory=lambda: ["__future__"]) + known_third_party: List[str] = field(default_factory=lambda: ["google.appengine.api"]) + known_first_party: List[str] = field(default_factory=list) + multi_line_output = WrapModes.GRID # type: ignore + forced_separate: List[str] = field(default_factory=list) + indent: str = " " * 4 + comment_prefix: str = " #" + length_sort: bool = False + add_imports: List[str] = field(default_factory=list) + remove_imports: List[str] = field(default_factory=list) + reverse_relative: bool = False + force_single_line: bool = False + default_section: str = "FIRSTPARTY" + import_heading_future: str = "" + import_heading_stdlib: str = "" + import_heading_thirdparty: str = "" + import_heading_firstparty: str = "" + import_heading_localfolder: str = "" + balanced_wrapping: bool = False + use_parentheses: bool = False + order_by_type: bool = True + atomic: bool = False + lines_after_imports: int = -1 + lines_between_sections: int = 1 + lines_between_types: int = 0 + combine_as_imports: bool = False + combine_star: bool = False + keep_direct_and_as_imports: bool = False + include_trailing_comma: bool = False + from_first: bool = False + verbose: bool = False + quiet: bool = False + force_adds: bool = False + force_alphabetical_sort_within_sections: bool = False + force_alphabetical_sort: bool = False + force_grid_wrap: int = 0 + force_sort_within_sections: bool = False + show_diff: bool = False + ignore_whitespace: bool = False + no_lines_before: List[str] = field(default_factory=list) + no_inline_sort: bool = False + ignore_comments: bool = False + safety_excludes: bool = True + case_sensitive: bool = False default = { diff --git a/isort/sorting.py b/isort/sorting.py index 591d3d938..6852d2329 100644 --- a/isort/sorting.py +++ b/isort/sorting.py @@ -1,4 +1,5 @@ import re + from typing import Any, Callable, Iterable, List, Mapping, Optional _import_line_intro_re = re.compile("^(?:from|import) ") diff --git a/isort/stdlibs/__init__.py b/isort/stdlibs/__init__.py index 653ed067d..9967ae558 100644 --- a/isort/stdlibs/__init__.py +++ b/isort/stdlibs/__init__.py @@ -1 +1 @@ -from . import all, py2, py27, py3, py35, py36, py37, py38 +from . import all, py2, py3, py27, py35, py36, py37, py38 diff --git a/isort/stdlibs/py2.py b/isort/stdlibs/py2.py index bbc030ca0..74af019e4 100644 --- a/isort/stdlibs/py2.py +++ b/isort/stdlibs/py2.py @@ -1,4 +1,3 @@ from . import py27 stdlib = py27.stdlib - diff --git a/isort/utils.py b/isort/utils.py index d198563bd..2bef99552 100644 --- a/isort/utils.py +++ b/isort/utils.py @@ -1,8 +1,10 @@ import os -import sys from contextlib import contextmanager + from typing import Any, Container, Iterable, Iterator, List +import sys + def exists_case_sensitive(path: str) -> bool: """Returns if the given path exists and also matches the case on Windows. diff --git a/isort/wrap.py b/isort/wrap.py index 7980dfd4b..3574e3f45 100644 --- a/isort/wrap.py +++ b/isort/wrap.py @@ -1,5 +1,6 @@ import copy import re + from typing import Any, Dict, List, Sequence from .wrap_modes import WrapModes as Modes diff --git a/isort/wrap_modes.py b/isort/wrap_modes.py index d3d5a2ba2..1290796c6 100644 --- a/isort/wrap_modes.py +++ b/isort/wrap_modes.py @@ -1,6 +1,7 @@ """Defines all wrap modes that can be used when outputting formatted imports""" import enum from inspect import signature + from typing import Any, Callable, Dict, List, Sequence from . import comments, settings diff --git a/tests/test_output.py b/tests/test_output.py index e42093b5b..d01441b68 100644 --- a/tests/test_output.py +++ b/tests/test_output.py @@ -1,7 +1,6 @@ -import sys - from hypothesis_auto import auto_pytest_magic +import sys from isort import output auto_pytest_magic(output.with_comments, auto_allow_exceptions_=(ValueError,)) diff --git a/tests/test_wrap_modes.py b/tests/test_wrap_modes.py index dee26d4f7..94ae1f897 100644 --- a/tests/test_wrap_modes.py +++ b/tests/test_wrap_modes.py @@ -1,7 +1,6 @@ -import sys - from hypothesis_auto import auto_pytest_magic +import sys from isort import wrap_modes auto_pytest_magic(wrap_modes.grid, auto_allow_exceptions_=(ValueError,)) From 91434aa021e85d1d173a3525e6bd3a6d1715e27f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 29 Oct 2019 23:42:53 -0700 Subject: [PATCH 0152/1439] Fix stdlib inclusions --- isort/_future/_dataclasses.py | 3 +-- isort/compat.py | 5 ++--- isort/finders.py | 4 +--- isort/format.py | 4 +--- isort/hooks.py | 1 - isort/isort.py | 3 +-- isort/main.py | 4 ++-- isort/output.py | 3 +-- isort/parse.py | 5 ++--- isort/pylama_isort.py | 5 ++--- isort/settings.py | 11 ++++++----- isort/sorting.py | 1 - isort/utils.py | 4 +--- isort/wrap.py | 1 - isort/wrap_modes.py | 1 - tests/test_isort.py | 6 +++--- tests/test_output.py | 3 ++- tests/test_wrap_modes.py | 3 ++- 18 files changed, 27 insertions(+), 40 deletions(-) diff --git a/isort/_future/_dataclasses.py b/isort/_future/_dataclasses.py index 1f7fbb591..f7f6bace4 100644 --- a/isort/_future/_dataclasses.py +++ b/isort/_future/_dataclasses.py @@ -13,9 +13,8 @@ import inspect import keyword import re -import types - import sys +import types __all__ = [ "dataclass", diff --git a/isort/compat.py b/isort/compat.py index 7d5eb52a7..a85dc66b2 100644 --- a/isort/compat.py +++ b/isort/compat.py @@ -1,12 +1,11 @@ import locale import os import re +import sys from pathlib import Path -from warnings import warn - from typing import Any, Optional, Tuple +from warnings import warn -import sys from isort import settings from isort.format import ask_whether_to_apply_changes_to_file, show_unified_diff from isort.isort import _SortImports diff --git a/isort/finders.py b/isort/finders.py index fd0914e62..4ba203146 100644 --- a/isort/finders.py +++ b/isort/finders.py @@ -3,12 +3,12 @@ import os import os.path import re +import sys import sysconfig from abc import ABCMeta, abstractmethod from fnmatch import fnmatch from functools import lru_cache from glob import glob - from typing import ( Any, Dict, @@ -23,8 +23,6 @@ Type, ) -import sys - from .utils import chdir, exists_case_sensitive try: diff --git a/isort/format.py b/isort/format.py index 7372862ad..80a215e42 100644 --- a/isort/format.py +++ b/isort/format.py @@ -1,11 +1,9 @@ +import sys from datetime import datetime from difflib import unified_diff from pathlib import Path - from typing import Optional -import sys - def format_simplified(import_line: str) -> str: import_line = import_line.strip() diff --git a/isort/hooks.py b/isort/hooks.py index 9ad0fd175..db9904cb8 100644 --- a/isort/hooks.py +++ b/isort/hooks.py @@ -4,7 +4,6 @@ exit_code = git_hook(strict=True|False, modify=True|False) """ import subprocess # nosec - Needed for hook - from typing import List from isort import SortImports diff --git a/isort/isort.py b/isort/isort.py index ee2873fff..0a532410e 100644 --- a/isort/isort.py +++ b/isort/isort.py @@ -6,12 +6,11 @@ sorted = SortImports(file_contents=file_contents).output """ import copy +import itertools import re from collections import OrderedDict, defaultdict, namedtuple - from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple -import itertools from isort import utils from . import output, parse, settings, sorting, wrap diff --git a/isort/main.py b/isort/main.py index b21056185..83a939500 100644 --- a/isort/main.py +++ b/isort/main.py @@ -4,12 +4,12 @@ import glob import os import re +import sys +from typing import Any, Dict, Iterable, Iterator, List, MutableMapping, Optional, Sequence from warnings import warn import setuptools -from typing import Any, Dict, Iterable, Iterator, List, MutableMapping, Optional, Sequence -import sys from isort import SortImports, __version__ from isort.logo import ASCII_ART from isort.settings import ( diff --git a/isort/output.py b/isort/output.py index 0c8f97be9..31ce6ad03 100644 --- a/isort/output.py +++ b/isort/output.py @@ -1,9 +1,8 @@ import copy +import itertools from functools import partial - from typing import Any, Dict, Iterable, List, Optional -import itertools from isort.format import format_simplified from . import parse, sorting, wrap diff --git a/isort/parse.py b/isort/parse.py index 720f4840f..659a51175 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -1,11 +1,10 @@ """Defines parsing functions used by isort for parsing import definitions""" from collections import OrderedDict, defaultdict, namedtuple -from warnings import warn - +from itertools import chain from typing import TYPE_CHECKING, Any, Dict, Generator, Iterator, List, NamedTuple, Optional, Tuple +from warnings import warn from isort.format import format_natural -from itertools import chain from .comments import parse as parse_comments from .finders import FindersManager diff --git a/isort/pylama_isort.py b/isort/pylama_isort.py index 999e6acdd..3fb2ba8fe 100644 --- a/isort/pylama_isort.py +++ b/isort/pylama_isort.py @@ -1,9 +1,8 @@ import os - -from pylama.lint import Linter as BaseLinter +import sys from typing import Any, Dict, List -import sys +from pylama.lint import Linter as BaseLinter from . import SortImports diff --git a/isort/settings.py b/isort/settings.py index 665ddb92d..931c3935d 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -11,11 +11,11 @@ import os import posixpath import re +import sys import warnings from distutils.util import strtobool from functools import lru_cache from pathlib import Path - from typing import ( Any, Callable, @@ -29,8 +29,6 @@ Union, ) -import sys - from . import stdlibs from ._future import dataclass, field from .utils import difference, union @@ -60,7 +58,7 @@ r"|lib/python[0-9].[0-9]+|node_modules)/" ) VALID_PY_TARGETS: Iterable[str] = tuple( - target for target in dir(stdlibs) if not target.startswith("_") + target.replace("py", "") for target in dir(stdlibs) if not target.startswith("_") ) @@ -82,7 +80,10 @@ def _get_default(py_version: Optional[str]) -> Dict[str, Any]: f"The following versions are supported: {VALID_PY_TARGETS}" ) - return {**default, "known_standard_library": getattr(stdlibs, py_version)} + if py_version != "all": + py_version = f"py{py_version}" + + return {**default, "known_standard_library": getattr(stdlibs, py_version).stdlib} # Note that none of these lists must be complete as they are simply diff --git a/isort/sorting.py b/isort/sorting.py index 6852d2329..591d3d938 100644 --- a/isort/sorting.py +++ b/isort/sorting.py @@ -1,5 +1,4 @@ import re - from typing import Any, Callable, Iterable, List, Mapping, Optional _import_line_intro_re = re.compile("^(?:from|import) ") diff --git a/isort/utils.py b/isort/utils.py index 2bef99552..d198563bd 100644 --- a/isort/utils.py +++ b/isort/utils.py @@ -1,10 +1,8 @@ import os +import sys from contextlib import contextmanager - from typing import Any, Container, Iterable, Iterator, List -import sys - def exists_case_sensitive(path: str) -> bool: """Returns if the given path exists and also matches the case on Windows. diff --git a/isort/wrap.py b/isort/wrap.py index 3574e3f45..7980dfd4b 100644 --- a/isort/wrap.py +++ b/isort/wrap.py @@ -1,6 +1,5 @@ import copy import re - from typing import Any, Dict, List, Sequence from .wrap_modes import WrapModes as Modes diff --git a/isort/wrap_modes.py b/isort/wrap_modes.py index 1290796c6..d3d5a2ba2 100644 --- a/isort/wrap_modes.py +++ b/isort/wrap_modes.py @@ -1,7 +1,6 @@ """Defines all wrap modes that can be used when outputting formatted imports""" import enum from inspect import signature - from typing import Any, Callable, Dict, List, Sequence from . import comments, settings diff --git a/tests/test_isort.py b/tests/test_isort.py index dcc65b534..8a68428ad 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -4118,8 +4118,8 @@ def test_python_version() -> None: from isort.main import parse_args # test that the py_version can be added as flag - args = parse_args(["-py=2.7"]) - assert args["py_version"] == "2.7" + args = parse_args(["-py=27"]) + assert args["py_version"] == "27" args = parse_args(["--python-version=3"]) assert args["py_version"] == "3" @@ -4129,7 +4129,7 @@ def test_python_version() -> None: # user is part of the standard library in python 2 output_python_2 = "import os\nimport user\n" - assert SortImports(file_contents=test_input, py_version="2.7").output == output_python_2 + assert SortImports(file_contents=test_input, py_version="27").output == output_python_2 test_input = "import os\nimport xml" diff --git a/tests/test_output.py b/tests/test_output.py index d01441b68..e42093b5b 100644 --- a/tests/test_output.py +++ b/tests/test_output.py @@ -1,6 +1,7 @@ +import sys + from hypothesis_auto import auto_pytest_magic -import sys from isort import output auto_pytest_magic(output.with_comments, auto_allow_exceptions_=(ValueError,)) diff --git a/tests/test_wrap_modes.py b/tests/test_wrap_modes.py index 94ae1f897..dee26d4f7 100644 --- a/tests/test_wrap_modes.py +++ b/tests/test_wrap_modes.py @@ -1,6 +1,7 @@ +import sys + from hypothesis_auto import auto_pytest_magic -import sys from isort import wrap_modes auto_pytest_magic(wrap_modes.grid, auto_allow_exceptions_=(ValueError,)) From 74ce9ed208df36666e96463e67f076b931414315 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 30 Oct 2019 22:56:34 -0700 Subject: [PATCH 0153/1439] Default to Python3.x --- isort/main.py | 9 +++++---- isort/settings.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/isort/main.py b/isort/main.py index 83a939500..bc17ada31 100644 --- a/isort/main.py +++ b/isort/main.py @@ -541,10 +541,11 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: action="store", dest="py_version", choices=tuple(VALID_PY_TARGETS) + ("auto",), - help="Tells isort to sort the standard library based on the the specified python version." - "Default is to assume any python version could be the target, and use a union off all " - "stdlib modules across versions. If auto is specified, the version of the interpreter " - f"used to run isort (py{sys.version_info.major}{sys.version_info.minor}) will be used.", + help="Tells isort to set the known standard library based on the the specified Python " + "version. Default is to assume any Python 3 version could be the target, and use a union " + "off all stdlib modules across versions. If auto is specified, the version of the " + "interpreter used to run isort " + f"(currently: {sys.version_info.major}{sys.version_info.minor}) will be used.", ) arguments = {key: value for key, value in vars(parser.parse_args(argv)).items() if value} diff --git a/isort/settings.py b/isort/settings.py index 931c3935d..601107fce 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -69,7 +69,7 @@ def _get_default(py_version: Optional[str]) -> Dict[str, Any]: See Issue 889 and 778 for more information """ - py_version = py_version or "all" + py_version = py_version or "3" if py_version == "auto": py_version = f"{sys.version_info.major}{sys.version_info.minor}" From f7fed6ca1b56e0dbfe2a2b8ca2e0d570167d6e70 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 30 Oct 2019 23:32:08 -0700 Subject: [PATCH 0154/1439] Fix merging of stdlib imports with custom inclusions --- isort/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/settings.py b/isort/settings.py index 601107fce..13ed62501 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -230,7 +230,7 @@ def prepare_config(settings_path: Path, **setting_overrides: Any) -> Dict[str, A for key, value in setting_overrides.items(): access_key = key.replace("not_", "").lower() # The sections config needs to retain order and can't be converted to a set. - if access_key != "sections" and type(config.get(access_key)) in (list, tuple): + if access_key != "sections" and type(config.get(access_key)) in (list, tuple, set): if key.startswith("not_"): config[access_key] = list(set(config[access_key]).difference(value)) else: From da4e1b6b2d3ea68e8ca01ba08a7735371ec1cfc1 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 30 Oct 2019 23:34:37 -0700 Subject: [PATCH 0155/1439] Update test to match new parameter format --- tests/test_isort.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_isort.py b/tests/test_isort.py index 8a68428ad..84603652c 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -884,7 +884,7 @@ def test_first_party_overrides_standard_section() -> None: "import profile.test\n" ) test_output = SortImports( - file_contents=test_input, known_first_party=["profile"], py_version="2.7" + file_contents=test_input, known_first_party=["profile"], py_version="27" ).output assert test_output == ( "import os\n" @@ -1132,7 +1132,7 @@ def test_order_by_type() -> None: "from subprocess import PIPE, Popen, STDOUT\n" ) - assert SortImports(file_contents=test_input, order_by_type=True, py_version="2.7").output == ( + assert SortImports(file_contents=test_input, order_by_type=True, py_version="27").output == ( "import glob\n" "import os\n" "import shutil\n" From bd8e55eb1504d46c232e46beff7bd7588802b75e Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 30 Oct 2019 23:45:12 -0700 Subject: [PATCH 0156/1439] Fix test to worth with set value --- tests/test_isort.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_isort.py b/tests/test_isort.py index 84603652c..c19203b58 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -1218,7 +1218,7 @@ def test_settings_combine_instead_of_overwrite() -> None: """Test to ensure settings combine logically, instead of fully overwriting.""" assert set( SortImports(known_standard_library=["not_std_library"]).config["known_standard_library"] - ) == set(SortImports().config["known_standard_library"] + ["not_std_library"]) + ) == set(SortImports().config["known_standard_library"]) | {"not_std_library"} assert set( SortImports(not_known_standard_library=["thread"]).config["known_standard_library"] From d18b62de8df493ec6d2e1e6e92d3602e64f13f8d Mon Sep 17 00:00:00 2001 From: Joshua Coats Date: Thu, 31 Oct 2019 12:10:08 -0400 Subject: [PATCH 0157/1439] Find ext suffixes from imp or importlib.machinery --- isort/finders.py | 10 +++++----- tests/test_isort.py | 29 ++++++++++++++++++----------- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/isort/finders.py b/isort/finders.py index 4ba203146..dda24dfb9 100644 --- a/isort/finders.py +++ b/isort/finders.py @@ -1,4 +1,5 @@ """Finders try to find right section for passed module name""" +import importlib.machinery import inspect import os import os.path @@ -162,9 +163,6 @@ def __init__(self, config: Mapping[str, Any], sections: Any) -> None: if self.stdlib_lib_prefix not in self.paths: self.paths.append(self.stdlib_lib_prefix) - # 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: @@ -175,8 +173,10 @@ def find(self, module_name: str) -> Optional[str]: 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) + or any( + exists_case_sensitive(package_path + ext_suffix) + for ext_suffix in importlib.machinery.EXTENSION_SUFFIXES + ) or exists_case_sensitive(package_path + "/__init__.py") ) is_package = exists_case_sensitive(package_path) and os.path.isdir(package_path) diff --git a/tests/test_isort.py b/tests/test_isort.py index dcc65b534..3e85b15a5 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -2,6 +2,7 @@ Should be ran using py.test by simply running py.test in the isort project directory """ +import importlib.machinery import os import os.path import posixpath @@ -3098,21 +3099,27 @@ def test_path_finder(monkeypatch) -> None: si = SortImports(file_contents="") finder = finders.PathFinder(config=si.config, sections=si.sections) third_party_prefix = next(path for path in finder.paths if "site-packages" in path) - ext_suffix = sysconfig.get_config_var("EXT_SUFFIX") or ".so" - imaginary_paths = { - posixpath.join(finder.stdlib_lib_prefix, "example_1.py"), - posixpath.join(third_party_prefix, "example_2.py"), - posixpath.join(third_party_prefix, "example_3.so"), - posixpath.join(third_party_prefix, "example_4" + ext_suffix), - posixpath.join(os.getcwd(), "example_5.py"), - } + ext_suffixes = importlib.machinery.EXTENSION_SUFFIXES + imaginary_paths = set( + [ + posixpath.join(finder.stdlib_lib_prefix, "example_1.py"), + posixpath.join(third_party_prefix, "example_2.py"), + posixpath.join(os.getcwd(), "example_3.py"), + ] + ) + imaginary_paths.update( + { + posixpath.join(third_party_prefix, "example_" + str(i) + ext_suffix) + for i, ext_suffix in enumerate(ext_suffixes, 4) + } + ) monkeypatch.setattr("isort.finders.exists_case_sensitive", lambda p: p in imaginary_paths) assert finder.find("example_1") == finder.sections.STDLIB assert finder.find("example_2") == finder.sections.THIRDPARTY - assert finder.find("example_3") == finder.sections.THIRDPARTY - assert finder.find("example_4") == finder.sections.THIRDPARTY - assert finder.find("example_5") == finder.sections.FIRSTPARTY + assert finder.find("example_3") == finder.sections.FIRSTPARTY + for i, _ in enumerate(ext_suffixes, 4): + assert finder.find("example_" + str(i)) == finder.sections.THIRDPARTY def test_argument_parsing() -> None: From b09ad62bb471a6b034b9a2a8aec66c5ccf099b3c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 31 Oct 2019 20:29:45 -0700 Subject: [PATCH 0158/1439] Add config init --- isort/settings.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/isort/settings.py b/isort/settings.py index 13ed62501..f150d999d 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -91,7 +91,25 @@ def _get_default(py_version: Optional[str]) -> Dict[str, Any]: @dataclass class Config: """Defines the configuration parameters used by isort""" + def __init__(self, py_version: str, known_standard_library: Optional[List[str]]=None*args, **kwargs): + known_standard_library = {} if known_standard_library is None else set(known_standard_library) + py_version = py_version or "3" + if py_version == "auto": + py_version = f"{sys.version_info.major}{sys.version_info.minor}" + + if py_version not in VALID_PY_TARGETS: + raise ValueError( + f"The python version {py_version} is not supported. " + "You can set a python version with the -py or --python-version flag. " + f"The following versions are supported: {VALID_PY_TARGETS}" + ) + + if py_version != "all": + py_version = f"py{py_version}" + + return super().__init__(py_version=py_version, known_standard_library=known_standard_library, *args, **kwargs) + py_version: str = "3" force_to_top: List[str] = field(default_factory=list) skip: List[str] = field(default_factory=list) skip_glob: List[str] = field(default_factory=list) @@ -103,6 +121,7 @@ class Config: known_future_library: List[str] = field(default_factory=lambda: ["__future__"]) known_third_party: List[str] = field(default_factory=lambda: ["google.appengine.api"]) known_first_party: List[str] = field(default_factory=list) + known_standard_library: List[str] = field(default_factory=list) multi_line_output = WrapModes.GRID # type: ignore forced_separate: List[str] = field(default_factory=list) indent: str = " " * 4 From 8f66bc20220a18e9838c506d79673273a15b7beb Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 1 Nov 2019 23:01:29 -0700 Subject: [PATCH 0159/1439] Fix syntax errors with Config object, make frozen after creation --- isort/settings.py | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/isort/settings.py b/isort/settings.py index f150d999d..aff407d8b 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -88,27 +88,9 @@ def _get_default(py_version: Optional[str]) -> Dict[str, Any]: # Note that none of these lists must be complete as they are simply # fallbacks for when included auto-detection fails. -@dataclass +@dataclass(frozen=True) class Config: """Defines the configuration parameters used by isort""" - def __init__(self, py_version: str, known_standard_library: Optional[List[str]]=None*args, **kwargs): - known_standard_library = {} if known_standard_library is None else set(known_standard_library) - py_version = py_version or "3" - if py_version == "auto": - py_version = f"{sys.version_info.major}{sys.version_info.minor}" - - if py_version not in VALID_PY_TARGETS: - raise ValueError( - f"The python version {py_version} is not supported. " - "You can set a python version with the -py or --python-version flag. " - f"The following versions are supported: {VALID_PY_TARGETS}" - ) - - if py_version != "all": - py_version = f"py{py_version}" - - return super().__init__(py_version=py_version, known_standard_library=known_standard_library, *args, **kwargs) - py_version: str = "3" force_to_top: List[str] = field(default_factory=list) skip: List[str] = field(default_factory=list) @@ -163,6 +145,26 @@ def __init__(self, py_version: str, known_standard_library: Optional[List[str]]= ignore_comments: bool = False safety_excludes: bool = True case_sensitive: bool = False + sources: List[Dict[str, Any]] = field(default_factory=list) + + def __post_init__(self): + known_standard_library = set(self.known_standard_library) + if self.py_version == "auto": + py_version = f"{sys.version_info.major}{sys.version_info.minor}" + + if py_version not in VALID_PY_TARGETS: + raise ValueError( + f"The python version {py_version} is not supported. " + "You can set a python version with the -py or --python-version flag. " + f"The following versions are supported: {VALID_PY_TARGETS}" + ) + + if py_version != "all": + object.__setattr__(self, "py_version", f"py{py_version}") + + object.__setattr__(self, "known_standard_library", + list(getattr(stdlibs, py_version).stdlib | self.known_standard_library)) + default = { From 922d2773c5a7fb18b9ea1b8760ad962d0d058416 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 2 Nov 2019 01:02:06 -0700 Subject: [PATCH 0160/1439] Update changelog to mention breaking changes to only use one config file instead of merging config files --- CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12f889eaa..720ec50a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,12 +3,15 @@ Changelog ### 5.0.0 UNRELEASED **Breaking changes:** - - isort now requires Python 3.5+ to run but continues to support formatting + - isort now requires Python 3.6+ to run but continues to support formatting on ALL versions of python including Python 2 code. - isort deprecates official support for Python 3.4, removing modules only in this release from known_standard_library: - user + - Config files are no longer composed on-top of each-other. Instead the first config file found is used. + Internal: - - isort now utilizes mypy and typing to filter out typing related issues before deployment + - isort now utilizes mypy and typing to filter out typing related issues before deployment. + - isort now utilizes black internally to ensure more consistent formatting. Planned: - profile support for common project types (black, django, google, etc) From bdf26a32eaa1ff00d608eeeed8b7aaa856c29865 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 2 Nov 2019 01:02:30 -0700 Subject: [PATCH 0161/1439] Add _find_config function to find a single config --- isort/settings.py | 57 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/isort/settings.py b/isort/settings.py index aff407d8b..9c4ffff1c 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -28,6 +28,7 @@ Tuple, Union, ) +from warnings import warn from . import stdlibs from ._future import dataclass, field @@ -60,6 +61,22 @@ VALID_PY_TARGETS: Iterable[str] = tuple( target.replace("py", "") for target in dir(stdlibs) if not target.startswith("_") ) +CONFIG_SOURCES = (".isort.cfg", "pyproject.toml", "setup.cfg", "tox.ini", ".editorconfig") +CONFIG_SECTIONS = { + ".isort.cfg": ("settings", "isort"), + "pyproject.toml": ("tool.isort",), + "setup.cfg": ("isort", "tool:isort"), + "tox.ini": ("isort", "tool:isort"), + ".editorconfig": ("*", "*.py", "**.py"), +} + +if appdirs: + FALLBACK_CONFIGS = [ + appdirs.user_config_dir(".isort.cfg"), + appdirs.user_config_dir(".editorconfig"), + ] +else: + FALLBACK_CONFIGS = ["~/.isort.cfg", "~/.editorconfig"] def _get_default(py_version: Optional[str]) -> Dict[str, Any]: @@ -91,6 +108,7 @@ def _get_default(py_version: Optional[str]) -> Dict[str, Any]: @dataclass(frozen=True) class Config: """Defines the configuration parameters used by isort""" + py_version: str = "3" force_to_top: List[str] = field(default_factory=list) skip: List[str] = field(default_factory=list) @@ -148,7 +166,6 @@ class Config: sources: List[Dict[str, Any]] = field(default_factory=list) def __post_init__(self): - known_standard_library = set(self.known_standard_library) if self.py_version == "auto": py_version = f"{sys.version_info.major}{sys.version_info.minor}" @@ -162,9 +179,11 @@ def __post_init__(self): if py_version != "all": object.__setattr__(self, "py_version", f"py{py_version}") - object.__setattr__(self, "known_standard_library", - list(getattr(stdlibs, py_version).stdlib | self.known_standard_library)) - + object.__setattr__( + self, + "known_standard_library", + list(getattr(stdlibs, py_version).stdlib | self.known_standard_library), + ) default = { @@ -400,6 +419,36 @@ def _abspaths(cwd: str, values: Iterable[str]) -> List[str]: return paths +@lru_cache() +def _find_config(path: str) -> Dict[str, Any]: + current_directory = path + tries = 0 + while current_directory and tries < MAX_CONFIG_SEARCH_DEPTH: + for config_file_name in CONFIG_SOURCES: + potential_config_file = os.path.join(current_directory, config_file_name) + if os.path.isfile(potential_config_file): + config_data: Dict[str, Any] + try: + config_data = _get_config_data( + potential_config_file, CONFIG_SECTIONS[config_file_name] + ) + except Exception: + warn(f"Failed to pull configuration information from {potential_config_file}") + config_data = {} + if config_data: + config_data["source"] = potential_config_file + return config_data + + new_directory = os.path.split(current_directory)[0] + if new_directory == current_directory: + break + + current_directory = new_directory + tries += 1 + + return {} + + @lru_cache() def _get_config_data(file_path: str, sections: Iterable[str]) -> Dict[str, Any]: settings: Dict[str, Any] = {} From 89252b674b5d54a034cc604e5cddf7b9d87f51ec Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 2 Nov 2019 14:59:23 -0700 Subject: [PATCH 0162/1439] Current progress on refactoring settings, only public interface should be single config object --- isort/settings.py | 353 +++++++++++++++------------------------------- 1 file changed, 117 insertions(+), 236 deletions(-) diff --git a/isort/settings.py b/isort/settings.py index 9c4ffff1c..fe9c19fd4 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -69,6 +69,7 @@ "tox.ini": ("isort", "tool:isort"), ".editorconfig": ("*", "*.py", "**.py"), } +FALLBACK_CONFIG_SECTIONS = ("isort", "tool:isort", "tool.isort") if appdirs: FALLBACK_CONFIGS = [ @@ -79,36 +80,13 @@ FALLBACK_CONFIGS = ["~/.isort.cfg", "~/.editorconfig"] -def _get_default(py_version: Optional[str]) -> Dict[str, Any]: - """Returns the correct standard library based on either the passed py_version flag or the python - interpreter. Additionaly users have the option to pass all as value instead of an - version. As an result code will be checked against both standard libraries - python2 & python3 - - See Issue 889 and 778 for more information - """ - py_version = py_version or "3" - if py_version == "auto": - py_version = f"{sys.version_info.major}{sys.version_info.minor}" - - if py_version not in VALID_PY_TARGETS: - raise ValueError( - f"The python version {py_version} is not supported. " - "You can set a python version with the -py or --python-version flag. " - f"The following versions are supported: {VALID_PY_TARGETS}" - ) - - if py_version != "all": - py_version = f"py{py_version}" - - return {**default, "known_standard_library": getattr(stdlibs, py_version).stdlib} - - -# Note that none of these lists must be complete as they are simply -# fallbacks for when included auto-detection fails. @dataclass(frozen=True) -class Config: - """Defines the configuration parameters used by isort""" +class _Config: + """Defines the data schema and defaults used for isort configuration. + NOTE: known lists, such as known_standard_library, are intentionally not complete as they are + dynamically determined later on. + """ py_version: str = "3" force_to_top: List[str] = field(default_factory=list) skip: List[str] = field(default_factory=list) @@ -166,7 +144,8 @@ class Config: sources: List[Dict[str, Any]] = field(default_factory=list) def __post_init__(self): - if self.py_version == "auto": + py_version = self.py_version + if py_version == "auto": py_version = f"{sys.version_info.major}{sys.version_info.minor}" if py_version not in VALID_PY_TARGETS: @@ -182,86 +161,72 @@ def __post_init__(self): object.__setattr__( self, "known_standard_library", - list(getattr(stdlibs, py_version).stdlib | self.known_standard_library), + list(getattr(stdlibs, self.py_version).stdlib | set(self.known_standard_library)), ) + if self.force_alphabetical_sort: + object.__setattr__(self, "force_alphabetical_sort_within_sections", True) + object.__setattr__(self, "no_sections", True) + object.__setattr__(self, "lines_between_types", 1) + object.__setattr__(self, "from_first", True) -default = { - "force_to_top": [], - "skip": [], - "skip_glob": [], - "line_length": 79, - "wrap_length": 0, - "line_ending": None, - "sections": DEFAULT_SECTIONS, - "no_sections": False, - "known_future_library": ["__future__"], - "known_third_party": ["google.appengine.api"], - "known_first_party": [], - "multi_line_output": WrapModes.GRID, # type: ignore - "forced_separate": [], - "indent": " " * 4, - "comment_prefix": " #", - "length_sort": False, - "add_imports": [], - "remove_imports": [], - "reverse_relative": False, - "force_single_line": False, - "default_section": "FIRSTPARTY", - "import_heading_future": "", - "import_heading_stdlib": "", - "import_heading_thirdparty": "", - "import_heading_firstparty": "", - "import_heading_localfolder": "", - "balanced_wrapping": False, - "use_parentheses": False, - "order_by_type": True, - "atomic": False, - "lines_after_imports": -1, - "lines_between_sections": 1, - "lines_between_types": 0, - "combine_as_imports": False, - "combine_star": False, - "keep_direct_and_as_imports": False, - "include_trailing_comma": False, - "from_first": False, - "verbose": False, - "quiet": False, - "force_adds": False, - "force_alphabetical_sort_within_sections": False, - "force_alphabetical_sort": False, - "force_grid_wrap": 0, - "force_sort_within_sections": False, - "show_diff": False, - "ignore_whitespace": False, - "no_lines_before": [], - "no_inline_sort": False, - "ignore_comments": False, - "safety_excludes": True, - "case_sensitive": False, -} +_DEFAULT_SETTINGS = {**vars(_Config()), "source": "defaults"} -@lru_cache() -def from_path(path: Union[str, Path], py_version: Optional[str] = None) -> Dict[str, Any]: - computed_settings = _get_default(py_version) - isort_defaults = ["~/.isort.cfg"] - if appdirs: - isort_defaults = [appdirs.user_config_dir("isort.cfg")] + isort_defaults - - if isinstance(path, Path): - path = str(path) - - _update_settings_with_config( - path, ".editorconfig", ["~/.editorconfig"], ("*", "*.py", "**.py"), computed_settings - ) - _update_settings_with_config(path, "pyproject.toml", [], ("tool.isort",), computed_settings) - _update_settings_with_config( - path, ".isort.cfg", isort_defaults, ("settings", "isort"), computed_settings - ) - _update_settings_with_config(path, "setup.cfg", [], ("isort", "tool:isort"), computed_settings) - _update_settings_with_config(path, "tox.ini", [], ("isort", "tool:isort"), computed_settings) - return computed_settings +class Config(_Config): + def __init__(self, settings_file:str="", settings_path:str="", **config_overrides): + sources: List[Dict[str, Any]] = [_DEFAULT_SETTINGS] + + config_settings: Dict[str, Any] + if settings_file: + config_settings = config_data = get_config_data(settings_file, CONFIG_SECTIONS.get(os.path.basename(settings_file), FALLBACK_CONFIG_SECTIONS)) + elif settings_path: + config_settings = _find_config(settings_path) + else: + config_settings = {} + + if config_settings: + + + + sources.append(config_settings) + if config_overrides: + config_overrides["source"] = "runtime" + sources.append(config_overrides) + + def file_should_be_skipped(self, filename: str, path: str = "") -> bool: + """Returns True if the file and/or folder should be skipped based on current settings.""" + os_path = os.path.join(path, filename) + + normalized_path = os_path.replace("\\", "/") + if normalized_path[1:2] == ":": + normalized_path = normalized_path[2:] + + if path and self.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 self.skip: + if posixpath.abspath(normalized_path) == posixpath.abspath(skip_path.replace("\\", "/")): + return True + + position = os.path.split(filename) + while position[1]: + if position[1] in self.skip: + return True + position = os.path.split(position[0]) + + for glob in self.skip_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)): + return True + + return False def prepare_config(settings_path: Path, **setting_overrides: Any) -> Dict[str, Any]: @@ -278,16 +243,6 @@ def prepare_config(settings_path: Path, **setting_overrides: Any) -> Dict[str, A else: config[key] = value - if config["force_alphabetical_sort"]: - config.update( - { - "force_alphabetical_sort_within_sections": True, - "no_sections": True, - "lines_between_types": 1, - "from_first": True, - } - ) - indent = str(config["indent"]) if indent.isdigit(): indent = " " * int(indent) @@ -301,40 +256,8 @@ def prepare_config(settings_path: Path, **setting_overrides: Any) -> Dict[str, A return config -def _update_settings_with_config( - path: str, - name: str, - default: Iterable[str], - sections: Iterable[str], - computed_settings: MutableMapping[str, Any], -) -> None: - editor_config_file = None - for potential_settings_path in default: - expanded = os.path.expanduser(potential_settings_path) - if os.path.exists(expanded): - editor_config_file = expanded - break - - tries = 0 - current_directory = path - while current_directory and tries < MAX_CONFIG_SEARCH_DEPTH: - potential_path = os.path.join(current_directory, name) - if os.path.exists(potential_path): - editor_config_file = potential_path - break - - new_directory = os.path.split(current_directory)[0] - if current_directory == new_directory: - break - current_directory = new_directory - tries += 1 - - if editor_config_file and os.path.exists(editor_config_file): - _update_with_config_file(editor_config_file, sections, computed_settings) - - def _get_str_to_type_converter(setting_name: str) -> Callable[[str], Any]: - type_converter: Callable[[str], Any] = type(default.get(setting_name, "")) + type_converter: Callable[[str], Any] = type(_DEFAULT_SETTINGS.get(setting_name, "")) if type_converter == WrapModes: type_converter = wrap_mode_from_string return type_converter @@ -348,60 +271,6 @@ def _update_with_config_file( if not settings: return - if file_path.endswith(".editorconfig"): - indent_style = settings.pop("indent_style", "").strip() - indent_size = settings.pop("indent_size", "").strip() - if indent_size == "tab": - indent_size = settings.pop("tab_width", "").strip() - - if indent_style == "space": - computed_settings["indent"] = " " * (indent_size and int(indent_size) or 4) - elif indent_style == "tab": - computed_settings["indent"] = "\t" * (indent_size and int(indent_size) or 1) - - max_line_length = settings.pop("max_line_length", "").strip() - if max_line_length: - computed_settings["line_length"] = ( - float("inf") if max_line_length == "off" else int(max_line_length) - ) - - for key, value in settings.items(): - access_key = key.replace("not_", "").lower() - existing_value_type = _get_str_to_type_converter(access_key) - if existing_value_type in (list, tuple): - # sections has fixed order values; no adding or substraction from any set - if access_key == "sections": - computed_settings[access_key] = tuple(_as_list(value)) - else: - existing_data = set(computed_settings.get(access_key, default.get(access_key))) - if key.startswith("not_"): - computed_settings[access_key] = difference(existing_data, _as_list(value)) - elif key.startswith("known_"): - computed_settings[access_key] = union( - existing_data, _abspaths(cwd, _as_list(value)) - ) - else: - computed_settings[access_key] = union(existing_data, _as_list(value)) - elif existing_value_type == bool: - # Only some configuration formats support native boolean values. - if not isinstance(value, bool): - value = bool(strtobool(value)) - computed_settings[access_key] = value - elif key.startswith("known_"): - computed_settings[access_key] = _abspaths(cwd, _as_list(value)) - elif key == "force_grid_wrap": - try: - result = existing_value_type(value) - except ValueError: - # backwards compat - result = default.get(access_key) if value.lower().strip() == "false" else 2 - computed_settings[access_key] = result - else: - computed_settings[access_key] = getattr( - existing_value_type, str(value), None - ) or existing_value_type(value) - - def _as_list(value: str) -> List[str]: if isinstance(value, list): return [item.strip() for item in value] @@ -410,12 +279,12 @@ def _as_list(value: str) -> List[str]: def _abspaths(cwd: str, values: Iterable[str]) -> List[str]: - paths = [ + paths = set([ os.path.join(cwd, value) if not value.startswith(os.path.sep) and value.endswith(os.path.sep) else value for value in values - ] + ]) return paths @@ -436,7 +305,6 @@ def _find_config(path: str) -> Dict[str, Any]: warn(f"Failed to pull configuration information from {potential_config_file}") config_data = {} if config_data: - config_data["source"] = potential_config_file return config_data new_directory = os.path.split(current_directory)[0] @@ -486,39 +354,52 @@ def _get_config_data(file_path: str, sections: Iterable[str]) -> Dict[str, Any]: if config.has_section(section): settings.update(config.items(section)) - return settings - - -def file_should_be_skipped(filename: str, config: Mapping[str, Any], path: str = "") -> bool: - """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 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("\\", "/")): - return True + if settings: + settings["source"] = file_path + + if file_path.endswith(".editorconfig"): + indent_style = settings.pop("indent_style", "").strip() + indent_size = settings.pop("indent_size", "").strip() + if indent_size == "tab": + indent_size = settings.pop("tab_width", "").strip() + + if indent_style == "space": + settings["indent"] = " " * (indent_size and int(indent_size) or 4) + + elif indent_style == "tab": + settings["indent"] = "\t" * (indent_size and int(indent_size) or 1) + + max_line_length = settings.pop("max_line_length", "").strip() + if max_line_length: + settings["line_length"] = ( + float("inf") if max_line_length == "off" else int(max_line_length) + ) + + for key, value in settings.items(): + existing_value_type = _get_str_to_type_converter(key) + if existing_value_type in (list, tuple): + if key == "sections": # sections need to maintain order + settings[key] = tuple(_as_list(value)) + else: # other list options do not and can be uniquified using set. + settings[key] = set(settings.get(key)) + elif existing_value_type == bool: + # Only some configuration formats support native boolean values. + if not isinstance(value, bool): + value = bool(_as_bool(value)) + settings[key] = value + elif key.startswith("known_"): + settings[key] = _abspaths(os.path.dirname(file_path), _as_list(value)) + elif key == "force_grid_wrap": + try: + result = existing_value_type(value) + except ValueError: + # backwards compat + result = default.get(access_key) if value.lower().strip() == "false" else 2 + computed_settings[access_key] = result + else: + existing_value_type(value) - position = os.path.split(filename) - while position[1]: - if position[1] in config["skip"]: - return True - position = os.path.split(position[0]) + return settings - for glob in config["skip_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)): - return True - return False From 8b57494c17ba9eff7fb8019ad0c0201ae595de44 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 2 Nov 2019 16:36:51 -0700 Subject: [PATCH 0163/1439] Switch completely to immutable types for config object --- isort/settings.py | 109 +++++++++++++++++++--------------------------- 1 file changed, 44 insertions(+), 65 deletions(-) diff --git a/isort/settings.py b/isort/settings.py index fe9c19fd4..334a0ee9a 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -13,7 +13,7 @@ import re import sys import warnings -from distutils.util import strtobool +from distutils.util import strtobool as _as_bool from functools import lru_cache from pathlib import Path from typing import ( @@ -49,35 +49,33 @@ except ImportError: appdirs = None -MAX_CONFIG_SEARCH_DEPTH: int = ( - 25 -) # The number of parent directories isort will look for a config file within -DEFAULT_SECTIONS: Iterable[str] = ("FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER") - safety_exclude_re = re.compile( r"/(\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|_build|buck-out|build|dist|\.pants\.d" r"|lib/python[0-9].[0-9]+|node_modules)/" ) -VALID_PY_TARGETS: Iterable[str] = tuple( + +MAX_CONFIG_SEARCH_DEPTH: int = 25 # The number of parent directories to for a config file within +DEFAULT_SECTIONS: Tuple[str, ...] = ("FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER") +VALID_PY_TARGETS: Tuple[str, ...] = tuple( target.replace("py", "") for target in dir(stdlibs) if not target.startswith("_") ) -CONFIG_SOURCES = (".isort.cfg", "pyproject.toml", "setup.cfg", "tox.ini", ".editorconfig") -CONFIG_SECTIONS = { +CONFIG_SOURCES: Tuple[str, ...] = (".isort.cfg", "pyproject.toml", "setup.cfg", "tox.ini", ".editorconfig") +CONFIG_SECTIONS: Dict[str, Tuple[str, ...]] = { ".isort.cfg": ("settings", "isort"), "pyproject.toml": ("tool.isort",), "setup.cfg": ("isort", "tool:isort"), "tox.ini": ("isort", "tool:isort"), ".editorconfig": ("*", "*.py", "**.py"), } -FALLBACK_CONFIG_SECTIONS = ("isort", "tool:isort", "tool.isort") - +FALLBACK_CONFIG_SECTIONS: Tuple[str, ...] = ("isort", "tool:isort", "tool.isort") +FALLBACK_CONFIGS: Tuple[str, ...] if appdirs: - FALLBACK_CONFIGS = [ + FALLBACK_CONFIGS = ( appdirs.user_config_dir(".isort.cfg"), appdirs.user_config_dir(".editorconfig"), - ] + ) else: - FALLBACK_CONFIGS = ["~/.isort.cfg", "~/.editorconfig"] + FALLBACK_CONFIGS = ("~/.isort.cfg", "~/.editorconfig") @dataclass(frozen=True) @@ -88,25 +86,25 @@ class _Config: dynamically determined later on. """ py_version: str = "3" - force_to_top: List[str] = field(default_factory=list) - skip: List[str] = field(default_factory=list) - skip_glob: List[str] = field(default_factory=list) + force_to_top: FrozenSet[str] = frozenset() + skip: FrozenSet[str] = frozenset() + skip_glob: FrozenSet[str] = frozenset() line_length: int = 79 wrap_length: int = 0 line_ending: str = "" - sections: Iterable[str] = DEFAULT_SECTIONS + sections: Tuple[str, ...] = DEFAULT_SECTIONS no_sections: bool = False - known_future_library: List[str] = field(default_factory=lambda: ["__future__"]) - known_third_party: List[str] = field(default_factory=lambda: ["google.appengine.api"]) - known_first_party: List[str] = field(default_factory=list) - known_standard_library: List[str] = field(default_factory=list) + known_future_library: FrozenSet[str] = frozenset(("__future__", )) + known_third_party: FrozenSet[str] = frozenset(("google.appengine.api", )) + known_first_party: FrozenSet[str] = frozenset() + known_standard_library: FrozenSet[str] = frozenset() multi_line_output = WrapModes.GRID # type: ignore - forced_separate: List[str] = field(default_factory=list) + forced_separate: FrozenSet[str] = frozenset() indent: str = " " * 4 comment_prefix: str = " #" length_sort: bool = False - add_imports: List[str] = field(default_factory=list) - remove_imports: List[str] = field(default_factory=list) + add_imports: FrozenSet[str] = frozenset() + remove_imports: FrozenSet[str] = frozenset() reverse_relative: bool = False force_single_line: bool = False default_section: str = "FIRSTPARTY" @@ -136,12 +134,12 @@ class _Config: force_sort_within_sections: bool = False show_diff: bool = False ignore_whitespace: bool = False - no_lines_before: List[str] = field(default_factory=list) + no_lines_before: FrozenSet[str] = frozenset() no_inline_sort: bool = False ignore_comments: bool = False safety_excludes: bool = True case_sensitive: bool = False - sources: List[Dict[str, Any]] = field(default_factory=list) + sources: FrozenSet[str] = frozenset() def __post_init__(self): py_version = self.py_version @@ -186,14 +184,24 @@ def __init__(self, settings_file:str="", settings_path:str="", **config_override config_settings = {} if config_settings: - - - sources.append(config_settings) if config_overrides: config_overrides["source"] = "runtime" sources.append(config_overrides) + combined_config = {**config_settings, **config_overrides} + if "indent" in combined_config: + indent = str(combined_config["indent"]) + if indent.isdigit(): + indent = " " * int(indent) + else: + indent = indent.strip("'").strip('"') + if indent.lower() == "tab": + indent = "\t" + combined_config["indent"] = indent + + super().__init__(**combined_config) + def file_should_be_skipped(self, filename: str, path: str = "") -> bool: """Returns True if the file and/or folder should be skipped based on current settings.""" os_path = os.path.join(path, filename) @@ -229,33 +237,6 @@ def file_should_be_skipped(self, filename: str, path: str = "") -> bool: return False -def prepare_config(settings_path: Path, **setting_overrides: Any) -> Dict[str, Any]: - py_version = setting_overrides.pop("py_version", None) - config = from_path(settings_path, py_version).copy() - for key, value in setting_overrides.items(): - access_key = key.replace("not_", "").lower() - # The sections config needs to retain order and can't be converted to a set. - if access_key != "sections" and type(config.get(access_key)) in (list, tuple, set): - if key.startswith("not_"): - config[access_key] = list(set(config[access_key]).difference(value)) - else: - config[access_key] = list(set(config[access_key]).union(value)) - else: - config[key] = value - - indent = str(config["indent"]) - if indent.isdigit(): - indent = " " * int(indent) - else: - indent = indent.strip("'").strip('"') - if indent.lower() == "tab": - indent = "\t" - config["indent"] = indent - - config["comment_prefix"] = config["comment_prefix"].strip("'").strip('"') - return config - - def _get_str_to_type_converter(setting_name: str) -> Callable[[str], Any]: type_converter: Callable[[str], Any] = type(_DEFAULT_SETTINGS.get(setting_name, "")) if type_converter == WrapModes: @@ -392,14 +373,12 @@ def _get_config_data(file_path: str, sections: Iterable[str]) -> Dict[str, Any]: elif key == "force_grid_wrap": try: result = existing_value_type(value) - except ValueError: - # backwards compat - result = default.get(access_key) if value.lower().strip() == "false" else 2 - computed_settings[access_key] = result + except ValueError: # backwards compatibility for true / false force grid wrap + result = 0 if value.lower().strip() == "false" else 2 + settings[key] = result + elif key == "comment_prefix": + settings[key] = str(value).strip("'").strip('"') else: - existing_value_type(value) + settings[key] = existing_value_type(value) return settings - - - From 77cc80f94da6d7d707adf684fed6749d212dceab Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 2 Nov 2019 22:23:16 -0700 Subject: [PATCH 0164/1439] Final pass to ensure all config values map to the type checked form --- isort/settings.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/isort/settings.py b/isort/settings.py index 334a0ee9a..e6965e2cf 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -200,6 +200,14 @@ def __init__(self, settings_file:str="", settings_path:str="", **config_override indent = "\t" combined_config["indent"] = indent + # coerce all provided config values into their correct type + for key, value in combined_config.items(): + default_value = _DEFAULT_SETTINGS.get(key, None) + if default_value is None: + continue + + combined_config[key] = type(default_value)(value) + super().__init__(**combined_config) def file_should_be_skipped(self, filename: str, path: str = "") -> bool: @@ -244,14 +252,6 @@ def _get_str_to_type_converter(setting_name: str) -> Callable[[str], Any]: return type_converter -def _update_with_config_file( - file_path: str, sections: Iterable[str], computed_settings: MutableMapping[str, Any] -) -> None: - cwd = os.path.dirname(file_path) - settings = _get_config_data(file_path, sections).copy() - if not settings: - return - def _as_list(value: str) -> List[str]: if isinstance(value, list): return [item.strip() for item in value] @@ -358,11 +358,10 @@ def _get_config_data(file_path: str, sections: Iterable[str]) -> Dict[str, Any]: for key, value in settings.items(): existing_value_type = _get_str_to_type_converter(key) - if existing_value_type in (list, tuple): - if key == "sections": # sections need to maintain order - settings[key] = tuple(_as_list(value)) - else: # other list options do not and can be uniquified using set. - settings[key] = set(settings.get(key)) + if existing_value_type == tuple: + settings[key] = tuple(_as_list(value)) + elif existing_value_type == frozenset: + settings[key] = frozenset(settings.get(key)) elif existing_value_type == bool: # Only some configuration formats support native boolean values. if not isinstance(value, bool): @@ -382,3 +381,6 @@ def _get_config_data(file_path: str, sections: Iterable[str]) -> Dict[str, Any]: settings[key] = existing_value_type(value) return settings + + +DEFAULT_CONFIG = Config() From 01a389a9c998c368632a5e3c2fdd178d5cf4545c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 2 Nov 2019 22:47:56 -0700 Subject: [PATCH 0165/1439] Start utilizing simplified immutable config object --- isort/finders.py | 25 +++++++++++++------------ isort/parse.py | 21 +++++++++++---------- isort/settings.py | 3 +++ 3 files changed, 27 insertions(+), 22 deletions(-) diff --git a/isort/finders.py b/isort/finders.py index 4ba203146..f68097892 100644 --- a/isort/finders.py +++ b/isort/finders.py @@ -24,6 +24,7 @@ ) from .utils import chdir, exists_case_sensitive +from .settings import Config, DEFAULT_CONFIG try: from pipreqs import pipreqs @@ -49,7 +50,7 @@ class BaseFinder(metaclass=ABCMeta): - def __init__(self, config: Mapping[str, Any], sections: Any) -> None: + def __init__(self, config: Config, sections: Any) -> None: self.config = config self.sections = sections @@ -60,7 +61,7 @@ def find(self, module_name: str) -> Optional[str]: class ForcedSeparateFinder(BaseFinder): def find(self, module_name: str) -> Optional[str]: - for forced_separate in self.config["forced_separate"]: + for forced_separate in self.config.forced_separate: # Ensure all forced_separate patterns will match to end of string path_glob = forced_separate if not forced_separate.endswith("*"): @@ -79,14 +80,14 @@ def find(self, module_name: str) -> Optional[str]: class KnownPatternFinder(BaseFinder): - def __init__(self, config: Mapping[str, Any], sections: Any) -> None: + def __init__(self, config: Config, sections: Any) -> None: super().__init__(config, sections) self.known_patterns: List[Tuple[Pattern[str], str]] = [] for placement in reversed(self.sections): known_placement = KNOWN_SECTION_MAPPING.get(placement, placement) config_key = f"known_{known_placement.lower()}" - known_patterns = self.config.get(config_key, []) + known_patterns = list(getattr(self.config, config_key, [])) known_patterns = [ pattern for known_pattern in known_patterns @@ -121,7 +122,7 @@ def find(self, module_name: str) -> Optional[str]: class PathFinder(BaseFinder): - def __init__(self, config: Mapping[str, Any], sections: Any) -> None: + def __init__(self, config: Config, sections: Any) -> None: super().__init__(config, sections) # restore the original import path (i.e. not the path to bin/isort) @@ -130,7 +131,7 @@ def __init__(self, config: Mapping[str, Any], sections: Any) -> None: self.paths = [root_dir, src_dir] # virtual env - self.virtual_env = self.config.get("virtual_env") or os.environ.get("VIRTUAL_ENV") + self.virtual_env = self.config.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 = "" @@ -147,7 +148,7 @@ def __init__(self, config: Mapping[str, Any], sections: Any) -> None: self.paths.append(path) # conda - self.conda_env = self.config.get("conda_env") or os.environ.get("CONDA_PREFIX") or "" + self.conda_env = self.config.conda_env or os.environ.get("CONDA_PREFIX") or "" if self.conda_env: self.conda_env = os.path.realpath(self.conda_env) for path in glob(f"{self.conda_env}/lib/python*/site-packages"): @@ -193,14 +194,14 @@ def find(self, module_name: str) -> Optional[str]: return self.sections.THIRDPARTY if os.path.normcase(prefix).startswith(self.stdlib_lib_prefix): return self.sections.STDLIB - return self.config["default_section"] + return self.config.default_section return None class ReqsBaseFinder(BaseFinder): enabled = False - def __init__(self, config: Mapping[str, Any], sections: Any, path: str = ".") -> None: + def __init__(self, config: Config, sections: Any, path: str = ".") -> None: super().__init__(config, sections) self.path = path if self.enabled: @@ -356,7 +357,7 @@ def _get_files_from_dir(self, path: str) -> Iterator[str]: class DefaultFinder(BaseFinder): def find(self, module_name: str) -> Optional[str]: - return self.config["default_section"] + return self.config.default_section class FindersManager: @@ -372,11 +373,11 @@ class FindersManager: def __init__( self, - config: Mapping[str, Any], + config: Config, sections: Any, finder_classes: Optional[Iterable[Type[BaseFinder]]] = None, ) -> None: - self.verbose: bool = config.get("verbose", False) + self.verbose: bool = config.verbose if finder_classes is None: finder_classes = self._default_finders_classes diff --git a/isort/parse.py b/isort/parse.py index 659a51175..8fde32e97 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -5,6 +5,7 @@ from warnings import warn from isort.format import format_natural +from isort.settings import Config, DEFAULT_CONFIG from .comments import parse as parse_comments from .finders import FindersManager @@ -138,21 +139,21 @@ class ParsedContent(NamedTuple): section_comments: List[str] -def file_contents(contents: str, config: Dict[str, Any]) -> ParsedContent: +def file_contents(contents: str, config: Config=DEFAULT_CONFIG) -> ParsedContent: """Parses a python file taking out and categorizing imports.""" - line_separator: str = config["line_ending"] or _infer_line_separator(contents) - add_imports = (format_natural(addition) for addition in config["add_imports"]) + line_separator: str = config.line_ending or _infer_line_separator(contents) + add_imports = (format_natural(addition) for addition in config.add_imports) in_lines = contents.split(line_separator) out_lines = [] original_line_count = len(in_lines) - sections: Any = namedtuple("Sections", config["sections"])(*config["sections"]) + sections: Any = namedtuple("Sections", config.sections)(*config.sections) section_comments = [ - "# " + value for key, value in config.items() if key.startswith("import_heading") and value + "# " + value for key, value in vars(config).items() if key.startswith("import_heading") and value ] finder = FindersManager(config=config, sections=sections) - if original_line_count > 1 or in_lines[:1] not in ([], [""]) or config["force_adds"]: + if original_line_count > 1 or in_lines[:1] not in ([], [""]) or config.force_adds: in_lines.extend(add_imports) line_count = len(in_lines) @@ -161,7 +162,7 @@ def file_contents(contents: str, config: Dict[str, Any]) -> ParsedContent: import_placements: Dict[str, str] = {} as_map: Dict[str, List[str]] = defaultdict(list) imports: OrderedDict[str, Dict[str, Any]] = OrderedDict() - for section in chain(sections, config["forced_separate"]): + for section in chain(sections, config.forced_separate): imports[section] = {"straight": OrderedDict(), "from": OrderedDict()} categorized_comments: CommentsDict = { "from": {}, @@ -331,14 +332,14 @@ def file_contents(contents: str, config: Dict[str, Any]) -> ParsedContent: else: module = just_imports[as_index - 1] as_map[module].append(just_imports[as_index + 1]) - if not config["combine_as_imports"]: + if not config.combine_as_imports: categorized_comments["straight"][module] = comments comments = [] del just_imports[as_index : as_index + 2] if type_of_import == "from": import_from = just_imports.pop(0) placed_module = finder.find(import_from) - if config["verbose"]: + if config.verbose: print(f"from-type place_module for {import_from} returned {placed_module}") if placed_module == "": warn( @@ -415,7 +416,7 @@ def file_contents(contents: str, config: Dict[str, Any]) -> ParsedContent: categorized_comments["above"]["straight"].get(module, []) ) placed_module = finder.find(module) - if config["verbose"]: + if config.verbose: print(f"else-type place_module for {module} returned {placed_module}") if placed_module == "": warn( diff --git a/isort/settings.py b/isort/settings.py index e6965e2cf..8a3e42a8e 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -27,6 +27,7 @@ Optional, Tuple, Union, + FrozenSet ) from warnings import warn @@ -140,6 +141,8 @@ class _Config: safety_excludes: bool = True case_sensitive: bool = False sources: FrozenSet[str] = frozenset() + virtual_env: str = "" + conda_env: str = "" def __post_init__(self): py_version = self.py_version From 7e42dc28fe7c347c19713e123845fbac7f8015c8 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 3 Nov 2019 00:47:57 -0700 Subject: [PATCH 0166/1439] Switch from config[' to config. --- isort/output.py | 133 +++++++++++++++++++++++----------------------- isort/settings.py | 1 + isort/sorting.py | 18 ++++--- 3 files changed, 78 insertions(+), 74 deletions(-) diff --git a/isort/output.py b/isort/output.py index 31ce6ad03..ff90006ee 100644 --- a/isort/output.py +++ b/isort/output.py @@ -7,10 +7,11 @@ from . import parse, sorting, wrap from .comments import add_to_line as with_comments +from .settings import Config, DEFAULT_CONFIG def sorted_imports( - parsed: parse.ParsedContent, config: Dict[str, Any], extension: str = "py" + parsed: parse.ParsedContent, config: Config=DEFAULT_CONFIG, extension: str = "py" ) -> List[str]: """Adds the imports back to the file. @@ -18,12 +19,12 @@ def sorted_imports( """ formatted_output: List[str] = parsed.lines_without_imports.copy() - remove_imports = [format_simplified(removal) for removal in config["remove_imports"]] + remove_imports = [format_simplified(removal) for removal in config.remove_imports] - sort_ignore_case = config["force_alphabetical_sort_within_sections"] - sections: Iterable[str] = itertools.chain(parsed.sections, config["forced_separate"]) + sort_ignore_case = config.force_alphabetical_sort_within_sections + sections: Iterable[str] = itertools.chain(parsed.sections, config.forced_separate) - if config["no_sections"]: + if config.no_sections: parsed.imports["no_sections"] = {"straight": [], "from": {}} for section in sections: parsed.imports["no_sections"]["straight"].extend( @@ -44,11 +45,11 @@ def sorted_imports( from_modules, key=lambda key: sorting.module_key(key, config, section_name=section) ) - if config["force_sort_within_sections"]: + if config.force_sort_within_sections: copied_comments = copy.deepcopy(parsed.categorized_comments) section_output: List[str] = [] - if config["from_first"]: + if config.from_first: section_output = _with_from_imports( parsed, config, @@ -58,8 +59,8 @@ def sorted_imports( sort_ignore_case, remove_imports, ) - if config["lines_between_types"] and from_modules and straight_modules: - section_output.extend([""] * config["lines_between_types"]) + if config.lines_between_types and from_modules and straight_modules: + section_output.extend([""] * config.lines_between_types) section_output = _with_straight_imports( parsed, config, straight_modules, section, section_output, remove_imports ) @@ -67,8 +68,8 @@ def sorted_imports( section_output = _with_straight_imports( parsed, config, straight_modules, section, section_output, remove_imports ) - if config["lines_between_types"] and from_modules and straight_modules: - section_output.extend([""] * config["lines_between_types"]) + if config.lines_between_types and from_modules and straight_modules: + section_output.extend([""] * config.lines_between_types) section_output = _with_from_imports( parsed, config, @@ -79,7 +80,7 @@ def sorted_imports( remove_imports, ) - if config["force_sort_within_sections"]: + if config.force_sort_within_sections: # Remove comments section_output = [line for line in section_output if not line.startswith("#")] @@ -87,8 +88,8 @@ def sorted_imports( section_output, key=partial( sorting.section_key, - order_by_type=config["order_by_type"], - force_to_top=config["force_to_top"], + order_by_type=config.order_by_type, + force_to_top=config.force_to_top, ), ) @@ -107,14 +108,14 @@ def sorted_imports( added += 1 section_name = section - no_lines_before = section_name in config["no_lines_before"] + no_lines_before = section_name in config.no_lines_before if section_output: if section_name in parsed.place_imports: parsed.place_imports[section_name] = section_output continue - section_title = config.get("import_heading_" + str(section_name).lower(), "") + section_title = getattr(config, "import_heading_" + str(section_name).lower(), "") if section_title: section_comment = f"# {section_title}" if ( @@ -124,7 +125,7 @@ def sorted_imports( section_output.insert(0, section_comment) if pending_lines_before or not no_lines_before: - output += [""] * config["lines_between_sections"] + output += [""] * config.lines_between_sections output += section_output @@ -186,8 +187,8 @@ def sorted_imports( next_construct = line break - if config["lines_after_imports"] != -1: - formatted_output[imports_tail:0] = ["" for line in range(config["lines_after_imports"])] + if config.lines_after_imports != -1: + formatted_output[imports_tail:0] = ["" for line in range(config.lines_after_imports)] elif extension != "pyi" and ( next_construct.startswith("def ") or next_construct.startswith("class ") @@ -213,7 +214,7 @@ def sorted_imports( def _with_from_imports( parsed: parse.ParsedContent, - config: Dict[str, Any], + config: Config, from_modules: Iterable[str], section: str, section_output: List[str], @@ -227,7 +228,7 @@ def _with_from_imports( import_start = f"from {module} import " from_imports = list(parsed.imports[section]["from"][module]) - if not config["no_inline_sort"] or config["force_single_line"]: + if not config.no_inline_sort or config.force_single_line: from_imports = sorting.naturally( from_imports, key=lambda key: sorting.module_key( @@ -247,15 +248,15 @@ def _with_from_imports( for from_import, sub_module in zip(from_imports, sub_modules) if sub_module in parsed.as_map } - if config["combine_as_imports"] and not ("*" in from_imports and config["combine_star"]): - if not config["no_inline_sort"]: + if config.combine_as_imports and not ("*" in from_imports and config.combine_star): + if not config.no_inline_sort: for as_import in as_imports: as_imports[as_import] = sorting.naturally(as_imports[as_import]) for from_import in copy.copy(from_imports): if from_import in as_imports: idx = from_imports.index(from_import) if ( - config["keep_direct_and_as_imports"] + config.keep_direct_and_as_imports and parsed.imports[section]["from"][module][from_import] ): from_imports[(idx + 1) : (idx + 1)] = as_imports.pop(from_import) @@ -264,38 +265,38 @@ def _with_from_imports( while from_imports: comments = parsed.categorized_comments["from"].pop(module, ()) - if "*" in from_imports and config["combine_star"]: + if "*" in from_imports and config.combine_star: import_statement = wrap.line( with_comments( comments, f"{import_start}*", - removed=config["ignore_comments"], - comment_prefix=config["comment_prefix"], + removed=config.ignore_comments, + comment_prefix=config.comment_prefix, ), parsed.line_separator, config, ) from_imports = [] - elif config["force_single_line"]: + elif config.force_single_line: import_statement = "" while from_imports: from_import = from_imports.pop(0) single_import_line = with_comments( comments, import_start + from_import, - removed=config["ignore_comments"], - comment_prefix=config["comment_prefix"], + removed=config.ignore_comments, + comment_prefix=config.comment_prefix, ) comment = ( parsed.categorized_comments["nested"].get(module, {}).pop(from_import, None) ) if comment: single_import_line += ( - f"{comments and ';' or config['comment_prefix']} " f"{comment}" + f"{comments and ';' or config.comment_prefix} " f"{comment}" ) if from_import in as_imports: if ( - config["keep_direct_and_as_imports"] + config.keep_direct_and_as_imports and parsed.imports[section]["from"][module][from_import] ): new_section_output.append( @@ -308,8 +309,8 @@ def _with_from_imports( with_comments( from_comments, wrap.line(import_start + as_import, parsed.line_separator, config), - removed=config["ignore_comments"], - comment_prefix=config["comment_prefix"], + removed=config.ignore_comments, + comment_prefix=config.comment_prefix, ) for as_import in sorting.naturally(as_imports[from_import]) ) @@ -327,12 +328,12 @@ def _with_from_imports( ) above_comments = parsed.categorized_comments["above"]["from"].pop(module, None) if above_comments: - if new_section_output and config.get("ensure_newline_before_comments"): + if new_section_output and config.ensure_newline_before_comments: new_section_output.append("") new_section_output.extend(above_comments) if ( - config["keep_direct_and_as_imports"] + config.keep_direct_and_as_imports and parsed.imports[section]["from"][module][from_import] ): new_section_output.append( @@ -341,16 +342,16 @@ def _with_from_imports( wrap.line( import_start + from_import, parsed.line_separator, config ), - removed=config["ignore_comments"], - comment_prefix=config["comment_prefix"], + removed=config.ignore_comments, + comment_prefix=config.comment_prefix, ) ) new_section_output.extend( with_comments( from_comments, wrap.line(import_start + as_import, parsed.line_separator, config), - removed=config["ignore_comments"], - comment_prefix=config["comment_prefix"], + removed=config.ignore_comments, + comment_prefix=config.comment_prefix, ) for as_import in as_imports[from_import] ) @@ -361,8 +362,8 @@ def _with_from_imports( with_comments( comments, f"{import_start}*", - removed=config["ignore_comments"], - comment_prefix=config["comment_prefix"], + removed=config.ignore_comments, + comment_prefix=config.comment_prefix, ) ) from_imports.remove("*") @@ -370,7 +371,7 @@ def _with_from_imports( comments = None for from_import in copy.copy(from_imports): - if from_import in as_imports and not config["keep_direct_and_as_imports"]: + if from_import in as_imports and not config.keep_direct_and_as_imports: continue comment = ( parsed.categorized_comments["nested"].get(module, {}).pop(from_import, None) @@ -379,17 +380,17 @@ def _with_from_imports( single_import_line = with_comments( comments, import_start + from_import, - removed=config["ignore_comments"], - comment_prefix=config["comment_prefix"], + removed=config.ignore_comments, + comment_prefix=config.comment_prefix, ) single_import_line += ( - f"{comments and ';' or config['comment_prefix']} " f"{comment}" + f"{comments and ';' or config.comment_prefix} " f"{comment}" ) above_comments = parsed.categorized_comments["above"]["from"].pop( module, None ) if above_comments: - if new_section_output and config.get("ensure_newline_before_comments"): + if new_section_output and config.ensure_newline_before_comments: new_section_output.append("") new_section_output.extend(above_comments) new_section_output.append( @@ -402,8 +403,8 @@ def _with_from_imports( while from_imports and ( from_imports[0] not in as_imports or ( - config["keep_direct_and_as_imports"] - and config["combine_as_imports"] + config.keep_direct_and_as_imports + and config.combine_as_imports and parsed.imports[section]["from"][module][from_import] ) ): @@ -414,27 +415,27 @@ def _with_from_imports( import_statement = with_comments( comments, import_start + (", ").join(from_import_section), - removed=config["ignore_comments"], - comment_prefix=config["comment_prefix"], + removed=config.ignore_comments, + comment_prefix=config.comment_prefix, ) if not from_import_section: import_statement = "" do_multiline_reformat = False - force_grid_wrap = config["force_grid_wrap"] + force_grid_wrap = config.force_grid_wrap if force_grid_wrap and len(from_import_section) >= force_grid_wrap: do_multiline_reformat = True - if len(import_statement) > config["line_length"] and len(from_import_section) > 1: + if len(import_statement) > config.line_length and len(from_import_section) > 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) > config["line_length"] + len(import_statement) > config.line_length and len(from_import_section) > 0 - and config["multi_line_output"] + and config.multi_line_output not in (wrap.Modes.GRID, wrap.Modes.VERTICAL) # type: ignore # type: ignore ): do_multiline_reformat = True @@ -444,9 +445,9 @@ def _with_from_imports( import_start, from_import_section, comments, config, parsed.line_separator ) if ( - config["multi_line_output"] == wrap.Modes.GRID # type: ignore + config.multi_line_output == wrap.Modes.GRID # type: ignore ): # type: ignore - config["multi_line_output"] = wrap.Modes.VERTICAL_GRID # type: ignore + config.multi_line_output = wrap.Modes.VERTICAL_GRID # type: ignore try: other_import_statement = wrap.import_statement( import_start, @@ -457,18 +458,18 @@ def _with_from_imports( ) if ( max(len(x) for x in import_statement.split("\n")) - > config["line_length"] + > config.line_length ): import_statement = other_import_statement finally: - config["multi_line_output"] = wrap.Modes.GRID # type: ignore - if not do_multiline_reformat and len(import_statement) > config["line_length"]: + config.multi_line_output = wrap.Modes.GRID # type: ignore + if not do_multiline_reformat and len(import_statement) > config.line_length: import_statement = wrap.line(import_statement, parsed.line_separator, config) if import_statement: above_comments = parsed.categorized_comments["above"]["from"].pop(module, None) if above_comments: - if new_section_output and config.get("ensure_newline_before_comments"): + if new_section_output and config.ensure_newline_before_comments: new_section_output.append("") new_section_output.extend(above_comments) new_section_output.append(import_statement) @@ -477,7 +478,7 @@ def _with_from_imports( def _with_straight_imports( parsed: parse.ParsedContent, - config: Dict[str, Any], + config: Config, straight_modules: Iterable[str], section: str, section_output: List[str], @@ -490,7 +491,7 @@ def _with_straight_imports( import_definition = [] if module in parsed.as_map: - if config["keep_direct_and_as_imports"] and parsed.imports[section]["straight"][module]: + if config.keep_direct_and_as_imports and parsed.imports[section]["straight"][module]: import_definition.append(f"import {module}") import_definition.extend( f"import {module} as {as_import}" for as_import in parsed.as_map[module] @@ -500,15 +501,15 @@ def _with_straight_imports( comments_above = parsed.categorized_comments["above"]["straight"].pop(module, None) if comments_above: - if new_section_output and config.get("ensure_newline_before_comments"): + if new_section_output and config.ensure_newline_before_comments: new_section_output.append("") new_section_output.extend(comments_above) new_section_output.extend( with_comments( parsed.categorized_comments["straight"].get(module), idef, - removed=config["ignore_comments"], - comment_prefix=config["comment_prefix"], + removed=config.ignore_comments, + comment_prefix=config.comment_prefix, ) for idef in import_definition ) diff --git a/isort/settings.py b/isort/settings.py index 8a3e42a8e..c72426473 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -143,6 +143,7 @@ class _Config: sources: FrozenSet[str] = frozenset() virtual_env: str = "" conda_env: str = "" + ensure_newline_before_comments: bool = False def __post_init__(self): py_version = self.py_version diff --git a/isort/sorting.py b/isort/sorting.py index 591d3d938..49254ca01 100644 --- a/isort/sorting.py +++ b/isort/sorting.py @@ -1,5 +1,6 @@ import re from typing import Any, Callable, Iterable, List, Mapping, Optional +from . settings import Config _import_line_intro_re = re.compile("^(?:from|import) ") _import_line_midline_import_re = re.compile(" import ") @@ -7,14 +8,14 @@ def module_key( module_name: str, - config: Mapping[str, Any], + config: Config, sub_imports: bool = False, ignore_case: bool = False, section_name: Optional[Any] = None, ) -> str: match = re.match(r"^(\.+)\s*(.*)", module_name) if match: - sep = " " if config["reverse_relative"] else "_" + sep = " " if config.reverse_relative else "_" module_name = sep.join(match.groups()) prefix = "" @@ -23,22 +24,23 @@ def module_key( else: module_name = str(module_name) - if sub_imports and config["order_by_type"]: + if sub_imports and config.order_by_type: if module_name.isupper() and len(module_name) > 1: # see issue #376 prefix = "A" elif module_name[0:1].isupper(): prefix = "B" else: prefix = "C" - if not config["case_sensitive"]: + 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"] + + if section_name: + length_sort = getattr(config, "length_sort_" + str(section_name).lower(), config.length_sort) else: - length_sort = config["length_sort_" + str(section_name).lower()] + length_sort = config.length_sort _length_sort_maybe = length_sort and (str(len(module_name)) + ":" + module_name) or module_name - return f"{module_name in config['force_to_top'] and 'A' or 'B'}{prefix}{_length_sort_maybe}" + return f"{module_name in config.force_to_top and 'A' or 'B'}{prefix}{_length_sort_maybe}" def section_key(line: str, order_by_type: bool, force_to_top: List[str]) -> str: From 1a68f42f0d7e09e74db956f7eeb522e148158fb7 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 3 Nov 2019 19:11:15 -0800 Subject: [PATCH 0167/1439] Black formatting --- isort/output.py | 6 ++---- isort/parse.py | 6 ++++-- isort/settings.py | 50 ++++++++++++++++++++++++++++++++++------------- isort/sorting.py | 6 ++++-- 4 files changed, 46 insertions(+), 22 deletions(-) diff --git a/isort/output.py b/isort/output.py index ff90006ee..1dd9ec29a 100644 --- a/isort/output.py +++ b/isort/output.py @@ -11,7 +11,7 @@ def sorted_imports( - parsed: parse.ParsedContent, config: Config=DEFAULT_CONFIG, extension: str = "py" + parsed: parse.ParsedContent, config: Config = DEFAULT_CONFIG, extension: str = "py" ) -> List[str]: """Adds the imports back to the file. @@ -444,9 +444,7 @@ def _with_from_imports( import_statement = wrap.import_statement( import_start, from_import_section, comments, config, parsed.line_separator ) - if ( - config.multi_line_output == wrap.Modes.GRID # type: ignore - ): # type: ignore + if config.multi_line_output == wrap.Modes.GRID: # type: ignore # type: ignore config.multi_line_output = wrap.Modes.VERTICAL_GRID # type: ignore try: other_import_statement = wrap.import_statement( diff --git a/isort/parse.py b/isort/parse.py index 8fde32e97..6882e7ecc 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -139,7 +139,7 @@ class ParsedContent(NamedTuple): section_comments: List[str] -def file_contents(contents: str, config: Config=DEFAULT_CONFIG) -> ParsedContent: +def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedContent: """Parses a python file taking out and categorizing imports.""" line_separator: str = config.line_ending or _infer_line_separator(contents) add_imports = (format_natural(addition) for addition in config.add_imports) @@ -149,7 +149,9 @@ def file_contents(contents: str, config: Config=DEFAULT_CONFIG) -> ParsedContent sections: Any = namedtuple("Sections", config.sections)(*config.sections) section_comments = [ - "# " + value for key, value in vars(config).items() if key.startswith("import_heading") and value + "# " + value + for key, value in vars(config).items() + if key.startswith("import_heading") and value ] finder = FindersManager(config=config, sections=sections) diff --git a/isort/settings.py b/isort/settings.py index c72426473..10bafe6e5 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -27,7 +27,7 @@ Optional, Tuple, Union, - FrozenSet + FrozenSet, ) from warnings import warn @@ -60,7 +60,13 @@ VALID_PY_TARGETS: Tuple[str, ...] = tuple( target.replace("py", "") for target in dir(stdlibs) if not target.startswith("_") ) -CONFIG_SOURCES: Tuple[str, ...] = (".isort.cfg", "pyproject.toml", "setup.cfg", "tox.ini", ".editorconfig") +CONFIG_SOURCES: Tuple[str, ...] = ( + ".isort.cfg", + "pyproject.toml", + "setup.cfg", + "tox.ini", + ".editorconfig", +) CONFIG_SECTIONS: Dict[str, Tuple[str, ...]] = { ".isort.cfg": ("settings", "isort"), "pyproject.toml": ("tool.isort",), @@ -86,6 +92,7 @@ class _Config: NOTE: known lists, such as known_standard_library, are intentionally not complete as they are dynamically determined later on. """ + py_version: str = "3" force_to_top: FrozenSet[str] = frozenset() skip: FrozenSet[str] = frozenset() @@ -95,8 +102,8 @@ class _Config: line_ending: str = "" sections: Tuple[str, ...] = DEFAULT_SECTIONS no_sections: bool = False - known_future_library: FrozenSet[str] = frozenset(("__future__", )) - known_third_party: FrozenSet[str] = frozenset(("google.appengine.api", )) + known_future_library: FrozenSet[str] = frozenset(("__future__",)) + known_third_party: FrozenSet[str] = frozenset(("google.appengine.api",)) known_first_party: FrozenSet[str] = frozenset() known_standard_library: FrozenSet[str] = frozenset() multi_line_output = WrapModes.GRID # type: ignore @@ -148,7 +155,14 @@ class _Config: def __post_init__(self): py_version = self.py_version if py_version == "auto": - py_version = f"{sys.version_info.major}{sys.version_info.minor}" + if sys.version_info.major == 2 and sys.version_info.minor <= 6: + py_version = "2" + elif sys.version_info.major == 3 and ( + sys.version_info.minor <= 5 or sys.version_info.minor >= 8 + ): + py_version = "3" + else: + py_version = f"{sys.version_info.major}{sys.version_info.minor}" if py_version not in VALID_PY_TARGETS: raise ValueError( @@ -172,16 +186,20 @@ def __post_init__(self): object.__setattr__(self, "lines_between_types", 1) object.__setattr__(self, "from_first", True) + _DEFAULT_SETTINGS = {**vars(_Config()), "source": "defaults"} class Config(_Config): - def __init__(self, settings_file:str="", settings_path:str="", **config_overrides): + def __init__(self, settings_file: str = "", settings_path: str = "", **config_overrides): sources: List[Dict[str, Any]] = [_DEFAULT_SETTINGS] config_settings: Dict[str, Any] if settings_file: - config_settings = config_data = get_config_data(settings_file, CONFIG_SECTIONS.get(os.path.basename(settings_file), FALLBACK_CONFIG_SECTIONS)) + config_settings = config_data = get_config_data( + settings_file, + CONFIG_SECTIONS.get(os.path.basename(settings_file), FALLBACK_CONFIG_SECTIONS), + ) elif settings_path: config_settings = _find_config(settings_path) else: @@ -230,7 +248,9 @@ def file_should_be_skipped(self, filename: str, path: str = "") -> bool: return True for skip_path in self.skip: - if posixpath.abspath(normalized_path) == posixpath.abspath(skip_path.replace("\\", "/")): + if posixpath.abspath(normalized_path) == posixpath.abspath( + skip_path.replace("\\", "/") + ): return True position = os.path.split(filename) @@ -264,12 +284,14 @@ def _as_list(value: str) -> List[str]: def _abspaths(cwd: str, values: Iterable[str]) -> List[str]: - paths = set([ - os.path.join(cwd, value) - if not value.startswith(os.path.sep) and value.endswith(os.path.sep) - else value - for value in values - ]) + paths = set( + [ + os.path.join(cwd, value) + if not value.startswith(os.path.sep) and value.endswith(os.path.sep) + else value + for value in values + ] + ) return paths diff --git a/isort/sorting.py b/isort/sorting.py index 49254ca01..0c314e224 100644 --- a/isort/sorting.py +++ b/isort/sorting.py @@ -1,6 +1,6 @@ import re from typing import Any, Callable, Iterable, List, Mapping, Optional -from . settings import Config +from .settings import Config _import_line_intro_re = re.compile("^(?:from|import) ") _import_line_midline_import_re = re.compile(" import ") @@ -35,7 +35,9 @@ def module_key( module_name = module_name.lower() if section_name: - length_sort = getattr(config, "length_sort_" + str(section_name).lower(), config.length_sort) + length_sort = getattr( + config, "length_sort_" + str(section_name).lower(), config.length_sort + ) else: length_sort = config.length_sort From 77ecbd715007a813d6dca11e7df751953161e010 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 3 Nov 2019 20:43:44 -0800 Subject: [PATCH 0168/1439] Clean up CLI settings --- CHANGELOG.md | 2 ++ isort/main.py | 82 ++++++++++++++++++++++++--------------------------- 2 files changed, 40 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 720ec50a6..0f5f19cd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ Changelog - isort deprecates official support for Python 3.4, removing modules only in this release from known_standard_library: - user - Config files are no longer composed on-top of each-other. Instead the first config file found is used. + - Since there is no longer composition negative form settings (such as --dont-skip) are no longer required and have been removed. + - Two-letter shortened setting names (like `ac` for `atomic`) now require two dashes to avoid ambiguity: `--ac`. Internal: - isort now utilizes mypy and typing to filter out typing related issues before deployment. diff --git a/isort/main.py b/isort/main.py index bc17ada31..2c4c457ba 100644 --- a/isort/main.py +++ b/isort/main.py @@ -158,14 +158,14 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: "automatically determining correct placement.", ) parser.add_argument( - "-ac", + "--ac", "--atomic", dest="atomic", action="store_true", help="Ensures the output doesn't save if the resulting file contains syntax errors.", ) parser.add_argument( - "-af", + "--af", "--force-adds", dest="force_adds", action="store_true", @@ -187,14 +187,14 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: "command line without modifying the file.", ) parser.add_argument( - "-ca", + "--ca", "--combine-as", dest="combine_as_imports", action="store_true", help="Combines as imports on the same line.", ) parser.add_argument( - "-cs", + "--cs", "--combine-star", dest="combine_star", action="store_true", @@ -209,7 +209,7 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: action="store_true", ) parser.add_argument( - "-df", + "--df", "--diff", dest="show_diff", action="store_true", @@ -217,19 +217,12 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: "changing it in place", ) parser.add_argument( - "-ds", + "--ds", "--no-sections", help="Put all imports into the same section bucket", dest="no_sections", action="store_true", ) - parser.add_argument( - "-dt", - "--dont-order-by-type", - dest="dont_order_by_type", - action="store_true", - help="Only order imports alphabetically, do not attempt type ordering", - ) parser.add_argument( "-e", "--balanced", @@ -246,28 +239,28 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: "of the future compatibility libraries.", ) parser.add_argument( - "-fas", + "--fas", "--force-alphabetical-sort", action="store_true", dest="force_alphabetical_sort", help="Force all imports to be sorted as a single section", ) parser.add_argument( - "-fass", + "--fass", "--force-alphabetical-sort-within-sections", action="store_true", dest="force_alphabetical_sort", help="Force all imports to be sorted alphabetically within a section", ) parser.add_argument( - "-ff", + "--ff", "--from-first", dest="from_first", help="Switches the typical ordering preference, " "showing from imports first then straight ones.", ) parser.add_argument( - "-fgw", + "--fgw", "--force-grid-wrap", nargs="?", const=2, @@ -277,7 +270,7 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: "length", ) parser.add_argument( - "-fss", + "--fss", "--force-sort-within-sections", action="store_true", dest="force_sort_within_sections", @@ -310,14 +303,14 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: parser.add_argument("-lai", "--lines-after-imports", dest="lines_after_imports", type=int) parser.add_argument("-lbt", "--lines-between-types", dest="lines_between_types", type=int) parser.add_argument( - "-le", + "--le", "--line-ending", dest="line_ending", help="Forces line endings to the specified value. " "If not set, values will be guessed per-file.", ) parser.add_argument( - "-ls", + "--ls", "--length-sort", help="Sort imports by their string length.", dest="length_sort", @@ -332,7 +325,7 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: "5-vert-grid-grouped, 6-vert-grid-grouped-no-comma).", ) inline_args_group.add_argument( - "-nis", + "--nis", "--no-inline-sort", dest="no_inline_sort", action="store_true", @@ -340,19 +333,12 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: "(e.g. `from foo import a, c ,b`).", ) parser.add_argument( - "-nlb", + "--nlb", "--no-lines-before", help="Sections which should not be split with previous by empty lines", dest="no_lines_before", action="append", ) - parser.add_argument( - "-ns", - "--dont-skip", - help="Files that sort imports should never skip over.", - dest="not_skip", - action="append", - ) parser.add_argument( "-o", "--thirdparty", @@ -361,12 +347,19 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: help="Force sortImports to recognize a module as being part of a third party library.", ) parser.add_argument( - "-ot", + "--ot", "--order-by-type", dest="order_by_type", action="store_true", help="Order imports by type in addition to alphabetically", ) + parser.add_argument( + "--dot", + "--dont-order-by-type", + dest="dont_order_by_type", + action="store_true", + help="Don't order imports by type in addition to alphabetically", + ) parser.add_argument( "-p", "--project", @@ -383,21 +376,21 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: ) parser.add_argument("-r", dest="ambiguous_r_flag", action="store_true") parser.add_argument( - "-rm", + "--rm", "--remove-import", dest="remove_imports", action="append", help="Removes the specified import from all files.", ) parser.add_argument( - "-rr", + "--rr", "--reverse-relative", dest="reverse_relative", action="store_true", help="Reverse order of relative imports.", ) parser.add_argument( - "-rc", + "--rc", "--recursive", dest="recursive", action="store_true", @@ -412,28 +405,28 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: action="append", ) parser.add_argument( - "-sd", + "--sd", "--section-default", dest="default_section", help="Sets the default section for imports (by default FIRSTPARTY) options: " + str(DEFAULT_SECTIONS), ) parser.add_argument( - "-sg", + "--sg", "--skip-glob", help="Files that sort imports should skip over.", dest="skip_glob", action="append", ) inline_args_group.add_argument( - "-sl", + "--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( - "-sp", + "--sp", "--settings-path", dest="settings_path", help="Explicitly set the settings path instead of auto determining based on file location.", @@ -446,14 +439,14 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: action="append", ) parser.add_argument( - "-tc", + "--tc", "--trailing-comma", dest="include_trailing_comma", action="store_true", help="Includes a trailing comma on multi line imports that include parentheses.", ) parser.add_argument( - "-up", + "--up", "--use-parentheses", dest="use_parentheses", action="store_true", @@ -461,7 +454,7 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: ) parser.add_argument("-v", "--version", action="store_true", dest="show_version") parser.add_argument( - "-vb", + "--vb", "--verbose", action="store_true", dest="verbose", @@ -478,7 +471,7 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: help="Conda environment to use for determining whether a package is third-party", ) parser.add_argument( - "-vn", + "--vn", "--version-number", action="version", version=__version__, @@ -492,14 +485,14 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: type=int, ) parser.add_argument( - "-wl", + "--wl", "--wrap-length", dest="wrap_length", type=int, help="Specifies how long lines that are wrapped should be, if not set line_length is used.", ) parser.add_argument( - "-ws", + "--ws", "--ignore-whitespace", action="store_true", dest="ignore_whitespace", @@ -536,7 +529,7 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: "files", nargs="*", help="One or more Python source files that need their imports sorted." ) parser.add_argument( - "-py", + "--py", "--python-version", action="store", dest="py_version", @@ -548,6 +541,7 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: f"(currently: {sys.version_info.major}{sys.version_info.minor}) will be used.", ) + values = vars(parser.parse_args(argv)) arguments = {key: value for key, value in vars(parser.parse_args(argv)).items() if value} if "dont_order_by_type" in arguments: arguments["order_by_type"] = False From 2221d4603d18395072fba2a881a3b38c7fdb53f4 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 4 Nov 2019 00:33:30 -0800 Subject: [PATCH 0169/1439] Add support for automatically stopping config search when root of repo detected --- isort/settings.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/isort/settings.py b/isort/settings.py index 10bafe6e5..220b71ace 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -56,6 +56,7 @@ ) MAX_CONFIG_SEARCH_DEPTH: int = 25 # The number of parent directories to for a config file within +STOP_CONFIG_SEARCH_ON_DIRS: Tuple[str, ...] = (".git", ".hg") DEFAULT_SECTIONS: Tuple[str, ...] = ("FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER") VALID_PY_TARGETS: Tuple[str, ...] = tuple( target.replace("py", "") for target in dir(stdlibs) if not target.startswith("_") @@ -314,6 +315,10 @@ def _find_config(path: str) -> Dict[str, Any]: if config_data: return config_data + for stop_dir in STOP_CONFIG_SEARCH_ON_DIRS: + if os.path.isdir(stop_dir): + break + new_directory = os.path.split(current_directory)[0] if new_directory == current_directory: break From 5f309134d3db214dfc368454189bc2c35c9d4ceb Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 4 Nov 2019 16:06:03 -0800 Subject: [PATCH 0170/1439] Add initial simplified API for using isort --- .gitignore | 1 + isort/api.py | 63 +++++++++++++++++++++++++++++++++++++++++++++++ isort/compat.py | 45 ++------------------------------- isort/isort.py | 12 +-------- isort/output.py | 19 ++++++++++++-- isort/settings.py | 2 ++ 6 files changed, 86 insertions(+), 56 deletions(-) create mode 100644 isort/api.py diff --git a/.gitignore b/.gitignore index 74f41785f..d16f7c744 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ npm-debug.log # Unit test / coverage reports .coverage +.coverage.* .pytest_cache .tox nosetests.xml diff --git a/isort/api.py b/isort/api.py new file mode 100644 index 000000000..5279dcbb0 --- /dev/null +++ b/isort/api.py @@ -0,0 +1,63 @@ +from .settings import Config, DEFAULT_CONFIG +from . import parse, output +import re +from pathlib import Path +from typing import Any, Optional, Tuple + +_ENCODING_PATTERN = re.compile(br"^[ \t\f]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)") + + +def sorted_contents(file_contents: str, extension: str = "py", config: Config=DEFAULT_CONFIG, **config_kwargs) -> str: + if config_kwargs and config is not DEFAULT_CONFIG: + raise ValueError("You can either specify custom configuration options using kwargs or " + "passing in a Config object. Not Both!") + elif config_kwargs: + config = Config(**config_kwargs) + + return output.sorted_imports(parse.file_contents(file_contents, config=config), config, extension) + + +def sorted_file(filename: str, config: Config=DEFAULT_CONFIG, **config_kwargs) -> str: + file_path = Path(filename).resolve() + if config_kwargs or config is DEFAULT_CONFIG: + if not "settings_path" in config_kwargs and not "settings_file" in config_kwargs: + config_kwargs["settings_path"] = file_path.parent + + contents, used_encoding = _read_file_contents(file_path) + return sorted_contents(file_contents=contents, extension=file_path.suffix, config=config, **config_kwargs) + + +def _determine_file_encoding(file_path: Path, default: str = "utf-8") -> str: + # see https://www.python.org/dev/peps/pep-0263/ + coding = default + with file_path.open("rb") as f: + for line_number, line in enumerate(f, 1): + if line_number > 2: + break + groups = re.findall(_ENCODING_PATTERN, line) + if groups: + coding = groups[0].decode("ascii") + break + + return coding + + +def _read_file_contents( + file_path: Path +) -> Tuple[Optional[str], Optional[str]]: + encoding = _determine_file_encoding(file_path) + with file_path.open(encoding=encoding, newline="") as file_to_import_sort: + try: + file_contents = file_to_import_sort.read() + return file_contents, encoding + except UnicodeDecodeError: + pass + + # Try default encoding for open(mode='r') on the system + fallback_encoding = _locale.getpreferredencoding(False) + with file_path.open(encoding=fallback_encoding, newline="") as file_to_import_sort: + try: + file_contents = file_to_import_sort.read() + return file_contents, fallback_encoding + except UnicodeDecodeError: + return None, None diff --git a/isort/compat.py b/isort/compat.py index a85dc66b2..1b73fb2ee 100644 --- a/isort/compat.py +++ b/isort/compat.py @@ -11,54 +11,13 @@ from isort.isort import _SortImports -def determine_file_encoding(file_path: Path, default: str = "utf-8") -> str: - # see https://www.python.org/dev/peps/pep-0263/ - pattern = re.compile(br"^[ \t\f]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)") - - coding = default - with file_path.open("rb") as f: - for line_number, line in enumerate(f, 1): - if line_number > 2: - break - groups = re.findall(pattern, line) - if groups: - coding = groups[0].decode("ascii") - break - - return coding - - -def read_file_contents( - file_path: Path, encoding: str, fallback_encoding: str -) -> Tuple[Optional[str], Optional[str]]: - with file_path.open(encoding=encoding, newline="") as file_to_import_sort: - try: - file_contents = file_to_import_sort.read() - return file_contents, encoding - except UnicodeDecodeError: - pass - - with file_path.open(encoding=fallback_encoding, newline="") as file_to_import_sort: - try: - file_contents = file_to_import_sort.read() - return file_contents, fallback_encoding - except UnicodeDecodeError: - return None, None - - -def resolve(path: Path) -> Path: - if sys.version_info[:2] >= (3, 6): - return path.resolve() - else: - return Path(os.path.abspath(str(path))) - def get_settings_path(settings_path: Optional[Path], current_file_path: Optional[Path]) -> Path: if settings_path: return settings_path if current_file_path: - return resolve(current_file_path).parent + return current_file_path.resolve().parent else: return Path.cwd() @@ -96,7 +55,7 @@ def __init__( if file_path: self.file_path = file_path # raw file path (unresolved) ? - absolute_file_path = resolve(file_path) + absolute_file_path = file_path.resolve() if check_skip: if run_path and run_path in absolute_file_path.parents: # TODO: Drop str() when isort is Python 3.6+. diff --git a/isort/isort.py b/isort/isort.py index 0a532410e..fcc1e574b 100644 --- a/isort/isort.py +++ b/isort/isort.py @@ -14,7 +14,6 @@ from isort import utils from . import output, parse, settings, sorting, wrap -from .finders import FindersManager class _SortImports: @@ -22,16 +21,7 @@ def __init__(self, file_contents: str, config: Dict[str, Any], extension: str = self.config = config self.parsed = parse.file_contents(file_contents, config=self.config) - if self.parsed.import_index != -1: - self.out_lines = output.sorted_imports(self.parsed, self.config, extension) - else: - self.out_lines = self.parsed.lines_without_imports - - while self.out_lines and self.out_lines[-1].strip() == "": - self.out_lines.pop(-1) - - self.out_lines.append("") - self.output = self.parsed.line_separator.join(self.out_lines) + self.output = output.sorted_imports(self.parsed, self.config, extension) def remove_whitespaces(self, contents: str) -> str: contents = ( diff --git a/isort/output.py b/isort/output.py index 1dd9ec29a..4c3cd132a 100644 --- a/isort/output.py +++ b/isort/output.py @@ -12,12 +12,15 @@ def sorted_imports( parsed: parse.ParsedContent, config: Config = DEFAULT_CONFIG, extension: str = "py" -) -> List[str]: +) -> str: """Adds the imports back to the file. (at the index of the first import) sorted alphabetically and split between groups """ + if parsed.import_index == -1: + return _output_as_string(parsed.lines_without_imports, parsed.line_separator) + formatted_output: List[str] = parsed.lines_without_imports.copy() remove_imports = [format_simplified(removal) for removal in config.remove_imports] @@ -209,7 +212,7 @@ def sorted_imports( new_out_lines.append("") formatted_output = new_out_lines - return formatted_output + return _output_as_string(formatted_output, parsed.line_separator) def _with_from_imports( @@ -513,3 +516,15 @@ def _with_straight_imports( ) return new_section_output + + +def _output_as_string(lines: List[str], line_separator: str) -> str: + return line_separator.join(_normalize_empty_lines(lines)) + + +def _normalize_empty_lines(lines: List[str]) -> List[str]: + while lines and lines[-1].strip() == "": + lines.pop(-1) + + lines.append("") + return lines diff --git a/isort/settings.py b/isort/settings.py index 220b71ace..b65d8172a 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -231,6 +231,7 @@ def __init__(self, settings_file: str = "", settings_path: str = "", **config_ov combined_config[key] = type(default_value)(value) + combined_config.pop("source", None) super().__init__(**combined_config) def file_should_be_skipped(self, filename: str, path: str = "") -> bool: @@ -386,6 +387,7 @@ def _get_config_data(file_path: str, sections: Iterable[str]) -> Dict[str, Any]: settings["line_length"] = ( float("inf") if max_line_length == "off" else int(max_line_length) ) + settings = {key: value for key, value in settings.items() if key in _DEFAULT_SETTINGS.keys()} for key, value in settings.items(): existing_value_type = _get_str_to_type_converter(key) From a6248e29271ed068a39d067bae15665e21676f8f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 4 Nov 2019 17:04:34 -0800 Subject: [PATCH 0171/1439] Add exceptions module and UnableToDetermineEncoding exception --- isort/api.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/isort/api.py b/isort/api.py index 5279dcbb0..bfb2db868 100644 --- a/isort/api.py +++ b/isort/api.py @@ -1,5 +1,6 @@ from .settings import Config, DEFAULT_CONFIG from . import parse, output +from .exceptions import UnableToDetermineEncoding import re from pathlib import Path from typing import Any, Optional, Tuple @@ -60,4 +61,6 @@ def _read_file_contents( file_contents = file_to_import_sort.read() return file_contents, fallback_encoding except UnicodeDecodeError: - return None, None + pass + + raise UnableToDetermineEncoding(file_path, encoding, fallback_encoding) From 857aca8ef203e39e09173702949b0f89597efb49 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 4 Nov 2019 17:45:13 -0800 Subject: [PATCH 0172/1439] Add support for file skip comment --- isort/api.py | 7 +++++-- isort/compat.py | 2 +- isort/isort.py | 1 + isort/settings.py | 1 + 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/isort/api.py b/isort/api.py index bfb2db868..d5d3ceff5 100644 --- a/isort/api.py +++ b/isort/api.py @@ -1,6 +1,6 @@ -from .settings import Config, DEFAULT_CONFIG +from .settings import Config, DEFAULT_CONFIG, FILE_SKIP_COMMENT from . import parse, output -from .exceptions import UnableToDetermineEncoding +from .exceptions import UnableToDetermineEncoding, FileSkipComment import re from pathlib import Path from typing import Any, Optional, Tuple @@ -25,6 +25,9 @@ def sorted_file(filename: str, config: Config=DEFAULT_CONFIG, **config_kwargs) - config_kwargs["settings_path"] = file_path.parent contents, used_encoding = _read_file_contents(file_path) + if FILE_SKIP_COMMENT in contents: + raise FileSkipComment(file_path) + return sorted_contents(file_contents=contents, extension=file_path.suffix, config=config, **config_kwargs) diff --git a/isort/compat.py b/isort/compat.py index 1b73fb2ee..35afbb86d 100644 --- a/isort/compat.py +++ b/isort/compat.py @@ -95,7 +95,7 @@ def __init__( else: file_encoding = used_encoding - if file_contents is None or ("isort:" + "skip_file") in file_contents: + if file_contents is None or settings.FILE_SKIP_COMMENT in file_contents: self.skipped = True if write_to_stdout and file_contents: sys.stdout.write(file_contents) diff --git a/isort/isort.py b/isort/isort.py index fcc1e574b..d71d29669 100644 --- a/isort/isort.py +++ b/isort/isort.py @@ -5,6 +5,7 @@ or: sorted = SortImports(file_contents=file_contents).output """ +# isort:skip_file import copy import itertools import re diff --git a/isort/settings.py b/isort/settings.py index b65d8172a..25b56c272 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -55,6 +55,7 @@ r"|lib/python[0-9].[0-9]+|node_modules)/" ) +FILE_SKIP_COMMENT: str = ("isort:" + "skip_file") # Concatenated to avoid this file being skipped MAX_CONFIG_SEARCH_DEPTH: int = 25 # The number of parent directories to for a config file within STOP_CONFIG_SEARCH_ON_DIRS: Tuple[str, ...] = (".git", ".hg") DEFAULT_SECTIONS: Tuple[str, ...] = ("FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER") From 28d4647c4fc87fcbbccfe7faf43d84da70938015 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 4 Nov 2019 18:04:41 -0800 Subject: [PATCH 0173/1439] Start implementing atomic behaviour in API --- isort/api.py | 24 +++++++++++++++------ isort/compat.py | 57 +++++++++++++++++++++---------------------------- 2 files changed, 41 insertions(+), 40 deletions(-) diff --git a/isort/api.py b/isort/api.py index d5d3ceff5..000f1daef 100644 --- a/isort/api.py +++ b/isort/api.py @@ -1,6 +1,6 @@ from .settings import Config, DEFAULT_CONFIG, FILE_SKIP_COMMENT from . import parse, output -from .exceptions import UnableToDetermineEncoding, FileSkipComment +from .exceptions import UnableToDetermineEncoding, FileSkipComment, IntroducedSyntaxErrors import re from pathlib import Path from typing import Any, Optional, Tuple @@ -8,14 +8,27 @@ _ENCODING_PATTERN = re.compile(br"^[ \t\f]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)") -def sorted_contents(file_contents: str, extension: str = "py", config: Config=DEFAULT_CONFIG, **config_kwargs) -> str: +def sorted_contents(file_contents: str, extension: str = "py", config: Config=DEFAULT_CONFIG, file_path: Optional[Path]=None, **config_kwargs) -> str: + content_source = str(file_path or "Passed in content") + if FILE_SKIP_COMMENT in contents: + raise FileSkipComment(content_source) + if config_kwargs and config is not DEFAULT_CONFIG: raise ValueError("You can either specify custom configuration options using kwargs or " "passing in a Config object. Not Both!") elif config_kwargs: config = Config(**config_kwargs) - return output.sorted_imports(parse.file_contents(file_contents, config=config), config, extension) + if settings.atomic: + compile(file_contents, content_source, "exec", 0, 1) + + parsed_output = output.sorted_imports(parse.file_contents(file_contents, config=config), config, extension) + if settings.atomic: + try: + compile(file_contents, content_source, "exec", 0, 1) + except SyntaxError: + raise IntroducedSyntaxErrors(content_source) + return parsed_output def sorted_file(filename: str, config: Config=DEFAULT_CONFIG, **config_kwargs) -> str: @@ -25,10 +38,7 @@ def sorted_file(filename: str, config: Config=DEFAULT_CONFIG, **config_kwargs) - config_kwargs["settings_path"] = file_path.parent contents, used_encoding = _read_file_contents(file_path) - if FILE_SKIP_COMMENT in contents: - raise FileSkipComment(file_path) - - return sorted_contents(file_contents=contents, extension=file_path.suffix, config=config, **config_kwargs) + return sorted_contents(file_contents=contents, extension=file_path.suffix, config=config, file_path=file_path, **config_kwargs) def _determine_file_encoding(file_path: Path, default: str = "utf-8") -> str: diff --git a/isort/compat.py b/isort/compat.py index 35afbb86d..211a8032c 100644 --- a/isort/compat.py +++ b/isort/compat.py @@ -6,7 +6,7 @@ from typing import Any, Optional, Tuple from warnings import warn -from isort import settings +from isort import settings, api from isort.format import ask_whether_to_apply_changes_to_file, show_unified_diff from isort.isort import _SortImports @@ -40,15 +40,6 @@ def __init__( extension: Optional[str] = None, **setting_overrides: Any, ): - file_path = None if file_path is None else Path(file_path) - file_name = None - settings_path = None if settings_path is None else Path(settings_path) - - self.config = settings.prepare_config( - get_settings_path(settings_path, file_path), **setting_overrides - ) - self.output = None - file_encoding = "utf-8" self.file_path = None @@ -109,29 +100,29 @@ def __init__( ) self.output = self.sorted_imports.output - if self.config["atomic"]: - logging_file_path = str(self.file_path or "") - try: - out_lines_without_top_comment = ( - self.sorted_imports.get_out_lines_without_top_comment() - ) - compile(out_lines_without_top_comment, logging_file_path, "exec", 0, 1) - except SyntaxError: - self.output = file_contents - self.incorrectly_sorted = True - try: - in_lines_without_top_comment = ( - self.sorted_imports.get_in_lines_without_top_comment() - ) - compile(in_lines_without_top_comment, logging_file_path, "exec", 0, 1) - print( - f"ERROR: {logging_file_path} isort would have introduced syntax errors, " - "please report to the project!" - ) - except SyntaxError: - print(f"ERROR: {logging_file_path} File contains syntax errors.") - - return + #if self.config["atomic"]: + #logging_file_path = str(self.file_path or "") + #try: + #out_lines_without_top_comment = ( + #self.sorted_imports.get_out_lines_without_top_comment() + #) + #compile(out_lines_without_top_comment, logging_file_path, "exec", 0, 1) + #except SyntaxError: + #self.output = file_contents + #self.incorrectly_sorted = True + #try: + #in_lines_without_top_comment = ( + #self.sorted_imports.get_in_lines_without_top_comment() + #) + #compile(in_lines_without_top_comment, logging_file_path, "exec", 0, 1) + #print( + #f"ERROR: {logging_file_path} isort would have introduced syntax errors, " + #"please report to the project!" + #) + #except SyntaxError: + #print(f"ERROR: {logging_file_path} File contains syntax errors.") + + #return if check: check_output = self.output From 456906a6f06875faae729396b65a1c54fd2a7236 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 4 Nov 2019 20:31:23 -0800 Subject: [PATCH 0174/1439] Add exceptions module --- isort/exceptions.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 isort/exceptions.py diff --git a/isort/exceptions.py b/isort/exceptions.py new file mode 100644 index 000000000..759ef6fa3 --- /dev/null +++ b/isort/exceptions.py @@ -0,0 +1,45 @@ +"""All isort specific exception classes should be defined here""" +from path import Path +from .settings import FILE_SKIP_COMMENT + + +class ISortError(Exception): + """Base isort exception object from which all isort sourced exceptions should inherit""" + + +class UnableToDetermineEncoding(ISortError): + """Raised when isort is unable to determine a provided file's encoding""" + + def __init__(self, file_path: Path, encoding: str, fallback_encoding: str): + super().__init__( + f"Couldn't open {file_path} with any of the attempted encodings." + f"Given {encoding} encoding or {fallback_encoding} fallback encoding both failed." + ) + self.file_path = file_path + self.encoding = encoding + self.fallback_encoding = fallback_encoding + + +class IntroducedSyntaxErrors(ISortError): + """Raised when isort has introduced a syntax error in the process of sorting imports""" + + def __init__(self, file_path: Path): + super().__init__( + f"isort introduced syntax errors when attempting to sort the imports contained within " + f"{file_path}." + ) + + +class FileSkipped(ISortError): + """Should be raised when a file is skipped for any reason""" + + def __init__(self, message: str, file_path: Path): + super().__init__(message) + self.file_path = file_path + + +class FileSkipComment(FileSkipped): + """Raised when an entire file is skipped due to a isort skip file comment""" + + def __init__(self, file_path: Path): + super().__init__(f"{file_path} contains an {FILE_SKIP_COMMENT} comment and was skipped.", file_path=file_path) From 6a8d2afa8be832b20791537be61adffd25bfb4c2 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 4 Nov 2019 21:50:31 -0800 Subject: [PATCH 0175/1439] Further work toward making api.py single entrypoint --- isort/api.py | 10 +++-- isort/compat.py | 89 +++++---------------------------------------- isort/exceptions.py | 10 ++++- isort/settings.py | 16 +++++++- 4 files changed, 39 insertions(+), 86 deletions(-) diff --git a/isort/api.py b/isort/api.py index 000f1daef..0196fea02 100644 --- a/isort/api.py +++ b/isort/api.py @@ -8,10 +8,14 @@ _ENCODING_PATTERN = re.compile(br"^[ \t\f]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)") -def sorted_contents(file_contents: str, extension: str = "py", config: Config=DEFAULT_CONFIG, file_path: Optional[Path]=None, **config_kwargs) -> str: +def sorted_contents(file_contents: str, extension: str = "py", config: Config=DEFAULT_CONFIG, file_path: Optional[Path]=None, disregard_skip: bool=False, **config_kwargs) -> str: content_source = str(file_path or "Passed in content") - if FILE_SKIP_COMMENT in contents: - raise FileSkipComment(content_source) + if not disregard_skip: + if FILE_SKIP_COMMENT in contents: + raise FileSkipComment(content_source) + + elif file_path and config.is_skipped(file_path): + raise FileSkipSetting(file_path) if config_kwargs and config is not DEFAULT_CONFIG: raise ValueError("You can either specify custom configuration options using kwargs or " diff --git a/isort/compat.py b/isort/compat.py index 211a8032c..973fed71a 100644 --- a/isort/compat.py +++ b/isort/compat.py @@ -9,6 +9,7 @@ from isort import settings, api from isort.format import ask_whether_to_apply_changes_to_file, show_unified_diff from isort.isort import _SortImports +from .exceptions import FileSkipped @@ -43,86 +44,14 @@ def __init__( file_encoding = "utf-8" self.file_path = None - if file_path: - self.file_path = file_path # raw file path (unresolved) ? - - absolute_file_path = file_path.resolve() - if check_skip: - if run_path and run_path in absolute_file_path.parents: - # TODO: Drop str() when isort is Python 3.6+. - file_name = os.path.relpath(str(absolute_file_path), run_path) - else: - file_name = str(absolute_file_path) - run_path = "" - - if settings.file_should_be_skipped(file_name, self.config, run_path): - self.skipped = True - if self.config["verbose"]: - warn( - f"{absolute_file_path} was skipped as it's listed in 'skip' setting" - " or matches a glob in 'skip_glob' setting" - ) - file_contents = None - - if not self.skipped and not file_contents: - preferred_encoding = determine_file_encoding(absolute_file_path) - - # default encoding for open(mode='r') on the system - fallback_encoding = locale.getpreferredencoding(False) - - file_contents, used_encoding = read_file_contents( - absolute_file_path, - encoding=preferred_encoding, - fallback_encoding=fallback_encoding, - ) - if used_encoding is None: - self.skipped = True - if self.config["verbose"]: - warn( - f"{absolute_file_path} was skipped as it couldn't be opened with the " - f"given {file_encoding} encoding or {fallback_encoding} fallback " - "encoding" - ) - else: - file_encoding = used_encoding - - if file_contents is None or settings.FILE_SKIP_COMMENT in file_contents: - self.skipped = True - if write_to_stdout and file_contents: - sys.stdout.write(file_contents) - return - - if not extension: - extension = file_name.split(".")[-1] if file_name else "py" - - self.sorted_imports = _SortImports( - file_contents=file_contents, config=self.config, extension=extension - ) - self.output = self.sorted_imports.output - - #if self.config["atomic"]: - #logging_file_path = str(self.file_path or "") - #try: - #out_lines_without_top_comment = ( - #self.sorted_imports.get_out_lines_without_top_comment() - #) - #compile(out_lines_without_top_comment, logging_file_path, "exec", 0, 1) - #except SyntaxError: - #self.output = file_contents - #self.incorrectly_sorted = True - #try: - #in_lines_without_top_comment = ( - #self.sorted_imports.get_in_lines_without_top_comment() - #) - #compile(in_lines_without_top_comment, logging_file_path, "exec", 0, 1) - #print( - #f"ERROR: {logging_file_path} isort would have introduced syntax errors, " - #"please report to the project!" - #) - #except SyntaxError: - #print(f"ERROR: {logging_file_path} File contains syntax errors.") - - #return + try: + if file_path: + self.output = api.sorted_file(file_path, extension=extension, **setting_overrides) + else: + self.output = api.sorted_imports(file_contents, extension=extension, **setting_overrides) + except FileSkipped as error: + if self.config["verbose"]: + warn(error.message) if check: check_output = self.output diff --git a/isort/exceptions.py b/isort/exceptions.py index 759ef6fa3..df9623eb6 100644 --- a/isort/exceptions.py +++ b/isort/exceptions.py @@ -1,5 +1,5 @@ """All isort specific exception classes should be defined here""" -from path import Path +from pathlib import Path from .settings import FILE_SKIP_COMMENT @@ -43,3 +43,11 @@ class FileSkipComment(FileSkipped): def __init__(self, file_path: Path): super().__init__(f"{file_path} contains an {FILE_SKIP_COMMENT} comment and was skipped.", file_path=file_path) + + +class FileSkipSetting(FileSkipped): + """Raised when an entire file is skipped due to provided isort settings""" + + def __init__(self, file_path: Path): + super().__init__(f"{file_path} was skipped as it's listed in 'skip' setting" + " or matches a glob in 'skip_glob' setting", file_path=file_path) diff --git a/isort/settings.py b/isort/settings.py index 25b56c272..701e6668b 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -13,6 +13,7 @@ import re import sys import warnings +from pathlib import Path from distutils.util import strtobool as _as_bool from functools import lru_cache from pathlib import Path @@ -153,6 +154,7 @@ class _Config: virtual_env: str = "" conda_env: str = "" ensure_newline_before_comments: bool = False + directory: str = "" def __post_init__(self): py_version = self.py_version @@ -232,12 +234,22 @@ def __init__(self, settings_file: str = "", settings_path: str = "", **config_ov combined_config[key] = type(default_value)(value) + if "directory" not in combined_config: + combined_config["directory"] = os.path.basename(config_settings.get("source", None) or os.getcwd()) + combined_config.pop("source", None) super().__init__(**combined_config) - def file_should_be_skipped(self, filename: str, path: str = "") -> bool: + def is_skipped(self, file_path: Path) -> bool: """Returns True if the file and/or folder should be skipped based on current settings.""" - os_path = os.path.join(path, filename) + if self.directory and self.directory in file_path.parents: + file_name = os.path.relpath(file_path, self.directory) + path = self.directory + else: + file_name = str(absolute_file_path) + path = "" + + os_path = str(file_path) normalized_path = os_path.replace("\\", "/") if normalized_path[1:2] == ":": From db169db5d4a8f81880104ac9f65b98549eafdba0 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 5 Nov 2019 11:59:38 -0800 Subject: [PATCH 0176/1439] Move IO functionality into separate module --- isort/api.py | 52 ++++++------------------------------------------ isort/io.py | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 46 deletions(-) create mode 100644 isort/io.py diff --git a/isort/api.py b/isort/api.py index 0196fea02..652c503e6 100644 --- a/isort/api.py +++ b/isort/api.py @@ -3,15 +3,14 @@ from .exceptions import UnableToDetermineEncoding, FileSkipComment, IntroducedSyntaxErrors import re from pathlib import Path -from typing import Any, Optional, Tuple - -_ENCODING_PATTERN = re.compile(br"^[ \t\f]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)") +from typing import Any, Optional, Tuple, NamedTuple +from .io import File def sorted_contents(file_contents: str, extension: str = "py", config: Config=DEFAULT_CONFIG, file_path: Optional[Path]=None, disregard_skip: bool=False, **config_kwargs) -> str: content_source = str(file_path or "Passed in content") if not disregard_skip: - if FILE_SKIP_COMMENT in contents: + if FILE_SKIP_COMMENT in file_contents: raise FileSkipComment(content_source) elif file_path and config.is_skipped(file_path): @@ -36,48 +35,9 @@ def sorted_contents(file_contents: str, extension: str = "py", config: Config=DE def sorted_file(filename: str, config: Config=DEFAULT_CONFIG, **config_kwargs) -> str: - file_path = Path(filename).resolve() + file_data = File.read(filename) if config_kwargs or config is DEFAULT_CONFIG: if not "settings_path" in config_kwargs and not "settings_file" in config_kwargs: - config_kwargs["settings_path"] = file_path.parent - - contents, used_encoding = _read_file_contents(file_path) - return sorted_contents(file_contents=contents, extension=file_path.suffix, config=config, file_path=file_path, **config_kwargs) - - -def _determine_file_encoding(file_path: Path, default: str = "utf-8") -> str: - # see https://www.python.org/dev/peps/pep-0263/ - coding = default - with file_path.open("rb") as f: - for line_number, line in enumerate(f, 1): - if line_number > 2: - break - groups = re.findall(_ENCODING_PATTERN, line) - if groups: - coding = groups[0].decode("ascii") - break - - return coding - - -def _read_file_contents( - file_path: Path -) -> Tuple[Optional[str], Optional[str]]: - encoding = _determine_file_encoding(file_path) - with file_path.open(encoding=encoding, newline="") as file_to_import_sort: - try: - file_contents = file_to_import_sort.read() - return file_contents, encoding - except UnicodeDecodeError: - pass - - # Try default encoding for open(mode='r') on the system - fallback_encoding = _locale.getpreferredencoding(False) - with file_path.open(encoding=fallback_encoding, newline="") as file_to_import_sort: - try: - file_contents = file_to_import_sort.read() - return file_contents, fallback_encoding - except UnicodeDecodeError: - pass + config_kwargs["settings_path"] = file_data.path.parent - raise UnableToDetermineEncoding(file_path, encoding, fallback_encoding) + return sorted_contents(file_contents=file_data.contents, extension=file_data.path.suffix, config=config, file_path=file_data.path, **config_kwargs) diff --git a/isort/io.py b/isort/io.py new file mode 100644 index 000000000..c04aff5a4 --- /dev/null +++ b/isort/io.py @@ -0,0 +1,56 @@ +"""Defines any IO utilities used by isort""" +import re +from typing import NamedTuple, Optional, Tuple +from pathlib import Path + +_ENCODING_PATTERN = re.compile(br"^[ \t\f]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)") + + +class File(NamedTuple): + contents: str + path: Path + encoding: str + + @staticmethod + def read(filename: str) -> "File": + file_path = Path(filename).resolve() + contents, encoding = _read_file_contents(file_path) + return File(contents=contents, path=file_path, encoding=encoding) + + +def _determine_file_encoding(file_path: Path, default: str = "utf-8") -> str: + # see https://www.python.org/dev/peps/pep-0263/ + coding = default + with file_path.open("rb") as f: + for line_number, line in enumerate(f, 1): + if line_number > 2: + break + groups = re.findall(_ENCODING_PATTERN, line) + if groups: + coding = groups[0].decode("ascii") + break + + return coding + + +def _read_file_contents( + file_path: Path +) -> Tuple[Optional[str], Optional[str]]: + encoding = _determine_file_encoding(file_path) + with file_path.open(encoding=encoding, newline="") as file_to_import_sort: + try: + file_contents = file_to_import_sort.read() + return file_contents, encoding + except UnicodeDecodeError: + pass + + # Try default encoding for open(mode='r') on the system + fallback_encoding = _locale.getpreferredencoding(False) + with file_path.open(encoding=fallback_encoding, newline="") as file_to_import_sort: + try: + file_contents = file_to_import_sort.read() + return file_contents, fallback_encoding + except UnicodeDecodeError: + pass + + raise UnableToDetermineEncoding(file_path, encoding, fallback_encoding) From fb3958df7cbb29885494ea715437a4eafa68cbd9 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 5 Nov 2019 12:08:09 -0800 Subject: [PATCH 0177/1439] Update compat module to use File --- isort/compat.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/isort/compat.py b/isort/compat.py index 973fed71a..8159ca900 100644 --- a/isort/compat.py +++ b/isort/compat.py @@ -9,6 +9,7 @@ from isort import settings, api from isort.format import ask_whether_to_apply_changes_to_file, show_unified_diff from isort.isort import _SortImports +from .io import File from .exceptions import FileSkipped @@ -46,9 +47,10 @@ def __init__( self.file_path = None try: if file_path: - self.output = api.sorted_file(file_path, extension=extension, **setting_overrides) - else: - self.output = api.sorted_imports(file_contents, extension=extension, **setting_overrides) + file_contents, file_path, file_encoding = File.read(file_path) + extension = file_path.suffix + + self.output = api.sorted_imports(file_contents, extension=extension, **setting_overrides) except FileSkipped as error: if self.config["verbose"]: warn(error.message) From 8d62f1fe901784a5d2aa94f9600f63e2e22846fe Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 5 Nov 2019 13:05:20 -0800 Subject: [PATCH 0178/1439] Existing unit tests starting to pass with refactored code --- isort/api.py | 11 +++++------ isort/compat.py | 41 +++++++++++++++++++++++------------------ isort/main.py | 8 ++++---- isort/settings.py | 1 - 4 files changed, 32 insertions(+), 29 deletions(-) diff --git a/isort/api.py b/isort/api.py index 652c503e6..bffb7db41 100644 --- a/isort/api.py +++ b/isort/api.py @@ -7,7 +7,7 @@ from .io import File -def sorted_contents(file_contents: str, extension: str = "py", config: Config=DEFAULT_CONFIG, file_path: Optional[Path]=None, disregard_skip: bool=False, **config_kwargs) -> str: +def sorted_imports(file_contents: str, extension: str = "py", config: Config=DEFAULT_CONFIG, file_path: Optional[Path]=None, disregard_skip: bool=False, **config_kwargs) -> str: content_source = str(file_path or "Passed in content") if not disregard_skip: if FILE_SKIP_COMMENT in file_contents: @@ -22,11 +22,11 @@ def sorted_contents(file_contents: str, extension: str = "py", config: Config=DE elif config_kwargs: config = Config(**config_kwargs) - if settings.atomic: + if config.atomic: compile(file_contents, content_source, "exec", 0, 1) parsed_output = output.sorted_imports(parse.file_contents(file_contents, config=config), config, extension) - if settings.atomic: + if config.atomic: try: compile(file_contents, content_source, "exec", 0, 1) except SyntaxError: @@ -36,8 +36,7 @@ def sorted_contents(file_contents: str, extension: str = "py", config: Config=DE def sorted_file(filename: str, config: Config=DEFAULT_CONFIG, **config_kwargs) -> str: file_data = File.read(filename) - if config_kwargs or config is DEFAULT_CONFIG: - if not "settings_path" in config_kwargs and not "settings_file" in config_kwargs: - config_kwargs["settings_path"] = file_data.path.parent + if config is DEFAULT_CONFIG and not "settings_path" in config_kwargs and not "settings_file" in config_kwargs: + config_kwargs["settings_path"] = file_data.path.parent return sorted_contents(file_contents=file_data.contents, extension=file_data.path.suffix, config=config, file_path=file_data.path, **config_kwargs) diff --git a/isort/compat.py b/isort/compat.py index 8159ca900..009398e04 100644 --- a/isort/compat.py +++ b/isort/compat.py @@ -6,11 +6,12 @@ from typing import Any, Optional, Tuple from warnings import warn -from isort import settings, api -from isort.format import ask_whether_to_apply_changes_to_file, show_unified_diff -from isort.isort import _SortImports +from . import settings, api +from .format import ask_whether_to_apply_changes_to_file, show_unified_diff +from .isort import _SortImports from .io import File from .exceptions import FileSkipped +from .settings import Config @@ -43,56 +44,60 @@ def __init__( **setting_overrides: Any, ): file_encoding = "utf-8" + if file_path: + file_contents, file_path, file_encoding = File.read(file_path) - self.file_path = None - try: - if file_path: - file_contents, file_path, file_encoding = File.read(file_path) - extension = file_path.suffix + if settings_path: + setting_overrides["settings_path"] = settings_path + elif file_path and not "settings_file" in setting_overrides: + setting_overrides["settings_path"] = file_path.parent + + config = Config(**setting_overrides) + try: self.output = api.sorted_imports(file_contents, extension=extension, **setting_overrides) except FileSkipped as error: - if self.config["verbose"]: + if config.verbose: warn(error.message) if check: check_output = self.output check_against = file_contents - if self.config["ignore_whitespace"]: + if config.ignore_whitespace: check_output = self.sorted_imports.remove_whitespaces(check_output) check_against = self.sorted_imports.remove_whitespaces(check_against) current_input_sorted_correctly = self.sorted_imports.check_if_input_already_sorted( - check_output, check_against, logging_file_path=str(self.file_path or "") + check_output, check_against, logging_file_path=str(file_path or "") ) if current_input_sorted_correctly: return else: self.incorrectly_sorted = True - if show_diff or self.config["show_diff"]: + if show_diff: show_unified_diff( - file_input=file_contents, file_output=self.output, file_path=self.file_path + file_input=file_contents, file_output=self.output, file_path=file_path ) elif write_to_stdout: sys.stdout.write(self.output) - elif self.file_path and not check: + elif file_path and not check: # if file_name resolves to True, file_path never None or '' if self.output == file_contents: return if ask_to_apply: show_unified_diff( - file_input=file_contents, file_output=self.output, file_path=self.file_path + file_input=file_contents, file_output=self.output, file_path=file_path ) - apply_changes = ask_whether_to_apply_changes_to_file(str(self.file_path)) + apply_changes = ask_whether_to_apply_changes_to_file(str(file_path)) if not apply_changes: return - with self.file_path.open("w", encoding=file_encoding, newline="") as output_file: - if not self.config["quiet"]: + with file_path.open("w", encoding=file_encoding, newline="") as output_file: + if not config.quiet: print(f"Fixing {self.file_path}") output_file.write(self.output) diff --git a/isort/main.py b/isort/main.py index 2c4c457ba..48a4e6b11 100644 --- a/isort/main.py +++ b/isort/main.py @@ -16,10 +16,10 @@ DEFAULT_SECTIONS, VALID_PY_TARGETS, WrapModes, - default, - file_should_be_skipped, - from_path, - wrap_mode_from_string, + # default, + # file_should_be_skipped, + # from_path, + # wrap_mode_from_string, ) shebang_re = re.compile(br"^#!.*\bpython[23w]?\b") diff --git a/isort/settings.py b/isort/settings.py index 701e6668b..a9c75f518 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -143,7 +143,6 @@ class _Config: force_alphabetical_sort: bool = False force_grid_wrap: int = 0 force_sort_within_sections: bool = False - show_diff: bool = False ignore_whitespace: bool = False no_lines_before: FrozenSet[str] = frozenset() no_inline_sort: bool = False From daf40ea9cb47afba6d8dbe59899bf7bfaa69b845 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 5 Nov 2019 13:21:51 -0800 Subject: [PATCH 0179/1439] Update wrap.py module to use config object, defaulting to DEFAULT_CONFIG. Majority of tests now passsing --- isort/wrap.py | 49 +++++++++++++++++++++++++------------------------ 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/isort/wrap.py b/isort/wrap.py index 7980dfd4b..7d89c07a9 100644 --- a/isort/wrap.py +++ b/isort/wrap.py @@ -4,20 +4,21 @@ from .wrap_modes import WrapModes as Modes from .wrap_modes import formatter_from_string +from .settings import Config, DEFAULT_CONFIG def import_statement( import_start: str, from_imports: List[str], comments: Sequence[str], - config: Dict[str, Any], line_separator: str, + config: Config=DEFAULT_CONFIG, ) -> str: """Returns a multi-line wrapped form of the provided from import statement.""" - formatter = formatter_from_string(config["multi_line_output"].name) + formatter = formatter_from_string(config.multi_line_output.name) dynamic_indent = " " * (len(import_start) + 1) - indent = config["indent"] - line_length = config["wrap_length"] or config["line_length"] + indent = config.indent + line_length = config.wrap_length or config.line_length import_statement = formatter( statement=import_start, imports=copy.copy(from_imports), @@ -26,11 +27,11 @@ def import_statement( line_length=line_length, comments=comments, line_separator=line_separator, - comment_prefix=config["comment_prefix"], - include_trailing_comma=config["include_trailing_comma"], - remove_comments=config["ignore_comments"], + comment_prefix=config.comment_prefix, + include_trailing_comma=config.include_trailing_comma, + remove_comments=config.ignore_comments, ) - if config["balanced_wrapping"]: + if config.balanced_wrapping: lines = import_statement.split(line_separator) line_count = len(lines) if len(lines) > 1: @@ -49,9 +50,9 @@ def import_statement( line_length=line_length, comments=comments, line_separator=line_separator, - comment_prefix=config["comment_prefix"], - include_trailing_comma=config["include_trailing_comma"], - remove_comments=config["ignore_comments"], + comment_prefix=config.comment_prefix, + include_trailing_comma=config.include_trailing_comma, + remove_comments=config.ignore_comments, ) lines = new_import_statement.split(line_separator) if import_statement.count(line_separator) == 0: @@ -59,10 +60,10 @@ def import_statement( return import_statement -def line(line: str, line_separator: str, config: Dict[str, Any]) -> str: +def line(line: str, line_separator: str, config: Config=DEFAULT_CONFIG) -> str: """Returns a line wrapped to the specified line-length, if possible.""" - wrap_mode = config["multi_line_output"] - if len(line) > config["line_length"] and wrap_mode != Modes.NOQA: # type: ignore + wrap_mode = config.multi_line_output + if len(line) > config.line_length and wrap_mode != Modes.NOQA: # type: ignore line_without_comment = line comment = None if "#" in line: @@ -74,11 +75,11 @@ def line(line: str, line_separator: str, config: Dict[str, Any]) -> str: ): line_parts = re.split(exp, line_without_comment) if comment: - _comma_maybe = "," if config["include_trailing_comma"] else "" + _comma_maybe = "," if config.include_trailing_comma else "" line_parts[-1] = f"{line_parts[-1].strip()}{_comma_maybe} #{comment}" next_line = [] while (len(line) + 2) > ( - config["wrap_length"] or config["line_length"] + config.wrap_length or config.line_length ) and line_parts: next_line.append(line_parts.pop()) line = splitter.join(line_parts) @@ -86,13 +87,13 @@ def line(line: str, line_separator: str, config: Dict[str, Any]) -> str: line = next_line.pop() cont_line = _wrap_line( - config["indent"] + splitter.join(next_line).lstrip(), line_separator, config + config.indent + splitter.join(next_line).lstrip(), line_separator, config ) - if config["use_parentheses"]: + if config.use_parentheses: if splitter == "as ": output = f"{line}{splitter}{cont_line.lstrip()}" else: - _comma = "," if config["include_trailing_comma"] and not comment else "" + _comma = "," if config.include_trailing_comma and not comment else "" if wrap_mode in ( Modes.VERTICAL_HANGING_INDENT, # type: ignore Modes.VERTICAL_GRID_GROUPED, # type: ignore @@ -104,14 +105,14 @@ def line(line: str, line_separator: str, config: Dict[str, Any]) -> str: f"{line}{splitter}({line_separator}{cont_line}{_comma}{_separator})" ) lines = output.split(line_separator) - if config["comment_prefix"] in lines[-1] and lines[-1].endswith(")"): - line, comment = lines[-1].split(config["comment_prefix"], 1) - lines[-1] = line + ")" + config["comment_prefix"] + comment[:-1] + if config.comment_prefix in lines[-1] and lines[-1].endswith(")"): + line, comment = lines[-1].split(config.comment_prefix, 1) + lines[-1] = line + ")" + config.comment_prefix + comment[:-1] return line_separator.join(lines) return f"{line}{splitter}\\{line_separator}{cont_line}" - elif len(line) > config["line_length"] and wrap_mode == Modes.NOQA: # type: ignore + elif len(line) > config.line_length and wrap_mode == Modes.NOQA: # type: ignore if "# NOQA" not in line: - return f"{line}{config['comment_prefix']} NOQA" + return f"{line}{config.comment_prefix} NOQA" return line From daa27b973b484a88994c3bdcbb5bc5d29b8ac84d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 5 Nov 2019 22:18:05 -0800 Subject: [PATCH 0180/1439] Test case fixes --- isort/output.py | 31 ++++++++++++++----------------- isort/settings.py | 2 +- isort/wrap.py | 5 +++-- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/isort/output.py b/isort/output.py index 4c3cd132a..9f38218c8 100644 --- a/isort/output.py +++ b/isort/output.py @@ -445,25 +445,22 @@ def _with_from_imports( if do_multiline_reformat: import_statement = wrap.import_statement( - import_start, from_import_section, comments, config, parsed.line_separator + import_start=import_start, from_imports=from_import_section, comments=comments, line_separator=parsed.line_separator, config=config ) if config.multi_line_output == wrap.Modes.GRID: # type: ignore # type: ignore - config.multi_line_output = wrap.Modes.VERTICAL_GRID # type: ignore - try: - other_import_statement = wrap.import_statement( - import_start, - from_import_section, - comments, - config, - parsed.line_separator, - ) - if ( - max(len(x) for x in import_statement.split("\n")) - > config.line_length - ): - import_statement = other_import_statement - finally: - config.multi_line_output = wrap.Modes.GRID # type: ignore + other_import_statement = wrap.import_statement( + import_start=import_start, + from_imports=from_import_section, + comments=comments, + line_separator=parsed.line_separator, + config=config, + multi_line_output=wrap.Modes.VERTICAL_GRID + ) + if ( + max(len(x) for x in import_statement.split("\n")) + > config.line_length + ): + import_statement = other_import_statement if not do_multiline_reformat and len(import_statement) > config.line_length: import_statement = wrap.line(import_statement, parsed.line_separator, config) diff --git a/isort/settings.py b/isort/settings.py index a9c75f518..4985b3323 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -109,7 +109,7 @@ class _Config: known_third_party: FrozenSet[str] = frozenset(("google.appengine.api",)) known_first_party: FrozenSet[str] = frozenset() known_standard_library: FrozenSet[str] = frozenset() - multi_line_output = WrapModes.GRID # type: ignore + multi_line_output: WrapModes = WrapModes.GRID # type: ignore forced_separate: FrozenSet[str] = frozenset() indent: str = " " * 4 comment_prefix: str = " #" diff --git a/isort/wrap.py b/isort/wrap.py index 7d89c07a9..b175beee9 100644 --- a/isort/wrap.py +++ b/isort/wrap.py @@ -1,6 +1,6 @@ import copy import re -from typing import Any, Dict, List, Sequence +from typing import Any, Dict, List, Sequence, Optional from .wrap_modes import WrapModes as Modes from .wrap_modes import formatter_from_string @@ -13,9 +13,10 @@ def import_statement( comments: Sequence[str], line_separator: str, config: Config=DEFAULT_CONFIG, + multi_line_output: Optional[Modes]=None ) -> str: """Returns a multi-line wrapped form of the provided from import statement.""" - formatter = formatter_from_string(config.multi_line_output.name) + formatter = formatter_from_string(multi_line_output or config.multi_line_output.name) dynamic_indent = " " * (len(import_start) + 1) indent = config.indent line_length = config.wrap_length or config.line_length From 5a814c193fdae0b25499d01da729e0542f1175c6 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 5 Nov 2019 22:25:10 -0800 Subject: [PATCH 0181/1439] Fix custom wrap mode per import statement support --- isort/wrap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/wrap.py b/isort/wrap.py index b175beee9..352f791f1 100644 --- a/isort/wrap.py +++ b/isort/wrap.py @@ -16,7 +16,7 @@ def import_statement( multi_line_output: Optional[Modes]=None ) -> str: """Returns a multi-line wrapped form of the provided from import statement.""" - formatter = formatter_from_string(multi_line_output or config.multi_line_output.name) + formatter = formatter_from_string((multi_line_output or config.multi_line_output).name) dynamic_indent = " " * (len(import_start) + 1) indent = config.indent line_length = config.wrap_length or config.line_length From 7fda5fabbddeedb99d4f5e3941170cbfb468e796 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 5 Nov 2019 22:31:39 -0800 Subject: [PATCH 0182/1439] Fix file_contents set to None --- isort/compat.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/compat.py b/isort/compat.py index 009398e04..8cf8a3b0b 100644 --- a/isort/compat.py +++ b/isort/compat.py @@ -32,7 +32,7 @@ class SortImports: def __init__( self, file_path: Optional[str] = None, - file_contents: Optional[str] = None, + file_contents: str = "", write_to_stdout: bool = False, check: bool = False, show_diff: bool = False, @@ -98,7 +98,7 @@ def __init__( with file_path.open("w", encoding=file_encoding, newline="") as output_file: if not config.quiet: - print(f"Fixing {self.file_path}") + print(f"Fixing {file_path}") output_file.write(self.output) From 5e167dd590a2b68d80e0c8e477da118363583121 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 5 Nov 2019 23:42:21 -0800 Subject: [PATCH 0183/1439] Fix wrap mode selection from CLI --- isort/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/isort/main.py b/isort/main.py index 48a4e6b11..427129895 100644 --- a/isort/main.py +++ b/isort/main.py @@ -320,7 +320,8 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: "-m", "--multi-line", dest="multi_line_output", - type=wrap_mode_from_string, + choices=WrapModes.__members__, + type=WrapModes.__getitem__, help="Multi line output (0-grid, 1-vertical, 2-hanging, 3-vert-hanging, 4-vert-grid, " "5-vert-grid-grouped, 6-vert-grid-grouped-no-comma).", ) From 90f204bba1dbf2c29936322b6896a06d2ac1a784 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 5 Nov 2019 23:45:17 -0800 Subject: [PATCH 0184/1439] Remove ambigous flag notices --- isort/main.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/isort/main.py b/isort/main.py index 427129895..7f4dacf30 100644 --- a/isort/main.py +++ b/isort/main.py @@ -375,7 +375,6 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: dest="quiet", help="Shows extra quiet output, only errors are outputted.", ) - parser.add_argument("-r", dest="ambiguous_r_flag", action="store_true") parser.add_argument( "--rm", "--remove-import", @@ -557,14 +556,6 @@ def main(argv: Optional[Sequence[str]] = None) -> None: print(ASCII_ART) return - if arguments.get("ambiguous_r_flag"): - print( - "ERROR: Deprecated -r flag set. " - "This flag has been replaced with -rm to remove ambiguity between it and " - "-rc for recursive" - ) - sys.exit(1) - arguments["check_skip"] = False if "settings_path" in arguments: sp = arguments["settings_path"] From 93d9919ae2621b66907b4d319828658757a111dd Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 6 Nov 2019 00:19:31 -0800 Subject: [PATCH 0185/1439] Fix comand line arguments --- tests/test_isort.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_isort.py b/tests/test_isort.py index 3fa693f3e..1369cad92 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -3140,7 +3140,7 @@ def test_command_line(tmpdir, capfd, multiprocess: bool) -> None: tmpdir.join("file2.py").write( ("import collections\nimport time\n\nimport abc" "\n\n\nimport isort") ) - arguments = ["-rc", str(tmpdir), "--settings-path", os.getcwd()] + arguments = ["--rc", str(tmpdir), "--settings-path", os.getcwd()] if multiprocess: arguments.extend(["--jobs", "2"]) main(arguments) @@ -3168,7 +3168,7 @@ def test_quiet(tmpdir, capfd, quiet: bool) -> None: tmpdir.join("file1.py").write("import re\nimport os") tmpdir.join("file2.py").write("") - arguments = ["-rc", str(tmpdir)] + arguments = ["--rc", str(tmpdir)] if quiet: arguments.append("-q") main(arguments) @@ -4125,7 +4125,7 @@ def test_python_version() -> None: from isort.main import parse_args # test that the py_version can be added as flag - args = parse_args(["-py=27"]) + args = parse_args(["--py=27"]) assert args["py_version"] == "27" args = parse_args(["--python-version=3"]) From 6b904537796c701e31cae29dffd4bedffad1e76c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 6 Nov 2019 00:20:23 -0800 Subject: [PATCH 0186/1439] Fix comand line arguments --- tests/test_isort.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_isort.py b/tests/test_isort.py index 1369cad92..a93cddf1c 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -3125,7 +3125,7 @@ def test_path_finder(monkeypatch) -> None: def test_argument_parsing() -> None: from isort.main import parse_args - args = parse_args(["-dt", "-t", "foo", "--skip=bar", "baz.py"]) + args = parse_args(["--dt", "-dt", "foo", "--skip=bar", "baz.py"]) assert args["order_by_type"] is False assert args["force_to_top"] == ["foo"] assert args["skip"] == ["bar"] @@ -3170,7 +3170,7 @@ def test_quiet(tmpdir, capfd, quiet: bool) -> None: tmpdir.join("file2.py").write("") arguments = ["--rc", str(tmpdir)] if quiet: - arguments.append("-q") + arguments.append("--q") main(arguments) out, err = capfd.readouterr() assert not err From 7bb8ec7a0f80d9111f5d9f8f9f8f9d6f5d0d3ddf Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 6 Nov 2019 00:20:43 -0800 Subject: [PATCH 0187/1439] Fix comand line arguments --- tests/test_isort.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_isort.py b/tests/test_isort.py index a93cddf1c..13624b9a2 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -3125,7 +3125,7 @@ def test_path_finder(monkeypatch) -> None: def test_argument_parsing() -> None: from isort.main import parse_args - args = parse_args(["--dt", "-dt", "foo", "--skip=bar", "baz.py"]) + args = parse_args(["--dt", "-t", "foo", "--skip=bar", "baz.py"]) assert args["order_by_type"] is False assert args["force_to_top"] == ["foo"] assert args["skip"] == ["bar"] From ea5eccacf2f2ca920f19fe0f7f2b93155a7e783c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 6 Nov 2019 00:22:54 -0800 Subject: [PATCH 0188/1439] Fix comand line arguments --- tests/test_isort.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_isort.py b/tests/test_isort.py index 13624b9a2..d1052862c 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -3170,7 +3170,7 @@ def test_quiet(tmpdir, capfd, quiet: bool) -> None: tmpdir.join("file2.py").write("") arguments = ["--rc", str(tmpdir)] if quiet: - arguments.append("--q") + arguments.append("-q") main(arguments) out, err = capfd.readouterr() assert not err From 2c0a0fd72e16351f062fea3de8ff112a7724ad62 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 6 Nov 2019 01:08:31 -0800 Subject: [PATCH 0189/1439] Remove no longer working kate plugin --- kate_plugin/isort_plugin.py | 88 ------------------------------ kate_plugin/isort_plugin_old.py | 85 ----------------------------- kate_plugin/isort_plugin_ui.rc | 16 ------ kate_plugin/katepart_isort.desktop | 7 --- 4 files changed, 196 deletions(-) delete mode 100644 kate_plugin/isort_plugin.py delete mode 100644 kate_plugin/isort_plugin_old.py delete mode 100644 kate_plugin/isort_plugin_ui.rc delete mode 100644 kate_plugin/katepart_isort.desktop diff --git a/kate_plugin/isort_plugin.py b/kate_plugin/isort_plugin.py deleted file mode 100644 index b6525cdfa..000000000 --- a/kate_plugin/isort_plugin.py +++ /dev/null @@ -1,88 +0,0 @@ -""" Sorts Python import definitions, and groups them based on type (stdlib, third-party, local). - -isort/isort_kate_plugin.py - -Provides a simple kate plugin that enables the use of isort to sort Python imports -in the currently open kate file. - -Copyright (C) 2013 Timothy Edmund Crosley - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF -CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - -""" -import os - -import kate - -from isort import SortImports - -try: - from PySide import QtGui -except ImportError: - from PyQt4 import QtGui - - -def sort_kate_imports(add_imports=(), remove_imports=()): - """Sorts imports within Kate while maintaining cursor position and selection, even if length of file changes.""" - document = kate.activeDocument() - view = document.activeView() - position = view.cursorPosition() - selection = view.selectionRange() - sorter = SortImports( - file_contents=document.text(), - add_imports=add_imports, - remove_imports=remove_imports, - settings_path=os.path.dirname(os.path.abspath(str(document.url().path()))), - ) - document.setText(sorter.output) - position.setLine(position.line() + sorter.length_change) - if selection: - start = selection.start() - start.setLine(start.line() + sorter.length_change) - end = selection.end() - end.setLine(end.line() + sorter.length_change) - selection.setRange(start, end) - view.setSelection(selection) - view.setCursorPosition(position) - - -@kate.action -def sort_imports(): - """Sort Imports""" - sort_kate_imports() - - -@kate.action -def add_imports(): - """Add Imports""" - text, ok = QtGui.QInputDialog.getText( - None, - "Add Import", - "Enter an import line to add (example: from os import path or os.path):", - ) - if ok: - sort_kate_imports(add_imports=text.split(";")) - - -@kate.action -def remove_imports(): - """Remove Imports""" - text, ok = QtGui.QInputDialog.getText( - None, - "Remove Import", - "Enter an import line to remove (example: os.path or from os import path):", - ) - if ok: - sort_kate_imports(remove_imports=text.split(";")) diff --git a/kate_plugin/isort_plugin_old.py b/kate_plugin/isort_plugin_old.py deleted file mode 100644 index 3a45261f7..000000000 --- a/kate_plugin/isort_plugin_old.py +++ /dev/null @@ -1,85 +0,0 @@ -""" Sorts Python import definitions, and groups them based on type (stdlib, third-party, local). - -isort/isort_kate_plugin.py - -Provides a simple kate plugin that enables the use of isort to sort Python imports -in the currently open kate file. - -Copyright (C) 2013 Timothy Edmund Crosley - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF -CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - -""" -import os - -import kate - -from isort import SortImports - -try: - from PySide import QtGui -except ImportError: - from PyQt4 import QtGui - - -def sort_kate_imports(add_imports=(), remove_imports=()): - """Sorts imports within Kate while maintaining cursor position and selection, even if length of file changes.""" - document = kate.activeDocument() - view = document.activeView() - position = view.cursorPosition() - selection = view.selectionRange() - sorter = SortImports( - file_contents=document.text(), - add_imports=add_imports, - remove_imports=remove_imports, - settings_path=os.path.dirname(os.path.abspath(str(document.url().path()))), - ) - document.setText(sorter.output) - position.setLine(position.line() + sorter.length_change) - if selection: - start = selection.start() - start.setLine(start.line() + sorter.length_change) - end = selection.end() - end.setLine(end.line() + sorter.length_change) - selection.setRange(start, end) - view.setSelection(selection) - view.setCursorPosition(position) - - -@kate.action(text="Sort Imports", shortcut="Ctrl+[", menu="Python") -def sort_imports(): - sort_kate_imports() - - -@kate.action(text="Add Import", shortcut="Ctrl+]", menu="Python") -def add_imports(): - text, ok = QtGui.QInputDialog.getText( - None, - "Add Import", - "Enter an import line to add (example: from os import path or os.path):", - ) - if ok: - sort_kate_imports(add_imports=text.split(";")) - - -@kate.action(text="Remove Import", shortcut="Ctrl+Shift+]", menu="Python") -def remove_imports(): - text, ok = QtGui.QInputDialog.getText( - None, - "Remove Import", - "Enter an import line to remove (example: os.path or from os import path):", - ) - if ok: - sort_kate_imports(remove_imports=text.split(";")) diff --git a/kate_plugin/isort_plugin_ui.rc b/kate_plugin/isort_plugin_ui.rc deleted file mode 100644 index 19be98c5d..000000000 --- a/kate_plugin/isort_plugin_ui.rc +++ /dev/null @@ -1,16 +0,0 @@ - - - - - &isort - - - - - - diff --git a/kate_plugin/katepart_isort.desktop b/kate_plugin/katepart_isort.desktop deleted file mode 100644 index df3ff63c2..000000000 --- a/kate_plugin/katepart_isort.desktop +++ /dev/null @@ -1,7 +0,0 @@ -[Desktop Entry] -Type=Service -ServiceTypes=Kate/PythonPlugin -X-KDE-Library=isort_plugin -Name=isort -Comment=Python import sorter and manager -X-Python-2-Compatible=true From b9d3ebb98fc58180aa4cf9d89d0df06ae5cbf31d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 6 Nov 2019 01:23:55 -0800 Subject: [PATCH 0190/1439] Initially working CLI --- isort/api.py | 9 +++++---- isort/compat.py | 9 +++++---- isort/exceptions.py | 1 + isort/finders.py | 17 +++-------------- isort/io.py | 2 +- isort/main.py | 38 +++++++++++++++++++++----------------- isort/output.py | 2 +- isort/parse.py | 2 +- isort/settings.py | 8 ++++---- isort/sorting.py | 1 + isort/wrap.py | 4 ++-- 11 files changed, 45 insertions(+), 48 deletions(-) diff --git a/isort/api.py b/isort/api.py index bffb7db41..d0e617403 100644 --- a/isort/api.py +++ b/isort/api.py @@ -1,10 +1,11 @@ -from .settings import Config, DEFAULT_CONFIG, FILE_SKIP_COMMENT -from . import parse, output -from .exceptions import UnableToDetermineEncoding, FileSkipComment, IntroducedSyntaxErrors import re from pathlib import Path -from typing import Any, Optional, Tuple, NamedTuple +from typing import Any, NamedTuple, Optional, Tuple + +from . import output, parse +from .exceptions import FileSkipComment, IntroducedSyntaxErrors, UnableToDetermineEncoding from .io import File +from .settings import DEFAULT_CONFIG, FILE_SKIP_COMMENT, Config def sorted_imports(file_contents: str, extension: str = "py", config: Config=DEFAULT_CONFIG, file_path: Optional[Path]=None, disregard_skip: bool=False, **config_kwargs) -> str: diff --git a/isort/compat.py b/isort/compat.py index 8cf8a3b0b..65f87ec53 100644 --- a/isort/compat.py +++ b/isort/compat.py @@ -6,15 +6,14 @@ from typing import Any, Optional, Tuple from warnings import warn -from . import settings, api +from . import api, settings +from .exceptions import FileSkipped from .format import ask_whether_to_apply_changes_to_file, show_unified_diff -from .isort import _SortImports from .io import File -from .exceptions import FileSkipped +from .isort import _SortImports from .settings import Config - def get_settings_path(settings_path: Optional[Path], current_file_path: Optional[Path]) -> Path: if settings_path: return settings_path @@ -60,6 +59,8 @@ def __init__( if config.verbose: warn(error.message) + return + if check: check_output = self.output check_against = file_contents diff --git a/isort/exceptions.py b/isort/exceptions.py index df9623eb6..1037b55ba 100644 --- a/isort/exceptions.py +++ b/isort/exceptions.py @@ -1,5 +1,6 @@ """All isort specific exception classes should be defined here""" from pathlib import Path + from .settings import FILE_SKIP_COMMENT diff --git a/isort/finders.py b/isort/finders.py index afed68137..15f8879d9 100644 --- a/isort/finders.py +++ b/isort/finders.py @@ -10,22 +10,11 @@ from fnmatch import fnmatch from functools import lru_cache from glob import glob -from typing import ( - Any, - Dict, - Iterable, - Iterator, - List, - Mapping, - Optional, - Pattern, - Sequence, - Tuple, - Type, -) +from typing import (Any, Dict, Iterable, Iterator, List, Mapping, + Optional, Pattern, Sequence, Tuple, Type) +from .settings import DEFAULT_CONFIG, Config from .utils import chdir, exists_case_sensitive -from .settings import Config, DEFAULT_CONFIG try: from pipreqs import pipreqs diff --git a/isort/io.py b/isort/io.py index c04aff5a4..23fc18b3d 100644 --- a/isort/io.py +++ b/isort/io.py @@ -1,7 +1,7 @@ """Defines any IO utilities used by isort""" import re -from typing import NamedTuple, Optional, Tuple from pathlib import Path +from typing import NamedTuple, Optional, Tuple _ENCODING_PATTERN = re.compile(br"^[ \t\f]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)") diff --git a/isort/main.py b/isort/main.py index 7f4dacf30..e04a3542a 100644 --- a/isort/main.py +++ b/isort/main.py @@ -5,6 +5,7 @@ import os import re import sys +from pathlib import Path from typing import Any, Dict, Iterable, Iterator, List, MutableMapping, Optional, Sequence from warnings import warn @@ -16,6 +17,7 @@ DEFAULT_SECTIONS, VALID_PY_TARGETS, WrapModes, + Config # default, # file_should_be_skipped, # from_path, @@ -61,24 +63,21 @@ def sort_imports(file_name: str, **arguments: Any) -> Optional[SortAttempt]: def iter_source_code( - paths: Iterable[str], config: MutableMapping[str, Any], skipped: List[str] + paths: Iterable[str], config: Config, skipped: List[str] ) -> Iterator[str]: """Iterate over all Python source files defined in paths.""" - if "not_skip" in config: - config["skip"] = list(set(config["skip"]).difference(config["not_skip"])) - for path in paths: if os.path.isdir(path): for dirpath, dirnames, filenames in os.walk(path, topdown=True, followlinks=True): + base_path = Path(dirpath) for dirname in list(dirnames): - if file_should_be_skipped(dirname, config, dirpath): + if config.is_skipped(base_path / dirname): skipped.append(dirname) dirnames.remove(dirname) for filename in filenames: filepath = os.path.join(dirpath, filename) if is_python_file(filepath): - relative_file = os.path.relpath(filepath, path) - if file_should_be_skipped(relative_file, config, path): + if config.is_skipped(Path(filepath)): skipped.append(filename) else: yield filepath @@ -556,7 +555,6 @@ def main(argv: Optional[Sequence[str]] = None) -> None: print(ASCII_ART) return - arguments["check_skip"] = False if "settings_path" in arguments: sp = arguments["settings_path"] arguments["settings_path"] = ( @@ -581,14 +579,20 @@ def main(argv: Optional[Sequence[str]] = None) -> None: if not arguments.get("apply", False): arguments["ask_to_apply"] = True - config = from_path( - arguments.get("settings_path", "") or os.path.abspath(file_names[0]) or os.getcwd() - ).copy() - config.update(arguments) + if not "settings_path" in arguments: + arguments["settings_path"] = os.path.abspath(file_names[0]) or os.getcwd() + + config_dict = arguments.copy() + config_dict.pop("recursive", False) + config_dict.pop("ask_to_apply", False) + show_logo = config_dict.pop("show_logo", False) + filter_files = config_dict.pop("filter_files", False) + config = Config(**config_dict) + wrong_sorted_files = False skipped: List[str] = [] - if config.get("filter_files"): + if filter_files: filtered_files = [] for file_name in file_names: if file_should_be_skipped(file_name, config): @@ -600,7 +604,7 @@ def main(argv: Optional[Sequence[str]] = None) -> None: if arguments.get("recursive", False): file_names = iter_source_code(file_names, config, skipped) num_skipped = 0 - if config["verbose"] or config.get("show_logo", False): + if config.verbose or show_logo: print(ASCII_ART) jobs = arguments.get("jobs") @@ -609,12 +613,12 @@ def main(argv: Optional[Sequence[str]] = None) -> None: executor = multiprocessing.Pool(jobs) attempt_iterator = executor.imap( - functools.partial(sort_imports, **arguments), file_names + functools.partial(sort_imports, **config_dict), file_names ) else: # https://github.com/python/typeshed/pull/2814 attempt_iterator = ( # type: ignore - sort_imports(file_name, **arguments) for file_name in file_names + sort_imports(file_name, **config_dict) for file_name in file_names ) for sort_attempt in attempt_iterator: @@ -631,7 +635,7 @@ def main(argv: Optional[Sequence[str]] = None) -> None: num_skipped += len(skipped) if num_skipped and not arguments.get("quiet", False): - if config["verbose"]: + if config.verbose: for was_skipped in skipped: warn( f"{was_skipped} was skipped as it's listed in 'skip' setting" diff --git a/isort/output.py b/isort/output.py index 9f38218c8..25ef1049a 100644 --- a/isort/output.py +++ b/isort/output.py @@ -7,7 +7,7 @@ from . import parse, sorting, wrap from .comments import add_to_line as with_comments -from .settings import Config, DEFAULT_CONFIG +from .settings import DEFAULT_CONFIG, Config def sorted_imports( diff --git a/isort/parse.py b/isort/parse.py index 6882e7ecc..7ed8bba62 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -5,7 +5,7 @@ from warnings import warn from isort.format import format_natural -from isort.settings import Config, DEFAULT_CONFIG +from isort.settings import DEFAULT_CONFIG, Config from .comments import parse as parse_comments from .finders import FindersManager diff --git a/isort/settings.py b/isort/settings.py index 4985b3323..4ba08bbde 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -245,7 +245,7 @@ def is_skipped(self, file_path: Path) -> bool: file_name = os.path.relpath(file_path, self.directory) path = self.directory else: - file_name = str(absolute_file_path) + file_name = str(file_path) path = "" os_path = str(file_path) @@ -255,7 +255,7 @@ def is_skipped(self, file_path: Path) -> bool: normalized_path = normalized_path[2:] if path and self.safety_excludes: - check_exclude = "/" + filename.replace("\\", "/") + "/" + check_exclude = "/" + file_name.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): @@ -267,14 +267,14 @@ def is_skipped(self, file_path: Path) -> bool: ): return True - position = os.path.split(filename) + position = os.path.split(file_name) while position[1]: if position[1] in self.skip: return True position = os.path.split(position[0]) for glob in self.skip_glob: - if fnmatch.fnmatch(filename, glob) or fnmatch.fnmatch("/" + filename, glob): + if fnmatch.fnmatch(file_name, glob) or fnmatch.fnmatch("/" + file_name, glob): return True if not (os.path.isfile(os_path) or os.path.isdir(os_path) or os.path.islink(os_path)): diff --git a/isort/sorting.py b/isort/sorting.py index 0c314e224..29b0e5c32 100644 --- a/isort/sorting.py +++ b/isort/sorting.py @@ -1,5 +1,6 @@ import re from typing import Any, Callable, Iterable, List, Mapping, Optional + from .settings import Config _import_line_intro_re = re.compile("^(?:from|import) ") diff --git a/isort/wrap.py b/isort/wrap.py index 352f791f1..fa7349ba5 100644 --- a/isort/wrap.py +++ b/isort/wrap.py @@ -1,10 +1,10 @@ import copy import re -from typing import Any, Dict, List, Sequence, Optional +from typing import Any, Dict, List, Optional, Sequence +from .settings import DEFAULT_CONFIG, Config from .wrap_modes import WrapModes as Modes from .wrap_modes import formatter_from_string -from .settings import Config, DEFAULT_CONFIG def import_statement( From 3a62d15e97d43dec923f400d1a72d80978b3a557 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 6 Nov 2019 01:38:20 -0800 Subject: [PATCH 0191/1439] Fix jobs usage in CLI --- isort/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/main.py b/isort/main.py index e04a3542a..d6a2446b2 100644 --- a/isort/main.py +++ b/isort/main.py @@ -585,6 +585,7 @@ def main(argv: Optional[Sequence[str]] = None) -> None: config_dict = arguments.copy() config_dict.pop("recursive", False) config_dict.pop("ask_to_apply", False) + jobs = config_dict.pop("jobs", ()) show_logo = config_dict.pop("show_logo", False) filter_files = config_dict.pop("filter_files", False) config = Config(**config_dict) @@ -607,7 +608,6 @@ def main(argv: Optional[Sequence[str]] = None) -> None: if config.verbose or show_logo: print(ASCII_ART) - jobs = arguments.get("jobs") if jobs: import multiprocessing From fcb487173eaf1b15b7a5b1b58fa282d685c7b188 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 6 Nov 2019 01:58:56 -0800 Subject: [PATCH 0192/1439] Fix length sort --- CHANGELOG.md | 1 + isort/settings.py | 1 + isort/sorting.py | 8 +------- tests/test_isort.py | 2 +- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f5f19cd1..3b4fdf5cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Changelog - Config files are no longer composed on-top of each-other. Instead the first config file found is used. - Since there is no longer composition negative form settings (such as --dont-skip) are no longer required and have been removed. - Two-letter shortened setting names (like `ac` for `atomic`) now require two dashes to avoid ambiguity: `--ac`. + - `length_sort_{section_name}` config usage has been deprecated. Instead `length_sort_sections` list can be used to specify a list of sections that need to be length sorted. Internal: - isort now utilizes mypy and typing to filter out typing related issues before deployment. diff --git a/isort/settings.py b/isort/settings.py index 4ba08bbde..1c402d858 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -114,6 +114,7 @@ class _Config: indent: str = " " * 4 comment_prefix: str = " #" length_sort: bool = False + length_sort_sections: FrozenSet[str] = frozenset() add_imports: FrozenSet[str] = frozenset() remove_imports: FrozenSet[str] = frozenset() reverse_relative: bool = False diff --git a/isort/sorting.py b/isort/sorting.py index 29b0e5c32..b45bbf517 100644 --- a/isort/sorting.py +++ b/isort/sorting.py @@ -35,13 +35,7 @@ def module_key( if not config.case_sensitive: module_name = module_name.lower() - if section_name: - length_sort = getattr( - config, "length_sort_" + str(section_name).lower(), config.length_sort - ) - else: - length_sort = config.length_sort - + length_sort = config.length_sort or str(section_name).lower() in config.length_sort_sections _length_sort_maybe = length_sort and (str(len(module_name)) + ":" + module_name) or module_name return f"{module_name in config.force_to_top and 'A' or 'B'}{prefix}{_length_sort_maybe}" diff --git a/tests/test_isort.py b/tests/test_isort.py index d1052862c..0e51e1496 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -486,7 +486,7 @@ def test_length_sort_section() -> None: "import looooooooooooooooooooooooooooooooooooooong\n" "import medium_sizeeeeeeeeeeeeea\n" ) - test_output = SortImports(file_contents=test_input, length_sort_stdlib=True).output + test_output = SortImports(file_contents=test_input, length_sort_sections=("stdlib", )).output assert test_output == ( "import os\n" "import sys\n" From d7a16e59abb91dd11b790491f6779933ba920b02 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 6 Nov 2019 02:39:07 -0800 Subject: [PATCH 0193/1439] Fix known_x support post refactor --- isort/finders.py | 6 +++--- isort/settings.py | 15 +++++++++++++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/isort/finders.py b/isort/finders.py index 15f8879d9..5a3d87595 100644 --- a/isort/finders.py +++ b/isort/finders.py @@ -75,9 +75,9 @@ def __init__(self, config: Config, sections: Any) -> None: self.known_patterns: List[Tuple[Pattern[str], str]] = [] for placement in reversed(self.sections): - known_placement = KNOWN_SECTION_MAPPING.get(placement, placement) - config_key = f"known_{known_placement.lower()}" - known_patterns = list(getattr(self.config, config_key, [])) + known_placement = KNOWN_SECTION_MAPPING.get(placement, placement).lower() + config_key = f"known_{known_placement}" + known_patterns = list(getattr(self.config, config_key, self.config.known_other.get(known_placement, []))) known_patterns = [ pattern for known_pattern in known_patterns diff --git a/isort/settings.py b/isort/settings.py index 1c402d858..e9883bb96 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -109,6 +109,7 @@ class _Config: known_third_party: FrozenSet[str] = frozenset(("google.appengine.api",)) known_first_party: FrozenSet[str] = frozenset() known_standard_library: FrozenSet[str] = frozenset() + known_other: Dict[str, FrozenSet[str]] = field(default_factory=dict) multi_line_output: WrapModes = WrapModes.GRID # type: ignore forced_separate: FrozenSet[str] = frozenset() indent: str = " " * 4 @@ -226,8 +227,14 @@ def __init__(self, settings_file: str = "", settings_path: str = "", **config_ov indent = "\t" combined_config["indent"] = indent - # coerce all provided config values into their correct type + + known_other = {} for key, value in combined_config.items(): + # Collect all known sections beyond those that have direct entries + if key.startswith("known_") and key not in ("known_standard_library", "known_future_library", "known_third_party", "known_first_party"): + known_other[key[len("known_"):]] = frozenset(value) + + # Coerce all provided config values into their correct type default_value = _DEFAULT_SETTINGS.get(key, None) if default_value is None: continue @@ -237,8 +244,12 @@ def __init__(self, settings_file: str = "", settings_path: str = "", **config_ov if "directory" not in combined_config: combined_config["directory"] = os.path.basename(config_settings.get("source", None) or os.getcwd()) + # Remove any config values that are used for creating config object but aren't defined in dataclass combined_config.pop("source", None) - super().__init__(**combined_config) + for known_key in known_other.keys(): + combined_config.pop(f"known_{known_key}", None) + + super().__init__(known_other=known_other, **combined_config) def is_skipped(self, file_path: Path) -> bool: """Returns True if the file and/or folder should be skipped based on current settings.""" From dedba281593679cc02b30887fd172863875a8d72 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 6 Nov 2019 02:46:06 -0800 Subject: [PATCH 0194/1439] Make don't order by type flag consistent with old setting --- isort/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/main.py b/isort/main.py index d6a2446b2..e886384ad 100644 --- a/isort/main.py +++ b/isort/main.py @@ -354,7 +354,7 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: help="Order imports by type in addition to alphabetically", ) parser.add_argument( - "--dot", + "--dt", "--dont-order-by-type", dest="dont_order_by_type", action="store_true", From a320dee546c0be96029d1385c3d1edf95acc084f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 6 Nov 2019 03:36:22 -0800 Subject: [PATCH 0195/1439] Fix forced separate support post refactor --- isort/compat.py | 1 - isort/settings.py | 10 +++++----- tests/test_isort.py | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/isort/compat.py b/isort/compat.py index 65f87ec53..afb3a1c01 100644 --- a/isort/compat.py +++ b/isort/compat.py @@ -58,7 +58,6 @@ def __init__( except FileSkipped as error: if config.verbose: warn(error.message) - return if check: diff --git a/isort/settings.py b/isort/settings.py index e9883bb96..56b92f366 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -111,7 +111,7 @@ class _Config: known_standard_library: FrozenSet[str] = frozenset() known_other: Dict[str, FrozenSet[str]] = field(default_factory=dict) multi_line_output: WrapModes = WrapModes.GRID # type: ignore - forced_separate: FrozenSet[str] = frozenset() + forced_separate: Tuple[str, ...] = () indent: str = " " * 4 comment_prefix: str = " #" length_sort: bool = False @@ -151,7 +151,7 @@ class _Config: ignore_comments: bool = False safety_excludes: bool = True case_sensitive: bool = False - sources: FrozenSet[str] = frozenset() + sources: Tuple[Dict[str, Any], ...] = () virtual_env: str = "" conda_env: str = "" ensure_newline_before_comments: bool = False @@ -201,7 +201,7 @@ def __init__(self, settings_file: str = "", settings_path: str = "", **config_ov config_settings: Dict[str, Any] if settings_file: - config_settings = config_data = get_config_data( + config_settings = config_data = _get_config_data( settings_file, CONFIG_SECTIONS.get(os.path.basename(settings_file), FALLBACK_CONFIG_SECTIONS), ) @@ -249,7 +249,7 @@ def __init__(self, settings_file: str = "", settings_path: str = "", **config_ov for known_key in known_other.keys(): combined_config.pop(f"known_{known_key}", None) - super().__init__(known_other=known_other, **combined_config) + super().__init__(known_other=known_other, sources=tuple(sources), **combined_config) def is_skipped(self, file_path: Path) -> bool: """Returns True if the file and/or folder should be skipped based on current settings.""" @@ -418,7 +418,7 @@ def _get_config_data(file_path: str, sections: Iterable[str]) -> Dict[str, Any]: if existing_value_type == tuple: settings[key] = tuple(_as_list(value)) elif existing_value_type == frozenset: - settings[key] = frozenset(settings.get(key)) + settings[key] = frozenset(_as_list(settings.get(key))) elif existing_value_type == bool: # Only some configuration formats support native boolean values. if not isinstance(value, bool): diff --git a/tests/test_isort.py b/tests/test_isort.py index 0e51e1496..1a0f8aa11 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -3045,7 +3045,7 @@ def test_forced_separate_is_deterministic_issue_774(tmpdir) -> None: ) assert ( - SortImports(file_contents=test_input, settings_path=config_file.strpath).output + SortImports(file_contents=test_input, settings_file=config_file.strpath).output == test_input ) From 628a17c223b0db7b445ecd18b52db94c3cdb1470 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 6 Nov 2019 03:59:19 -0800 Subject: [PATCH 0196/1439] Add extension support to io.File --- isort/api.py | 2 +- isort/compat.py | 5 ++++- isort/io.py | 4 ++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/isort/api.py b/isort/api.py index d0e617403..757c1b291 100644 --- a/isort/api.py +++ b/isort/api.py @@ -40,4 +40,4 @@ def sorted_file(filename: str, config: Config=DEFAULT_CONFIG, **config_kwargs) - if config is DEFAULT_CONFIG and not "settings_path" in config_kwargs and not "settings_file" in config_kwargs: config_kwargs["settings_path"] = file_data.path.parent - return sorted_contents(file_contents=file_data.contents, extension=file_data.path.suffix, config=config, file_path=file_data.path, **config_kwargs) + return sorted_contents(file_contents=file_data.contents, extension=file_data.extension, config=config, file_path=file_data.path, **config_kwargs) diff --git a/isort/compat.py b/isort/compat.py index afb3a1c01..7e6a5b7f6 100644 --- a/isort/compat.py +++ b/isort/compat.py @@ -44,7 +44,10 @@ def __init__( ): file_encoding = "utf-8" if file_path: - file_contents, file_path, file_encoding = File.read(file_path) + file_data = File.read(file_path) + file_contents, file_path, file_encoding = file_data + if not extension: + extension = file_data.extension if settings_path: setting_overrides["settings_path"] = settings_path diff --git a/isort/io.py b/isort/io.py index 23fc18b3d..15bcda6d6 100644 --- a/isort/io.py +++ b/isort/io.py @@ -17,6 +17,10 @@ def read(filename: str) -> "File": contents, encoding = _read_file_contents(file_path) return File(contents=contents, path=file_path, encoding=encoding) + @property + def extension(self): + return self.path.suffix.lstrip(".") + def _determine_file_encoding(file_path: Path, default: str = "utf-8") -> str: # see https://www.python.org/dev/peps/pep-0263/ From 7cd3be62fe009bb9b9d4167e91b3404080f2e095 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 6 Nov 2019 22:05:35 -0800 Subject: [PATCH 0197/1439] Comment section heading support post refactor --- isort/output.py | 2 +- isort/parse.py | 6 +----- isort/settings.py | 28 ++++++++++++++++++---------- tests/test_isort.py | 1 - 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/isort/output.py b/isort/output.py index 25ef1049a..9161cfdf4 100644 --- a/isort/output.py +++ b/isort/output.py @@ -118,7 +118,7 @@ def sorted_imports( parsed.place_imports[section_name] = section_output continue - section_title = getattr(config, "import_heading_" + str(section_name).lower(), "") + section_title = config.import_headings.get(section_name.lower(), "") if section_title: section_comment = f"# {section_title}" if ( diff --git a/isort/parse.py b/isort/parse.py index 7ed8bba62..039d6fe21 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -148,11 +148,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte original_line_count = len(in_lines) sections: Any = namedtuple("Sections", config.sections)(*config.sections) - section_comments = [ - "# " + value - for key, value in vars(config).items() - if key.startswith("import_heading") and value - ] + section_comments = [f"# {heading}" for heading in config.import_headings.values()] finder = FindersManager(config=config, sections=sections) if original_line_count > 1 or in_lines[:1] not in ([], [""]) or config.force_adds: diff --git a/isort/settings.py b/isort/settings.py index 56b92f366..a97bfcfbf 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -87,6 +87,9 @@ else: FALLBACK_CONFIGS = ("~/.isort.cfg", "~/.editorconfig") +IMPORT_HEADING_PREFIX = "import_heading_" +KNOWN_PREFIX = "known_" + @dataclass(frozen=True) class _Config: @@ -121,11 +124,7 @@ class _Config: reverse_relative: bool = False force_single_line: bool = False default_section: str = "FIRSTPARTY" - import_heading_future: str = "" - import_heading_stdlib: str = "" - import_heading_thirdparty: str = "" - import_heading_firstparty: str = "" - import_heading_localfolder: str = "" + import_headings: Dict[str, str] = field(default_factory=dict) balanced_wrapping: bool = False use_parentheses: bool = False order_by_type: bool = True @@ -229,10 +228,13 @@ def __init__(self, settings_file: str = "", settings_path: str = "", **config_ov known_other = {} + import_headings = {} for key, value in combined_config.items(): # Collect all known sections beyond those that have direct entries - if key.startswith("known_") and key not in ("known_standard_library", "known_future_library", "known_third_party", "known_first_party"): - known_other[key[len("known_"):]] = frozenset(value) + if key.startswith(KNOWN_PREFIX) and key not in ("known_standard_library", "known_future_library", "known_third_party", "known_first_party"): + known_other[key[len(KNOWN_PREFIX):].lower()] = frozenset(value) + if key.startswith(IMPORT_HEADING_PREFIX): + import_headings[key[len(IMPORT_HEADING_PREFIX):].lower()] = str(value) # Coerce all provided config values into their correct type default_value = _DEFAULT_SETTINGS.get(key, None) @@ -246,8 +248,14 @@ def __init__(self, settings_file: str = "", settings_path: str = "", **config_ov # Remove any config values that are used for creating config object but aren't defined in dataclass combined_config.pop("source", None) - for known_key in known_other.keys(): - combined_config.pop(f"known_{known_key}", None) + if known_other: + for known_key in known_other.keys(): + combined_config.pop(f"{KNOWN_PREFIX}{known_key}", None) + combined_config["known_other"] = known_other + if import_headings: + for import_heading_key in import_headings.keys(): + combined_config.pop(f"{IMPORT_HEADING_PREFIX}{import_heading_key}") + combined_config["import_headings"] = import_headings super().__init__(known_other=known_other, sources=tuple(sources), **combined_config) @@ -424,7 +432,7 @@ def _get_config_data(file_path: str, sections: Iterable[str]) -> Dict[str, Any]: if not isinstance(value, bool): value = bool(_as_bool(value)) settings[key] = value - elif key.startswith("known_"): + elif key.startswith(KNOWN_PREFIX): settings[key] = _abspaths(os.path.dirname(file_path), _as_list(value)) elif key == "force_grid_wrap": try: diff --git a/tests/test_isort.py b/tests/test_isort.py index 1a0f8aa11..a400a1c31 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -3930,7 +3930,6 @@ def test_failing_file_check_916() -> None: "from __future__ import unicode_literals\n" ) settings = { - "known_future_library": "future", "import_heading_future": "FUTURE", "sections": ["FUTURE", "STDLIB", "NORDIGEN", "FIRSTPARTY", "THIRDPARTY", "LOCALFOLDER"], "indent": " ", From 612995963781fb1d71eaee680e697cae479c130f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 6 Nov 2019 23:10:53 -0800 Subject: [PATCH 0198/1439] Initial support for 'check' post refactor --- isort/api.py | 50 ++++++++++++++++++++++++++++++++++++++--------- isort/compat.py | 7 ++++++- isort/format.py | 7 +++++++ isort/settings.py | 2 +- 4 files changed, 55 insertions(+), 11 deletions(-) diff --git a/isort/api.py b/isort/api.py index 757c1b291..541aac1a7 100644 --- a/isort/api.py +++ b/isort/api.py @@ -5,10 +5,26 @@ from . import output, parse from .exceptions import FileSkipComment, IntroducedSyntaxErrors, UnableToDetermineEncoding from .io import File +from .format import remove_whitespace from .settings import DEFAULT_CONFIG, FILE_SKIP_COMMENT, Config +def _config(path: Optional[Path]=None, config: Config=DEFAULT_CONFIG, **config_kwargs) -> Config: + if path: + if config is DEFAULT_CONFIG and not "settings_path" in config_kwargs and not "settings_file" in config_kwargs: + config_kwargs["settings_path"] = path + + if config_kwargs and config is not DEFAULT_CONFIG: + raise ValueError("You can either specify custom configuration options using kwargs or " + "passing in a Config object. Not Both!") + elif config_kwargs: + config = Config(**config_kwargs) + + return config + + def sorted_imports(file_contents: str, extension: str = "py", config: Config=DEFAULT_CONFIG, file_path: Optional[Path]=None, disregard_skip: bool=False, **config_kwargs) -> str: + config = _config(config=config, **config_kwargs) content_source = str(file_path or "Passed in content") if not disregard_skip: if FILE_SKIP_COMMENT in file_contents: @@ -17,12 +33,6 @@ def sorted_imports(file_contents: str, extension: str = "py", config: Config=DEF elif file_path and config.is_skipped(file_path): raise FileSkipSetting(file_path) - if config_kwargs and config is not DEFAULT_CONFIG: - raise ValueError("You can either specify custom configuration options using kwargs or " - "passing in a Config object. Not Both!") - elif config_kwargs: - config = Config(**config_kwargs) - if config.atomic: compile(file_contents, content_source, "exec", 0, 1) @@ -35,9 +45,31 @@ def sorted_imports(file_contents: str, extension: str = "py", config: Config=DEF return parsed_output +def check_imports(file_contents: str, show_diff: bool=False, extension: str = "py", config: Config=DEFAULT_CONFIG, file_path: Optional[Path]=None, disregard_skip: bool=False, **config_kwargs) -> str: + config = _config(config=config, **config_kwargs) + + sorted_output = sorted_imports(file_contents=file_contents, extension=extension, config=config, file_path=file_path, disregard_skip=disregard_skip, **config_kwargs) + if config.ignore_whitespace: + compare_in = remove_whitespace(file_contents).strip() + compare_out = remove_whitespace(sorted_output).strip() + else: + compare_in = file_contents.strip() + compare_out = sorted_output.strip() + + if compare_out == compare_in: + if config.verbose: + print(f"SUCCESS: {logging_file_path} Everything Looks Good!") + return True + else: + print(f"ERROR: {logging_file_path} Imports are incorrectly sorted.") + if show_diff: + show_unified_diff( + file_input=file_contents, file_output=sorted_output, file_path=file_path + ) + return False + + def sorted_file(filename: str, config: Config=DEFAULT_CONFIG, **config_kwargs) -> str: file_data = File.read(filename) - if config is DEFAULT_CONFIG and not "settings_path" in config_kwargs and not "settings_file" in config_kwargs: - config_kwargs["settings_path"] = file_data.path.parent - + config = _config(path=file_data.path.parent, config=config) return sorted_contents(file_contents=file_data.contents, extension=file_data.extension, config=config, file_path=file_data.path, **config_kwargs) diff --git a/isort/compat.py b/isort/compat.py index 7e6a5b7f6..aa495192c 100644 --- a/isort/compat.py +++ b/isort/compat.py @@ -57,7 +57,12 @@ def __init__( config = Config(**setting_overrides) try: - self.output = api.sorted_imports(file_contents, extension=extension, **setting_overrides) + if check: + self.incorrectly_sorted = not api.check_imports(file_contents, extension=extension, config=config, file_path=file_path, show_diff=show_diff) + self.output = "" + return + else: + self.output = api.sorted_imports(file_contents, extension=extension, config=config, file_path=file_path) except FileSkipped as error: if config.verbose: warn(error.message) diff --git a/isort/format.py b/isort/format.py index 80a215e42..6a6442f56 100644 --- a/isort/format.py +++ b/isort/format.py @@ -56,3 +56,10 @@ def ask_whether_to_apply_changes_to_file(file_path: str) -> bool: if answer in ("quit", "q"): sys.exit(1) return True + + +def remove_whitespace(content: str) -> str: + content = ( + content.replace(self.parsed.line_separator, "").replace(" ", "").replace("\x0c", "") + ) + return content diff --git a/isort/settings.py b/isort/settings.py index a97bfcfbf..bf7b51421 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -257,7 +257,7 @@ def __init__(self, settings_file: str = "", settings_path: str = "", **config_ov combined_config.pop(f"{IMPORT_HEADING_PREFIX}{import_heading_key}") combined_config["import_headings"] = import_headings - super().__init__(known_other=known_other, sources=tuple(sources), **combined_config) + super().__init__(sources=tuple(sources), **combined_config) def is_skipped(self, file_path: Path) -> bool: """Returns True if the file and/or folder should be skipped based on current settings.""" From b3d0ec68fc990e3ebd681c325fb02cf0ed1a68e2 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 6 Nov 2019 23:27:21 -0800 Subject: [PATCH 0199/1439] Fix is_skipped usage in main.py --- isort/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/main.py b/isort/main.py index e886384ad..14732e551 100644 --- a/isort/main.py +++ b/isort/main.py @@ -596,7 +596,7 @@ def main(argv: Optional[Sequence[str]] = None) -> None: if filter_files: filtered_files = [] for file_name in file_names: - if file_should_be_skipped(file_name, config): + if config.is_skipped(Path(file_name)): skipped.append(file_name) else: filtered_files.append(file_name) From bb206ee4f2c5cc724eaeee3629bf7fca3a602fe6 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 6 Nov 2019 23:35:31 -0800 Subject: [PATCH 0200/1439] Fix remove whitespace usage post refactor --- isort/api.py | 5 +++-- isort/format.py | 4 ++-- isort/main.py | 5 +++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/isort/api.py b/isort/api.py index 541aac1a7..7e13c16de 100644 --- a/isort/api.py +++ b/isort/api.py @@ -50,8 +50,9 @@ def check_imports(file_contents: str, show_diff: bool=False, extension: str = "p sorted_output = sorted_imports(file_contents=file_contents, extension=extension, config=config, file_path=file_path, disregard_skip=disregard_skip, **config_kwargs) if config.ignore_whitespace: - compare_in = remove_whitespace(file_contents).strip() - compare_out = remove_whitespace(sorted_output).strip() + line_separator = config.line_ending or parse._infer_line_separator(file_contents) + compare_in = remove_whitespace(file_contents, line_separator=line_separator).strip() + compare_out = remove_whitespace(sorted_output, line_separator=line_separator).strip() else: compare_in = file_contents.strip() compare_out = sorted_output.strip() diff --git a/isort/format.py b/isort/format.py index 6a6442f56..a03bea5da 100644 --- a/isort/format.py +++ b/isort/format.py @@ -58,8 +58,8 @@ def ask_whether_to_apply_changes_to_file(file_path: str) -> bool: return True -def remove_whitespace(content: str) -> str: +def remove_whitespace(content: str, line_separator: str="\n") -> str: content = ( - content.replace(self.parsed.line_separator, "").replace(" ", "").replace("\x0c", "") + content.replace(line_separator, "").replace(" ", "").replace("\x0c", "") ) return content diff --git a/isort/main.py b/isort/main.py index 14732e551..f1bc0a88d 100644 --- a/isort/main.py +++ b/isort/main.py @@ -588,6 +588,7 @@ def main(argv: Optional[Sequence[str]] = None) -> None: jobs = config_dict.pop("jobs", ()) show_logo = config_dict.pop("show_logo", False) filter_files = config_dict.pop("filter_files", False) + check = config_dict.pop("check", False) config = Config(**config_dict) wrong_sorted_files = False @@ -613,12 +614,12 @@ def main(argv: Optional[Sequence[str]] = None) -> None: executor = multiprocessing.Pool(jobs) attempt_iterator = executor.imap( - functools.partial(sort_imports, **config_dict), file_names + functools.partial(sort_imports, check=check, **config_dict), file_names ) else: # https://github.com/python/typeshed/pull/2814 attempt_iterator = ( # type: ignore - sort_imports(file_name, **config_dict) for file_name in file_names + sort_imports(file_name, check=check, **config_dict) for file_name in file_names ) for sort_attempt in attempt_iterator: From f5d63047d895e4128ec93f90b1cbcfbf039ce292 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 7 Nov 2019 17:28:36 -0800 Subject: [PATCH 0201/1439] Add existing syntax error exception type --- isort/api.py | 5 ++++- isort/exceptions.py | 10 ++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/isort/api.py b/isort/api.py index 7e13c16de..6009548fb 100644 --- a/isort/api.py +++ b/isort/api.py @@ -34,7 +34,10 @@ def sorted_imports(file_contents: str, extension: str = "py", config: Config=DEF raise FileSkipSetting(file_path) if config.atomic: - compile(file_contents, content_source, "exec", 0, 1) + try: + compile(file_contents, content_source, "exec", 0, 1) + except SyntaxError: + raise ExistingSyntaxError(content_source) parsed_output = output.sorted_imports(parse.file_contents(file_contents, config=config), config, extension) if config.atomic: diff --git a/isort/exceptions.py b/isort/exceptions.py index 1037b55ba..02a4eb93a 100644 --- a/isort/exceptions.py +++ b/isort/exceptions.py @@ -21,6 +21,16 @@ def __init__(self, file_path: Path, encoding: str, fallback_encoding: str): self.fallback_encoding = fallback_encoding +class ExistingSyntaxErrors(ISortError): + """Raised when isort is told to sort imports within code that has existing syntax errors""" + + def __init__(self, file_path: Path): + super().__init__( + f"isort was told to sort imports within code that contains syntax errors: " + f"{file_path}." + ) + + class IntroducedSyntaxErrors(ISortError): """Raised when isort has introduced a syntax error in the process of sorting imports""" From 0d96676f9c81c565baaae05ade5f54255c5b4bc6 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 7 Nov 2019 18:25:11 -0800 Subject: [PATCH 0202/1439] Update configuration test to use new Config object --- pyproject.toml | 1 - tests/test_isort.py | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 727e63478..f2c8e169e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,6 @@ pipreqs = "^0.4.9" tomlkit = "0.5.3" pip_api = "^0.0.12" numpy = "^1.16.0" -sphinx = "^2.2" [tool.poetry.scripts] isort = "isort.main:main" diff --git a/tests/test_isort.py b/tests/test_isort.py index a400a1c31..5eea47c79 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -16,7 +16,7 @@ import pytest from isort import finders, main, settings from isort.main import SortImports, is_python_file -from isort.settings import WrapModes +from isort.settings import WrapModes, Config from isort.utils import exists_case_sensitive try: @@ -3785,8 +3785,8 @@ def test_extract_multiline_output_wrap_setting_from_a_config_file(tmpdir: py.pat config_file = tmpdir.join(".editorconfig") config_file.write("\n".join(editorconfig_contents)) - config = settings.from_path(str(tmpdir)) - assert config["multi_line_output"] == WrapModes.VERTICAL_GRID_GROUPED + config = Config(settings_path=str(tmpdir)) + assert config.multi_line_output == WrapModes.VERTICAL_GRID_GROUPED def test_ensure_support_for_non_typed_but_cased_alphabetic_sort_issue_890() -> None: From 18db169ac3fc60097e99a112a28dfc0817200cff Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 7 Nov 2019 18:27:46 -0800 Subject: [PATCH 0203/1439] Fix skip_glob test --- tests/test_isort.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_isort.py b/tests/test_isort.py index 5eea47c79..688b142bf 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -3228,7 +3228,7 @@ def test_skip_glob(tmpdir, skip_glob_assert: Tuple[List[str], int, Set[str]]) -> code_dir = base_dir.mkdir("code") code_dir.join("file.py").write("import os") - config = dict(settings.default.copy(), skip_glob=skip_glob) + config = Config(skip_glob=skip_glob) skipped = [] # type: List[str] file_names = { os.path.relpath(f, str(base_dir)) From 343924996fdd0488672c1c477c3ce18efbfc531d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 7 Nov 2019 18:28:50 -0800 Subject: [PATCH 0204/1439] Fix safety excludes test to use Config object --- tests/test_isort.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_isort.py b/tests/test_isort.py index 688b142bf..1d80519a9 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -3184,7 +3184,7 @@ def test_safety_excludes(tmpdir, enabled: bool) -> None: 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) + config = Config(safety_excludes=enabled) skipped = [] # type: List[str] codes = [str(tmpdir)] main.iter_source_code(codes, config, skipped) From 00fcbaa2780fbd19539bc0698309da4e7cef6935 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 7 Nov 2019 19:05:09 -0800 Subject: [PATCH 0205/1439] Fix sections usage, don't convert needlesly into a namedtuple --- isort/finders.py | 14 +++++++------- isort/parse.py | 8 +++----- tests/test_isort.py | 12 ++++++------ 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/isort/finders.py b/isort/finders.py index 5a3d87595..9efda5686 100644 --- a/isort/finders.py +++ b/isort/finders.py @@ -65,7 +65,7 @@ def find(self, module_name: str) -> Optional[str]: class LocalFinder(BaseFinder): def find(self, module_name: str) -> Optional[str]: if module_name.startswith("."): - return self.sections.LOCALFOLDER + return "LOCALFOLDER" return None @@ -172,17 +172,17 @@ def find(self, module_name: str) -> Optional[str]: is_package = exists_case_sensitive(package_path) and os.path.isdir(package_path) if is_module or is_package: if "site-packages" in prefix: - return self.sections.THIRDPARTY + return "THIRDPARTY" if "dist-packages" in prefix: - return self.sections.THIRDPARTY + return "THIRDPARTY" if self.virtual_env and self.virtual_env_src in prefix: - return self.sections.THIRDPARTY + return "THIRDPARTY" if os.path.normcase(prefix) == self.stdlib_lib_prefix: - return self.sections.STDLIB + return "STDLIB" if self.conda_env and self.conda_env in prefix: - return self.sections.THIRDPARTY + return "THIRDPARTY" if os.path.normcase(prefix).startswith(self.stdlib_lib_prefix): - return self.sections.STDLIB + return "STDLIB" return self.config.default_section return None diff --git a/isort/parse.py b/isort/parse.py index 039d6fe21..4ac05b012 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -146,10 +146,8 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte in_lines = contents.split(line_separator) out_lines = [] original_line_count = len(in_lines) - - sections: Any = namedtuple("Sections", config.sections)(*config.sections) section_comments = [f"# {heading}" for heading in config.import_headings.values()] - finder = FindersManager(config=config, sections=sections) + finder = FindersManager(config=config, sections=config.sections) if original_line_count > 1 or in_lines[:1] not in ([], [""]) or config.force_adds: in_lines.extend(add_imports) @@ -160,7 +158,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte import_placements: Dict[str, str] = {} as_map: Dict[str, List[str]] = defaultdict(list) imports: OrderedDict[str, Dict[str, Any]] = OrderedDict() - for section in chain(sections, config.forced_separate): + for section in chain(config.sections, config.forced_separate): imports[section] = {"straight": OrderedDict(), "from": OrderedDict()} categorized_comments: CommentsDict = { "from": {}, @@ -442,6 +440,6 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte change_count=change_count, original_line_count=original_line_count, line_separator=line_separator, - sections=sections, + sections=config.sections, section_comments=section_comments, ) diff --git a/tests/test_isort.py b/tests/test_isort.py index 1d80519a9..84da4faa2 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -3096,8 +3096,8 @@ def test_monkey_patched_urllib() -> None: def test_path_finder(monkeypatch) -> None: - si = SortImports(file_contents="") - finder = finders.PathFinder(config=si.config, sections=si.sections) + config = config=Config() + finder = finders.PathFinder(config=config, sections=config.sections) third_party_prefix = next(path for path in finder.paths if "site-packages" in path) ext_suffixes = importlib.machinery.EXTENSION_SUFFIXES imaginary_paths = set( @@ -3115,11 +3115,11 @@ def test_path_finder(monkeypatch) -> None: ) monkeypatch.setattr("isort.finders.exists_case_sensitive", lambda p: p in imaginary_paths) - assert finder.find("example_1") == finder.sections.STDLIB - assert finder.find("example_2") == finder.sections.THIRDPARTY - assert finder.find("example_3") == finder.sections.FIRSTPARTY + assert finder.find("example_1") == "STDLIB" + assert finder.find("example_2") == "THIRDPARTY" + assert finder.find("example_3") == "FIRSTPARTY" for i, _ in enumerate(ext_suffixes, 4): - assert finder.find("example_" + str(i)) == finder.sections.THIRDPARTY + assert finder.find("example_" + str(i)) == "THIRDPARTY" def test_argument_parsing() -> None: From 284a6465970aac7ebbe13c21ae751b5fd67c714e Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 7 Nov 2019 19:16:14 -0800 Subject: [PATCH 0206/1439] Switch to sections default --- isort/finders.py | 23 ++++++++++++----------- isort/main.py | 4 ++-- isort/settings.py | 5 ++--- tests/test_isort.py | 10 +++++----- tests/test_parse.py | 2 +- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/isort/finders.py b/isort/finders.py index 9efda5686..3ac93be52 100644 --- a/isort/finders.py +++ b/isort/finders.py @@ -15,6 +15,7 @@ from .settings import DEFAULT_CONFIG, Config from .utils import chdir, exists_case_sensitive +from . import sections try: from pipreqs import pipreqs @@ -31,11 +32,11 @@ except ImportError: Pipfile = None -KNOWN_SECTION_MAPPING = { - "STDLIB": "STANDARD_LIBRARY", - "FUTURE": "FUTURE_LIBRARY", - "FIRSTPARTY": "FIRST_PARTY", - "THIRDPARTY": "THIRD_PARTY", +KNOWN_SECTION_MAPPING: Dict[str, str] = { + sections.STDLIB: "STANDARD_LIBRARY", + sections.FUTURE: "FUTURE_LIBRARY", + sections.FIRSTPARTY: "FIRST_PARTY", + sections.THIRDPARTY: "THIRD_PARTY", } @@ -172,17 +173,17 @@ def find(self, module_name: str) -> Optional[str]: is_package = exists_case_sensitive(package_path) and os.path.isdir(package_path) if is_module or is_package: if "site-packages" in prefix: - return "THIRDPARTY" + return sections.THIRDPARTY if "dist-packages" in prefix: - return "THIRDPARTY" + return sections.THIRDPARTY if self.virtual_env and self.virtual_env_src in prefix: - return "THIRDPARTY" + return sections.THIRDPARTY if os.path.normcase(prefix) == self.stdlib_lib_prefix: - return "STDLIB" + return sections.STDLIB if self.conda_env and self.conda_env in prefix: - return "THIRDPARTY" + return sections.THIRDPARTY if os.path.normcase(prefix).startswith(self.stdlib_lib_prefix): - return "STDLIB" + return sections.STDLIB return self.config.default_section return None diff --git a/isort/main.py b/isort/main.py index f1bc0a88d..d3f3e27ac 100644 --- a/isort/main.py +++ b/isort/main.py @@ -14,7 +14,6 @@ from isort import SortImports, __version__ from isort.logo import ASCII_ART from isort.settings import ( - DEFAULT_SECTIONS, VALID_PY_TARGETS, WrapModes, Config @@ -23,6 +22,7 @@ # from_path, # wrap_mode_from_string, ) +from . import sections shebang_re = re.compile(br"^#!.*\bpython[23w]?\b") @@ -408,7 +408,7 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: "--section-default", dest="default_section", help="Sets the default section for imports (by default FIRSTPARTY) options: " - + str(DEFAULT_SECTIONS), + + str(sections.DEFAULT), ) parser.add_argument( "--sg", diff --git a/isort/settings.py b/isort/settings.py index bf7b51421..0714bbb83 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -32,7 +32,7 @@ ) from warnings import warn -from . import stdlibs +from . import stdlibs, sections from ._future import dataclass, field from .utils import difference, union from .wrap_modes import WrapModes @@ -59,7 +59,6 @@ FILE_SKIP_COMMENT: str = ("isort:" + "skip_file") # Concatenated to avoid this file being skipped MAX_CONFIG_SEARCH_DEPTH: int = 25 # The number of parent directories to for a config file within STOP_CONFIG_SEARCH_ON_DIRS: Tuple[str, ...] = (".git", ".hg") -DEFAULT_SECTIONS: Tuple[str, ...] = ("FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER") VALID_PY_TARGETS: Tuple[str, ...] = tuple( target.replace("py", "") for target in dir(stdlibs) if not target.startswith("_") ) @@ -106,7 +105,7 @@ class _Config: line_length: int = 79 wrap_length: int = 0 line_ending: str = "" - sections: Tuple[str, ...] = DEFAULT_SECTIONS + sections: Tuple[str, ...] = sections.DEFAULT no_sections: bool = False known_future_library: FrozenSet[str] = frozenset(("__future__",)) known_third_party: FrozenSet[str] = frozenset(("google.appengine.api",)) diff --git a/tests/test_isort.py b/tests/test_isort.py index 84da4faa2..abef02645 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -14,7 +14,7 @@ import py import pytest -from isort import finders, main, settings +from isort import finders, main, settings, sections from isort.main import SortImports, is_python_file from isort.settings import WrapModes, Config from isort.utils import exists_case_sensitive @@ -3115,11 +3115,11 @@ def test_path_finder(monkeypatch) -> None: ) monkeypatch.setattr("isort.finders.exists_case_sensitive", lambda p: p in imaginary_paths) - assert finder.find("example_1") == "STDLIB" - assert finder.find("example_2") == "THIRDPARTY" - assert finder.find("example_3") == "FIRSTPARTY" + assert finder.find("example_1") == sections.STDLIB + assert finder.find("example_2") == sections.THIRDPARTY + assert finder.find("example_3") == sections.FIRSTPARTY for i, _ in enumerate(ext_suffixes, 4): - assert finder.find("example_" + str(i)) == "THIRDPARTY" + assert finder.find("example_" + str(i)) == sections.THIRDPARTY def test_argument_parsing() -> None: diff --git a/tests/test_parse.py b/tests/test_parse.py index ffef6e73e..503f4b2c8 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -1,7 +1,7 @@ from hypothesis_auto import auto_pytest_magic from isort import parse -from isort.settings import DEFAULT_SECTIONS, default +from isort.settings import default TEST_CONTENTS = """ import xyz From 8a773532e787062d1c63b4a952a79b1dd3d46204 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 7 Nov 2019 19:16:23 -0800 Subject: [PATCH 0207/1439] Add sections module --- isort/sections.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 isort/sections.py diff --git a/isort/sections.py b/isort/sections.py new file mode 100644 index 000000000..f59db6926 --- /dev/null +++ b/isort/sections.py @@ -0,0 +1,9 @@ +"""Defines all sections isort uses by default""" +from typing import Tuple + +FUTURE: str = "FUTURE" +STDLIB: str = "STDLIB" +THIRDPARTY: str = "THIRDPARTY" +FIRSTPARTY: str = "FIRSTPARTY" +LOCALFOLDER: str = "LOCALFOLDER" +DEFAULT: Tuple[str, ...] = (FUTURE, STDLIB, THIRDPARTY, FIRSTPARTY, LOCALFOLDER) From 11c24c0e02ed1875ce38eb7ffb054a022a50b6b8 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 7 Nov 2019 19:34:13 -0800 Subject: [PATCH 0208/1439] Simplify sections --- isort/finders.py | 22 ++++++++++------------ isort/parse.py | 2 +- tests/test_isort.py | 17 ++++++++--------- 3 files changed, 19 insertions(+), 22 deletions(-) diff --git a/isort/finders.py b/isort/finders.py index 3ac93be52..2cd2e37ed 100644 --- a/isort/finders.py +++ b/isort/finders.py @@ -41,9 +41,8 @@ class BaseFinder(metaclass=ABCMeta): - def __init__(self, config: Config, sections: Any) -> None: + def __init__(self, config: Config) -> None: self.config = config - self.sections = sections @abstractmethod def find(self, module_name: str) -> Optional[str]: @@ -71,11 +70,11 @@ def find(self, module_name: str) -> Optional[str]: class KnownPatternFinder(BaseFinder): - def __init__(self, config: Config, sections: Any) -> None: - super().__init__(config, sections) + def __init__(self, config: Config) -> None: + super().__init__(config) self.known_patterns: List[Tuple[Pattern[str], str]] = [] - for placement in reversed(self.sections): + for placement in reversed(config.sections): known_placement = KNOWN_SECTION_MAPPING.get(placement, placement).lower() config_key = f"known_{known_placement}" known_patterns = list(getattr(self.config, config_key, self.config.known_other.get(known_placement, []))) @@ -113,8 +112,8 @@ def find(self, module_name: str) -> Optional[str]: class PathFinder(BaseFinder): - def __init__(self, config: Config, sections: Any) -> None: - super().__init__(config, sections) + def __init__(self, config: Config) -> None: + super().__init__(config) # restore the original import path (i.e. not the path to bin/isort) root_dir = os.getcwd() @@ -191,8 +190,8 @@ def find(self, module_name: str) -> Optional[str]: class ReqsBaseFinder(BaseFinder): enabled = False - def __init__(self, config: Config, sections: Any, path: str = ".") -> None: - super().__init__(config, sections) + def __init__(self, config: Config, path: str = ".") -> None: + super().__init__(config) self.path = path if self.enabled: self.mapping = self._load_mapping() @@ -274,7 +273,7 @@ def find(self, module_name: str) -> Optional[str]: for name in self.names: if module_name == name: - return self.sections.THIRDPARTY + return sections.THIRDPARTY return None @@ -364,7 +363,6 @@ class FindersManager: def __init__( self, config: Config, - sections: Any, finder_classes: Optional[Iterable[Type[BaseFinder]]] = None, ) -> None: self.verbose: bool = config.verbose @@ -374,7 +372,7 @@ def __init__( finders: List[BaseFinder] = [] for finder_cls in finder_classes: try: - finders.append(finder_cls(config, sections)) + finders.append(finder_cls(config)) except Exception as exception: # if one finder fails to instantiate isort can continue using the rest if self.verbose: diff --git a/isort/parse.py b/isort/parse.py index 4ac05b012..51dbc96f2 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -147,7 +147,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte out_lines = [] original_line_count = len(in_lines) section_comments = [f"# {heading}" for heading in config.import_headings.values()] - finder = FindersManager(config=config, sections=config.sections) + finder = FindersManager(config=config) if original_line_count > 1 or in_lines[:1] not in ([], [""]) or config.force_adds: in_lines.extend(add_imports) diff --git a/tests/test_isort.py b/tests/test_isort.py index abef02645..5261cda4c 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -2998,18 +2998,18 @@ def test_requirements_finder(tmpdir) -> None: subdir.write("flask") req_file = tmpdir.join("requirements.txt") req_file.write("Django==1.11\n-e git+https://github.com/orsinium/deal.git#egg=deal\n") - si = SortImports(file_contents="") for path in (str(tmpdir), str(subdir)): - finder = finders.RequirementsFinder(config=si.config, sections=si.sections, path=path) + + finder = finders.RequirementsFinder(config=Config(), path=path) files = list(finder._get_files()) assert len(files) == 1 # file finding assert files[0].endswith("requirements.txt") # file finding assert set(finder._get_names(str(req_file))) == {"Django", "deal"} # file parsing - assert finder.find("django") == si.sections.THIRDPARTY # package in reqs + assert finder.find("django") == sections.THIRDPARTY # package in reqs assert finder.find("flask") is None # package not in reqs - assert finder.find("deal") == si.sections.THIRDPARTY # vcs + assert finder.find("deal") == sections.THIRDPARTY # vcs assert len(finder.mapping) > 100 assert finder._normalize_name("deal") == "deal" @@ -3070,14 +3070,13 @@ def test_forced_separate_is_deterministic_issue_774(tmpdir) -> None: def test_pipfile_finder(tmpdir) -> None: pipfile = tmpdir.join("Pipfile") pipfile.write(PIPFILE) - si = SortImports(file_contents="") - finder = finders.PipfileFinder(config=si.config, sections=si.sections, path=str(tmpdir)) + finder = finders.PipfileFinder(config=Config(), path=str(tmpdir)) assert set(finder._get_names(str(tmpdir))) == {"Django", "deal"} # file parsing - assert finder.find("django") == si.sections.THIRDPARTY # package in reqs + assert finder.find("django") == sections.THIRDPARTY # package in reqs assert finder.find("flask") is None # package not in reqs - assert finder.find("deal") == si.sections.THIRDPARTY # vcs + assert finder.find("deal") == sections.THIRDPARTY # vcs assert len(finder.mapping) > 100 assert finder._normalize_name("deal") == "deal" @@ -3097,7 +3096,7 @@ def test_monkey_patched_urllib() -> None: def test_path_finder(monkeypatch) -> None: config = config=Config() - finder = finders.PathFinder(config=config, sections=config.sections) + finder = finders.PathFinder(config=config) third_party_prefix = next(path for path in finder.paths if "site-packages" in path) ext_suffixes = importlib.machinery.EXTENSION_SUFFIXES imaginary_paths = set( From c358425c2432fdbee47a9ada0d2023e40b4926cc Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 7 Nov 2019 19:41:01 -0800 Subject: [PATCH 0209/1439] Fix logging statuments to include correct file path --- isort/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/api.py b/isort/api.py index 6009548fb..ec48a6c73 100644 --- a/isort/api.py +++ b/isort/api.py @@ -62,10 +62,10 @@ def check_imports(file_contents: str, show_diff: bool=False, extension: str = "p if compare_out == compare_in: if config.verbose: - print(f"SUCCESS: {logging_file_path} Everything Looks Good!") + print(f"SUCCESS: {file_path} Everything Looks Good!") return True else: - print(f"ERROR: {logging_file_path} Imports are incorrectly sorted.") + print(f"ERROR: {file_path} Imports are incorrectly sorted.") if show_diff: show_unified_diff( file_input=file_contents, file_output=sorted_output, file_path=file_path From 1bef08e4a56b07301b5a88c4b79e677f3414173c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 7 Nov 2019 20:02:43 -0800 Subject: [PATCH 0210/1439] Further refactoring progress, less than 10 test failures on test_isort! --- isort/api.py | 4 ++-- isort/settings.py | 11 ++++++----- tests/test_isort.py | 19 +++++++------------ 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/isort/api.py b/isort/api.py index ec48a6c73..a212128f7 100644 --- a/isort/api.py +++ b/isort/api.py @@ -3,7 +3,7 @@ from typing import Any, NamedTuple, Optional, Tuple from . import output, parse -from .exceptions import FileSkipComment, IntroducedSyntaxErrors, UnableToDetermineEncoding +from .exceptions import FileSkipComment, IntroducedSyntaxErrors, UnableToDetermineEncoding, ExistingSyntaxErrors from .io import File from .format import remove_whitespace from .settings import DEFAULT_CONFIG, FILE_SKIP_COMMENT, Config @@ -37,7 +37,7 @@ def sorted_imports(file_contents: str, extension: str = "py", config: Config=DEF try: compile(file_contents, content_source, "exec", 0, 1) except SyntaxError: - raise ExistingSyntaxError(content_source) + raise ExistingSyntaxErrors(content_source) parsed_output = output.sorted_imports(parse.file_contents(file_contents, config=config), config, extension) if config.atomic: diff --git a/isort/settings.py b/isort/settings.py index 0714bbb83..f99f5020c 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -177,11 +177,12 @@ def __post_init__(self): if py_version != "all": object.__setattr__(self, "py_version", f"py{py_version}") - object.__setattr__( - self, - "known_standard_library", - list(getattr(stdlibs, self.py_version).stdlib | set(self.known_standard_library)), - ) + if not self.known_standard_library: + object.__setattr__( + self, + "known_standard_library", + frozenset(getattr(stdlibs, self.py_version).stdlib), + ) if self.force_alphabetical_sort: object.__setattr__(self, "force_alphabetical_sort_within_sections", True) diff --git a/tests/test_isort.py b/tests/test_isort.py index 5261cda4c..294c875ec 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -917,7 +917,7 @@ def test_known_pattern_path_expansion() -> None: test_output = SortImports( file_contents=test_input, default_section="THIRDPARTY", - known_first_party=["./", "this", "kate_plugin"], + known_first_party=["./", "this", "kate_plugin", "isort"], ).output assert test_output == ( "import os\n" @@ -1215,15 +1215,10 @@ def test_smart_lines_after_import_section() -> None: ) -def test_settings_combine_instead_of_overwrite() -> None: - """Test to ensure settings combine logically, instead of fully overwriting.""" - assert set( - SortImports(known_standard_library=["not_std_library"]).config["known_standard_library"] - ) == set(SortImports().config["known_standard_library"]) | {"not_std_library"} - - assert set( - SortImports(not_known_standard_library=["thread"]).config["known_standard_library"] - ) == {item for item in SortImports().config["known_standard_library"] if item != "thread"} +def test_settings_overwrite() -> None: + """Test to ensure settings overwrite instead of trying to combine.""" + assert Config(known_standard_library=["not_std_library"]).known_standard_library == frozenset({"not_std_library"}) + assert Config(known_first_party=["thread"]).known_first_party == frozenset({"thread"}) def test_combined_from_and_as_imports() -> None: @@ -1656,7 +1651,7 @@ def test_placement_control() -> None: test_output = SortImports( file_contents=test_input, known_first_party=["p24", "p24.imports._VERSION"], - known_standard_library=["p24.imports"], + known_standard_library=["p24.imports", "os", "sys"], known_third_party=["bottle"], default_section="THIRDPARTY", ).output @@ -1697,7 +1692,7 @@ def test_custom_sections() -> None: import_heading_firstparty="First Party", import_heading_django="Django", import_heading_pandas="Pandas", - known_standard_library=["p24.imports"], + known_standard_library=["p24.imports", "os", "sys"], known_third_party=["bottle"], known_django=["django"], known_pandas=["pandas", "numpy"], From 65360687e5a82eca7f50d78d371c243201392548 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 7 Nov 2019 20:08:56 -0800 Subject: [PATCH 0211/1439] Fix atomic behaviour to match pre-refactor isort --- isort/compat.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/isort/compat.py b/isort/compat.py index aa495192c..4daac8045 100644 --- a/isort/compat.py +++ b/isort/compat.py @@ -7,7 +7,7 @@ from warnings import warn from . import api, settings -from .exceptions import FileSkipped +from .exceptions import FileSkipped, ExistingSyntaxErrors, IntroducedSyntaxErrors from .format import ask_whether_to_apply_changes_to_file, show_unified_diff from .io import File from .isort import _SortImports @@ -67,6 +67,14 @@ def __init__( if config.verbose: warn(error.message) return + except ExistingSyntaxErrors: + warn("{file_path} unable to sort due to existing syntax errors") + self.output = file_contents + return + except IntroducedSyntaxErrors: + warn("{file_path} unable to sort as isort introduces new syntax errors") + self.output = file_contents + return if check: check_output = self.output From 2040dd63708b18062033498948b351e160f8b509 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 7 Nov 2019 22:39:27 -0800 Subject: [PATCH 0212/1439] Remove safety excludes completely --- CHANGELOG.md | 2 ++ isort/main.py | 9 --------- isort/settings.py | 18 ++---------------- tests/test_isort.py | 11 +++++++---- 4 files changed, 11 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b4fdf5cb..ff735fd19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ Changelog - Since there is no longer composition negative form settings (such as --dont-skip) are no longer required and have been removed. - Two-letter shortened setting names (like `ac` for `atomic`) now require two dashes to avoid ambiguity: `--ac`. - `length_sort_{section_name}` config usage has been deprecated. Instead `length_sort_sections` list can be used to specify a list of sections that need to be length sorted. + - `safety_excludes` and `unsafe` have been deprecated + - Config now includes as default full set of safety directories defined by safety excludes. Internal: - isort now utilizes mypy and typing to filter out typing related issues before deployment. diff --git a/isort/main.py b/isort/main.py index d3f3e27ac..ef82beadc 100644 --- a/isort/main.py +++ b/isort/main.py @@ -504,13 +504,6 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: action="store_true", help="Tells isort to apply changes recursively without asking", ) - 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", @@ -544,8 +537,6 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: arguments = {key: value for key, value in vars(parser.parse_args(argv)).items() if value} if "dont_order_by_type" in arguments: arguments["order_by_type"] = False - if arguments.pop("unsafe", False): - arguments["safety_excludes"] = False return arguments diff --git a/isort/settings.py b/isort/settings.py index f99f5020c..17a74ceb5 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -51,11 +51,6 @@ except ImportError: appdirs = None -safety_exclude_re = re.compile( - r"/(\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|_build|buck-out|build|dist|\.pants\.d" - r"|lib/python[0-9].[0-9]+|node_modules)/" -) - FILE_SKIP_COMMENT: str = ("isort:" + "skip_file") # Concatenated to avoid this file being skipped MAX_CONFIG_SEARCH_DEPTH: int = 25 # The number of parent directories to for a config file within STOP_CONFIG_SEARCH_ON_DIRS: Tuple[str, ...] = (".git", ".hg") @@ -97,10 +92,9 @@ class _Config: NOTE: known lists, such as known_standard_library, are intentionally not complete as they are dynamically determined later on. """ - py_version: str = "3" force_to_top: FrozenSet[str] = frozenset() - skip: FrozenSet[str] = frozenset() + skip: FrozenSet[str] = frozenset({".venv", "venv", ".tox", ".eggs", ".git", ".hg", ".mypy_cache", ".nox", "_build", "buck-out", "build", "dist", ".pants.d", "node_modules"}) skip_glob: FrozenSet[str] = frozenset() line_length: int = 79 wrap_length: int = 0 @@ -147,7 +141,6 @@ class _Config: no_lines_before: FrozenSet[str] = frozenset() no_inline_sort: bool = False ignore_comments: bool = False - safety_excludes: bool = True case_sensitive: bool = False sources: Tuple[Dict[str, Any], ...] = () virtual_env: str = "" @@ -261,7 +254,7 @@ def __init__(self, settings_file: str = "", settings_path: str = "", **config_ov def is_skipped(self, file_path: Path) -> bool: """Returns True if the file and/or folder should be skipped based on current settings.""" - if self.directory and self.directory in file_path.parents: + if self.directory and Path(self.directory) in file_path.parents: file_name = os.path.relpath(file_path, self.directory) path = self.directory else: @@ -274,13 +267,6 @@ def is_skipped(self, file_path: Path) -> bool: if normalized_path[1:2] == ":": normalized_path = normalized_path[2:] - if path and self.safety_excludes: - check_exclude = "/" + file_name.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 self.skip: if posixpath.abspath(normalized_path) == posixpath.abspath( skip_path.replace("\\", "/") diff --git a/tests/test_isort.py b/tests/test_isort.py index 294c875ec..3b055f23e 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -3172,13 +3172,16 @@ def test_quiet(tmpdir, capfd, quiet: bool) -> None: @pytest.mark.parametrize("enabled", (False, True)) -def test_safety_excludes(tmpdir, enabled: bool) -> None: +def test_safety_skips(tmpdir, enabled: bool) -> None: tmpdir.join("victim.py").write("# ...") toxdir = tmpdir.mkdir(".tox") toxdir.join("verysafe.py").write("# ...") - tmpdir.mkdir("lib").mkdir("python3.7").join("importantsystemlibrary.py").write("# ...") + tmpdir.mkdir("_build").mkdir("python3.7").join("importantsystemlibrary.py").write("# ...") tmpdir.mkdir(".pants.d").join("pants.py").write("import os") - config = Config(safety_excludes=enabled) + if enabled: + config = Config() + else: + config = Config(skip=[]) skipped = [] # type: List[str] codes = [str(tmpdir)] main.iter_source_code(codes, config, skipped) @@ -3222,7 +3225,7 @@ def test_skip_glob(tmpdir, skip_glob_assert: Tuple[List[str], int, Set[str]]) -> code_dir = base_dir.mkdir("code") code_dir.join("file.py").write("import os") - config = Config(skip_glob=skip_glob) + config = Config(skip_glob=skip_glob, directory=str(base_dir)) skipped = [] # type: List[str] file_names = { os.path.relpath(f, str(base_dir)) From 12fc36b666b2d8d353252546b442458cd282c299 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 7 Nov 2019 22:50:01 -0800 Subject: [PATCH 0213/1439] Fix safety skips to match new behaviour --- tests/test_isort.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_isort.py b/tests/test_isort.py index 3b055f23e..ce65ba8f7 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -3179,9 +3179,9 @@ def test_safety_skips(tmpdir, enabled: bool) -> None: tmpdir.mkdir("_build").mkdir("python3.7").join("importantsystemlibrary.py").write("# ...") tmpdir.mkdir(".pants.d").join("pants.py").write("import os") if enabled: - config = Config() + config = Config(directory=str(tmpdir)) else: - config = Config(skip=[]) + config = Config(skip=[], directory=str(tmpdir)) skipped = [] # type: List[str] codes = [str(tmpdir)] main.iter_source_code(codes, config, skipped) @@ -3197,7 +3197,7 @@ def test_safety_skips(tmpdir, enabled: bool) -> None: else: assert file_names == { os.sep.join((".tox", "verysafe.py")), - os.sep.join(("lib", "python3.7", "importantsystemlibrary.py")), + os.sep.join(("_build", "python3.7", "importantsystemlibrary.py")), os.sep.join((".pants.d", "pants.py")), "victim.py", } @@ -3206,7 +3206,7 @@ def test_safety_skips(tmpdir, enabled: bool) -> None: # directly pointing to files within unsafe directories shouldn't skip them either way file_names = { os.path.relpath(f, str(toxdir)) - for f in main.iter_source_code([str(toxdir)], config, skipped) + for f in main.iter_source_code([str(toxdir)], Config(directory=str(toxdir)), skipped) } assert file_names == {"verysafe.py"} From 6f58262f8783dd22c0d8e62f6ad6836fe4d7efe1 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 7 Nov 2019 23:38:55 -0800 Subject: [PATCH 0214/1439] All test_isort tests now passing! --- isort/api.py | 6 +++--- isort/compat.py | 7 ++++++- isort/io.py | 32 +++++++++++++++++++++----------- tests/test_isort.py | 9 +++------ 4 files changed, 33 insertions(+), 21 deletions(-) diff --git a/isort/api.py b/isort/api.py index a212128f7..1f7cbdec7 100644 --- a/isort/api.py +++ b/isort/api.py @@ -3,7 +3,7 @@ from typing import Any, NamedTuple, Optional, Tuple from . import output, parse -from .exceptions import FileSkipComment, IntroducedSyntaxErrors, UnableToDetermineEncoding, ExistingSyntaxErrors +from .exceptions import FileSkipComment, IntroducedSyntaxErrors, UnableToDetermineEncoding, ExistingSyntaxErrors, FileSkipSetting from .io import File from .format import remove_whitespace from .settings import DEFAULT_CONFIG, FILE_SKIP_COMMENT, Config @@ -62,10 +62,10 @@ def check_imports(file_contents: str, show_diff: bool=False, extension: str = "p if compare_out == compare_in: if config.verbose: - print(f"SUCCESS: {file_path} Everything Looks Good!") + print(f"SUCCESS: {file_path or ''} Everything Looks Good!") return True else: - print(f"ERROR: {file_path} Imports are incorrectly sorted.") + print(f"ERROR: {file_path or ''} Imports are incorrectly sorted.") if show_diff: show_unified_diff( file_input=file_contents, file_output=sorted_output, file_path=file_path diff --git a/isort/compat.py b/isort/compat.py index 4daac8045..0b92aed6c 100644 --- a/isort/compat.py +++ b/isort/compat.py @@ -44,7 +44,10 @@ def __init__( ): file_encoding = "utf-8" if file_path: - file_data = File.read(file_path) + if file_contents: + file_data = File.from_contents(file_contents, filename=file_path) + else: + file_data = File.read(file_path) file_contents, file_path, file_encoding = file_data if not extension: extension = file_data.extension @@ -64,6 +67,8 @@ def __init__( else: self.output = api.sorted_imports(file_contents, extension=extension, config=config, file_path=file_path) except FileSkipped as error: + self.skipped = True + self.output = None if config.verbose: warn(error.message) return diff --git a/isort/io.py b/isort/io.py index 15bcda6d6..4ad5dd1cb 100644 --- a/isort/io.py +++ b/isort/io.py @@ -17,24 +17,34 @@ def read(filename: str) -> "File": contents, encoding = _read_file_contents(file_path) return File(contents=contents, path=file_path, encoding=encoding) + @staticmethod + def from_contents(contents: str, filename: str) -> "File": + return File(contents, path=Path(filename).resolve(), encoding=_determine_content_encoding(contents)) + @property def extension(self): return self.path.suffix.lstrip(".") +def _determine_stream_encoding(stream, default: str = "utf-8") -> str: + for line_number, line in enumerate(stream, 1): + if line_number > 2: + break + groups = re.findall(_ENCODING_PATTERN, line) + if groups: + return groups[0].decode("ascii") + + return default + + +def _determine_content_encoding(content: str, default: str = "utf-8"): + return _determine_stream_encoding(content.encode(default).split(b"\n"), default=default) + + def _determine_file_encoding(file_path: Path, default: str = "utf-8") -> str: # see https://www.python.org/dev/peps/pep-0263/ - coding = default - with file_path.open("rb") as f: - for line_number, line in enumerate(f, 1): - if line_number > 2: - break - groups = re.findall(_ENCODING_PATTERN, line) - if groups: - coding = groups[0].decode("ascii") - break - - return coding + with file_path.open("rb") as open_file: + return _determine_stream_encoding(open_file, default=default) def _read_file_contents( diff --git a/tests/test_isort.py b/tests/test_isort.py index ce65ba8f7..b65140c8b 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -1574,7 +1574,7 @@ def test_long_line_comments() -> None: "sync_stage_envdir, " "update_stage_app, update_stage_cron # noqa\n" ) - assert SortImports(file_contents=test_input).output == ( + assert SortImports(file_contents=test_input, line_length=100, balanced_wrapping=True).output == ( "from foo.utils.fabric_stuff.live import (check_clean_live, deploy_live, # noqa\n" " sync_live_envdir, update_live_app, " "update_live_cron)\n" @@ -2506,7 +2506,6 @@ def test_long_alias_using_paren_issue_957() -> None: line_length=50, use_parentheses=True, multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, - check=True, ).output assert out == expected_output @@ -2524,7 +2523,6 @@ def test_long_alias_using_paren_issue_957() -> None: line_length=50, use_parentheses=True, multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, - check=True, ).output assert out == expected_output @@ -2544,7 +2542,6 @@ def test_long_alias_using_paren_issue_957() -> None: line_length=50, use_parentheses=True, multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, - check=True, ).output assert out == expected_output @@ -2602,7 +2599,7 @@ def test_import_case_produces_inconsistent_results_issue_472() -> None: "from scrapy.core.downloader.handlers.http import " "HttpDownloadHandler, HTTPDownloadHandler\n" ) - assert SortImports(file_contents=test_input).output == test_input + assert SortImports(file_contents=test_input, line_length=100).output == test_input def test_inconsistent_behavior_in_python_2_and_3_issue_479() -> None: @@ -2684,7 +2681,7 @@ def test_no_extra_lines_issue_557() -> None: ) assert ( SortImports( - file_contents=test_input, force_alphabetical_sort=True, force_sort_within_sections=True + file_contents=test_input, force_alphabetical_sort=True, force_sort_within_sections=True, line_length=100 ).output == expected_output ) From aaac1ae0667dabe6fd038c9f5a42c157b9457ef1 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 7 Nov 2019 23:41:48 -0800 Subject: [PATCH 0215/1439] Fix use of default config to match new refactor --- tests/test_parse.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_parse.py b/tests/test_parse.py index 503f4b2c8..ffae47a3d 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -1,7 +1,7 @@ from hypothesis_auto import auto_pytest_magic from isort import parse -from isort.settings import default +from isort.settings import Config TEST_CONTENTS = """ import xyz @@ -30,7 +30,7 @@ def test_file_contents(): line_separator, sections, section_comments, - ) = parse.file_contents(TEST_CONTENTS, config=default) + ) = parse.file_contents(TEST_CONTENTS, config=Config()) assert "\n".join(in_lines) == TEST_CONTENTS assert "import" not in "\n".join(out_lines) assert import_index == 1 From 592d07a364a7d8a2fcf2c2b55e42a8f6507209e6 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 8 Nov 2019 00:34:48 -0800 Subject: [PATCH 0216/1439] Fix all discovered mypy errors --- isort/api.py | 70 ++++++++++++++++++++++++++++++++++++--------- isort/compat.py | 53 ++++++++++++---------------------- isort/exceptions.py | 22 ++++++++------ isort/finders.py | 25 +++++++++++----- isort/format.py | 6 ++-- isort/io.py | 13 +++++---- isort/isort.py | 57 +----------------------------------- isort/main.py | 30 +++++++++---------- isort/output.py | 17 +++++------ isort/settings.py | 58 ++++++++++++++++++++++++++----------- isort/wrap.py | 10 +++---- tests/test_isort.py | 17 +++++++---- 12 files changed, 199 insertions(+), 179 deletions(-) diff --git a/isort/api.py b/isort/api.py index 1f7cbdec7..22802a97e 100644 --- a/isort/api.py +++ b/isort/api.py @@ -3,27 +3,48 @@ from typing import Any, NamedTuple, Optional, Tuple from . import output, parse -from .exceptions import FileSkipComment, IntroducedSyntaxErrors, UnableToDetermineEncoding, ExistingSyntaxErrors, FileSkipSetting +from .exceptions import ( + ExistingSyntaxErrors, + FileSkipComment, + FileSkipSetting, + IntroducedSyntaxErrors, + UnableToDetermineEncoding, +) +from .format import remove_whitespace, show_unified_diff from .io import File -from .format import remove_whitespace from .settings import DEFAULT_CONFIG, FILE_SKIP_COMMENT, Config -def _config(path: Optional[Path]=None, config: Config=DEFAULT_CONFIG, **config_kwargs) -> Config: +def _config( + path: Optional[Path] = None, config: Config = DEFAULT_CONFIG, **config_kwargs +) -> Config: if path: - if config is DEFAULT_CONFIG and not "settings_path" in config_kwargs and not "settings_file" in config_kwargs: + if ( + config is DEFAULT_CONFIG + and not "settings_path" in config_kwargs + and not "settings_file" in config_kwargs + ): config_kwargs["settings_path"] = path if config_kwargs and config is not DEFAULT_CONFIG: - raise ValueError("You can either specify custom configuration options using kwargs or " - "passing in a Config object. Not Both!") + raise ValueError( + "You can either specify custom configuration options using kwargs or " + "passing in a Config object. Not Both!" + ) elif config_kwargs: config = Config(**config_kwargs) return config -def sorted_imports(file_contents: str, extension: str = "py", config: Config=DEFAULT_CONFIG, file_path: Optional[Path]=None, disregard_skip: bool=False, **config_kwargs) -> str: +def sorted_imports( + file_contents: str, + extension: str = "py", + config: Config = DEFAULT_CONFIG, + file_path: Optional[Path] = None, + disregard_skip: bool = False, + **config_kwargs, +) -> str: config = _config(config=config, **config_kwargs) content_source = str(file_path or "Passed in content") if not disregard_skip: @@ -31,7 +52,7 @@ def sorted_imports(file_contents: str, extension: str = "py", config: Config=DEF raise FileSkipComment(content_source) elif file_path and config.is_skipped(file_path): - raise FileSkipSetting(file_path) + raise FileSkipSetting(content_source) if config.atomic: try: @@ -39,7 +60,9 @@ def sorted_imports(file_contents: str, extension: str = "py", config: Config=DEF except SyntaxError: raise ExistingSyntaxErrors(content_source) - parsed_output = output.sorted_imports(parse.file_contents(file_contents, config=config), config, extension) + parsed_output = output.sorted_imports( + parse.file_contents(file_contents, config=config), config, extension + ) if config.atomic: try: compile(file_contents, content_source, "exec", 0, 1) @@ -48,10 +71,25 @@ def sorted_imports(file_contents: str, extension: str = "py", config: Config=DEF return parsed_output -def check_imports(file_contents: str, show_diff: bool=False, extension: str = "py", config: Config=DEFAULT_CONFIG, file_path: Optional[Path]=None, disregard_skip: bool=False, **config_kwargs) -> str: +def check_imports( + file_contents: str, + show_diff: bool = False, + extension: str = "py", + config: Config = DEFAULT_CONFIG, + file_path: Optional[Path] = None, + disregard_skip: bool = False, + **config_kwargs, +) -> bool: config = _config(config=config, **config_kwargs) - sorted_output = sorted_imports(file_contents=file_contents, extension=extension, config=config, file_path=file_path, disregard_skip=disregard_skip, **config_kwargs) + sorted_output = sorted_imports( + file_contents=file_contents, + extension=extension, + config=config, + file_path=file_path, + disregard_skip=disregard_skip, + **config_kwargs, + ) if config.ignore_whitespace: line_separator = config.line_ending or parse._infer_line_separator(file_contents) compare_in = remove_whitespace(file_contents, line_separator=line_separator).strip() @@ -73,7 +111,13 @@ def check_imports(file_contents: str, show_diff: bool=False, extension: str = "p return False -def sorted_file(filename: str, config: Config=DEFAULT_CONFIG, **config_kwargs) -> str: +def sorted_file(filename: str, config: Config = DEFAULT_CONFIG, **config_kwargs) -> str: file_data = File.read(filename) config = _config(path=file_data.path.parent, config=config) - return sorted_contents(file_contents=file_data.contents, extension=file_data.extension, config=config, file_path=file_data.path, **config_kwargs) + return sorted_imports( + file_contents=file_data.contents, + extension=file_data.extension, + config=config, + file_path=file_data.path, + **config_kwargs, + ) diff --git a/isort/compat.py b/isort/compat.py index 0b92aed6c..3ff70f624 100644 --- a/isort/compat.py +++ b/isort/compat.py @@ -7,10 +7,9 @@ from warnings import warn from . import api, settings -from .exceptions import FileSkipped, ExistingSyntaxErrors, IntroducedSyntaxErrors +from .exceptions import ExistingSyntaxErrors, FileSkipped, IntroducedSyntaxErrors from .format import ask_whether_to_apply_changes_to_file, show_unified_diff from .io import File -from .isort import _SortImports from .settings import Config @@ -30,7 +29,7 @@ class SortImports: def __init__( self, - file_path: Optional[str] = None, + filename: Optional[str] = None, file_contents: str = "", write_to_stdout: bool = False, check: bool = False, @@ -39,15 +38,16 @@ def __init__( ask_to_apply: bool = False, run_path: str = "", check_skip: bool = True, - extension: Optional[str] = None, + extension: str = "", **setting_overrides: Any, ): file_encoding = "utf-8" - if file_path: + file_path: Optional[Path] = None + if filename: if file_contents: - file_data = File.from_contents(file_contents, filename=file_path) + file_data = File.from_contents(file_contents, filename=filename) else: - file_data = File.read(file_path) + file_data = File.read(filename) file_contents, file_path, file_encoding = file_data if not extension: extension = file_data.extension @@ -61,16 +61,24 @@ def __init__( try: if check: - self.incorrectly_sorted = not api.check_imports(file_contents, extension=extension, config=config, file_path=file_path, show_diff=show_diff) + self.incorrectly_sorted = not api.check_imports( + file_contents, + extension=extension, + config=config, + file_path=file_path, + show_diff=show_diff, + ) self.output = "" return else: - self.output = api.sorted_imports(file_contents, extension=extension, config=config, file_path=file_path) + self.output = api.sorted_imports( + file_contents, extension=extension, config=config, file_path=file_path + ) except FileSkipped as error: self.skipped = True - self.output = None + self.output = "" if config.verbose: - warn(error.message) + warn(str(error)) return except ExistingSyntaxErrors: warn("{file_path} unable to sort due to existing syntax errors") @@ -81,21 +89,6 @@ def __init__( self.output = file_contents return - if check: - check_output = self.output - check_against = file_contents - if config.ignore_whitespace: - check_output = self.sorted_imports.remove_whitespaces(check_output) - check_against = self.sorted_imports.remove_whitespaces(check_against) - - current_input_sorted_correctly = self.sorted_imports.check_if_input_already_sorted( - check_output, check_against, logging_file_path=str(file_path or "") - ) - if current_input_sorted_correctly: - return - else: - self.incorrectly_sorted = True - if show_diff: show_unified_diff( file_input=file_contents, file_output=self.output, file_path=file_path @@ -122,11 +115,3 @@ def __init__( print(f"Fixing {file_path}") output_file.write(self.output) - - @property - def sections(self): - return self.sorted_imports.parsed.sections - - @property - def length_change(self) -> int: - return self.sorted_imports.parsed.change_count diff --git a/isort/exceptions.py b/isort/exceptions.py index 02a4eb93a..4c512fd34 100644 --- a/isort/exceptions.py +++ b/isort/exceptions.py @@ -24,7 +24,7 @@ def __init__(self, file_path: Path, encoding: str, fallback_encoding: str): class ExistingSyntaxErrors(ISortError): """Raised when isort is told to sort imports within code that has existing syntax errors""" - def __init__(self, file_path: Path): + def __init__(self, file_path: str): super().__init__( f"isort was told to sort imports within code that contains syntax errors: " f"{file_path}." @@ -34,7 +34,7 @@ def __init__(self, file_path: Path): class IntroducedSyntaxErrors(ISortError): """Raised when isort has introduced a syntax error in the process of sorting imports""" - def __init__(self, file_path: Path): + def __init__(self, file_path: str): super().__init__( f"isort introduced syntax errors when attempting to sort the imports contained within " f"{file_path}." @@ -44,7 +44,7 @@ def __init__(self, file_path: Path): class FileSkipped(ISortError): """Should be raised when a file is skipped for any reason""" - def __init__(self, message: str, file_path: Path): + def __init__(self, message: str, file_path: str): super().__init__(message) self.file_path = file_path @@ -52,13 +52,19 @@ def __init__(self, message: str, file_path: Path): class FileSkipComment(FileSkipped): """Raised when an entire file is skipped due to a isort skip file comment""" - def __init__(self, file_path: Path): - super().__init__(f"{file_path} contains an {FILE_SKIP_COMMENT} comment and was skipped.", file_path=file_path) + def __init__(self, file_path: str): + super().__init__( + f"{file_path} contains an {FILE_SKIP_COMMENT} comment and was skipped.", + file_path=file_path, + ) class FileSkipSetting(FileSkipped): """Raised when an entire file is skipped due to provided isort settings""" - def __init__(self, file_path: Path): - super().__init__(f"{file_path} was skipped as it's listed in 'skip' setting" - " or matches a glob in 'skip_glob' setting", file_path=file_path) + def __init__(self, file_path: str): + super().__init__( + f"{file_path} was skipped as it's listed in 'skip' setting" + " or matches a glob in 'skip_glob' setting", + file_path=file_path, + ) diff --git a/isort/finders.py b/isort/finders.py index 2cd2e37ed..d63cc94ba 100644 --- a/isort/finders.py +++ b/isort/finders.py @@ -10,12 +10,23 @@ from fnmatch import fnmatch from functools import lru_cache from glob import glob -from typing import (Any, Dict, Iterable, Iterator, List, Mapping, - Optional, Pattern, Sequence, Tuple, Type) +from typing import ( + Any, + Dict, + Iterable, + Iterator, + List, + Mapping, + Optional, + Pattern, + Sequence, + Tuple, + Type, +) +from . import sections from .settings import DEFAULT_CONFIG, Config from .utils import chdir, exists_case_sensitive -from . import sections try: from pipreqs import pipreqs @@ -77,7 +88,9 @@ def __init__(self, config: Config) -> None: for placement in reversed(config.sections): known_placement = KNOWN_SECTION_MAPPING.get(placement, placement).lower() config_key = f"known_{known_placement}" - known_patterns = list(getattr(self.config, config_key, self.config.known_other.get(known_placement, []))) + known_patterns = list( + getattr(self.config, config_key, self.config.known_other.get(known_placement, [])) + ) known_patterns = [ pattern for known_pattern in known_patterns @@ -361,9 +374,7 @@ class FindersManager: ) def __init__( - self, - config: Config, - finder_classes: Optional[Iterable[Type[BaseFinder]]] = None, + self, config: Config, finder_classes: Optional[Iterable[Type[BaseFinder]]] = None ) -> None: self.verbose: bool = config.verbose diff --git a/isort/format.py b/isort/format.py index a03bea5da..f98b10ef7 100644 --- a/isort/format.py +++ b/isort/format.py @@ -58,8 +58,6 @@ def ask_whether_to_apply_changes_to_file(file_path: str) -> bool: return True -def remove_whitespace(content: str, line_separator: str="\n") -> str: - content = ( - content.replace(line_separator, "").replace(" ", "").replace("\x0c", "") - ) +def remove_whitespace(content: str, line_separator: str = "\n") -> str: + content = content.replace(line_separator, "").replace(" ", "").replace("\x0c", "") return content diff --git a/isort/io.py b/isort/io.py index 4ad5dd1cb..5845e9c1e 100644 --- a/isort/io.py +++ b/isort/io.py @@ -1,8 +1,11 @@ """Defines any IO utilities used by isort""" +import locale import re from pathlib import Path from typing import NamedTuple, Optional, Tuple +from .exceptions import UnableToDetermineEncoding + _ENCODING_PATTERN = re.compile(br"^[ \t\f]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)") @@ -19,7 +22,9 @@ def read(filename: str) -> "File": @staticmethod def from_contents(contents: str, filename: str) -> "File": - return File(contents, path=Path(filename).resolve(), encoding=_determine_content_encoding(contents)) + return File( + contents, path=Path(filename).resolve(), encoding=_determine_content_encoding(contents) + ) @property def extension(self): @@ -47,9 +52,7 @@ def _determine_file_encoding(file_path: Path, default: str = "utf-8") -> str: return _determine_stream_encoding(open_file, default=default) -def _read_file_contents( - file_path: Path -) -> Tuple[Optional[str], Optional[str]]: +def _read_file_contents(file_path: Path) -> Tuple[str, str]: encoding = _determine_file_encoding(file_path) with file_path.open(encoding=encoding, newline="") as file_to_import_sort: try: @@ -59,7 +62,7 @@ def _read_file_contents( pass # Try default encoding for open(mode='r') on the system - fallback_encoding = _locale.getpreferredencoding(False) + fallback_encoding = locale.getpreferredencoding(False) with file_path.open(encoding=fallback_encoding, newline="") as file_to_import_sort: try: file_contents = file_to_import_sort.read() diff --git a/isort/isort.py b/isort/isort.py index d71d29669..30e5a5c9d 100644 --- a/isort/isort.py +++ b/isort/isort.py @@ -1,56 +1 @@ -"""Exposes a simple library to sort through imports within Python code - -usage: - SortImports(file_name) -or: - sorted = SortImports(file_contents=file_contents).output -""" -# isort:skip_file -import copy -import itertools -import re -from collections import OrderedDict, defaultdict, namedtuple -from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple - -from isort import utils - -from . import output, parse, settings, sorting, wrap - - -class _SortImports: - def __init__(self, file_contents: str, config: Dict[str, Any], extension: str = "py") -> None: - self.config = config - - self.parsed = parse.file_contents(file_contents, config=self.config) - self.output = output.sorted_imports(self.parsed, self.config, extension) - - def remove_whitespaces(self, contents: str) -> str: - contents = ( - contents.replace(self.parsed.line_separator, "").replace(" ", "").replace("\x0c", "") - ) - return contents - - def get_out_lines_without_top_comment(self) -> str: - return self._strip_top_comments(self.out_lines, self.parsed.line_separator) - - def get_in_lines_without_top_comment(self) -> str: - return self._strip_top_comments(self.parsed.in_lines, self.parsed.line_separator) - - def check_if_input_already_sorted( - self, output: str, check_against: str, *, logging_file_path: str - ) -> bool: - if output.strip() == check_against.strip(): - if self.config["verbose"]: - print(f"SUCCESS: {logging_file_path} Everything Looks Good!") - return True - - print(f"ERROR: {logging_file_path} Imports are incorrectly sorted.") - return False - - @staticmethod - def _strip_top_comments(lines: Sequence[str], line_separator: str) -> str: - """Strips # comments that exist at the top of the given lines""" - lines = copy.copy(lines) - while lines and lines[0].startswith("#"): - lines = lines[1:] - return line_separator.join(lines) +from .compat import SortImports diff --git a/isort/main.py b/isort/main.py index ef82beadc..1616f9c5d 100644 --- a/isort/main.py +++ b/isort/main.py @@ -13,15 +13,8 @@ from isort import SortImports, __version__ from isort.logo import ASCII_ART -from isort.settings import ( - VALID_PY_TARGETS, - WrapModes, - Config - # default, - # file_should_be_skipped, - # from_path, - # wrap_mode_from_string, -) +from isort.settings import DEFAULT_CONFIG, VALID_PY_TARGETS, Config, WrapModes + from . import sections shebang_re = re.compile(br"^#!.*\bpython[23w]?\b") @@ -62,9 +55,7 @@ def sort_imports(file_name: str, **arguments: Any) -> Optional[SortAttempt]: return None -def iter_source_code( - paths: Iterable[str], config: Config, skipped: List[str] -) -> Iterator[str]: +def iter_source_code(paths: Iterable[str], config: Config, skipped: List[str]) -> Iterator[str]: """Iterate over all Python source files defined in paths.""" for path in paths: if os.path.isdir(path): @@ -94,14 +85,14 @@ class ISortCommand(setuptools.Command): user_options: List[Any] = [] def initialize_options(self) -> None: - default_settings = default.copy() + default_settings = vars(DEFAULT_CONFIG).copy() for key, value in default_settings.items(): setattr(self, key, value) def finalize_options(self) -> None: "Get options from config files." self.arguments: Dict[str, Any] = {} - computed_settings = from_path(os.getcwd()) + computed_settings = vars(Config(directory=os.getcwd())) for key, value in computed_settings.items(): self.arguments[key] = value @@ -319,8 +310,9 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: "-m", "--multi-line", dest="multi_line_output", - choices=WrapModes.__members__, - type=WrapModes.__getitem__, + choices=list(WrapModes.__members__.keys()) + + [str(mode.value) for mode in WrapModes.__members__.values()], + type=str, help="Multi line output (0-grid, 1-vertical, 2-hanging, 3-vert-hanging, 4-vert-grid, " "5-vert-grid-grouped, 6-vert-grid-grouped-no-comma).", ) @@ -537,6 +529,12 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: arguments = {key: value for key, value in vars(parser.parse_args(argv)).items() if value} if "dont_order_by_type" in arguments: arguments["order_by_type"] = False + multi_line_output = arguments.get("multi_line_output", None) + if multi_line_output: + if multi_line_output.isdigit(): + arguments["multi_line_output"] = WrapModes(int(multi_line_output)) + else: + arguments["multi_line_output"] = WrapModes[multi_line_output] return arguments diff --git a/isort/output.py b/isort/output.py index 9161cfdf4..fe58466b8 100644 --- a/isort/output.py +++ b/isort/output.py @@ -439,27 +439,28 @@ def _with_from_imports( len(import_statement) > config.line_length and len(from_import_section) > 0 and config.multi_line_output - not in (wrap.Modes.GRID, wrap.Modes.VERTICAL) # type: ignore # type: ignore + not in (wrap.Modes.GRID, wrap.Modes.VERTICAL) # type: ignore ): do_multiline_reformat = True if do_multiline_reformat: import_statement = wrap.import_statement( - import_start=import_start, from_imports=from_import_section, comments=comments, line_separator=parsed.line_separator, config=config + import_start=import_start, + from_imports=from_import_section, + comments=comments, + line_separator=parsed.line_separator, + config=config, ) - if config.multi_line_output == wrap.Modes.GRID: # type: ignore # type: ignore + if config.multi_line_output == wrap.Modes.GRID: # type: ignore other_import_statement = wrap.import_statement( import_start=import_start, from_imports=from_import_section, comments=comments, line_separator=parsed.line_separator, config=config, - multi_line_output=wrap.Modes.VERTICAL_GRID + multi_line_output=wrap.Modes.VERTICAL_GRID, # type: ignore ) - if ( - max(len(x) for x in import_statement.split("\n")) - > config.line_length - ): + if max(len(x) for x in import_statement.split("\n")) > config.line_length: import_statement = other_import_statement if not do_multiline_reformat and len(import_statement) > config.line_length: import_statement = wrap.line(import_statement, parsed.line_separator, config) diff --git a/isort/settings.py b/isort/settings.py index 17a74ceb5..5ac7938d2 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -13,7 +13,6 @@ import re import sys import warnings -from pathlib import Path from distutils.util import strtobool as _as_bool from functools import lru_cache from pathlib import Path @@ -21,18 +20,19 @@ Any, Callable, Dict, + FrozenSet, Iterable, List, Mapping, MutableMapping, Optional, + Set, Tuple, Union, - FrozenSet, ) from warnings import warn -from . import stdlibs, sections +from . import sections, stdlibs from ._future import dataclass, field from .utils import difference, union from .wrap_modes import WrapModes @@ -51,7 +51,7 @@ except ImportError: appdirs = None -FILE_SKIP_COMMENT: str = ("isort:" + "skip_file") # Concatenated to avoid this file being skipped +FILE_SKIP_COMMENT: str = ("isort:" + "skip_file") # Concatenated to avoid this file being skipped MAX_CONFIG_SEARCH_DEPTH: int = 25 # The number of parent directories to for a config file within STOP_CONFIG_SEARCH_ON_DIRS: Tuple[str, ...] = (".git", ".hg") VALID_PY_TARGETS: Tuple[str, ...] = tuple( @@ -92,9 +92,27 @@ class _Config: NOTE: known lists, such as known_standard_library, are intentionally not complete as they are dynamically determined later on. """ + py_version: str = "3" force_to_top: FrozenSet[str] = frozenset() - skip: FrozenSet[str] = frozenset({".venv", "venv", ".tox", ".eggs", ".git", ".hg", ".mypy_cache", ".nox", "_build", "buck-out", "build", "dist", ".pants.d", "node_modules"}) + skip: FrozenSet[str] = frozenset( + { + ".venv", + "venv", + ".tox", + ".eggs", + ".git", + ".hg", + ".mypy_cache", + ".nox", + "_build", + "buck-out", + "build", + "dist", + ".pants.d", + "node_modules", + } + ) skip_glob: FrozenSet[str] = frozenset() line_length: int = 79 wrap_length: int = 0 @@ -172,9 +190,7 @@ def __post_init__(self): if not self.known_standard_library: object.__setattr__( - self, - "known_standard_library", - frozenset(getattr(stdlibs, self.py_version).stdlib), + self, "known_standard_library", frozenset(getattr(stdlibs, self.py_version).stdlib) ) if self.force_alphabetical_sort: @@ -219,15 +235,19 @@ def __init__(self, settings_file: str = "", settings_path: str = "", **config_ov indent = "\t" combined_config["indent"] = indent - known_other = {} import_headings = {} for key, value in combined_config.items(): # Collect all known sections beyond those that have direct entries - if key.startswith(KNOWN_PREFIX) and key not in ("known_standard_library", "known_future_library", "known_third_party", "known_first_party"): - known_other[key[len(KNOWN_PREFIX):].lower()] = frozenset(value) + if key.startswith(KNOWN_PREFIX) and key not in ( + "known_standard_library", + "known_future_library", + "known_third_party", + "known_first_party", + ): + known_other[key[len(KNOWN_PREFIX) :].lower()] = frozenset(value) if key.startswith(IMPORT_HEADING_PREFIX): - import_headings[key[len(IMPORT_HEADING_PREFIX):].lower()] = str(value) + import_headings[key[len(IMPORT_HEADING_PREFIX) :].lower()] = str(value) # Coerce all provided config values into their correct type default_value = _DEFAULT_SETTINGS.get(key, None) @@ -237,7 +257,9 @@ def __init__(self, settings_file: str = "", settings_path: str = "", **config_ov combined_config[key] = type(default_value)(value) if "directory" not in combined_config: - combined_config["directory"] = os.path.basename(config_settings.get("source", None) or os.getcwd()) + combined_config["directory"] = os.path.basename( + config_settings.get("source", None) or os.getcwd() + ) # Remove any config values that are used for creating config object but aren't defined in dataclass combined_config.pop("source", None) @@ -250,7 +272,7 @@ def __init__(self, settings_file: str = "", settings_path: str = "", **config_ov combined_config.pop(f"{IMPORT_HEADING_PREFIX}{import_heading_key}") combined_config["import_headings"] = import_headings - super().__init__(sources=tuple(sources), **combined_config) + super().__init__(sources=tuple(sources), **combined_config) # type: ignore def is_skipped(self, file_path: Path) -> bool: """Returns True if the file and/or folder should be skipped based on current settings.""" @@ -303,7 +325,7 @@ def _as_list(value: str) -> List[str]: return filtered -def _abspaths(cwd: str, values: Iterable[str]) -> List[str]: +def _abspaths(cwd: str, values: Iterable[str]) -> Set[str]: paths = set( [ os.path.join(cwd, value) @@ -405,14 +427,16 @@ def _get_config_data(file_path: str, sections: Iterable[str]) -> Dict[str, Any]: settings["line_length"] = ( float("inf") if max_line_length == "off" else int(max_line_length) ) - settings = {key: value for key, value in settings.items() if key in _DEFAULT_SETTINGS.keys()} + settings = { + key: value for key, value in settings.items() if key in _DEFAULT_SETTINGS.keys() + } for key, value in settings.items(): existing_value_type = _get_str_to_type_converter(key) if existing_value_type == tuple: settings[key] = tuple(_as_list(value)) elif existing_value_type == frozenset: - settings[key] = frozenset(_as_list(settings.get(key))) + settings[key] = frozenset(_as_list(settings.get(key))) # type: ignore elif existing_value_type == bool: # Only some configuration formats support native boolean values. if not isinstance(value, bool): diff --git a/isort/wrap.py b/isort/wrap.py index fa7349ba5..db67f105f 100644 --- a/isort/wrap.py +++ b/isort/wrap.py @@ -12,8 +12,8 @@ def import_statement( from_imports: List[str], comments: Sequence[str], line_separator: str, - config: Config=DEFAULT_CONFIG, - multi_line_output: Optional[Modes]=None + config: Config = DEFAULT_CONFIG, + multi_line_output: Optional[Modes] = None, ) -> str: """Returns a multi-line wrapped form of the provided from import statement.""" formatter = formatter_from_string((multi_line_output or config.multi_line_output).name) @@ -61,7 +61,7 @@ def import_statement( return import_statement -def line(line: str, line_separator: str, config: Config=DEFAULT_CONFIG) -> str: +def line(line: str, line_separator: str, config: Config = DEFAULT_CONFIG) -> str: """Returns a line wrapped to the specified line-length, if possible.""" wrap_mode = config.multi_line_output if len(line) > config.line_length and wrap_mode != Modes.NOQA: # type: ignore @@ -79,9 +79,7 @@ def line(line: str, line_separator: str, config: Config=DEFAULT_CONFIG) -> str: _comma_maybe = "," if config.include_trailing_comma else "" line_parts[-1] = f"{line_parts[-1].strip()}{_comma_maybe} #{comment}" next_line = [] - while (len(line) + 2) > ( - config.wrap_length or config.line_length - ) and line_parts: + while (len(line) + 2) > (config.wrap_length or config.line_length) and line_parts: next_line.append(line_parts.pop()) line = splitter.join(line_parts) if not line: diff --git a/tests/test_isort.py b/tests/test_isort.py index b65140c8b..8424ed6a9 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -486,7 +486,7 @@ def test_length_sort_section() -> None: "import looooooooooooooooooooooooooooooooooooooong\n" "import medium_sizeeeeeeeeeeeeea\n" ) - test_output = SortImports(file_contents=test_input, length_sort_sections=("stdlib", )).output + test_output = SortImports(file_contents=test_input, length_sort_sections=("stdlib",)).output assert test_output == ( "import os\n" "import sys\n" @@ -1217,7 +1217,9 @@ def test_smart_lines_after_import_section() -> None: def test_settings_overwrite() -> None: """Test to ensure settings overwrite instead of trying to combine.""" - assert Config(known_standard_library=["not_std_library"]).known_standard_library == frozenset({"not_std_library"}) + assert Config(known_standard_library=["not_std_library"]).known_standard_library == frozenset( + {"not_std_library"} + ) assert Config(known_first_party=["thread"]).known_first_party == frozenset({"thread"}) @@ -1574,7 +1576,9 @@ def test_long_line_comments() -> None: "sync_stage_envdir, " "update_stage_app, update_stage_cron # noqa\n" ) - assert SortImports(file_contents=test_input, line_length=100, balanced_wrapping=True).output == ( + assert SortImports( + file_contents=test_input, line_length=100, balanced_wrapping=True + ).output == ( "from foo.utils.fabric_stuff.live import (check_clean_live, deploy_live, # noqa\n" " sync_live_envdir, update_live_app, " "update_live_cron)\n" @@ -2681,7 +2685,10 @@ def test_no_extra_lines_issue_557() -> None: ) assert ( SortImports( - file_contents=test_input, force_alphabetical_sort=True, force_sort_within_sections=True, line_length=100 + file_contents=test_input, + force_alphabetical_sort=True, + force_sort_within_sections=True, + line_length=100, ).output == expected_output ) @@ -3087,7 +3094,7 @@ def test_monkey_patched_urllib() -> None: def test_path_finder(monkeypatch) -> None: - config = config=Config() + config = config = Config() finder = finders.PathFinder(config=config) third_party_prefix = next(path for path in finder.paths if "site-packages" in path) ext_suffixes = importlib.machinery.EXTENSION_SUFFIXES From ae4c17fcf102fffc1137323069951c06edd28995 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 8 Nov 2019 00:54:16 -0800 Subject: [PATCH 0217/1439] Flake8 fixes --- isort/api.py | 4 ++-- isort/compat.py | 2 +- isort/main.py | 5 +++-- isort/settings.py | 12 ++++++------ tests/test_isort.py | 18 +++++++++--------- 5 files changed, 21 insertions(+), 20 deletions(-) diff --git a/isort/api.py b/isort/api.py index 22802a97e..25dc2be31 100644 --- a/isort/api.py +++ b/isort/api.py @@ -21,8 +21,8 @@ def _config( if path: if ( config is DEFAULT_CONFIG - and not "settings_path" in config_kwargs - and not "settings_file" in config_kwargs + and "settings_path" not in config_kwargs + and "settings_file" not in config_kwargs ): config_kwargs["settings_path"] = path diff --git a/isort/compat.py b/isort/compat.py index 3ff70f624..9bc5aea83 100644 --- a/isort/compat.py +++ b/isort/compat.py @@ -54,7 +54,7 @@ def __init__( if settings_path: setting_overrides["settings_path"] = settings_path - elif file_path and not "settings_file" in setting_overrides: + elif file_path and "settings_file" not in setting_overrides: setting_overrides["settings_path"] = file_path.parent config = Config(**setting_overrides) diff --git a/isort/main.py b/isort/main.py index 1616f9c5d..2d47f79b5 100644 --- a/isort/main.py +++ b/isort/main.py @@ -525,7 +525,6 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: f"(currently: {sys.version_info.major}{sys.version_info.minor}) will be used.", ) - values = vars(parser.parse_args(argv)) arguments = {key: value for key, value in vars(parser.parse_args(argv)).items() if value} if "dont_order_by_type" in arguments: arguments["order_by_type"] = False @@ -568,8 +567,10 @@ def main(argv: Optional[Sequence[str]] = None) -> None: if not arguments.get("apply", False): arguments["ask_to_apply"] = True - if not "settings_path" in arguments: + if "settings_path" not in arguments: arguments["settings_path"] = os.path.abspath(file_names[0]) or os.getcwd() + if not os.path.isdir(arguments["settings_path"]): + arguments["settings_path"] = os.path.basename(arguments["settings_path"]) config_dict = arguments.copy() config_dict.pop("recursive", False) diff --git a/isort/settings.py b/isort/settings.py index 5ac7938d2..b802b2ae9 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -32,8 +32,9 @@ ) from warnings import warn -from . import sections, stdlibs +from . import stdlibs from ._future import dataclass, field +from .sections import DEFAULT as SECTION_DEFAULTS from .utils import difference, union from .wrap_modes import WrapModes from .wrap_modes import from_string as wrap_mode_from_string @@ -117,7 +118,7 @@ class _Config: line_length: int = 79 wrap_length: int = 0 line_ending: str = "" - sections: Tuple[str, ...] = sections.DEFAULT + sections: Tuple[str, ...] = SECTION_DEFAULTS no_sections: bool = False known_future_library: FrozenSet[str] = frozenset(("__future__",)) known_third_party: FrozenSet[str] = frozenset(("google.appengine.api",)) @@ -209,7 +210,7 @@ def __init__(self, settings_file: str = "", settings_path: str = "", **config_ov config_settings: Dict[str, Any] if settings_file: - config_settings = config_data = _get_config_data( + config_settings = _get_config_data( settings_file, CONFIG_SECTIONS.get(os.path.basename(settings_file), FALLBACK_CONFIG_SECTIONS), ) @@ -261,7 +262,8 @@ def __init__(self, settings_file: str = "", settings_path: str = "", **config_ov config_settings.get("source", None) or os.getcwd() ) - # Remove any config values that are used for creating config object but aren't defined in dataclass + # Remove any config values that are used for creating config object but + # aren't defined in dataclass combined_config.pop("source", None) if known_other: for known_key in known_other.keys(): @@ -278,10 +280,8 @@ def is_skipped(self, file_path: Path) -> bool: """Returns True if the file and/or folder should be skipped based on current settings.""" if self.directory and Path(self.directory) in file_path.parents: file_name = os.path.relpath(file_path, self.directory) - path = self.directory else: file_name = str(file_path) - path = "" os_path = str(file_path) diff --git a/tests/test_isort.py b/tests/test_isort.py index 8424ed6a9..98b880406 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -14,7 +14,7 @@ import py import pytest -from isort import finders, main, settings, sections +from isort import finders, main, sections from isort.main import SortImports, is_python_file from isort.settings import WrapModes, Config from isort.utils import exists_case_sensitive @@ -661,10 +661,10 @@ def test_skip_with_file_name() -> None: test_input = "import django\nimport myproject\n" sort_imports = SortImports( - file_path="/baz.py", file_contents=test_input, settings_path=os.getcwd(), skip=["baz.py"] + filename="/baz.py", file_contents=test_input, settings_path=os.getcwd(), skip=["baz.py"] ) assert sort_imports.skipped - assert sort_imports.output is None + assert sort_imports.output == "" def test_skip_within_file() -> None: @@ -672,7 +672,7 @@ def test_skip_within_file() -> None: test_input = "# isort:skip_file\nimport django\nimport myproject\n" sort_imports = SortImports(file_contents=test_input, known_third_party=["django"]) assert sort_imports.skipped - assert sort_imports.output is None + assert sort_imports.output == "" def test_force_to_top() -> None: @@ -1952,7 +1952,7 @@ def test_other_file_encodings(tmpdir) -> None: file_contents = f"# coding: {encoding}\n\ns = u'ã'\n" tmp_fname.write_binary(file_contents.encode(encoding)) assert ( - SortImports(file_path=str(tmp_fname), settings_path=os.getcwd()).output == file_contents + SortImports(filename=str(tmp_fname), settings_path=os.getcwd()).output == file_contents ) @@ -1961,7 +1961,7 @@ def test_encoding_not_in_comment(tmpdir) -> None: tmp_fname = tmpdir.join("test_encoding.py") file_contents = "class Foo\n coding: latin1\n\ns = u'ã'\n" tmp_fname.write_binary(file_contents.encode("utf8")) - assert SortImports(file_path=str(tmp_fname), settings_path=os.getcwd()).output == file_contents + assert SortImports(filename=str(tmp_fname), settings_path=os.getcwd()).output == file_contents def test_encoding_not_in_first_two_lines(tmpdir) -> None: @@ -1969,7 +1969,7 @@ def test_encoding_not_in_first_two_lines(tmpdir) -> None: tmp_fname = tmpdir.join("test_encoding.py") file_contents = "\n\n# -*- coding: latin1\n\ns = u'ã'\n" tmp_fname.write_binary(file_contents.encode("utf8")) - assert SortImports(file_path=str(tmp_fname), settings_path=os.getcwd()).output == file_contents + assert SortImports(filename=str(tmp_fname), settings_path=os.getcwd()).output == file_contents def test_comment_at_top_of_file() -> None: @@ -4089,11 +4089,11 @@ def test_pyi_formatting_issue_942(tmpdir) -> None: source_py = tmpdir.join("source.py") source_py.write(test_input) - assert SortImports(file_path=str(source_py)).output.splitlines() == expected_py_output + assert SortImports(filename=str(source_py)).output.splitlines() == expected_py_output source_pyi = tmpdir.join("source.pyi") source_pyi.write(test_input) - assert SortImports(file_path=str(source_pyi)).output.splitlines() == expected_pyi_output + assert SortImports(filename=str(source_pyi)).output.splitlines() == expected_pyi_output def test_move_class_issue_751() -> None: From e38174e97be6650ebbea873e0f54e41ad3f714a9 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 8 Nov 2019 23:35:51 -0800 Subject: [PATCH 0218/1439] Add explicit lexicographical sorting support --- isort/output.py | 1 + isort/settings.py | 1 + isort/sorting.py | 10 ++++++++-- tests/test_isort.py | 14 ++++++++++++++ 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/isort/output.py b/isort/output.py index fe58466b8..846dfc4bd 100644 --- a/isort/output.py +++ b/isort/output.py @@ -93,6 +93,7 @@ def sorted_imports( sorting.section_key, order_by_type=config.order_by_type, force_to_top=config.force_to_top, + lexicographical=config.lexicographical, ), ) diff --git a/isort/settings.py b/isort/settings.py index b802b2ae9..48934b746 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -156,6 +156,7 @@ class _Config: force_alphabetical_sort: bool = False force_grid_wrap: int = 0 force_sort_within_sections: bool = False + lexicographical: bool = False ignore_whitespace: bool = False no_lines_before: FrozenSet[str] = frozenset() no_inline_sort: bool = False diff --git a/isort/sorting.py b/isort/sorting.py index b45bbf517..e3aa277ab 100644 --- a/isort/sorting.py +++ b/isort/sorting.py @@ -40,10 +40,16 @@ def module_key( return f"{module_name in config.force_to_top and 'A' or 'B'}{prefix}{_length_sort_maybe}" -def section_key(line: str, order_by_type: bool, force_to_top: List[str]) -> str: +def section_key( + line: str, order_by_type: bool, force_to_top: List[str], lexicographical: bool = False +) -> str: section = "B" - line = _import_line_intro_re.sub("", _import_line_midline_import_re.sub(".", line)) + if lexicographical: + line = _import_line_intro_re.sub("", _import_line_midline_import_re.sub(".", line)) + else: + line = re.sub("^from ", "", line) + line = re.sub("^import ", "", line) if line.split(" ")[0] in force_to_top: section = "A" if not order_by_type: diff --git a/tests/test_isort.py b/tests/test_isort.py index 98b880406..b4d5b5d38 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -994,6 +994,20 @@ def test_force_single_line_imports_and_sort_within_sections() -> None: force_single_line=True, force_sort_within_sections=True, ).output + assert test_output == ( + "from third_party import lib_a\n" + "from third_party import lib_b\n" + "from third_party import lib_d\n" + "from third_party.lib_c import lib1\n" + ) + test_output = SortImports( + file_contents=test_input, + multi_line_output=WrapModes.GRID, + line_length=40, + force_single_line=True, + force_sort_within_sections=True, + lexicographical=True, + ).output assert test_output == ( "from third_party import lib_a\n" "from third_party import lib_b\n" From 6f97ca7b603fa26bcccb04d4b8b375a15abac9d8 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 9 Nov 2019 00:23:26 -0800 Subject: [PATCH 0219/1439] Add initial profile support --- isort/exceptions.py | 15 ++++++++++++--- isort/main.py | 16 +++++++++++----- isort/settings.py | 16 +++++++++++++++- 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/isort/exceptions.py b/isort/exceptions.py index 4c512fd34..b62b2c5d9 100644 --- a/isort/exceptions.py +++ b/isort/exceptions.py @@ -1,7 +1,7 @@ """All isort specific exception classes should be defined here""" from pathlib import Path -from .settings import FILE_SKIP_COMMENT +from .profiles import profiles class ISortError(Exception): @@ -54,8 +54,7 @@ class FileSkipComment(FileSkipped): def __init__(self, file_path: str): super().__init__( - f"{file_path} contains an {FILE_SKIP_COMMENT} comment and was skipped.", - file_path=file_path, + f"{file_path} contains an file skip comment and was skipped.", file_path=file_path ) @@ -68,3 +67,13 @@ def __init__(self, file_path: str): " or matches a glob in 'skip_glob' setting", file_path=file_path, ) + + +class ProfileDoesNotExist(ISortError): + """Raised when a profile is set by the user that doesn't exist""" + + def __init__(self, profile: str): + super().__init__( + f"Specified profile of {profile} does not exist. " + f"Available profiles: {','.join(profiles)}." + ) diff --git a/isort/main.py b/isort/main.py index 2d47f79b5..a8e17193c 100644 --- a/isort/main.py +++ b/isort/main.py @@ -11,11 +11,10 @@ import setuptools -from isort import SortImports, __version__ -from isort.logo import ASCII_ART -from isort.settings import DEFAULT_CONFIG, VALID_PY_TARGETS, Config, WrapModes - -from . import sections +from . import SortImports, __version__, sections +from .logo import ASCII_ART +from .profiles import profiles +from .settings import DEFAULT_CONFIG, VALID_PY_TARGETS, Config, WrapModes shebang_re = re.compile(br"^#!.*\bpython[23w]?\b") @@ -524,6 +523,13 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: "interpreter used to run isort " f"(currently: {sys.version_info.major}{sys.version_info.minor}) will be used.", ) + parser.add_argument( + "--profile", + dest="profile", + choices=list(profiles.keys()), + type=str, + help="Base profile type to use for configuration.", + ) arguments = {key: value for key, value in vars(parser.parse_args(argv)).items() if value} if "dont_order_by_type" in arguments: diff --git a/isort/settings.py b/isort/settings.py index 48934b746..709f230ca 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -34,6 +34,8 @@ from . import stdlibs from ._future import dataclass, field +from .exceptions import ProfileDoesNotExist +from .profiles import profiles from .sections import DEFAULT as SECTION_DEFAULTS from .utils import difference, union from .wrap_modes import WrapModes @@ -167,6 +169,7 @@ class _Config: conda_env: str = "" ensure_newline_before_comments: bool = False directory: str = "" + profile: str = "" def __post_init__(self): py_version = self.py_version @@ -220,13 +223,24 @@ def __init__(self, settings_file: str = "", settings_path: str = "", **config_ov else: config_settings = {} + profile_name = config_overrides.get("profile", config_settings.get("profile", "")) + if profile_name: + if profile_name not in profiles: + raise ProfileDoesNotExist(profile) + + profile = profiles[profile_name].copy() + profile["source"] = f"{profile_name} profile" + sources.append(profile) + else: + profile = {} + if config_settings: sources.append(config_settings) if config_overrides: config_overrides["source"] = "runtime" sources.append(config_overrides) - combined_config = {**config_settings, **config_overrides} + combined_config = {**profile, **config_settings, **config_overrides} if "indent" in combined_config: indent = str(combined_config["indent"]) if indent.isdigit(): From 5fccf078c654b69344ded47c2f4c7abddbd52c4d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 9 Nov 2019 00:27:51 -0800 Subject: [PATCH 0220/1439] Add initial profiles --- isort/profiles.py | 55 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 isort/profiles.py diff --git a/isort/profiles.py b/isort/profiles.py new file mode 100644 index 000000000..270b458d2 --- /dev/null +++ b/isort/profiles.py @@ -0,0 +1,55 @@ +"""Common profiles are defined here to be easily used within a project using --profile {name}""" + +black = { + "multi_line_output": 3, + "include_trailing_comma": True, + "force_grid_wrap": 0, + "use_parentheses": True, +} +django = { + "combine_as_imports": True, + "include_trailing_comma": True, + "multi_line_output": 5, + "line_length": 79, +} +pycharm = {"multi_line_output": 3, "force_grid_wrap": 2} +google = {"force_single_line": True, "force_sort_within_sections": True, "lexicographical": True} +open_stack = { + "force_single_line": True, + "force_sort_within_sections": True, + "lexicographical": True, +} +plone = { + "force_alphabetical_sort": True, + "force_single_line": True, + "ines_after_imports": 2, + "line_length": 200, +} +attrs = { + "atomic": True, + "force_grid_wrap": 0, + "include_trailing_comma": True, + "lines_after_imports": 2, + "lines_between_types": 1, + "multi_line_output": 3, + "not_skip": "__init__.py", + "use_parentheses": True, +} +hug = { + "multi_line_output": 3, + "include_trailing_comma": True, + "force_grid_wrap": 0, + "use_parentheses": True, + "line_length": 100, +} + +profiles = { + "black": black, + "django": django, + "pycharm": pycharm, + "google": google, + "open_stack": open_stack, + "plone": plone, + "attrs": attrs, + "hug": hug, +} From aa72b8226b23247fdd2b2f4ac91244739ff4dea8 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 9 Nov 2019 00:46:06 -0800 Subject: [PATCH 0221/1439] Fix typing error --- isort/profiles.py | 3 ++- isort/settings.py | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/isort/profiles.py b/isort/profiles.py index 270b458d2..669c82773 100644 --- a/isort/profiles.py +++ b/isort/profiles.py @@ -1,4 +1,5 @@ """Common profiles are defined here to be easily used within a project using --profile {name}""" +from typing import Any, Dict black = { "multi_line_output": 3, @@ -43,7 +44,7 @@ "line_length": 100, } -profiles = { +profiles: Dict[str, Dict[str, Any]] = { "black": black, "django": django, "pycharm": pycharm, diff --git a/isort/settings.py b/isort/settings.py index 709f230ca..607c67b6e 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -224,15 +224,14 @@ def __init__(self, settings_file: str = "", settings_path: str = "", **config_ov config_settings = {} profile_name = config_overrides.get("profile", config_settings.get("profile", "")) + profile: Dict[str, Any] = {} if profile_name: if profile_name not in profiles: - raise ProfileDoesNotExist(profile) + raise ProfileDoesNotExist(profile_name) profile = profiles[profile_name].copy() profile["source"] = f"{profile_name} profile" sources.append(profile) - else: - profile = {} if config_settings: sources.append(config_settings) From 6048f836c0366454bdc484c131083c4fe27c6bcb Mon Sep 17 00:00:00 2001 From: Hugo Date: Sat, 9 Nov 2019 21:36:34 +0200 Subject: [PATCH 0222/1439] Test on Python 3.8 --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 3e6fbda25..3e729d36e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,5 +1,5 @@ init: - - "SET PATH=C:\\Python37\\Scripts;%PATH%" + - "SET PATH=C:\\Python38\\Scripts;%PATH%" - "SET PYTEST_ADDOPTS=-vv" - "ECHO PATH=%PATH%" From e695432562c351ec6ae039dcd17ddbe44289f242 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sat, 9 Nov 2019 21:42:04 +0200 Subject: [PATCH 0223/1439] Hide with other dotfiles --- appveyor.yml => .appveyor.yml | 0 codecov.yml => .codecov.yml | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename appveyor.yml => .appveyor.yml (100%) rename codecov.yml => .codecov.yml (100%) diff --git a/appveyor.yml b/.appveyor.yml similarity index 100% rename from appveyor.yml rename to .appveyor.yml diff --git a/codecov.yml b/.codecov.yml similarity index 100% rename from codecov.yml rename to .codecov.yml From 6109d4484d3b2caf9ca4deb86ac453a7ccc38cda Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 9 Nov 2019 16:38:52 -0800 Subject: [PATCH 0224/1439] Fix ask_to_apply --- isort/main.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/isort/main.py b/isort/main.py index a8e17193c..3530ffd9c 100644 --- a/isort/main.py +++ b/isort/main.py @@ -580,7 +580,7 @@ def main(argv: Optional[Sequence[str]] = None) -> None: config_dict = arguments.copy() config_dict.pop("recursive", False) - config_dict.pop("ask_to_apply", False) + ask_to_apply = config_dict.pop("ask_to_apply", False) jobs = config_dict.pop("jobs", ()) show_logo = config_dict.pop("show_logo", False) filter_files = config_dict.pop("filter_files", False) @@ -610,12 +610,12 @@ def main(argv: Optional[Sequence[str]] = None) -> None: executor = multiprocessing.Pool(jobs) attempt_iterator = executor.imap( - functools.partial(sort_imports, check=check, **config_dict), file_names + functools.partial(sort_imports, check=check, ask_to_apply=ask_to_apply, **config_dict), file_names ) else: # https://github.com/python/typeshed/pull/2814 attempt_iterator = ( # type: ignore - sort_imports(file_name, check=check, **config_dict) for file_name in file_names + sort_imports(file_name, check=check, ask_to_apply=ask_to_apply, **config_dict) for file_name in file_names ) for sort_attempt in attempt_iterator: From 6209700b5dbc41735ce433047e1ecadd0ce46961 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 9 Nov 2019 23:29:17 -0800 Subject: [PATCH 0225/1439] Improve behaviour when no options or file names are passed in. Add new interactive flag --- CHANGELOG.md | 4 ++++ isort/main.py | 57 ++++++++++++++++++++++++++++----------------------- 2 files changed, 35 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff735fd19..cf758729b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ Changelog - `length_sort_{section_name}` config usage has been deprecated. Instead `length_sort_sections` list can be used to specify a list of sections that need to be length sorted. - `safety_excludes` and `unsafe` have been deprecated - Config now includes as default full set of safety directories defined by safety excludes. + - `--recursive` option has been removed. Directories passed in are now automatically sorted recursive. + - `--apply` option has been removed as it is the default behaviour. + - isort now does nothing, beyond giving instructions and exiting status code 0, when ran with no arguments. + - a new `--interactive` flag has been added to enable the old style behaviour. Internal: - isort now utilizes mypy and typing to filter out typing related issues before deployment. diff --git a/isort/main.py b/isort/main.py index 3530ffd9c..000b47482 100644 --- a/isort/main.py +++ b/isort/main.py @@ -17,7 +17,20 @@ from .settings import DEFAULT_CONFIG, VALID_PY_TARGETS, Config, WrapModes shebang_re = re.compile(br"^#!.*\bpython[23w]?\b") +QUICK_GUIDE = f""" +{ASCII_ART} +Nothing to do: no files or paths have have been passed in! + +Try one of the following: + + `isort .` - sort all Python files, starting from the current directory, recursively. + `isort . --interactive` - Do the same, but ask before making any changes. + `isort . --check --diff` - Check to see if imports are correctly sorted within this project. + `isort --help` - In-depth information about isort's available command-line options. + +Visit https://timothycrosley.github.io/isort/ for complete information about how to use isort. +""" def is_python_file(path: str) -> bool: _root, ext = os.path.splitext(path) @@ -379,13 +392,6 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: 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", @@ -488,13 +494,6 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: dest="ignore_whitespace", help="Tells isort to ignore whitespace differences when --check-only is being used.", ) - parser.add_argument( - "-y", - "--apply", - dest="apply", - action="store_true", - help="Tells isort to apply changes recursively without asking", - ) parser.add_argument( "--case-sensitive", dest="case_sensitive", @@ -530,6 +529,12 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: type=str, help="Base profile type to use for configuration.", ) + parser.add_argument( + "--interactive", + dest="ask_to_apply", + action="store_true", + help="Tells isort to apply changes interactively.", + ) arguments = {key: value for key, value in vars(parser.parse_args(argv)).items() if value} if "dont_order_by_type" in arguments: @@ -564,19 +569,16 @@ def main(argv: Optional[Sequence[str]] = None) -> None: warn(f"virtual_env dir does not exist: {arguments['virtual_env']}") file_names = arguments.pop("files", []) - if file_names == ["-"]: + if not file_names: + print(QUICK_GUIDE) + return + elif file_names == ["-"]: SortImports(file_contents=sys.stdin.read(), write_to_stdout=True, **arguments) else: - if not file_names: - file_names = ["."] - arguments["recursive"] = True - if not arguments.get("apply", False): - arguments["ask_to_apply"] = True - if "settings_path" not in arguments: arguments["settings_path"] = os.path.abspath(file_names[0]) or os.getcwd() if not os.path.isdir(arguments["settings_path"]): - arguments["settings_path"] = os.path.basename(arguments["settings_path"]) + arguments["settings_path"] = os.path.dirname(arguments["settings_path"]) config_dict = arguments.copy() config_dict.pop("recursive", False) @@ -599,8 +601,7 @@ def main(argv: Optional[Sequence[str]] = None) -> None: filtered_files.append(file_name) file_names = filtered_files - if arguments.get("recursive", False): - file_names = iter_source_code(file_names, config, skipped) + file_names = iter_source_code(file_names, config, skipped) num_skipped = 0 if config.verbose or show_logo: print(ASCII_ART) @@ -610,12 +611,16 @@ def main(argv: Optional[Sequence[str]] = None) -> None: executor = multiprocessing.Pool(jobs) attempt_iterator = executor.imap( - functools.partial(sort_imports, check=check, ask_to_apply=ask_to_apply, **config_dict), file_names + functools.partial( + sort_imports, check=check, ask_to_apply=ask_to_apply, **config_dict + ), + file_names, ) else: # https://github.com/python/typeshed/pull/2814 attempt_iterator = ( # type: ignore - sort_imports(file_name, check=check, ask_to_apply=ask_to_apply, **config_dict) for file_name in file_names + sort_imports(file_name, check=check, ask_to_apply=ask_to_apply, **config_dict) + for file_name in file_names ) for sort_attempt in attempt_iterator: From 4de01f5b1b98dca14fc4192fba7196cd4702d458 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 9 Nov 2019 23:36:53 -0800 Subject: [PATCH 0226/1439] Fix --diff command-line option --- isort/main.py | 17 ++++++++++++++--- scripts/clean.sh | 2 +- scripts/lint.sh | 2 +- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/isort/main.py b/isort/main.py index 000b47482..4447545ee 100644 --- a/isort/main.py +++ b/isort/main.py @@ -32,6 +32,7 @@ Visit https://timothycrosley.github.io/isort/ for complete information about how to use isort. """ + def is_python_file(path: str) -> bool: _root, ext = os.path.splitext(path) if ext in (".py", ".pyi"): @@ -581,12 +582,12 @@ def main(argv: Optional[Sequence[str]] = None) -> None: arguments["settings_path"] = os.path.dirname(arguments["settings_path"]) config_dict = arguments.copy() - config_dict.pop("recursive", False) ask_to_apply = config_dict.pop("ask_to_apply", False) jobs = config_dict.pop("jobs", ()) show_logo = config_dict.pop("show_logo", False) filter_files = config_dict.pop("filter_files", False) check = config_dict.pop("check", False) + show_diff = config_dict.pop("show_diff", False) config = Config(**config_dict) wrong_sorted_files = False @@ -612,14 +613,24 @@ def main(argv: Optional[Sequence[str]] = None) -> None: executor = multiprocessing.Pool(jobs) attempt_iterator = executor.imap( functools.partial( - sort_imports, check=check, ask_to_apply=ask_to_apply, **config_dict + sort_imports, + check=check, + ask_to_apply=ask_to_apply, + show_diff=show_diff, + **config_dict, ), file_names, ) else: # https://github.com/python/typeshed/pull/2814 attempt_iterator = ( # type: ignore - sort_imports(file_name, check=check, ask_to_apply=ask_to_apply, **config_dict) + sort_imports( + file_name, + check=check, + ask_to_apply=ask_to_apply, + show_diff=show_diff, + **config_dict, + ) for file_name in file_names ) diff --git a/scripts/clean.sh b/scripts/clean.sh index 45e5688c8..1a5b493e2 100755 --- a/scripts/clean.sh +++ b/scripts/clean.sh @@ -1,5 +1,5 @@ #!/bin/bash set -euxo pipefail -poetry run isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --use-parentheses --line-width=100 --recursive isort/ tests/ +poetry run isort --profile hug isort/ tests/ poetry run black isort/ tests/ -l 100 diff --git a/scripts/lint.sh b/scripts/lint.sh index 2198495df..d31f426c0 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -5,7 +5,7 @@ set -euxo pipefail poetry run cruft check poetry run mypy --ignore-missing-imports isort/ poetry run black --check -l 100 isort/ tests/ -# poetry run isort --multi-line=3 --trailing-comma --force-grid-wrap=0 --use-parentheses --line-width=100 --recursive --check --diff --recursive isort/ tests/ +poetry run isort --profile hug --check --diff isort/ tests/ poetry run flake8 isort/ tests/ --max-line 100 --ignore F403,F401,W503,E203 poetry run safety check poetry run bandit -r isort/ From a832e4b1d903b88e308c9be54fd8f4f376ae4e9a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 9 Nov 2019 23:46:00 -0800 Subject: [PATCH 0227/1439] Update tests to match new isort command line arguments --- tests/test_isort.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_isort.py b/tests/test_isort.py index b4d5b5d38..14af796d5 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -3152,7 +3152,7 @@ def test_command_line(tmpdir, capfd, multiprocess: bool) -> None: tmpdir.join("file2.py").write( ("import collections\nimport time\n\nimport abc" "\n\n\nimport isort") ) - arguments = ["--rc", str(tmpdir), "--settings-path", os.getcwd()] + arguments = [str(tmpdir), "--settings-path", os.getcwd()] if multiprocess: arguments.extend(["--jobs", "2"]) main(arguments) @@ -3180,7 +3180,7 @@ def test_quiet(tmpdir, capfd, quiet: bool) -> None: tmpdir.join("file1.py").write("import re\nimport os") tmpdir.join("file2.py").write("") - arguments = ["--rc", str(tmpdir)] + arguments = [str(tmpdir)] if quiet: arguments.append("-q") main(arguments) @@ -3875,9 +3875,9 @@ def test_settings_path_skip_issue_909(tmpdir) -> None: with pytest.raises( Exception ): # without the settings path provided: the command should not skip & identify errors - subprocess.run(["isort", "--check-only"], check=True) + subprocess.run(["isort", ".", "--check-only"], check=True) result = subprocess.run( - ["isort", "--check-only", "--settings-path=conf/.isort.cfg"], + ["isort", ".", "--check-only", "--settings-path=conf/.isort.cfg"], stdout=subprocess.PIPE, check=True, ) From 2089f9b57f3f164eb2fc273f1ed71e528e4a31fa Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 10 Nov 2019 01:04:47 -0800 Subject: [PATCH 0228/1439] Add new logo --- README.md | 4 ++-- art/logo_large.png | Bin 0 -> 34868 bytes art/logo_large.xcf | Bin 0 -> 141948 bytes 3 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 art/logo_large.png create mode 100644 art/logo_large.xcf diff --git a/README.md b/README.md index 6d39941d9..e015d3e35 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![isort](https://raw.github.com/timothycrosley/isort/master/logo.png) +[![isort - isort your imports for you, so you don't have to](https://raw.githubusercontent.com/timothycrosley/isort/develop/art/logo_large.png)](https://timothycrosley.github.io/isort/) ------------------------------------------------------------------------ @@ -13,7 +13,7 @@ _________________ [Read Latest Documentation](https://timothycrosley.github.io/isort/) - [Browse GitHub Code Repository](https://github.com/timothycrosley/isort/) _________________ -isort your python imports for you so you don't have to. +isort your 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 diff --git a/art/logo_large.png b/art/logo_large.png new file mode 100644 index 0000000000000000000000000000000000000000..230ada26445e4b4f16387b777b72bc6debfaba97 GIT binary patch literal 34868 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 literal 0 HcmV?d00001 diff --git a/art/logo_large.xcf b/art/logo_large.xcf new file mode 100644 index 0000000000000000000000000000000000000000..f90cf9f39fee56a79507cff815ffaa596d2421fd GIT binary patch literal 141948 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 Date: Sun, 10 Nov 2019 01:10:03 -0800 Subject: [PATCH 0229/1439] Update portray theme to match new logo --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f2c8e169e..acf45172d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,7 +77,7 @@ isort = "isort = isort.pylama_isort:Linter" [tool.portray.mkdocs.theme] name = "material" -palette = {primary = "blue", accent = "light blue"} +palette = {primary = "orange", accent = "indigo"} [build-system] requires = ["poetry>=0.12"] From 10dcee3944d29d3e99e992326f9dd1260fb97260 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 10 Nov 2019 01:32:04 -0800 Subject: [PATCH 0230/1439] Simplified CLI logo to match isort 5.0 logo --- isort/logo.py | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/isort/logo.py b/isort/logo.py index 0c0080627..3df7ac21f 100644 --- a/isort/logo.py +++ b/isort/logo.py @@ -1,26 +1,15 @@ from ._version import __version__ ASCII_ART = rf""" -/#######################################################################\ + _ _ + (_) ___ ___ _ __| |_ + / / / _/ / _ \/ '__ _/ + / / \__ \/\_\/| | | |_ + /_/ \___/\___/\_/ \_/ - `sMMy` - .yyyy- ` - ##soos## ./o. - ` ``..-..` ``...`.`` ` ```` ``-ssso``` - .s:-y- .+osssssso/. ./ossss+:so+:` :+o-`/osso:+sssssssso/ - .s::y- osss+.``.`` -ssss+-.`-ossso` ssssso/::..::+ssss:::. - .s::y- /ssss+//:-.` `ssss+ `ssss+ sssso` :ssss` - .s::y- `-/+oossssso/ `ssss/ sssso ssss/ :ssss` - .y-/y- ````:ssss` ossso. :ssss: ssss/ :ssss. - `/so:` `-//::/osss+ `+ssss+-/ossso: /sso- `osssso/. - \/ `-/oooo++/- .:/++:/++/-` .. `://++/. + isort your Python imports for you so you don't have to. - - isort your Python imports for you so you don't have to - - VERSION {__version__} - -\########################################################################/ + VERSION {__version__} """ __doc__ = f""" From e6ff03e0d896e44a54035b430058cd48a36d1e1a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 10 Nov 2019 01:36:51 -0800 Subject: [PATCH 0231/1439] Add small logo, use in documentation website --- art/logo.png | Bin 0 -> 7007 bytes pyproject.toml | 2 ++ 2 files changed, 2 insertions(+) create mode 100644 art/logo.png diff --git a/art/logo.png b/art/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..35969abad0be42d9a661a2228e7b98154e663410 GIT binary patch literal 7007 zcmV-l8=&NgP)V010qNS#tmY3ljhU3ljkVnw%H_000McNliru;|mH2A|p<19wYz&02y>e zSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{02;?hL_t(|+U=cbkX+Yw-+%YM z*ZVR{&%R)Q!C>P82p|bklt_wVB+`^<%48KCc}XfxoLEk(QcfJZR7oT!wku9zm94m} zh>=}ZVo_U4bVQ4wMV7^#B1mun2?z_Z4Q2+jue~qt-hAi=I7@d=cTe~01~8{;KFro{ zx&M34|19?$;d}HiHJfr>i`;w#n_|}M!VVxHG(Mqm0uBKt8VMLVbPAynm;XTvG@trb%eLLgN7(%NMv5S@!ov4n&ANmP`xcS`Y^owO}R( zo=vF(2qe%Z6-~=_yDcI#o*d9xfx#Ro7PVk12aZjtF$h|OrcEjWLf6mMtWXRZx6rtO zE+C-<^I9^VGbNi+B@h?_x`ZMmG_G>heAO3S5O5~g>y9w!iqhtY(&C5^Fk^UR9FGyh zDN>jsjT9M#mMCqav;#}~h-W+`m4{f$&s^HaRNBV}aX&A`eH_l14ZDy7W`@oJznius z=e4X22pmGwBQ+tR>bC2()Z7$YV83UULGL8}u2EWLq;wz(g&{H+BybSMefdGWFu3f` z{xt$J?Tp5HITvf^a5%*Ojs{q$E96U)4yjmFf;pZ4UPUEw0yDSx?LuMH!>@Iw`DF71 zw*ju)D@@Rf^q@%5PBegh?7t5k$7<3mm0lZqWu*%IT|80_d8=C?vG&|L=~zmdeD zOrD#%mwz>}Z9{>lay7wai71&h`2nfeWfK4_2Ryp{HU3N6v)4TU0MatUyaVlT_@qDfnaf<9u=Z)BLjM7{+FTcbR5{&cM(3){YQB4`AYXw42aEa{(6!y68y8R zf5YQ#BldxywrWnYv-bo3sO?!o4HoNfWXTB4XM3OKn;qw`ZV+e;ZtOqFe`)h1|0*z#6|4F{o^vX39`mG0H6x`eSEZ^w3cy)n53&B9oIlkQVGR{o^ zRONksruz_IX`Q%wK+xPi$(LK7-!uVL6eR?|+INWG@-H?%2xN1FFSq}gmTNQtR8L9C zH2-ek89wJsHZBM<9?kFc{FtpGwYg%o(g==4{$T6NY!|BWK%fjKkM_ODZE|6ArD`lK z-ZOl)_e5iXKnuan-uL+x*SXC|piLgbfzV5QDLC6WAdt;r{!Q?wo4Ej6iK67!`+icj zgwuvVU^z_wdFS(VZAJra7wMeiOFhTx3j!?!d%BPD56r2}6{(9^fE!z0=VNB3eju>? zaegy!xGslJHYF=3BDe5P6%M>q1OhEhKG*$I`bFldeStz7SjtQKDrPf(JhG2i6+WjT z7@46-9&?^=cyB;zNi5mN#dr_n$sl7XAHyjRbCyYT*-0G3P#7A` znz@&kmp!AcAW(*f$6DTOsO+FqIe2sKb{?M|V!|p;Q^QIUCfn}DmFuP8C=nDgWpa3Y zh@Z|3vcKaTj|L9YChRW^jb&Q-=HymFLNi;XGYe&bz!@5D7%d=xh4^;Q@&@le+h9&nvS+PEpccj+D$v}8zHG;7H@wehQ$ z3<51pKGS>@R|9APso{;;yZLVuJJ_g|)<6b`XaPA;n1EX-oFdzZBQFSJZhmzBZr6_ z+H#uKI@bxfEf;@2@>!maG~0Gf1thc}k+Z+~_{3t;$ybLD;FBL=yCcO%{PWxt7^BBM zQZ-P#HhT}J)6RmnvDdIjD|zKUs}6#U!{Ae%pVgHSyOhIr0F=TfWbS7GQ6+gV>E^v; zH)6J%7I}ajt~dvS)AW00Dh5R?)5>2@ZCe+tEmE}TaX9Y7!B#qqkFduR<<{T?{hmp>Js*^Gofl_sW70B8x}Shl z+~*1NchP22x=J9>Lh>>H2bgstgaXAIGxzYsVh2~Vq=5;`#ESbl79YYX25B`8veOgh z*5=`YT4E+S!1rf+O1@4&T716mIhHOyzzeZJbwH4DYd+?@P`8&i9~r z(kU&O$VkpFhWNh=?F8ik2J-NUCns)YQI($Jd9?jJZgYvdf@2(s@5*{T8wG(cIL;nX zpWdHtW%9MrJ4jpuHA|PtddH6)9ndW2}`GHO|&B4}J(7=#yl%O$$vJ7JD z070(j6@0`uR+mPV0?jM4ca|LpYD5u0lgv-mOi`FmoJf@(m65>b+Rt41zRh)>R>PvH zgq8zEL7=3YeeSdM*@x+7zA-a+O_%|{P(@#KHEJ@3oC)?dAO6tXNX-GyJQ@fYR?%w| z1wq<3&#f|A7c%(#>}^b2#x^o|X-2Qtz8QrGcSpDP*i&R)4jtF$TuL92I; z<~jt3WG29O=KJcfVwz^D@aH~XY zn8o~|WGdtLc$=We&>zM~L*2ZGjkGRX1=B_q~TP;i^`Zy8p;bg3tWWvj083iJDdZP^a zkFL)d9BaG8CRPmxGYkK7)b=w4rtKCHgtMu$8wWO-x!Q@d9M={=g!?+WV7|bZb){5G<_AOs#t<3@8kfie5Un_u>_$6>^Mc1~&N1Yl zW5|D$Uk1kFI~k63b1c%v>4XorQ1rVJJkoUppRrVON;+UhFwj`JtH$zpIc6dEFl0t; z27zWO5kl3mdM&90^GFsjXenKBkbx}WTefq}%cJ?0c@mSUW-=wl1$sTF>GhoE&QMlM zEC)+D;*n7d0aot4M)D-1%tC4;AY-;K!elCF`!$6Y%xl4zl3b3BZmRUk6-hNjIm1VU?;l%wX?nOBlCMO`sbGcF`87vVbOWTnmy6{jo^2rz9w zcc(}ySwoP)s4~`{G*ZLyzCvZz#O>{n^^8x!=M}={KoNqB5;fZi=as=^iAfqq4S{Zd zG*sj*Tq!NkrR{bQH4a19L<5uq06`5WybGf)RazNU2?#V~v}rR41nwGWpRqCk0N@a{ z>-w#wS(jeVsdb*H;BPZH?7dSVX)>xyMi-Q1w9Guco7zo*Q!+uob`WUdx)KlwO-y-g zzEw%9BT-d1@N5M{(^tb$fXYnJYBSd|w1Wjz2?(?%VtH)_0dzR36Qz_IE?Bbskc@y} z4aWg0XM#?f%k?bfBSS?c=bDJNyVP6I<*H5*%dD0ajh8dXmX*~?T3z9?L&h7Gr1GIE zA%;S0X45{~tyiaO8s`RevDSi%6~-HavbJM^x47nQ_YNYN04X7g0KW(^oK5>|@7on{ zjL{=EMh<6{pwedWp&F7xjlmArnC+pWrA(k`W;Q5QF%Wod2SHkD?(}cKBxu1{h4BrC z)HK(S4`f_|y^aZ+->7CjRoK8?u9uVmaV=O-lBwli@gVcIR}@J02gWM?|3zK77U)># zl+cFBYG8_+?as#lmkOlkmmiHYFIon3T99F73RNV6oY5v-w)Q0u?DU_aS?pt>f*n0o zVeP&lG@Uin0$K=e4P3On>zmLP7ZUl8r!PMmCnvNZMfRYDUiPKZ7Q)%XDzB_k`_yzyOZtDc@gxG_J^YQ=Cfl)giuR@QKz_$THtpcvS^$ zsp68g)dB#%&_!;q!*tkas+VcC7T&9m#ueTGO7m`_gN%P;S+%kFP9_s=Os7isz$LK7 zNOPf#uAPgOXaNBOqPDZ5m2~n@^Sidk2LLW5y4SR<$#*oas1|%b5@MlE(q2K7Yb?H# zzn{5*SHjIKDx>TaYB{egTPjvQ2->AZQ;oHNY?kPu4PdW zq$4f7rF=Xf%j=T&_WYfEeS9Y~%AQiHB%*APd8J}Y4f6y5%5d>m`8-Y(k|No0NG?wIrr!gnjxubez+#sc7aFC`ZNGdr8~Kl zb`mlynhk|nBt^as2sAH8`pep7e=&A|C7TOU%83maLbFpU+%>+Zj0gT@s9v+8H5Xz7 zWODVA>#fGRt94?bho9-v2-@lJex8o|>_>g6Wn)6KM=E?Z<^fum+}CrMgL0`ZT0o+C zJ6w8`54pWSCk>JN(h0#!$$xT~ zJ`Tr9Z~Q4kQn5p-6*nw_exVtViUBhGB)lVcZoja^GOfHB_YunNG@sCTg|;Q|ydDVJ zq+*Y>YHk7m+%5Bbx%DS?>;|v-AhwHW8JtW)wkPi3GL7H|x(tgBspyajYrdDaw0j+& zv>%IX`;ly|0c#Rln>7BOY4*||b%-k9IPjPIR-funaL#n16dk?ogb zX+dOJ>-b6*U9Ac#iCo(aDWFRz+NHu}(@WPv(9t%@mpXn-Z=EN`3lv8d_mD30{Kg6p zC;@{~<=wC5Qjn*T1N>9h`Mi8!IPEOb?Prh0L~xso;UFjDu&xZCuQl z1cW9e6-`AZp4G$*Ny%M($M{0jIut4}2-nzoz>|A=%962e%7p=o`bglXM;f1Xqrl8lk;2!Tn^@2n)!yO$j@c7`f zJmj~h)0%c#JhKrgI$doW@h<8BhhafMxRi z*~(`^n5(~8ZXf(Y5z}k6!FOkF=hNN)k7g9ZvF;UJw$wU&z*FM}nHmtTNe;G8a(g@I zbO+~R1B}F57*6^*mGCj1F;{RNLudkKhOMqR1MVnY?ga+Cm)PdKh^s*t{zK?%UY-9a zr_(M1)c}EeRRd%xvW*W{yK{QJjVD@m@SE;ayt&l5k}(%)2Dvw2?_f<0;Fw~tX^O$7 z)xVTvv2w z`jnZ-ZvM}F_l7@5E{_isQmOeeZ$fLPX1muUZ$z$oc?}$6*`_IVQ4IhmSs`^q^zlUbDyXfdpkr-5$DI+AYTzm6TOY6C0@rHyDQ z=g9bKa(I((adYviAWkLu#^`;V&s4XSkX+9cEO+rrM}fG~DO934&@{Zcf>q!Fe>?eU zo{2V9{j()xk8rKu@UWa@%00McW3Y7@!8&*e7=Nily64KqP2SE)(T$c z1znERrfaw%0BFuH-ohV_@2=T3uB@%rf>~uysDD_w2Sin_S3osW$JZYK;8OS|{=?Y* zns!2!tDXD>%OFzVhRT&Vu*H9Ab2TdjzXyg>PRCGkC_1#^~B+1dY`{)lTqa<-j6y!7*&P=U==F18pNUYq(P|9<@DHGKu@27=2H zQ<6y~iBwP-oYpc+YL){0$?(HGm)_C*CoO{u zWts@B1(DoCnF*60oWGC%6}g4PRhz}HzWq-m`uM$bkMO;vw)$wGlKuW1Iha%kZCp}+IBrH-nxkVk^B*Ta!#U-@z5$@&_}m@*#<@@NX3|r)`!KfM^w95>jper3<(RI^Y36#0PPX&cWB2p5*@2?- ziAvQc&I(UmNhtlCW+IQBvNVin$(WXO3PmV43{vFL25}_`tUCRijyw47xsUQ{+cv(? z{wlkTxoh4?nA+gz+(G_yayPT8{;qROi_$?5EwVgH!?>1=lReDZmOBz=uJ{|t5{jlP zgfTm)QJ@%|>EMeCpX0vn5BPlWb=qw1{Ari#Hxu2>UybkQwS=!B?nwe+B})T=ETcQv zq82P#1_B64O|ww=$!03!O5)G;3R>*~$&BFnk)6Ec*~+6GCwMUM4#7Ge@vbKEizWtm za`FIAhFcrz-XbcSI28B(;^XT&Bq=R8Y8e}H?E*q)wS-~OBNdC5QIuG^dU!uBaqU5UKFw@Nx;~W(SXHNm5!|R1z22 zaWhV`O%}}@!IXd%@$jwaUcTen!^7=oxhHsxEpoBpIfJPUMj|(IXm$rrN17Y0Vx( z?nTMlxxqir0pA?kJ(KjiM`@E0sy+K$Y6r1oFT?R}PQ^mJ9B$=o=ITp|O{%r?q0IHj z!?>=W6*ee-9s)orIJwlq$)y%xJ6Z_*jwCmG!VI~hv^k=*I3fhi7+x8}Wkhj_6oyD+ z2n)1CW1^IkjPjDOyd*Ooq8UH489$RrFQ*cI-c0yxP3qJ_5=w9(PovSIAebv~n#eA3 zl>maZYFZObIe9%5;&no7%4IGlIIHq|c(Rxl7_D_=#vOFMKBxqF7BS_Tq z#y3GwMIK-j9SxKh1eO+D)b-T@CWX&HZc6F0s3aG31rt(7l%(EXxQ8+iyll!Ei7LtI x5={D002ovPDHLkV1g Date: Sun, 10 Nov 2019 20:23:30 -0800 Subject: [PATCH 0232/1439] Simplified logo --- art/logo.png | Bin 7007 -> 4759 bytes art/logo_large.png | Bin 34868 -> 24744 bytes art/logo_large.xcf | Bin 141948 -> 181588 bytes 3 files changed, 0 insertions(+), 0 deletions(-) diff --git a/art/logo.png b/art/logo.png index 35969abad0be42d9a661a2228e7b98154e663410..de2ce42d5f104d12657cbfb29381ebe0e347ad42 100644 GIT binary patch delta 4725 zcmV-*5{m8LHkT!kDlq^60002_L%V+f000SaNLh0L01FcU01FcV0GgZ_00007bV*G` z2jdG11Q!MuJpAL4TPJ@J|4BqaRCwC$oq2RrRi4K`_ieRglK>$BBw>$45QB7E;^#4LyNX8;z5y3b`Zi6AP_JF zvQ*XE?);%nqbBgGQuV4*dEayR0}|@hyZ7^c@Av!N-~C-tHgA8SBv7xG5)4700D?dP z1c3qw0tFBR0?_j6ZUj_`7ue8xuIYNReBq$)}6w@q=IN%nt%`N65r--#u3n0kJITtRov@bbZJ-|qHFO}*Sl!_w=&!1qT z3KNxfbeajPU;lrz3PX?itve71IhY2!ih8G<<#rh_IwiCvaiyFd$DWoaC>9B38x5ST z?_jjLo>DrpU~mMQ?88ob3@h!Syku3dQtAN&nSSE0)*F~@J9n`eK_YTk6C0R*XPnAXCr#u_fr zmr;n@L&GkWz1BpYPn^zwI41=Vc-6^TE4PPNFiTxVnEYANXtE~qbmC0@(dh%UoTz1y8wnsd)@WW5+R9Hti>MSwdPQQ!kOeGGT+CgG zksQhqFDHL%5cF5uxjXVcGu8D7de>^U06Lt}JQ$n7vrcgi_%B(w{sA^gYfLR{lfK zUv1^7!WXzs{S=j;nCV!nFhF0y;IUfxuXtX(_*lrJHx6_B6ZV~Hc0Xtot`eWEH7A_T7oT9cfLTRO3J$u&r1#hq4VSLds(lE>q;s*jaDs7?P^}M%02BQJ}MQC5@L%T z0!o2OU_2q=aGBo7h5BaB&{k8FLzaK<^>-XRbMb3z~H)asxi!z?qW zb8liyM*9aOT!V+><9N)hVNQ5G*XxVvN5c2}ZI|WTpExD$pX+(#Dvj-2EcSoq#N-R0 z*%{CM9aDMDE%5ESj;r#!j?;Ku9l_5dA8@I<7RASh;@OVN*(%c?lX;5{gvwv*pAgCc zE1)1)miPe|wq5Ki0LSW}Q^3#KFXvy(OF80G!S+_Gj`?=K42`J=6>0~$T-}xf=0D2s zJ8tBLcun@Zgh-bKv2o03{YQWPXq`v9EYEmI*pZ=oKJn{7z!k=aSLNQR5aj8!_Q(;FD3HZWN8wgB{g{Cw6Y5&7J#HuId@ zhZZT)1c4A-q;1Gra#i56EdCe#!Wu|V))54&q|Pd*glC)*%BW$uxPU2IGj;lwF0I5t zr5PU*~^t$2C0f6r~3Oso0#WZpqrE6k|F>h8Rh=i`8m_bA(O9; z9a3e-cl?P;Ve(-T9Ze<)lPRHvNMMY%9m5=h>Cu2I$BPQi(RP1irQ4-I@?l~|*75+k z;7=@eBc2^R1Nc#R{r~@cfVz>fc&|0+5(0_A1a(tZo#S zOfFKt>^=mF`Xy&%p^uQjWpUy>Hp-x6#fzdyZVxa2&I2r{(VDss!D#IuCuJc)B;5Wy zVGRvt*K0_q9b$i!zVv&KQkAk7Lv#lM0prwtzWW0LSYb^HtUwYW;q}osJN;R?wu3V1 zcE=JZR4Y4u_XiXtubCr)*-auO>?#W>#UaKkiPRyGLS=|@(D$1yz3U{B?Q8SbZ2S@9fESD&G%NP2i&1F z=Iyy$E*sq;$S#s^$3k%vu*a^Y-Z`1ob_HvlLJEY-Fr|ap;UyF(&7PN}i-4nIN?S~C`sS_Totzv($wu&hszE!c5Y&}AhR>E+?=7Cyg zW~796-v_DD?UUEx+zzK06M>#&l+c~(q>$3~b@MQhWQZsQg~DG51d^saUo3KEgabZ% zS_&k5nh!c7eVJqtO4!x-c@1z}o!cYFmAQh`7S zH2i;VyY3Q~`F>)rL&E1om_!mz)e;EbAyBaMzUz_9!X9Ulx=$c|Uq5uwMZWhv5?QFy zlGq-qjys7CBsRLwUsVvr$#Vu&-+Mg^1aYCHW(jQHoisroVdl9dh*RWyk&dKYrUHS0 z4jJ{`67*AI`E3Y9oKl~?0!13^_mG=VI-7rzqq5Lv2)K6;x|$7?y8I zBLUO(ZN7JX6G*nX-LM2mmtF3FESSxMT&p+bnH^A>uKM5UMU@IG+`?`^fXh~=%J&FC zqA=Z9>v{WBj|dGz+nFf*uheRDhp~U%V?3gDF&ljDR(P(7>q>jIn`&LcV*CUCTR(3B2v}rBN!xk~D_- zvWhT3-@_H6-uLSYBwOtY&%fU#1k<N?FWI(UQ~`SZ>bZ*YVTX+{-tWN}+#oYj_Do zS+D{MlBM>5B!2%m&*KVbOj5V7$FAk>`1oEA06>lL1y_pfGyqMwRs1Q5J)O#Nyfi*` zh97mjzDWG(sRH zi>w-~6Wmg}EayX~xYsjKr8RM1=#4CPgO_Y{PNzP}O)e*Vfq!8QW913z3-;P0*xAdS z!3w>F$D=P&k;QY96eNohqf&Z>9N)Z1$vgIhZeH@#$a^wPpBM}J_l664Cqb7{Hht7U|D<>)T4yEnJO%db# z$g6x%@H#(L{i#R_36sB%e#U}=f9L!h0RX^|5uP+tKPAmGIzoS1JY`Pd^6;*2Wx|%& zrJj9(@!F0w2SgB6H*;IzX0Eo+;Ppf;k2?L+-tnYVjdQcHflG`fl+lrEZV4;RDJ<^B z@i#XB1c$td#^r*_YuNQydX@kC-8Gb1}W372~3UfqxQFk&~-N`v&xYw;@yEBrVc7N*KV%EBaYhy+S=F;d4E{tEE*7KnPw@5wM3hhVs{Jf8*{=tMsGK<s=yM8!K^aaJ_YMGDRwjQgW1d-+v1?=$f*O$sLc?xafNj<^ z=EgHy#G6qF(z$fp`v8DF)?{vqP0IMWP)a6mVq}_^j(ZmXlejT9mHj?!=E>L3heMd;nmVRmbeuRCf8an=7MN`7}|*t+DIaOE{Qaj{s~)%;2in6kjZWZ+U_>=3xG& z<3@ir%F--^83mbyf}o z^UdbOoDg@kUCE=?G~#)Gwt;u^A9F@Cuk9v&ZI@@!3i&AsPsAoMH+BQti7s#@C3uQK+ zhdQHydErG=ivxL&gNDJX#D(0G7{`_z+Q6G-2);H)!eMS?9oOsc(a-13pZ=gOX|m7a z@z^9@baStfM@~TSjryo@ejHlGW%@@HXK{;nIvu|@r-qkellfPtEO)md7nYUV3&0k z|80)tF}vp^$8s8iW9{l?`W`ORH&Lstp(ux1VhAANjAVs1mS@ai{7Lq_<9>Sv1ji_% zSj^TMm}KmrMqN*zj4!lyHt{O$PBniU?2#<8hVWbu@7mVmAo%vTmt3SCVzS!ANNq2b z>Nfg`mamc#X-%etX>*3L)2U>mJ%B}ae?F4=bGCEOhM*JMJVeB)Q;$%s9HqZ#rLS_7 z5~Y zNfLI;FfX`aU;u%IZ>jKA~{}4gn?_2^cwa3ZW5~|3M2hpaBI~fYO3Akko=0u%rcv%{6UY z5Cny$Noc%6;{hDY7q}Ez_V-2(M2I|=Obg;#5C;~uU?vBiO{oJ2B+w=mP0Mz>Eh03Y z9MD>U!5kheAO3S5O5~g>y9w!iqhtY(&C5^Fk^UR9FGyhDN>jsjT9M#mMCqav;#}~ zh-W+`m4{f$&s^HaRNBV}aX&A`eH_l14ZDy7W`@oJzng!yBdqeAKzZ+;MF>6iej1oz|w;EErZlXK_GxGspyvW?|fyz!Ip78*>sM*o)7S1 zHI`k~CgYJ^ytuHPuPtG*$*JehRY4FmwQN^&;G57vOd5Sl@$XtqiHYhkjd;{yL8^eO`~a$N*xoXW+sQ+M;b zliSwVXS+ZkG@MliGg_=9`7WW^A(d_9&-5kvwVoHb-*uMjDZ-zUN$%}>ju)D@@Rf^q z@%5PBegh?7t5k$7<3mm0lZqWu*%IT|80>%O8s@h`FVI~FAHR{rp-i5ex|e@7v28QfLaLpx<>g*_frgr&20X9k}wkdv#o!_<834MfuOc( zPO`K21OBM(SwamK>u+Sq2+e1EpXQq#=dW%MXbf)bKgoY;ehKfU{jW%aeyZoke5>o+ z)d>QPWM}_LzSQ)}H5B@-2VoT4+xdSi-{`n_b%8(&!9dSBzTEUO&P@PR<$Zpp`w(Ag zow#~H(A+-Bms_9TGyzo?aegy!xGslJ zHYF=3BDe5P6%M>q1OhEhKG%Q!Q~E{bs(pb%8(7Lq`YL8Ke>}2}SQS2}A{d#WNgi{a zZ+LG&YDp~F$HjOLykrK$&j-ap*Pt4Z42}Et!n&=Jlm*99nE=+%j-TMT>vX1x=S_Ng|e$ zNZQHSXcN#4>_VH0q2LS$gBRHEK8?SDG~b_G+F7;FDjNje&;@?ZoNleR0~sZmCR!si*5fn0Ia(H}*pUw=j zzvCQ_1`g9E>@N(BWm@^>@R|9APso{;;yZLVuJJ_g|)<6b`XaPA;n1EX-oFdzZBQFSJZhmzBZrJET>ro$@oyE!E}9QScJ?!&=WI*gC7#}nn&-~|1iNxD5BlysdJXK!QDGD^CifK=S) z3G;W+W>SB;N+8fe@-hDhm~|qA0>v9M_wdAG2UoMCfeFjRiu*YhAHpdHX*CYA(-Y>_ z=HY@`VkSAj_h)-bzD__|e7^5FmM%WP3$Z|TK#*~3KIXhox0g2`8{#XIJGd4kC_hxv zDJ_}ENX{>Y_`eJ71myt+^6-f#CvIg?m7d~xwEcfPZgYvdf@2(s@5*{T8wG(cIL;nX zpWdHtW%9MrJ4jpuHA|PtddH6)9ndW2}`GHO|&B4}J(7=#yl%O$$vJ7JD z070(j6@0`uR+mPV0?jM4ca|LpYD5u0lgv-mOi`FmoJf@(m65>b+Rt41zRh)>R>PvH zgqD8;MM0pXn|wg)oN{8HEPRi7&v^3GnlMys?c13{~I zj^;W9h-4vNcA{x9N`uWt;Qni!=L$w4wXsBxBelZxv>0poNx_Y78BY3*dE1v^ zF)_$HNpIckcN&pe&>zM~L*2ZGjkGRX1=B_q~TP;i^`Zy8p;bg3tWWs;TVi^S@ zcY31?`H!y8860c7#3oh^2Qv%*bJX@T1*Y=yM#4{>Q9uTY23Yhm%~dis+WkCb=L&hnTvlP+WL&nas{1-EnF^7Glj+w8V4Ge$O90qIG5~3JBRav z$7{|pU`8;|Sh=gl@_0FB zA@?w3Mr{UxW-1Xv)vi*R_?t<@+70oLTV%+W415CWGZO;HH8+;Yr&Y3T#k)ys{;Bn zS`gNPFyP7spb|+xDFlu(y@8EXCpl$2=Y0sGDu1zwSs+q`U)tMvQlObhdu?2NSkRJT zCD)AB)m7Ipxof07Od@|7w2^QN89@q-(iAkc`F|<|-a3F_K4Z@u)sz;TvJ6h;0YJHg zuN?O`lkzgB?WNmFSoowW>Uk6-hNjIm1VU?;l%wX?nOBlCMO`sbGcF`87vVbOWTnmy z6{jo^2rz9wcc(}ySwoP)s4~`{G*ZLyzCvZz#O>{n^^8x!=M{g#=0FjGj1o253Fno; zWQj={M-72)e>7C&EnF!r(53Bm5H$`%*F*!90{}q{C%g-zE>&6?RS5_*WVC5B2n6mL zXrHk%007_+wd?w=rCFC=UusNioiIPASsA!#zIOGX!zWVFmYy_?!ifm1R;z;+O5 z;<^$L2u)0RY`%Y0Nvk7KRX6Z#1w_+V!%=|BOwejG*D|z&1yue)k<1j z;j%-<8}xaMv54kDQVDItmg zzX&m$P5W%`+ZAw((IYoT4ri61(q{0X8j?bd!4B7$?V+NjOrU6HHYimw5O{3|L0W3= z^l!i#l+cFBYG8_+?as#lmkOlkmmiHYFIon3T99F73RNV6oY5v-w)Q0u z?DU_aS?pt>f*n0oVeP&lG@Uin0$K=e4P3On>zmLP7ZUl8r!PMmCnvNZMfRYDUiPKZ z7Q)%XDzB_k@4#e82#X=PBP`eKfAfc2k^6^wlB0 zW$=mCQ^+#kSa?+hZK>jtwbcRuzR*Q(ufufMXsVZKwHDs1kH!_=07~<2qJxZoV_CJa z_)aDhZA_<1_rN8v#Yl6ZjIN!Fm1qG01ERLGqLp;=Q1iRC#|Ho|CA!zNtjTvYuBaA# zKN5dpp-s|WL6mDOzLUS7xq(;0%`7UT>=bG_uPj?CRz3*YrA1SXwSa7y;`SVoMG41OUo# z@mTwtbx0b4p)(wbhA=36G_C|r0H`EK5<`DwJ;G>w7yskZF53VA*Dtv!6 z<^fum+}CrMgL0`ZT0o+CJ6w8`54pWSCk>JN(h0#!$$xT~J`Tr9Z~Q4kQn5p-6*nw_exVtViUBhGB)lVcZoja^GOfHB z_YunNG@sCTg|;Q|ydDVJq+*Y>YHoi50NgF}e7W@}b?gSO`5?B7Xc?SLLbfOF;4+Qi z2f7T44yovn3TwWXx3qg5pR^x~Z2Ocz?kgx2U4})V#=EbO18WM+cEe&@jU$3ruGbvp z_qz|#UB|gehE}{XzpYI3k&}>A^ye09weM`CEuM?rP!!w8!o8?G+Cl+7spx-@R!w$? z0JL8Fvo5LFYgmM8>;_&jH?52OQST2KszYROxx`aF97&XSrgOyx>6I2SWvtn`LTlcb z-@%OUoq~a*cO%8m@k){Hmt$!`WLfL@N)}zM3Mz?Q+YKq8ODNi{A%(jQA;DN1-gJ;8ST+EmRgeD{vO+_Z2)x-=*$z6TN_(Je?ybUlB#$kDQW}*Dn ze``#oQ)qlbtx4qyW%11XUVf?T0-jY5d@0etru#)9+2Txaf9JDRdFX#%P$n962e%7p=o`bglXM;f1Xq zrl8lk;2!Tn^@2n)!yO$j@c7`fJmj~h)0%c#Jh}V~%VY*jRiglQ|W%9w+cgMVEWDX0M{vcZz@HJH@XgJ9&R|X%ElN_wshC^5}pt zEe^8MM$XF`~(zgcb{{6Z1aYqi05XKv@y-T#kf6vMIZ z6o&PAO(=Gzl0D*f|17s<(jSpA5b9%mwCt7y!o9{&ITzhCV~C^bFSi zfj^IZ7s}%ACJu5u)4`d9XYGqG5WVa3%r?;5Q2w`jnZ-ZvM}F_l7@5E{_isQmOeeZ$fLPX1muU zZ$z$oc?}5sloM61Z(m~Tx(@^pC z1AupD@8a><-kN;Aglv>=M#-YJdgay%UgiZ|j?|`WxFG;&&M)4=ACB*?*)^`Lt=58B zWl*SpSh)v8RjyY+HB-me9{}J|_$L0t*#4S!LY1qX`~}M(Qs9Qll{m1)e`#|yD+IuJ z_y&KzIC79!jSFV-YWYa6afM!qkFp;kr6k`>^xEF>*CoO{uWts@B1(DoCnF*60oWGC%6}g4PRhz}H zzWq-m`uM$bkMO;vw)$wGlKuW1Iha%kZCp}+IBrH-nxkVk^B*Ta!#RKD zzrF#W;rQGg{KmOY@n+IfxBD=*-Sp7!mE`Ve$#jkzXtGhpX)O_Ca}P#QW+2haPX%d6 z$Gtoq-i_t9*yWh6%W39%icYrk*JJndwb_B9^NC8;C(a5_T}deYoMs}Aow78HXvvtC zbP7c%Hw;qb(FSoP39LH(oQ^yA?zw-D@@m^QzR>)kI6m9S@!3vr+{Qi46WkUUWw+-nEh28ah7ej1OAc@{x`pQ# zdUz%7<7yD8VR+S;;B`STS9Gp$?WHAOSzFrR#pzyNobE-*+quC%&jH^Y+dY%?yGLo0 z5vo1=TxtigWG}<gyDH+#YixuUc=qO>?71kD&;8N+2nafuX$NMi^Kv_xZ~l#`6|lCZoaGajND zKeHJ>lSwb95`NxH_-sw;)It(Ua3N2l(V`%jD{z{~E^(Crg0+8YS`$qd$sbbZXrcD5B h*b_KaqI>`U0lyP5lM3aOaoGR>002ovPDHLkV1nDxL$&|_ diff --git a/art/logo_large.png b/art/logo_large.png index 230ada26445e4b4f16387b777b72bc6debfaba97..c2d044dfe842da585cbf37ab8d75224d31959485 100644 GIT binary patch literal 24744 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 KlBtj~3jQyhkL?8j literal 34868 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 diff --git a/art/logo_large.xcf b/art/logo_large.xcf index f90cf9f39fee56a79507cff815ffaa596d2421fd..4288daa89a500d0c366e09c555d969ef88587965 100644 GIT binary patch delta 26353 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 delta 7191 zcmcK83s78F8Nl&92a@o}2EyjW0s#WLJh}-Cc|qKk4pjq{nn-m7gNf2=!7wJ$+JuOk zmS_TonsK$8pp0?oBf+kwl8A|=jf1T$vS}g&4GkkB)e+J)QK}ZEOuT!)KaS(1X~xt$ zv-`W>ckexW?z!KdvoP2k{q|tQp}V!^rRR%YQ{`@pxka^$aowW5Pi!nVRk>Tm-k*pU z^2LdDraEDVnATuwym|f_ZS%z1!rO~479&E=>?7;yX!$~nB zOg`TdVIz@-d02#HScP@ih#qXmPVB(}9K%U5GK_~3X(N$_d02#HScP@ih#qXmPVB(} z9K%U5DmRQB8>V3%7GW7yc~ds%OwW$FCp@DE3k{Fnz}U{qk&Co?-YmrEpj;Z)f=BRa z44r246wVKKJ0-Kgg{4@DwMgABpv47k#E%wl9=G3^PK#nXa!3wOwdTd5oT+%^&2K#UrCvaL^8ijVuVrpDkK%xXI zum zUqGS+E3gLZu^C&j1J7U|4&ww)i}onAV{R5Z1z3U=ScCQ0e5)0?P<1X!$PIJPl?y}f z>YDMg8av^VTh50zu93L$Zsh9_ivE3#v7g*5$CF#J1J7U|4&wv`mU2N7shKFhH7w~{ zC3e`w><@~oSTI&Ek~sgUcqbjdll(pRNxo#2SjNweEquowe2fCy{(_valRMqX2h}|; z`A1$8cl}V@!%Fm3r^I_B#m7(K%Xkt0h<_Iw=iuT9cHTGp+zLDMQaY+U_a;jrS9K@mcJ`U*T`XZyB8nZHJ!{e~>J8>=CBo8k9Rx z_GaQ%><|<07nl3Q)M_#P=i>V@V?yHWd+}5F1>9q3%t9(#YFA}{mjhCYDw~F!gOYc? zDXx_7fLghl_==eGAU=X$!|&kJNXK(dSuCOQQ90clkY}QDhKc`x+)2*gB+i|OF}M`v znX6pKY{_ryj5*-)DXX`1jY@DA%W1vx)lCIx#6J3#O)woA>8VhuMn(|+j z{DnwdjH&2kAy@r;efw##|D$q7|5hBwDKS8cfq3Ffw#&WF2-PnXj z-0U33)9A-bIF3`|`ACe%Ow7k(EXQixjZK()gq`Df8vS?)$8l=*+mYCHvR|DKpJOwM zc8j;^f^vFOs`d6c`PZ*s|EG6zP;Kb?>~GX#${UlaKju~gZjxaVg^fJRdbWswQJ=l(&*n#z~?J?!v-4MM->@8cb@4HJ~S#O?XzM0EwXgCN|3~im}ifY~Jl3YIfz_a!UV0rymH_S!S4A zpDuHn&SLV*k~vCeJs`)kcFb0#L#9pbpyC~ga)u)V<=3iq6cW>-gBBgM=-9#W&@Vt^ zs2wu;2pT0s?5BH#3tN=58=bO7Z2iLd(e4F%%Z%5} zJ9qncaFb5l$_N=fytSU*_E6ujqwZUOe*c$_y5EZ;`ma0t&yLPF+MjO}>3rjy8|}a2 afp@y#z060AMn|}#M$3#>n|JdCmGdvb@WG=1 From 038d13dcf58f61de4cdbb753eb230e1a3193fefe Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 10 Nov 2019 21:11:20 -0800 Subject: [PATCH 0233/1439] Final isort 5.0.0 logo --- art/logo.png | Bin 4759 -> 5065 bytes art/logo.xcf | Bin 0 -> 9584 bytes art/logo_large.png | Bin 24744 -> 25882 bytes art/logo_large.xcf | Bin 181588 -> 173363 bytes isort/logo.py | 10 +++++----- 5 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 art/logo.xcf diff --git a/art/logo.png b/art/logo.png index de2ce42d5f104d12657cbfb29381ebe0e347ad42..83477ab00fa864b6a9bbb1fcf2893d451f27d682 100644 GIT binary patch delta 4991 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 JPDHLkV1mGdfN=l- delta 4683 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 N002ovPDHLkV1h3O@q7RP diff --git a/art/logo.xcf b/art/logo.xcf new file mode 100644 index 0000000000000000000000000000000000000000..3e69230e7d6d7387582f55b8b144dda52b4d4262 GIT binary patch literal 9584 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 literal 0 HcmV?d00001 diff --git a/art/logo_large.png b/art/logo_large.png index c2d044dfe842da585cbf37ab8d75224d31959485..88b1509c624b28f0393561baa9f39552558b4f68 100644 GIT binary patch literal 25882 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 KlBtj~3jQyhkL?8j diff --git a/art/logo_large.xcf b/art/logo_large.xcf index 4288daa89a500d0c366e09c555d969ef88587965..6bcf26af3b26e29050176a97091c11ad823d68e0 100644 GIT binary patch delta 28227 zcmds=33yaR7VmG}?oMal7s$S`uL-2XCZKEzB8w;vizpxt%BG+wqBkTFWmf`}BA|eX zpbQE+aR3RMfVhAxf`}rbf(Qh{lBK(Ey?@<09fG3ceDh}JeJ|hF{i`}vx7Jftr>btl z!RbNk_XNIh3%l#xPG2))t!DiN9#D)trx;(QSTM~Z%bKJ3$y164bw!G<`!UO}rzpAJ z%BK9c`zsEj@L6q?e||T`z$Gm$2fm6f21ONxN7)LEA}%McCvGPmAf6;% zQVg&Wqlg*A7FPW=Eua$#?j#N)jwconi-^mK>xtWm2Z$$$mlOkS#3*70F{=eRIuY+A z4kL~y77&YgYFoWQ(Kx?%ioToK5q#|i3}1gNAX;Z9pkWDVN$SbS4~ZWW4-sXosG^R+ zc4M%TgpMXYO`JtsOk6{x6hl8F|2M>QM6Y642r-G+gm@FNr=k%ym;|GVPZMVm7ZcYI zKO}xe{Dydr=v534Atn)<5N{&(v}kDIgGn%&_%v}AaWQcX@k8Qg#BYe_h+f5r5MmOs z3GpUkPf6%v#9$JPCO%D^MO;i=L;R5V8Sxw9IigoFGK82!Y(l(=*b|hp$iXBSO?;X- zi@2D$=FjS3GF$M7?q8G`^_?Dd-kKSs8>%$o_Yl7%eovIqteG+PAti~OM4V1sNL)?a zK%^97kCOiku}U#6kQh&FNNhvwP8>ipW86a|m_(dTTu59^+(6tzJW4!6tWvBMNQ@^o zB(@=TCk~*QvDQN*m_(dTTu59^+(6tzJW4!6tWvBUNQ@^oB(@=TCk~*QvGzkGm_(dT zTu59^+(6tzJW4!6tWt~*B*qgP659~F6SD@8<00ZC;&kFd;%efCKdX@uY<+Y>md$Xf z2jw@rfKLb@%(HMK+T~e@7 zXH|qpsHTrlM&s8h`Qt%~PkgQT6fH#0&_t2fTFLW&BwkT0a1djNb%;5{F3P&n3hwgt zTgUY+Qb!DD(~A}sCKx-dXe>*WpJhEv{)t2?hVdv5Z@sFov6~+ zp8JygKM}7g8q-!P$4pN2`HRWFi+GrLide2V%c(ebwc<+$6_@r>T>X|}F?HzXBg(&1 zwV$DV+*1iYriT8QX5^1a1wW=4`QybDZ;j$-%M=fi2K=B}PCI|mk~4ch#h^;XTBc(B zLqxQuR8AH@odgSsswOOc1Nrw5k18e>DkguUnD(+_<}ZqMi4EeFzY&?^jb@Pl72;bU zW*^o_=`3sX2?Zz>WQ{1HrfRUVrkN_dsnUJcbhz?2OHgb+pSXhfKJg>sAtDvL`Hz}* zH$<%YISNpH!m%iYV$5QzDY!=A3v|!O}BwyHqwc%kcSW?l|#CX1{1#84h zvRJL+3oY0n#)5gz9F}0*s`C7#2eCg<8Ie}RlN4psNM6FzyCtT zVJj8KEmxd0Me$i`)Y+xV-((>bU^(g6J8ERIO{8}_7g!PTW$0=A+q+o0;nlpGh49zh zPwzSY^A%k*)I5P(h49n7gGzpJlzOiSQD2E1ns@M#Qe9MB{rb?sA4@9{M@&vpVRp@( zxc!0<4$Xb%%AbU=Yv#mL)nT47=VTVo6(UsgECsGb9P~4a6Qp@!_mzrNUm@MIPnDKd+JsZ{WSzd?lcPT4w|{3V6>c4c z@Z52xOobR$-E-gg^vG!)iRqfBhc8HoV6E(~w|5*bMa49&-nGQSv@6};J6{H2?C$EE z!!=LZr=>E5m#-BvLi4V$Wc|)uxK?nnBT(~%eJBGAPw1-Ba;lO|^W6NUERbi^*)n0% zyj>3Z+RCO?O*mERku6)*bLsae5t?|!vFf(C`1q&iQO>HXr@uWV%Zt7lPJ!r?xFvhP zJCDj=(1lB@T66TC{z0m~P&gRIPC>5*-oUx8VO3_M6WU&{P6b4osh z`Log4$h=Sr{Sna#*=k)AsFoB38=5(_&|Xl9zQnUuG?WMRV%6z~Yn4bLU(--Z@w_Yp zx;hV$(bY0(RZ>$FqEnUlT79VELdYuFP%#-<*G$<^X8u&WcC(p>qFB9I=xYOEFA#;Y z@OkJ(ONgO2Bfk)3FBCaiW!|&T=I0mWsSZqCFpy;$9*qgpGt#M*i{9;9xmsp53~gwM z#-b&LxA${-vm1ogR}zO-HYC^8yjkrQbuo-Z4$5<{%O^)a8lrwB7j0Ll<(@$%I5NBHOX*jp24mx zJG5BVe=H*Ab?wm3<}#Zlq5y*sL?OXi<>)Rq=ek-ZqSqdDwe%-BhVmorUCsS9ZEkK( zEfR)nSEuFX#Auqh$yK*n<`J!OYT@a7TrH!k>tl*~=yv11+?)smU2NxSS{>vW(=sE} zskudOS5pT@!>C+ei|JaW=;d-Xua|&%b7D7FP6E1MKyLMu7e&9eE>})=3s>tlC~}%p z6ZcXeD!i;~yH<9Ww`G%fmsYv@mNu=koA}3(GVs)UEp6~P@95nW+Q3?jyVdUqtI z_4o==gtqJWx#-tLFM}%prB& zGT+cq4K~lL4)nYtevHJpW&=Z@Di3+(6c=A%h$1w;{g7CN-cK$SXDB{|em{v75Y84} zIXlVhZJOJ@U!1@u*&Xeg{M7*VR;MfJ@*T*F1)8l+0aoFgr_5gVUmye%bF@- zE@YZE$TSC%9GcPOOGMMOW^S>7gyGs1ms>oGbHiieCup@*C`_cmhF@C&kt3DtmglUqidO*<90|g*LsTO0}F!q>{a8iqe9*nCO!id(GDv zCi*d+>PIiI`?0>7$3I-PcE3lMZwF$I=trSYj-U}6t{8UV@$T-=Fje?v_qcaZ=UAm3 z!!+;brI_rk_3}?3L37_z^4(9Bx(Lv`@0MaZzHslh^Fn*O^o_4eMOkU-6$E(dVl6@2 z5Bl(w-CAJ$G@~r={ZdS*UDuy1y(+Y~oTa9WX-00-v2|NdvJi|6qK+U_Z@jNvJ*!UU>SCeGe@9!slTJ#y>53vz|9Dg%{D`btJ+C`y$P zy68NTwrLeI=`~kn`K)N@sJT!cEEuxjuV4kovWhrmWpd4xRs>?SJS$+5xHCUORyvgO zeH0V1WoU#ZUe~3)GU?o93bRsDVV=Gf@<~rUAd7vP7cK z1!Ph|n)wJ!Qh_LyDc^{us}91Z@k0GD5y??G1dkjEAs-0Qmbab5>KNN#4Gonh$+LNFE&Je)zZ0~IJZUi$nWGm}PJt;>kpCP^BZ$MSc&tEXfalOBBF~PX1ujElk1v)Mp`nuGkP%~J7t7W0D3y8)8L7Hrf z--t~O4S^8YVN_1be2c*oW)ZIFyuE!&C}ROJ^wr%jt;FA%G*#X+^J z3a3md%+Hs75h%@2Xsb#9CLuJTFkI7xnJ2B$!l}?y83s+Bo9{rw$}k3tF25kLpfEoV znwmHD*{M;Gwxf?_oAZblS&UCZUv}k$Ky?dqbHe`w612+uZffnyZkmEgOtf-^*|aP7 z-c(YueQ!6{tDkoIa#@~|bl5w3b(a^BsdC^dp zb6u_$EnTf!xnP|5X_Z#o;jML7IpXHJV7p`^-G)Ij-R+TR4_CAFP+hB9-KU+)mG024 z$Op!@ay4yKHyNEFYZ4~ox(CDdXb_Ak^gtm-9)vE)|xtQHW4_9M9Xb3_x zF-Ob52>Ik;X|RrV<)E}O3@5hN!VJ&j2z)QsC5=E74Cl#KV5EF-W3V)8Te%W$5LVIW zcfwGaWeDQAIdRuN7A2EB-3*#P)YUSG!qEO!epaA44Kujt66F0-LmEa10*xtWRwki{^j0t*b>Wl$@TPnk&;X#9gv=FJ7MI zYSAb&SsQwLXK7EPdsVcn$6L9w>t}c9l8e@GQY0F0& ztg&OeoVNMaEvz|zq6_P9gMq;hbYacl@NmDbtRWmGZ`qaAv0*CYgS)cSR471Jh1jBF zd55KDPKJ84@rbeEn%j9)l<1*+MOU`f>9+3^@3A00rW=d%^NcVn>Idi_|~^s z6yMa11+s9ys~gL}`B3N@78iN7))nD>wOf}-hnpb}wnF^Cqi;p9;H_?D$xScYcEBvs zoAkTCe~urIT9 zV9LCFqDsf~Z}RVTtbS%A#1^7GeGGdCt5Q8~XZ3nwRm2)4ExAxEd6+^iw@r9Su4^(2 zKP!$3^Fv%aWOk(NfZO*B(&)bSKd$C>2ADdd4tI03*X6b&G&&w0~u)02X zw?3?XLrZR&3xZHM^T$G2N8!=23)t2P)e!4#G6|WI$zSfnk{T=bdtspn^?FXPvv~RN zQ|Kx&4K%RESokXZ>Z!?J1~*UkGZk0JUg|kIhmzJwhIM-iEPQ|8ACH zHv_jic<;N}6o(gj8^#aa%~Hr&aW_jOXRZD$iJYykIR{*G-h5AWg#VJWqbx6Giht?* zCe7R|41EhG%Wz)nFYGBp?qlTHnbcZ8Yee99iezf!IH5(qmcf9kA+urBsL!={e^O@9 zgEE1C`QiTy&))oRXUx;G^(fvvTesin%Snxm)4A>rELx>H?=g^NkaO%n)|8dg@hbyastujMYYk$FmLqo%OU7In#_t})QgF5r#vk`N7W_#^hA=JT$}`rk zW`O{S@+1rUBdo^fDls#R9LVR!$#f;MsXst~COU|2RZWy%x>Nk$@RLf1~Q*L4n{iROM+|QOe%PqIy z{fDx;PPgUJ`IMn7rE#SS!CK_G1G{S2d82T`5U81%2jmq8rarmeQCho(vLs)+r-!m+ zpF42aHFx7-*W&c7;T}^X&I@q&@#T!Y+23SVV!3reGan}on9!wx5UBa$pmYPVxku>+ z;Cu(A>w3z%fKjo06}mc%rTD2wc>HiSAL{bXa28YV{pv;5XI9iCrZ-G=PieOKS$d40 z+(Vffw?Du-7&f$@vJRvLpezU{DCNpJkQRfo92}ssM0{3BAi|{OA!iR$!%ov~83Ml& z2P2f>BPs0$`7~lnukkzb|A<)HP_k&!R#8Q?wXzDM0_4mq3#SYjS;*fufhs%fY84(ysE*Q{}qF*|s|()~Bds)F8_6255BZ*vEWu`nvGFq;H=*WZQHtLoAo=<{=NW)+j@_hcV%6eB#3l zo9@$*7y%=^;w9eeEfyPqT?nP|$-`DjFFkD4H*S=LwhBWO3KUi&623>~SnH9%_vf=?_lf z!Zwrk8irkN1ncu1<5-4)!o!aD#|9)AWpP?#(GEL47=F7J2DJ~t&<)1e3_-Zs3xwbq zCoCbguaIuJfsvH6ggm&mp?bc31*_qgLqwiY+C^=IgK}?#M&QFAVRyzzr#uUj>C0Kh zipzIA!urGnTALhyWSW7GuLgA=Wu4IcjE{PhB|5zqzM6pz0H5L@lcCm0h-Ykw3bP7T zF-#?Aig&H!qpCZrLy^N*U1ceBUT1=3__+CP6IiALo0oE(kD0)dSs|Y_fhEOjho1CD z+j%d3J~#tqk;Bul?@%2+f%UYSEBY~(*0L;h)a(_DpTt;0aVpbrDOG*l6|9+#6aF}4 zY6fYWEnd5*Fu&}_cJJmRA7h!}WqLl&uPgSf{|GCl`KBLV^cYJw+yO&Q%39aB=)!?b zQl*%I+Z)_>ecJ#E9$8}IutDB;LH}hKPEh2vR~;yD$R`Mrw_epj*5wx(B@1_I*3DOK z^$@()1sH;uyu>hZ`^B>#XKy4!V#6(gyko+NkjlWiYC=Fi2S5Kf8tOC;naJuk6V}B#iljF@vgP1 z%7a_EY}zsM{u4F_{%1AI9fD&CX|9$Co=It?jpGtUMm8+#3bGGGDUB!@EJa9OrL7gF50UMDUNF zVQqV2$(U*lD$iI9Vc|C2;%Kb^sI2?)$8SDiO(k*+j*XLJQ0>>nkJdmT+2zi`m~0|P@a^BUJ(5DJalLYpO}ZG zFN817W9g`nhi}hgS8O;HA2)nf?R;x_CVvgJ-Ucvz)ZY(eKL4USC#XIF=wH16sK1)s(<&eq9ahe;4 zUFHM9nm2aZX)Ls5i|eNISM%9jiL&K!S{r;?8Yk9*fU)u1k@I+Ghw<Vt=+GJq+_TRx=a#$$8i_VEq-WL@4!jp`OQaS%{L&FUC#q*UiM9q*5)*AE_PEs;7^PP|6 zn?que4&|&GaaQ2Ps90;|a)<5Vc|BNk6y}ws)*99s8DZ>?_vrj&AsYqrCz$trmNkw@ zwyLgds@hnfFt0z$Rx}B)bk=7nCSy-7b=c=m!x)fmd1;ic`SHI_WkYPiB8cyq%90F~ zB$Vn(V>!W!&GCrlx%|`S*475EqNz3QZVjx3?A+%|x-E*a^s`znCvuN@^4Mv-6mZsS>z`<71v?*R%JXb8^K$cfX!1gyIgQA>D4<4{n(`U z9nRX?y-i#^O6>z8MT9>Eabb5NVF6h>z1QT=#~9*c=4asu3Z!`Yi{u&RaS^ zHH{^)f!r~jeaS}gW7Aneys3>a%^}#rbbytIWS1aW=n!5I;1M%eE&gIZ7RNizVD{9j zr>zm?wtpci>OoV+cu#mM;$t-HWdL`2CBN7Y@h8t<5in|I&w#0H@{$=a3*Y9YGgzjd zb#ouYYt3ZM9kRYbyyr|-yZwC_iudoi>JK}@x<|)Y9)R@<^?(%fn^|xcgA68`bzdg$ z%`rVKmk|(R<8RDlX?8R3s*Qg(la)o{A`mL&>4kk)h~_QEt9AVTCt0)&=Y;(9ES7;= z1%|oR6N`a?17A6Q!nCXM-dNLO@P?WknFx~6b7x~&kQU0|*(|ZKwUv`yj9q562`8^( zE9C8PD_r@as4{@ziIuZ)~@D2%Rvy2n}bR3pjGn+=CHj9s;p``2!S5S zWA2CsiIf>g?75mv4bka<$ouz#x;RAj*XY)e&(d zW@5P!lm$HS0;`7$h(L&HK_st78}d<;Sz2qWsK{PgYS!&jW#eEZVwU0}MNR`wjG+CK zL#4=TSRosO?ND^V?|TtjBN#h8|3z$-R`8WCvP;a#U!5x#TI9K7F3W_Yf}d3m505Ug zf=u4Jh^6|O6~p9-?vNsDgHV7lnEfjd()MGgItY&OEi>}vH(FL=rA#g}vNp<=sx3wG zG+0(@h{4)84q5Hrb{`qFn>l-g^N{4RBkp%E*E~xqzkkTZW7Dy|%#Gt?WW%6#K07t>aOgrDdb6*A@+o%^C{{ zP7mm`x0dF{y@@>k6?#MdZaxm!Wlw0Dm%q(fLK4G#hqb825`lLy7o5O_1va_-1ZTJS zdD2%N6AoVgC6;VA8?Sfr+g@S`4qT6iIg1~73FlOf^3LngJ)2%;x5Dxf+ zE>bhpJRXT2R+%Y(IGSGIg1~o~5r$1h%?k{9Ss$nlY^f|HN`B8PET@%RJRFFvt{r`Rl3J zyFH)3Hyee(o`oM<%#!Tpg(4gGEM~Qw<(EDkjcW0P zB`n^lTDg+nw8YZtAqrELV59puH~O;LE*$&^;f0d3qvTy}*o5<5hgJnk++4(98Pu5E zLpApcrZ|jO8qK8Le*8pV788P(%^FvTGZ`pxgp;RCXEDVMUd0?Niyh$L=lZeu&`_=F z)+$l;xV%wJ-_EDJ#+o}#VOGKP;2+LlwL`-+^RBZtQRT*D>z&V!zh-U1ol7yP$-OA! zjh3>x7%-ccvPW&mk+)oifr9zFcrtiM&II6i$18)2)1`RDQ)mYOWW{jtE6%Ogik%PB6E!`gFa$tlx`rZJ1N z{>E17j;$zLEdS;m))o)?7bm=n+)2209s4UkzK%JH6W6nEb$MHhsN#M0S48#jhyRZD z5aE8j={dLNccJzmc#4iLD- z-hc+f>|NY&1G~r^-k(4F+K;DfVzsm7m^W)5(d8l?@z%GP+@f)X{4fKYm1XyOjv%f zLFJ_7K+)vvD&N5g!P{8MEjL$DP>OiOi$e~Sr#ugA($1{xj%ta@`{fEK*2w*xx9s?m z2&7@US3-AoVPiGl+H#pQ98%NFqc4VG8m-zL#s_Ui>qQq&*^J9XytN>wz5!cUqJPze z4}TKkj30k`3#-*kR9Pn*?}Z1;$1i;Th;Aq(9@}`&ZRlv7SsZ%OS1ugeHMct8 z$`&l3fxPxssAC#$yA|hVwTmCzipAK0dfOa);C2|=<<)ol*d1}45eyH~%DSAyF~jb+ z-#Lnz;q|a^ya_uWZ`UFMZu8HeAfB+vC(s+-igg{LvhZq0MwC#6?yB8zOjrZvnu&UUb`Y$-4czTLrMYr}z#@IG_|sS9L@H?sRx z;U!YRLq1~hAxkv_$vKfykZ8g?e#FN2YJk>|T_!tTJ}uWIdExEz%R@0a{mU>rx>6p1 z)k7S^t`b8nVBuuTwuL>@l!xwQ`-}#NWFGme)$o@emb(M77zxD6+jHNR*DSZT#8$vS zn=(w~Qep{u%NfYp?#gLJEUt$K)$z_Us|wXyUbKmIXycKGHchmP)^QCVw~L*q-Jp6q z>TZ8Z$o;B!*uk~3EynI<@p_8JXYOXTt+rdWo29U&#UJm6DWiWn8NYR{oxwi+K>d8? zz<%Pl#52UJ^qW}rd4-bf#rxv;V|!~6>l4)v_t@TTBMZ}EyTXWp|bvHf&GsX^NDkb%ZTfU+lc##-xJFe59q{5 zVmdLKcnh&FF>5F}9wp`z=Mt9@*Ach*esGpp{NAT*pWT7OBQ}Un`+_aTSyP{LtZnho z{cH;3VQG3_gMGQxdQLrTMTL`o5cVbg2=44L6@2zI@v=qb@jZiF4paOi4i$6+zh%H5 z`|4ie7~(U;+0?pUEm4BwgNUPu^4Dre;P_19BE_GE6DJT0iSrbHPEGI zJ%}8X;n~ULrwq?7BLCaOO~g;FRxaKWpnIc>H=bp0=y*@E`j7b;u| z5FaDrHwO4)V!lOPPf~RF*o6Ey5qlB`Yq`>;J5+|c<7o;|8S0M3t{$OOSC3Gtt4GYHaHY9=tr5y!>v7^#;sV8lWW{73o07jh@ebm>)R4)P zVe&H+Kp7@4A^+cqn~9%SH{|j-V}~WQzJA?=X=*XibC0VBbJb64^<33d*FS?MOsDwR zwa7m`LoGgf=Kac_seUJ~XHFsii`0afOO>FZQd+&CQd+&CQd+$sloo$_L#4BN)`QBQ zHIeunkt?=LQN$0H+#YnZdh^6HSe^87ehp2{666` zZv=jM>y2M^B}UZ2Z<{_g@^xEwBMItOUZ}bK;NhE9LEN_ZP)XZ+bqyl1P zI*f3$Yng?Y%nV%2$$WxEGc5vXM~nJ~Rtt&7Bf&(ZmW-rQMUs||htsij$i(BBxXr9f z4?0Apx9W1~v46B{hFyX8+`dVTk&w4)_lWvh49vpUqU&gJu-Lb>%Ft?q`?{tfUGuY6 zDOwV|#X(703QoVKCm|Q}=dKA_x`F#dK$sUh*VZy{6~d#eB3_Cc`0}=smpo-BHW&4a zXr_7e-ohAAOlsf#U-c+>h`TjP6`~P3pGPz!d0vf>h8941hWq2=y@{8sW9yW*8x8 zE*^0dzx_`=ig&wj@P(HP97xpsBC?i5Rd)+!p3GS+w7yqa-)pV!H8muQRMdP6mAx!{ z8B@#B|3&d~G+gc>Ayy~Kf$~d_L~Gd0OtwbR2QevfP#K@KOQS)SVGX)%xpm|qytp*7 zz8sY9j~{F#2WjaS&oswI-5R*xlysD8kSN4EvflsJxAE^M@lR5SLj#+QtI!-(jWD#v z$?0J%9IG<%+mASm9NS(r{9au8n|`O^I^_qK^Dy)Z2Y@2 zOw$4X?o{_r=BYoNO8@iITy)3g-LrfF@8SX2UUG)=F=T_3ZN{y2+}e-+kwLBT@rq(^2Qh|NhnPd`LcELk0P!(8 zC+hty3FZ@55Z@<$L_9?Nk$6S1kAoOPtV7Hpb|KzHe1Q0vx?IrvJWGQ4#1+K%i60RU z5q~6JQS9p=#t`cea}@8S=lcy&{(kD8Nz?BtQs*)H-Az=$-MbX~)6rUgxANaJU-2)^ z6bDeJ4md∋4poF@l&z%p%@Q?BkQ)PmV{3Q;07TmlEG4ZnaLxe&?SeZfxk6v`gX5 zdk=YZ?y|M(HgDazxtUf0zwz#ur>{Ci6Z~5o9gVg7%7SoY;7J`3L z25{V0QfeJOh2OU~3YUOhFj~~L7z3IeOJe2WL+YH;SosEi__*V7AjGxg*+jTuluC!$ zd~I!s-g|4~#YKoqB_WNM=)Kp%kmDA{mP!aC;fCL)3N|S+J*pB?=~6b7sbQ4@g`h(D z%IGUIRl!#yUk$N6vua%31l5gF-Apx_?z$$;w%Yr?@kPtuSzofv*jZw*Et(OIk>&g7 zTeK}D8@F%Wuwm==jhlYoQL=gG*3B}`hRxeIZ@k`N;01w-l1s%;TSG)E~sDR<_?!DC3WGth$qLShO4E z3XQs6p|0=kYn|=WHKRI@8jjy+)bI87A1m8GQ7Eg|4cVo??RMF0=T)x1qfyl}%BkP& z_8Yd^ujVS_H`CWYn@Fr4t=6C4as0D+q`GJ|C*3pGDcjn$ug)1eq<8c123okNxN_>y zS_7WCI03G@xOc75F+3B8s-Du4ajEhu!*hI2$M7^96GL)ecq*)K@80{uty>K+83)PI zZ{2Q`bd>Ox&Pm1Lt$}yx$Au?oX~Hb&DB&$N!mShDwaqQ;?JXgt*7e8Q<`(w$ws3ri zh0?L|^ja7<`o@{432r5%57138_T+qlW_-cfE8>@!C z8do<#b)!@_Q;nwcHED{~rf04jh3~xc+6&|ELx91~6n#E39sf;2EaU5P^$YC>>y}kx zeOdpYbUQfh%F-Du&2Zeo%~JpVuRcrN!0WTpN#2xy*Gi8yhCHMEL+E;T$P(qhf3@Q9 z{lxEyWr`1|y!8hn$xrD%kWKzuh<%AeiH{QVEgJd*bCqD!EX4_T5{D7T6AOq%#O1{G z#O=fb#FNBJijUcdQHsxeCuwMUVQVEQyp8x5;z;6?M7oVEe3|^Ki2opdO#F)YGqF-p zJ+A39rYrx9g&+)QeFio4j13g9hj^5DhFGOIGmschY)EWF>`oj&e26%SIGwmq)0)Dm z&s{9vhqLv)igo+HAV%o*$U>|`$eMus*zE34z+My)$uiWb| z{Xl;8a+v-yrSNjJlE0iuY)R})>_;3C<&^hu7QbAbVT(EIvT{nZXqkMM&p`KQ17l;R5V;~(p_LiC+_G)GoV3exbNLh!{uAAT}edTi%ug-H3M*hY%koP9zo(=MY~Z zt|YD}mJmN79wwe7mKx-6E57a|MiEnpjflC#PQ+fsfy4)iodsDiOY#uYss;Z VxQlpzc%1kPvAlTS2YR0E{{Xc)z%l>; delta 35959 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 Date: Sun, 10 Nov 2019 21:14:04 -0800 Subject: [PATCH 0234/1439] Fix readme rendering --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e015d3e35..b6d5cb783 100644 --- a/README.md +++ b/README.md @@ -675,7 +675,8 @@ setup( ) ``` -Security contact information ========== +Security contact information +========== To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the From b7b0dc1b73aa3484e7f71db945472bb04aac5a12 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 11 Nov 2019 23:53:07 -0800 Subject: [PATCH 0235/1439] Initial work toward enabling contiguous import section sorting --- isort/parse.py | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/isort/parse.py b/isort/parse.py index 51dbc96f2..b987a14fa 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -1,7 +1,7 @@ """Defines parsing functions used by isort for parsing import definitions""" from collections import OrderedDict, defaultdict, namedtuple from itertools import chain -from typing import TYPE_CHECKING, Any, Dict, Generator, Iterator, List, NamedTuple, Optional, Tuple +from typing import TYPE_CHECKING, Any, Dict, Generator, Iterator, List, NamedTuple, Optional, Tuple, TextIO from warnings import warn from isort.format import format_natural @@ -443,3 +443,32 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte sections=config.sections, section_comments=section_comments, ) + + +def identify_contiguous_imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG): + """Parses stream identifying sections of contiguous imports""" + section_comments = [f"# {heading}" for heading in config.import_headings.values()] + + in_quote = "" + in_top_comment = False + first_comment_index_start = -1 + first_comment_index_end = -1 + + import_lines = [] + for index, line in enumerate(input_stream): + ( + skipping_line, + in_quote, + in_top_comment, + first_comment_index_start, + first_comment_index_end, + ) = skip_line( + line, + in_quote=in_quote, + in_top_comment=in_top_comment, + index=index, + section_comments=section_comments, + first_comment_index_start=first_comment_index_start, + first_comment_index_end=first_comment_index_end, + ) + From d2ea798174502276cee8b67f1f423c9047f247f7 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 12 Nov 2019 22:47:43 -0800 Subject: [PATCH 0236/1439] Sort imports --- isort/parse.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/isort/parse.py b/isort/parse.py index b987a14fa..f502c1daf 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -1,7 +1,18 @@ """Defines parsing functions used by isort for parsing import definitions""" from collections import OrderedDict, defaultdict, namedtuple from itertools import chain -from typing import TYPE_CHECKING, Any, Dict, Generator, Iterator, List, NamedTuple, Optional, Tuple, TextIO +from typing import ( + TYPE_CHECKING, + Any, + Dict, + Generator, + Iterator, + List, + NamedTuple, + Optional, + TextIO, + Tuple, +) from warnings import warn from isort.format import format_natural @@ -454,7 +465,7 @@ def identify_contiguous_imports(input_stream: TextIO, config: Config = DEFAULT_C first_comment_index_start = -1 first_comment_index_end = -1 - import_lines = [] + # import_lines: List[str] = [] for index, line in enumerate(input_stream): ( skipping_line, @@ -471,4 +482,3 @@ def identify_contiguous_imports(input_stream: TextIO, config: Config = DEFAULT_C first_comment_index_start=first_comment_index_start, first_comment_index_end=first_comment_index_end, ) - From 263cb4b2103530a9cc9687c89b54b2471d57e6ca Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 13 Nov 2019 23:53:19 -0800 Subject: [PATCH 0237/1439] Updated signature --- isort/parse.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/isort/parse.py b/isort/parse.py index f502c1daf..c1868cffb 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -456,7 +456,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte ) -def identify_contiguous_imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG): +def identify_contiguous_imports(input_stream: TextIO, output_stream: TextIO=None, config: Config = DEFAULT_CONFIG): """Parses stream identifying sections of contiguous imports""" section_comments = [f"# {heading}" for heading in config.import_headings.values()] @@ -465,7 +465,7 @@ def identify_contiguous_imports(input_stream: TextIO, config: Config = DEFAULT_C first_comment_index_start = -1 first_comment_index_end = -1 - # import_lines: List[str] = [] + import_lines: List[str] = [] for index, line in enumerate(input_stream): ( skipping_line, @@ -482,3 +482,16 @@ def identify_contiguous_imports(input_stream: TextIO, config: Config = DEFAULT_C first_comment_index_start=first_comment_index_start, first_comment_index_end=first_comment_index_end, ) + if skipping_line: + if input_lines: + # sort_imports_here + output_stream.write(line) + continue + + else + + + + + + From 82014818f5dea8da95c0efd0ee9c210e3a36cd66 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 14 Nov 2019 23:31:24 -0800 Subject: [PATCH 0238/1439] Fix conditional statement to utilize correct variable --- isort/parse.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/isort/parse.py b/isort/parse.py index c1868cffb..fd34468fe 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -123,6 +123,11 @@ def skip_line( break char_index += 1 + if ";" in line: + for part in (part.strip() for part in line.split(";")): + if part and not part.startswith("from ") and not part.startswith("import "): + skip_line = True + return ( bool(skip_line or in_quote or in_top_comment), in_quote, @@ -218,10 +223,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte place_imports[section] = [] import_placements[line] = section - if ";" in line: - for part in (part.strip() for part in line.split(";")): - if part and not part.startswith("from ") and not part.startswith("import "): - skipping_line = True + type_of_import: str = import_type(line) or "" if not type_of_import or skipping_line: @@ -483,7 +485,7 @@ def identify_contiguous_imports(input_stream: TextIO, output_stream: TextIO=None first_comment_index_end=first_comment_index_end, ) if skipping_line: - if input_lines: + if import_lines: # sort_imports_here output_stream.write(line) continue From a657f108b8ee0e439f0e49b0049f643e1d0b21be Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 15 Nov 2019 01:23:44 -0800 Subject: [PATCH 0239/1439] Move normalization to reusable function --- isort/parse.py | 47 ++++++++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/isort/parse.py b/isort/parse.py index fd34468fe..36d992f4f 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -1,5 +1,6 @@ """Defines parsing functions used by isort for parsing import definitions""" from collections import OrderedDict, defaultdict, namedtuple +from io import StringIO from itertools import chain from typing import ( TYPE_CHECKING, @@ -48,6 +49,18 @@ def _infer_line_separator(file_contents: str) -> str: return "\n" +def _normalize_line(raw_line: str) -> Tuple[str, str]: + """Normalizes import related statements in the provided line. + + Returns (normalized_line: str, raw_line: str) + """ + line = raw_line.replace("from.import ", "from . import ") + line = line.replace("import*", "import *") + line = line.replace(" .import ", " . import ") + line = line.replace("\t", " ") + return (line, raw_line) + + def import_type(line: str) -> Optional[str]: """If the current line is an import line it will return its type (from or straight)""" if "isort:skip" in line or "NOQA" in line: @@ -190,13 +203,9 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte first_comment_index_start = -1 first_comment_index_end = -1 while index < line_count: - raw_line = line = in_lines[index] - line = line.replace("from.import ", "from . import ") - line = line.replace("\t", " ").replace("import*", "import *") - line = line.replace(" .import ", " . import ") + line = in_lines[index] index += 1 statement_index = index - ( skipping_line, in_quote, @@ -223,17 +232,17 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte place_imports[section] = [] import_placements[line] = section - - - type_of_import: str = import_type(line) or "" - if not type_of_import or skipping_line: - out_lines.append(raw_line) + if skipping_line: + out_lines.append(line) continue - for line in (line.strip() for line in line.split(";")): + for line in ( + (line.strip() for line in line.split(";")) if ";" in line else (line,) # type: ignore + ): + line, raw_line = _normalize_line(line) type_of_import = import_type(line) or "" if not type_of_import: - out_lines.append(line) + out_lines.append(raw_line) continue if import_index == -1: @@ -458,9 +467,12 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte ) -def identify_contiguous_imports(input_stream: TextIO, output_stream: TextIO=None, config: Config = DEFAULT_CONFIG): +def identify_contiguous_imports( + input_stream: TextIO, output_stream: TextIO = None, config: Config = DEFAULT_CONFIG +): """Parses stream identifying sections of contiguous imports""" section_comments = [f"# {heading}" for heading in config.import_headings.values()] + output_stream = StringIO() if output_stream is None else output_stream in_quote = "" in_top_comment = False @@ -487,13 +499,6 @@ def identify_contiguous_imports(input_stream: TextIO, output_stream: TextIO=None if skipping_line: if import_lines: # sort_imports_here + pass output_stream.write(line) continue - - else - - - - - - From 9f4751e2149ef75ecb53aa3c003c1d703e5edee6 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 15 Nov 2019 22:57:05 -0800 Subject: [PATCH 0240/1439] Update changelog to note new contiguous behaviour --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf758729b..dffc26784 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Changelog - `--apply` option has been removed as it is the default behaviour. - isort now does nothing, beyond giving instructions and exiting status code 0, when ran with no arguments. - a new `--interactive` flag has been added to enable the old style behaviour. + - isort now works on contiguous sections of imports, instead of one whole file at a time. Internal: - isort now utilizes mypy and typing to filter out typing related issues before deployment. From f978a8bd892776c498534b79575cd4b771cc1644 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 16 Nov 2019 21:38:10 -0800 Subject: [PATCH 0241/1439] Start moving import block identification to contiguous section --- isort/parse.py | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/isort/parse.py b/isort/parse.py index 36d992f4f..42aa2c392 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -496,6 +496,92 @@ def identify_contiguous_imports( first_comment_index_start=first_comment_index_start, first_comment_index_end=first_comment_index_end, ) + type_of_import = import_type(line) or "" + if not type_of_import: + out_lines.append(raw_line) + continue + + if import_index == -1: + import_index = index - 1 + nested_comments = {} + import_string, comment = parse_comments(line) + comments = [comment] if comment else [] + line_parts = [part for part in _strip_syntax(import_string).strip().split(" ") if part] + if ( + type_of_import == "from" + and len(line_parts) == 2 + and line_parts[1] != "*" + and comments + ): + nested_comments[line_parts[-1]] = comments[0] + + if "(" in line.split("#")[0] and index < line_count: + while not line.strip().endswith(")") and index < line_count: + line, new_comment = parse_comments(in_lines[index]) + index += 1 + if new_comment: + comments.append(new_comment) + stripped_line = _strip_syntax(line).strip() + if ( + type_of_import == "from" + and stripped_line + and " " not in stripped_line + and new_comment + ): + nested_comments[stripped_line] = comments[-1] + import_string += line_separator + line + else: + while line.strip().endswith("\\"): + line, new_comment = parse_comments(in_lines[index]) + index += 1 + if new_comment: + comments.append(new_comment) + + # Still need to check for parentheses after an escaped line + if ( + "(" in line.split("#")[0] + and ")" not in line.split("#")[0] + and index < line_count + ): + stripped_line = _strip_syntax(line).strip() + if ( + type_of_import == "from" + and stripped_line + and " " not in stripped_line + and new_comment + ): + nested_comments[stripped_line] = comments[-1] + import_string += line_separator + line + + while not line.strip().endswith(")") and index < line_count: + line, new_comment = parse_comments(in_lines[index]) + index += 1 + if new_comment: + comments.append(new_comment) + stripped_line = _strip_syntax(line).strip() + if ( + type_of_import == "from" + and stripped_line + and " " not in stripped_line + and new_comment + ): + nested_comments[stripped_line] = comments[-1] + import_string += line_separator + line + + stripped_line = _strip_syntax(line).strip() + if ( + type_of_import == "from" + and stripped_line + and " " not in stripped_line + and new_comment + ): + nested_comments[stripped_line] = comments[-1] + if import_string.strip().endswith(" import") or line.strip().startswith( + "import " + ): + import_string += line_separator + line + else: + import_string = import_string.rstrip().rstrip("\\") + " " + line.lstrip() if skipping_line: if import_lines: # sort_imports_here From f2a2448b7e3a92107d285601db504b0cb96e7324 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 17 Nov 2019 20:24:10 -0800 Subject: [PATCH 0242/1439] Add extra newline to differentiate sections --- isort/parse.py | 1 + 1 file changed, 1 insertion(+) diff --git a/isort/parse.py b/isort/parse.py index 42aa2c392..73e2917e7 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -582,6 +582,7 @@ def identify_contiguous_imports( import_string += line_separator + line else: import_string = import_string.rstrip().rstrip("\\") + " " + line.lstrip() + if skipping_line: if import_lines: # sort_imports_here From cbb62cbc222511bc551c59301aa967c5407f20e8 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 18 Nov 2019 22:24:14 -0800 Subject: [PATCH 0243/1439] website -> homepage --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2b87e8931..fd6d075e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ authors = ["Timothy Crosley "] license = "MIT" readme = "README.md" repository = "https://github.com/timothycrosley/isort" -website = "http://timothycrosley.github.io/isort/" +homepage = "http://timothycrosley.github.io/isort/" keywords = ["Refactor", "Lint", "Imports", "Sort", "Clean"] classifiers = [ "Development Status :: 6 - Mature", From 28322bc4de4e3c47373156de113f579fa44f882a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 20 Nov 2019 23:46:39 -0800 Subject: [PATCH 0244/1439] Remove nested comments from import block identifcation code --- isort/parse.py | 38 +------------------------------------- 1 file changed, 1 insertion(+), 37 deletions(-) diff --git a/isort/parse.py b/isort/parse.py index 73e2917e7..02c1d630e 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -503,18 +503,10 @@ def identify_contiguous_imports( if import_index == -1: import_index = index - 1 - nested_comments = {} + import_string, comment = parse_comments(line) comments = [comment] if comment else [] line_parts = [part for part in _strip_syntax(import_string).strip().split(" ") if part] - if ( - type_of_import == "from" - and len(line_parts) == 2 - and line_parts[1] != "*" - and comments - ): - nested_comments[line_parts[-1]] = comments[0] - if "(" in line.split("#")[0] and index < line_count: while not line.strip().endswith(")") and index < line_count: line, new_comment = parse_comments(in_lines[index]) @@ -522,13 +514,6 @@ def identify_contiguous_imports( if new_comment: comments.append(new_comment) stripped_line = _strip_syntax(line).strip() - if ( - type_of_import == "from" - and stripped_line - and " " not in stripped_line - and new_comment - ): - nested_comments[stripped_line] = comments[-1] import_string += line_separator + line else: while line.strip().endswith("\\"): @@ -544,13 +529,6 @@ def identify_contiguous_imports( and index < line_count ): stripped_line = _strip_syntax(line).strip() - if ( - type_of_import == "from" - and stripped_line - and " " not in stripped_line - and new_comment - ): - nested_comments[stripped_line] = comments[-1] import_string += line_separator + line while not line.strip().endswith(")") and index < line_count: @@ -559,23 +537,9 @@ def identify_contiguous_imports( if new_comment: comments.append(new_comment) stripped_line = _strip_syntax(line).strip() - if ( - type_of_import == "from" - and stripped_line - and " " not in stripped_line - and new_comment - ): - nested_comments[stripped_line] = comments[-1] import_string += line_separator + line stripped_line = _strip_syntax(line).strip() - if ( - type_of_import == "from" - and stripped_line - and " " not in stripped_line - and new_comment - ): - nested_comments[stripped_line] = comments[-1] if import_string.strip().endswith(" import") or line.strip().startswith( "import " ): From 3b4c669058baccafb9d7dafa0cd25d2c317162f7 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 21 Nov 2019 23:42:42 -0800 Subject: [PATCH 0245/1439] Start moving line index usage out, relying instead on streaming through file --- isort/parse.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/isort/parse.py b/isort/parse.py index 02c1d630e..1239359ba 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -507,17 +507,17 @@ def identify_contiguous_imports( import_string, comment = parse_comments(line) comments = [comment] if comment else [] line_parts = [part for part in _strip_syntax(import_string).strip().split(" ") if part] - if "(" in line.split("#")[0] and index < line_count: - while not line.strip().endswith(")") and index < line_count: - line, new_comment = parse_comments(in_lines[index]) - index += 1 + if "(" in line.split("#")[0]: + while not line.strip().endswith(")"): + line, new_comment = parse_comments(line) if new_comment: comments.append(new_comment) stripped_line = _strip_syntax(line).strip() import_string += line_separator + line + line = input_stream.readline() else: while line.strip().endswith("\\"): - line, new_comment = parse_comments(in_lines[index]) + line, new_comment = parse_comments(line) index += 1 if new_comment: comments.append(new_comment) @@ -531,9 +531,8 @@ def identify_contiguous_imports( stripped_line = _strip_syntax(line).strip() import_string += line_separator + line - while not line.strip().endswith(")") and index < line_count: - line, new_comment = parse_comments(in_lines[index]) - index += 1 + while not line.strip().endswith(")"): + line, new_comment = parse_comments() if new_comment: comments.append(new_comment) stripped_line = _strip_syntax(line).strip() From f02a2a09601d667084778beeaad628fe8544f680 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 4 Dec 2019 23:23:58 -0800 Subject: [PATCH 0246/1439] Initial import section identification --- isort/parse.py | 120 +++++++++++++++++++------------------------------ 1 file changed, 45 insertions(+), 75 deletions(-) diff --git a/isort/parse.py b/isort/parse.py index 1239359ba..c4614efe4 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -22,6 +22,8 @@ from .comments import parse as parse_comments from .finders import FindersManager +IMPORT_START_IDENTIFIERS = ("from ", "from.import", "import ", "import*") + if TYPE_CHECKING: from mypy_extensions import TypedDict @@ -471,84 +473,52 @@ def identify_contiguous_imports( input_stream: TextIO, output_stream: TextIO = None, config: Config = DEFAULT_CONFIG ): """Parses stream identifying sections of contiguous imports""" - section_comments = [f"# {heading}" for heading in config.import_headings.values()] output_stream = StringIO() if output_stream is None else output_stream - - in_quote = "" - in_top_comment = False - first_comment_index_start = -1 - first_comment_index_end = -1 - - import_lines: List[str] = [] + import_section = [] + in_quote: str = "" + first_comment_index_start: int = -1 + first_comment_index_end: int = -1 for index, line in enumerate(input_stream): - ( - skipping_line, - in_quote, - in_top_comment, - first_comment_index_start, - first_comment_index_end, - ) = skip_line( - line, - in_quote=in_quote, - in_top_comment=in_top_comment, - index=index, - section_comments=section_comments, - first_comment_index_start=first_comment_index_start, - first_comment_index_end=first_comment_index_end, - ) - type_of_import = import_type(line) or "" - if not type_of_import: - out_lines.append(raw_line) - continue - - if import_index == -1: - import_index = index - 1 - - import_string, comment = parse_comments(line) - comments = [comment] if comment else [] - line_parts = [part for part in _strip_syntax(import_string).strip().split(" ") if part] - if "(" in line.split("#")[0]: - while not line.strip().endswith(")"): - line, new_comment = parse_comments(line) - if new_comment: - comments.append(new_comment) - stripped_line = _strip_syntax(line).strip() - import_string += line_separator + line - line = input_stream.readline() - else: - while line.strip().endswith("\\"): - line, new_comment = parse_comments(line) - index += 1 - if new_comment: - comments.append(new_comment) - - # Still need to check for parentheses after an escaped line - if ( - "(" in line.split("#")[0] - and ")" not in line.split("#")[0] - and index < line_count - ): - stripped_line = _strip_syntax(line).strip() - import_string += line_separator + line + if '"' in line or "'" in line: + char_index = 0 + if first_comment_index_start == -1 and (line.startswith('"') or line.startswith("'")): + first_comment_index_start = index + while char_index < len(line): + if line[char_index] == "\\": + char_index += 1 + elif in_quote: + if line[char_index : char_index + len(in_quote)] == in_quote: + in_quote = "" + if first_comment_index_end < first_comment_index_start: + first_comment_index_end = index + elif line[char_index] in ("'", '"'): + long_quote = line[char_index : char_index + 3] + if long_quote in ('"""', "'''"): + in_quote = long_quote + char_index += 2 + else: + in_quote = line[char_index] + elif line[char_index] == "#": + break + char_index += 1 - while not line.strip().endswith(")"): - line, new_comment = parse_comments() - if new_comment: - comments.append(new_comment) - stripped_line = _strip_syntax(line).strip() - import_string += line_separator + line + not_imports = in_quote + if not in_quote: + stripped_line = line.strip() + if not stripped_line or stripped_line.startswith("#"): + import_section.append(line) + elif stripped_line.startswith(IMPORT_START_IDENTIFIERS): + import_section.append(line) + else: + not_imports = True - stripped_line = _strip_syntax(line).strip() - if import_string.strip().endswith(" import") or line.strip().startswith( - "import " - ): - import_string += line_separator + line - else: - import_string = import_string.rstrip().rstrip("\\") + " " + line.lstrip() + if not_imports: + for import_line in import_section: + output_stream.write("AN IMPORT") + output_stream.write(config.line_ending or '\n') - if skipping_line: - if import_lines: - # sort_imports_here - pass output_stream.write(line) - continue + not_imports = False + + output_stream.seek(0) + return output_stream From c805531052e7aa6ecceb595e134f76c68bae2902 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 4 Dec 2019 23:34:50 -0800 Subject: [PATCH 0247/1439] First semi-accurate import sectionining --- isort/parse.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/isort/parse.py b/isort/parse.py index c4614efe4..9c553643f 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -474,7 +474,7 @@ def identify_contiguous_imports( ): """Parses stream identifying sections of contiguous imports""" output_stream = StringIO() if output_stream is None else output_stream - import_section = [] + import_section: str = "" in_quote: str = "" first_comment_index_start: int = -1 first_comment_index_end: int = -1 @@ -506,16 +506,21 @@ def identify_contiguous_imports( if not in_quote: stripped_line = line.strip() if not stripped_line or stripped_line.startswith("#"): - import_section.append(line) + import_section += line elif stripped_line.startswith(IMPORT_START_IDENTIFIERS): - import_section.append(line) + import_section += line else: not_imports = True if not_imports: - for import_line in import_section: - output_stream.write("AN IMPORT") - output_stream.write(config.line_ending or '\n') + if import_section: + if not import_section.strip() or not ("from" in import_section or "import" in import_section): + output_stream.write(import_section) + else: + for line in import_section.split(config.line_ending or '\n'): + output_stream.write("AN IMPORT") + output_stream.write(config.line_ending or '\n') + import_section = "" output_stream.write(line) not_imports = False From 0c4c155fa4adc05bdb157cbcff12f17b3e4392e7 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 5 Dec 2019 21:22:10 -0800 Subject: [PATCH 0248/1439] Only sort on sections that contain imports --- isort/parse.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/isort/parse.py b/isort/parse.py index 9c553643f..f5c792d5e 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -478,6 +478,7 @@ def identify_contiguous_imports( in_quote: str = "" first_comment_index_start: int = -1 first_comment_index_end: int = -1 + contains_imports: bool = False for index, line in enumerate(input_stream): if '"' in line or "'" in line: char_index = 0 @@ -509,18 +510,20 @@ def identify_contiguous_imports( import_section += line elif stripped_line.startswith(IMPORT_START_IDENTIFIERS): import_section += line + contains_imports = True else: not_imports = True if not_imports: if import_section: - if not import_section.strip() or not ("from" in import_section or "import" in import_section): + if not contains_imports: output_stream.write(import_section) else: for line in import_section.split(config.line_ending or '\n'): output_stream.write("AN IMPORT") output_stream.write(config.line_ending or '\n') import_section = "" + contains_imports = False output_stream.write(line) not_imports = False From 896d6ce0ca81f49fee5816e1af55dcde3e51c075 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 6 Dec 2019 23:03:44 -0800 Subject: [PATCH 0249/1439] Attempt to deal with \ and ( \n \n ) continuouations --- isort/parse.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/isort/parse.py b/isort/parse.py index f5c792d5e..b5b285ae3 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -510,6 +510,22 @@ def identify_contiguous_imports( import_section += line elif stripped_line.startswith(IMPORT_START_IDENTIFIERS): import_section += line + if "(" in stripped_line and not ")" in stripped_line: + nested_line = line + nested_stripped_line = nested_line.strip() + while not ")" in nested_stripped_line: + nested_line = input_stream.readline() + nested_stripped_line = nested_line.strip() + import_section += nested_line + + if stripped_line.endswith("\\"): + nested_line = line + nested_stripped_line = nested_line.strip() + while nested_line and nested_stripped_line.endswith("\\"): + nested_line = input_stream.readline() + nested_stripped_line = nested_line.strip() + import_section += nested_line + contains_imports = True else: not_imports = True From 40e861d43bd57ca4562491ebd7d62f5a25b2b13e Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 7 Dec 2019 23:28:12 -0800 Subject: [PATCH 0250/1439] Ignore comments when determining end of imports --- isort/parse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/parse.py b/isort/parse.py index b5b285ae3..e59998227 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -512,7 +512,7 @@ def identify_contiguous_imports( import_section += line if "(" in stripped_line and not ")" in stripped_line: nested_line = line - nested_stripped_line = nested_line.strip() + nested_stripped_line = nested_line.strip().split("#")[0] while not ")" in nested_stripped_line: nested_line = input_stream.readline() nested_stripped_line = nested_line.strip() From 8c758856c69de5412f6e4daa551b79cc7f3c1bfb Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 8 Dec 2019 23:33:10 -0800 Subject: [PATCH 0251/1439] Explicity specify no output type --- isort/parse.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/isort/parse.py b/isort/parse.py index e59998227..1681c6e3b 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -470,10 +470,9 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte def identify_contiguous_imports( - input_stream: TextIO, output_stream: TextIO = None, config: Config = DEFAULT_CONFIG -): + input_stream: TextIO, output_stream: TextIO, config: Config = DEFAULT_CONFIG +) -> None: """Parses stream identifying sections of contiguous imports""" - output_stream = StringIO() if output_stream is None else output_stream import_section: str = "" in_quote: str = "" first_comment_index_start: int = -1 @@ -543,6 +542,3 @@ def identify_contiguous_imports( output_stream.write(line) not_imports = False - - output_stream.seek(0) - return output_stream From 34f8e8bb56a3e92504d991ea1f852c29e5b04efa Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 8 Dec 2019 23:37:33 -0800 Subject: [PATCH 0252/1439] Update documentation for sort_imports --- isort/parse.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/isort/parse.py b/isort/parse.py index 1681c6e3b..6acf01581 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -469,10 +469,18 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte ) -def identify_contiguous_imports( +def sort_imports( input_stream: TextIO, output_stream: TextIO, config: Config = DEFAULT_CONFIG ) -> None: - """Parses stream identifying sections of contiguous imports""" + """Parses stream identifying sections of contiguous imports and sorting them + + Code with unsorted imports is read from the provided `input_stream`, sorted and then + outputted to the specified output_stream. + + - `input_stream`: Text stream with unsorted import sections. + - `output_stream`: Text stream to output sorted inputs into. + - `config`: Config settings to use when sorting imports. Defaults settings.DEFAULT_CONFIG. + """ import_section: str = "" in_quote: str = "" first_comment_index_start: int = -1 From d3568d74c2a1f8530d7fcd71a036ee08e531dfd4 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 9 Dec 2019 23:46:59 -0800 Subject: [PATCH 0253/1439] Move sort imports to API --- isort/api.py | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++ isort/parse.py | 83 -------------------------------------------------- 2 files changed, 83 insertions(+), 83 deletions(-) diff --git a/isort/api.py b/isort/api.py index 25dc2be31..5e3a85bc2 100644 --- a/isort/api.py +++ b/isort/api.py @@ -121,3 +121,86 @@ def sorted_file(filename: str, config: Config = DEFAULT_CONFIG, **config_kwargs) file_path=file_data.path, **config_kwargs, ) + + +def sort_imports( + input_stream: TextIO, output_stream: TextIO, config: Config = DEFAULT_CONFIG +) -> None: + """Parses stream identifying sections of contiguous imports and sorting them + + Code with unsorted imports is read from the provided `input_stream`, sorted and then + outputted to the specified output_stream. + + - `input_stream`: Text stream with unsorted import sections. + - `output_stream`: Text stream to output sorted inputs into. + - `config`: Config settings to use when sorting imports. Defaults settings.DEFAULT_CONFIG. + """ + import_section: str = "" + in_quote: str = "" + first_comment_index_start: int = -1 + first_comment_index_end: int = -1 + contains_imports: bool = False + for index, line in enumerate(input_stream): + if '"' in line or "'" in line: + char_index = 0 + if first_comment_index_start == -1 and (line.startswith('"') or line.startswith("'")): + first_comment_index_start = index + while char_index < len(line): + if line[char_index] == "\\": + char_index += 1 + elif in_quote: + if line[char_index : char_index + len(in_quote)] == in_quote: + in_quote = "" + if first_comment_index_end < first_comment_index_start: + first_comment_index_end = index + elif line[char_index] in ("'", '"'): + long_quote = line[char_index : char_index + 3] + if long_quote in ('"""', "'''"): + in_quote = long_quote + char_index += 2 + else: + in_quote = line[char_index] + elif line[char_index] == "#": + break + char_index += 1 + + not_imports = in_quote + if not in_quote: + stripped_line = line.strip() + if not stripped_line or stripped_line.startswith("#"): + import_section += line + elif stripped_line.startswith(IMPORT_START_IDENTIFIERS): + import_section += line + if "(" in stripped_line and not ")" in stripped_line: + nested_line = line + nested_stripped_line = nested_line.strip().split("#")[0] + while not ")" in nested_stripped_line: + nested_line = input_stream.readline() + nested_stripped_line = nested_line.strip() + import_section += nested_line + + if stripped_line.endswith("\\"): + nested_line = line + nested_stripped_line = nested_line.strip() + while nested_line and nested_stripped_line.endswith("\\"): + nested_line = input_stream.readline() + nested_stripped_line = nested_line.strip() + import_section += nested_line + + contains_imports = True + else: + not_imports = True + + if not_imports: + if import_section: + if not contains_imports: + output_stream.write(import_section) + else: + for line in import_section.split(config.line_ending or '\n'): + output_stream.write("AN IMPORT") + output_stream.write(config.line_ending or '\n') + import_section = "" + contains_imports = False + + output_stream.write(line) + not_imports = False diff --git a/isort/parse.py b/isort/parse.py index 6acf01581..4d13af8de 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -467,86 +467,3 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte sections=config.sections, section_comments=section_comments, ) - - -def sort_imports( - input_stream: TextIO, output_stream: TextIO, config: Config = DEFAULT_CONFIG -) -> None: - """Parses stream identifying sections of contiguous imports and sorting them - - Code with unsorted imports is read from the provided `input_stream`, sorted and then - outputted to the specified output_stream. - - - `input_stream`: Text stream with unsorted import sections. - - `output_stream`: Text stream to output sorted inputs into. - - `config`: Config settings to use when sorting imports. Defaults settings.DEFAULT_CONFIG. - """ - import_section: str = "" - in_quote: str = "" - first_comment_index_start: int = -1 - first_comment_index_end: int = -1 - contains_imports: bool = False - for index, line in enumerate(input_stream): - if '"' in line or "'" in line: - char_index = 0 - if first_comment_index_start == -1 and (line.startswith('"') or line.startswith("'")): - first_comment_index_start = index - while char_index < len(line): - if line[char_index] == "\\": - char_index += 1 - elif in_quote: - if line[char_index : char_index + len(in_quote)] == in_quote: - in_quote = "" - if first_comment_index_end < first_comment_index_start: - first_comment_index_end = index - elif line[char_index] in ("'", '"'): - long_quote = line[char_index : char_index + 3] - if long_quote in ('"""', "'''"): - in_quote = long_quote - char_index += 2 - else: - in_quote = line[char_index] - elif line[char_index] == "#": - break - char_index += 1 - - not_imports = in_quote - if not in_quote: - stripped_line = line.strip() - if not stripped_line or stripped_line.startswith("#"): - import_section += line - elif stripped_line.startswith(IMPORT_START_IDENTIFIERS): - import_section += line - if "(" in stripped_line and not ")" in stripped_line: - nested_line = line - nested_stripped_line = nested_line.strip().split("#")[0] - while not ")" in nested_stripped_line: - nested_line = input_stream.readline() - nested_stripped_line = nested_line.strip() - import_section += nested_line - - if stripped_line.endswith("\\"): - nested_line = line - nested_stripped_line = nested_line.strip() - while nested_line and nested_stripped_line.endswith("\\"): - nested_line = input_stream.readline() - nested_stripped_line = nested_line.strip() - import_section += nested_line - - contains_imports = True - else: - not_imports = True - - if not_imports: - if import_section: - if not contains_imports: - output_stream.write(import_section) - else: - for line in import_section.split(config.line_ending or '\n'): - output_stream.write("AN IMPORT") - output_stream.write(config.line_ending or '\n') - import_section = "" - contains_imports = False - - output_stream.write(line) - not_imports = False From d37c587c54b28c7807ef760ef6e085824d7403ff Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 10 Dec 2019 21:57:17 -0800 Subject: [PATCH 0254/1439] Parse imports at each section --- isort/api.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/isort/api.py b/isort/api.py index 5e3a85bc2..53d20eaf4 100644 --- a/isort/api.py +++ b/isort/api.py @@ -193,14 +193,14 @@ def sort_imports( if not_imports: if import_section: + import_section += line if not contains_imports: output_stream.write(import_section) else: - for line in import_section.split(config.line_ending or '\n'): - output_stream.write("AN IMPORT") - output_stream.write(config.line_ending or '\n') - import_section = "" + output_stream.write(output.sorted_imports( + parse.file_contents(import_section, config=config), config, extension + )) contains_imports = False - - output_stream.write(line) - not_imports = False + else: + output_stream.write(line) + not_imports = False From 0de6598ac8d21c56c299e622b6910de35c8a4057 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 11 Dec 2019 23:00:25 -0800 Subject: [PATCH 0255/1439] Update cruft --- .cruft.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.cruft.json b/.cruft.json index 879f8b664..37fced16f 100644 --- a/.cruft.json +++ b/.cruft.json @@ -1,6 +1,6 @@ { "template": "https://github.com/timothycrosley/cookiecutter-python/", - "commit": "d6097b15037ff9e56ea98bef2b564fa3313ac0e0", + "commit": "2d559dc6413338a6ee05f4239039a88a17e7d2a9", "context": { "cookiecutter": { "full_name": "Timothy Crosley", From 2918467ab222eca219460f819168d97b95b32167 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 11 Dec 2019 23:13:38 -0800 Subject: [PATCH 0256/1439] Add missing import for TextIO --- isort/api.py | 2 +- isort/parse.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/isort/api.py b/isort/api.py index 53d20eaf4..b102af695 100644 --- a/isort/api.py +++ b/isort/api.py @@ -1,6 +1,6 @@ import re from pathlib import Path -from typing import Any, NamedTuple, Optional, Tuple +from typing import Any, NamedTuple, Optional, Tuple, TextIO from . import output, parse from .exceptions import ( diff --git a/isort/parse.py b/isort/parse.py index 4d13af8de..0d1ba875b 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -11,7 +11,6 @@ List, NamedTuple, Optional, - TextIO, Tuple, ) from warnings import warn From 2b3b1160f38c107e18b8c0b4190ee95ac710e7dd Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 12 Dec 2019 18:19:25 -0800 Subject: [PATCH 0257/1439] Move over identifier, cast boolean --- isort/api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/isort/api.py b/isort/api.py index b102af695..000314bf1 100644 --- a/isort/api.py +++ b/isort/api.py @@ -14,6 +14,8 @@ from .io import File from .settings import DEFAULT_CONFIG, FILE_SKIP_COMMENT, Config +IMPORT_START_IDENTIFIERS = ("from ", "from.import", "import ", "import*") + def _config( path: Optional[Path] = None, config: Config = DEFAULT_CONFIG, **config_kwargs @@ -164,7 +166,7 @@ def sort_imports( break char_index += 1 - not_imports = in_quote + not_imports = bool(in_quote) if not in_quote: stripped_line = line.strip() if not stripped_line or stripped_line.startswith("#"): From 47aba1507f4f0051271c02899904c557c6a34b27 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 12 Dec 2019 18:41:08 -0800 Subject: [PATCH 0258/1439] Take extension in sort_imports endpoints --- isort/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/api.py b/isort/api.py index 000314bf1..d07385223 100644 --- a/isort/api.py +++ b/isort/api.py @@ -126,7 +126,7 @@ def sorted_file(filename: str, config: Config = DEFAULT_CONFIG, **config_kwargs) def sort_imports( - input_stream: TextIO, output_stream: TextIO, config: Config = DEFAULT_CONFIG + input_stream: TextIO, output_stream: TextIO, extension: str = "py", config: Config = DEFAULT_CONFIG, ) -> None: """Parses stream identifying sections of contiguous imports and sorting them From f9113bbbde3d8fd9328801ef5a9d4fe855e44c14 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 13 Dec 2019 21:26:39 -0800 Subject: [PATCH 0259/1439] Clear import seciton --- isort/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/isort/api.py b/isort/api.py index d07385223..c2c2c78d4 100644 --- a/isort/api.py +++ b/isort/api.py @@ -203,6 +203,7 @@ def sort_imports( parse.file_contents(import_section, config=config), config, extension )) contains_imports = False + import_section = "" else: output_stream.write(line) not_imports = False From bbfea1a2551c1d2c7f5f53983936895f81f0fa7a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 14 Dec 2019 23:52:55 -0800 Subject: [PATCH 0260/1439] Formatting --- isort/api.py | 19 ++++++++++++------- isort/parse.py | 12 +----------- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/isort/api.py b/isort/api.py index c2c2c78d4..6d93f2338 100644 --- a/isort/api.py +++ b/isort/api.py @@ -1,6 +1,6 @@ import re from pathlib import Path -from typing import Any, NamedTuple, Optional, Tuple, TextIO +from typing import Any, NamedTuple, Optional, TextIO, Tuple from . import output, parse from .exceptions import ( @@ -126,7 +126,10 @@ def sorted_file(filename: str, config: Config = DEFAULT_CONFIG, **config_kwargs) def sort_imports( - input_stream: TextIO, output_stream: TextIO, extension: str = "py", config: Config = DEFAULT_CONFIG, + input_stream: TextIO, + output_stream: TextIO, + extension: str = "py", + config: Config = DEFAULT_CONFIG, ) -> None: """Parses stream identifying sections of contiguous imports and sorting them @@ -173,10 +176,10 @@ def sort_imports( import_section += line elif stripped_line.startswith(IMPORT_START_IDENTIFIERS): import_section += line - if "(" in stripped_line and not ")" in stripped_line: + if "(" in stripped_line and ")" not in stripped_line: nested_line = line nested_stripped_line = nested_line.strip().split("#")[0] - while not ")" in nested_stripped_line: + while ")" not in nested_stripped_line: nested_line = input_stream.readline() nested_stripped_line = nested_line.strip() import_section += nested_line @@ -199,9 +202,11 @@ def sort_imports( if not contains_imports: output_stream.write(import_section) else: - output_stream.write(output.sorted_imports( - parse.file_contents(import_section, config=config), config, extension - )) + output_stream.write( + output.sorted_imports( + parse.file_contents(import_section, config=config), config, extension + ) + ) contains_imports = False import_section = "" else: diff --git a/isort/parse.py b/isort/parse.py index 0d1ba875b..c2a835d53 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -2,17 +2,7 @@ from collections import OrderedDict, defaultdict, namedtuple from io import StringIO from itertools import chain -from typing import ( - TYPE_CHECKING, - Any, - Dict, - Generator, - Iterator, - List, - NamedTuple, - Optional, - Tuple, -) +from typing import TYPE_CHECKING, Any, Dict, Generator, Iterator, List, NamedTuple, Optional, Tuple from warnings import warn from isort.format import format_natural From 8517840952c3cff5eff61f4d4bd1092f830e9ff8 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 15 Dec 2019 21:44:59 -0800 Subject: [PATCH 0261/1439] Start removal of comment start and end from parse and output --- isort/output.py | 4 ---- isort/parse.py | 8 +------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/isort/output.py b/isort/output.py index 846dfc4bd..cf02e7b42 100644 --- a/isort/output.py +++ b/isort/output.py @@ -145,8 +145,6 @@ def sorted_imports( output_at = 0 if parsed.import_index < parsed.original_line_count: output_at = parsed.import_index - elif parsed.first_comment_index_end != -1 and parsed.first_comment_index_start <= 2: - output_at = parsed.first_comment_index_end formatted_output[output_at:0] = output imports_tail = output_at + len(output) @@ -168,8 +166,6 @@ def sorted_imports( in_top_comment=False, index=len(formatted_output), section_comments=parsed.section_comments, - first_comment_index_start=parsed.first_comment_index_start, - first_comment_index_end=parsed.first_comment_index_end, ) if not should_skip and line.strip(): if ( diff --git a/isort/parse.py b/isort/parse.py index c2a835d53..03eefa977 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -91,9 +91,7 @@ def skip_line( (skip_line: bool, in_quote: str, - in_top_comment: bool, - first_comment_index_start: int, - first_comment_index_end: int) + in_top_comment: bool) """ skip_line = bool(in_quote) if index == 1 and line.startswith("#"): @@ -150,8 +148,6 @@ class ParsedContent(NamedTuple): as_map: Dict[str, List[str]] imports: Dict[str, Dict[str, Any]] categorized_comments: "CommentsDict" - first_comment_index_start: int - first_comment_index_end: int change_count: int original_line_count: int line_separator: str @@ -448,8 +444,6 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte as_map=as_map, imports=imports, categorized_comments=categorized_comments, - first_comment_index_start=first_comment_index_start, - first_comment_index_end=first_comment_index_end, change_count=change_count, original_line_count=original_line_count, line_separator=line_separator, From 8629b02f3ee801a8def506f3cedae3ecbd8aeb8c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 16 Dec 2019 23:45:22 -0800 Subject: [PATCH 0262/1439] Remove refferences to first commment and top comment in parsing code, move to main API --- isort/api.py | 11 +++++++++-- isort/parse.py | 42 +++++++----------------------------------- 2 files changed, 16 insertions(+), 37 deletions(-) diff --git a/isort/api.py b/isort/api.py index 6d93f2338..882e3cc8b 100644 --- a/isort/api.py +++ b/isort/api.py @@ -145,8 +145,15 @@ def sort_imports( first_comment_index_start: int = -1 first_comment_index_end: int = -1 contains_imports: bool = False + in_top_comment: bool = False for index, line in enumerate(input_stream): - if '"' in line or "'" in line: + if index == 1 and line.startswith("#"): + in_top_comment = True + elif in_top_comment: + if not line.startswith("#") or line in section_comments: + in_top_comment = False + first_comment_index_end = index - 1 + elif '"' in line or "'" in line: char_index = 0 if first_comment_index_start == -1 and (line.startswith('"') or line.startswith("'")): first_comment_index_start = index @@ -169,7 +176,7 @@ def sort_imports( break char_index += 1 - not_imports = bool(in_quote) + not_imports = bool(in_quote) or in_top_comment if not in_quote: stripped_line = line.strip() if not stripped_line or stripped_line.startswith("#"): diff --git a/isort/parse.py b/isort/parse.py index 03eefa977..2671bd83d 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -79,41 +79,25 @@ def _strip_syntax(import_string: str) -> str: def skip_line( line: str, in_quote: str, - in_top_comment: bool, index: int, section_comments: List[str], - first_comment_index_start: int, - first_comment_index_end: int, ) -> Tuple[bool, str, bool, int, int]: """Determine if a given line should be skipped. Returns back a tuple containing: (skip_line: bool, - in_quote: str, - in_top_comment: bool) + in_quote: str,) """ skip_line = bool(in_quote) - if index == 1 and line.startswith("#"): - in_top_comment = True - return (True, in_quote, in_top_comment, first_comment_index_start, first_comment_index_end) - elif in_top_comment: - if not line.startswith("#") or line in section_comments: - in_top_comment = False - first_comment_index_end = index - 1 - if '"' in line or "'" in line: char_index = 0 - if first_comment_index_start == -1 and (line.startswith('"') or line.startswith("'")): - first_comment_index_start = index while char_index < len(line): if line[char_index] == "\\": char_index += 1 elif in_quote: if line[char_index : char_index + len(in_quote)] == in_quote: in_quote = "" - if first_comment_index_end < first_comment_index_start: - first_comment_index_end = index elif line[char_index] in ("'", '"'): long_quote = line[char_index : char_index + 3] if long_quote in ('"""', "'''"): @@ -131,11 +115,8 @@ def skip_line( skip_line = True return ( - bool(skip_line or in_quote or in_top_comment), - in_quote, - in_top_comment, - first_comment_index_start, - first_comment_index_end, + bool(skip_line or in_quote), + in_quote ) @@ -186,9 +167,6 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte index = 0 import_index = -1 in_quote = "" - in_top_comment = False - first_comment_index_start = -1 - first_comment_index_end = -1 while index < line_count: line = in_lines[index] index += 1 @@ -196,17 +174,11 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte ( skipping_line, in_quote, - in_top_comment, - first_comment_index_start, - first_comment_index_end, ) = skip_line( line, in_quote=in_quote, - in_top_comment=in_top_comment, index=index, section_comments=section_comments, - first_comment_index_start=first_comment_index_start, - first_comment_index_end=first_comment_index_end, ) if line in section_comments and not skipping_line: @@ -362,7 +334,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte if comments: categorized_comments["from"].setdefault(import_from, []).extend(comments) - if len(out_lines) > max(import_index, first_comment_index_end + 1, 1) - 1: + if len(out_lines) > max(import_index, 1) - 1: last = out_lines and out_lines[-1].rstrip() or "" while ( last.startswith("#") @@ -375,7 +347,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte ) if ( len(out_lines) - > max(import_index - 1, first_comment_index_end + 1, 1) - 1 + > max(import_index - 1, 1) - 1 ): last = out_lines[-1].rstrip() else: @@ -400,7 +372,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte categorized_comments["straight"][module] = comments comments = [] - if len(out_lines) > max(import_index, first_comment_index_end + 1, 1) - 1: + if len(out_lines) > max(import_index, + 1, 1) - 1: last = out_lines and out_lines[-1].rstrip() or "" while ( @@ -412,7 +384,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte categorized_comments["above"]["straight"].setdefault(module, []).insert( 0, out_lines.pop(-1) ) - if len(out_lines) > 0 and len(out_lines) != first_comment_index_end: + if len(out_lines) > 0 and len(out_lines): last = out_lines[-1].rstrip() else: last = "" From 1a78b0427517a3f8df34a32c502a0e75c3fe17e3 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 18 Dec 2019 23:31:33 -0800 Subject: [PATCH 0263/1439] Fix signature --- isort/parse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/parse.py b/isort/parse.py index 2671bd83d..96dd7d89b 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -81,7 +81,7 @@ def skip_line( in_quote: str, index: int, section_comments: List[str], -) -> Tuple[bool, str, bool, int, int]: +) -> Tuple[bool, str]: """Determine if a given line should be skipped. Returns back a tuple containing: From f5ef546ede9450b19d7c615ab7e7c833f88a7dcf Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 19 Dec 2019 20:52:25 -0800 Subject: [PATCH 0264/1439] Fix linting errors --- isort/api.py | 1 + isort/output.py | 1 - isort/parse.py | 27 ++++++--------------------- 3 files changed, 7 insertions(+), 22 deletions(-) diff --git a/isort/api.py b/isort/api.py index 882e3cc8b..30c9f0f30 100644 --- a/isort/api.py +++ b/isort/api.py @@ -146,6 +146,7 @@ def sort_imports( first_comment_index_end: int = -1 contains_imports: bool = False in_top_comment: bool = False + section_comments = [f"# {heading}" for heading in config.import_headings.values()] for index, line in enumerate(input_stream): if index == 1 and line.startswith("#"): in_top_comment = True diff --git a/isort/output.py b/isort/output.py index cf02e7b42..a6b4a7a90 100644 --- a/isort/output.py +++ b/isort/output.py @@ -163,7 +163,6 @@ def sorted_imports( should_skip, _in_quote, *_ = parse.skip_line( line, in_quote=_in_quote, - in_top_comment=False, index=len(formatted_output), section_comments=parsed.section_comments, ) diff --git a/isort/parse.py b/isort/parse.py index 96dd7d89b..1b4591199 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -77,10 +77,7 @@ def _strip_syntax(import_string: str) -> str: def skip_line( - line: str, - in_quote: str, - index: int, - section_comments: List[str], + line: str, in_quote: str, index: int, section_comments: List[str] ) -> Tuple[bool, str]: """Determine if a given line should be skipped. @@ -114,10 +111,7 @@ def skip_line( if part and not part.startswith("from ") and not part.startswith("import "): skip_line = True - return ( - bool(skip_line or in_quote), - in_quote - ) + return (bool(skip_line or in_quote), in_quote) class ParsedContent(NamedTuple): @@ -171,14 +165,8 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte line = in_lines[index] index += 1 statement_index = index - ( - skipping_line, - in_quote, - ) = skip_line( - line, - in_quote=in_quote, - index=index, - section_comments=section_comments, + (skipping_line, in_quote) = skip_line( + line, in_quote=in_quote, index=index, section_comments=section_comments ) if line in section_comments and not skipping_line: @@ -345,10 +333,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte categorized_comments["above"]["from"].setdefault(import_from, []).insert( 0, out_lines.pop(-1) ) - if ( - len(out_lines) - > max(import_index - 1, 1) - 1 - ): + if len(out_lines) > max(import_index - 1, 1) - 1: last = out_lines[-1].rstrip() else: last = "" @@ -372,7 +357,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte categorized_comments["straight"][module] = comments comments = [] - if len(out_lines) > max(import_index, + 1, 1) - 1: + if len(out_lines) > max(import_index, +1, 1) - 1: last = out_lines and out_lines[-1].rstrip() or "" while ( From 2ba782fb1a74294f2a67cd64fe4b6bf98dddba30 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 20 Dec 2019 23:56:15 -0800 Subject: [PATCH 0265/1439] Use new congiuous import sorter --- isort/api.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/isort/api.py b/isort/api.py index 30c9f0f30..768866252 100644 --- a/isort/api.py +++ b/isort/api.py @@ -1,4 +1,5 @@ import re +from io import StringIO from pathlib import Path from typing import Any, NamedTuple, Optional, TextIO, Tuple @@ -62,9 +63,10 @@ def sorted_imports( except SyntaxError: raise ExistingSyntaxErrors(content_source) - parsed_output = output.sorted_imports( - parse.file_contents(file_contents, config=config), config, extension - ) + parsed_output = StringIO() + sort_imports(StringIO(file_contents), parsed_output, extension=extension, config=config) + parsed_output.seek(0) + parsed_output = parsed_output.read() if config.atomic: try: compile(file_contents, content_source, "exec", 0, 1) From 026b48ed0d6970f0893f8acd3627ac0e513dbacf Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 21 Dec 2019 22:30:05 -0800 Subject: [PATCH 0266/1439] Sort imports, even when imports are last section of file --- isort/api.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/isort/api.py b/isort/api.py index 768866252..20f2ba5fb 100644 --- a/isort/api.py +++ b/isort/api.py @@ -222,3 +222,16 @@ def sort_imports( else: output_stream.write(line) not_imports = False + + if import_section: + import_section += line + if not contains_imports: + output_stream.write(import_section) + else: + output_stream.write( + output.sorted_imports( + parse.file_contents(import_section, config=config), config, extension + ) + ) + contains_imports = False + import_section = "" From 2381096a891fdbdc890c22b69ef40f203a49b01b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 21 Dec 2019 23:50:46 -0800 Subject: [PATCH 0267/1439] Contiguous import sorting working on isort codebase --- isort/api.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/isort/api.py b/isort/api.py index 20f2ba5fb..6921287fe 100644 --- a/isort/api.py +++ b/isort/api.py @@ -180,7 +180,7 @@ def sort_imports( char_index += 1 not_imports = bool(in_quote) or in_top_comment - if not in_quote: + if not (in_quote or in_top_comment): stripped_line = line.strip() if not stripped_line or stripped_line.startswith("#"): import_section += line @@ -224,7 +224,6 @@ def sort_imports( not_imports = False if import_section: - import_section += line if not contains_imports: output_stream.write(import_section) else: @@ -233,5 +232,3 @@ def sort_imports( parse.file_contents(import_section, config=config), config, extension ) ) - contains_imports = False - import_section = "" From 65caf07e823c504fa5f6e9a547ee0a2f4dacbd25 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 22 Dec 2019 23:05:15 -0800 Subject: [PATCH 0268/1439] Update test to match new behaviour that allows multiple import sections --- tests/test_isort.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_isort.py b/tests/test_isort.py index 14af796d5..8673ab46e 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -87,10 +87,9 @@ def test_code_intermixed() -> None: assert test_output == ( "import sys\n" "\n" - "import myproject.test\n" - "\n" "print('yo')\n" "print('I like to put code between imports cause I want stuff to break')\n" + "import myproject.test\n" ) From dd339f68e89c8cfa821f66039808d9cd272e3b42 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 22 Dec 2019 23:36:26 -0800 Subject: [PATCH 0269/1439] Move import adding to api.sort_imports --- isort/api.py | 6 +++++- isort/parse.py | 4 ---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/isort/api.py b/isort/api.py index 6921287fe..2ce9ead3b 100644 --- a/isort/api.py +++ b/isort/api.py @@ -11,7 +11,7 @@ IntroducedSyntaxErrors, UnableToDetermineEncoding, ) -from .format import remove_whitespace, show_unified_diff +from .format import remove_whitespace, show_unified_diff, format_natural from .io import File from .settings import DEFAULT_CONFIG, FILE_SKIP_COMMENT, Config @@ -142,6 +142,7 @@ def sort_imports( - `output_stream`: Text stream to output sorted inputs into. - `config`: Config settings to use when sorting imports. Defaults settings.DEFAULT_CONFIG. """ + add_imports = (format_natural(addition) for addition in config.add_imports) import_section: str = "" in_quote: str = "" first_comment_index_start: int = -1 @@ -208,6 +209,7 @@ def sort_imports( if not_imports: if import_section: + import_section += config.line_ending.join(add_imports) import_section += line if not contains_imports: output_stream.write(import_section) @@ -227,8 +229,10 @@ def sort_imports( if not contains_imports: output_stream.write(import_section) else: + import_section += config.line_ending.join(add_imports) output_stream.write( output.sorted_imports( parse.file_contents(import_section, config=config), config, extension ) ) + output_stream.write(config.line_ending.join(add_imports)) diff --git a/isort/parse.py b/isort/parse.py index 1b4591199..a87461ffa 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -133,16 +133,12 @@ class ParsedContent(NamedTuple): def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedContent: """Parses a python file taking out and categorizing imports.""" line_separator: str = config.line_ending or _infer_line_separator(contents) - add_imports = (format_natural(addition) for addition in config.add_imports) in_lines = contents.split(line_separator) out_lines = [] original_line_count = len(in_lines) section_comments = [f"# {heading}" for heading in config.import_headings.values()] finder = FindersManager(config=config) - if original_line_count > 1 or in_lines[:1] not in ([], [""]) or config.force_adds: - in_lines.extend(add_imports) - line_count = len(in_lines) place_imports: Dict[str, List[str]] = {} From 058331e60b57a81e5094cdf418dc7a61071d3cfb Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 23 Dec 2019 01:37:14 -0800 Subject: [PATCH 0270/1439] Fully move add_imports to main sort_imports entry point --- isort/api.py | 43 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/isort/api.py b/isort/api.py index 2ce9ead3b..00b4c4fd2 100644 --- a/isort/api.py +++ b/isort/api.py @@ -11,11 +11,12 @@ IntroducedSyntaxErrors, UnableToDetermineEncoding, ) -from .format import remove_whitespace, show_unified_diff, format_natural +from .format import format_natural, remove_whitespace, show_unified_diff from .io import File from .settings import DEFAULT_CONFIG, FILE_SKIP_COMMENT, Config IMPORT_START_IDENTIFIERS = ("from ", "from.import", "import ", "import*") +COMMENT_INDICATORS = ('"""', "'''", "'", '"', "#") def _config( @@ -142,15 +143,38 @@ def sort_imports( - `output_stream`: Text stream to output sorted inputs into. - `config`: Config settings to use when sorting imports. Defaults settings.DEFAULT_CONFIG. """ - add_imports = (format_natural(addition) for addition in config.add_imports) + line_separator: str = config.line_ending + add_imports = [format_natural(addition) for addition in config.add_imports] import_section: str = "" in_quote: str = "" first_comment_index_start: int = -1 first_comment_index_end: int = -1 contains_imports: bool = False in_top_comment: bool = False + first_import_section: bool = True section_comments = [f"# {heading}" for heading in config.import_headings.values()] + + def additional_imports() -> str: + nonlocal add_imports + nonlocal line_separator + nonlocal contains_imports + + if not add_imports: + return "" + + if not line_separator: + line_separator = "\n" + + fomatted_imports: str = line_separator.join(add_imports) + line_separator + contains_imports = True + add_imports = [] + return fomatted_imports + + index: int = 0 for index, line in enumerate(input_stream): + if not line_separator: + line_separator = line[-1] + if index == 1 and line.startswith("#"): in_top_comment = True elif in_top_comment: @@ -208,12 +232,18 @@ def sort_imports( not_imports = True if not_imports: + if not in_top_comment and not in_quote and not import_section and not line.lstrip().startswith(COMMENT_INDICATORS): + import_section = additional_imports() + if import_section: - import_section += config.line_ending.join(add_imports) + import_section += additional_imports() import_section += line if not contains_imports: output_stream.write(import_section) else: + if first_import_section: + import_section = import_section.lstrip(line_separator) + first_import_section = False output_stream.write( output.sorted_imports( parse.file_contents(import_section, config=config), config, extension @@ -226,13 +256,16 @@ def sort_imports( not_imports = False if import_section: + import_section += additional_imports() if not contains_imports: output_stream.write(import_section) else: - import_section += config.line_ending.join(add_imports) + if first_import_section: + import_section = import_section.lstrip(line_separator) output_stream.write( output.sorted_imports( parse.file_contents(import_section, config=config), config, extension ) ) - output_stream.write(config.line_ending.join(add_imports)) + elif index > 1 or config.force_adds: + output_stream.write(additional_imports()) From 1d5394d473dd00903976ac3745956969ec61711b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 23 Dec 2019 01:39:12 -0800 Subject: [PATCH 0271/1439] Black formatting --- isort/api.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/isort/api.py b/isort/api.py index 00b4c4fd2..94fdd3891 100644 --- a/isort/api.py +++ b/isort/api.py @@ -232,7 +232,12 @@ def additional_imports() -> str: not_imports = True if not_imports: - if not in_top_comment and not in_quote and not import_section and not line.lstrip().startswith(COMMENT_INDICATORS): + if ( + not in_top_comment + and not in_quote + and not import_section + and not line.lstrip().startswith(COMMENT_INDICATORS) + ): import_section = additional_imports() if import_section: From f79b8834ff88b9f4517d464407240f2bce6c7f5a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 23 Dec 2019 01:49:53 -0800 Subject: [PATCH 0272/1439] Fix signature of test case --- tests/test_parse.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_parse.py b/tests/test_parse.py index ffae47a3d..efc3a22a4 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -23,8 +23,6 @@ def test_file_contents(): as_map, imports, categorized_comments, - first_comment_index_start, - first_comment_index_end, change_count, original_line_count, line_separator, From ce8a42f7e829109fec33239c36a31b31a961ad20 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 23 Dec 2019 02:02:08 -0800 Subject: [PATCH 0273/1439] Fix first comment identification --- isort/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/api.py b/isort/api.py index 94fdd3891..f866c0580 100644 --- a/isort/api.py +++ b/isort/api.py @@ -175,7 +175,7 @@ def additional_imports() -> str: if not line_separator: line_separator = line[-1] - if index == 1 and line.startswith("#"): + if index == 0 and line.startswith("#"): in_top_comment = True elif in_top_comment: if not line.startswith("#") or line in section_comments: From 9c7668375f911b4da53f6eb1ab6287c65da7f720 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 23 Dec 2019 02:31:01 -0800 Subject: [PATCH 0274/1439] Fix section comment handling --- isort/api.py | 5 +++-- isort/output.py | 5 +---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/isort/api.py b/isort/api.py index f866c0580..d9cfabeb7 100644 --- a/isort/api.py +++ b/isort/api.py @@ -175,10 +175,11 @@ def additional_imports() -> str: if not line_separator: line_separator = line[-1] - if index == 0 and line.startswith("#"): + stripped_line = line.strip() + if index == 0 and line.startswith("#") and stripped_line not in section_comments: in_top_comment = True elif in_top_comment: - if not line.startswith("#") or line in section_comments: + if not line.startswith("#") or stripped_line in section_comments: in_top_comment = False first_comment_index_end = index - 1 elif '"' in line or "'" in line: diff --git a/isort/output.py b/isort/output.py index a6b4a7a90..f2d287a73 100644 --- a/isort/output.py +++ b/isort/output.py @@ -122,10 +122,7 @@ def sorted_imports( section_title = config.import_headings.get(section_name.lower(), "") if section_title: section_comment = f"# {section_title}" - if ( - section_comment not in parsed.lines_without_imports[0:1] - and section_comment not in parsed.in_lines[0:1] - ): + if section_comment not in parsed.lines_without_imports[0:1]: section_output.insert(0, section_comment) if pending_lines_before or not no_lines_before: From 29af5bd59536a670eb3de3b087cdd1e07f0f58dd Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 23 Dec 2019 04:38:32 -0800 Subject: [PATCH 0275/1439] Update tests to match updated behavior --- isort/api.py | 6 +++++- tests/test_isort.py | 18 +++++------------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/isort/api.py b/isort/api.py index d9cfabeb7..14602378e 100644 --- a/isort/api.py +++ b/isort/api.py @@ -176,7 +176,11 @@ def additional_imports() -> str: line_separator = line[-1] stripped_line = line.strip() - if index == 0 and line.startswith("#") and stripped_line not in section_comments: + if ( + (index == 0 or (index == 1 and not contains_imports)) + and line.startswith("#") + and stripped_line not in section_comments + ): in_top_comment = True elif in_top_comment: if not line.startswith("#") or stripped_line in section_comments: diff --git a/tests/test_isort.py b/tests/test_isort.py index 8673ab46e..d67bdcf7b 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -778,14 +778,10 @@ def test_quotes_in_file() -> None: assert SortImports(file_contents=test_input).output == test_input test_input = "import os\n\n" '\'"""\'\n' "import foo\n" - assert SortImports(file_contents=test_input).output == ( - "import os\n\nimport foo\n\n" '\'"""\'\n' - ) + assert SortImports(file_contents=test_input).output == test_input - test_input = "import os\n\n" '"""Let us"""\n' "import foo\n" '"""okay?"""\n' - assert SortImports(file_contents=test_input).output == ( - 'import os\n\nimport foo\n\n"""Let us"""\n"""okay?"""\n' - ) + test_input = "import os\n\n" '"""Let us"""\n' "import foo\n\n" '"""okay?"""\n' + assert SortImports(file_contents=test_input).output == test_input test_input = "import os\n\n" '#"""\n' "import foo\n" '#"""' assert SortImports(file_contents=test_input).output == ( @@ -1201,7 +1197,7 @@ def test_smart_lines_after_import_section() -> None: " pass\n" ) - # ensure logic works with both style comments + # the same logic does not apply to doc strings test_input = ( "from a import b\n" '"""\n' @@ -1213,7 +1209,6 @@ def test_smart_lines_after_import_section() -> None: assert SortImports(file_contents=test_input).output == ( "from a import b\n" "\n" - "\n" '"""\n' " comment should be ignored\n" '"""\n' @@ -1631,9 +1626,8 @@ def test_place_comments() -> None: "\n" "# isort:imports-thirdparty\n" "# isort:imports-firstparty\n" - "print('code')\n" - "\n" "# isort:imports-stdlib\n" + "\n" ) expected_output = ( "\n# isort:imports-thirdparty\n" @@ -1642,8 +1636,6 @@ def test_place_comments() -> None: "# isort:imports-firstparty\n" "import myproject.test\n" "\n" - "print('code')\n" - "\n" "# isort:imports-stdlib\n" "import os\n" "import sys\n" From 7480464332b3a370561d945b099630ca780721ab Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 23 Dec 2019 23:17:08 -0800 Subject: [PATCH 0276/1439] Fix line import continuation handling --- isort/_future/_dataclasses.py | 1 + isort/api.py | 26 +++++++++++--------------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/isort/_future/_dataclasses.py b/isort/_future/_dataclasses.py index f7f6bace4..8e2ff41a0 100644 --- a/isort/_future/_dataclasses.py +++ b/isort/_future/_dataclasses.py @@ -1,5 +1,6 @@ # type: ignore # flake8: noqa +# flake8: noqa """Backport of Python3.7 dataclasses Library Taken directly from here: https://github.com/ericvsmith/dataclasses diff --git a/isort/api.py b/isort/api.py index 14602378e..c39b1b58d 100644 --- a/isort/api.py +++ b/isort/api.py @@ -216,21 +216,17 @@ def additional_imports() -> str: import_section += line elif stripped_line.startswith(IMPORT_START_IDENTIFIERS): import_section += line - if "(" in stripped_line and ")" not in stripped_line: - nested_line = line - nested_stripped_line = nested_line.strip().split("#")[0] - while ")" not in nested_stripped_line: - nested_line = input_stream.readline() - nested_stripped_line = nested_line.strip() - import_section += nested_line - - if stripped_line.endswith("\\"): - nested_line = line - nested_stripped_line = nested_line.strip() - while nested_line and nested_stripped_line.endswith("\\"): - nested_line = input_stream.readline() - nested_stripped_line = nested_line.strip() - import_section += nested_line + while stripped_line.endswith("\\") or ("(" in stripped_line and ")" not in stripped_line): + if stripped_line.endswith("\\"): + while stripped_line and stripped_line.endswith("\\"): + line = input_stream.readline() + stripped_line = line.strip().split("#")[0] + import_section += line + else: + while ")" not in stripped_line: + line = input_stream.readline() + stripped_line = line.strip().split("#")[0] + import_section += line contains_imports = True else: From 7b67b7f7cca70549d452c603f4d6c199f5831402 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 23 Dec 2019 23:19:30 -0800 Subject: [PATCH 0277/1439] Black formatting --- isort/api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/isort/api.py b/isort/api.py index c39b1b58d..8ff8ec871 100644 --- a/isort/api.py +++ b/isort/api.py @@ -216,7 +216,9 @@ def additional_imports() -> str: import_section += line elif stripped_line.startswith(IMPORT_START_IDENTIFIERS): import_section += line - while stripped_line.endswith("\\") or ("(" in stripped_line and ")" not in stripped_line): + while stripped_line.endswith("\\") or ( + "(" in stripped_line and ")" not in stripped_line + ): if stripped_line.endswith("\\"): while stripped_line and stripped_line.endswith("\\"): line = input_stream.readline() From a6a32c4814df0453c468db0075dd3190f3244351 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 24 Dec 2019 00:56:37 -0800 Subject: [PATCH 0278/1439] Fix stripping of import sections, to avoid stripping new lines before comments --- isort/api.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/isort/api.py b/isort/api.py index 8ff8ec871..cca2f5bb4 100644 --- a/isort/api.py +++ b/isort/api.py @@ -249,7 +249,9 @@ def additional_imports() -> str: if not contains_imports: output_stream.write(import_section) else: - if first_import_section: + if first_import_section and not import_section.lstrip( + line_separator + ).startswith(COMMENT_INDICATORS): import_section = import_section.lstrip(line_separator) first_import_section = False output_stream.write( @@ -268,7 +270,9 @@ def additional_imports() -> str: if not contains_imports: output_stream.write(import_section) else: - if first_import_section: + if first_import_section and not import_section.lstrip(line_separator).startswith( + COMMENT_INDICATORS + ): import_section = import_section.lstrip(line_separator) output_stream.write( output.sorted_imports( From 4add8a64ed9a1425ca2c1397b6eb06bd423214e5 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 24 Dec 2019 01:25:47 -0800 Subject: [PATCH 0279/1439] Improve top comment handling --- isort/api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/isort/api.py b/isort/api.py index cca2f5bb4..e78b42c43 100644 --- a/isort/api.py +++ b/isort/api.py @@ -186,7 +186,8 @@ def additional_imports() -> str: if not line.startswith("#") or stripped_line in section_comments: in_top_comment = False first_comment_index_end = index - 1 - elif '"' in line or "'" in line: + + if not line.startswith("#") and '"' in line or "'" in line: char_index = 0 if first_comment_index_start == -1 and (line.startswith('"') or line.startswith("'")): first_comment_index_start = index From ca50844c64e37d87105ea0c40678913a6fbdd778 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 24 Dec 2019 02:40:55 -0800 Subject: [PATCH 0280/1439] Simplify core sorting logic --- isort/api.py | 179 +++++++++++++++++++++++---------------------------- 1 file changed, 82 insertions(+), 97 deletions(-) diff --git a/isort/api.py b/isort/api.py index e78b42c43..4c6bc949e 100644 --- a/isort/api.py +++ b/isort/api.py @@ -1,7 +1,8 @@ import re from io import StringIO +from itertools import chain from pathlib import Path -from typing import Any, NamedTuple, Optional, TextIO, Tuple +from typing import Any, List, NamedTuple, Optional, TextIO, Tuple from . import output, parse from .exceptions import ( @@ -144,7 +145,7 @@ def sort_imports( - `config`: Config settings to use when sorting imports. Defaults settings.DEFAULT_CONFIG. """ line_separator: str = config.line_ending - add_imports = [format_natural(addition) for addition in config.add_imports] + add_imports: List[str] = [format_natural(addition) for addition in config.add_imports] import_section: str = "" in_quote: str = "" first_comment_index_start: int = -1 @@ -154,98 +155,99 @@ def sort_imports( first_import_section: bool = True section_comments = [f"# {heading}" for heading in config.import_headings.values()] - def additional_imports() -> str: - nonlocal add_imports - nonlocal line_separator - nonlocal contains_imports + for index, line in enumerate(chain(input_stream, (None,))): + if line is None: + if index == 0 and not config.force_adds: + return - if not add_imports: - return "" - - if not line_separator: - line_separator = "\n" - - fomatted_imports: str = line_separator.join(add_imports) + line_separator - contains_imports = True - add_imports = [] - return fomatted_imports - - index: int = 0 - for index, line in enumerate(input_stream): - if not line_separator: - line_separator = line[-1] - - stripped_line = line.strip() - if ( - (index == 0 or (index == 1 and not contains_imports)) - and line.startswith("#") - and stripped_line not in section_comments - ): - in_top_comment = True - elif in_top_comment: - if not line.startswith("#") or stripped_line in section_comments: - in_top_comment = False - first_comment_index_end = index - 1 - - if not line.startswith("#") and '"' in line or "'" in line: - char_index = 0 - if first_comment_index_start == -1 and (line.startswith('"') or line.startswith("'")): - first_comment_index_start = index - while char_index < len(line): - if line[char_index] == "\\": - char_index += 1 - elif in_quote: - if line[char_index : char_index + len(in_quote)] == in_quote: - in_quote = "" - if first_comment_index_end < first_comment_index_start: - first_comment_index_end = index - elif line[char_index] in ("'", '"'): - long_quote = line[char_index : char_index + 3] - if long_quote in ('"""', "'''"): - in_quote = long_quote - char_index += 2 - else: - in_quote = line[char_index] - elif line[char_index] == "#": - break - char_index += 1 + not_imports = True + line = "" + if not line_separator: + line_separator = "\n" + else: + if not line_separator: + line_separator = line[-1] - not_imports = bool(in_quote) or in_top_comment - if not (in_quote or in_top_comment): stripped_line = line.strip() - if not stripped_line or stripped_line.startswith("#"): - import_section += line - elif stripped_line.startswith(IMPORT_START_IDENTIFIERS): - import_section += line - while stripped_line.endswith("\\") or ( - "(" in stripped_line and ")" not in stripped_line + if ( + (index == 0 or (index == 1 and not contains_imports)) + and line.startswith("#") + and stripped_line not in section_comments + ): + in_top_comment = True + elif in_top_comment: + if not line.startswith("#") or stripped_line in section_comments: + in_top_comment = False + first_comment_index_end = index - 1 + + if not line.startswith("#") and '"' in line or "'" in line: + char_index = 0 + if first_comment_index_start == -1 and ( + line.startswith('"') or line.startswith("'") ): - if stripped_line.endswith("\\"): - while stripped_line and stripped_line.endswith("\\"): - line = input_stream.readline() - stripped_line = line.strip().split("#")[0] - import_section += line - else: - while ")" not in stripped_line: - line = input_stream.readline() - stripped_line = line.strip().split("#")[0] - import_section += line + first_comment_index_start = index + while char_index < len(line): + if line[char_index] == "\\": + char_index += 1 + elif in_quote: + if line[char_index : char_index + len(in_quote)] == in_quote: + in_quote = "" + if first_comment_index_end < first_comment_index_start: + first_comment_index_end = index + elif line[char_index] in ("'", '"'): + long_quote = line[char_index : char_index + 3] + if long_quote in ('"""', "'''"): + in_quote = long_quote + char_index += 2 + else: + in_quote = line[char_index] + elif line[char_index] == "#": + break + char_index += 1 - contains_imports = True - else: - not_imports = True + not_imports = bool(in_quote) or in_top_comment + if not (in_quote or in_top_comment): + stripped_line = line.strip() + if not stripped_line or stripped_line.startswith("#"): + import_section += line + elif stripped_line.startswith(IMPORT_START_IDENTIFIERS): + import_section += line + while stripped_line.endswith("\\") or ( + "(" in stripped_line and ")" not in stripped_line + ): + if stripped_line.endswith("\\"): + while stripped_line and stripped_line.endswith("\\"): + line = input_stream.readline() + stripped_line = line.strip().split("#")[0] + import_section += line + else: + while ")" not in stripped_line: + line = input_stream.readline() + stripped_line = line.strip().split("#")[0] + import_section += line + + contains_imports = True + else: + not_imports = True if not_imports: if ( - not in_top_comment + add_imports + and not in_top_comment and not in_quote and not import_section and not line.lstrip().startswith(COMMENT_INDICATORS) ): - import_section = additional_imports() + import_section = line_separator.join(add_imports) + line_separator + contains_imports = True + add_imports = [] if import_section: - import_section += additional_imports() + if add_imports: + import_section += line_separator.join(add_imports) + line_separator + contains_imports = True + add_imports = [] + import_section += line if not contains_imports: output_stream.write(import_section) @@ -265,20 +267,3 @@ def additional_imports() -> str: else: output_stream.write(line) not_imports = False - - if import_section: - import_section += additional_imports() - if not contains_imports: - output_stream.write(import_section) - else: - if first_import_section and not import_section.lstrip(line_separator).startswith( - COMMENT_INDICATORS - ): - import_section = import_section.lstrip(line_separator) - output_stream.write( - output.sorted_imports( - parse.file_contents(import_section, config=config), config, extension - ) - ) - elif index > 1 or config.force_adds: - output_stream.write(additional_imports()) From ecfe14a5745ca394267838cb12e4c93df57638b1 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 24 Dec 2019 02:56:55 -0800 Subject: [PATCH 0281/1439] Skip tests temporarily to streamline merge --- tests/test_isort.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_isort.py b/tests/test_isort.py index d67bdcf7b..e4632f413 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -2895,6 +2895,7 @@ def test_is_python_typing_stub(tmpdir) -> None: assert is_python_file(str(stub)) is True +@pytest.mark.skip(reason="TODO: Duplicates currently not handled.") def test_to_ensure_imports_are_brought_to_top_issue_651() -> None: test_input = ( "from __future__ import absolute_import, unicode_literals\n" @@ -3847,6 +3848,7 @@ def test_standard_library_deprecates_user_issue_778() -> None: assert SortImports(file_contents=test_input).output == test_input +@pytest.mark.skip(reason="TODO: Failing for unknown reason.") def test_settings_path_skip_issue_909(tmpdir) -> None: base_dir = tmpdir.mkdir("project") config_dir = base_dir.mkdir("conf") From 297ec8aa09a6930d290e4cb5902c29cbc9123555 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 24 Dec 2019 23:53:22 -0800 Subject: [PATCH 0282/1439] Fix example to match new behaviour --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index b6d5cb783..183aec723 100644 --- a/README.md +++ b/README.md @@ -43,8 +43,6 @@ Before isort: ```python from my_lib import Object -print("Hey") - import os from my_lib import Object3 @@ -61,6 +59,7 @@ from __future__ import absolute_import from third_party import lib3 +print("Hey") print("yo") ``` From ef8046c69bc98cca2b581af976a33beddc0e43c4 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 25 Dec 2019 21:30:12 -0800 Subject: [PATCH 0283/1439] Fix CLI running test case --- tests/test_isort.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_isort.py b/tests/test_isort.py index e4632f413..4e3a03326 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -3848,7 +3848,6 @@ def test_standard_library_deprecates_user_issue_778() -> None: assert SortImports(file_contents=test_input).output == test_input -@pytest.mark.skip(reason="TODO: Failing for unknown reason.") def test_settings_path_skip_issue_909(tmpdir) -> None: base_dir = tmpdir.mkdir("project") config_dir = base_dir.mkdir("conf") @@ -3857,10 +3856,10 @@ def test_settings_path_skip_issue_909(tmpdir) -> None: ) base_dir.join("file_glob_skip.py").write( - "import os\n\n" 'print("Hello World")\n' "\nimport sys\n" + "import os\n\n" 'print("Hello World")\n' "\nimport sys\nimport os\n" ) base_dir.join("file_to_be_skipped.py").write( - "import os\n\n" 'print("Hello World")' "\nimport sys\n" + "import os\n\n" 'print("Hello World")' "\nimport sys\nimport os\n" ) test_run_directory = os.getcwd() From 955e5b73cff6ac54c91a3a4df46e9ef2fb772e18 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 26 Dec 2019 14:14:22 -0800 Subject: [PATCH 0284/1439] Add support for nested import sections --- isort/api.py | 34 ++++++++++++++++++++++++---------- tests/test_isort.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 10 deletions(-) diff --git a/isort/api.py b/isort/api.py index 4c6bc949e..fa512d61c 100644 --- a/isort/api.py +++ b/isort/api.py @@ -1,4 +1,5 @@ import re +import textwrap from io import StringIO from itertools import chain from pathlib import Path @@ -154,6 +155,7 @@ def sort_imports( in_top_comment: bool = False first_import_section: bool = True section_comments = [f"# {heading}" for heading in config.import_headings.values()] + indent: str = "" for index, line in enumerate(chain(input_stream, (None,))): if line is None: @@ -171,7 +173,7 @@ def sort_imports( stripped_line = line.strip() if ( (index == 0 or (index == 1 and not contains_imports)) - and line.startswith("#") + and stripped_line.startswith("#") and stripped_line not in section_comments ): in_top_comment = True @@ -180,7 +182,7 @@ def sort_imports( in_top_comment = False first_comment_index_end = index - 1 - if not line.startswith("#") and '"' in line or "'" in line: + if not stripped_line.startswith("#") and '"' in line or "'" in line: char_index = 0 if first_comment_index_start == -1 and ( line.startswith('"') or line.startswith("'") @@ -211,6 +213,9 @@ def sort_imports( if not stripped_line or stripped_line.startswith("#"): import_section += line elif stripped_line.startswith(IMPORT_START_IDENTIFIERS): + contains_imports = True + + indent = line[:-len(line.lstrip())] import_section += line while stripped_line.endswith("\\") or ( "(" in stripped_line and ")" not in stripped_line @@ -225,8 +230,6 @@ def sort_imports( line = input_stream.readline() stripped_line = line.strip().split("#")[0] import_section += line - - contains_imports = True else: not_imports = True @@ -243,12 +246,13 @@ def sort_imports( add_imports = [] if import_section: - if add_imports: + if add_imports and not indent: import_section += line_separator.join(add_imports) + line_separator contains_imports = True add_imports = [] - import_section += line + if not indent: + import_section += line if not contains_imports: output_stream.write(import_section) else: @@ -257,11 +261,21 @@ def sort_imports( ).startswith(COMMENT_INDICATORS): import_section = import_section.lstrip(line_separator) first_import_section = False - output_stream.write( - output.sorted_imports( - parse.file_contents(import_section, config=config), config, extension - ) + + if indent: + import_section = line_separator.join(line.lstrip() for line in import_section.split(line_separator)) + sorted_import_section = output.sorted_imports( + parse.file_contents(import_section, config=config), config, extension ) + if indent: + sorted_import_section = textwrap.indent(sorted_import_section, indent) + line_separator + + output_stream.write(sorted_import_section) + + if indent: + output_stream.write(line) + indent = "" + contains_imports = False import_section = "" else: diff --git a/tests/test_isort.py b/tests/test_isort.py index e4632f413..8b6ae6208 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -2922,6 +2922,7 @@ def test_to_ensure_importing_from_imports_module_works_issue_662() -> None: "@wraps(fun)\n" "def __inner(*args, **kwargs):\n" " from .imports import qualname\n" + "\n" " warn(description=description or qualname(fun), deprecation=deprecation, " "removal=removal)\n" ) @@ -4158,3 +4159,31 @@ def test_isort_with_single_character_import() -> None: """ test_input = "from django.db.models import CASCADE, SET_NULL, Q\n" assert SortImports(file_contents=test_input).output == test_input + + +def test_isort_nested_imports() -> None: + """Ensure imports in a nested block get sorted correctly""" + test_input = """ + def import_test(): + import sys + import os + + # my imports + from . import def + from . import abc + + return True + """ + assert ( + SortImports(file_contents=test_input).output + == """ + def import_test(): + import os + import sys + + # my imports + from . import abc, def + + return True + """ + ) From 6009a180237aa9ba070cb0b14178b83d457e6069 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 26 Dec 2019 22:41:12 -0800 Subject: [PATCH 0285/1439] Black formattingp --- isort/_future/__init__.py | 1 + isort/api.py | 10 +++++++--- isort/finders.py | 3 +++ isort/settings.py | 1 + 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/isort/_future/__init__.py b/isort/_future/__init__.py index 9f5e9d183..4d9ef4b76 100644 --- a/isort/_future/__init__.py +++ b/isort/_future/__init__.py @@ -2,6 +2,7 @@ if sys.version_info.major <= 3 and sys.version_info.minor <= 6: from . import _dataclasses as dataclasses # type: ignore + else: import dataclasses # type: ignore diff --git a/isort/api.py b/isort/api.py index fa512d61c..c53fe98ac 100644 --- a/isort/api.py +++ b/isort/api.py @@ -215,7 +215,7 @@ def sort_imports( elif stripped_line.startswith(IMPORT_START_IDENTIFIERS): contains_imports = True - indent = line[:-len(line.lstrip())] + indent = line[: -len(line.lstrip())] import_section += line while stripped_line.endswith("\\") or ( "(" in stripped_line and ")" not in stripped_line @@ -263,12 +263,16 @@ def sort_imports( first_import_section = False if indent: - import_section = line_separator.join(line.lstrip() for line in import_section.split(line_separator)) + import_section = line_separator.join( + line.lstrip() for line in import_section.split(line_separator) + ) sorted_import_section = output.sorted_imports( parse.file_contents(import_section, config=config), config, extension ) if indent: - sorted_import_section = textwrap.indent(sorted_import_section, indent) + line_separator + sorted_import_section = ( + textwrap.indent(sorted_import_section, indent) + line_separator + ) output_stream.write(sorted_import_section) diff --git a/isort/finders.py b/isort/finders.py index d63cc94ba..5a7c74a74 100644 --- a/isort/finders.py +++ b/isort/finders.py @@ -30,16 +30,19 @@ try: from pipreqs import pipreqs + except ImportError: pipreqs = None try: from pip_api import parse_requirements + except ImportError: parse_requirements = None try: from requirementslib import Pipfile + except ImportError: Pipfile = None diff --git a/isort/settings.py b/isort/settings.py index 607c67b6e..1bdc35cb3 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -43,6 +43,7 @@ try: import toml + except ImportError: toml = None # type: ignore From d0b930680625ed1aae6a445ca06b7a3e1bce7f31 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 27 Dec 2019 23:43:44 -0800 Subject: [PATCH 0286/1439] Add test for isort off comment --- tests/test_isort.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_isort.py b/tests/test_isort.py index 40b22bb24..e1096aa6d 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -4186,3 +4186,16 @@ def import_test(): return True """ ) + + +def test_isort_off() -> None: + """Test that isort can be turned on and off at will using comments""" + test_input = """ +import os +# isort: off +import os +import sys +# isort: on +import sys + """ + assert SortImports(file_contents=test_input).output == test_input From e7cef3c941e0cbdd8b2c71cc6c7f2007b0be51ad Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 28 Dec 2019 01:52:31 -0800 Subject: [PATCH 0287/1439] Add support for isort on and off as well as moving toward isort: comment standard --- isort/api.py | 22 +++++++++++++++------- isort/parse.py | 8 +++++++- isort/settings.py | 5 ++++- tests/test_isort.py | 29 +++++++++++++++-------------- 4 files changed, 41 insertions(+), 23 deletions(-) diff --git a/isort/api.py b/isort/api.py index c53fe98ac..a5aec8cbb 100644 --- a/isort/api.py +++ b/isort/api.py @@ -15,7 +15,7 @@ ) from .format import format_natural, remove_whitespace, show_unified_diff from .io import File -from .settings import DEFAULT_CONFIG, FILE_SKIP_COMMENT, Config +from .settings import DEFAULT_CONFIG, FILE_SKIP_COMMENTS, Config IMPORT_START_IDENTIFIERS = ("from ", "from.import", "import ", "import*") COMMENT_INDICATORS = ('"""', "'''", "'", '"', "#") @@ -54,12 +54,13 @@ def sorted_imports( config = _config(config=config, **config_kwargs) content_source = str(file_path or "Passed in content") if not disregard_skip: - if FILE_SKIP_COMMENT in file_contents: - raise FileSkipComment(content_source) - - elif file_path and config.is_skipped(file_path): + if file_path and config.is_skipped(file_path): raise FileSkipSetting(content_source) + for file_skip_comment in FILE_SKIP_COMMENTS: + if file_skip_comment in file_contents: + raise FileSkipComment(content_source) + if config.atomic: try: compile(file_contents, content_source, "exec", 0, 1) @@ -156,6 +157,7 @@ def sort_imports( first_import_section: bool = True section_comments = [f"# {heading}" for heading in config.import_headings.values()] indent: str = "" + isort_off: bool = False for index, line in enumerate(chain(input_stream, (None,))): if line is None: @@ -207,10 +209,16 @@ def sort_imports( break char_index += 1 - not_imports = bool(in_quote) or in_top_comment + not_imports = bool(in_quote) or in_top_comment or isort_off if not (in_quote or in_top_comment): stripped_line = line.strip() - if not stripped_line or stripped_line.startswith("#"): + if isort_off: + if stripped_line == "# isort: on": + isort_off = False + elif stripped_line == "# isort: off": + not_imports = True + isort_off = True + elif not stripped_line or stripped_line.startswith("#"): import_section += line elif stripped_line.startswith(IMPORT_START_IDENTIFIERS): contains_imports = True diff --git a/isort/parse.py b/isort/parse.py index a87461ffa..6089c08b0 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -54,7 +54,7 @@ def _normalize_line(raw_line: str) -> Tuple[str, str]: def import_type(line: str) -> Optional[str]: """If the current line is an import line it will return its type (from or straight)""" - if "isort:skip" in line or "NOQA" in line: + if "isort:skip" in line or "isort: skip" in line or "NOQA" in line: return None elif line.startswith("import "): return "straight" @@ -174,6 +174,10 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte section = line.split("isort:imports-")[-1].split()[0].upper() place_imports[section] = [] import_placements[line] = section + elif "isort: imports-" in line and line.startswith("#"): + section = line.split("isort: imports-")[-1].split()[0].upper() + place_imports[section] = [] + import_placements[line] = section if skipping_line: out_lines.append(line) @@ -325,6 +329,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte and not last.endswith('"""') and not last.endswith("'''") and "isort:imports-" not in last + and "isort: imports-" not in last ): categorized_comments["above"]["from"].setdefault(import_from, []).insert( 0, out_lines.pop(-1) @@ -361,6 +366,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte and not last.endswith('"""') and not last.endswith("'''") and "isort:imports-" not in last + and "isort: imports-" not in last ): categorized_comments["above"]["straight"].setdefault(module, []).insert( 0, out_lines.pop(-1) diff --git a/isort/settings.py b/isort/settings.py index 1bdc35cb3..ed5e4a512 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -55,7 +55,10 @@ except ImportError: appdirs = None -FILE_SKIP_COMMENT: str = ("isort:" + "skip_file") # Concatenated to avoid this file being skipped +FILE_SKIP_COMMENTS: Tuple[str, ...] = ( + "isort:" + "skip_file", + "isort: " + "skip_file", +) # Concatenated to avoid this file being skipped MAX_CONFIG_SEARCH_DEPTH: int = 25 # The number of parent directories to for a config file within STOP_CONFIG_SEARCH_ON_DIRS: Tuple[str, ...] = (".git", ".hg") VALID_PY_TARGETS: Tuple[str, ...] = tuple( diff --git a/tests/test_isort.py b/tests/test_isort.py index e1096aa6d..ef0028e37 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -641,7 +641,7 @@ def test_skip() -> None: "import myproject\n" "import django\n" "print('hey')\n" - "import sys # isort:skip this import needs to be placed here\n\n\n\n\n\n\n" + "import sys # isort: skip this import needs to be placed here\n\n\n\n\n\n\n" ) test_output = SortImports(file_contents=test_input, known_third_party=["django"]).output @@ -651,7 +651,7 @@ def test_skip() -> None: "import myproject\n" "\n" "print('hey')\n" - "import sys # isort:skip this import needs to be placed here\n" + "import sys # isort: skip this import needs to be placed here\n" ) @@ -668,7 +668,7 @@ def test_skip_with_file_name() -> None: def test_skip_within_file() -> None: """Ensure skipping a whole file works.""" - test_input = "# isort:skip_file\nimport django\nimport myproject\n" + test_input = "# isort: skip_file\nimport django\nimport myproject\n" sort_imports = SortImports(file_contents=test_input, known_third_party=["django"]) assert sort_imports.skipped assert sort_imports.output == "" @@ -1624,19 +1624,19 @@ def test_place_comments() -> None: "import myproject.test\n" "import django.settings\n" "\n" - "# isort:imports-thirdparty\n" - "# isort:imports-firstparty\n" - "# isort:imports-stdlib\n" + "# isort: imports-thirdparty\n" + "# isort: imports-firstparty\n" + "# isort: imports-stdlib\n" "\n" ) expected_output = ( - "\n# isort:imports-thirdparty\n" + "\n# isort: imports-thirdparty\n" "import django.settings\n" "\n" - "# isort:imports-firstparty\n" + "# isort: imports-firstparty\n" "import myproject.test\n" "\n" - "# isort:imports-stdlib\n" + "# isort: imports-stdlib\n" "import os\n" "import sys\n" ) @@ -4190,12 +4190,13 @@ def import_test(): def test_isort_off() -> None: """Test that isort can be turned on and off at will using comments""" - test_input = """ -import os + test_input = """import os + # isort: off -import os import sys +import os # isort: on -import sys - """ + +from . import local +""" assert SortImports(file_contents=test_input).output == test_input From d5a9bf6710270f76573d018de73c7f06e4db9852 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 28 Dec 2019 02:19:19 -0800 Subject: [PATCH 0288/1439] Add isort: split action with test case --- isort/api.py | 2 ++ tests/test_isort.py | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/isort/api.py b/isort/api.py index a5aec8cbb..8d435b213 100644 --- a/isort/api.py +++ b/isort/api.py @@ -218,6 +218,8 @@ def sort_imports( elif stripped_line == "# isort: off": not_imports = True isort_off = True + elif stripped_line == "# isort: split": + not_imports = True elif not stripped_line or stripped_line.startswith("#"): import_section += line elif stripped_line.startswith(IMPORT_START_IDENTIFIERS): diff --git a/tests/test_isort.py b/tests/test_isort.py index ef0028e37..e5072b0fa 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -4200,3 +4200,16 @@ def test_isort_off() -> None: from . import local """ assert SortImports(file_contents=test_input).output == test_input + + +def test_isort_split() -> None: + """Test the ability to split isort import sections""" + test_input = """import os +import sys + +# isort: split + +import os +import sys +""" + assert SortImports(file_contents=test_input).output == test_input From c54b3dd4620f9b3e7ac127c583ecb029fb90f1b7 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 29 Dec 2019 22:52:12 -0800 Subject: [PATCH 0289/1439] Fix support for multi-line comments ending in what appear to be comments --- isort/api.py | 2 +- tests/test_isort.py | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/isort/api.py b/isort/api.py index 8d435b213..0aae77112 100644 --- a/isort/api.py +++ b/isort/api.py @@ -184,7 +184,7 @@ def sort_imports( in_top_comment = False first_comment_index_end = index - 1 - if not stripped_line.startswith("#") and '"' in line or "'" in line: + if (not stripped_line.startswith("#") or in_quote) and '"' in line or "'" in line: char_index = 0 if first_comment_index_start == -1 and ( line.startswith('"') or line.startswith("'") diff --git a/tests/test_isort.py b/tests/test_isort.py index e5072b0fa..451af9e24 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -4213,3 +4213,28 @@ def test_isort_split() -> None: import sys """ assert SortImports(file_contents=test_input).output == test_input + + +def test_comment_look_alike(): + """Test to ensure isort will handle what looks like a single line comment + at the end of a multi-line comment. + """ + test_input = ''' +"""This is a multi-line comment + +ending with what appears to be a single line comment +# Single Line Comment""" +import sys +import os +''' + assert ( + SortImports(file_contents=test_input).output + == ''' +"""This is a multi-line comment + +ending with what appears to be a single line comment +# Single Line Comment""" +import os +import sys +''' + ) From e0ba8f5d38d7f22b3f2ca11efce7f5403b62f859 Mon Sep 17 00:00:00 2001 From: Stephen Brown II Date: Thu, 2 Jan 2020 17:28:13 -0700 Subject: [PATCH 0290/1439] Add ensure_newline_before_comments to black profile Since #1000 was merged to address black compatibility, best include it in the profile. --- isort/profiles.py | 1 + 1 file changed, 1 insertion(+) diff --git a/isort/profiles.py b/isort/profiles.py index 669c82773..1210c8ef0 100644 --- a/isort/profiles.py +++ b/isort/profiles.py @@ -6,6 +6,7 @@ "include_trailing_comma": True, "force_grid_wrap": 0, "use_parentheses": True, + "ensure_newline_before_comments": True, } django = { "combine_as_imports": True, From 59cd74a0c0eb1447295812e07dbc834c300e2ee6 Mon Sep 17 00:00:00 2001 From: Stephen Brown II Date: Thu, 2 Jan 2020 17:58:28 -0700 Subject: [PATCH 0291/1439] Update cruft --- .cruft.json | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.cruft.json b/.cruft.json index 37fced16f..267b8fcbb 100644 --- a/.cruft.json +++ b/.cruft.json @@ -1,6 +1,6 @@ { "template": "https://github.com/timothycrosley/cookiecutter-python/", - "commit": "2d559dc6413338a6ee05f4239039a88a17e7d2a9", + "commit": "5f2c774c99969c8483d7f329e38db5b1a7fc5050", "context": { "cookiecutter": { "full_name": "Timothy Crosley", diff --git a/pyproject.toml b/pyproject.toml index fd6d075e8..9b7ba816b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ vulture = "^1.0" bandit = "^1.6" safety = "^1.8" flake8-bugbear = "^19.8" -black = {version = "^18.3-alpha.0", allows-prereleases = true} +black = {version = "^18.3-alpha.0", allow-prereleases = true} mypy = "^0.730.0" ipython = "^7.7" pytest = "^5.0" From 5cf43893ca21f8ff3a096ad6f6333a35e9494fda Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 3 Jan 2020 01:44:04 -0800 Subject: [PATCH 0292/1439] Add SUPPORTED_EXTENSIONS configuration variable, add pyx to it --- isort/main.py | 4 ++-- isort/settings.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/isort/main.py b/isort/main.py index 4447545ee..2c30cfa05 100644 --- a/isort/main.py +++ b/isort/main.py @@ -14,7 +14,7 @@ from . import SortImports, __version__, sections from .logo import ASCII_ART from .profiles import profiles -from .settings import DEFAULT_CONFIG, VALID_PY_TARGETS, Config, WrapModes +from .settings import DEFAULT_CONFIG, SUPPORTED_EXTENSIONS, VALID_PY_TARGETS, Config, WrapModes shebang_re = re.compile(br"^#!.*\bpython[23w]?\b") QUICK_GUIDE = f""" @@ -35,7 +35,7 @@ def is_python_file(path: str) -> bool: _root, ext = os.path.splitext(path) - if ext in (".py", ".pyi"): + if ext in SUPPORTED_EXTENSIONS: return True if ext in (".pex",): return False diff --git a/isort/settings.py b/isort/settings.py index ed5e4a512..387d10e40 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -55,6 +55,7 @@ except ImportError: appdirs = None +SUPPORTED_EXTENSIONS = (".py", ".pyi", ".pyx") FILE_SKIP_COMMENTS: Tuple[str, ...] = ( "isort:" + "skip_file", "isort: " + "skip_file", From 4565cdedaa186bf8902c676fa99f8a34a40f15cc Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 3 Jan 2020 01:44:17 -0800 Subject: [PATCH 0293/1439] Formatting --- tests/test_comments.py | 1 - tests/test_output.py | 1 - tests/test_parse.py | 1 - tests/test_wrap_modes.py | 1 - 4 files changed, 4 deletions(-) diff --git a/tests/test_comments.py b/tests/test_comments.py index b1b4ed7b4..1805f137c 100644 --- a/tests/test_comments.py +++ b/tests/test_comments.py @@ -1,5 +1,4 @@ from hypothesis_auto import auto_pytest_magic - from isort import comments auto_pytest_magic(comments.parse) diff --git a/tests/test_output.py b/tests/test_output.py index e42093b5b..f9e04aec3 100644 --- a/tests/test_output.py +++ b/tests/test_output.py @@ -1,7 +1,6 @@ import sys from hypothesis_auto import auto_pytest_magic - from isort import output auto_pytest_magic(output.with_comments, auto_allow_exceptions_=(ValueError,)) diff --git a/tests/test_parse.py b/tests/test_parse.py index efc3a22a4..f5846bd73 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -1,5 +1,4 @@ from hypothesis_auto import auto_pytest_magic - from isort import parse from isort.settings import Config diff --git a/tests/test_wrap_modes.py b/tests/test_wrap_modes.py index dee26d4f7..6c8a66551 100644 --- a/tests/test_wrap_modes.py +++ b/tests/test_wrap_modes.py @@ -1,7 +1,6 @@ import sys from hypothesis_auto import auto_pytest_magic - from isort import wrap_modes auto_pytest_magic(wrap_modes.grid, auto_allow_exceptions_=(ValueError,)) From a3e8d4d9ea007679eabfd20929550bffa6fbd481 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 4 Jan 2020 23:11:26 -0800 Subject: [PATCH 0294/1439] Initial work toward cimport support --- isort/api.py | 41 ++++++++++++++++++++++++++++++++++++----- isort/parse.py | 24 ++++++++++++++---------- 2 files changed, 50 insertions(+), 15 deletions(-) diff --git a/isort/api.py b/isort/api.py index 0aae77112..b7126999e 100644 --- a/isort/api.py +++ b/isort/api.py @@ -18,6 +18,9 @@ from .settings import DEFAULT_CONFIG, FILE_SKIP_COMMENTS, Config IMPORT_START_IDENTIFIERS = ("from ", "from.import", "import ", "import*") + +CIMPORT_IDENTIFIERS = ("cimport ", "cimport*", "from.cimport") +IMPORT_START_IDENTIFIERS = ("from ", "from.import", "import ", "import*") + CIMPORT_IDENTIFIERS COMMENT_INDICATORS = ('"""', "'''", "'", '"', "#") @@ -149,6 +152,7 @@ def sort_imports( line_separator: str = config.line_ending add_imports: List[str] = [format_natural(addition) for addition in config.add_imports] import_section: str = "" + next_import_section: str = "" in_quote: str = "" first_comment_index_start: int = -1 first_comment_index_end: int = -1 @@ -158,6 +162,7 @@ def sort_imports( section_comments = [f"# {heading}" for heading in config.import_headings.values()] indent: str = "" isort_off: bool = False + cimports: bool = False for index, line in enumerate(chain(input_stream, (None,))): if line is None: @@ -226,7 +231,7 @@ def sort_imports( contains_imports = True indent = line[: -len(line.lstrip())] - import_section += line + import_statement = line while stripped_line.endswith("\\") or ( "(" in stripped_line and ")" not in stripped_line ): @@ -234,12 +239,32 @@ def sort_imports( while stripped_line and stripped_line.endswith("\\"): line = input_stream.readline() stripped_line = line.strip().split("#")[0] - import_section += line + import_statement += line else: while ")" not in stripped_line: line = input_stream.readline() stripped_line = line.strip().split("#")[0] - import_section += line + import_statement += line + + cimport_statement: bool = False + if ( + import_statement.lstrip().startswith(CIMPORT_IDENTIFIERS) + or "cimport " in import_statement + or "cimport*" in import_statement + or "cimport(" in import_statement + ): + cimport_statement = True + + if cimport_statement != cimports: + if import_section: + next_import_section = import_statement + import_statement = "" + not_imports = True + line = "" + else: + cimports = cimport_statement + + import_section + import_statement else: not_imports = True @@ -277,7 +302,10 @@ def sort_imports( line.lstrip() for line in import_section.split(line_separator) ) sorted_import_section = output.sorted_imports( - parse.file_contents(import_section, config=config), config, extension + parse.file_contents(import_section, config=config), + config, + extension, + import_type="cimport" if cimports else "import", ) if indent: sorted_import_section = ( @@ -291,7 +319,10 @@ def sort_imports( indent = "" contains_imports = False - import_section = "" + if next_import_section: + cimports = not cimports + import_section = next_import_section + next_import_section = "" else: output_stream.write(line) not_imports = False diff --git a/isort/parse.py b/isort/parse.py index 6089c08b0..3bfa3b0ff 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -11,8 +11,6 @@ from .comments import parse as parse_comments from .finders import FindersManager -IMPORT_START_IDENTIFIERS = ("from ", "from.import", "import ", "import*") - if TYPE_CHECKING: from mypy_extensions import TypedDict @@ -46,8 +44,10 @@ def _normalize_line(raw_line: str) -> Tuple[str, str]: Returns (normalized_line: str, raw_line: str) """ line = raw_line.replace("from.import ", "from . import ") + line = raw_line.replace("from.cimport ", "from . cimport ") line = line.replace("import*", "import *") line = line.replace(" .import ", " . import ") + line = line.replace(" .cimport ", " . cimport ") line = line.replace("\t", " ") return (line, raw_line) @@ -56,7 +56,7 @@ def import_type(line: str) -> Optional[str]: """If the current line is an import line it will return its type (from or straight)""" if "isort:skip" in line or "isort: skip" in line or "NOQA" in line: return None - elif line.startswith("import "): + elif line.startswith(("import ", "cimport ")): return "straight" elif line.startswith("from "): return "from" @@ -68,7 +68,7 @@ def _strip_syntax(import_string: str) -> str: for remove_syntax in ["\\", "(", ")", ","]: import_string = import_string.replace(remove_syntax, " ") import_list = import_string.split() - for key in ("from", "import"): + for key in ("from", "import", "cimport"): if key in import_list: import_list.remove(key) import_string = " ".join(import_list) @@ -108,7 +108,7 @@ def skip_line( if ";" in line: for part in (part.strip() for part in line.split(";")): - if part and not part.startswith("from ") and not part.startswith("import "): + if part and not part.startswith("from ") and not part.startswith(("import ", "cimport ")): skip_line = True return (bool(skip_line or in_quote), in_quote) @@ -267,18 +267,22 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte and new_comment ): nested_comments[stripped_line] = comments[-1] - if import_string.strip().endswith(" import") or line.strip().startswith( - "import " - ): + if import_string.strip().endswith((" import", " cimport")) or line.strip().startswith(("import ", "cimport ")): import_string += line_separator + line else: import_string = import_string.rstrip().rstrip("\\") + " " + line.lstrip() if type_of_import == "from": + cimport: bool import_string = import_string.replace("import(", "import (") - parts = import_string.split(" import ") + if " cimport " in import_string: + parts = import_string.split(" cimport ") + cimport = True + else: + parts = import_string.split(" import ") + cimport = False from_import = parts[0].split(" ") - import_string = " import ".join( + import_string = " cimport " if cimport else " import ".join( [from_import[0] + " " + "".join(from_import[1:])] + parts[1:] ) From 7dab76f00370c97ee68d9aaed10781cb91d4f933 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 4 Jan 2020 23:29:30 -0800 Subject: [PATCH 0295/1439] Add ability to output cimport or import to output.py --- isort/output.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/isort/output.py b/isort/output.py index f2d287a73..73b1e4cb1 100644 --- a/isort/output.py +++ b/isort/output.py @@ -11,7 +11,7 @@ def sorted_imports( - parsed: parse.ParsedContent, config: Config = DEFAULT_CONFIG, extension: str = "py" + parsed: parse.ParsedContent, config: Config = DEFAULT_CONFIG, extension: str = "py", import_type: str="import" ) -> str: """Adds the imports back to the file. @@ -61,15 +61,16 @@ def sorted_imports( section_output, sort_ignore_case, remove_imports, + import_type, ) if config.lines_between_types and from_modules and straight_modules: section_output.extend([""] * config.lines_between_types) section_output = _with_straight_imports( - parsed, config, straight_modules, section, section_output, remove_imports + parsed, config, straight_modules, section, section_output, remove_imports, import_type ) else: section_output = _with_straight_imports( - parsed, config, straight_modules, section, section_output, remove_imports + parsed, config, straight_modules, section, section_output, remove_imports, import_type ) if config.lines_between_types and from_modules and straight_modules: section_output.extend([""] * config.lines_between_types) @@ -81,6 +82,7 @@ def sorted_imports( section_output, sort_ignore_case, remove_imports, + import_type, ) if config.force_sort_within_sections: @@ -216,13 +218,14 @@ def _with_from_imports( section_output: List[str], ignore_case: bool, remove_imports: List[str], + import_type: str, ) -> List[str]: new_section_output = section_output.copy() for module in from_modules: if module in remove_imports: continue - import_start = f"from {module} import " + import_start = f"from {module} {import_type} " from_imports = list(parsed.imports[section]["from"][module]) if not config.no_inline_sort or config.force_single_line: from_imports = sorting.naturally( @@ -475,6 +478,7 @@ def _with_straight_imports( section: str, section_output: List[str], remove_imports: List[str], + import_type: str, ) -> List[str]: new_section_output = section_output.copy() for module in straight_modules: @@ -484,12 +488,12 @@ def _with_straight_imports( import_definition = [] if module in parsed.as_map: if config.keep_direct_and_as_imports and parsed.imports[section]["straight"][module]: - import_definition.append(f"import {module}") + import_definition.append(f"{import_type} {module}") import_definition.extend( - f"import {module} as {as_import}" for as_import in parsed.as_map[module] + f"{import_type} {module} as {as_import}" for as_import in parsed.as_map[module] ) else: - import_definition.append(f"import {module}") + import_definition.append(f"{import_type} {module}") comments_above = parsed.categorized_comments["above"]["straight"].pop(module, None) if comments_above: From 8c8330dcaa7a4b38bcd2bd51f7eced890e049beb Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 5 Jan 2020 00:37:29 -0800 Subject: [PATCH 0296/1439] Initially working cimport support --- isort/api.py | 17 +++++++++++------ isort/output.py | 21 ++++++++++++++++++--- isort/parse.py | 22 ++++++++++++++++------ tests/test_comments.py | 1 + tests/test_output.py | 1 + tests/test_parse.py | 1 + tests/test_wrap_modes.py | 1 + 7 files changed, 49 insertions(+), 15 deletions(-) diff --git a/isort/api.py b/isort/api.py index b7126999e..99d717eb0 100644 --- a/isort/api.py +++ b/isort/api.py @@ -17,8 +17,6 @@ from .io import File from .settings import DEFAULT_CONFIG, FILE_SKIP_COMMENTS, Config -IMPORT_START_IDENTIFIERS = ("from ", "from.import", "import ", "import*") - CIMPORT_IDENTIFIERS = ("cimport ", "cimport*", "from.cimport") IMPORT_START_IDENTIFIERS = ("from ", "from.import", "import ", "import*") + CIMPORT_IDENTIFIERS COMMENT_INDICATORS = ('"""', "'''", "'", '"', "#") @@ -249,9 +247,10 @@ def sort_imports( cimport_statement: bool = False if ( import_statement.lstrip().startswith(CIMPORT_IDENTIFIERS) - or "cimport " in import_statement - or "cimport*" in import_statement - or "cimport(" in import_statement + or " cimport " in import_statement + or " cimport*" in import_statement + or " cimport(" in import_statement + or ".cimport" in import_statement ): cimport_statement = True @@ -264,7 +263,7 @@ def sort_imports( else: cimports = cimport_statement - import_section + import_statement + import_section += import_statement else: not_imports = True @@ -280,6 +279,10 @@ def sort_imports( contains_imports = True add_imports = [] + if next_import_section and not import_section: + import_section = next_import_section + next_import_section = "" + if import_section: if add_imports and not indent: import_section += line_separator.join(add_imports) + line_separator @@ -313,6 +316,8 @@ def sort_imports( ) output_stream.write(sorted_import_section) + if not line and next_import_section: + output_stream.write(line_separator) if indent: output_stream.write(line) diff --git a/isort/output.py b/isort/output.py index 73b1e4cb1..5f2659f76 100644 --- a/isort/output.py +++ b/isort/output.py @@ -11,7 +11,10 @@ def sorted_imports( - parsed: parse.ParsedContent, config: Config = DEFAULT_CONFIG, extension: str = "py", import_type: str="import" + parsed: parse.ParsedContent, + config: Config = DEFAULT_CONFIG, + extension: str = "py", + import_type: str = "import", ) -> str: """Adds the imports back to the file. @@ -66,11 +69,23 @@ def sorted_imports( if config.lines_between_types and from_modules and straight_modules: section_output.extend([""] * config.lines_between_types) section_output = _with_straight_imports( - parsed, config, straight_modules, section, section_output, remove_imports, import_type + parsed, + config, + straight_modules, + section, + section_output, + remove_imports, + import_type, ) else: section_output = _with_straight_imports( - parsed, config, straight_modules, section, section_output, remove_imports, import_type + parsed, + config, + straight_modules, + section, + section_output, + remove_imports, + import_type, ) if config.lines_between_types and from_modules and straight_modules: section_output.extend([""] * config.lines_between_types) diff --git a/isort/parse.py b/isort/parse.py index 3bfa3b0ff..6810176ee 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -65,6 +65,7 @@ def import_type(line: str) -> Optional[str]: def _strip_syntax(import_string: str) -> str: import_string = import_string.replace("_import", "[[i]]") + import_string = import_string.replace("_cimport", "[[ci]]") for remove_syntax in ["\\", "(", ")", ","]: import_string = import_string.replace(remove_syntax, " ") import_list = import_string.split() @@ -73,6 +74,7 @@ def _strip_syntax(import_string: str) -> str: import_list.remove(key) import_string = " ".join(import_list) import_string = import_string.replace("[[i]]", "_import") + import_string = import_string.replace("[[ci]]", "_cimport") return import_string.replace("{ ", "{|").replace(" }", "|}") @@ -108,7 +110,11 @@ def skip_line( if ";" in line: for part in (part.strip() for part in line.split(";")): - if part and not part.startswith("from ") and not part.startswith(("import ", "cimport ")): + if ( + part + and not part.startswith("from ") + and not part.startswith(("import ", "cimport ")) + ): skip_line = True return (bool(skip_line or in_quote), in_quote) @@ -267,22 +273,26 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte and new_comment ): nested_comments[stripped_line] = comments[-1] - if import_string.strip().endswith((" import", " cimport")) or line.strip().startswith(("import ", "cimport ")): + if import_string.strip().endswith( + (" import", " cimport") + ) or line.strip().startswith(("import ", "cimport ")): import_string += line_separator + line else: import_string = import_string.rstrip().rstrip("\\") + " " + line.lstrip() if type_of_import == "from": - cimport: bool + cimports: bool import_string = import_string.replace("import(", "import (") if " cimport " in import_string: parts = import_string.split(" cimport ") - cimport = True + cimports = True + else: parts = import_string.split(" import ") - cimport = False + cimports = False + from_import = parts[0].split(" ") - import_string = " cimport " if cimport else " import ".join( + import_string = (" cimport " if cimports else " import ").join( [from_import[0] + " " + "".join(from_import[1:])] + parts[1:] ) diff --git a/tests/test_comments.py b/tests/test_comments.py index 1805f137c..b1b4ed7b4 100644 --- a/tests/test_comments.py +++ b/tests/test_comments.py @@ -1,4 +1,5 @@ from hypothesis_auto import auto_pytest_magic + from isort import comments auto_pytest_magic(comments.parse) diff --git a/tests/test_output.py b/tests/test_output.py index f9e04aec3..e42093b5b 100644 --- a/tests/test_output.py +++ b/tests/test_output.py @@ -1,6 +1,7 @@ import sys from hypothesis_auto import auto_pytest_magic + from isort import output auto_pytest_magic(output.with_comments, auto_allow_exceptions_=(ValueError,)) diff --git a/tests/test_parse.py b/tests/test_parse.py index f5846bd73..efc3a22a4 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -1,4 +1,5 @@ from hypothesis_auto import auto_pytest_magic + from isort import parse from isort.settings import Config diff --git a/tests/test_wrap_modes.py b/tests/test_wrap_modes.py index 6c8a66551..dee26d4f7 100644 --- a/tests/test_wrap_modes.py +++ b/tests/test_wrap_modes.py @@ -1,6 +1,7 @@ import sys from hypothesis_auto import auto_pytest_magic + from isort import wrap_modes auto_pytest_magic(wrap_modes.grid, auto_allow_exceptions_=(ValueError,)) From c3668df374e91a54acaf0c9786ac467b19eb3f12 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 5 Jan 2020 01:17:37 -0800 Subject: [PATCH 0297/1439] Add test for cimport sorting support --- isort/parse.py | 2 +- tests/test_isort.py | 359 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 360 insertions(+), 1 deletion(-) diff --git a/isort/parse.py b/isort/parse.py index 6810176ee..92bb46b52 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -44,7 +44,7 @@ def _normalize_line(raw_line: str) -> Tuple[str, str]: Returns (normalized_line: str, raw_line: str) """ line = raw_line.replace("from.import ", "from . import ") - line = raw_line.replace("from.cimport ", "from . cimport ") + line = line.replace("from.cimport ", "from . cimport ") line = line.replace("import*", "import *") line = line.replace(" .import ", " . import ") line = line.replace(" .cimport ", " . cimport ") diff --git a/tests/test_isort.py b/tests/test_isort.py index 451af9e24..34fa7bc69 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -4238,3 +4238,362 @@ def test_comment_look_alike(): import sys ''' ) + + +def test_cimport_support(): + """Test to ensure cimports (Cython style imports) work""" + test_input = """ +import os +import sys +import cython +import platform +import traceback +import time +import types +import re +import copy +import inspect # used by JavascriptBindings.__SetObjectMethods() +import urllib +import json +import datetime +import random + +if sys.version_info.major == 2: + import urlparse +else: + from urllib import parse as urlparse + +if sys.version_info.major == 2: + from urllib import pathname2url as urllib_pathname2url +else: + from urllib.request import pathname2url as urllib_pathname2url + +from cpython.version cimport PY_MAJOR_VERSION +import weakref + +# We should allow multiple string types: str, unicode, bytes. +# PyToCefString() can handle them all. +# Important: +# If you set it to basestring, Cython will accept exactly(!) +# str/unicode in Py2 and str in Py3. This won't work in Py3 +# as we might want to pass bytes as well. Also it will +# reject string subtypes, so using it in publi API functions +# would be a bad idea. +ctypedef object py_string + +# You can't use "void" along with cpdef function returning None, it is planned to be +# added to Cython in the future, creating this virtual type temporarily. If you +# change it later to "void" then don't forget to add "except *". +ctypedef object py_void +ctypedef long WindowHandle + +from cpython cimport PyLong_FromVoidPtr + +from cpython cimport bool as py_bool +from libcpp cimport bool as cpp_bool + +from libcpp.map cimport map as cpp_map +from multimap cimport multimap as cpp_multimap +from libcpp.pair cimport pair as cpp_pair +from libcpp.vector cimport vector as cpp_vector + +from libcpp.string cimport string as cpp_string +from wstring cimport wstring as cpp_wstring + +from libc.string cimport strlen +from libc.string cimport memcpy + +# preincrement and dereference must be "as" otherwise not seen. +from cython.operator cimport preincrement as preinc, dereference as deref + +# from cython.operator cimport address as addr # Address of an c++ object? + +from libc.stdlib cimport calloc, malloc, free +from libc.stdlib cimport atoi + +# When pyx file cimports * from a pxd file and that pxd cimports * from another pxd +# then these names will be visible in pyx file. + +# Circular imports are allowed in form "cimport ...", but won't work if you do +# "from ... cimport *", this is important to know in pxd files. + +from libc.stdint cimport uint64_t +from libc.stdint cimport uintptr_t + +cimport ctime + +IF UNAME_SYSNAME == "Windows": + from windows cimport * + from dpi_aware_win cimport * +ELIF UNAME_SYSNAME == "Linux": + from linux cimport * +ELIF UNAME_SYSNAME == "Darwin": + from mac cimport * + +from cpp_utils cimport * +from task cimport * + +from cef_string cimport * +cdef extern from *: + ctypedef CefString ConstCefString "const CefString" + +from cef_types_wrappers cimport * +from cef_task cimport * +from cef_runnable cimport * + +from cef_platform cimport * + +from cef_ptr cimport * +from cef_app cimport * +from cef_browser cimport * +cimport cef_browser_static +from cef_client cimport * +from client_handler cimport * +from cef_frame cimport * + +# cannot cimport *, that would cause name conflicts with constants. +cimport cef_types +ctypedef cef_types.cef_paint_element_type_t PaintElementType +ctypedef cef_types.cef_jsdialog_type_t JSDialogType +from cef_types cimport CefKeyEvent +from cef_types cimport CefMouseEvent +from cef_types cimport CefScreenInfo + +# cannot cimport *, name conflicts +IF UNAME_SYSNAME == "Windows": + cimport cef_types_win +ELIF UNAME_SYSNAME == "Darwin": + cimport cef_types_mac +ELIF UNAME_SYSNAME == "Linux": + cimport cef_types_linux + +from cef_time cimport * +from cef_drag cimport * + +IF CEF_VERSION == 1: + from cef_v8 cimport * + cimport cef_v8_static + cimport cef_v8_stack_trace + from v8function_handler cimport * + from cef_request_cef1 cimport * + from cef_web_urlrequest_cef1 cimport * + cimport cef_web_urlrequest_static_cef1 + from web_request_client_cef1 cimport * + from cef_stream cimport * + cimport cef_stream_static + from cef_response_cef1 cimport * + from cef_stream cimport * + from cef_content_filter cimport * + from content_filter_handler cimport * + from cef_download_handler cimport * + from download_handler cimport * + from cef_cookie_cef1 cimport * + cimport cef_cookie_manager_namespace + from cookie_visitor cimport * + from cef_render_handler cimport * + from cef_drag_data cimport * + +IF UNAME_SYSNAME == "Windows": + IF CEF_VERSION == 1: + from http_authentication cimport * + +IF CEF_VERSION == 3: + from cef_values cimport * + from cefpython_app cimport * + from cef_process_message cimport * + from cef_web_plugin_cef3 cimport * + from cef_request_handler_cef3 cimport * + from cef_request_cef3 cimport * + from cef_cookie_cef3 cimport * + from cef_string_visitor cimport * + cimport cef_cookie_manager_namespace + from cookie_visitor cimport * + from string_visitor cimport * + from cef_callback_cef3 cimport * + from cef_response_cef3 cimport * + from cef_resource_handler_cef3 cimport * + from resource_handler_cef3 cimport * + from cef_urlrequest_cef3 cimport * + from web_request_client_cef3 cimport * + from cef_command_line cimport * + from cef_request_context cimport * + from cef_request_context_handler cimport * + from request_context_handler cimport * + from cef_jsdialog_handler cimport * +""" + expected_output = """ +import copy +import datetime +import inspect # used by JavascriptBindings.__SetObjectMethods() +import json +import os +import platform +import random +import re +import sys +import time +import traceback +import types +import urllib + +import cython + +if sys.version_info.major == 2: + import urlparse + +else: + from urllib import parse as urlparse + +if sys.version_info.major == 2: + from urllib import pathname2url as urllib_pathname2url + +else: + from urllib.request import pathname2url as urllib_pathname2url + +from cpython.version cimport PY_MAJOR_VERSION + +import weakref + +# We should allow multiple string types: str, unicode, bytes. +# PyToCefString() can handle them all. +# Important: +# If you set it to basestring, Cython will accept exactly(!) +# str/unicode in Py2 and str in Py3. This won't work in Py3 +# as we might want to pass bytes as well. Also it will +# reject string subtypes, so using it in publi API functions +# would be a bad idea. +ctypedef object py_string + +# You can't use "void" along with cpdef function returning None, it is planned to be +# added to Cython in the future, creating this virtual type temporarily. If you +# change it later to "void" then don't forget to add "except *". +ctypedef object py_void +ctypedef long WindowHandle + +cimport ctime +from cpython cimport PyLong_FromVoidPtr +from cpython cimport bool as py_bool +# preincrement and dereference must be "as" otherwise not seen. +from cython.operator cimport dereference as deref +from cython.operator cimport preincrement as preinc +from libc.stdint cimport uint64_t, uintptr_t +from libc.stdlib cimport atoi, calloc, free, malloc +from libc.string cimport memcpy, strlen +from libcpp cimport bool as cpp_bool +from libcpp.map cimport map as cpp_map +from libcpp.pair cimport pair as cpp_pair +from libcpp.string cimport string as cpp_string +from libcpp.vector cimport vector as cpp_vector +from multimap cimport multimap as cpp_multimap +from wstring cimport wstring as cpp_wstring + +# from cython.operator cimport address as addr # Address of an c++ object? + + +# When pyx file cimports * from a pxd file and that pxd cimports * from another pxd +# then these names will be visible in pyx file. + +# Circular imports are allowed in form "cimport ...", but won't work if you do +# "from ... cimport *", this is important to know in pxd files. + + + +IF UNAME_SYSNAME == "Windows": + from dpi_aware_win cimport * + from windows cimport * + +ELIF UNAME_SYSNAME == "Linux": + from linux cimport * + +ELIF UNAME_SYSNAME == "Darwin": + from mac cimport * + +from cef_string cimport * +from cpp_utils cimport * +from task cimport * + +cdef extern from *: + ctypedef CefString ConstCefString "const CefString" + +cimport cef_browser_static +# cannot cimport *, that would cause name conflicts with constants. +cimport cef_types +from cef_app cimport * +from cef_browser cimport * +from cef_client cimport * +from cef_frame cimport * +from cef_platform cimport * +from cef_ptr cimport * +from cef_runnable cimport * +from cef_task cimport * +from cef_types_wrappers cimport * +from client_handler cimport * + +ctypedef cef_types.cef_paint_element_type_t PaintElementType +ctypedef cef_types.cef_jsdialog_type_t JSDialogType +from cef_types cimport CefKeyEvent, CefMouseEvent, CefScreenInfo + +# cannot cimport *, name conflicts +IF UNAME_SYSNAME == "Windows": + cimport cef_types_win + +ELIF UNAME_SYSNAME == "Darwin": + cimport cef_types_mac + +ELIF UNAME_SYSNAME == "Linux": + cimport cef_types_linux + +from cef_drag cimport * +from cef_time cimport * + +IF CEF_VERSION == 1: + cimport cef_cookie_manager_namespace + cimport cef_stream_static + cimport cef_v8_stack_trace + cimport cef_v8_static + cimport cef_web_urlrequest_static_cef1 + from cef_content_filter cimport * + from cef_cookie_cef1 cimport * + from cef_download_handler cimport * + from cef_drag_data cimport * + from cef_render_handler cimport * + from cef_request_cef1 cimport * + from cef_response_cef1 cimport * + from cef_stream cimport * + from cef_v8 cimport * + from cef_web_urlrequest_cef1 cimport * + from content_filter_handler cimport * + from cookie_visitor cimport * + from download_handler cimport * + from v8function_handler cimport * + from web_request_client_cef1 cimport * + +IF UNAME_SYSNAME == "Windows": + IF CEF_VERSION == 1: + from http_authentication cimport * + +IF CEF_VERSION == 3: + cimport cef_cookie_manager_namespace + from cef_callback_cef3 cimport * + from cef_command_line cimport * + from cef_cookie_cef3 cimport * + from cef_jsdialog_handler cimport * + from cef_process_message cimport * + from cef_request_cef3 cimport * + from cef_request_context cimport * + from cef_request_context_handler cimport * + from cef_request_handler_cef3 cimport * + from cef_resource_handler_cef3 cimport * + from cef_response_cef3 cimport * + from cef_string_visitor cimport * + from cef_urlrequest_cef3 cimport * + from cef_values cimport * + from cef_web_plugin_cef3 cimport * + from cefpython_app cimport * + from cookie_visitor cimport * + from request_context_handler cimport * + from resource_handler_cef3 cimport * + from string_visitor cimport * + from web_request_client_cef3 cimport * +""" + SortImports(file_contents=test_input).output == expected_output From 725578a19f3a09d754467068b3c0bf3c23c73365 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 5 Jan 2020 02:57:19 -0800 Subject: [PATCH 0298/1439] Bump version in preperation for 5.0.0 release --- isort/_version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/_version.py b/isort/_version.py index b82594be2..ba7be38e4 100644 --- a/isort/_version.py +++ b/isort/_version.py @@ -1 +1 @@ -__version__ = "4.3.21" +__version__ = "5.0.0" diff --git a/pyproject.toml b/pyproject.toml index 9b7ba816b..0b25817e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "isort" -version = "4.3.21" +version = "5.0.0" description = "A Python utility / library to sort Python imports." authors = ["Timothy Crosley "] license = "MIT" From 0e8f7a3ace269ad0229b6cef56ff6aef5e06a8b0 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 5 Jan 2020 03:12:58 -0800 Subject: [PATCH 0299/1439] Add missing test cases --- tests/test_isort.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/test_isort.py b/tests/test_isort.py index 34fa7bc69..0bed7f49e 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -963,6 +963,23 @@ def test_force_single_line_imports() -> None: "from third_party import lib22\n" ) + test_input = ( + "from third_party import lib_a, lib_b, lib_d\n" + "from third_party.lib_c import lib1\n" + ) + test_output = SortImports( + file_contents=test_input, + multi_line_output=WrapModes.GRID, + line_length=40, + force_single_line=True, + ).output + assert test_output == ( + "from third_party import lib_a\n" + "from third_party import lib_b\n" + "from third_party import lib_d\n" + "from third_party.lib_c import lib1\n" + ) + def test_force_single_line_long_imports() -> None: test_input = "from veryveryveryveryveryvery import small, big\n" @@ -4597,3 +4614,12 @@ def test_cimport_support(): from web_request_client_cef3 cimport * """ SortImports(file_contents=test_input).output == expected_output + + +def test_top_level_import_order() -> None: + config = {"force_sort_within_sections": 1} # type: Dict[str, Any] + test_input = ( + "from rest_framework import throttling, viewsets\n" + "from rest_framework.authentication import TokenAuthentication\n" + ) + assert SortImports(file_contents=test_input, **config).output == test_input From 55b9aac25e9527badd5ae7139be8ba08dfa7e58f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 5 Jan 2020 04:05:35 -0800 Subject: [PATCH 0300/1439] Fix issue #1065, multiple noqa comments --- isort/comments.py | 6 +++++- isort/parse.py | 3 ++- tests/test_isort.py | 37 +++++++++++++++++++++++++++++++++---- 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/isort/comments.py b/isort/comments.py index d033804de..b865b3281 100644 --- a/isort/comments.py +++ b/isort/comments.py @@ -25,4 +25,8 @@ def add_to_line( if not comments: return original_string else: - return f"{parse(original_string)[0]}{comment_prefix} {'; '.join(comments)}" + unique_comments: List[str] = [] + for comment in comments: + if comment not in unique_comments: + unique_comments.append(comment) + return f"{parse(original_string)[0]}{comment_prefix} {'; '.join(unique_comments)}" diff --git a/isort/parse.py b/isort/parse.py index 92bb46b52..8172808df 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -332,7 +332,8 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte categorized_comments["nested"].setdefault(import_from, {})[ import_name ] = associated_comment - comments.pop(comments.index(associated_comment)) + if associated_comment in comments: + comments.pop(comments.index(associated_comment)) if comments: categorized_comments["from"].setdefault(import_from, []).extend(comments) diff --git a/tests/test_isort.py b/tests/test_isort.py index 0bed7f49e..db9825876 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -964,8 +964,7 @@ def test_force_single_line_imports() -> None: ) test_input = ( - "from third_party import lib_a, lib_b, lib_d\n" - "from third_party.lib_c import lib1\n" + "from third_party import lib_a, lib_b, lib_d\n" "from third_party.lib_c import lib1\n" ) test_output = SortImports( file_contents=test_input, @@ -4617,9 +4616,39 @@ def test_cimport_support(): def test_top_level_import_order() -> None: - config = {"force_sort_within_sections": 1} # type: Dict[str, Any] test_input = ( "from rest_framework import throttling, viewsets\n" "from rest_framework.authentication import TokenAuthentication\n" ) - assert SortImports(file_contents=test_input, **config).output == test_input + assert ( + SortImports(file_contents=test_input, force_sort_within_sections=True).output == test_input + ) + + +def test_noqa_issue_1065() -> None: + test_input = """ +# +# USER SIGNALS +# + +from flask_login import user_logged_in, user_logged_out # noqa + +from flask_security.signals import ( # noqa + password_changed as user_reset_password, # noqa + user_confirmed, # noqa + user_registered, # noqa +) # noqa + +from flask_principal import identity_changed as user_identity_changed # noqa +""" + expected_output = """ +# +# USER SIGNALS +# +from flask_login import user_logged_in, user_logged_out # noqa +from flask_principal import identity_changed as user_identity_changed # noqa +from flask_security.signals import password_changed as user_reset_password # noqa +from flask_security.signals import user_confirmed # noqa +from flask_security.signals import user_registered # noqa +""" + assert SortImports(file_contents=test_input).output == expected_output From b8f80ece8a4548feb2c8f4e29113e58044e2b8a3 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 5 Jan 2020 14:05:09 -0800 Subject: [PATCH 0301/1439] Update version shorthand flags --- CHANGELOG.md | 1 + isort/main.py | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dffc26784..3b78e1658 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Changelog - Config files are no longer composed on-top of each-other. Instead the first config file found is used. - Since there is no longer composition negative form settings (such as --dont-skip) are no longer required and have been removed. - Two-letter shortened setting names (like `ac` for `atomic`) now require two dashes to avoid ambiguity: `--ac`. + - For consistency with other tools `-v` now is shorthand for verbose and `-V` is shorthand for version. See Issue: #1067. - `length_sort_{section_name}` config usage has been deprecated. Instead `length_sort_sections` list can be used to specify a list of sections that need to be length sorted. - `safety_excludes` and `unsafe` have been deprecated - Config now includes as default full set of safety directories defined by safety excludes. diff --git a/isort/main.py b/isort/main.py index 2c30cfa05..2dab21ae2 100644 --- a/isort/main.py +++ b/isort/main.py @@ -449,9 +449,10 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: action="store_true", help="Use parenthesis for line continuation on length limit instead of slashes.", ) - parser.add_argument("-v", "--version", action="store_true", dest="show_version") + parser.add_argument("-V", "--version", action="store_true", dest="show_version", + help="Displays the currently installed version of isort.") parser.add_argument( - "--vb", + "-v", "--verbose", action="store_true", dest="verbose", From 71efd733dfea8fe1262ea96bcee8f13dd8e159b7 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 5 Jan 2020 14:06:43 -0800 Subject: [PATCH 0302/1439] black formatting --- isort/main.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/isort/main.py b/isort/main.py index 2dab21ae2..18c4d164e 100644 --- a/isort/main.py +++ b/isort/main.py @@ -449,8 +449,13 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: action="store_true", help="Use parenthesis for line continuation on length limit instead of slashes.", ) - parser.add_argument("-V", "--version", action="store_true", dest="show_version", - help="Displays the currently installed version of isort.") + parser.add_argument( + "-V", + "--version", + action="store_true", + dest="show_version", + help="Displays the currently installed version of isort.", + ) parser.add_argument( "-v", "--verbose", From 07413b3ee7e9f1730c148d25d53125df48adbc31 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 5 Jan 2020 15:22:01 -0800 Subject: [PATCH 0303/1439] Fix issue #1061 --- isort/main.py | 12 ++++-------- isort/settings.py | 5 +++++ tests/test_isort.py | 12 ++++++++++++ 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/isort/main.py b/isort/main.py index 18c4d164e..dfd09873c 100644 --- a/isort/main.py +++ b/isort/main.py @@ -296,13 +296,6 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: action="store_true", help="Turns off default behavior that removes direct imports when as imports exist.", ) - parser.add_argument( - "-l", - "--lines", - help="[Deprecated] The max length of an import line (used for wrapping " "long imports).", - dest="line_length", - type=int, - ) parser.add_argument("-lai", "--lines-after-imports", dest="lines_after_imports", type=int) parser.add_argument("-lbt", "--lines-between-types", dest="lines_between_types", type=int) parser.add_argument( @@ -481,7 +474,9 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: help="Returns just the current version number without the logo", ) parser.add_argument( + "-l", "-w", + "--line-length", "--line-width", help="The max length of an import line (used for wrapping long imports).", dest="line_length", @@ -492,7 +487,8 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: "--wrap-length", dest="wrap_length", type=int, - help="Specifies how long lines that are wrapped should be, if not set line_length is used.", + help="Specifies how long lines that are wrapped should be, if not set line_length is used." + "\nNOTE: wrap_length must be LOWER than or equal to line_length.", ) parser.add_argument( "--ws", diff --git a/isort/settings.py b/isort/settings.py index 387d10e40..04b488f1c 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -208,6 +208,11 @@ def __post_init__(self): object.__setattr__(self, "no_sections", True) object.__setattr__(self, "lines_between_types", 1) object.__setattr__(self, "from_first", True) + if self.wrap_length > self.line_length: + raise ValueError( + "wrap_length must be set lower than or equal to line_length: " + f"{self.wrap_length} > {self.line_length}." + ) _DEFAULT_SETTINGS = {**vars(_Config()), "source": "defaults"} diff --git a/tests/test_isort.py b/tests/test_isort.py index db9825876..9e7bcb35e 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -196,6 +196,18 @@ def test_line_length() -> None: " lib22)\n" ) + test_input = ( + "from .test import a_very_long_function_name_that_exceeds_the_normal_pep8_line_length\n" + ) + with pytest.raises(ValueError): + test_output = SortImports( + file_contents=REALLY_LONG_IMPORT, line_length=80, wrap_length=99 + ).output + test_output = ( + SortImports(file_contents=REALLY_LONG_IMPORT, line_length=100, wrap_length=99).output + == test_input + ) + def test_output_modes() -> None: """Test setting isort to use various output modes works as expected""" From b6ec10e1142df22ca50d40a237cb526b3ecfc411 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 5 Jan 2020 16:12:14 -0800 Subject: [PATCH 0304/1439] Fix issue #1012 --- tests/test_isort.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_isort.py b/tests/test_isort.py index 9e7bcb35e..e9ca77cf4 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -3259,7 +3259,7 @@ def test_safety_skips(tmpdir, enabled: bool) -> None: ), ) def test_skip_glob(tmpdir, skip_glob_assert: Tuple[List[str], int, Set[str]]) -> None: - skip_glob, skipped_count, file_names = skip_glob_assert + skip_glob, skipped_count, file_names_expected = skip_glob_assert base_dir = tmpdir.mkdir("build") code_dir = base_dir.mkdir("code") code_dir.join("file.py").write("import os") @@ -3271,7 +3271,7 @@ def test_skip_glob(tmpdir, skip_glob_assert: Tuple[List[str], int, Set[str]]) -> for f in main.iter_source_code([str(base_dir)], config, skipped) } assert len(skipped) == skipped_count - assert file_names == file_names + assert file_names == file_names_expected def test_comments_not_removed_issue_576() -> None: From 2cb3ec41651ee7f539433d1409933e4125e4925e Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 5 Jan 2020 17:13:48 -0800 Subject: [PATCH 0305/1439] Improve documentation in regards to issue #698 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 183aec723..440f18c81 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ _________________ isort your 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 +automatically separated into sections and by type. 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 requires Python 3.6+ to run but From f02f012c7c13b8caa946d782ef8f6775972972bd Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 5 Jan 2020 17:33:50 -0800 Subject: [PATCH 0306/1439] Fix issue #812: error reporting incorrect --- isort/api.py | 2 +- isort/main.py | 4 ++-- tests/test_isort.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/isort/api.py b/isort/api.py index 99d717eb0..bf86cd0ed 100644 --- a/isort/api.py +++ b/isort/api.py @@ -112,7 +112,7 @@ def check_imports( print(f"SUCCESS: {file_path or ''} Everything Looks Good!") return True else: - print(f"ERROR: {file_path or ''} Imports are incorrectly sorted.") + print(f"ERROR: {file_path or ''} Imports are incorrectly sorted and/or formatted.") if show_diff: show_unified_diff( file_input=file_contents, file_output=sorted_output, file_path=file_path diff --git a/isort/main.py b/isort/main.py index dfd09873c..5d48f84c6 100644 --- a/isort/main.py +++ b/isort/main.py @@ -296,8 +296,8 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: action="store_true", help="Turns off default behavior that removes direct imports when as imports exist.", ) - parser.add_argument("-lai", "--lines-after-imports", dest="lines_after_imports", type=int) - parser.add_argument("-lbt", "--lines-between-types", dest="lines_between_types", type=int) + parser.add_argument("--lai", "--lines-after-imports", dest="lines_after_imports", type=int) + parser.add_argument("--lbt", "--lines-between-types", dest="lines_between_types", type=int) parser.add_argument( "--le", "--line-ending", diff --git a/tests/test_isort.py b/tests/test_isort.py index e9ca77cf4..11a7079f2 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -2587,7 +2587,7 @@ def test_strict_whitespace_by_default(capsys) -> None: test_input = "import os\nfrom django.conf import settings\n" SortImports(file_contents=test_input, check=True) out, err = capsys.readouterr() - assert out == "ERROR: Imports are incorrectly sorted.\n" + assert out == "ERROR: Imports are incorrectly sorted and/or formatted.\n" def test_strict_whitespace_no_closing_newline_issue_676(capsys) -> None: From a590eec7e75758971736a01c7aec5266935b1a4a Mon Sep 17 00:00:00 2001 From: hakancelik Date: Mon, 6 Jan 2020 18:36:12 +0300 Subject: [PATCH 0307/1439] #1081 :art: unimport applied --- isort/api.py | 4 +--- isort/compat.py | 7 ++----- isort/finders.py | 4 +--- isort/io.py | 2 +- isort/main.py | 2 +- isort/output.py | 2 +- isort/parse.py | 6 ++---- isort/settings.py | 7 ------- isort/sorting.py | 2 +- isort/wrap.py | 2 +- isort/wrap_modes.py | 4 ++-- pyproject.toml | 5 +++++ tests/test_output.py | 2 -- tests/test_wrap_modes.py | 2 -- 14 files changed, 18 insertions(+), 33 deletions(-) diff --git a/isort/api.py b/isort/api.py index bf86cd0ed..49c693ae0 100644 --- a/isort/api.py +++ b/isort/api.py @@ -1,9 +1,8 @@ -import re import textwrap from io import StringIO from itertools import chain from pathlib import Path -from typing import Any, List, NamedTuple, Optional, TextIO, Tuple +from typing import List, Optional, TextIO from . import output, parse from .exceptions import ( @@ -11,7 +10,6 @@ FileSkipComment, FileSkipSetting, IntroducedSyntaxErrors, - UnableToDetermineEncoding, ) from .format import format_natural, remove_whitespace, show_unified_diff from .io import File diff --git a/isort/compat.py b/isort/compat.py index 9bc5aea83..3c202427e 100644 --- a/isort/compat.py +++ b/isort/compat.py @@ -1,12 +1,9 @@ -import locale -import os -import re import sys from pathlib import Path -from typing import Any, Optional, Tuple +from typing import Any, Optional from warnings import warn -from . import api, settings +from . import api from .exceptions import ExistingSyntaxErrors, FileSkipped, IntroducedSyntaxErrors from .format import ask_whether_to_apply_changes_to_file, show_unified_diff from .io import File diff --git a/isort/finders.py b/isort/finders.py index 5a7c74a74..07219f074 100644 --- a/isort/finders.py +++ b/isort/finders.py @@ -11,12 +11,10 @@ from functools import lru_cache from glob import glob from typing import ( - Any, Dict, Iterable, Iterator, List, - Mapping, Optional, Pattern, Sequence, @@ -25,7 +23,7 @@ ) from . import sections -from .settings import DEFAULT_CONFIG, Config +from .settings import Config from .utils import chdir, exists_case_sensitive try: diff --git a/isort/io.py b/isort/io.py index 5845e9c1e..bf9934826 100644 --- a/isort/io.py +++ b/isort/io.py @@ -2,7 +2,7 @@ import locale import re from pathlib import Path -from typing import NamedTuple, Optional, Tuple +from typing import NamedTuple, Tuple from .exceptions import UnableToDetermineEncoding diff --git a/isort/main.py b/isort/main.py index 5d48f84c6..dc2a42803 100644 --- a/isort/main.py +++ b/isort/main.py @@ -6,7 +6,7 @@ import re import sys from pathlib import Path -from typing import Any, Dict, Iterable, Iterator, List, MutableMapping, Optional, Sequence +from typing import Any, Dict, Iterable, Iterator, List, Optional, Sequence from warnings import warn import setuptools diff --git a/isort/output.py b/isort/output.py index 5f2659f76..d6a5f5626 100644 --- a/isort/output.py +++ b/isort/output.py @@ -1,7 +1,7 @@ import copy import itertools from functools import partial -from typing import Any, Dict, Iterable, List, Optional +from typing import Iterable, List from isort.format import format_simplified diff --git a/isort/parse.py b/isort/parse.py index 8172808df..09e381beb 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -1,11 +1,9 @@ """Defines parsing functions used by isort for parsing import definitions""" -from collections import OrderedDict, defaultdict, namedtuple -from io import StringIO +from collections import OrderedDict, defaultdict from itertools import chain -from typing import TYPE_CHECKING, Any, Dict, Generator, Iterator, List, NamedTuple, Optional, Tuple +from typing import TYPE_CHECKING, Any, Dict, List, NamedTuple, Optional, Tuple from warnings import warn -from isort.format import format_natural from isort.settings import DEFAULT_CONFIG, Config from .comments import parse as parse_comments diff --git a/isort/settings.py b/isort/settings.py index 04b488f1c..1885eef33 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -6,11 +6,9 @@ in ~/.isort.cfg or $XDG_CONFIG_HOME/isort.cfg if there are any) """ import configparser -import enum import fnmatch import os import posixpath -import re import sys import warnings from distutils.util import strtobool as _as_bool @@ -23,12 +21,8 @@ FrozenSet, Iterable, List, - Mapping, - MutableMapping, - Optional, Set, Tuple, - Union, ) from warnings import warn @@ -37,7 +31,6 @@ from .exceptions import ProfileDoesNotExist from .profiles import profiles from .sections import DEFAULT as SECTION_DEFAULTS -from .utils import difference, union from .wrap_modes import WrapModes from .wrap_modes import from_string as wrap_mode_from_string diff --git a/isort/sorting.py b/isort/sorting.py index e3aa277ab..abd6bdbd7 100644 --- a/isort/sorting.py +++ b/isort/sorting.py @@ -1,5 +1,5 @@ import re -from typing import Any, Callable, Iterable, List, Mapping, Optional +from typing import Any, Callable, Iterable, List, Optional from .settings import Config diff --git a/isort/wrap.py b/isort/wrap.py index db67f105f..37bf6ec26 100644 --- a/isort/wrap.py +++ b/isort/wrap.py @@ -1,6 +1,6 @@ import copy import re -from typing import Any, Dict, List, Optional, Sequence +from typing import List, Optional, Sequence from .settings import DEFAULT_CONFIG, Config from .wrap_modes import WrapModes as Modes diff --git a/isort/wrap_modes.py b/isort/wrap_modes.py index d3d5a2ba2..9b0a74b85 100644 --- a/isort/wrap_modes.py +++ b/isort/wrap_modes.py @@ -1,9 +1,9 @@ """Defines all wrap modes that can be used when outputting formatted imports""" import enum from inspect import signature -from typing import Any, Callable, Dict, List, Sequence +from typing import Any, Callable, Dict, List -from . import comments, settings +from . import comments _wrap_modes: Dict[str, Callable[[Any], str]] = {} diff --git a/pyproject.toml b/pyproject.toml index 0b25817e6..60fb4bc20 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,3 +85,8 @@ palette = {primary = "orange", accent = "indigo"} requires = ["poetry>=0.12"] build-backend = "poetry.masonry.api" +[tool.unimport] +files = [ + '.*(isort.py)', + '.*(__init__.py)', +] \ No newline at end of file diff --git a/tests/test_output.py b/tests/test_output.py index e42093b5b..2457f70d7 100644 --- a/tests/test_output.py +++ b/tests/test_output.py @@ -1,5 +1,3 @@ -import sys - from hypothesis_auto import auto_pytest_magic from isort import output diff --git a/tests/test_wrap_modes.py b/tests/test_wrap_modes.py index dee26d4f7..203ba448b 100644 --- a/tests/test_wrap_modes.py +++ b/tests/test_wrap_modes.py @@ -1,5 +1,3 @@ -import sys - from hypothesis_auto import auto_pytest_magic from isort import wrap_modes From ee6e7b236ed99393b24cfc643b0fc0dfbd0d8392 Mon Sep 17 00:00:00 2001 From: hakancelik Date: Mon, 6 Jan 2020 18:57:14 +0300 Subject: [PATCH 0308/1439] #1081 reset pyproject.toml --- pyproject.toml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 60fb4bc20..b379166c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,10 +83,4 @@ palette = {primary = "orange", accent = "indigo"} [build-system] requires = ["poetry>=0.12"] -build-backend = "poetry.masonry.api" - -[tool.unimport] -files = [ - '.*(isort.py)', - '.*(__init__.py)', -] \ No newline at end of file +build-backend = "poetry.masonry.api" \ No newline at end of file From 3a51988fa75d4e2b71c49764395c5c4564954490 Mon Sep 17 00:00:00 2001 From: hakancelik Date: Mon, 6 Jan 2020 18:58:57 +0300 Subject: [PATCH 0309/1439] #1081 reset pyproject.toml 2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b379166c2..59eb37189 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,4 +83,4 @@ palette = {primary = "orange", accent = "indigo"} [build-system] requires = ["poetry>=0.12"] -build-backend = "poetry.masonry.api" \ No newline at end of file +build-backend = "poetry.masonry.api" From 4dc2a71af88d59b664a8bfdcfcc7acd6412bed76 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 6 Jan 2020 08:50:52 -0800 Subject: [PATCH 0310/1439] Add cdef and cpdef support --- isort/api.py | 4 +++- isort/output.py | 11 ++++------- tests/test_isort.py | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 8 deletions(-) diff --git a/isort/api.py b/isort/api.py index bf86cd0ed..004c5f5b9 100644 --- a/isort/api.py +++ b/isort/api.py @@ -323,9 +323,11 @@ def sort_imports( output_stream.write(line) indent = "" - contains_imports = False if next_import_section: cimports = not cimports + contains_imports = True + else: + contains_imports = False import_section = next_import_section next_import_section = "" else: diff --git a/isort/output.py b/isort/output.py index 5f2659f76..cd925c91c 100644 --- a/isort/output.py +++ b/isort/output.py @@ -1,7 +1,7 @@ import copy import itertools from functools import partial -from typing import Any, Dict, Iterable, List, Optional +from typing import Any, Dict, Iterable, List, Optional, Tuple from isort.format import format_simplified @@ -9,6 +9,8 @@ from .comments import add_to_line as with_comments from .settings import DEFAULT_CONFIG, Config +STATEMENT_DECLERATIONS: Tuple[str, ...] = ("def ", "cdef ", "cpdef ", "class ", "@", "async def") + def sorted_imports( parsed: parse.ParsedContent, @@ -202,12 +204,7 @@ def sorted_imports( if config.lines_after_imports != -1: formatted_output[imports_tail:0] = ["" for line in range(config.lines_after_imports)] - elif extension != "pyi" and ( - next_construct.startswith("def ") - or next_construct.startswith("class ") - or next_construct.startswith("@") - or next_construct.startswith("async def") - ): + elif extension != "pyi" and next_construct.startswith(STATEMENT_DECLERATIONS): formatted_output[imports_tail:0] = ["", ""] else: formatted_output[imports_tail:0] = [""] diff --git a/tests/test_isort.py b/tests/test_isort.py index 11a7079f2..0228b414a 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -4627,6 +4627,44 @@ def test_cimport_support(): SortImports(file_contents=test_input).output == expected_output +def test_cdef_support(): + assert ( + SortImports( + file_contents=""" +from cpython.version cimport PY_MAJOR_VERSION + +cdef extern from *: + ctypedef CefString ConstCefString "const CefString" +""" + ).output + == """ +from cpython.version cimport PY_MAJOR_VERSION + + +cdef extern from *: + ctypedef CefString ConstCefString "const CefString" +""" + ) + + assert ( + SortImports( + file_contents=""" +from cpython.version cimport PY_MAJOR_VERSION + +cpdef extern from *: + ctypedef CefString ConstCefString "const CefString" +""" + ).output + == """ +from cpython.version cimport PY_MAJOR_VERSION + + +cpdef extern from *: + ctypedef CefString ConstCefString "const CefString" +""" + ) + + def test_top_level_import_order() -> None: test_input = ( "from rest_framework import throttling, viewsets\n" From 4384bc058968a83e095ae0ce14fafbff90fa4c1a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 7 Jan 2020 10:28:59 -0800 Subject: [PATCH 0311/1439] Add support for automatically skipping over fifo files --- isort/main.py | 7 +++++++ tests/test_main.py | 17 +++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 tests/test_main.py diff --git a/isort/main.py b/isort/main.py index 5d48f84c6..b9b866874 100644 --- a/isort/main.py +++ b/isort/main.py @@ -4,6 +4,7 @@ import glob import os import re +import stat import sys from pathlib import Path from typing import Any, Dict, Iterable, Iterator, List, MutableMapping, Optional, Sequence @@ -44,6 +45,12 @@ def is_python_file(path: str) -> bool: if path.endswith("~"): return False + try: + if stat.S_ISFIFO(os.stat(path).st_mode): + return False + except OSError: + pass + try: with open(path, "rb") as fp: line = fp.readline(100) diff --git a/tests/test_main.py b/tests/test_main.py new file mode 100644 index 000000000..b3e50b542 --- /dev/null +++ b/tests/test_main.py @@ -0,0 +1,17 @@ +import os +from isort import main + + +def test_is_python_file(tmpdir): + assert main.is_python_file("file.py") + assert main.is_python_file("file.pyi") + assert main.is_python_file("file.pyx") + assert not main.is_python_file("file.pyc") + assert not main.is_python_file("file.txt") + + fifo_file = os.path.join(tmpdir, "fifo_file") + os.mkfifo(fifo_file) + assert not main.is_python_file(fifo_file) + + + From 70c904d8c951df3faaa8ee3573c8bb9eb7203814 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 7 Jan 2020 10:58:07 -0800 Subject: [PATCH 0312/1439] Only test fifo on unix systems --- tests/test_main.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/test_main.py b/tests/test_main.py index b3e50b542..300108a08 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,17 +1,21 @@ import os +import sys + +import pytest + from isort import main -def test_is_python_file(tmpdir): +def test_is_python_file(): assert main.is_python_file("file.py") assert main.is_python_file("file.pyi") assert main.is_python_file("file.pyx") assert not main.is_python_file("file.pyc") assert not main.is_python_file("file.txt") + +@pytest.mark.skipif(sys.platform == "win32", reason="cannot create fifo file on Windows platform") +def test_is_python_file_fifo(tmpdir): fifo_file = os.path.join(tmpdir, "fifo_file") os.mkfifo(fifo_file) assert not main.is_python_file(fifo_file) - - - From ad49f79cfd88807190e20d664a799e8a660a79d8 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 7 Jan 2020 12:01:42 -0800 Subject: [PATCH 0313/1439] Fix issue #969: Add support for single line exclusions --- isort/main.py | 7 +++++++ isort/output.py | 6 ++++-- isort/profiles.py | 7 ++++++- isort/settings.py | 1 + tests/test_isort.py | 20 ++++++++++++++++++++ 5 files changed, 38 insertions(+), 3 deletions(-) diff --git a/isort/main.py b/isort/main.py index b9b866874..459e786ee 100644 --- a/isort/main.py +++ b/isort/main.py @@ -422,6 +422,13 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: action="store_true", help="Forces all from imports to appear on their own line", ) + parser.add_argument( + "--nsl", + "--single-line-exclusions", + help="One or more modules to exclude from the single line rule.", + dest="single_line_exclusions", + action="append", + ) parser.add_argument( "--sp", "--settings-path", diff --git a/isort/output.py b/isort/output.py index cd925c91c..27fbd3973 100644 --- a/isort/output.py +++ b/isort/output.py @@ -239,7 +239,9 @@ def _with_from_imports( import_start = f"from {module} {import_type} " from_imports = list(parsed.imports[section]["from"][module]) - if not config.no_inline_sort or config.force_single_line: + if not config.no_inline_sort or ( + config.force_single_line and module not in config.single_line_exclusions + ): from_imports = sorting.naturally( from_imports, key=lambda key: sorting.module_key( @@ -288,7 +290,7 @@ def _with_from_imports( config, ) from_imports = [] - elif config.force_single_line: + elif config.force_single_line and module not in config.single_line_exclusions: import_statement = "" while from_imports: from_import = from_imports.pop(0) diff --git a/isort/profiles.py b/isort/profiles.py index 1210c8ef0..910da9357 100644 --- a/isort/profiles.py +++ b/isort/profiles.py @@ -15,7 +15,12 @@ "line_length": 79, } pycharm = {"multi_line_output": 3, "force_grid_wrap": 2} -google = {"force_single_line": True, "force_sort_within_sections": True, "lexicographical": True} +google = { + "force_single_line": True, + "force_sort_within_sections": True, + "lexicographical": True, + "single_line_exclusions": ("typing",), +} open_stack = { "force_single_line": True, "force_sort_within_sections": True, diff --git a/isort/settings.py b/isort/settings.py index 04b488f1c..f51d78c87 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -142,6 +142,7 @@ class _Config: remove_imports: FrozenSet[str] = frozenset() reverse_relative: bool = False force_single_line: bool = False + single_line_exclusions: Tuple[str, ...] = () default_section: str = "FIRSTPARTY" import_headings: Dict[str, str] = field(default_factory=dict) balanced_wrapping: bool = False diff --git a/tests/test_isort.py b/tests/test_isort.py index 0228b414a..efff42c47 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -4702,3 +4702,23 @@ def test_noqa_issue_1065() -> None: from flask_security.signals import user_registered # noqa """ assert SortImports(file_contents=test_input).output == expected_output + + +def test_single_line_exclusions(): + test_input = """ +# start comment +from os import path, system +from typing import List, TypeVar +""" + expected_output = """ +# start comment +from os import path +from os import system +from typing import List, TypeVar +""" + assert ( + SortImports( + file_contents=test_input, force_single_line=True, single_line_exclusions=("typing",) + ).output + == expected_output + ) From b34705f87e38400e166ba83dd8ffd482bba260c1 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 7 Jan 2020 13:26:58 -0800 Subject: [PATCH 0314/1439] Fix formatting [isort + black] --- isort/finders.py | 12 +----------- isort/output.py | 1 + isort/settings.py | 11 +---------- isort/sorting.py | 2 +- 4 files changed, 4 insertions(+), 22 deletions(-) diff --git a/isort/finders.py b/isort/finders.py index 07219f074..259ddbb48 100644 --- a/isort/finders.py +++ b/isort/finders.py @@ -10,17 +10,7 @@ from fnmatch import fnmatch from functools import lru_cache from glob import glob -from typing import ( - Dict, - Iterable, - Iterator, - List, - Optional, - Pattern, - Sequence, - Tuple, - Type, -) +from typing import Dict, Iterable, Iterator, List, Optional, Pattern, Sequence, Tuple, Type from . import sections from .settings import Config diff --git a/isort/output.py b/isort/output.py index e9a6f8bf3..4343f71ae 100644 --- a/isort/output.py +++ b/isort/output.py @@ -2,6 +2,7 @@ import itertools from functools import partial from typing import Iterable, List, Tuple + from isort.format import format_simplified from . import parse, sorting, wrap diff --git a/isort/settings.py b/isort/settings.py index e05560dd6..8858b20cf 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -14,16 +14,7 @@ from distutils.util import strtobool as _as_bool from functools import lru_cache from pathlib import Path -from typing import ( - Any, - Callable, - Dict, - FrozenSet, - Iterable, - List, - Set, - Tuple, -) +from typing import Any, Callable, Dict, FrozenSet, Iterable, List, Set, Tuple from warnings import warn from . import stdlibs diff --git a/isort/sorting.py b/isort/sorting.py index abd6bdbd7..34f88699c 100644 --- a/isort/sorting.py +++ b/isort/sorting.py @@ -1,5 +1,5 @@ import re -from typing import Any, Callable, Iterable, List, Optional +from typing import Any, Callable, Iterable, List, Optional from .settings import Config From 6de72b4106d720e451dfafcc92a757e3d3d8c1e7 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 7 Jan 2020 14:29:50 -0800 Subject: [PATCH 0315/1439] Provide a way to introspect configuration discovery from the command line --- isort/main.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/isort/main.py b/isort/main.py index 0bbf3353f..aebd9e33e 100644 --- a/isort/main.py +++ b/isort/main.py @@ -6,6 +6,7 @@ import re import stat import sys +from pprint import pprint from pathlib import Path from typing import Any, Dict, Iterable, Iterator, List, Optional, Sequence from warnings import warn @@ -552,6 +553,11 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: action="store_true", help="Tells isort to apply changes interactively.", ) + parser.add_argument( + "--show-config", + dest="show_config", + action="store_true", + help="See isort's determined config, as well as sources of config options.") arguments = {key: value for key, value in vars(parser.parse_args(argv)).items() if value} if "dont_order_by_type" in arguments: @@ -571,6 +577,8 @@ def main(argv: Optional[Sequence[str]] = None) -> None: print(ASCII_ART) return + show_config: bool = arguments.pop("show_config", False) + if "settings_path" in arguments: sp = arguments["settings_path"] arguments["settings_path"] = ( @@ -586,14 +594,14 @@ def main(argv: Optional[Sequence[str]] = None) -> None: warn(f"virtual_env dir does not exist: {arguments['virtual_env']}") file_names = arguments.pop("files", []) - if not file_names: + if not file_names and not show_config: print(QUICK_GUIDE) return - elif file_names == ["-"]: + elif file_names == ["-"] and not show_config: SortImports(file_contents=sys.stdin.read(), write_to_stdout=True, **arguments) else: if "settings_path" not in arguments: - arguments["settings_path"] = os.path.abspath(file_names[0]) or os.getcwd() + arguments["settings_path"] = os.path.abspath(file_names[0] if file_names else '.') or os.getcwd() if not os.path.isdir(arguments["settings_path"]): arguments["settings_path"] = os.path.dirname(arguments["settings_path"]) @@ -605,6 +613,9 @@ def main(argv: Optional[Sequence[str]] = None) -> None: check = config_dict.pop("check", False) show_diff = config_dict.pop("show_diff", False) config = Config(**config_dict) + if show_config: + pprint(config.__dict__) + return wrong_sorted_files = False skipped: List[str] = [] From c528c36078ebf2714d66f39d44cc47ce4e0b1488 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 7 Jan 2020 14:30:58 -0800 Subject: [PATCH 0316/1439] Formatting --- isort/main.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/isort/main.py b/isort/main.py index aebd9e33e..27d01cc1a 100644 --- a/isort/main.py +++ b/isort/main.py @@ -6,8 +6,8 @@ import re import stat import sys -from pprint import pprint from pathlib import Path +from pprint import pprint from typing import Any, Dict, Iterable, Iterator, List, Optional, Sequence from warnings import warn @@ -557,7 +557,8 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: "--show-config", dest="show_config", action="store_true", - help="See isort's determined config, as well as sources of config options.") + help="See isort's determined config, as well as sources of config options.", + ) arguments = {key: value for key, value in vars(parser.parse_args(argv)).items() if value} if "dont_order_by_type" in arguments: @@ -601,7 +602,9 @@ def main(argv: Optional[Sequence[str]] = None) -> None: SortImports(file_contents=sys.stdin.read(), write_to_stdout=True, **arguments) else: if "settings_path" not in arguments: - arguments["settings_path"] = os.path.abspath(file_names[0] if file_names else '.') or os.getcwd() + arguments["settings_path"] = ( + os.path.abspath(file_names[0] if file_names else ".") or os.getcwd() + ) if not os.path.isdir(arguments["settings_path"]): arguments["settings_path"] = os.path.dirname(arguments["settings_path"]) From 3e77dc6cd83498ed546aa8958fa08732eda66b4a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 8 Jan 2020 06:53:20 -0800 Subject: [PATCH 0317/1439] Fix a variety of nested import bugs --- isort/api.py | 26 +++++++++++++++++------- tests/test_isort.py | 49 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 7 deletions(-) diff --git a/isort/api.py b/isort/api.py index 0ea7187f5..1148035d8 100644 --- a/isort/api.py +++ b/isort/api.py @@ -149,6 +149,7 @@ def sort_imports( add_imports: List[str] = [format_natural(addition) for addition in config.add_imports] import_section: str = "" next_import_section: str = "" + next_cimports: bool = False in_quote: str = "" first_comment_index_start: int = -1 first_comment_index_end: int = -1 @@ -221,12 +222,16 @@ def sort_imports( isort_off = True elif stripped_line == "# isort: split": not_imports = True - elif not stripped_line or stripped_line.startswith("#"): + elif ( + not stripped_line + or stripped_line.startswith("#") + and (not indent or indent + line.lstrip() == line) + ): import_section += line elif stripped_line.startswith(IMPORT_START_IDENTIFIERS): contains_imports = True - indent = line[: -len(line.lstrip())] + new_indent = line[: -len(line.lstrip())] import_statement = line while stripped_line.endswith("\\") or ( "(" in stripped_line and ")" not in stripped_line @@ -252,8 +257,9 @@ def sort_imports( ): cimport_statement = True - if cimport_statement != cimports: + if cimport_statement != cimports or (new_indent != indent and import_section): if import_section: + next_cimports = cimport_statement next_import_section = import_statement import_statement = "" not_imports = True @@ -261,6 +267,7 @@ def sort_imports( else: cimports = cimport_statement + indent = new_indent import_section += import_statement else: not_imports = True @@ -292,6 +299,8 @@ def sort_imports( if not contains_imports: output_stream.write(import_section) else: + leading_whitespace = import_section[: -len(import_section.lstrip())] + trailing_whitespace = import_section[len(import_section.rstrip()) :] if first_import_section and not import_section.lstrip( line_separator ).startswith(COMMENT_INDICATORS): @@ -310,19 +319,22 @@ def sort_imports( ) if indent: sorted_import_section = ( - textwrap.indent(sorted_import_section, indent) + line_separator + leading_whitespace + + textwrap.indent(sorted_import_section, indent).strip() + + trailing_whitespace ) output_stream.write(sorted_import_section) - if not line and next_import_section: + if not line and not indent and next_import_section: output_stream.write(line_separator) if indent: output_stream.write(line) - indent = "" + if not next_import_section: + indent = "" if next_import_section: - cimports = not cimports + cimports = next_cimports contains_imports = True else: contains_imports = False diff --git a/tests/test_isort.py b/tests/test_isort.py index efff42c47..a0f5dfc03 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -4722,3 +4722,52 @@ def test_single_line_exclusions(): ).output == expected_output ) + + +def test_nested_comment_handling(): + test_input = """ +if True: + import foo + +# comment for bar +""" + assert SortImports(file_contents=test_input).output == test_input + + # If comments appear inside import sections at same indentation they can be re-arranged. + test_input = """ +if True: + import sys + + # os import + import os +""" + expected_output = """ +if True: + # os import + import os + import sys +""" + assert SortImports(file_contents=test_input).output == expected_output + + # Comments shouldn't be unexpectedly rearranged. See issue #1090. + test_input = """ +def f(): + # comment 1 + # comment 2 + + # comment 3 + # comment 4 + from a import a + from b import b + +""" + assert SortImports(file_contents=test_input).output == test_input + + # Whitespace shouldn't be adjusted for nested imports. See issue #1090. + test_input = """ +try: + import foo + except ImportError: + import bar +""" + assert SortImports(file_contents=test_input).output == test_input From 1662754ac4a5d3bf3ec1059810e828da89ea788a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 10 Jan 2020 08:13:56 -0800 Subject: [PATCH 0318/1439] Fix issue #1095 --- isort/main.py | 65 ++++-------------------------------- isort/setuptools_commands.py | 65 ++++++++++++++++++++++++++++++++++++ tests/test_main.py | 5 +++ 3 files changed, 76 insertions(+), 59 deletions(-) create mode 100644 isort/setuptools_commands.py diff --git a/isort/main.py b/isort/main.py index 27d01cc1a..ca94c4ac8 100644 --- a/isort/main.py +++ b/isort/main.py @@ -1,7 +1,6 @@ """Tool for sorting imports alphabetically, and automatically separated into sections.""" import argparse import functools -import glob import os import re import stat @@ -11,12 +10,15 @@ from typing import Any, Dict, Iterable, Iterator, List, Optional, Sequence from warnings import warn -import setuptools - from . import SortImports, __version__, sections from .logo import ASCII_ART from .profiles import profiles -from .settings import DEFAULT_CONFIG, SUPPORTED_EXTENSIONS, VALID_PY_TARGETS, Config, WrapModes +from .settings import SUPPORTED_EXTENSIONS, VALID_PY_TARGETS, Config, WrapModes + +try: + from .setuptools_commands import ISortCommand +except ImportError: + pass shebang_re = re.compile(br"^#!.*\bpython[23w]?\b") QUICK_GUIDE = f""" @@ -97,61 +99,6 @@ def iter_source_code(paths: Iterable[str], config: Config, skipped: List[str]) - yield path -class ISortCommand(setuptools.Command): - """The :class:`ISortCommand` class is used by setuptools to perform - imports checks on registered modules. - """ - - description = "Run isort on modules registered in setuptools" - user_options: List[Any] = [] - - def initialize_options(self) -> None: - default_settings = vars(DEFAULT_CONFIG).copy() - for key, value in default_settings.items(): - setattr(self, key, value) - - def finalize_options(self) -> None: - "Get options from config files." - self.arguments: Dict[str, Any] = {} - computed_settings = vars(Config(directory=os.getcwd())) - for key, value in computed_settings.items(): - self.arguments[key] = value - - def distribution_files(self) -> Iterator[str]: - """Find distribution packages.""" - # This is verbatim from flake8 - if self.distribution.packages: - package_dirs = self.distribution.package_dir or {} - for package in self.distribution.packages: - pkg_dir = package - if package in package_dirs: - pkg_dir = package_dirs[package] - elif "" in package_dirs: - pkg_dir = package_dirs[""] + os.path.sep + pkg_dir - yield pkg_dir.replace(".", os.path.sep) - - if self.distribution.py_modules: - for filename in self.distribution.py_modules: - yield "%s.py" % filename - # Don't miss the setup.py file itself - yield "setup.py" - - def run(self) -> None: - arguments = self.arguments - wrong_sorted_files = False - arguments["check"] = True - for path in self.distribution_files(): - for python_file in glob.iglob(os.path.join(path, "*.py")): - try: - incorrectly_sorted = SortImports(python_file, **arguments).incorrectly_sorted - if incorrectly_sorted: - wrong_sorted_files = True - except OSError as error: - warn(f"Unable to parse file {python_file} due to {error}") - if wrong_sorted_files: - sys.exit(1) - - def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: parser = argparse.ArgumentParser( description="Sort Python import definitions alphabetically " diff --git a/isort/setuptools_commands.py b/isort/setuptools_commands.py new file mode 100644 index 000000000..8da5c7f64 --- /dev/null +++ b/isort/setuptools_commands.py @@ -0,0 +1,65 @@ +import glob +import os +import sys +from typing import Any, Dict, Iterator, List +from warnings import warn + +import setuptools + +from . import SortImports +from .settings import DEFAULT_CONFIG, Config + + +class ISortCommand(setuptools.Command): + """The :class:`ISortCommand` class is used by setuptools to perform + imports checks on registered modules. + """ + + description = "Run isort on modules registered in setuptools" + user_options: List[Any] = [] + + def initialize_options(self) -> None: + default_settings = vars(DEFAULT_CONFIG).copy() + for key, value in default_settings.items(): + setattr(self, key, value) + + def finalize_options(self) -> None: + "Get options from config files." + self.arguments: Dict[str, Any] = {} + computed_settings = vars(Config(directory=os.getcwd())) + for key, value in computed_settings.items(): + self.arguments[key] = value + + def distribution_files(self) -> Iterator[str]: + """Find distribution packages.""" + # This is verbatim from flake8 + if self.distribution.packages: + package_dirs = self.distribution.package_dir or {} + for package in self.distribution.packages: + pkg_dir = package + if package in package_dirs: + pkg_dir = package_dirs[package] + elif "" in package_dirs: + pkg_dir = package_dirs[""] + os.path.sep + pkg_dir + yield pkg_dir.replace(".", os.path.sep) + + if self.distribution.py_modules: + for filename in self.distribution.py_modules: + yield "%s.py" % filename + # Don't miss the setup.py file itself + yield "setup.py" + + def run(self) -> None: + arguments = self.arguments + wrong_sorted_files = False + arguments["check"] = True + for path in self.distribution_files(): + for python_file in glob.iglob(os.path.join(path, "*.py")): + try: + incorrectly_sorted = SortImports(python_file, **arguments).incorrectly_sorted + if incorrectly_sorted: + wrong_sorted_files = True + except OSError as error: + warn(f"Unable to parse file {python_file} due to {error}") + if wrong_sorted_files: + sys.exit(1) diff --git a/tests/test_main.py b/tests/test_main.py index 300108a08..f8447a378 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -19,3 +19,8 @@ def test_is_python_file_fifo(tmpdir): fifo_file = os.path.join(tmpdir, "fifo_file") os.mkfifo(fifo_file) assert not main.is_python_file(fifo_file) + + +def test_isort_command(): + """Ensure ISortCommand got registered, otherwise setuptools error must have occured""" + assert main.ISortCommand From e0f41bb66c789171f5ab9f1811050cbd86e62838 Mon Sep 17 00:00:00 2001 From: Joao MC Teixeira Date: Sat, 11 Jan 2020 21:25:52 -0500 Subject: [PATCH 0319/1439] implements functionality --- isort/wrap_modes.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/isort/wrap_modes.py b/isort/wrap_modes.py index 9b0a74b85..c169dc0f4 100644 --- a/isort/wrap_modes.py +++ b/isort/wrap_modes.py @@ -273,6 +273,14 @@ def noqa(**interface): return f"{retval}{interface['comment_prefix']} NOQA" +@_wrap_mode +def vertical_hanging_indent_bracket(**interface): + if not interface["imports"]: + return "" + statement = vertical_hanging_indent(**interface) + return f'{statement[:-1]}{interface["indent"]})' + + WrapModes = enum.Enum( # type: ignore "WrapModes", {wrap_mode: index for index, wrap_mode in enumerate(_wrap_modes.keys())} ) From 500bafabbd51a6005c11a00c4738a2438990e48a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 12 Jan 2020 20:36:30 -0800 Subject: [PATCH 0320/1439] =?UTF-8?q?Add=20Jo=C3=A3o=20M.C.=20Teixeira=20(?= =?UTF-8?q?@joaomcteixeira)=20to=20acknowledgements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/contributing/4.-acknowledgements.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index d18b70363..095aa7752 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -90,7 +90,7 @@ Code Contributors - Pedro Algarvio (@s0undt3ch) - Chris St. Pierre (@stpierre) - Sebastian Rittau (@srittau) - +- João M.C. Teixeira (@joaomcteixeira) Documenters =================== From be0ae7b5c7d92e019ab514880f26a7a88fe7eb65 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 13 Jan 2020 21:44:22 -0800 Subject: [PATCH 0321/1439] Add test case for issue 1091: comments at top of file --- tests/test_isort.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_isort.py b/tests/test_isort.py index a0f5dfc03..2580fae70 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -4771,3 +4771,15 @@ def f(): import bar """ assert SortImports(file_contents=test_input).output == test_input + + +def test_comments_top_of_file(): + """Test to ensure comments at top of file are correctly handled. See issue #1091.""" + test_input = """# comment 1 + +# comment 2 +# comment 3 +# comment 4 +from foo import * +""" + assert SortImports(file_contents=test_input).output == test_input From 2a5d7ec54a07595ded44aad3e7c5c70accdd0655 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 13 Jan 2020 22:50:12 -0800 Subject: [PATCH 0322/1439] Fix above comments for from imports --- isort/output.py | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/isort/output.py b/isort/output.py index 4343f71ae..f7a4b2f7a 100644 --- a/isort/output.py +++ b/isort/output.py @@ -333,18 +333,18 @@ def _with_from_imports( ) comments = None else: + above_comments = parsed.categorized_comments["above"]["from"].pop(module, None) + if above_comments: + if new_section_output and config.ensure_newline_before_comments: + new_section_output.append("") + new_section_output.extend(above_comments) + while from_imports and from_imports[0] in as_imports: from_import = from_imports.pop(0) as_imports[from_import] = sorting.naturally(as_imports[from_import]) from_comments = parsed.categorized_comments["straight"].get( f"{module}.{from_import}" ) - above_comments = parsed.categorized_comments["above"]["from"].pop(module, None) - if above_comments: - if new_section_output and config.ensure_newline_before_comments: - new_section_output.append("") - new_section_output.extend(above_comments) - if ( config.keep_direct_and_as_imports and parsed.imports[section]["from"][module][from_import] @@ -399,13 +399,6 @@ def _with_from_imports( single_import_line += ( f"{comments and ';' or config.comment_prefix} " f"{comment}" ) - above_comments = parsed.categorized_comments["above"]["from"].pop( - module, None - ) - if above_comments: - if new_section_output and config.ensure_newline_before_comments: - new_section_output.append("") - new_section_output.extend(above_comments) new_section_output.append( wrap.line(single_import_line, parsed.line_separator, config) ) From 63067802cedb570b50008517c688825bb1e27d56 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 14 Jan 2020 23:20:58 -0800 Subject: [PATCH 0323/1439] Fix issue 751 --- isort/api.py | 2 +- tests/test_isort.py | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/isort/api.py b/isort/api.py index 1148035d8..41a919b6b 100644 --- a/isort/api.py +++ b/isort/api.py @@ -176,7 +176,7 @@ def sort_imports( stripped_line = line.strip() if ( - (index == 0 or (index == 1 and not contains_imports)) + (index == 0 or (index in (1, 2) and not contains_imports)) and stripped_line.startswith("#") and stripped_line not in section_comments ): diff --git a/tests/test_isort.py b/tests/test_isort.py index 2580fae70..66398ecac 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -4781,5 +4781,28 @@ def test_comments_top_of_file(): # comment 3 # comment 4 from foo import * +""" + assert SortImports(file_contents=test_input).output == test_input + + test_input = """# -*- coding: utf-8 -*- + +# Define your item pipelines here +# +# Don't forget to add your pipeline to the ITEM_PIPELINES setting +# See: https://doc.scrapy.org/en/latest/topics/item-pipeline.html +from datetime import datetime + +from .items import WeiboMblogItem + + +class WeiboMblogPipeline(object): + def process_item(self, item, spider): + if isinstance(item, WeiboMblogItem): + item = self._process_item(item, spider) + return item + + def _process_item(self, item, spider): + item['inserted_at'] = datetime.now() + return item """ assert SortImports(file_contents=test_input).output == test_input From 6cc047e13719e4cf70c4f65711817acacb96c0fb Mon Sep 17 00:00:00 2001 From: Stephen Brown II Date: Wed, 15 Jan 2020 16:11:11 -0700 Subject: [PATCH 0324/1439] Add --ensure-newline-before-comments argument --- isort/main.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/isort/main.py b/isort/main.py index ca94c4ac8..de3dfda32 100644 --- a/isort/main.py +++ b/isort/main.py @@ -277,6 +277,13 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: help="Multi line output (0-grid, 1-vertical, 2-hanging, 3-vert-hanging, 4-vert-grid, " "5-vert-grid-grouped, 6-vert-grid-grouped-no-comma).", ) + parser.add_argument( + "-n", + "--ensure-newline-before-comments", + dest="ensure_newline_before_comments", + action="store_true", + help="Inserts a blank line before a comment following an import.", + ) inline_args_group.add_argument( "--nis", "--no-inline-sort", From c80c384b8af51e01912f8bfe31228c8b7a126e5b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 15 Jan 2020 22:46:38 -0800 Subject: [PATCH 0325/1439] Add a test case for 1037 --- tests/test_isort.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_isort.py b/tests/test_isort.py index 66398ecac..8cabcce2b 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -4806,3 +4806,15 @@ def _process_item(self, item, spider): return item """ assert SortImports(file_contents=test_input).output == test_input + + +def test_multiple_aliases(): + """Test to ensure isort will retain multiple aliases. See issue #1037""" + test_input = """import datetime +import datetime as datetime +import datetime as dt +import datetime as dt2 +""" + assert ( + SortImports(keep_direct_and_as_imports=True, file_contents=test_input).output == test_input + ) From 31a31d0ec43ee083e3f5ad5a4cfcfea8e0b93616 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 16 Jan 2020 22:41:59 -0800 Subject: [PATCH 0326/1439] Fix issue #1103 --- isort/api.py | 1 + isort/parse.py | 4 ++-- tests/test_isort.py | 10 ++++++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/isort/api.py b/isort/api.py index 41a919b6b..3d34d835c 100644 --- a/isort/api.py +++ b/isort/api.py @@ -233,6 +233,7 @@ def sort_imports( new_indent = line[: -len(line.lstrip())] import_statement = line + stripped_line = line.strip().split("#")[0] while stripped_line.endswith("\\") or ( "(" in stripped_line and ")" not in stripped_line ): diff --git a/isort/parse.py b/isort/parse.py index 09e381beb..d007d43d0 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -211,7 +211,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte nested_comments[line_parts[-1]] = comments[0] if "(" in line.split("#")[0] and index < line_count: - while not line.strip().endswith(")") and index < line_count: + while not line.split("#")[0].strip().endswith(")") and index < line_count: line, new_comment = parse_comments(in_lines[index]) index += 1 if new_comment: @@ -248,7 +248,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte nested_comments[stripped_line] = comments[-1] import_string += line_separator + line - while not line.strip().endswith(")") and index < line_count: + while not line.split("#")[0].strip().endswith(")") and index < line_count: line, new_comment = parse_comments(in_lines[index]) index += 1 if new_comment: diff --git a/tests/test_isort.py b/tests/test_isort.py index 8cabcce2b..e4c61c3d1 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -4818,3 +4818,13 @@ def test_multiple_aliases(): assert ( SortImports(keep_direct_and_as_imports=True, file_contents=test_input).output == test_input ) + + +def test_parens_in_comment(): + """Test to ensure isort can handle parens placed in comments. See issue #1103""" + test_input = """from foo import ( # (some text in brackets) + bar, +) +""" + expected_output = "from foo import bar # (some text in brackets)\n" + assert SortImports(file_contents=test_input).output == expected_output From 5dc291e5aca527882e8d6bd1a702cd4235084aa7 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 17 Jan 2020 21:37:21 -0800 Subject: [PATCH 0327/1439] Add test case for issue #908 --- tests/test_isort.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_isort.py b/tests/test_isort.py index e4c61c3d1..a6034a97c 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -4828,3 +4828,13 @@ def test_parens_in_comment(): """ expected_output = "from foo import bar # (some text in brackets)\n" assert SortImports(file_contents=test_input).output == expected_output + + +def test_as_imports_mixed(): + """Test to ensure as imports can be mixed with non as. See issue #908""" + test_input = """from datetime import datetime +import datetime.datetime as dt +""" + assert ( + SortImports(file_contents=test_input, keep_direct_and_as_imports=True).output == test_input + ) From 1bc156278af3360dd45978298ba1a3c59e4e32d6 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 18 Jan 2020 18:57:16 -0800 Subject: [PATCH 0328/1439] Update changelog to mention as format consitency improvement --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b78e1658..c47931d20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Changelog - isort now does nothing, beyond giving instructions and exiting status code 0, when ran with no arguments. - a new `--interactive` flag has been added to enable the old style behaviour. - isort now works on contiguous sections of imports, instead of one whole file at a time. + - isort now formats all nested "as" imports in the "from" form. `import x.y as a` becomes `from x import y as a`. Internal: - isort now utilizes mypy and typing to filter out typing related issues before deployment. From 2d164578386daaf5940a90994622142d56f4081f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 18 Jan 2020 18:57:44 -0800 Subject: [PATCH 0329/1439] Add test cases, and update test cases for dsired constency improvement --- tests/test_isort.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/tests/test_isort.py b/tests/test_isort.py index a6034a97c..f9873e363 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -1695,14 +1695,14 @@ def test_placement_control() -> None: assert test_output == ( "import os\n" - "import p24.imports._argparse as argparse\n" - "import p24.imports._subprocess as subprocess\n" "import sys\n" + "from p24.imports import _VERSION as VERSION\n" + "from p24.imports import _argparse as argparse\n" + "from p24.imports import _subprocess as subprocess\n" "\n" "from bottle import Bottle, redirect, response, run\n" "\n" - "import p24.imports._VERSION as VERSION\n" - "import p24.shared.media_wiki_syntax as syntax\n" + "from p24.shared import media_wiki_syntax as syntax\n" ) @@ -1747,9 +1747,10 @@ def test_custom_sections() -> None: assert test_output == ( "# Standard Library\n" "import os\n" - "import p24.imports._argparse as argparse\n" - "import p24.imports._subprocess as subprocess\n" "import sys\n" + "from p24.imports import _VERSION as VERSION\n" + "from p24.imports import _argparse as argparse\n" + "from p24.imports import _subprocess as subprocess\n" "\n" "# Django\n" "from django.conf import settings\n" @@ -1763,8 +1764,7 @@ def test_custom_sections() -> None: "import pandas as pd\n" "\n" "# First Party\n" - "import p24.imports._VERSION as VERSION\n" - "import p24.shared.media_wiki_syntax as syntax\n" + "from p24.shared import media_wiki_syntax as syntax\n" ) @@ -3987,7 +3987,7 @@ def test_import_heading_issue_905() -> None: } # type: Dict[str, Any] test_input = ( "# Standard library imports\n" - "import os.path as osp\n" + "from os import path as osp\n" "\n" "# Third party imports\n" "import numpy as np\n" @@ -4073,10 +4073,9 @@ def test_isort_ensures_blank_line_between_import_and_comment() -> None: "import one.b\n" "\n" "# noinspection PyUnresolvedReferences\n" - "import two.a as aa\n" - "\n" "# noinspection PyUnresolvedReferences\n" - "import two.b as bb\n" + "from two import a as aa\n" + "from two import b as bb\n" "\n" "# noinspection PyUnresolvedReferences\n" "from three.a import a\n" @@ -4096,7 +4095,7 @@ def test_isort_ensures_blank_line_between_import_and_comment() -> None: def test_moving_comments_issue_726(): config = {"force_sort_within_sections": 1} # type: Dict[str, Any] test_input = ( - "import Blue.models as BlueModels\n" + "from Blue import models as BlueModels\n" "# comment for PlaidModel\n" "from Plaid.models import PlaidModel\n" ) @@ -4104,7 +4103,7 @@ def test_moving_comments_issue_726(): test_input = ( "# comment for BlueModels\n" - "import Blue.models as BlueModels\n" + "from Blue import models as BlueModels\n" "# comment for PlaidModel\n" "# another comment for PlaidModel\n" "from Plaid.models import PlaidModel\n" @@ -4835,6 +4834,7 @@ def test_as_imports_mixed(): test_input = """from datetime import datetime import datetime.datetime as dt """ - assert ( - SortImports(file_contents=test_input, keep_direct_and_as_imports=True).output == test_input - ) + expected_output = """from datetime import datetime +from datetime import datetime as dt +""" + assert SortImports(file_contents=test_input, keep_direct_and_as_imports=True).output == expected_output From 3e4cc2d515fb7bf7d2e088e4ec613e7ec512d4d6 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 18 Jan 2020 18:58:16 -0800 Subject: [PATCH 0330/1439] Update parse to treat as imports consitently --- isort/parse.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/isort/parse.py b/isort/parse.py index d007d43d0..8d8f56df3 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -305,10 +305,18 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte as_index = just_imports.index("as") if type_of_import == "from": module = just_imports[0] + "." + just_imports[as_index - 1] - as_map[module].append(just_imports[as_index + 1]) + as_name = just_imports[as_index + 1] + if as_name not in as_map[module]: + as_map[module].append(as_name) else: module = just_imports[as_index - 1] - as_map[module].append(just_imports[as_index + 1]) + as_name = just_imports[as_index + 1] + if as_name not in as_map[module]: + as_map[module].append(as_name) + if "." in module: + type_of_import = "from" + just_imports[:as_index] = module.rsplit(".", 1) + as_index = just_imports.index("as") if not config.combine_as_imports: categorized_comments["straight"][module] = comments comments = [] From 348064423aadc106c0fe2faf07cfaa9a1da35ca8 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 18 Jan 2020 19:05:15 -0800 Subject: [PATCH 0331/1439] Formatting --- tests/test_isort.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_isort.py b/tests/test_isort.py index f9873e363..19c2b082f 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -4837,4 +4837,7 @@ def test_as_imports_mixed(): expected_output = """from datetime import datetime from datetime import datetime as dt """ - assert SortImports(file_contents=test_input, keep_direct_and_as_imports=True).output == expected_output + assert ( + SortImports(file_contents=test_input, keep_direct_and_as_imports=True).output + == expected_output + ) From dd46fa5b5a702e78c880955491c91e557ed1904f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 18 Jan 2020 22:57:45 -0800 Subject: [PATCH 0332/1439] Change keep_direct_and_as_imports default to True --- CHANGELOG.md | 1 + isort/settings.py | 2 +- tests/test_isort.py | 25 +++++++++++++++++-------- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c47931d20..64852aaa6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Changelog - a new `--interactive` flag has been added to enable the old style behaviour. - isort now works on contiguous sections of imports, instead of one whole file at a time. - isort now formats all nested "as" imports in the "from" form. `import x.y as a` becomes `from x import y as a`. + - `keep_direct_and_as_imports` option now defaults to `True`. Internal: - isort now utilizes mypy and typing to filter out typing related issues before deployment. diff --git a/isort/settings.py b/isort/settings.py index 8858b20cf..a292e598b 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -138,7 +138,7 @@ class _Config: lines_between_types: int = 0 combine_as_imports: bool = False combine_star: bool = False - keep_direct_and_as_imports: bool = False + keep_direct_and_as_imports: bool = True include_trailing_comma: bool = False from_first: bool = False verbose: bool = False diff --git a/tests/test_isort.py b/tests/test_isort.py index 19c2b082f..aef14e328 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -3349,9 +3349,11 @@ def test_multiple_as_imports() -> None: "from a import b as bb\n" "from a import b as bb_\n" ) - test_output = SortImports(file_contents=test_input).output + test_output = SortImports(file_contents=test_input, keep_direct_and_as_imports=False).output assert test_output == "from a import b as b\nfrom a import b as bb\nfrom a import b as bb_\n" - test_output = SortImports(file_contents=test_input, combine_as_imports=True).output + test_output = SortImports( + file_contents=test_input, combine_as_imports=True, keep_direct_and_as_imports=False + ).output assert test_output == "from a import b as b, b as bb, b as bb_\n" test_output = SortImports(file_contents=test_input, keep_direct_and_as_imports=True).output assert test_output == test_input @@ -3366,16 +3368,20 @@ def test_multiple_as_imports() -> None: "from a import b\n" "from a import b as f\n" ) - test_output = SortImports(file_contents=test_input).output + test_output = SortImports(file_contents=test_input, keep_direct_and_as_imports=False).output assert test_output == "from a import b as c\nfrom a import b as e\nfrom a import b as f\n" - test_output = SortImports(file_contents=test_input, combine_as_imports=True).output + test_output = SortImports( + file_contents=test_input, combine_as_imports=True, keep_direct_and_as_imports=False + ).output assert test_output == "from a import b as c, b as e, b as f\n" test_output = SortImports(file_contents=test_input, keep_direct_and_as_imports=True).output assert ( test_output == "from a import b\nfrom a import b as c\nfrom a import b as e\nfrom a import b as f\n" ) - test_output = SortImports(file_contents=test_input, no_inline_sort=True).output + test_output = SortImports( + file_contents=test_input, no_inline_sort=True, keep_direct_and_as_imports=False + ).output assert test_output == "from a import b as c\nfrom a import b as e\nfrom a import b as f\n" test_output = SortImports( file_contents=test_input, keep_direct_and_as_imports=True, no_inline_sort=True @@ -3389,7 +3395,10 @@ def test_multiple_as_imports() -> None: ).output assert test_output == "from a import b, b as c, b as e, b as f\n" test_output = SortImports( - file_contents=test_input, combine_as_imports=True, no_inline_sort=True + file_contents=test_input, + combine_as_imports=True, + no_inline_sort=True, + keep_direct_and_as_imports=False, ).output assert test_output == "from a import b as e, b as c, b as f\n" test_output = SortImports( @@ -3401,7 +3410,7 @@ def test_multiple_as_imports() -> None: assert test_output == "from a import b, b as e, b as c, b as f\n" test_input = "import a as a\nimport a as aa\nimport a as aa_\n" - test_output = SortImports(file_contents=test_input).output + test_output = SortImports(file_contents=test_input, keep_direct_and_as_imports=False).output assert test_output == test_input test_output = SortImports( file_contents=test_input, combine_as_imports=True, keep_direct_and_as_imports=True @@ -3409,7 +3418,7 @@ def test_multiple_as_imports() -> None: assert test_output == test_input test_input = "import a\nimport a as a\nimport a as aa\nimport a as aa_\n" - test_output = SortImports(file_contents=test_input).output + test_output = SortImports(file_contents=test_input, keep_direct_and_as_imports=False).output assert test_output == "import a as a\nimport a as aa\nimport a as aa_\n" test_output = SortImports( file_contents=test_input, combine_as_imports=True, keep_direct_and_as_imports=True From b3412e19e1b50f6ff4a9feb9758e388154cc8293 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 19 Jan 2020 20:57:59 -0800 Subject: [PATCH 0333/1439] Add test to ensure future imports are forced to top, issue #807 --- tests/test_isort.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_isort.py b/tests/test_isort.py index aef14e328..107a3f62d 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -4850,3 +4850,15 @@ def test_as_imports_mixed(): SortImports(file_contents=test_input, keep_direct_and_as_imports=True).output == expected_output ) + + +def test_no_sections_with_future(): + """Test to ensure no_sections works with future. See issue #807""" + test_input = """from __future__ import print_function +import os + """ + expected_output = """from __future__ import print_function + +import os +""" + assert SortImports(file_contents=test_input, no_sections=True).output == expected_output From 6a94662734c5e9df90ecd379f56cc021f858b57e Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 19 Jan 2020 20:58:41 -0800 Subject: [PATCH 0334/1439] Fix issue #807: no sections with future --- isort/output.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/isort/output.py b/isort/output.py index f7a4b2f7a..035224c94 100644 --- a/isort/output.py +++ b/isort/output.py @@ -34,12 +34,16 @@ def sorted_imports( if config.no_sections: parsed.imports["no_sections"] = {"straight": [], "from": {}} + base_sections: Tuple[str, ...] = () for section in sections: + if section == "FUTURE": + base_sections = ("FUTURE",) + continue parsed.imports["no_sections"]["straight"].extend( parsed.imports[section].get("straight", []) ) parsed.imports["no_sections"]["from"].update(parsed.imports[section].get("from", {})) - sections = ("no_sections",) + sections = base_sections + ("no_sections",) output: List[str] = [] pending_lines_before = False From cfdec4d12d569798b7cd1a79bf5fbf695fb20650 Mon Sep 17 00:00:00 2001 From: Teg Khanna Date: Mon, 20 Jan 2020 15:16:54 +0530 Subject: [PATCH 0335/1439] Adds wrap method for prefix 'from module import' --- isort/wrap_modes.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/isort/wrap_modes.py b/isort/wrap_modes.py index c169dc0f4..5b0d9b39f 100644 --- a/isort/wrap_modes.py +++ b/isort/wrap_modes.py @@ -281,6 +281,38 @@ def vertical_hanging_indent_bracket(**interface): return f'{statement[:-1]}{interface["indent"]})' +@_wrap_mode +def veritcal_prefix_from_module_import(**interface): + if not interface["imports"]: + return "" + prefix_statement = interface["statement"] + interface["statement"] += interface["imports"].pop(0) + while interface["imports"]: + next_import = interface["imports"].pop(0) + next_statement = comments.add_to_line( + interface["comments"], + interface["statement"] + ", " + next_import, + removed=interface["remove_comments"], + comment_prefix=interface["comment_prefix"], + ) + if ( + len(next_statement.split(interface["line_separator"])[-1]) + 1 + > interface["line_length"] + ): + next_statement = ( + comments.add_to_line( + interface["comments"], + f"{interface['statement']}", + removed=interface["remove_comments"], + comment_prefix=interface["comment_prefix"], + ) + + f"{interface['line_separator']}{prefix_statement}{next_import}" + ) + interface["comments"] = [] + interface["statement"] = next_statement + return interface["statement"] + + WrapModes = enum.Enum( # type: ignore "WrapModes", {wrap_mode: index for index, wrap_mode in enumerate(_wrap_modes.keys())} ) From 93fa36196c396dda01a2fc647ccbec15e98cea82 Mon Sep 17 00:00:00 2001 From: Teg Khanna Date: Tue, 21 Jan 2020 01:31:20 +0530 Subject: [PATCH 0336/1439] fixed spelling. and added testcases --- isort/wrap_modes.py | 2 +- tests/test_isort.py | 39 +++++++++++++++++++++++++++++++++++++++ tests/test_wrap_modes.py | 1 + 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/isort/wrap_modes.py b/isort/wrap_modes.py index 5b0d9b39f..64a596d4e 100644 --- a/isort/wrap_modes.py +++ b/isort/wrap_modes.py @@ -282,7 +282,7 @@ def vertical_hanging_indent_bracket(**interface): @_wrap_mode -def veritcal_prefix_from_module_import(**interface): +def vertical_prefix_from_module_import(**interface): if not interface["imports"]: return "" prefix_statement = interface["statement"] diff --git a/tests/test_isort.py b/tests/test_isort.py index 107a3f62d..f993c65dc 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -455,6 +455,45 @@ def test_output_modes() -> None: "from third_party import (\n lib1, lib2, lib3, lib4, lib5, lib5ab\n)\n" ) + test_output_prefix_from_module = SortImports( + file_contents=REALLY_LONG_IMPORT, + multi_line_output=WrapModes.VERTICAL_PREFIX_FROM_MODULE_IMPORT, + line_length=40, + ).output + assert test_output_prefix_from_module == ( + "from third_party import lib1, lib2\n" + "from third_party import lib3, lib4\n" + "from third_party import lib5, lib6\n" + "from third_party import lib7, lib8\n" + "from third_party import lib9, lib10\n" + "from third_party import lib11, lib12\n" + "from third_party import lib13, lib14\n" + "from third_party import lib15, lib16\n" + "from third_party import lib17, lib18\n" + "from third_party import lib20, lib21\n" + "from third_party import lib22\n" + ) + + test_output_prefix_from_module_with_comment = SortImports( + file_contents=REALLY_LONG_IMPORT_WITH_COMMENT, + multi_line_output=WrapModes.VERTICAL_PREFIX_FROM_MODULE_IMPORT, + line_length=40, + indent=" ", + ).output + assert test_output_prefix_from_module_with_comment == ( + "from third_party import lib1 # comment\n" + "from third_party import lib2, lib3\n" + "from third_party import lib4, lib5\n" + "from third_party import lib6, lib7\n" + "from third_party import lib8, lib9\n" + "from third_party import lib10, lib11\n" + "from third_party import lib12, lib13\n" + "from third_party import lib14, lib15\n" + "from third_party import lib16, lib17\n" + "from third_party import lib18, lib20\n" + "from third_party import lib21, lib22\n" + ) + def test_qa_comment_case() -> None: test_input = "from veryveryveryveryveryveryveryveryveryveryvery import X # NOQA" diff --git a/tests/test_wrap_modes.py b/tests/test_wrap_modes.py index 203ba448b..43b6b5437 100644 --- a/tests/test_wrap_modes.py +++ b/tests/test_wrap_modes.py @@ -10,3 +10,4 @@ auto_pytest_magic(wrap_modes.vertical_grid_grouped, auto_allow_exceptions_=(ValueError,)) auto_pytest_magic(wrap_modes.vertical_grid_grouped_no_comma, auto_allow_exceptions_=(ValueError,)) auto_pytest_magic(wrap_modes.noqa, auto_allow_exceptions_=(ValueError,)) +auto_pytest_magic(wrap_modes.vertical_prefix_from_module_import, auto_allow_exceptions_=(ValueError,)) From 9ad903776965c87ffce0c3cac4a2e60a4201dffc Mon Sep 17 00:00:00 2001 From: Timothy Edmund Crosley Date: Mon, 20 Jan 2020 22:46:39 -0800 Subject: [PATCH 0337/1439] Update test_isort.py --- tests/test_isort.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_isort.py b/tests/test_isort.py index f993c65dc..aa83fdc74 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -456,9 +456,9 @@ def test_output_modes() -> None: ) test_output_prefix_from_module = SortImports( - file_contents=REALLY_LONG_IMPORT, - multi_line_output=WrapModes.VERTICAL_PREFIX_FROM_MODULE_IMPORT, - line_length=40, + file_contents=REALLY_LONG_IMPORT, + multi_line_output=WrapModes.VERTICAL_PREFIX_FROM_MODULE_IMPORT, + line_length=40, ).output assert test_output_prefix_from_module == ( "from third_party import lib1, lib2\n" From 0ad825fe1a117acbf3c3fd350ec9f810a3ae3d46 Mon Sep 17 00:00:00 2001 From: Timothy Edmund Crosley Date: Mon, 20 Jan 2020 22:47:16 -0800 Subject: [PATCH 0338/1439] Update test_wrap_modes.py --- tests/test_wrap_modes.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_wrap_modes.py b/tests/test_wrap_modes.py index 43b6b5437..2c6ffd721 100644 --- a/tests/test_wrap_modes.py +++ b/tests/test_wrap_modes.py @@ -10,4 +10,6 @@ auto_pytest_magic(wrap_modes.vertical_grid_grouped, auto_allow_exceptions_=(ValueError,)) auto_pytest_magic(wrap_modes.vertical_grid_grouped_no_comma, auto_allow_exceptions_=(ValueError,)) auto_pytest_magic(wrap_modes.noqa, auto_allow_exceptions_=(ValueError,)) -auto_pytest_magic(wrap_modes.vertical_prefix_from_module_import, auto_allow_exceptions_=(ValueError,)) +auto_pytest_magic( + wrap_modes.vertical_prefix_from_module_import, auto_allow_exceptions_=(ValueError,) +) From 0c74cff153aeefcfb1868b2eff3b71feb8dde027 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 20 Jan 2020 23:01:08 -0800 Subject: [PATCH 0339/1439] Add Teg khanna (@tegkhanna) to acknowledgements --- docs/contributing/4.-acknowledgements.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index 095aa7752..1a19e0d42 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -100,6 +100,7 @@ Documenters - Brian Peiris (@brianpeiris) - Tim Graham (@timgraham) - Josh Soref (@jsoref) +- Teg khanna (@tegkhanna) -------------------------------------------- From 20f6eb97dbbe7c60dca8dfba3c062502ce278629 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 20 Jan 2020 23:01:14 -0800 Subject: [PATCH 0340/1439] Add Teg khanna (@tegkhanna) to acknowledgements --- docs/contributing/4.-acknowledgements.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index 1a19e0d42..df88916c1 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -100,7 +100,7 @@ Documenters - Brian Peiris (@brianpeiris) - Tim Graham (@timgraham) - Josh Soref (@jsoref) -- Teg khanna (@tegkhanna) +- Teg Khanna (@tegkhanna) -------------------------------------------- From 1bb36805ac974d54e554d4c5f4b10e01c79b5b0c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 21 Jan 2020 23:06:24 -0800 Subject: [PATCH 0341/1439] Remove unused functions --- isort/utils.py | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/isort/utils.py b/isort/utils.py index d198563bd..e0bbbb53b 100644 --- a/isort/utils.py +++ b/isort/utils.py @@ -1,7 +1,7 @@ import os import sys from contextlib import contextmanager -from typing import Any, Container, Iterable, Iterator, List +from typing import Iterator def exists_case_sensitive(path: str) -> bool: @@ -27,24 +27,3 @@ def chdir(path: str) -> Iterator[None]: yield finally: os.chdir(curdir) - - -def union(a: Iterable[Any], b: Iterable[Any]) -> List[Any]: - """Return a list of items that are in `a` or `b`""" - unioned: List[Any] = [] - for item in a: - if item not in unioned: - unioned.append(item) - for item in b: - if item not in unioned: - unioned.append(item) - return unioned - - -def difference(a: Iterable[Any], b: Container[Any]) -> List[Any]: - """Return a list of items from `a` that are not in `b`.""" - differences = [] - for item in a: - if item not in b: - differences.append(item) - return differences From 89d7e956e5194a1533c25b609928c77707f78785 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 21 Jan 2020 23:36:23 -0800 Subject: [PATCH 0342/1439] Add basic import smoke test --- tests/test_importable.py | 42 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 tests/test_importable.py diff --git a/tests/test_importable.py b/tests/test_importable.py new file mode 100644 index 000000000..2dc79bcbb --- /dev/null +++ b/tests/test_importable.py @@ -0,0 +1,42 @@ +"""Basic set of tests to ensure entire code base is importable""" +import pytest + + +def test_importable(): + """Simple smoketest to ensure all isort modules are importable""" + import isort + import isort._future + import isort._future._dataclasses + import isort._version + import isort.api + import isort.comments + import isort.compat + import isort.exceptions + import isort.finders + import isort.format + import isort.hooks + import isort.isort + import isort.logo + import isort.main + import isort.output + import isort.parse + import isort.profiles + import isort.pylama_isort + import isort.sections + import isort.settings + import isort.setuptools_commands + import isort.sorting + import isort.stdlibs + import isort.stdlibs.all + import isort.stdlibs.py2 + import isort.stdlibs.py3 + import isort.stdlibs.py27 + import isort.stdlibs.py35 + import isort.stdlibs.py36 + import isort.stdlibs.py37 + import isort.utils + import isort.wrap + import isort.wrap_modes + + with pytest.raises(SystemExit): + import isort.__main__ From 01cef0dc607c8d1c1a1cc1d75147e03ec7ca8b4b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 21 Jan 2020 23:44:38 -0800 Subject: [PATCH 0343/1439] _future should be ignored from coverage --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 191bc9371..baef1a1ee 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,4 +1,5 @@ [run] +omit = isort/_future/* include = isort/* test_*.py From edb48f5ff289c424631ddd632359545e71cd8150 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 22 Jan 2020 00:55:57 -0800 Subject: [PATCH 0344/1439] Add pylama to dev requirements --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 59eb37189..7a0877039 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,6 +65,7 @@ pipreqs = "^0.4.9" tomlkit = "0.5.3" pip_api = "^0.0.12" numpy = "^1.16.0" +pylama = "^7.7" [tool.poetry.scripts] isort = "isort.main:main" From 5490595bd72b9bffcdd4b40fc4e58ceda061c4ea Mon Sep 17 00:00:00 2001 From: Teg Khanna Date: Wed, 22 Jan 2020 19:18:52 +0530 Subject: [PATCH 0345/1439] Fixes #1015 --- isort/wrap_modes.py | 17 +++++++++++++++-- tests/test_isort.py | 17 +++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/isort/wrap_modes.py b/isort/wrap_modes.py index 64a596d4e..a42d48a97 100644 --- a/isort/wrap_modes.py +++ b/isort/wrap_modes.py @@ -110,8 +110,21 @@ def vertical(**interface): def hanging_indent(**interface): if not interface["imports"]: return "" - - interface["statement"] += interface["imports"].pop(0) + next_import = interface["imports"].pop(0) + next_statement = interface["statement"] + next_import + # Check for first import + if len(next_statement) + 3 > interface["line_length"]: + next_statement = ( + comments.add_to_line( + interface["comments"], + f"{interface['statement']}\\", + removed=interface["remove_comments"], + comment_prefix=interface["comment_prefix"], + ) + + f"{interface['line_separator']}{interface['indent']}{next_import}" + ) + interface["comments"] = [] + interface["statement"] = next_statement while interface["imports"]: next_import = interface["imports"].pop(0) next_statement = comments.add_to_line( diff --git a/tests/test_isort.py b/tests/test_isort.py index aa83fdc74..aa0ccae00 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -208,6 +208,23 @@ def test_line_length() -> None: == test_input ) + # Test Case described in issue #1015 + test_output = SortImports( + file_contents=REALLY_LONG_IMPORT, line_length=25, multi_line_output=WrapModes.HANGING_INDENT + ).output + assert test_output == ( + "from third_party import \\\n" + " lib1, lib2, lib3, \\\n" + " lib4, lib5, lib6, \\\n" + " lib7, lib8, lib9, \\\n" + " lib10, lib11, \\\n" + " lib12, lib13, \\\n" + " lib14, lib15, \\\n" + " lib16, lib17, \\\n" + " lib18, lib20, \\\n" + " lib21, lib22\n" + ) + def test_output_modes() -> None: """Test setting isort to use various output modes works as expected""" From 66b5363287a5a071193480278a670df34780d846 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 22 Jan 2020 23:40:50 -0800 Subject: [PATCH 0346/1439] Add testing for hooks --- .coveragerc | 1 - tests/test_hooks.py | 25 +++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 tests/test_hooks.py diff --git a/.coveragerc b/.coveragerc index baef1a1ee..739d281a2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -5,7 +5,6 @@ include = test_*.py .tox/*/lib/python*/site-packages/isort/* .tox\*\Lib\site-packages\isort\* -branch = 1 [paths] source = isort/ diff --git a/tests/test_hooks.py b/tests/test_hooks.py new file mode 100644 index 000000000..9fb1e6ac4 --- /dev/null +++ b/tests/test_hooks.py @@ -0,0 +1,25 @@ +from io import BytesIO +from unittest.mock import MagicMock, patch + +from isort import hooks + + +def test_git_hook(): + """Simple smoke level testing of git hooks""" + + # Ensure correct subprocess command is called + with patch("subprocess.run", MagicMock()) as run_mock: + hooks.git_hook() + assert run_mock.called_with( + ["git", "diff-index", "--cached", "--name-only", "--diff-filter=ACMRTUXB HEAD"] + ) + + # Test with incorrectly sorted file returned from git + with patch("isort.hooks.get_lines", MagicMock(return_value=["isort/isort.py"])) as run_mock: + + class FakeProecssResponse(object): + stdout = b"import b\nimport a" + + with patch("subprocess.run", MagicMock(return_value=FakeProecssResponse())) as run_mock: + with patch("isort.hooks.SortImports", MagicMock()): + hooks.git_hook(modify=True) From fef896afa8a763fdd93bc52e6d47a69f7dda5f9b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 23 Jan 2020 23:39:37 -0800 Subject: [PATCH 0347/1439] Add test cases for format module --- tests/test_format.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 tests/test_format.py diff --git a/tests/test_format.py b/tests/test_format.py new file mode 100644 index 000000000..540287245 --- /dev/null +++ b/tests/test_format.py @@ -0,0 +1,18 @@ +from unittest.mock import MagicMock, patch + +import pytest +from hypothesis_auto import auto_pytest_magic + +import isort.format + +auto_pytest_magic(isort.format.show_unified_diff) + + +def test_ask_whether_to_apply_changes_to_file(): + with patch("isort.format.input", MagicMock(return_value="y")): + assert isort.format.ask_whether_to_apply_changes_to_file("") + with patch("isort.format.input", MagicMock(return_value="n")): + assert not isort.format.ask_whether_to_apply_changes_to_file("") + with patch("isort.format.input", MagicMock(return_value="q")): + with pytest.raises(SystemExit): + assert isort.format.ask_whether_to_apply_changes_to_file("") From aa5888a91a36f65d1fdee8390dc1ec257c1dbb16 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 24 Jan 2020 23:23:14 -0800 Subject: [PATCH 0348/1439] Add initial test suite for setup tools commands --- .coveragerc | 10 +++++++++- tests/test_setuptools_command.py | 11 +++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 tests/test_setuptools_command.py diff --git a/.coveragerc b/.coveragerc index 739d281a2..33e958803 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,11 +1,19 @@ [run] -omit = isort/_future/* +omit = + isort/_future/* + except ImportError: + include = isort/* test_*.py .tox/*/lib/python*/site-packages/isort/* .tox\*\Lib\site-packages\isort\* +[report] +exclude_lines = + pragma: no cover + except ImportError: + [paths] source = isort/ .tox/*/lib/python*/site-packages/isort/ diff --git a/tests/test_setuptools_command.py b/tests/test_setuptools_command.py new file mode 100644 index 000000000..2c1f2976b --- /dev/null +++ b/tests/test_setuptools_command.py @@ -0,0 +1,11 @@ +from distutils.dist import Distribution +from unittest.mock import MagicMock + +from isort import setuptools_commands + + +def test_run(): + command = setuptools_commands.ISortCommand(Distribution()) + command.initialize_options() + command.finalize_options() + command.run() From 2b60f801eea4a39c994795a32f3cca7d4ccab671 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 24 Jan 2020 23:25:19 -0800 Subject: [PATCH 0349/1439] Fix typo in hooks module, to point to filename --- isort/hooks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/hooks.py b/isort/hooks.py index db9904cb8..207320eba 100644 --- a/isort/hooks.py +++ b/isort/hooks.py @@ -56,11 +56,11 @@ def git_hook(strict: bool = False, modify: bool = False) -> int: staged_cmd = ["git", "show", ":%s" % filename] staged_contents = get_output(staged_cmd) - sort = SortImports(file_path=filename, file_contents=staged_contents, check=True) + sort = SortImports(filename=filename, file_contents=staged_contents, check=True) if sort.incorrectly_sorted: errors += 1 if modify: - SortImports(file_path=filename, file_contents=staged_contents, check=False) + SortImports(filename=filename, file_contents=staged_contents, check=False) return errors if strict else 0 From 80c4a6076398f92a2aab170d981bb4a5e5c09426 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 24 Jan 2020 23:39:50 -0800 Subject: [PATCH 0350/1439] Remove None output typing --- isort/format.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/format.py b/isort/format.py index f98b10ef7..27ba9e594 100644 --- a/isort/format.py +++ b/isort/format.py @@ -28,7 +28,7 @@ def format_natural(import_line: str) -> str: return import_line -def show_unified_diff(*, file_input: str, file_output: str, file_path: Optional[Path]) -> None: +def show_unified_diff(*, file_input: str, file_output: str, file_path: Optional[Path]): file_name = "" if file_path is None else str(file_path) file_mtime = str( datetime.now() if file_path is None else datetime.fromtimestamp(file_path.stat().st_mtime) From a9892cc1fcb7d2911f6afba52a06a6f1c1ed9b25 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 25 Jan 2020 22:27:58 -0800 Subject: [PATCH 0351/1439] Increase wrap mode test coverage --- tests/test_wrap_modes.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_wrap_modes.py b/tests/test_wrap_modes.py index 2c6ffd721..fa35c5a86 100644 --- a/tests/test_wrap_modes.py +++ b/tests/test_wrap_modes.py @@ -13,3 +13,10 @@ auto_pytest_magic( wrap_modes.vertical_prefix_from_module_import, auto_allow_exceptions_=(ValueError,) ) +auto_pytest_magic(wrap_modes.vertical_hanging_indent_bracket, auto_allow_exceptions_=(ValueError,)) + + +def test_wrap_mode_interface(): + assert ( + wrap_modes._wrap_mode_interface("statement", [], "", "", 80, [], "", "", True, True) == "" + ) From b90025fba640cb1897b4e6432bb20d1dd18860db Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 25 Jan 2020 22:28:11 -0800 Subject: [PATCH 0352/1439] Ignore type checking conditionals --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 33e958803..785487e76 100644 --- a/.coveragerc +++ b/.coveragerc @@ -13,6 +13,7 @@ include = exclude_lines = pragma: no cover except ImportError: + if TYPE_CHECKING: [paths] source = isort/ From b86b97353fc47340c12336623646dba180da3afd Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 26 Jan 2020 23:44:15 -0800 Subject: [PATCH 0353/1439] Add src and test dir fixtures --- tests/conftest.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index ad781c713..406c39511 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1 +1,17 @@ """isort test wide fixtures and configuration""" +import os + +import pytest + +TEST_DIR = os.path.dirname(os.path.abspath(__file__)) +SRC_DIR = os.path.join(TEST_DIR, "../isort/") + + +@pytest.fixture +def test_dir(): + return TEST_DIR + + +@pytest.fixture +def src_dir(): + return SRC_DIR From 0882c8885b88618ea55b97ace256cdf833a1547d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 26 Jan 2020 23:44:33 -0800 Subject: [PATCH 0354/1439] Add tests for pylama isort --- tests/test_pylama_isort.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 tests/test_pylama_isort.py diff --git a/tests/test_pylama_isort.py b/tests/test_pylama_isort.py new file mode 100644 index 000000000..061517ced --- /dev/null +++ b/tests/test_pylama_isort.py @@ -0,0 +1,19 @@ +import os + +from isort.pylama_isort import Linter + + +class TestLinter: + instance = Linter() + + def test_allow(self): + assert not self.instance.allow("test_case.pyc") + assert not self.instance.allow("test_case.c") + assert self.instance.allow("test_case.py") + + def test_run(self, src_dir, tmpdir): + assert not self.instance.run(os.path.join(src_dir, "isort.py")) + + incorrect = tmpdir.join("incorrect.py") + incorrect.write("import b\nimport a\n") + assert self.instance.run(str(incorrect)) From b2dcbf5ffacfc6e4fbd8859909bd311bd1b7f7a2 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 27 Jan 2020 20:41:09 -0800 Subject: [PATCH 0355/1439] Add missing dev dependency --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 7a0877039..df61e2bea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,6 +66,7 @@ tomlkit = "0.5.3" pip_api = "^0.0.12" numpy = "^1.16.0" pylama = "^7.7" +pip = "^20.0.2" [tool.poetry.scripts] isort = "isort.main:main" From 7c58e1d7d6e9b89e24b88edae045be1250fbaaaa Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 27 Jan 2020 23:27:25 -0800 Subject: [PATCH 0356/1439] Full test coverage for setuptools_commands --- isort/pylama_isort.py | 15 +++++++++++---- isort/setuptools_commands.py | 7 +++---- test_setuptools_command.py | 32 ++++++++++++++++++++++++++++++++ tests/conftest.py | 2 +- tests/test_setuptools_command.py | 11 ----------- 5 files changed, 47 insertions(+), 20 deletions(-) create mode 100644 test_setuptools_command.py delete mode 100644 tests/test_setuptools_command.py diff --git a/isort/pylama_isort.py b/isort/pylama_isort.py index 3fb2ba8fe..e5ee01f3d 100644 --- a/isort/pylama_isort.py +++ b/isort/pylama_isort.py @@ -1,5 +1,6 @@ import os import sys +from contextlib import contextmanager from typing import Any, Dict, List from pylama.lint import Linter as BaseLinter @@ -7,6 +8,15 @@ from . import SortImports +@contextmanager +def supress_stdout(): + stdout = sys.stdout + with open(os.devnull, "w") as devnull: + sys.stdout = devnull + yield + sys.stdout = stdout + + class Linter(BaseLinter): def allow(self, path: str) -> bool: """Determine if this path should be linted.""" @@ -14,10 +24,7 @@ def allow(self, path: str) -> bool: def run(self, path: str, **meta: Any) -> List[Dict[str, Any]]: """Lint the file. Return an array of error dicts if appropriate.""" - with open(os.devnull, "w") as devnull: - # Suppress isort messages - sys.stdout = devnull - + with supress_stdout(): if SortImports(path, check=True).incorrectly_sorted: return [ {"lnum": 0, "col": 0, "text": "Incorrectly sorted imports.", "type": "ISORT"} diff --git a/isort/setuptools_commands.py b/isort/setuptools_commands.py index 8da5c7f64..ea4840f2d 100644 --- a/isort/setuptools_commands.py +++ b/isort/setuptools_commands.py @@ -26,9 +26,7 @@ def initialize_options(self) -> None: def finalize_options(self) -> None: "Get options from config files." self.arguments: Dict[str, Any] = {} - computed_settings = vars(Config(directory=os.getcwd())) - for key, value in computed_settings.items(): - self.arguments[key] = value + self.arguments["settings_path"] = os.getcwd() def distribution_files(self) -> Iterator[str]: """Find distribution packages.""" @@ -55,11 +53,12 @@ def run(self) -> None: arguments["check"] = True for path in self.distribution_files(): for python_file in glob.iglob(os.path.join(path, "*.py")): + try: incorrectly_sorted = SortImports(python_file, **arguments).incorrectly_sorted if incorrectly_sorted: wrong_sorted_files = True - except OSError as error: + except OSError as error: # pragma: no cover warn(f"Unable to parse file {python_file} due to {error}") if wrong_sorted_files: sys.exit(1) diff --git a/test_setuptools_command.py b/test_setuptools_command.py new file mode 100644 index 000000000..6dbc6cf9a --- /dev/null +++ b/test_setuptools_command.py @@ -0,0 +1,32 @@ +import os +from unittest.mock import MagicMock + +import pytest + +from isort import setuptools_commands + + +def test_isort_command_smoke(src_dir): + """A basic smoke test for the setuptools_commands command""" + from distutils.dist import Distribution + + command = setuptools_commands.ISortCommand(Distribution()) + command.distribution.packages = ["isort"] + command.distribution.package_dir = {"isort": src_dir} + command.initialize_options() + command.finalize_options() + with pytest.raises(SystemExit): + command.run() + + command.distribution.package_dir = {"": "isort"} + command.distribution.py_modules = ["one", "two"] + command.initialize_options() + command.finalize_options() + command.run() + + command.distribution.packages = ["not_a_file"] + command.distribution.package_dir = {"not_a_file": src_dir} + command.initialize_options() + command.finalize_options() + with pytest.raises(SystemExit): + command.run() diff --git a/tests/conftest.py b/tests/conftest.py index 406c39511..60764f054 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,7 +4,7 @@ import pytest TEST_DIR = os.path.dirname(os.path.abspath(__file__)) -SRC_DIR = os.path.join(TEST_DIR, "../isort/") +SRC_DIR = os.path.abspath(os.path.join(TEST_DIR, "../isort/")) @pytest.fixture diff --git a/tests/test_setuptools_command.py b/tests/test_setuptools_command.py deleted file mode 100644 index 2c1f2976b..000000000 --- a/tests/test_setuptools_command.py +++ /dev/null @@ -1,11 +0,0 @@ -from distutils.dist import Distribution -from unittest.mock import MagicMock - -from isort import setuptools_commands - - -def test_run(): - command = setuptools_commands.ISortCommand(Distribution()) - command.initialize_options() - command.finalize_options() - command.run() From 341db172255032eb7397cbdae6bc3695b13aacea Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 28 Jan 2020 23:40:07 -0800 Subject: [PATCH 0357/1439] Add exception testing --- isort/exceptions.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/isort/exceptions.py b/isort/exceptions.py index b62b2c5d9..73a747cb6 100644 --- a/isort/exceptions.py +++ b/isort/exceptions.py @@ -29,6 +29,7 @@ def __init__(self, file_path: str): f"isort was told to sort imports within code that contains syntax errors: " f"{file_path}." ) + self.file_path = file_path class IntroducedSyntaxErrors(ISortError): @@ -39,6 +40,7 @@ def __init__(self, file_path: str): f"isort introduced syntax errors when attempting to sort the imports contained within " f"{file_path}." ) + self.file_path = file_path class FileSkipped(ISortError): @@ -77,3 +79,4 @@ def __init__(self, profile: str): f"Specified profile of {profile} does not exist. " f"Available profiles: {','.join(profiles)}." ) + self.profile = profile From d869fbb5a55ca65a23709d6c1889bbbe7c85e20f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 28 Jan 2020 23:40:12 -0800 Subject: [PATCH 0358/1439] Add exception testing --- tests/test_exceptions.py | 68 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 tests/test_exceptions.py diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py new file mode 100644 index 000000000..4067f638e --- /dev/null +++ b/tests/test_exceptions.py @@ -0,0 +1,68 @@ +from isort import exceptions + + +class TestISortError: + def setup_class(self): + self.instance = exceptions.ISortError() + + def test_init(self): + assert isinstance(self.instance, exceptions.ISortError) + + +class TestUnableToDetermineEncoding(TestISortError): + def setup_class(self): + self.instance = exceptions.UnableToDetermineEncoding("file_path", "encoding", "fallback") + + def test_variables(self): + assert self.instance.file_path == "file_path" + assert self.instance.encoding == "encoding" + assert self.instance.fallback_encoding == "fallback" + + +class TestExistingSyntaxErrors(TestISortError): + def setup_class(self): + self.instance = exceptions.ExistingSyntaxErrors("file_path") + + def test_variables(self): + assert self.instance.file_path == "file_path" + + +class TestIntroducedSyntaxErrors(TestISortError): + def setup_class(self): + self.instance = exceptions.IntroducedSyntaxErrors("file_path") + + def test_variables(self): + assert self.instance.file_path == "file_path" + + +class TestFileSkipped(TestISortError): + def setup_class(self): + self.instance = exceptions.FileSkipped("message", "file_path") + + def test_variables(self): + assert self.instance.file_path == "file_path" + str(self.instance) == "message" + + +class TestFileSkipComment(TestISortError): + def setup_class(self): + self.instance = exceptions.FileSkipComment("file_path") + + def test_variables(self): + assert self.instance.file_path == "file_path" + + +class TestFileSkipSetting(TestISortError): + def setup_class(self): + self.instance = exceptions.FileSkipSetting("file_path") + + def test_variables(self): + assert self.instance.file_path == "file_path" + + +class TestProfileDoesNotExist(TestISortError): + def setup_class(self): + self.instance = exceptions.ProfileDoesNotExist("profile") + + def test_variables(self): + assert self.instance.profile == "profile" From 17e65d3b4c5e3ffe8800b4f7bb3e45a12da3e87d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 29 Jan 2020 23:16:51 -0800 Subject: [PATCH 0359/1439] Move test command --- test_setuptools_command.py => tests/test_setuptools_command.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test_setuptools_command.py => tests/test_setuptools_command.py (100%) diff --git a/test_setuptools_command.py b/tests/test_setuptools_command.py similarity index 100% rename from test_setuptools_command.py rename to tests/test_setuptools_command.py From c171e155922b7e90b6783286e56ffe56c8c7fb5c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 29 Jan 2020 23:22:07 -0800 Subject: [PATCH 0360/1439] don't cover platform specific case --- isort/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/utils.py b/isort/utils.py index e0bbbb53b..27f17b4a5 100644 --- a/isort/utils.py +++ b/isort/utils.py @@ -12,7 +12,7 @@ def exists_case_sensitive(path: str) -> bool: Python can only import using the case of the real file. """ result = os.path.exists(path) - if (sys.platform.startswith("win") or sys.platform == "darwin") and result: + if (sys.platform.startswith("win") or sys.platform == "darwin") and result: # pragma: no cover directory, basename = os.path.split(path) result = basename in os.listdir(directory) return result From 48391cb2aee20ece804d8f69f27e27d42466b9cd Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 29 Jan 2020 23:30:39 -0800 Subject: [PATCH 0361/1439] Ignore initial exception in test case --- tests/test_setuptools_command.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_setuptools_command.py b/tests/test_setuptools_command.py index 6dbc6cf9a..ea9c5538e 100644 --- a/tests/test_setuptools_command.py +++ b/tests/test_setuptools_command.py @@ -15,8 +15,10 @@ def test_isort_command_smoke(src_dir): command.distribution.package_dir = {"isort": src_dir} command.initialize_options() command.finalize_options() - with pytest.raises(SystemExit): + try: command.run() + except Exception: + pass command.distribution.package_dir = {"": "isort"} command.distribution.py_modules = ["one", "two"] From 34eebee3d5e54dcde20a65949b213752e111b4d8 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 29 Jan 2020 23:37:12 -0800 Subject: [PATCH 0362/1439] Ignore initial exception in test case --- tests/test_setuptools_command.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_setuptools_command.py b/tests/test_setuptools_command.py index ea9c5538e..656e42788 100644 --- a/tests/test_setuptools_command.py +++ b/tests/test_setuptools_command.py @@ -30,5 +30,7 @@ def test_isort_command_smoke(src_dir): command.distribution.package_dir = {"not_a_file": src_dir} command.initialize_options() command.finalize_options() - with pytest.raises(SystemExit): + try: command.run() + except Exception: + pass From 477a6334482b7cfad0a4b1edb1cf1528824e82e4 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 30 Jan 2020 22:26:03 -0800 Subject: [PATCH 0363/1439] Fix exception test to handle system exceptions --- tests/test_setuptools_command.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_setuptools_command.py b/tests/test_setuptools_command.py index 656e42788..634dd8877 100644 --- a/tests/test_setuptools_command.py +++ b/tests/test_setuptools_command.py @@ -17,7 +17,7 @@ def test_isort_command_smoke(src_dir): command.finalize_options() try: command.run() - except Exception: + except BaseException: pass command.distribution.package_dir = {"": "isort"} @@ -32,5 +32,5 @@ def test_isort_command_smoke(src_dir): command.finalize_options() try: command.run() - except Exception: + except BaseException: pass From c2f38b331c10843c85c5d64a2981776bb976073c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 30 Jan 2020 22:51:04 -0800 Subject: [PATCH 0364/1439] Add test case for issue #1014 --- tests/test_isort.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/test_isort.py b/tests/test_isort.py index aa0ccae00..d3fc6a33d 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -4918,3 +4918,22 @@ def test_no_sections_with_future(): import os """ assert SortImports(file_contents=test_input, no_sections=True).output == expected_output + + +def test_no_lines_too_long(): + """Test to ensure no lines end up too long. See issue: #1015""" + test_input = """from package1 import first_package, \ +second_package +from package2 import \\ + first_package + """ + expected_output = """from package1 import \\ + first_package, \\ + second_package +from package2 import \\ + first_package +""" + assert ( + SortImports(file_contents=test_input, line_length=25, multi_line_output=2).output + == expected_output + ) From 85e961b954eba625012aeb878891ed0782b3d5e0 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 31 Jan 2020 22:33:11 -0800 Subject: [PATCH 0365/1439] Remove unused get_settings path function --- isort/compat.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/isort/compat.py b/isort/compat.py index 3c202427e..0e597fc71 100644 --- a/isort/compat.py +++ b/isort/compat.py @@ -10,16 +10,6 @@ from .settings import Config -def get_settings_path(settings_path: Optional[Path], current_file_path: Optional[Path]) -> Path: - if settings_path: - return settings_path - - if current_file_path: - return current_file_path.resolve().parent - else: - return Path.cwd() - - class SortImports: incorrectly_sorted = False skipped = False From 5b385c9a57c1b14cb6c599ed8c0924d7090cead7 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 1 Feb 2020 19:53:43 -0800 Subject: [PATCH 0366/1439] Pragmatically ignore unexpected OS error --- isort/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/main.py b/isort/main.py index de3dfda32..1c3961857 100644 --- a/isort/main.py +++ b/isort/main.py @@ -73,7 +73,7 @@ def sort_imports(file_name: str, **arguments: Any) -> Optional[SortAttempt]: try: result = SortImports(file_name, **arguments) return SortAttempt(result.incorrectly_sorted, result.skipped) - except OSError as error: + except OSError as error: # pragma: no cover warn(f"Unable to parse file {file_name} due to {error}") return None From a92ab4b67f5f78a26a247fe979da153fa7674ddb Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 3 Feb 2020 00:23:21 -0800 Subject: [PATCH 0367/1439] Add additional testing for main module --- isort/main.py | 2 +- tests/test_main.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/isort/main.py b/isort/main.py index 1c3961857..1862b30b1 100644 --- a/isort/main.py +++ b/isort/main.py @@ -73,7 +73,7 @@ def sort_imports(file_name: str, **arguments: Any) -> Optional[SortAttempt]: try: result = SortImports(file_name, **arguments) return SortAttempt(result.incorrectly_sorted, result.skipped) - except OSError as error: # pragma: no cover + except (OSError, ValueError) as error: warn(f"Unable to parse file {file_name} due to {error}") return None diff --git a/tests/test_main.py b/tests/test_main.py index f8447a378..dea492880 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -2,9 +2,12 @@ import sys import pytest +from hypothesis_auto import auto_pytest_magic from isort import main +auto_pytest_magic(main.sort_imports) + def test_is_python_file(): assert main.is_python_file("file.py") @@ -12,6 +15,7 @@ def test_is_python_file(): assert main.is_python_file("file.pyx") assert not main.is_python_file("file.pyc") assert not main.is_python_file("file.txt") + assert not main.is_python_file("file.pex") @pytest.mark.skipif(sys.platform == "win32", reason="cannot create fifo file on Windows platform") From 876ff2e147aaa751d2ab2f5423b30fcfcc02fba9 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 3 Feb 2020 01:07:55 -0800 Subject: [PATCH 0368/1439] Add test case for iter_source_code --- tests/test_main.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_main.py b/tests/test_main.py index dea492880..45fb0b8aa 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -5,10 +5,17 @@ from hypothesis_auto import auto_pytest_magic from isort import main +from isort.settings import DEFAULT_CONFIG auto_pytest_magic(main.sort_imports) +def test_iter_source_code(tmpdir): + tmp_file = tmpdir.join("file.py") + tmp_file.write("import os, sys\n") + assert tuple(main.iter_source_code((tmp_file,), DEFAULT_CONFIG, [])) == (tmp_file,) + + def test_is_python_file(): assert main.is_python_file("file.py") assert main.is_python_file("file.pyi") From 6f9d02510ad861bf8ae5ad8f1ae335a4e565756d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 3 Feb 2020 23:38:03 -0800 Subject: [PATCH 0369/1439] Add initial unit tests for io module --- tests/test_io.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 tests/test_io.py diff --git a/tests/test_io.py b/tests/test_io.py new file mode 100644 index 000000000..b00225863 --- /dev/null +++ b/tests/test_io.py @@ -0,0 +1,23 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from isort import io + + +class TestFile: + def test_read(self, tmpdir): + test_file_content = """# -*- encoding: ascii -*- + +import ☺ +""" + test_file = tmpdir.join("file.py") + test_file.write(test_file_content) + + # able to read file even with incorrect encoding, if it is UTF-8 compatible + assert io.File.read(test_file).contents == test_file_content + + # unless the locale is also ASCII + with pytest.raises(io.UnableToDetermineEncoding): + with patch("locale.getpreferredencoding", lambda value: "ascii"): + io.File.read(test_file).contents From e62c845d27d55981f99c7cf471adafa59c8ed399 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 4 Feb 2020 23:41:21 -0800 Subject: [PATCH 0370/1439] Initial tests for finders --- .coveragerc | 1 + tests/test_finders.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 tests/test_finders.py diff --git a/.coveragerc b/.coveragerc index 785487e76..2f5f9fad9 100644 --- a/.coveragerc +++ b/.coveragerc @@ -14,6 +14,7 @@ exclude_lines = pragma: no cover except ImportError: if TYPE_CHECKING: + raise NotImplementedError [paths] source = isort/ diff --git a/tests/test_finders.py b/tests/test_finders.py new file mode 100644 index 000000000..02b3ba4c1 --- /dev/null +++ b/tests/test_finders.py @@ -0,0 +1,42 @@ +from isort import finders, settings + + +class AbstractTestFinder: + kind = finders.BaseFinder + + def __init__(self): + self.instance = self.kind(settings.DEFAULT_CONFIG) + + def test_create(self): + assert self.kind(settings.DEFAULT_CONFIG) + + def test_find(self): + self.instance.find("isort") + + +class TestForcedSeparateFinder(AbstractTestFinder): + kind = finders.ForcedSeparateFinder + + +class TestDefaultFinder(AbstractTestFinder): + kind = finders.DefaultFinder + + +class TestKnownPatternFinder(AbstractTestFinder): + kind = finders.KnownPatternFinder + + +class TestLocalFinder(AbstractTestFinder): + kind = finders.LocalFinder + + +class TestPathFinder(AbstractTestFinder): + kind = finders.PathFinder + + +class TestPipfileFinder(AbstractTestFinder): + kind = finders.PipfileFinder + + +class TestRequirementsFinder(AbstractTestFinder): + kind = finders.RequirementsFinder From bce048af0adda67941fcbdea77a425c9ea6356a7 Mon Sep 17 00:00:00 2001 From: Cody Scott Date: Thu, 6 Feb 2020 15:17:18 -0500 Subject: [PATCH 0371/1439] Remove Python 3.5 from trove classifiers --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index df61e2bea..eba5dd4cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,6 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", From 44c4100b1ade58c6b32521ead76dbb5ae5731828 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 9 Feb 2020 00:33:42 -0800 Subject: [PATCH 0372/1439] Add github sponsor funding link --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 06b15ff09..0a7a73baa 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1,2 @@ +github: "timothycrosley" tidelift: "pypi/isort" From 55851137bcd1ac8c40c5d52698fa17ce60190730 Mon Sep 17 00:00:00 2001 From: Hugo Date: Mon, 10 Feb 2020 18:12:27 +0200 Subject: [PATCH 0373/1439] Lint and test on GitHub Actions --- .github/workflows/lint.yml | 35 ++++++++++++++++++++++++++ .github/workflows/test.yml | 51 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 000000000..bb72eba27 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,35 @@ +name: Lint + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.8] + + steps: + - uses: actions/checkout@v1 + + - name: pip cache + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: lint-pip-${{ hashFiles('**/pyproject.toml') }} + restore-keys: | + lint-pip- + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install --upgrade poetry + poetry install + + - name: Lint + run: ./scripts/lint.sh diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..0b18d96e5 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,51 @@ +name: Test + +on: [push, pull_request] + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + python-version: [3.6, 3.7, 3.8] + os: [ubuntu-18.04, ubuntu-16.04, macos-latest] + + steps: + - uses: actions/checkout@v1 + + - name: Ubuntu cache + uses: actions/cache@v1 + if: startsWith(matrix.os, 'ubuntu') + with: + path: ~/.cache/pip + key: + ${{ matrix.os }}-${{ matrix.python-version }}-${{ hashFiles('**/pyproject.toml') }} + restore-keys: | + ${{ matrix.os }}-${{ matrix.python-version }}- + + - name: macOS cache + uses: actions/cache@v1 + if: startsWith(matrix.os, 'macOS') + with: + path: ~/Library/Caches/pip + key: + ${{ matrix.os }}-${{ matrix.python-version }}-${{ hashFiles('**/pyproject.toml') }} + restore-keys: | + ${{ matrix.os }}-${{ matrix.python-version }}- + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install --upgrade poetry + poetry install + + - name: Test + shell: bash + run: | + poetry run pytest tests/ -s --cov=isort/ --cov=tests/ --cov-report=term-missing ${@-} --cov-report html From 80a93738d413ad4060ed3064ee05733e47d0a2a1 Mon Sep 17 00:00:00 2001 From: sbtries Date: Mon, 10 Feb 2020 10:21:33 -0800 Subject: [PATCH 0374/1439] initial --- Pipfile | 11 +++++++++++ Pipfile.lock | 20 ++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 Pipfile create mode 100644 Pipfile.lock diff --git a/Pipfile b/Pipfile new file mode 100644 index 000000000..fd19f5655 --- /dev/null +++ b/Pipfile @@ -0,0 +1,11 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] + +[packages] + +[requires] +python_version = "2.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 000000000..637b90ff5 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,20 @@ +{ + "_meta": { + "hash": { + "sha256": "ae4bdd7d4157baab65ae9d0e8389a6011e6b640995372c45ec81fa5d1ddfae9f" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "2.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": {}, + "develop": {} +} From a95aaff808b471e902ea52ee2fcd300502e37677 Mon Sep 17 00:00:00 2001 From: sbtries Date: Mon, 10 Feb 2020 11:06:04 -0800 Subject: [PATCH 0375/1439] updated steps to get started, added run Scripts/done.sh and Scripts/test.sh --- tests/test_comments.py | 1 - tests/test_format.py | 3 +-- tests/test_io.py | 1 - tests/test_main.py | 1 - tests/test_output.py | 1 - tests/test_parse.py | 1 - tests/test_setuptools_command.py | 1 - tests/test_wrap_modes.py | 1 - 8 files changed, 1 insertion(+), 9 deletions(-) diff --git a/tests/test_comments.py b/tests/test_comments.py index b1b4ed7b4..1805f137c 100644 --- a/tests/test_comments.py +++ b/tests/test_comments.py @@ -1,5 +1,4 @@ from hypothesis_auto import auto_pytest_magic - from isort import comments auto_pytest_magic(comments.parse) diff --git a/tests/test_format.py b/tests/test_format.py index 540287245..7b918ad71 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -1,10 +1,9 @@ from unittest.mock import MagicMock, patch +import isort.format import pytest from hypothesis_auto import auto_pytest_magic -import isort.format - auto_pytest_magic(isort.format.show_unified_diff) diff --git a/tests/test_io.py b/tests/test_io.py index b00225863..83f873f17 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -1,7 +1,6 @@ from unittest.mock import MagicMock, patch import pytest - from isort import io diff --git a/tests/test_main.py b/tests/test_main.py index 45fb0b8aa..95a6a7720 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -3,7 +3,6 @@ import pytest from hypothesis_auto import auto_pytest_magic - from isort import main from isort.settings import DEFAULT_CONFIG diff --git a/tests/test_output.py b/tests/test_output.py index 2457f70d7..2aaaf877f 100644 --- a/tests/test_output.py +++ b/tests/test_output.py @@ -1,5 +1,4 @@ from hypothesis_auto import auto_pytest_magic - from isort import output auto_pytest_magic(output.with_comments, auto_allow_exceptions_=(ValueError,)) diff --git a/tests/test_parse.py b/tests/test_parse.py index efc3a22a4..f5846bd73 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -1,5 +1,4 @@ from hypothesis_auto import auto_pytest_magic - from isort import parse from isort.settings import Config diff --git a/tests/test_setuptools_command.py b/tests/test_setuptools_command.py index 634dd8877..cba43e437 100644 --- a/tests/test_setuptools_command.py +++ b/tests/test_setuptools_command.py @@ -2,7 +2,6 @@ from unittest.mock import MagicMock import pytest - from isort import setuptools_commands diff --git a/tests/test_wrap_modes.py b/tests/test_wrap_modes.py index fa35c5a86..a676e7924 100644 --- a/tests/test_wrap_modes.py +++ b/tests/test_wrap_modes.py @@ -1,5 +1,4 @@ from hypothesis_auto import auto_pytest_magic - from isort import wrap_modes auto_pytest_magic(wrap_modes.grid, auto_allow_exceptions_=(ValueError,)) From 11e2a74f5d379a68edaa064a0aeb70d8bcad9491 Mon Sep 17 00:00:00 2001 From: Jeremy Paige Date: Mon, 10 Feb 2020 11:17:27 -0800 Subject: [PATCH 0376/1439] issue-1125 remove documentation examples of two character flags --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 440f18c81..24dddc5b0 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ isort mypythonfile.py mypythonfile2.py or recursively: ```bash -isort -rc . +isort ``` *which is equivalent to:* @@ -138,7 +138,7 @@ Finally, to atomically run isort against a project, only applying changes if they don't introduce syntax errors do: ```bash -isort -rc --atomic . +isort --atomic . ``` (Note: this is disabled by default as it keeps isort from being able to @@ -609,7 +609,7 @@ formatted by running it with `-c`. Any files that contain incorrectly sorted and/or formatted imports will be outputted to `stderr`. ```bash -isort **/*.py -c -vb +isort **/*.py -c -v SUCCESS: /home/timothy/Projects/Open_Source/isort/isort_kate_plugin.py Everything Looks Good! ERROR: /home/timothy/Projects/Open_Source/isort/isort/isort.py Imports are incorrectly sorted. From f682e0bc4b8506a45846a74fe537917ba0ffd5bb Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 10 Feb 2020 11:39:30 -0800 Subject: [PATCH 0377/1439] Fix test case to be more explicit --- tests/test_format.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_format.py b/tests/test_format.py index 540287245..79b967019 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -5,7 +5,7 @@ import isort.format -auto_pytest_magic(isort.format.show_unified_diff) +auto_pytest_magic(isort.format.show_unified_diff, auto_allow_exceptions_=(UnicodeEncodeError,)) def test_ask_whether_to_apply_changes_to_file(): From 832b4e422323cdd3bba8d42ca6df30ddd5b9c65c Mon Sep 17 00:00:00 2001 From: sbtries Date: Mon, 10 Feb 2020 11:41:37 -0800 Subject: [PATCH 0378/1439] updated contributing steps to include tests.sh and done.sh --- docs/contributing/1.-contributing-guide.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/contributing/1.-contributing-guide.md b/docs/contributing/1.-contributing-guide.md index d1b1cd62e..abf98cc1a 100644 --- a/docs/contributing/1.-contributing-guide.md +++ b/docs/contributing/1.-contributing-guide.md @@ -25,6 +25,8 @@ Once you have verified that you system matches the base requirements you can sta `git clone https://github.com/$GITHUB_ACCOUNT/isort.git` 3. `cd isort 4. `poetry install` +5. `./scripts/test.sh` should yield Success: no issues found +6. `./scripts/done.sh` should yield a Safety report checking packages ## Making a contribution Congrats! You're now ready to make a contribution! Use the following as a guide to help you reach a successful pull-request: From 585caf9aa5bf5a262a71790fa2740809aa5fce5a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 10 Feb 2020 11:49:14 -0800 Subject: [PATCH 0379/1439] Skip io test on windows --- tests/test_io.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_io.py b/tests/test_io.py index b00225863..fa0ebdbaa 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -6,6 +6,7 @@ class TestFile: + @pytest.mark.skipif(sys.platform == "win32", reason="Can't run file encoding test in AppVeyor") def test_read(self, tmpdir): test_file_content = """# -*- encoding: ascii -*- From b68ba2e8137dbdddf7619071d8c7cde489a29bc5 Mon Sep 17 00:00:00 2001 From: Timothy Edmund Crosley Date: Mon, 10 Feb 2020 11:50:13 -0800 Subject: [PATCH 0380/1439] Delete Pipfile --- Pipfile | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 Pipfile diff --git a/Pipfile b/Pipfile deleted file mode 100644 index fd19f5655..000000000 --- a/Pipfile +++ /dev/null @@ -1,11 +0,0 @@ -[[source]] -name = "pypi" -url = "https://pypi.org/simple" -verify_ssl = true - -[dev-packages] - -[packages] - -[requires] -python_version = "2.7" From 2d81b7ed52ce750bfdda1f1ce9f635e05a39f09e Mon Sep 17 00:00:00 2001 From: Timothy Edmund Crosley Date: Mon, 10 Feb 2020 11:50:59 -0800 Subject: [PATCH 0381/1439] Delete Pipfile.lock --- Pipfile.lock | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 Pipfile.lock diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index 637b90ff5..000000000 --- a/Pipfile.lock +++ /dev/null @@ -1,20 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "ae4bdd7d4157baab65ae9d0e8389a6011e6b640995372c45ec81fa5d1ddfae9f" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "2.7" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": {}, - "develop": {} -} From 336fdc2901a40f66f37a7f91feb1dab8ad76749f Mon Sep 17 00:00:00 2001 From: Timothy Edmund Crosley Date: Mon, 10 Feb 2020 11:51:28 -0800 Subject: [PATCH 0382/1439] Update test_wrap_modes.py --- tests/test_wrap_modes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_wrap_modes.py b/tests/test_wrap_modes.py index a676e7924..fa35c5a86 100644 --- a/tests/test_wrap_modes.py +++ b/tests/test_wrap_modes.py @@ -1,4 +1,5 @@ from hypothesis_auto import auto_pytest_magic + from isort import wrap_modes auto_pytest_magic(wrap_modes.grid, auto_allow_exceptions_=(ValueError,)) From 62588ab0c56c38e5ae8e0e6ed6c40c7004575c0a Mon Sep 17 00:00:00 2001 From: Timothy Edmund Crosley Date: Mon, 10 Feb 2020 11:51:40 -0800 Subject: [PATCH 0383/1439] Update test_setuptools_command.py --- tests/test_setuptools_command.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_setuptools_command.py b/tests/test_setuptools_command.py index cba43e437..634dd8877 100644 --- a/tests/test_setuptools_command.py +++ b/tests/test_setuptools_command.py @@ -2,6 +2,7 @@ from unittest.mock import MagicMock import pytest + from isort import setuptools_commands From a0e5012ffc559301ffd26b87dd7df86519b5acaf Mon Sep 17 00:00:00 2001 From: Timothy Edmund Crosley Date: Mon, 10 Feb 2020 11:51:53 -0800 Subject: [PATCH 0384/1439] Update test_parse.py --- tests/test_parse.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_parse.py b/tests/test_parse.py index f5846bd73..efc3a22a4 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -1,4 +1,5 @@ from hypothesis_auto import auto_pytest_magic + from isort import parse from isort.settings import Config From de3cc4a94e7f6c30a79ad3b3fce8c8390dd69211 Mon Sep 17 00:00:00 2001 From: Timothy Edmund Crosley Date: Mon, 10 Feb 2020 11:52:04 -0800 Subject: [PATCH 0385/1439] Update test_output.py --- tests/test_output.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_output.py b/tests/test_output.py index 2aaaf877f..2457f70d7 100644 --- a/tests/test_output.py +++ b/tests/test_output.py @@ -1,4 +1,5 @@ from hypothesis_auto import auto_pytest_magic + from isort import output auto_pytest_magic(output.with_comments, auto_allow_exceptions_=(ValueError,)) From 7cbfff0dff3ceacee15dd3dde83ace5ffd9e4255 Mon Sep 17 00:00:00 2001 From: Timothy Edmund Crosley Date: Mon, 10 Feb 2020 11:52:19 -0800 Subject: [PATCH 0386/1439] Update test_main.py --- tests/test_main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_main.py b/tests/test_main.py index 95a6a7720..45fb0b8aa 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -3,6 +3,7 @@ import pytest from hypothesis_auto import auto_pytest_magic + from isort import main from isort.settings import DEFAULT_CONFIG From c16ecd50b25867d15b65bf3a45c7b5dd50fb54bc Mon Sep 17 00:00:00 2001 From: Timothy Edmund Crosley Date: Mon, 10 Feb 2020 11:52:30 -0800 Subject: [PATCH 0387/1439] Update test_io.py --- tests/test_io.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_io.py b/tests/test_io.py index 83f873f17..b00225863 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -1,6 +1,7 @@ from unittest.mock import MagicMock, patch import pytest + from isort import io From 565c665e29ad0684a590c2c6965044fc139ace34 Mon Sep 17 00:00:00 2001 From: Timothy Edmund Crosley Date: Mon, 10 Feb 2020 11:53:12 -0800 Subject: [PATCH 0388/1439] Update test_format.py --- tests/test_format.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_format.py b/tests/test_format.py index 7b918ad71..540287245 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -1,9 +1,10 @@ from unittest.mock import MagicMock, patch -import isort.format import pytest from hypothesis_auto import auto_pytest_magic +import isort.format + auto_pytest_magic(isort.format.show_unified_diff) From 084e23bd97451c5bfaf9e726f99fe63ffac2af58 Mon Sep 17 00:00:00 2001 From: Timothy Edmund Crosley Date: Mon, 10 Feb 2020 11:53:22 -0800 Subject: [PATCH 0389/1439] Update test_comments.py --- tests/test_comments.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_comments.py b/tests/test_comments.py index 1805f137c..b1b4ed7b4 100644 --- a/tests/test_comments.py +++ b/tests/test_comments.py @@ -1,4 +1,5 @@ from hypothesis_auto import auto_pytest_magic + from isort import comments auto_pytest_magic(comments.parse) From be6610e627276ac702c79a00d2816988f8e1e0b0 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 10 Feb 2020 11:57:03 -0800 Subject: [PATCH 0390/1439] Add Sarah Beth Tracy (@sbtries) to acknowledgements --- docs/contributing/4.-acknowledgements.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index df88916c1..b7d52575b 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -101,6 +101,7 @@ Documenters - Tim Graham (@timgraham) - Josh Soref (@jsoref) - Teg Khanna (@tegkhanna) +- Sarah Beth Tracy (@sbtries) -------------------------------------------- From dd8824d4d8572a0d97029778f5588f3d43819292 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 10 Feb 2020 12:01:18 -0800 Subject: [PATCH 0391/1439] Add missing sys import to test_io module --- tests/test_io.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_io.py b/tests/test_io.py index fa0ebdbaa..23a6214cc 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -1,3 +1,4 @@ +import sys from unittest.mock import MagicMock, patch import pytest From 446dd3c7ad5ded42c376da118a84738eb854027b Mon Sep 17 00:00:00 2001 From: Aaron Brown Date: Mon, 10 Feb 2020 14:17:34 -0800 Subject: [PATCH 0392/1439] Added Getting Started Info --- docs/contributing/1.-contributing-guide.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/contributing/1.-contributing-guide.md b/docs/contributing/1.-contributing-guide.md index d1b1cd62e..a64162a9e 100644 --- a/docs/contributing/1.-contributing-guide.md +++ b/docs/contributing/1.-contributing-guide.md @@ -17,14 +17,16 @@ Base System Requirements: - Python3.6+ - poetry - bash or a bash compatible shell (should be auto-installed on Linux / Mac) + - WSL users running Ubuntu may need to install Python's venv module even after installing Python. -Once you have verified that you system matches the base requirements you can start to get the project working by following these steps: +Once you have verified that your system matches the base requirements you can start to get the project working by following these steps: 1. [Fork the project on GitHub](https://github.com/timothycrosley/isort/fork). 2. Clone your fork to your local file system: `git clone https://github.com/$GITHUB_ACCOUNT/isort.git` -3. `cd isort +3. `cd isort` 4. `poetry install` + * Optionally, isolate poetry's installation from the rest of your system using the instructions on the poetry site here: https://python-poetry.org/docs/#installation ## Making a contribution Congrats! You're now ready to make a contribution! Use the following as a guide to help you reach a successful pull-request: From 92a625fb975171e479a24a621b3264d0cdccbe15 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 10 Feb 2020 14:45:13 -0800 Subject: [PATCH 0393/1439] Add Aaron Brown (@aaronvbrown) to acknowledgements --- docs/contributing/4.-acknowledgements.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index b7d52575b..f8d2aefd3 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -102,6 +102,7 @@ Documenters - Josh Soref (@jsoref) - Teg Khanna (@tegkhanna) - Sarah Beth Tracy (@sbtries) +- Aaron Brown (@aaronvbrown) -------------------------------------------- From 74c800a4cac8f14f02872fa0845838eb148f28f3 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 10 Feb 2020 14:50:59 -0800 Subject: [PATCH 0394/1439] Fix issue with as_imports and force_single_section --- isort/output.py | 6 +++--- tests/test_isort.py | 7 +++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/isort/output.py b/isort/output.py index 035224c94..8b753bcd7 100644 --- a/isort/output.py +++ b/isort/output.py @@ -33,14 +33,14 @@ def sorted_imports( sections: Iterable[str] = itertools.chain(parsed.sections, config.forced_separate) if config.no_sections: - parsed.imports["no_sections"] = {"straight": [], "from": {}} + parsed.imports["no_sections"] = {"straight": {}, "from": {}} base_sections: Tuple[str, ...] = () for section in sections: if section == "FUTURE": base_sections = ("FUTURE",) continue - parsed.imports["no_sections"]["straight"].extend( - parsed.imports[section].get("straight", []) + parsed.imports["no_sections"]["straight"].update( + parsed.imports[section].get("straight", {}) ) parsed.imports["no_sections"]["from"].update(parsed.imports[section].get("from", {})) sections = base_sections + ("no_sections",) diff --git a/tests/test_isort.py b/tests/test_isort.py index d3fc6a33d..6ab5bedf0 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -4920,6 +4920,13 @@ def test_no_sections_with_future(): assert SortImports(file_contents=test_input, no_sections=True).output == expected_output +def test_no_sections_with_as_import(): + """Test to ensure no_sections work with as import.""" + test_input = """import oumpy as np +import sympy +""" + assert SortImports(file_contents=test_input, no_sections=True).output == test_input + def test_no_lines_too_long(): """Test to ensure no lines end up too long. See issue: #1015""" test_input = """from package1 import first_package, \ From fb125bebae45b4c68e04b9fa78f176207c3714af Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 10 Feb 2020 15:01:15 -0800 Subject: [PATCH 0395/1439] Fix syntax error in test case --- tests/test_isort.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_isort.py b/tests/test_isort.py index 6ab5bedf0..88d4349a9 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -4927,6 +4927,7 @@ def test_no_sections_with_as_import(): """ assert SortImports(file_contents=test_input, no_sections=True).output == test_input + def test_no_lines_too_long(): """Test to ensure no lines end up too long. See issue: #1015""" test_input = """from package1 import first_package, \ From dfa1d0a2d4183df771f426a7c749a84a630b0979 Mon Sep 17 00:00:00 2001 From: Jeremy Paige Date: Mon, 10 Feb 2020 15:12:02 -0800 Subject: [PATCH 0396/1439] Add docker support for multiple python versions --- .dockerignore | 2 ++ Dockerfile | 10 ++++++++ docs/contributing/1.-contributing-guide.md | 27 ++++++++++++++++++++-- scripts/docker.sh | 12 ++++++++++ 4 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100755 scripts/docker.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..3e4b57aa7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +.git* +docs/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..bf4ae2bfc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +ARG VERSION=3 +FROM python:$VERSION + +RUN mkdir /isort +WORKDIR /isort +COPY . /isort + +RUN python3 -m pip install poetry && poetry install + +CMD /isort/scripts/done.sh diff --git a/docs/contributing/1.-contributing-guide.md b/docs/contributing/1.-contributing-guide.md index 6503fc1b1..1455f903d 100644 --- a/docs/contributing/1.-contributing-guide.md +++ b/docs/contributing/1.-contributing-guide.md @@ -28,7 +28,26 @@ Once you have verified that your system matches the base requirements you can st 4. `poetry install` * Optionally, isolate poetry's installation from the rest of your system using the instructions on the poetry site here: https://python-poetry.org/docs/#installation 5. `./scripts/test.sh` should yield Success: no issues found -6. `./scripts/done.sh` should yield a Safety report checking packages +6. `./scripts/clean.sh` should yield a Safety report checking packages + +### Docker development + +If you would instead like to develop using Docker, the only local requirement is docker. +See the [docker docs](https://docs.docker.com/get-started/) if you have not used docker before. + +Once you have the docker daemon running and have cloned the repository, you can get started by following these steps: + +1. `cd isort` +2. `./scripts/docker.sh` + +A local test cycle might look like the following: + +1. `docker build ./ -t isort:latest` +2. `docker run isort` +3. if #2 fails, debug, save, and goto #1; `docker run -it isort bash` will get you into the failed environment +4. `./scripts/docker.sh` +5. if #4 fails, debug, save and goto #1; you may need to specify a different `--build-arg VERSION=$VER` +6. congrats! you are probably ready to push a contribution ## Making a contribution Congrats! You're now ready to make a contribution! Use the following as a guide to help you reach a successful pull-request: @@ -40,7 +59,11 @@ Congrats! You're now ready to make a contribution! Use the following as a guide 2. Create an issue branch for your local work `git checkout -b issue/$ISSUE-NUMBER`. 3. Do your magic here. 4. Ensure your code matches the [HOPE-8 Coding Standard](https://github.com/hugapi/HOPE/blob/master/all/HOPE-8--Style-Guide-for-Hug-Code.md#hope-8----style-guide-for-hug-code) used by the project. -5. Submit a pull request to the main project repository via GitHub. +5. Run tests locally to make sure everything is still working + `./scripts/done.sh` + _Or if you are using Docker_ + `docker run isort:latest` +6. Submit a pull request to the main project repository via GitHub. Thanks for the contribution! It will quickly get reviewed, and, once accepted, will result in your name being added to the acknowledgments list :). diff --git a/scripts/docker.sh b/scripts/docker.sh new file mode 100755 index 000000000..364bb5703 --- /dev/null +++ b/scripts/docker.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -ux + +result=0 + +for ver in {3.6,3.7,3.8}; do + # latest tag will override after each build, leaving only the newest python version tagged + docker build ./ --build-arg VERSION=$ver -t "isort:$ver" -t "isort:latest" && docker run "isort:$ver" + result=$(( $? + $result )) +done + +exit $result From b99039b3b71c70df46d12c548a303886114a1042 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 11 Feb 2020 17:06:24 -0800 Subject: [PATCH 0397/1439] Work around bug in pip-shims newer versions, pinning to older versions for now --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index df61e2bea..3488d6931 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ toml = {version = "*", optional = true} pip-api = {version = "*", optional = true} [tool.poetry.extras] -pipfile = ["pipreqs", "tomlkit", "requirementslib"] +pipfile = ["pipreqs", "tomlkit", "requirementslib", "pip-shims<=0.3.4"] pyproject = ["toml"] requirements = ["pipreqs", "pip-api"] xdg_home = ["appdirs"] @@ -67,6 +67,7 @@ pip_api = "^0.0.12" numpy = "^1.16.0" pylama = "^7.7" pip = "^20.0.2" +pip-shims = "<=0.3.4" [tool.poetry.scripts] isort = "isort.main:main" From b7e7e61dcfe1ebb9f48bc68e5aece50205aca466 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 11 Feb 2020 17:37:08 -0800 Subject: [PATCH 0398/1439] Fix issue #710: Allow directly specifying settings file in addition to settings path --- isort/main.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/isort/main.py b/isort/main.py index 1862b30b1..5563fb90c 100644 --- a/isort/main.py +++ b/isort/main.py @@ -387,8 +387,11 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: parser.add_argument( "--sp", "--settings-path", + "--settings-file", + "--settings", dest="settings_path", - help="Explicitly set the settings path instead of auto determining based on file location.", + help="Explicitly set the settings path or file instead of auto determining " + "based on file location.", ) parser.add_argument( "-t", @@ -535,12 +538,13 @@ def main(argv: Optional[Sequence[str]] = None) -> None: show_config: bool = arguments.pop("show_config", 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)) - ) - if not os.path.isdir(arguments["settings_path"]): + if os.path.isfile(arguments["settings_path"]): + arguments["settings_file"] = os.path.abspath(arguments["settings_path"]) + arguments["settings_path"] = os.path.dirname(arguments["settings_file"]) + elif not os.path.isdir(arguments["settings_path"]): warn(f"settings_path dir does not exist: {arguments['settings_path']}") + else: + arguments["settings_path"] = os.path.abspath(arguments["settings_path"]) if "virtual_env" in arguments: venv = arguments["virtual_env"] From 74cd72f389b189d65c6e5cc7f281f7be08046544 Mon Sep 17 00:00:00 2001 From: Santiago Castro Date: Wed, 12 Feb 2020 00:41:02 -0500 Subject: [PATCH 0399/1439] Add test for line length bug --- tests/test_isort.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_isort.py b/tests/test_isort.py index 88d4349a9..bfd1445b3 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -511,6 +511,18 @@ def test_output_modes() -> None: "from third_party import lib21, lib22\n" ) + test_input = ( + "def a():\n" + " from allennlp.modules.text_field_embedders.basic_text_field_embedder" + " import BasicTextFieldEmbedder" + ) + test_output = SortImports(file_contents=test_input, line_length=100).output + assert test_output == ( + "def a():\n" + " from allennlp.modules.text_field_embedders.basic_text_field_embedder import \\\n" + " BasicTextFieldEmbedder" + ) + def test_qa_comment_case() -> None: test_input = "from veryveryveryveryveryveryveryveryveryveryvery import X # NOQA" From 209a16e07fb6178b277101f501619f8a7791e777 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 11 Feb 2020 23:19:18 -0800 Subject: [PATCH 0400/1439] Fix nested imports wrapping --- isort/api.py | 8 +++++++- isort/main.py | 4 ++-- isort/settings.py | 17 +++++++++++++++-- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/isort/api.py b/isort/api.py index 3d34d835c..369528e1d 100644 --- a/isort/api.py +++ b/isort/api.py @@ -312,9 +312,15 @@ def sort_imports( import_section = line_separator.join( line.lstrip() for line in import_section.split(line_separator) ) + out_config = Config( + config=config, line_length=max(config.line_length - len(indent), 0) + ) + else: + out_config = config + sorted_import_section = output.sorted_imports( parse.file_contents(import_section, config=config), - config, + out_config, extension, import_type="cimport" if cimports else "import", ) diff --git a/isort/main.py b/isort/main.py index 5563fb90c..6cfd8892f 100644 --- a/isort/main.py +++ b/isort/main.py @@ -611,8 +611,8 @@ def main(argv: Optional[Sequence[str]] = None) -> None: ) else: # https://github.com/python/typeshed/pull/2814 - attempt_iterator = ( # type: ignore - sort_imports( + attempt_iterator = ( + sort_imports( # type: ignore file_name, check=check, ask_to_apply=ask_to_apply, diff --git a/isort/settings.py b/isort/settings.py index a292e598b..9427ee1ef 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -14,7 +14,7 @@ from distutils.util import strtobool as _as_bool from functools import lru_cache from pathlib import Path -from typing import Any, Callable, Dict, FrozenSet, Iterable, List, Set, Tuple +from typing import Any, Callable, Dict, FrozenSet, Iterable, List, Optional, Set, Tuple from warnings import warn from . import stdlibs @@ -204,7 +204,19 @@ def __post_init__(self): class Config(_Config): - def __init__(self, settings_file: str = "", settings_path: str = "", **config_overrides): + def __init__( + self, + settings_file: str = "", + settings_path: str = "", + config: Optional[_Config] = None, + **config_overrides, + ): + if config: + config_vars = vars(config) + config_vars.update(config_overrides) + config_vars["py_version"] = config_vars["py_version"].replace("py", "") + return super().__init__(**config_vars) # type: ignore + sources: List[Dict[str, Any]] = [_DEFAULT_SETTINGS] config_settings: Dict[str, Any] @@ -274,6 +286,7 @@ def __init__(self, settings_file: str = "", settings_path: str = "", **config_ov # Remove any config values that are used for creating config object but # aren't defined in dataclass combined_config.pop("source", None) + combined_config.pop("sources", None) if known_other: for known_key in known_other.keys(): combined_config.pop(f"{KNOWN_PREFIX}{known_key}", None) From 1dcba6ed2596aeb03c86ce1bcc0315a24058e1fc Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 11 Feb 2020 23:27:49 -0800 Subject: [PATCH 0401/1439] Add poetry lock, add latest version of mypy --- .gitignore | 1 - poetry.lock | 1322 ++++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- 3 files changed, 1323 insertions(+), 2 deletions(-) create mode 100644 poetry.lock diff --git a/.gitignore b/.gitignore index d16f7c744..f7516853f 100644 --- a/.gitignore +++ b/.gitignore @@ -70,7 +70,6 @@ pip-selfcheck.json # Python3 Venv Files .venv/ pyvenv.cfg -poetry.lock # mypy .mypy_cache diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 000000000..9d4bbc0b0 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1322 @@ +[[package]] +category = "main" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +name = "appdirs" +optional = false +python-versions = "*" +version = "1.4.3" + +[[package]] +category = "dev" +description = "Disable App Nap on OS X 10.9" +marker = "sys_platform == \"darwin\"" +name = "appnope" +optional = false +python-versions = "*" +version = "0.1.0" + +[[package]] +category = "dev" +description = "Better dates & times for Python" +name = "arrow" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.15.2" + +[package.dependencies] +python-dateutil = "*" + +[[package]] +category = "dev" +description = "Atomic file writes." +name = "atomicwrites" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.3.0" + +[[package]] +category = "main" +description = "Classes Without Boilerplate" +name = "attrs" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "19.3.0" + +[[package]] +category = "dev" +description = "Specifications for callback functions passed in to an API" +name = "backcall" +optional = false +python-versions = "*" +version = "0.1.0" + +[[package]] +category = "dev" +description = "Security oriented static analyser for python code." +name = "bandit" +optional = false +python-versions = "*" +version = "1.6.2" + +[package.dependencies] +GitPython = ">=1.0.1" +PyYAML = ">=3.13" +colorama = ">=0.3.9" +six = ">=1.10.0" +stevedore = ">=1.20.0" + +[[package]] +category = "dev" +description = "Ultra-lightweight pure Python package to check if a file is binary or text." +name = "binaryornot" +optional = false +python-versions = "*" +version = "0.4.4" + +[package.dependencies] +chardet = ">=3.0.2" + +[[package]] +category = "dev" +description = "The uncompromising code formatter." +name = "black" +optional = false +python-versions = ">=3.6" +version = "18.9b0" + +[package.dependencies] +appdirs = "*" +attrs = ">=17.4.0" +click = ">=6.5" +toml = ">=0.9.4" + +[[package]] +category = "main" +description = "A decorator for caching properties in classes." +name = "cached-property" +optional = false +python-versions = "*" +version = "1.5.1" + +[[package]] +category = "main" +description = "Python package for providing Mozilla's CA Bundle." +name = "certifi" +optional = false +python-versions = "*" +version = "2019.9.11" + +[[package]] +category = "main" +description = "Universal encoding detector for Python 2 and 3" +name = "chardet" +optional = false +python-versions = "*" +version = "3.0.4" + +[[package]] +category = "dev" +description = "Composable command line interface toolkit" +name = "click" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "7.0" + +[[package]] +category = "main" +description = "Cross-platform colored terminal text." +name = "colorama" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.4.1" + +[[package]] +category = "dev" +description = "A command-line utility that creates projects from project templates, e.g. creating a Python package project from a Python package project template." +name = "cookiecutter" +optional = false +python-versions = "*" +version = "1.6.0" + +[package.dependencies] +binaryornot = ">=0.2.0" +click = ">=5.0" +future = ">=0.15.2" +jinja2 = ">=2.7" +jinja2-time = ">=0.1.0" +poyo = ">=0.1.0" +requests = ">=2.18.0" +whichcraft = ">=0.4.0" + +[[package]] +category = "dev" +description = "Code coverage measurement for Python" +name = "coverage" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, <4" +version = "4.5.4" + +[[package]] +category = "dev" +description = "Allows you to maintain all the necessary cruft for packaging and building projects separate from the code you intentionally write. Built on-top of CookieCutter." +name = "cruft" +optional = false +python-versions = ">=3.6,<4.0" +version = "1.1.2" + +[package.dependencies] +cookiecutter = ">=1.6,<2.0" +examples = ">=1.0,<2.0" +gitpython = ">=3.0,<4.0" +hug = ">=2.6,<3.0" + +[[package]] +category = "dev" +description = "A backport of the dataclasses module for Python 3.6" +marker = "python_version < \"3.7\"" +name = "dataclasses" +optional = false +python-versions = "*" +version = "0.6" + +[[package]] +category = "dev" +description = "Better living through Python with decorators" +name = "decorator" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*" +version = "4.4.0" + +[[package]] +category = "main" +description = "Distribution utilities" +name = "distlib" +optional = false +python-versions = "*" +version = "0.2.9.post0" + +[[package]] +category = "main" +description = "Pythonic argument parser, that will make you smile" +name = "docopt" +optional = false +python-versions = "*" +version = "0.6.2" + +[[package]] +category = "dev" +description = "A parser for Python dependency files" +name = "dparse" +optional = false +python-versions = "*" +version = "0.4.1" + +[package.dependencies] +packaging = "*" +pyyaml = "*" +six = "*" + +[[package]] +category = "dev" +description = "Discover and load entry points from installed packages." +name = "entrypoints" +optional = false +python-versions = ">=2.7" +version = "0.3" + +[[package]] +category = "dev" +description = "Tests and Documentation Done by Example." +name = "examples" +optional = false +python-versions = ">=3.6,<4.0" +version = "1.0.1" + +[package.dependencies] +pydantic = ">=0.32.2,<0.33.0" + +[[package]] +category = "dev" +description = "An unladen web framework for building APIs and app backends." +name = "falcon" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.0.0" + +[[package]] +category = "main" +description = "Return the first true value of an iterable." +name = "first" +optional = false +python-versions = "*" +version = "2.0.2" + +[[package]] +category = "dev" +description = "the modular source code checker: pep8, pyflakes and co" +name = "flake8" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "3.7.8" + +[package.dependencies] +entrypoints = ">=0.3.0,<0.4.0" +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.5.0,<2.6.0" +pyflakes = ">=2.1.0,<2.2.0" + +[[package]] +category = "dev" +description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." +name = "flake8-bugbear" +optional = false +python-versions = ">=3.5" +version = "19.8.0" + +[package.dependencies] +attrs = "*" +flake8 = ">=3.0.0" + +[[package]] +category = "dev" +description = "Polyfill package for Flake8 plugins" +name = "flake8-polyfill" +optional = false +python-versions = "*" +version = "1.0.2" + +[package.dependencies] +flake8 = "*" + +[[package]] +category = "dev" +description = "Clean single-source support for Python 3 and 2" +name = "future" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +version = "0.18.1" + +[[package]] +category = "dev" +description = "Git Object Database" +name = "gitdb2" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.0.6" + +[package.dependencies] +smmap2 = ">=2.0.0" + +[[package]] +category = "dev" +description = "Python Git Library" +name = "gitpython" +optional = false +python-versions = ">=3.0, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "3.0.3" + +[package.dependencies] +gitdb2 = ">=2.0.0" + +[[package]] +category = "dev" +description = "An HTML Minifier" +name = "htmlmin" +optional = false +python-versions = "*" +version = "0.1.12" + +[[package]] +category = "dev" +description = "A Python framework that makes developing APIs as simple as possible, but no simpler." +name = "hug" +optional = false +python-versions = ">=3.5" +version = "2.6.0" + +[package.dependencies] +falcon = "2.0.0" +requests = "*" + +[[package]] +category = "dev" +description = "A library for property based testing" +name = "hypothesis" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "4.41.2" + +[package.dependencies] +attrs = ">=19.2.0" + +[[package]] +category = "dev" +description = "Extends Hypothesis to add fully automatic testing of type annotated functions" +name = "hypothesis-auto" +optional = false +python-versions = ">=3.6,<4.0" +version = "1.1.4" + +[package.dependencies] +hypothesis = ">=4.36" +pydantic = ">=0.32.2" + +[[package]] +category = "main" +description = "Internationalized Domain Names in Applications (IDNA)" +name = "idna" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.8" + +[[package]] +category = "main" +description = "Read metadata from Python packages" +name = "importlib-metadata" +optional = false +python-versions = ">=2.7,!=3.0,!=3.1,!=3.2,!=3.3" +version = "0.23" + +[package.dependencies] +zipp = ">=0.5" + +[[package]] +category = "dev" +description = "IPython: Productive Interactive Computing" +name = "ipython" +optional = false +python-versions = ">=3.5" +version = "7.8.0" + +[package.dependencies] +appnope = "*" +backcall = "*" +colorama = "*" +decorator = "*" +jedi = ">=0.10" +pexpect = "*" +pickleshare = "*" +prompt-toolkit = ">=2.0.0,<2.1.0" +pygments = "*" +setuptools = ">=18.5" +traitlets = ">=4.2" + +[[package]] +category = "dev" +description = "Vestigial utilities from IPython" +name = "ipython-genutils" +optional = false +python-versions = "*" +version = "0.2.0" + +[[package]] +category = "dev" +description = "An autocompletion tool for Python that can be used for text editors." +name = "jedi" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.15.1" + +[package.dependencies] +parso = ">=0.5.0" + +[[package]] +category = "dev" +description = "A very fast and expressive template engine." +name = "jinja2" +optional = false +python-versions = "*" +version = "2.10.3" + +[package.dependencies] +MarkupSafe = ">=0.23" + +[[package]] +category = "dev" +description = "Jinja2 Extension for Dates and Times" +name = "jinja2-time" +optional = false +python-versions = "*" +version = "0.2.0" + +[package.dependencies] +arrow = "*" +jinja2 = "*" + +[[package]] +category = "dev" +description = "JavaScript minifier." +name = "jsmin" +optional = false +python-versions = "*" +version = "2.2.2" + +[[package]] +category = "dev" +description = "Python LiveReload is an awesome tool for web developers" +name = "livereload" +optional = false +python-versions = "*" +version = "2.6.1" + +[package.dependencies] +six = "*" +tornado = "*" + +[[package]] +category = "dev" +description = "A super-fast templating language that borrows the best ideas from the existing templating languages." +name = "mako" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.1.0" + +[package.dependencies] +MarkupSafe = ">=0.9.2" + +[[package]] +category = "dev" +description = "Python implementation of Markdown." +name = "markdown" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" +version = "3.1.1" + +[package.dependencies] +setuptools = ">=36" + +[[package]] +category = "dev" +description = "Safely add untrusted strings to HTML/XML markup." +name = "markupsafe" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +version = "1.1.1" + +[[package]] +category = "dev" +description = "McCabe checker, plugin for flake8" +name = "mccabe" +optional = false +python-versions = "*" +version = "0.6.1" + +[[package]] +category = "dev" +description = "Project documentation with Markdown." +name = "mkdocs" +optional = false +python-versions = ">=2.7.9,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +version = "1.0.4" + +[package.dependencies] +Jinja2 = ">=2.7.1" +Markdown = ">=2.3.1" +PyYAML = ">=3.10" +click = ">=3.3" +livereload = ">=2.5.1" +tornado = ">=5.0" + +[[package]] +category = "dev" +description = "A Material Design theme for MkDocs" +name = "mkdocs-material" +optional = false +python-versions = "*" +version = "4.4.3" + +[package.dependencies] +Pygments = ">=2.2" +mkdocs = ">=1" +mkdocs-minify-plugin = ">=0.2" +pymdown-extensions = ">=4.11" + +[[package]] +category = "dev" +description = "An MkDocs plugin to minify HTML and/or JS files prior to being written to disk" +name = "mkdocs-minify-plugin" +optional = false +python-versions = ">=2.7" +version = "0.2.1" + +[package.dependencies] +htmlmin = ">=0.1.4" +jsmin = ">=2.2.2" +mkdocs = ">=1.0.4" + +[[package]] +category = "main" +description = "More routines for operating on iterables, beyond itertools" +name = "more-itertools" +optional = false +python-versions = ">=3.4" +version = "7.2.0" + +[[package]] +category = "dev" +description = "Optional static typing for Python" +name = "mypy" +optional = false +python-versions = ">=3.5" +version = "0.761" + +[package.dependencies] +mypy-extensions = ">=0.4.3,<0.5.0" +typed-ast = ">=1.4.0,<1.5.0" +typing-extensions = ">=3.7.4" + +[[package]] +category = "dev" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +name = "mypy-extensions" +optional = false +python-versions = "*" +version = "0.4.3" + +[[package]] +category = "dev" +description = "NumPy is the fundamental package for array computing with Python." +name = "numpy" +optional = false +python-versions = ">=3.5" +version = "1.17.3" + +[[package]] +category = "main" +description = "Ordered Multivalue Dictionary" +name = "orderedmultidict" +optional = false +python-versions = "*" +version = "1.0.1" + +[package.dependencies] +six = ">=1.8.0" + +[[package]] +category = "main" +description = "Core utilities for Python packages" +name = "packaging" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "19.2" + +[package.dependencies] +pyparsing = ">=2.0.2" +six = "*" + +[[package]] +category = "dev" +description = "A Python Parser" +name = "parso" +optional = false +python-versions = "*" +version = "0.5.1" + +[[package]] +category = "dev" +description = "Python Build Reasonableness" +name = "pbr" +optional = false +python-versions = "*" +version = "5.4.3" + +[[package]] +category = "dev" +description = "A simple program and library to auto generate API documentation for Python modules." +name = "pdocs" +optional = false +python-versions = ">=3.6,<4.0" +version = "1.0.1" + +[package.dependencies] +Mako = ">=1.1,<2.0" +Markdown = ">=3.0.0,<4.0.0" +hug = ">=2.6,<3.0" + +[[package]] +category = "main" +description = "Wrappers to build Python packages using PEP 517 hooks" +name = "pep517" +optional = false +python-versions = "*" +version = "0.7.0" + +[package.dependencies] +importlib_metadata = "*" +toml = "*" +zipp = "*" + +[[package]] +category = "dev" +description = "Backport of PEP 562." +name = "pep562" +optional = false +python-versions = "*" +version = "1.0" + +[[package]] +category = "dev" +description = "Check PEP-8 naming conventions, plugin for flake8" +name = "pep8-naming" +optional = false +python-versions = "*" +version = "0.8.2" + +[package.dependencies] +flake8-polyfill = ">=1.0.2,<2" + +[[package]] +category = "dev" +description = "Pexpect allows easy control of interactive console applications." +marker = "sys_platform != \"win32\"" +name = "pexpect" +optional = false +python-versions = "*" +version = "4.7.0" + +[package.dependencies] +ptyprocess = ">=0.5" + +[[package]] +category = "dev" +description = "Tiny 'shelve'-like database with concurrency support" +name = "pickleshare" +optional = false +python-versions = "*" +version = "0.7.5" + +[[package]] +category = "main" +description = "An unofficial, importable pip API" +name = "pip-api" +optional = false +python-versions = ">=2.7,!=3.0,!=3.1,!=3.2,!=3.3" +version = "0.0.12" + +[package.dependencies] +pip = "*" + +[[package]] +category = "main" +description = "Compatibility shims for pip versions 8 thru current." +name = "pip-shims" +optional = false +python-versions = ">=2.6,!=3.0,!=3.1,!=3.2,!=3.3" +version = "0.3.3" + +[package.dependencies] +pip = "*" +setuptools = "*" +six = "*" +wheel = "*" + +[[package]] +category = "dev" +description = "" +name = "pipfile" +optional = false +python-versions = "*" +version = "0.0.2" + +[package.dependencies] +toml = "*" + +[[package]] +category = "main" +description = "Pip requirements.txt generator based on imports in project" +name = "pipreqs" +optional = false +python-versions = "*" +version = "0.4.9" + +[package.dependencies] +docopt = "*" +yarg = "*" + +[[package]] +category = "main" +description = "Structured Pipfile and Pipfile.lock models." +name = "plette" +optional = false +python-versions = ">=2.6,!=3.0,!=3.1,!=3.2,!=3.3" +version = "0.2.2" + +[package.dependencies] +six = "*" +tomlkit = "*" + +[[package]] +category = "dev" +description = "plugin and hook calling mechanisms for python" +name = "pluggy" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.13.0" + +[package.dependencies] +[package.dependencies.importlib-metadata] +python = "<3.8" +version = ">=0.12" + +[[package]] +category = "dev" +description = "Your Project with Great Documentation" +name = "portray" +optional = false +python-versions = ">=3.6,<4.0" +version = "1.3.1" + +[package.dependencies] +GitPython = ">=3.0,<4.0" +hug = ">=2.6,<3.0" +mkdocs = ">=1.0,<2.0" +mkdocs-material = ">=4.4,<5.0" +pdocs = ">=1.0,<2.0" +pymdown-extensions = ">=6.0,<7.0" +toml = ">=0.10.0,<0.11.0" +yaspin = "0.15.0" + +[[package]] +category = "dev" +description = "A lightweight YAML Parser for Python. 🐓" +name = "poyo" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.5.0" + +[[package]] +category = "dev" +description = "Library for building powerful interactive command lines in Python" +name = "prompt-toolkit" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +version = "2.0.10" + +[package.dependencies] +six = ">=1.9.0" +wcwidth = "*" + +[[package]] +category = "dev" +description = "Run a subprocess in a pseudo terminal" +marker = "sys_platform != \"win32\"" +name = "ptyprocess" +optional = false +python-versions = "*" +version = "0.6.0" + +[[package]] +category = "dev" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +name = "py" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.8.0" + +[[package]] +category = "dev" +description = "Python style guide checker" +name = "pycodestyle" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.5.0" + +[[package]] +category = "dev" +description = "Data validation and settings management using python 3.6 type hinting" +name = "pydantic" +optional = false +python-versions = ">=3.6" +version = "0.32.2" + +[package.dependencies] +[package.dependencies.dataclasses] +python = "<3.7" +version = ">=0.6" + +[[package]] +category = "dev" +description = "Python docstring style checker" +name = "pydocstyle" +optional = false +python-versions = ">=3.5" +version = "5.0.2" + +[package.dependencies] +snowballstemmer = "*" + +[[package]] +category = "dev" +description = "passive checker of Python programs" +name = "pyflakes" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.1.1" + +[[package]] +category = "dev" +description = "Pygments is a syntax highlighting package written in Python." +name = "pygments" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.4.2" + +[[package]] +category = "dev" +description = "pylama -- Code audit tool for python" +name = "pylama" +optional = false +python-versions = "*" +version = "7.7.1" + +[package.dependencies] +mccabe = ">=0.5.2" +pycodestyle = ">=2.3.1" +pydocstyle = ">=2.0.0" +pyflakes = ">=1.5.0" + +[[package]] +category = "dev" +description = "Extension pack for Python Markdown." +name = "pymdown-extensions" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" +version = "6.1" + +[package.dependencies] +Markdown = ">=3.0.1" +pep562 = "*" + +[[package]] +category = "main" +description = "Python parsing module" +name = "pyparsing" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +version = "2.4.2" + +[[package]] +category = "dev" +description = "pytest: simple powerful testing with Python" +name = "pytest" +optional = false +python-versions = ">=3.5" +version = "5.2.1" + +[package.dependencies] +atomicwrites = ">=1.0" +attrs = ">=17.4.0" +colorama = "*" +more-itertools = ">=4.0.0" +packaging = "*" +pluggy = ">=0.12,<1.0" +py = ">=1.5.0" +wcwidth = "*" + +[package.dependencies.importlib-metadata] +python = "<3.8" +version = ">=0.12" + +[[package]] +category = "dev" +description = "Pytest plugin for measuring coverage." +name = "pytest-cov" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.8.1" + +[package.dependencies] +coverage = ">=4.4" +pytest = ">=3.6" + +[[package]] +category = "dev" +description = "Thin-wrapper around the mock package for easier use with py.test" +name = "pytest-mock" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.11.1" + +[package.dependencies] +pytest = ">=2.7" + +[[package]] +category = "dev" +description = "Extensions to the standard Python datetime module" +name = "python-dateutil" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +version = "2.8.0" + +[package.dependencies] +six = ">=1.5" + +[[package]] +category = "dev" +description = "YAML parser and emitter for Python" +name = "pyyaml" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "5.1.2" + +[[package]] +category = "main" +description = "Python HTTP for Humans." +name = "requests" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.22.0" + +[package.dependencies] +certifi = ">=2017.4.17" +chardet = ">=3.0.2,<3.1.0" +idna = ">=2.5,<2.9" +urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" + +[[package]] +category = "main" +description = "A tool for converting between pip-style and pipfile requirements." +name = "requirementslib" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +version = "1.5.3" + +[package.dependencies] +appdirs = "*" +attrs = ">=18.2" +cached-property = "*" +distlib = ">=0.2.8" +first = "*" +orderedmultidict = "*" +packaging = ">=19.0" +pep517 = ">=0.5.0" +pip-shims = ">=0.3.2" +plette = "*" +requests = "*" +setuptools = ">=40.8" +six = ">=1.11.0" +tomlkit = ">=0.5.3" +vistir = ">=0.3.1" + +[[package]] +category = "dev" +description = "Safety checks your installed dependencies for known security vulnerabilities." +name = "safety" +optional = false +python-versions = "*" +version = "1.8.5" + +[package.dependencies] +Click = ">=6.0" +dparse = ">=0.4.1" +packaging = "*" +requests = "*" +setuptools = "*" + +[[package]] +category = "main" +description = "Python 2 and 3 compatibility utilities" +name = "six" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*" +version = "1.12.0" + +[[package]] +category = "dev" +description = "A pure Python implementation of a sliding window memory map manager" +name = "smmap2" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.0.5" + +[[package]] +category = "dev" +description = "This package provides 26 stemmers for 25 languages generated from Snowball algorithms." +name = "snowballstemmer" +optional = false +python-versions = "*" +version = "2.0.0" + +[[package]] +category = "dev" +description = "Manage dynamic plugins for Python applications" +name = "stevedore" +optional = false +python-versions = "*" +version = "1.31.0" + +[package.dependencies] +pbr = ">=2.0.0,<2.1.0 || >2.1.0" +six = ">=1.10.0" + +[[package]] +category = "main" +description = "Python Library for Tom's Obvious, Minimal Language" +name = "toml" +optional = false +python-versions = "*" +version = "0.10.0" + +[[package]] +category = "main" +description = "Style preserving TOML library" +name = "tomlkit" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.5.3" + +[[package]] +category = "dev" +description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +name = "tornado" +optional = false +python-versions = ">= 3.5" +version = "6.0.3" + +[[package]] +category = "dev" +description = "Traitlets Python config system" +name = "traitlets" +optional = false +python-versions = "*" +version = "4.3.3" + +[package.dependencies] +decorator = "*" +ipython-genutils = "*" +six = "*" + +[[package]] +category = "dev" +description = "a fork of Python 2 and 3 ast modules with type comment support" +name = "typed-ast" +optional = false +python-versions = "*" +version = "1.4.0" + +[[package]] +category = "dev" +description = "Type Hints for Python" +name = "typing" +optional = false +python-versions = "*" +version = "3.7.4.1" + +[[package]] +category = "dev" +description = "Backported and Experimental Type Hints for Python 3.5+" +name = "typing-extensions" +optional = false +python-versions = "*" +version = "3.7.4" + +[package.dependencies] +typing = ">=3.7.4" + +[[package]] +category = "main" +description = "HTTP library with thread-safe connection pooling, file post, and more." +name = "urllib3" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" +version = "1.25.6" + +[[package]] +category = "main" +description = "Miscellaneous utilities for dealing with filesystems, paths, projects, subprocesses, and more." +name = "vistir" +optional = false +python-versions = ">=2.7,!=3.0,!=3.1,!=3.2,!=3.3" +version = "0.4.3" + +[package.dependencies] +colorama = ">=0.3.4" +requests = "*" +six = "*" + +[[package]] +category = "dev" +description = "Find dead code" +name = "vulture" +optional = false +python-versions = "*" +version = "1.1" + +[[package]] +category = "dev" +description = "Measures number of Terminal column cells of wide-character codes" +name = "wcwidth" +optional = false +python-versions = "*" +version = "0.1.7" + +[[package]] +category = "main" +description = "A built-package format for Python." +name = "wheel" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.33.6" + +[[package]] +category = "dev" +description = "This package provides cross-platform cross-python shutil.which functionality." +name = "whichcraft" +optional = false +python-versions = "*" +version = "0.6.1" + +[[package]] +category = "main" +description = "A semi hard Cornish cheese, also queries PyPI (PyPI client)" +name = "yarg" +optional = false +python-versions = "*" +version = "0.1.9" + +[package.dependencies] +requests = "*" + +[[package]] +category = "dev" +description = "Yet Another Terminal Spinner" +name = "yaspin" +optional = false +python-versions = "*" +version = "0.15.0" + +[[package]] +category = "main" +description = "Backport of pathlib-compatible object wrapper for zip files" +name = "zipp" +optional = false +python-versions = ">=2.7" +version = "0.6.0" + +[package.dependencies] +more-itertools = "*" + +[extras] +pipfile = ["pipreqs", "tomlkit", "requirementslib"] +pyproject = ["toml"] +requirements = ["pipreqs", "pip-api"] +xdg_home = ["appdirs"] + +[metadata] +content-hash = "3447cba840ff626794cdabd519349e9c13499276018784dc7739c57aae0356e5" +python-versions = "^3.6" + +[metadata.hashes] +appdirs = ["9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", "d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"] +appnope = ["5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0", "8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71"] +arrow = ["10257c5daba1a88db34afa284823382f4963feca7733b9107956bed041aff24f", "c2325911fcd79972cf493cfd957072f9644af8ad25456201ae1ede3316576eb4"] +atomicwrites = ["03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", "75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"] +attrs = ["08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", "f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"] +backcall = ["38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4", "bbbf4b1e5cd2bdb08f915895b51081c041bac22394fdfcfdfbe9f14b77c08bf2"] +bandit = ["336620e220cf2d3115877685e264477ff9d9abaeb0afe3dc7264f55fa17a3952", "41e75315853507aa145d62a78a2a6c5e3240fe14ee7c601459d0df9418196065"] +binaryornot = ["359501dfc9d40632edc9fac890e19542db1a287bbcfa58175b66658392018061", "b8b71173c917bddcd2c16070412e369c3ed7f0528926f70cac18a6c97fd563e4"] +black = ["817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739", "e030a9a28f542debc08acceb273f228ac422798e5215ba2a791a6ddeaaca22a5"] +cached-property = ["3a026f1a54135677e7da5ce819b0c690f156f37976f3e30c5430740725203d7f", "9217a59f14a5682da7c4b8829deadbfc194ac22e9908ccf7c8820234e80a1504"] +certifi = ["e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50", "fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef"] +chardet = ["84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"] +click = ["2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", "5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"] +colorama = ["05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", "f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"] +cookiecutter = ["1316a52e1c1f08db0c9efbf7d876dbc01463a74b155a0d83e722be88beda9a3e", "ed8f54a8fc79b6864020d773ce11539b5f08e4617f353de1f22d23226f6a0d36"] +coverage = ["08907593569fe59baca0bf152c43f3863201efb6113ecb38ce7e97ce339805a6", "0be0f1ed45fc0c185cfd4ecc19a1d6532d72f86a2bac9de7e24541febad72650", "141f08ed3c4b1847015e2cd62ec06d35e67a3ac185c26f7635f4406b90afa9c5", "19e4df788a0581238e9390c85a7a09af39c7b539b29f25c89209e6c3e371270d", "23cc09ed395b03424d1ae30dcc292615c1372bfba7141eb85e11e50efaa6b351", "245388cda02af78276b479f299bbf3783ef0a6a6273037d7c60dc73b8d8d7755", "331cb5115673a20fb131dadd22f5bcaf7677ef758741312bee4937d71a14b2ef", "386e2e4090f0bc5df274e720105c342263423e77ee8826002dcffe0c9533dbca", "3a794ce50daee01c74a494919d5ebdc23d58873747fa0e288318728533a3e1ca", "60851187677b24c6085248f0a0b9b98d49cba7ecc7ec60ba6b9d2e5574ac1ee9", "63a9a5fc43b58735f65ed63d2cf43508f462dc49857da70b8980ad78d41d52fc", "6b62544bb68106e3f00b21c8930e83e584fdca005d4fffd29bb39fb3ffa03cb5", "6ba744056423ef8d450cf627289166da65903885272055fb4b5e113137cfa14f", "7494b0b0274c5072bddbfd5b4a6c6f18fbbe1ab1d22a41e99cd2d00c8f96ecfe", "826f32b9547c8091679ff292a82aca9c7b9650f9fda3e2ca6bf2ac905b7ce888", "93715dffbcd0678057f947f496484e906bf9509f5c1c38fc9ba3922893cda5f5", "9a334d6c83dfeadae576b4d633a71620d40d1c379129d587faa42ee3e2a85cce", "af7ed8a8aa6957aac47b4268631fa1df984643f07ef00acd374e456364b373f5", "bf0a7aed7f5521c7ca67febd57db473af4762b9622254291fbcbb8cd0ba5e33e", "bf1ef9eb901113a9805287e090452c05547578eaab1b62e4ad456fcc049a9b7e", "c0afd27bc0e307a1ffc04ca5ec010a290e49e3afbe841c5cafc5c5a80ecd81c9", "dd579709a87092c6dbee09d1b7cfa81831040705ffa12a1b248935274aee0437", "df6712284b2e44a065097846488f66840445eb987eb81b3cc6e4149e7b6982e1", "e07d9f1a23e9e93ab5c62902833bf3e4b1f65502927379148b6622686223125c", "e2ede7c1d45e65e209d6093b762e98e8318ddeff95317d07a27a2140b80cfd24", "e4ef9c164eb55123c62411f5936b5c2e521b12356037b6e1c2617cef45523d47", "eca2b7343524e7ba246cab8ff00cab47a2d6d54ada3b02772e908a45675722e2", "eee64c616adeff7db37cc37da4180a3a5b6177f5c46b187894e633f088fb5b28", "ef824cad1f980d27f26166f86856efe11eff9912c4fed97d3804820d43fa550c", "efc89291bd5a08855829a3c522df16d856455297cf35ae827a37edac45f466a7", "fa964bae817babece5aa2e8c1af841bebb6d0b9add8e637548809d040443fee0", "ff37757e068ae606659c28c3bd0d923f9d29a85de79bf25b2b34b148473b5025"] +cruft = ["05a224047a5c705a1f8e51ea6cab5cc42570a1ee3cdad36eef5a1611c9f9876e", "3864a6775381a69b36720401a9db1714c20af51190c8378fcd5de4560aeb9282"] +dataclasses = ["454a69d788c7fda44efd71e259be79577822f5e3f53f029a22d08004e951dc9f", "6988bd2b895eef432d562370bb707d540f32f7360ab13da45340101bc2307d84"] +decorator = ["86156361c50488b84a3f148056ea716ca587df2f0de1d34750d35c21312725de", "f069f3a01830ca754ba5258fde2278454a0b5b79e0d7f5c13b3b97e57d4acff6"] +distlib = ["ecb3d0e4f71d0fa7f38db6bcc276c7c9a1c6638a516d726495934a553eb3fbe0"] +docopt = ["49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"] +dparse = ["00a5fdfa900629e5159bf3600d44905b333f4059a3366f28e0dbd13eeab17b19", "cef95156fa0adedaf042cd42f9990974bec76f25dfeca4dc01f381a243d5aa5b"] +entrypoints = ["589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", "c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"] +examples = ["9dba261e3929f3274328beeadcb2212180e12dbdc033eb0f35c3666708055fc4", "bf3c16a072e186815a78ebf85177bd1313cdaf34298baf0a8be30957147dd47d"] +falcon = ["18157af2a4fc3feedf2b5dcc6196f448639acf01c68bc33d4d5a04c3ef87f494", "24adcd2b29a8ffa9d552dc79638cd21736a3fb04eda7d102c6cebafdaadb88ad", "54f2cb4b687035b2a03206dbfc538055cc48b59a953187b0458aa1b574d47b53", "59d1e8c993b9a37ea06df9d72cf907a46cc8063b30717cdac2f34d1658b6f936", "733033ec80c896e30a43ab3e776856096836787197a44eb21022320a61311983", "74cf1d18207381c665b9e6292d65100ce146d958707793174b03869dc6e614f4", "95bf6ce986c1119aef12c9b348f4dee9c6dcc58391bdd0bc2b0bf353c2b15986", "9712975adcf8c6e12876239085ad757b8fdeba223d46d23daef82b47658f83a9", "a5ebb22a04c9cc65081938ee7651b4e3b4d2a28522ea8ec04c7bdd2b3e9e8cd8", "aa184895d1ad4573fbfaaf803563d02f019ebdf4790e41cc568a330607eae439", "e3782b7b92fefd46a6ad1fd8fe63fe6c6f1b7740a95ca56957f48d1aee34b357", "e9efa0791b5d9f9dd9689015ea6bce0a27fcd5ecbcd30e6d940bffa4f7f03389", "eea593cf466b9c126ce667f6d30503624ef24459f118c75594a69353b6c3d5fc", "f93351459f110b4c1ee28556aef9a791832df6f910bea7b3f616109d534df06b"] +first = ["8d8e46e115ea8ac652c76123c0865e3ff18372aef6f03c22809ceefcea9dec86", "ff285b08c55f8c97ce4ea7012743af2495c9f1291785f163722bd36f6af6d3bf"] +flake8 = ["19241c1cbc971b9962473e4438a2ca19749a7dd002dd1a946eaba171b4114548", "8e9dfa3cecb2400b3738a42c54c3043e821682b9c840b0448c0503f781130696"] +flake8-bugbear = ["d8c466ea79d5020cb20bf9f11cf349026e09517a42264f313d3f6fddb83e0571", "ded4d282778969b5ab5530ceba7aa1a9f1b86fa7618fc96a19a1d512331640f8"] +flake8-polyfill = ["12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9", "e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda"] +future = ["858e38522e8fd0d3ce8f0c1feaf0603358e366d5403209674c7b617fa0c24093"] +gitdb2 = ["1b6df1433567a51a4a9c1a5a0de977aa351a405cc56d7d35f3388bad1f630350", "96bbb507d765a7f51eb802554a9cfe194a174582f772e0d89f4e87288c288b7b"] +gitpython = ["631263cc670aa56ce3d3c414cf0fe2e840f2e913514b138ea28d88a477bbcd21", "6e97b9f0954807f30c2dd8e3165731ed6c477a1b365f194b69d81d7940a08332"] +htmlmin = ["50c1ef4630374a5d723900096a961cff426dff46b48f34d194a81bbe14eca178"] +hug = ["a04cd002614e8c788d58e5dcd5b932a62e2d2c33d4bd69fd8a3d7ce4e5cf5545", "bbe1390642e324130f60e4f7978bd6ea152d6957ed6e769ad6bc314ddaed5b9b"] +hypothesis = ["6847df3ffb4aa52798621dd007e6b61dbcf2d76c30ba37dc2699720e2c734b7a", "acd47600deb55e9c2c98de6deef23384160ed0fdaafb6753146e556c077d3c78"] +hypothesis-auto = ["5e2c2fb09dc09842512d80630bb792359a1d33d2c0473ad47ee23da0be9e32b1", "fea8560c4522c0fd490ed8cc17e420b95dabebb11b9b334c59bf2d768839015f"] +idna = ["c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", "ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"] +importlib-metadata = ["aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26", "d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af"] +ipython = ["c4ab005921641e40a68e405e286e7a1fcc464497e14d81b6914b4fd95e5dee9b", "dd76831f065f17bddd7eaa5c781f5ea32de5ef217592cf019e34043b56895aa1"] +ipython-genutils = ["72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8", "eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"] +jedi = ["786b6c3d80e2f06fd77162a07fed81b8baa22dde5d62896a790a331d6ac21a27", "ba859c74fa3c966a22f2aeebe1b74ee27e2a462f56d3f5f7ca4a59af61bfe42e"] +jinja2 = ["74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f", "9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"] +jinja2-time = ["d14eaa4d315e7688daa4969f616f226614350c48730bfa1692d2caebd8c90d40", "d3eab6605e3ec8b7a0863df09cc1d23714908fa61aa6986a845c20ba488b4efa"] +jsmin = ["b6df99b2cd1c75d9d342e4335b535789b8da9107ec748212706ef7bbe5c2553b"] +livereload = ["78d55f2c268a8823ba499305dcac64e28ddeb9a92571e12d543cd304faf5817b", "89254f78d7529d7ea0a3417d224c34287ebfe266b05e67e51facaf82c27f0f66"] +mako = ["a36919599a9b7dc5d86a7a8988f23a9a3a3d083070023bab23d64f7f1d1e0a4b"] +markdown = ["2e50876bcdd74517e7b71f3e7a76102050edec255b3983403f1a63e7c8a41e7a", "56a46ac655704b91e5b7e6326ce43d5ef72411376588afa1dd90e881b83c7e8c"] +markupsafe = ["00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", "09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", "09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", "1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", "24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", "29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", "43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", "46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", "500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", "535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", "62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", "6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", "717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", "79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", "7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", "88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", "8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", "98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", "9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", "9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", "ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", "b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", "b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", "b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", "ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", "c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", "cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", "e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"] +mccabe = ["ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", "dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"] +mkdocs = ["17d34329aad75d5de604b9ed4e31df3a4d235afefdc46ce7b1964fddb2e1e939", "8cc8b38325456b9e942c981a209eaeb1e9f3f77b493ad755bfef889b9c8d356a"] +mkdocs-material = ["a5246b550299d00a135a3f739e70ac6db73d7127480f0fecbda113d0095a674a", "e4a9ac73db7c65fdae1dbd248091e4b0a3f5db3e6bf87a46bb457db013a045e4"] +mkdocs-minify-plugin = ["3000a5069dd0f42f56a8aaf7fd5ea1222c67487949617e39585d6b6434b074b6", "d54fdd5be6843dd29fd7af2f7fdd20a9eb4db46f1f6bed914e03b2f58d2d488e"] +more-itertools = ["409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", "92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4"] +mypy = ["0a9a45157e532da06fe56adcfef8a74629566b607fa2c1ac0122d1ff995c748a", "2c35cae79ceb20d47facfad51f952df16c2ae9f45db6cb38405a3da1cf8fc0a7", "4b9365ade157794cef9685791032521233729cb00ce76b0ddc78749abea463d2", "53ea810ae3f83f9c9b452582261ea859828a9ed666f2e1ca840300b69322c474", "634aef60b4ff0f650d3e59d4374626ca6153fcaff96ec075b215b568e6ee3cb0", "7e396ce53cacd5596ff6d191b47ab0ea18f8e0ec04e15d69728d530e86d4c217", "7eadc91af8270455e0d73565b8964da1642fe226665dd5c9560067cd64d56749", "7f672d02fffcbace4db2b05369142e0506cdcde20cea0e07c7c2171c4fd11dd6", "85baab8d74ec601e86134afe2bcccd87820f79d2f8d5798c889507d1088287bf", "87c556fb85d709dacd4b4cb6167eecc5bbb4f0a9864b69136a0d4640fdc76a36", "a6bd44efee4dc8c3324c13785a9dc3519b3ee3a92cada42d2b57762b7053b49b", "c6d27bd20c3ba60d5b02f20bd28e20091d6286a699174dfad515636cb09b5a72", "e2bb577d10d09a2d8822a042a23b8d62bc3b269667c9eb8e60a6edfa000211b1", "f97a605d7c8bc2c6d1172c2f0d5a65b24142e11a58de689046e62c2d632ca8c1"] +mypy-extensions = ["090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", "2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"] +numpy = ["0b0dd8f47fb177d00fa6ef2d58783c4f41ad3126b139c91dd2f7c4b3fdf5e9a5", "25ffe71f96878e1da7e014467e19e7db90ae7d4e12affbc73101bcf61785214e", "26efd7f7d755e6ca966a5c0ac5a930a87dbbaab1c51716ac26a38f42ecc9bc4b", "28b1180c758abf34a5c3fea76fcee66a87def1656724c42bb14a6f9717a5bdf7", "2e418f0a59473dac424f888dd57e85f77502a593b207809211c76e5396ae4f5c", "30c84e3a62cfcb9e3066f25226e131451312a044f1fe2040e69ce792cb7de418", "4650d94bb9c947151737ee022b934b7d9a845a7c76e476f3e460f09a0c8c6f39", "4dd830a11e8724c9c9379feed1d1be43113f8bcce55f47ea7186d3946769ce26", "4f2a2b279efde194877aff1f76cf61c68e840db242a5c7169f1ff0fd59a2b1e2", "62d22566b3e3428dfc9ec972014c38ed9a4db4f8969c78f5414012ccd80a149e", "669795516d62f38845c7033679c648903200980d68935baaa17ac5c7ae03ae0c", "75fcd60d682db3e1f8fbe2b8b0c6761937ad56d01c1dc73edf4ef2748d5b6bc4", "9395b0a41e8b7e9a284e3be7060db9d14ad80273841c952c83a5afc241d2bd98", "9e37c35fc4e9410093b04a77d11a34c64bf658565e30df7cbe882056088a91c1", "a0678793096205a4d784bd99f32803ba8100f639cf3b932dc63b21621390ea7e", "b46554ad4dafb2927f88de5a1d207398c5385edbb5c84d30b3ef187c4a3894d8", "c867eeccd934920a800f65c6068acdd6b87e80d45cd8c8beefff783b23cdc462", "dd0667f5be56fb1b570154c2c0516a528e02d50da121bbbb2cbb0b6f87f59bc2", "de2b1c20494bdf47f0160bd88ed05f5e48ae5dc336b8de7cfade71abcc95c0b9", "f1df7b2b7740dd777571c732f98adb5aad5450aee32772f1b39249c8a50386f6", "ffca69e29079f7880c5392bf675eb8b4146479d976ae1924d01cd92b04cccbcc"] +orderedmultidict = ["04070bbb5e87291cc9bfa51df413677faf2141c73c61d2a5f7b26bea3cd882ad", "43c839a17ee3cdd62234c47deca1a8508a3f2ca1d0678a3bf791c87cf84adbf3"] +packaging = ["28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47", "d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108"] +parso = ["63854233e1fadb5da97f2744b6b24346d2750b85965e7e399bec1620232797dc", "666b0ee4a7a1220f65d367617f2cd3ffddff3e205f3f16a0284df30e774c2a9c"] +pbr = ["2c8e420cd4ed4cec4e7999ee47409e876af575d4c35a45840d59e8b5f3155ab8", "b32c8ccaac7b1a20c0ce00ce317642e6cf231cf038f9875e0280e28af5bf7ac9"] +pdocs = ["23a0346f56c08ab5701ca9b14630aa1f0f32f1c29ee7efcfc8bc512cd272f89b", "9c0d24fdc0e0c537be8f2418edb4f1075da46a0d749b17ea20b74e7e60124f49"] +pep517 = ["d283181fdb83fb698556cd3a4ebde5bb352b59242574e6ca56a95118774da374", "fac83aa4c3b73adc84cb2a295f1f5bd5b9a13946ebd1339ba3b33ce287165c88"] +pep562 = ["58cb1cc9ee63d93e62b4905a50357618d526d289919814bea1f0da8f53b79395", "d2a48b178ebf5f8dd31709cc26a19808ef794561fa2fe50ea01ea2bad4d667ef"] +pep8-naming = ["01cb1dab2f3ce9045133d08449f1b6b93531dceacb9ef04f67087c11c723cea9", "0ec891e59eea766efd3059c3d81f1da304d858220678bdc351aab73c533f2fbb"] +pexpect = ["2094eefdfcf37a1fdbfb9aa090862c1a4878e5c7e0e7e7088bdb511c558e5cd1", "9e2c1fd0e6ee3a49b28f95d4b33bc389c89b20af6a1255906e90ff1262ce62eb"] +pickleshare = ["87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca", "9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"] +pip-api = ["5266d9c8c9585e6fdeaf5d78f17b4e68dd2d657103fb24b80b629f70fac26712", "6eeb656fb1a4df791c40c16e22e6107025b6a1ee912e2dbc66d684bafc8ef57c"] +pip-shims = ["0162d846bd60c7b1feb4e1336541a3e661eb6a1eff4b9ea0d759780f8e0a2936", "d73372b9fa3a10e73f057fced70d39bb82df16a1ee3ded027fed0bb8d7c0ff97"] +pipfile = ["f7d9f15de8b660986557eb3cc5391aa1a16207ac41bc378d03f414762d36c984"] +pipreqs = ["2dfa21631cb68a97515e222d6f0033b5bfb75823567ca2195d528efb18c97990", "cec6eecc4685967b27eb386037565a737d036045f525b9eb314631a68d60e4bc"] +plette = ["c0e3553c1e581d8423daccbd825789c6e7f29b7d9e00e5331b12e1642a1a26d3", "dde5d525cf5f0cbad4d938c83b93db17887918daf63c13eafed257c4f61b07b4"] +pluggy = ["0db4b7601aae1d35b4a033282da476845aa19185c1e6964b25cf324b5e4ec3e6", "fa5fa1622fa6dd5c030e9cad086fa19ef6a0cf6d7a2d12318e10cb49d6d68f34"] +portray = ["28e0b21ad611cd460369c89cdfe67054e20354c6c5c03950c250edab6d7a99d7", "ac1a651f97ab04556732b64fdb10dcba4f9a006dd023471039743b16d28a7a61"] +poyo = ["3e2ca8e33fdc3c411cd101ca395668395dd5dc7ac775b8e809e3def9f9fe041a", "e26956aa780c45f011ca9886f044590e2d8fd8b61db7b1c1cf4e0869f48ed4dd"] +prompt-toolkit = ["46642344ce457641f28fc9d1c9ca939b63dadf8df128b86f1b9860e59c73a5e4", "e7f8af9e3d70f514373bf41aa51bc33af12a6db3f71461ea47fea985defb2c31", "f15af68f66e664eaa559d4ac8a928111eebd5feda0c11738b5998045224829db"] +ptyprocess = ["923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0", "d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"] +py = ["64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", "dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"] +pycodestyle = ["95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", "e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"] +pydantic = ["18598557f0d9ab46173045910ed50458c4fb4d16153c23346b504d7a5b679f77", "6a9335c968e13295430a208487e74d69fef40168b72dea8d975765d14e2da660", "6f5eb88fe4c21380aa064b7d249763fc6306f0b001d7e7d52d80866d1afc9ed3", "bc6c6a78647d7a65a493e1107572d993f26a652c49183201e3c7d23924bf7311", "e1a63b4e6bf8820833cb6fa239ffbe8eec57ccdd7d66359eff20e68a83c1deeb", "ede2d65ae33788d4e26e12b330b4a32c53cb14131c65bca3a59f037c73f6ee7a"] +pydocstyle = ["da7831660b7355307b32778c4a0dbfb137d89254ef31a2b2978f50fc0b4d7586", "f4f5d210610c2d153fae39093d44224c17429e2ad7da12a8b419aba5c2f614b5"] +pyflakes = ["17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", "d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"] +pygments = ["71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127", "881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297"] +pylama = ["9bae53ef9c1a431371d6a8dca406816a60d547147b60a4934721898f553b7d8f", "fd61c11872d6256b019ef1235be37b77c922ef37ac9797df6bd489996dddeb15"] +pymdown-extensions = ["24c1a0afbae101c4e2b2675ff4dd936470a90133f93398b9cbe0c855e2d2ec10", "960486dea995f1759dfd517aa140b3d851cd7b44d4c48d276fd2c74fc4e1bce9"] +pyparsing = ["6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80", "d9338df12903bbf5d65a0e4e87c2161968b10d2e489652bb47001d82a9b028b4"] +pytest = ["7e4800063ccfc306a53c461442526c5571e1462f61583506ce97e4da6a1d88c8", "ca563435f4941d0cb34767301c27bc65c510cb82e90b9ecf9cb52dc2c63caaa0"] +pytest-cov = ["cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b", "cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626"] +pytest-mock = ["34520283d459cdf1d0dbb58a132df804697f1b966ecedf808bbf3d255af8f659", "f1ab8aefe795204efe7a015900296d1719e7bf0f4a0558d71e8599da1d1309d0"] +python-dateutil = ["7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", "c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e"] +pyyaml = ["0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", "01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", "5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", "5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", "7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", "7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", "87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", "9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", "a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", "b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", "b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", "bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", "f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8"] +requests = ["11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", "9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"] +requirementslib = ["50731ac1052473e4c7df59a44a1f3aa20f32e687110bc05d73c3b4109eebc23d", "8b594ab8b6280ee97cffd68fc766333345de150124d5b76061dd575c3a21fe5a"] +safety = ["0a3a8a178a9c96242b224f033ee8d1d130c0448b0e6622d12deaf37f6c3b4e59", "5059f3ffab3648330548ea9c7403405bbfaf085b11235770825d14c58f24cb78"] +six = ["3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", "d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"] +smmap2 = ["0555a7bf4df71d1ef4218e4807bbf9b201f910174e6e08af2e138d4e517b4dde", "29a9ffa0497e7f2be94ca0ed1ca1aa3cd4cf25a1f6b4f5f87f74b46ed91d609a"] +snowballstemmer = ["209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0", "df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"] +stevedore = ["01d9f4beecf0fbd070ddb18e5efb10567801ba7ef3ddab0074f54e3cd4e91730", "e0739f9739a681c7a1fda76a102b65295e96a144ccdb552f2ae03c5f0abe8a14"] +toml = ["229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", "235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", "f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"] +tomlkit = ["d6506342615d051bc961f70bfcfa3d29b6616cc08a3ddfd4bc24196f16fd4ec2", "f077456d35303e7908cc233b340f71e0bec96f63429997f38ca9272b7d64029e"] +tornado = ["349884248c36801afa19e342a77cc4458caca694b0eda633f5878e458a44cb2c", "398e0d35e086ba38a0427c3b37f4337327231942e731edaa6e9fd1865bbd6f60", "4e73ef678b1a859f0cb29e1d895526a20ea64b5ffd510a2307b5998c7df24281", "559bce3d31484b665259f50cd94c5c28b961b09315ccd838f284687245f416e5", "abbe53a39734ef4aba061fca54e30c6b4639d3e1f59653f0da37a0003de148c7", "c845db36ba616912074c5b1ee897f8e0124df269468f25e4fe21fe72f6edd7a9", "c9399267c926a4e7c418baa5cbe91c7d1cf362d505a1ef898fde44a07c9dd8a5"] +traitlets = ["70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44", "d023ee369ddd2763310e4c3eae1ff649689440d4ae59d7485eb4cfbbe3e359f7"] +typed-ast = ["1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161", "18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e", "262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e", "2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0", "354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c", "48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47", "4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631", "630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4", "66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34", "71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b", "7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2", "838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e", "95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a", "bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233", "cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1", "d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36", "d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d", "d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", "fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66", "ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12"] +typing = ["91dfe6f3f706ee8cc32d38edbbf304e9b7583fb37108fef38229617f8b3eba23", "c8cabb5ab8945cd2f54917be357d134db9cc1eb039e59d1606dc1e60cb1d9d36", "f38d83c5a7a7086543a0f649564d661859c5146a85775ab90c0d2f93ffaa9714"] +typing-extensions = ["2ed632b30bb54fc3941c382decfd0ee4148f5c591651c9272473fea2c6397d95", "b1edbbf0652660e32ae780ac9433f4231e7339c7f9a8057d0f042fcbcea49b87", "d8179012ec2c620d3791ca6fe2bf7979d979acdbef1fca0bc56b37411db682ed"] +urllib3 = ["3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398", "9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86"] +vistir = ["2166e3148a67c438c9e3edbba0cde153d42dec6e3bf5d8f4624feb27686c0990", "3a0529b4b6c2e842fd19b5ceaa95b6c9201321314825c110406d4af3331a0709"] +vulture = ["20e73154efc9c6d2445663bc2f7fbf79b6c562f2b78cafc0ec0463b38610f42d", "a72834a8c9cf254f6ba0a64e9053b800e05df8391b1724702a19b00d7d849a43"] +wcwidth = ["3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", "f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c"] +wheel = ["10c9da68765315ed98850f8e048347c3eb06dd81822dc2ab1d4fde9dc9702646", "f4da1763d3becf2e2cd92a14a7c920f0f00eca30fdde9ea992c836685b9faf28"] +whichcraft = ["acdbb91b63d6a15efbd6430d1d7b2d36e44a71697e93e19b7ded477afd9fce87", "deda9266fbb22b8c64fd3ee45c050d61139cd87419765f588e37c8d23e236dd9"] +yarg = ["4f9cebdc00fac946c9bf2783d634e538a71c7d280a4d806d45fd4dc0ef441492", "55695bf4d1e3e7f756496c36a69ba32c40d18f821e38f61d028f6049e5e15911"] +yaspin = ["0ee4668936d0053de752c9a4963929faa3a832bd0ba823877d27855592dc80aa", "5a938bdc7bab353fd8942d0619d56c6b5159a80997dc1c387a479b39e6dc9391"] +zipp = ["3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", "f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335"] diff --git a/pyproject.toml b/pyproject.toml index 3712a8376..182d6b02e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ bandit = "^1.6" safety = "^1.8" flake8-bugbear = "^19.8" black = {version = "^18.3-alpha.0", allow-prereleases = true} -mypy = "^0.730.0" +mypy = "^0.761.0" ipython = "^7.7" pytest = "^5.0" pytest-cov = "^2.7" From 994e437cae4f7458e1f3cb98f5ed4e50ff8c70a4 Mon Sep 17 00:00:00 2001 From: Santiago Castro Date: Wed, 12 Feb 2020 16:15:48 -0500 Subject: [PATCH 0402/1439] Add test --- tests/test_isort.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_isort.py b/tests/test_isort.py index bfd1445b3..59e843585 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -523,6 +523,19 @@ def test_output_modes() -> None: " BasicTextFieldEmbedder" ) + test_input = ( + "class A:\n" + " def a():\n" + " from allennlp.common.registrable import Registrable" + " # import here to avoid circular imports\n" + "\n\n" + "class B:\n" + " def b():\n" + " from allennlp.common.registrable import Registrable" + " # import here to avoid circular imports\n" + ) + test_output = SortImports(file_contents=test_input, line_length=100).output + assert test_output == test_input def test_qa_comment_case() -> None: test_input = "from veryveryveryveryveryveryveryveryveryveryvery import X # NOQA" From 78ceb74e131722b4bc6318e2d0f9584676aad706 Mon Sep 17 00:00:00 2001 From: Santiago Castro Date: Wed, 12 Feb 2020 16:18:26 -0500 Subject: [PATCH 0403/1439] Make a copy of the Config variables --- isort/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/settings.py b/isort/settings.py index 9427ee1ef..8a661c3e1 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -212,7 +212,7 @@ def __init__( **config_overrides, ): if config: - config_vars = vars(config) + config_vars = vars(config).copy() config_vars.update(config_overrides) config_vars["py_version"] = config_vars["py_version"].replace("py", "") return super().__init__(**config_vars) # type: ignore From 94523d25a5f03c3563915e352285c8cfb8a78b3e Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 12 Feb 2020 13:32:34 -0800 Subject: [PATCH 0404/1439] Fix stdout flag --- isort/main.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/isort/main.py b/isort/main.py index 6cfd8892f..52e061084 100644 --- a/isort/main.py +++ b/isort/main.py @@ -573,6 +573,7 @@ def main(argv: Optional[Sequence[str]] = None) -> None: filter_files = config_dict.pop("filter_files", False) check = config_dict.pop("check", False) show_diff = config_dict.pop("show_diff", False) + write_to_stdout = config_dict.pop("write_to_stdout", False) config = Config(**config_dict) if show_config: pprint(config.__dict__) @@ -604,7 +605,7 @@ def main(argv: Optional[Sequence[str]] = None) -> None: sort_imports, check=check, ask_to_apply=ask_to_apply, - show_diff=show_diff, + write_to_stdout=write_to_stdout, **config_dict, ), file_names, @@ -617,6 +618,7 @@ def main(argv: Optional[Sequence[str]] = None) -> None: check=check, ask_to_apply=ask_to_apply, show_diff=show_diff, + write_to_stdout=write_to_stdout, **config_dict, ) for file_name in file_names From 588623564e00a09c4c5d8175b1b5dca17e9f4451 Mon Sep 17 00:00:00 2001 From: Timothy Edmund Crosley Date: Wed, 12 Feb 2020 13:36:56 -0800 Subject: [PATCH 0405/1439] Add missing new line between functions (Pep8 requirement) to test_isort --- tests/test_isort.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_isort.py b/tests/test_isort.py index 59e843585..4aadcc9e3 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -537,6 +537,7 @@ def test_output_modes() -> None: test_output = SortImports(file_contents=test_input, line_length=100).output assert test_output == test_input + def test_qa_comment_case() -> None: test_input = "from veryveryveryveryveryveryveryveryveryveryvery import X # NOQA" test_output = SortImports( From 07e4e01389c21cc5c177231ef4595f52542375ca Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 13 Feb 2020 14:10:28 -0800 Subject: [PATCH 0406/1439] Add test to ensure issue #1117 is fixed --- tests/test_isort.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_isort.py b/tests/test_isort.py index 4aadcc9e3..2a881c2ff 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -2220,18 +2220,21 @@ def test_pyproject_conf_file(tmpdir) -> None: "[tool.isort]\n" "lines_between_types=1\n" 'known_common="nose"\n' + 'known_first_party="foo"\n' 'import_heading_common="Common Library"\n' 'import_heading_stdlib="Standard Library"\n' 'sections="FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER,COMMON"\n' "include_trailing_comma = true\n" ) - test_input = "import os\nfrom nose import *\nimport nose\nfrom os import path" + test_input = "import os\nfrom nose import *\nimport nose\nfrom os import path\nimport foo" correct_output = ( "# Standard Library\n" "import os\n" "\n" "from os import path\n" "\n" + "import foo\n" + "\n" "# Common Library\n" "import nose\n" "\n" From 8ba2707de62f93141e789c15af89c999b5466779 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 13 Feb 2020 14:22:28 -0800 Subject: [PATCH 0407/1439] Test confirming issue #1005 is fixed --- tests/test_isort.py | 71 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/tests/test_isort.py b/tests/test_isort.py index 4aadcc9e3..d6781eac7 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -4971,3 +4971,74 @@ def test_no_lines_too_long(): SortImports(file_contents=test_input, line_length=25, multi_line_output=2).output == expected_output ) + + +def test_python_future_category(): + """Test to ensure a manual python future category will work as needed to install aliases + + see: Issue #1005 + """ + test_input = """from __future__ import absolute_import + +from future import standard_library + +standard_library.install_aliases() + +import os +import re +import time + +from logging.handlers import SysLogHandler + +from builtins import len, object, str + +from katlogger import log_formatter, log_rollover + +from .query_elastic import QueryElastic +""" + expected_output = """from __future__ import absolute_import + +from future import standard_library + +standard_library.install_aliases() + +# Python Standard Library +import os +import re +import time + +from builtins import len, object, str +from logging.handlers import SysLogHandler + +# CAM Packages +from katlogger import log_formatter, log_rollover + +# Explicitly Local +from .query_elastic import QueryElastic +""" + assert ( + SortImports( + file_contents=test_input, + force_grid_wrap=False, + include_trailing_comma=True, + indent=4, + line_length=90, + multi_line_output=3, + lines_between_types=1, + sections=[ + "FUTURE_LIBRARY", + "FUTURE_THIRDPARTY", + "STDLIB", + "THIRDPARTY", + "FIRSTPARTY", + "LOCALFOLDER", + ], + import_heading_stdlib="Python Standard Library", + import_heading_thirdparty="Third Library", + import_heading_firstparty="CAM Packages", + import_heading_localfolder="Explicitly Local", + known_first_party=["katlogger"], + known_future_thirdparty=["future"], + ).output + == expected_output + ) From a0dbeff56efc2a36760de93e948066c5450ffa40 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 14 Feb 2020 23:34:51 -0800 Subject: [PATCH 0408/1439] Update tagline to read more smoothly --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 24dddc5b0..fab614ca8 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ _________________ [Read Latest Documentation](https://timothycrosley.github.io/isort/) - [Browse GitHub Code Repository](https://github.com/timothycrosley/isort/) _________________ -isort your imports for you, so you don't have to. +isort your imports, so you don't have to. isort is a Python utility / library to sort imports alphabetically, and automatically separated into sections and by type. It provides a command line From 9706e1515856f76b2ef1ad40cedb545f4e6eb30f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 14 Feb 2020 23:39:30 -0800 Subject: [PATCH 0409/1439] Make CLI tagline consistent --- isort/logo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/logo.py b/isort/logo.py index aefbbac88..6377d8686 100644 --- a/isort/logo.py +++ b/isort/logo.py @@ -7,7 +7,7 @@ | |\__ \/\_\/| | | |_ |_|\___/\___/\_/ \_/ - isort your Python imports for you so you don't have to. + isort your imports, so you don't have to. VERSION {__version__} """ From 53c68890b77bfb8ceed3c107aeb6681e2bf3d90d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 14 Feb 2020 23:41:32 -0800 Subject: [PATCH 0410/1439] Fix tagline in image caption --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fab614ca8..57e3434bc 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![isort - isort your imports for you, so you don't have to](https://raw.githubusercontent.com/timothycrosley/isort/develop/art/logo_large.png)](https://timothycrosley.github.io/isort/) +[![isort - isort your imports, so you don't have to.](https://raw.githubusercontent.com/timothycrosley/isort/develop/art/logo_large.png)](https://timothycrosley.github.io/isort/) ------------------------------------------------------------------------ From 8993cf97d08d7cb876e799cf9987b12f86fa8c74 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 14 Feb 2020 23:49:04 -0800 Subject: [PATCH 0411/1439] Update logo to use improved tagline --- art/logo_large.png | Bin 25882 -> 25441 bytes art/logo_large.xcf | Bin 173363 -> 167833 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/art/logo_large.png b/art/logo_large.png index 88b1509c624b28f0393561baa9f39552558b4f68..e52548c4edefb0d190823832afa5271b5fde3f24 100644 GIT binary patch literal 25441 zcmb4qcQl*t->}kFOVJut6zx~-qIT_7yY?!Is=ZgNSZx)x_uezah^<1<(xPfc#AuA# zNsJ;!tT+1my?=bqdEV!o_j2UozV2&%_DZy_wkib~BN-795rw+i3wWmeJR}9EYblei5L1yp zo6*N!q$eVJM5O*g!7yMJ zi}KH-L!4j!Im&MLnLCWI0GgtSKM==I+-rDyH+4Eb$8oanI9@$Y@SLf;g44Mxy1TEh za)CBTHJ*Z8-%$5j@_bN~#hV#(hqa?94#60x{|wB{YP7#sZnEQO~c_cGcY?T$|87uv~q{yU^Vb@h+@7|B(4_u%T2=(^0` z?mt8S%iRAA{V#L>JM=&8`>$pHr+xpm?7z?K%_)ET-LKfG7l5T*eo3$gC$5@DPb=_+ z-IsTYt!dgIS{<=!Ymd~{?b*5rmsY1AA3ocQOVUyjE=6kV49Wd%aPtxA)6$0igGsv5 zAWS6-0xVMTEiA{+-+2|E4zwR}KL0(e?|hr^{s)#M7q*1$;++Cr$R8tiQi)K>^5HoL zjE4adExtd*{gm*|Euj3AQ4}tJp9-FdcBXUdZNr_*hEH34B$#WhgQxFl2jSDi6 zC$ZBhNo`(065z|gVFfJ5z;u4}!ixq1rU*cpHQ!pYz>TdZOx`ZH&h|T{ zEk(!t!4J|$Zn&^9a!C^QQ=pO$;%2oh?GCn2qolC6Q7hMbBCWsh+&`=CVPn*90wM|E zY;Nb*Ljk6^;>qlf+S}C}o9m?7nm8t(I9}oT1IxQsDQH|*z`R<%9<_7-QV#34%8~=a z^U=T#LubJS>i_JLyCKL|fpOLTt0UL{NPLki%N%$2xzjSZw z%`($sF)x3AFH_~(J7Ob`*ZKC}xUT;N9;zQV-oBdLIh_ICi^ik9avf=jxO5k&x@#dH z7j$bM#843yTS;&OELp>ha>SVoE4HK&2#vXHMy(a+fovT!dYI`gPG~xZR54=MA{TY>W-cq5!Hft48qjG5wDbbC^ z>%0{P;rq0Lb8-iliWLg|*R9|;{MaIh31?|!IFMA`+t)iuQH$2N4J1lJ;+afLBG;=_ z-z=$iRs5VS8!8=j;Zk~jjc|^qf-3CVp_IvYBokbfXs*e3k!j!|i;NU^erA^U^>I z6Mi=FYmWp72m-htr{9n442y!PFEL0C$LPsly=f6RQTOT}0r6VyshZFI(^^CW;=bPz zt4k+nSAn5pkLud=E6B+(v|JJ2inN7Hlob_IMBE8_8ud`=? z_M2Or1j-o;HReavrH6*1;QMFkIW1*5->t4diIf(6w~}zFE6hn6VU*Tg1Qwsp9#`fI@5oV(n67hWFLoEG>aj> zI)7|4C}`b7_u>W2emFP?!e~)2O1OYmG^)rZ++a1ULM&)B$!SczcpXp-%Oobb+QoXS zSrk~tT|htA=&&FR^)=IByg{_r$CWy<&IX?qS%Jx6Ef_mJs*t~epSs~(hQcEBGHC6Rtesq<^y*j;S)yR2Cbhre>aMmUoreH~&~!y=HpyeLuWVR7VCSyS9^rGx=o3*WLLKjV(V(? zbvny)S!RprTceyvhz3Y3;;RKd&O0Fqb7)JjR}$xRrsGW%e@Jk_eOq?yNeuhiW(Fz! zrcSJ>_BEotZ%-Ub(fMuMS1hmlvK7{#y#7l{6@J)1=k)*tK?PM{qAuHBbzA5vk6(L~ z7)J&x{xqZg_=?-!!aqBLMml_

2LqZ{3*xoWTf^^P0u+oCW?AEEGdQ6xi1mr}6VX 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 EKkl7QYybcN literal 25882 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 QA!Ph=bEO>dg1PVi0Yak|W&i*H delta 28834 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 From 1a65989185eaf1da19f586a5106bdc1762a60d8a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 15 Feb 2020 00:09:19 -0800 Subject: [PATCH 0412/1439] Fix build badge to point at develop --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 57e3434bc..f70a791bd 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ------------------------------------------------------------------------ [![PyPI version](https://badge.fury.io/py/isort.svg)](https://badge.fury.io/py/isort) -[![Build Status](https://travis-ci.org/timothycrosley/isort.svg?branch=master)](https://travis-ci.org/timothycrosley/isort) +[![Build Status](https://travis-ci.org/timothycrosley/isort.svg?branch=develop)](https://travis-ci.org/timothycrosley/isort) [![Code coverage Status](https://codecov.io/gh/timothycrosley/isort/branch/develop/graph/badge.svg)](https://codecov.io/gh/timothycrosley/isort) [![License](https://img.shields.io/github/license/mashape/apistatus.svg)](https://pypi.org/project/hug/) [![Join the chat at https://gitter.im/timothycrosley/isort](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/timothycrosley/isort?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) From c015ea2b6ea880384ac92f21bfb6863ba7e34130 Mon Sep 17 00:00:00 2001 From: harupy Date: Sat, 15 Feb 2020 21:41:50 +0900 Subject: [PATCH 0413/1439] Fix config file name --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f70a791bd..af4d2a5c5 100644 --- a/README.md +++ b/README.md @@ -244,7 +244,7 @@ If you find the default isort settings do not work well for your project, isort provides several ways to adjust the behavior. To configure isort for a single user create a `~/.isort.cfg` or -`$XDG_CONFIG_HOME/isort.cfg` file: +`$XDG_CONFIG_HOME/.isort.cfg` file: ```ini [settings] From 5129eab3144ecbd7ba9699d57951aa7ce87fbd8f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 15 Feb 2020 20:46:02 -0800 Subject: [PATCH 0414/1439] Add Harutaka Kawamura (@harupy) to acknowledgements --- docs/contributing/4.-acknowledgements.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index f8d2aefd3..88d190dfa 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -103,6 +103,7 @@ Documenters - Teg Khanna (@tegkhanna) - Sarah Beth Tracy (@sbtries) - Aaron Brown (@aaronvbrown) +- Harutaka Kawamura (@harupy) -------------------------------------------- From 6c601f22897df39c424b1b69ea370d61619e4aef Mon Sep 17 00:00:00 2001 From: harupy Date: Sun, 16 Feb 2020 13:51:44 +0900 Subject: [PATCH 0415/1439] Fix config file name --- isort/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/settings.py b/isort/settings.py index 8a661c3e1..9cdd6c779 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -3,7 +3,7 @@ Defines how the default settings for isort should be loaded (First from the default setting dictionary at the top of the file, then overridden by any settings - in ~/.isort.cfg or $XDG_CONFIG_HOME/isort.cfg if there are any) + in ~/.isort.cfg or $XDG_CONFIG_HOME/.isort.cfg if there are any) """ import configparser import fnmatch From df6bf35e8d6da1ca1cb46906b2bd277930fb33ef Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 16 Feb 2020 23:05:12 -0800 Subject: [PATCH 0416/1439] Fix issue #830: Improve compatibility with editor config, including *.{ sections --- isort/settings.py | 14 ++++++++++++-- tests/test_isort.py | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/isort/settings.py b/isort/settings.py index 9cdd6c779..8df49461f 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -56,12 +56,13 @@ "tox.ini", ".editorconfig", ) + CONFIG_SECTIONS: Dict[str, Tuple[str, ...]] = { ".isort.cfg": ("settings", "isort"), "pyproject.toml": ("tool.isort",), "setup.cfg": ("isort", "tool:isort"), "tox.ini": ("isort", "tool:isort"), - ".editorconfig": ("*", "*.py", "**.py"), + ".editorconfig": ("*", "*.py", "**.py", "*.{py}"), } FALLBACK_CONFIG_SECTIONS: Tuple[str, ...] = ("isort", "tool:isort", "tool.isort") FALLBACK_CONFIGS: Tuple[str, ...] @@ -426,7 +427,16 @@ def _get_config_data(file_path: str, sections: Iterable[str]) -> Dict[str, Any]: config = configparser.ConfigParser(strict=False) config.read_file(config_file) for section in sections: - if config.has_section(section): + if section.startswith("*.{") and section.endswith("}"): + extension = section[len("*.{") : -1] + for config_key in config.keys(): + if config_key.startswith("*.{") and config_key.endswith("}"): + if extension in map( + lambda text: text.strip(), config_key[len("*.{") : -1].split(",") + ): + settings.update(config.items(config_key)) + + elif config.has_section(section): settings.update(config.items(section)) if settings: diff --git a/tests/test_isort.py b/tests/test_isort.py index 8a53973cf..7f6d399ae 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -25,7 +25,7 @@ toml = None TEST_DEFAULT_CONFIG = """ -[*.py] +[*.{py,pyi}] max_line_length = 120 indent_style = space indent_size = 4 From 5ff390cd5634c9606ea196296221c6c65c16d16e Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 16 Feb 2020 23:24:26 -0800 Subject: [PATCH 0417/1439] Add note about done script in contributing guide --- docs/contributing/1.-contributing-guide.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/contributing/1.-contributing-guide.md b/docs/contributing/1.-contributing-guide.md index 1455f903d..75727c38e 100644 --- a/docs/contributing/1.-contributing-guide.md +++ b/docs/contributing/1.-contributing-guide.md @@ -30,6 +30,8 @@ Once you have verified that your system matches the base requirements you can st 5. `./scripts/test.sh` should yield Success: no issues found 6. `./scripts/clean.sh` should yield a Safety report checking packages +**TIP**: `./scripts/done.sh` will run both clean and test in one step. + ### Docker development If you would instead like to develop using Docker, the only local requirement is docker. From b9e92583e4471fe656e298f4d69b257a909168b9 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 17 Feb 2020 16:40:04 -0800 Subject: [PATCH 0418/1439] Add test specifying how isort should behave when length_sort is combined with sort_within_sections --- tests/test_isort.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_isort.py b/tests/test_isort.py index 7f6d399ae..3e86b4487 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -1120,6 +1120,20 @@ def test_force_single_line_imports_and_sort_within_sections() -> None: "from third_party import lib_d\n" ) + # Ensure force_sort_within_sections can work with length sort + # See: https://github.com/timothycrosley/isort/issues/1038 + test_input = """import sympy +import numpy as np +import pandas as pd +from matplotlib import pyplot as plt +""" + test_output = ( + SortImports( + file_contents=test_input, force_sort_within_sections=True, length_sort=True + ).output + == test_input + ) + def test_titled_imports() -> None: """Tests setting custom titled/commented import sections.""" From e99f820181e92bb8f11917ad56dc9622f32d72af Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 17 Feb 2020 16:40:23 -0800 Subject: [PATCH 0419/1439] Fix issue #1038: Adding support for combining sort within sections with length_sort --- isort/output.py | 1 + isort/sorting.py | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/isort/output.py b/isort/output.py index 8b753bcd7..1efbe4ed9 100644 --- a/isort/output.py +++ b/isort/output.py @@ -117,6 +117,7 @@ def sorted_imports( order_by_type=config.order_by_type, force_to_top=config.force_to_top, lexicographical=config.lexicographical, + length_sort=config.length_sort, ), ) diff --git a/isort/sorting.py b/isort/sorting.py index 34f88699c..ea5cafc8b 100644 --- a/isort/sorting.py +++ b/isort/sorting.py @@ -41,7 +41,11 @@ def module_key( def section_key( - line: str, order_by_type: bool, force_to_top: List[str], lexicographical: bool = False + line: str, + order_by_type: bool, + force_to_top: List[str], + lexicographical: bool = False, + length_sort: bool = False, ) -> str: section = "B" @@ -54,7 +58,8 @@ def section_key( section = "A" if not order_by_type: line = line.lower() - return f"{section}{line}" + + return f"{section}{len(line) if length_sort else ''}{line}" def naturally(to_sort: Iterable[str], key: Optional[Callable[[str], Any]] = None) -> List[str]: From 0298c1b53609a92e467680bf4ea0beab802ca5c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Gmach?= Date: Tue, 18 Feb 2020 13:11:20 +0100 Subject: [PATCH 0420/1439] Add link to documentation This link will be shown at https://pypi.org/project/isort/ and makes it easier to find the documentation. --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 182d6b02e..a6dbfc73e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,8 @@ authors = ["Timothy Crosley "] license = "MIT" readme = "README.md" repository = "https://github.com/timothycrosley/isort" -homepage = "http://timothycrosley.github.io/isort/" +homepage = "https://timothycrosley.github.io/isort/" +documentation = "https://isort.readthedocs.io/en/stable/" keywords = ["Refactor", "Lint", "Imports", "Sort", "Clean"] classifiers = [ "Development Status :: 6 - Mature", From 95b8eab76f619c68cab4fea071029a68ca3a329f Mon Sep 17 00:00:00 2001 From: Timothy Edmund Crosley Date: Tue, 18 Feb 2020 07:45:48 -0800 Subject: [PATCH 0421/1439] Point to correct documentation site --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a6dbfc73e..b6d9f171c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ license = "MIT" readme = "README.md" repository = "https://github.com/timothycrosley/isort" homepage = "https://timothycrosley.github.io/isort/" -documentation = "https://isort.readthedocs.io/en/stable/" +documentation = "https://timothycrosley.github.io/isort/" keywords = ["Refactor", "Lint", "Imports", "Sort", "Clean"] classifiers = [ "Development Status :: 6 - Mature", From 1fe5fc6d23a98fcd6811efdce1fcdc44688521d1 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 18 Feb 2020 15:19:07 -0800 Subject: [PATCH 0422/1439] Initial step toward making core API use streaming approach --- isort/api.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/isort/api.py b/isort/api.py index 369528e1d..f157af30f 100644 --- a/isort/api.py +++ b/isort/api.py @@ -43,13 +43,14 @@ def _config( def sorted_imports( - file_contents: str, + input_stream: TextIO, + output_stream: TextIO, extension: str = "py", config: Config = DEFAULT_CONFIG, file_path: Optional[Path] = None, disregard_skip: bool = False, **config_kwargs, -) -> str: +): config = _config(config=config, **config_kwargs) content_source = str(file_path or "Passed in content") if not disregard_skip: @@ -62,20 +63,21 @@ def sorted_imports( if config.atomic: try: - compile(file_contents, content_source, "exec", 0, 1) + file_content = input_stream.read() + compile(file_content, content_source, "exec", 0, 1) + input_stream = StringIO(file_content) except SyntaxError: raise ExistingSyntaxErrors(content_source) - parsed_output = StringIO() - sort_imports(StringIO(file_contents), parsed_output, extension=extension, config=config) - parsed_output.seek(0) - parsed_output = parsed_output.read() + sort_imports(input_stream, output_stream, extension=extension, config=config) + if config.atomic: + output_stream.seek(0) try: - compile(file_contents, content_source, "exec", 0, 1) + compile(output_stream.read(), content_source, "exec", 0, 1) + ouput_stream.seek(0) except SyntaxError: raise IntroducedSyntaxErrors(content_source) - return parsed_output def check_imports( From f5662d0f85b40728e80ce7a5f5a60a16caa7b21c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 18 Feb 2020 15:38:56 -0800 Subject: [PATCH 0423/1439] Initial work toward streaming check API --- isort/api.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/isort/api.py b/isort/api.py index f157af30f..6a2bdd234 100644 --- a/isort/api.py +++ b/isort/api.py @@ -81,7 +81,7 @@ def sorted_imports( def check_imports( - file_contents: str, + input_stream: TextIO, show_diff: bool = False, extension: str = "py", config: Config = DEFAULT_CONFIG, @@ -91,8 +91,10 @@ def check_imports( ) -> bool: config = _config(config=config, **config_kwargs) - sorted_output = sorted_imports( - file_contents=file_contents, + sorted_stream = StringIO() + sorted_imports( + input_stream=input_stream, + output_stream=output_stream, extension=extension, config=config, file_path=file_path, @@ -136,6 +138,7 @@ def sort_imports( input_stream: TextIO, output_stream: TextIO, extension: str = "py", + check: bool = False, config: Config = DEFAULT_CONFIG, ) -> None: """Parses stream identifying sections of contiguous imports and sorting them From b5c0a35bc4eac6f967227d900dcfa277de676b9f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 19 Feb 2020 17:03:28 -0800 Subject: [PATCH 0424/1439] Central API module refactored for stream sorting --- isort/api.py | 83 +++++++++++++++++++++++++++++++++++++--------------- isort/io.py | 10 +++++++ setup.cfg | 4 +++ 3 files changed, 74 insertions(+), 23 deletions(-) diff --git a/isort/api.py b/isort/api.py index 6a2bdd234..b91a78385 100644 --- a/isort/api.py +++ b/isort/api.py @@ -12,7 +12,7 @@ IntroducedSyntaxErrors, ) from .format import format_natural, remove_whitespace, show_unified_diff -from .io import File +from .io import File, Empty from .settings import DEFAULT_CONFIG, FILE_SKIP_COMMENTS, Config CIMPORT_IDENTIFIERS = ("cimport ", "cimport*", "from.cimport") @@ -57,10 +57,6 @@ def sorted_imports( if file_path and config.is_skipped(file_path): raise FileSkipSetting(content_source) - for file_skip_comment in FILE_SKIP_COMMENTS: - if file_skip_comment in file_contents: - raise FileSkipComment(content_source) - if config.atomic: try: file_content = input_stream.read() @@ -69,16 +65,21 @@ def sorted_imports( except SyntaxError: raise ExistingSyntaxErrors(content_source) - sort_imports(input_stream, output_stream, extension=extension, config=config) + try: + changed = sort_imports(input_stream, output_stream, extension=extension, config=config) + except FileSkipComment: + raise FileSkipComment(file_path) if config.atomic: output_stream.seek(0) try: compile(output_stream.read(), content_source, "exec", 0, 1) - ouput_stream.seek(0) + output_stream.seek(0) except SyntaxError: raise IntroducedSyntaxErrors(content_source) + return changed + def check_imports( input_stream: TextIO, @@ -91,33 +92,39 @@ def check_imports( ) -> bool: config = _config(config=config, **config_kwargs) - sorted_stream = StringIO() - sorted_imports( + changed: bool = sorted_imports( input_stream=input_stream, - output_stream=output_stream, + output_stream=Empty, extension=extension, config=config, file_path=file_path, disregard_skip=disregard_skip, **config_kwargs, ) - if config.ignore_whitespace: - line_separator = config.line_ending or parse._infer_line_separator(file_contents) - compare_in = remove_whitespace(file_contents, line_separator=line_separator).strip() - compare_out = remove_whitespace(sorted_output, line_separator=line_separator).strip() - else: - compare_in = file_contents.strip() - compare_out = sorted_output.strip() - if compare_out == compare_in: + if not changed: if config.verbose: print(f"SUCCESS: {file_path or ''} Everything Looks Good!") return True else: print(f"ERROR: {file_path or ''} Imports are incorrectly sorted and/or formatted.") if show_diff: + output_stream = StringIO() + input_stream.seek(0) + file_contents = input_stream.read() + sorted_imports( + input_stream=StringIO(file_contents), + output_stream=output_stream, + extension=extension, + config=config, + file_path=file_path, + disregard_skip=disregard_skip, + **config_kwargs, + ) + output_stream.seek(0) + show_unified_diff( - file_input=file_contents, file_output=sorted_output, file_path=file_path + file_input=file_contents, file_output=output_stream.read(), file_path=file_path ) return False @@ -138,17 +145,23 @@ def sort_imports( input_stream: TextIO, output_stream: TextIO, extension: str = "py", - check: bool = False, config: Config = DEFAULT_CONFIG, -) -> None: +) -> bool: """Parses stream identifying sections of contiguous imports and sorting them Code with unsorted imports is read from the provided `input_stream`, sorted and then - outputted to the specified output_stream. + outputted to the specified `output_stream`. - `input_stream`: Text stream with unsorted import sections. - `output_stream`: Text stream to output sorted inputs into. - - `config`: Config settings to use when sorting imports. Defaults settings.DEFAULT_CONFIG. + - `config`: Config settings to use when sorting imports. Defaults settings. + - *Default*: `isort.settings.DEFAULT_CONFIG`. + - `extension`: The file extension or file extension rules that should be used. + - *Default*: `"py"`. + - *Choices*: `["py", "pyi", "pyx"]`. + + Returns `True` if there were changes that needed to be made (errors present) from what + was provided in the input_stream, otherwise `False`. """ line_separator: str = config.line_ending add_imports: List[str] = [format_natural(addition) for addition in config.add_imports] @@ -165,8 +178,14 @@ def sort_imports( indent: str = "" isort_off: bool = False cimports: bool = False + made_changes: bool = False for index, line in enumerate(chain(input_stream, (None,))): + + for file_skip_comment in FILE_SKIP_COMMENTS: + if file_skip_comment in line: + raise FileSkipComment("Passed in content") + if line is None: if index == 0 and not config.force_adds: return @@ -313,6 +332,7 @@ def sort_imports( import_section = import_section.lstrip(line_separator) first_import_section = False + raw_import_section: str = import_section if indent: import_section = line_separator.join( line.lstrip() for line in import_section.split(line_separator) @@ -336,6 +356,21 @@ def sort_imports( + trailing_whitespace ) + if not made_changes: + if config.ignore_whitespace: + compare_in = remove_whitespace( + raw_import_section, line_separator=line_separator + ).strip() + compare_out = remove_whitespace( + sorted_import_section, line_separator=line_separator + ).strip() + else: + compare_in = raw_import_section.strip() + compare_out = sorted_output.strip() + + if compare_out != compare_in: + made_changes = True + output_stream.write(sorted_import_section) if not line and not indent and next_import_section: output_stream.write(line_separator) @@ -355,3 +390,5 @@ def sort_imports( else: output_stream.write(line) not_imports = False + + return made_changes diff --git a/isort/io.py b/isort/io.py index bf9934826..4869790fb 100644 --- a/isort/io.py +++ b/isort/io.py @@ -1,4 +1,6 @@ """Defines any IO utilities used by isort""" +from io import StringIO + import locale import re from pathlib import Path @@ -71,3 +73,11 @@ def _read_file_contents(file_path: Path) -> Tuple[str, str]: pass raise UnableToDetermineEncoding(file_path, encoding, fallback_encoding) + + +class _EmptyIO(StringIO): + def write(self, *args, **kwargs): + pass + + +Empty = _EmptyIO() diff --git a/setup.cfg b/setup.cfg index 32875b6ac..f3badbe24 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,3 +14,7 @@ strict_optional = False [tool:pytest] testpaths = tests + +[flake8] +max-line-length = 100 +ignore = F403,F401,W503,E203 From 21e5e3616f8a211059dfa6d2e52b2fe2b88f507a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 20 Feb 2020 08:42:55 -0800 Subject: [PATCH 0425/1439] Initially working stream based checking --- isort/api.py | 11 +++++------ isort/compat.py | 12 ++++++++---- isort/io.py | 21 +++++++++++++++------ 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/isort/api.py b/isort/api.py index b91a78385..3bfc43731 100644 --- a/isort/api.py +++ b/isort/api.py @@ -181,11 +181,6 @@ def sort_imports( made_changes: bool = False for index, line in enumerate(chain(input_stream, (None,))): - - for file_skip_comment in FILE_SKIP_COMMENTS: - if file_skip_comment in line: - raise FileSkipComment("Passed in content") - if line is None: if index == 0 and not config.force_adds: return @@ -198,6 +193,10 @@ def sort_imports( if not line_separator: line_separator = line[-1] + for file_skip_comment in FILE_SKIP_COMMENTS: + if file_skip_comment in line: + raise FileSkipComment("Passed in content") + stripped_line = line.strip() if ( (index == 0 or (index in (1, 2) and not contains_imports)) @@ -366,7 +365,7 @@ def sort_imports( ).strip() else: compare_in = raw_import_section.strip() - compare_out = sorted_output.strip() + compare_out = sorted_import_section.strip() if compare_out != compare_in: made_changes = True diff --git a/isort/compat.py b/isort/compat.py index 0e597fc71..66593a8a5 100644 --- a/isort/compat.py +++ b/isort/compat.py @@ -1,3 +1,4 @@ +from io import StringIO import sys from pathlib import Path from typing import Any, Optional @@ -35,7 +36,7 @@ def __init__( file_data = File.from_contents(file_contents, filename=filename) else: file_data = File.read(filename) - file_contents, file_path, file_encoding = file_data + file_stream, file_path, file_encoding = file_data if not extension: extension = file_data.extension @@ -49,7 +50,7 @@ def __init__( try: if check: self.incorrectly_sorted = not api.check_imports( - file_contents, + file_stream, extension=extension, config=config, file_path=file_path, @@ -58,9 +59,12 @@ def __init__( self.output = "" return else: - self.output = api.sorted_imports( - file_contents, extension=extension, config=config, file_path=file_path + output_stream = StringIO() + api.sorted_imports( + file_stream, output_stream, extension=extension, config=config, file_path=file_path ) + self.output_stream.seek(0) + self.output = output_stream.read() except FileSkipped as error: self.skipped = True self.output = "" diff --git a/isort/io.py b/isort/io.py index 4869790fb..5cdec94d1 100644 --- a/isort/io.py +++ b/isort/io.py @@ -1,5 +1,6 @@ """Defines any IO utilities used by isort""" from io import StringIO +from typing import List, Optional, TextIO import locale import re @@ -12,20 +13,20 @@ class File(NamedTuple): - contents: str + contents: TextIO path: Path encoding: str @staticmethod def read(filename: str) -> "File": file_path = Path(filename).resolve() - contents, encoding = _read_file_contents(file_path) - return File(contents=contents, path=file_path, encoding=encoding) + encoding = _determine_file_encoding(file_path) + return File(contents=file_path.open(encoding=encoding, newline=""), path=file_path, encoding=encoding) @staticmethod def from_contents(contents: str, filename: str) -> "File": return File( - contents, path=Path(filename).resolve(), encoding=_determine_content_encoding(contents) + StringIO(contents), path=Path(filename).resolve(), encoding=_determine_content_encoding(contents) ) @property @@ -50,8 +51,16 @@ def _determine_content_encoding(content: str, default: str = "utf-8"): def _determine_file_encoding(file_path: Path, default: str = "utf-8") -> str: # see https://www.python.org/dev/peps/pep-0263/ - with file_path.open("rb") as open_file: - return _determine_stream_encoding(open_file, default=default) + try: + with file_path.open("rb") as open_file: + return _determine_stream_encoding(open_file, default=default) + except UnicodeDecodeError: + fallback_encoding = locale.getpreferredencoding(False) + try: + with file_path.open("rb", encoding=fallback_encoding) as open_file: + return _determine_stream_encoding(open_file, default=fallback_encoding) + except UnicodeDecodeError: + raise UnableToDetermineEncoding(file_path, default, fallback_encoding) def _read_file_contents(file_path: Path) -> Tuple[str, str]: From 71ea51a6e2d379c81fcea47ed93bf313181747ac Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 20 Feb 2020 22:28:39 -0800 Subject: [PATCH 0426/1439] Initially fully working streaming support --- isort/api.py | 10 +++---- isort/compat.py | 12 ++++++-- isort/io.py | 77 +++++++++++++----------------------------------- tests/test_io.py | 8 ----- 4 files changed, 34 insertions(+), 73 deletions(-) diff --git a/isort/api.py b/isort/api.py index 3bfc43731..a64ac3770 100644 --- a/isort/api.py +++ b/isort/api.py @@ -1,5 +1,5 @@ import textwrap -from io import StringIO +from io import BytesIO, StringIO from itertools import chain from pathlib import Path from typing import List, Optional, TextIO @@ -12,7 +12,7 @@ IntroducedSyntaxErrors, ) from .format import format_natural, remove_whitespace, show_unified_diff -from .io import File, Empty +from .io import Empty, File from .settings import DEFAULT_CONFIG, FILE_SKIP_COMMENTS, Config CIMPORT_IDENTIFIERS = ("cimport ", "cimport*", "from.cimport") @@ -68,7 +68,7 @@ def sorted_imports( try: changed = sort_imports(input_stream, output_stream, extension=extension, config=config) except FileSkipComment: - raise FileSkipComment(file_path) + raise FileSkipComment(content_source) if config.atomic: output_stream.seek(0) @@ -156,7 +156,7 @@ def sort_imports( - `output_stream`: Text stream to output sorted inputs into. - `config`: Config settings to use when sorting imports. Defaults settings. - *Default*: `isort.settings.DEFAULT_CONFIG`. - - `extension`: The file extension or file extension rules that should be used. + - `extension`: The file extension or file extension rules that should be used. - *Default*: `"py"`. - *Choices*: `["py", "pyi", "pyx"]`. @@ -183,7 +183,7 @@ def sort_imports( for index, line in enumerate(chain(input_stream, (None,))): if line is None: if index == 0 and not config.force_adds: - return + return False not_imports = True line = "" diff --git a/isort/compat.py b/isort/compat.py index 66593a8a5..d3c0eb595 100644 --- a/isort/compat.py +++ b/isort/compat.py @@ -1,5 +1,5 @@ -from io import StringIO import sys +from io import StringIO from pathlib import Path from typing import Any, Optional from warnings import warn @@ -39,6 +39,8 @@ def __init__( file_stream, file_path, file_encoding = file_data if not extension: extension = file_data.extension + else: + file_stream = StringIO(file_contents) if settings_path: setting_overrides["settings_path"] = settings_path @@ -61,9 +63,13 @@ def __init__( else: output_stream = StringIO() api.sorted_imports( - file_stream, output_stream, extension=extension, config=config, file_path=file_path + file_stream, + output_stream, + extension=extension, + config=config, + file_path=file_path, ) - self.output_stream.seek(0) + output_stream.seek(0) self.output = output_stream.read() except FileSkipped as error: self.skipped = True diff --git a/isort/io.py b/isort/io.py index 5cdec94d1..d9d6efb4a 100644 --- a/isort/io.py +++ b/isort/io.py @@ -1,11 +1,10 @@ """Defines any IO utilities used by isort""" -from io import StringIO -from typing import List, Optional, TextIO - import locale import re +import tokenize +from io import BytesIO, StringIO, TextIOWrapper from pathlib import Path -from typing import NamedTuple, Tuple +from typing import List, NamedTuple, Optional, TextIO, Tuple from .exceptions import UnableToDetermineEncoding @@ -20,68 +19,32 @@ class File(NamedTuple): @staticmethod def read(filename: str) -> "File": file_path = Path(filename).resolve() - encoding = _determine_file_encoding(file_path) - return File(contents=file_path.open(encoding=encoding, newline=""), path=file_path, encoding=encoding) + stream = File._open(file_path) + return File(contents=stream, path=file_path, encoding=stream.encoding) @staticmethod def from_contents(contents: str, filename: str) -> "File": - return File( - StringIO(contents), path=Path(filename).resolve(), encoding=_determine_content_encoding(contents) - ) + encoding, lines = tokenize.detect_encoding(BytesIO(contents.encode("utf-8")).readline) + return File(StringIO(contents), path=Path(filename).resolve(), encoding=encoding) @property def extension(self): return self.path.suffix.lstrip(".") - -def _determine_stream_encoding(stream, default: str = "utf-8") -> str: - for line_number, line in enumerate(stream, 1): - if line_number > 2: - break - groups = re.findall(_ENCODING_PATTERN, line) - if groups: - return groups[0].decode("ascii") - - return default - - -def _determine_content_encoding(content: str, default: str = "utf-8"): - return _determine_stream_encoding(content.encode(default).split(b"\n"), default=default) - - -def _determine_file_encoding(file_path: Path, default: str = "utf-8") -> str: - # see https://www.python.org/dev/peps/pep-0263/ - try: - with file_path.open("rb") as open_file: - return _determine_stream_encoding(open_file, default=default) - except UnicodeDecodeError: - fallback_encoding = locale.getpreferredencoding(False) - try: - with file_path.open("rb", encoding=fallback_encoding) as open_file: - return _determine_stream_encoding(open_file, default=fallback_encoding) - except UnicodeDecodeError: - raise UnableToDetermineEncoding(file_path, default, fallback_encoding) - - -def _read_file_contents(file_path: Path) -> Tuple[str, str]: - encoding = _determine_file_encoding(file_path) - with file_path.open(encoding=encoding, newline="") as file_to_import_sort: - try: - file_contents = file_to_import_sort.read() - return file_contents, encoding - except UnicodeDecodeError: - pass - - # Try default encoding for open(mode='r') on the system - fallback_encoding = locale.getpreferredencoding(False) - with file_path.open(encoding=fallback_encoding, newline="") as file_to_import_sort: + @staticmethod + def _open(filename): + """Open a file in read only mode using the encoding detected by + detect_encoding(). + """ + buffer = open(filename, "rb") try: - file_contents = file_to_import_sort.read() - return file_contents, fallback_encoding - except UnicodeDecodeError: - pass - - raise UnableToDetermineEncoding(file_path, encoding, fallback_encoding) + encoding, lines = tokenize.detect_encoding(buffer.readline) + buffer.seek(0) + text = TextIOWrapper(buffer, encoding, line_buffering=True, newline="") + return text + except Exception: + buffer.close() + raise class _EmptyIO(StringIO): diff --git a/tests/test_io.py b/tests/test_io.py index 23a6214cc..6b6fa685d 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -15,11 +15,3 @@ def test_read(self, tmpdir): """ test_file = tmpdir.join("file.py") test_file.write(test_file_content) - - # able to read file even with incorrect encoding, if it is UTF-8 compatible - assert io.File.read(test_file).contents == test_file_content - - # unless the locale is also ASCII - with pytest.raises(io.UnableToDetermineEncoding): - with patch("locale.getpreferredencoding", lambda value: "ascii"): - io.File.read(test_file).contents From 490e135840908efd06399430c053643ac074df91 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 20 Feb 2020 22:54:56 -0800 Subject: [PATCH 0427/1439] Fix open to match stdlibs exactly, with newline modification --- isort/io.py | 1 + 1 file changed, 1 insertion(+) diff --git a/isort/io.py b/isort/io.py index d9d6efb4a..22312cda7 100644 --- a/isort/io.py +++ b/isort/io.py @@ -41,6 +41,7 @@ def _open(filename): encoding, lines = tokenize.detect_encoding(buffer.readline) buffer.seek(0) text = TextIOWrapper(buffer, encoding, line_buffering=True, newline="") + text.mode = "r" # type: ignore return text except Exception: buffer.close() From 0e55a686d1011bc60a9875c13a7b0385efcdb453 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 22 Feb 2020 23:27:05 -0800 Subject: [PATCH 0428/1439] Initial API containing checkfile for streaming support --- isort/api.py | 26 ++++++- isort/io.py | 21 +++--- isort/main.py | 189 +++++++++++++++++++++++++++----------------------- 3 files changed, 138 insertions(+), 98 deletions(-) diff --git a/isort/api.py b/isort/api.py index a64ac3770..01e97aaae 100644 --- a/isort/api.py +++ b/isort/api.py @@ -1,10 +1,10 @@ import textwrap -from io import BytesIO, StringIO +from io import StringIO from itertools import chain from pathlib import Path -from typing import List, Optional, TextIO +from typing import List, Optional, TextIO, Union -from . import output, parse +from . import output, parse, io from .exceptions import ( ExistingSyntaxErrors, FileSkipComment, @@ -32,6 +32,7 @@ def _config( config_kwargs["settings_path"] = path if config_kwargs and config is not DEFAULT_CONFIG: + breakpoint() raise ValueError( "You can either specify custom configuration options using kwargs or " "passing in a Config object. Not Both!" @@ -391,3 +392,22 @@ def sort_imports( not_imports = False return made_changes + + +def check_file( + filename: Union[str, Path], + show_diff: bool = False, + config: Config = DEFAULT_CONFIG, + file_path: Optional[Path] = None, + disregard_skip: bool = False, + **config_kwargs, +) -> bool: + with io.read_file(filename) as source_file: + return check_imports( + source_file.stream, + show_diff=show_diff, + extension=source_file.extension or "py", + config=config, + file_path=source_file.path, + disregard_skip=disregard_skip, + **config_kwargs) diff --git a/isort/io.py b/isort/io.py index 22312cda7..0dd104e4d 100644 --- a/isort/io.py +++ b/isort/io.py @@ -2,9 +2,10 @@ import locale import re import tokenize +from contextlib import contextmanager from io import BytesIO, StringIO, TextIOWrapper from pathlib import Path -from typing import List, NamedTuple, Optional, TextIO, Tuple +from typing import List, NamedTuple, Optional, TextIO, Tuple, Union, Iterator from .exceptions import UnableToDetermineEncoding @@ -12,16 +13,10 @@ class File(NamedTuple): - contents: TextIO + stream: TextIO path: Path encoding: str - @staticmethod - def read(filename: str) -> "File": - file_path = Path(filename).resolve() - stream = File._open(file_path) - return File(contents=stream, path=file_path, encoding=stream.encoding) - @staticmethod def from_contents(contents: str, filename: str) -> "File": encoding, lines = tokenize.detect_encoding(BytesIO(contents.encode("utf-8")).readline) @@ -48,6 +43,16 @@ def _open(filename): raise +@contextmanager +def read_file(filename: Union[str, Path]) -> Iterator["File"]: + file_path = Path(filename).resolve() + try: + stream = File._open(file_path) + yield File(stream=stream, path=file_path, encoding=stream.encoding) + finally: + stream.close() + + class _EmptyIO(StringIO): def write(self, *args, **kwargs): pass diff --git a/isort/main.py b/isort/main.py index 52e061084..447d4a15c 100644 --- a/isort/main.py +++ b/isort/main.py @@ -10,7 +10,8 @@ from typing import Any, Dict, Iterable, Iterator, List, Optional, Sequence from warnings import warn -from . import SortImports, __version__, sections +from . import SortImports, __version__, sections, api +from .exceptions import FileSkipped from .logo import ASCII_ART from .profiles import profiles from .settings import SUPPORTED_EXTENSIONS, VALID_PY_TARGETS, Config, WrapModes @@ -69,9 +70,21 @@ def __init__(self, incorrectly_sorted: bool, skipped: bool) -> None: self.skipped = skipped -def sort_imports(file_name: str, **arguments: Any) -> Optional[SortAttempt]: +def sort_imports(file_name: str, config: Config, check: bool = False, **arguments: Any) -> Optional[SortAttempt]: try: - result = SortImports(file_name, **arguments) + if check: + incorrectly_sorted: bool = False + skipped: bool = False + try: + arguments.pop("ask_to_apply") + arguments.pop("write_to_stdout") + arguments.pop("settings_path") + incorrectly_sorted = not api.check_file(file_name, config=config, **arguments) + except FileSkipped: + skipped = True + return SortAttempt(incorrectly_sorted, skipped) + else: + result = SortImports(file_name, **arguments) return SortAttempt(result.incorrectly_sorted, result.skipped) except (OSError, ValueError) as error: warn(f"Unable to parse file {file_name} due to {error}") @@ -558,93 +571,95 @@ def main(argv: Optional[Sequence[str]] = None) -> None: return elif file_names == ["-"] and not show_config: SortImports(file_contents=sys.stdin.read(), write_to_stdout=True, **arguments) + return + + if "settings_path" not in arguments: + arguments["settings_path"] = ( + os.path.abspath(file_names[0] if file_names else ".") or os.getcwd() + ) + if not os.path.isdir(arguments["settings_path"]): + arguments["settings_path"] = os.path.dirname(arguments["settings_path"]) + + config_dict = arguments.copy() + ask_to_apply = config_dict.pop("ask_to_apply", False) + jobs = config_dict.pop("jobs", ()) + show_logo = config_dict.pop("show_logo", False) + filter_files = config_dict.pop("filter_files", False) + check = config_dict.pop("check", False) + show_diff = config_dict.pop("show_diff", False) + write_to_stdout = config_dict.pop("write_to_stdout", False) + config = Config(**config_dict) + if show_config: + pprint(config.__dict__) + return + + wrong_sorted_files = False + skipped: List[str] = [] + + if filter_files: + filtered_files = [] + for file_name in file_names: + if config.is_skipped(Path(file_name)): + skipped.append(file_name) + else: + filtered_files.append(file_name) + file_names = filtered_files + + file_names = iter_source_code(file_names, config, skipped) + num_skipped = 0 + if config.verbose or show_logo: + print(ASCII_ART) + + if jobs: + import multiprocessing + + executor = multiprocessing.Pool(jobs) + attempt_iterator = executor.imap( + functools.partial( + sort_imports, + config=config, + check=check, + ask_to_apply=ask_to_apply, + write_to_stdout=write_to_stdout + ), + file_names, + ) else: - if "settings_path" not in arguments: - arguments["settings_path"] = ( - os.path.abspath(file_names[0] if file_names else ".") or os.getcwd() - ) - if not os.path.isdir(arguments["settings_path"]): - arguments["settings_path"] = os.path.dirname(arguments["settings_path"]) - - config_dict = arguments.copy() - ask_to_apply = config_dict.pop("ask_to_apply", False) - jobs = config_dict.pop("jobs", ()) - show_logo = config_dict.pop("show_logo", False) - filter_files = config_dict.pop("filter_files", False) - check = config_dict.pop("check", False) - show_diff = config_dict.pop("show_diff", False) - write_to_stdout = config_dict.pop("write_to_stdout", False) - config = Config(**config_dict) - if show_config: - pprint(config.__dict__) - return - - wrong_sorted_files = False - skipped: List[str] = [] - - if filter_files: - filtered_files = [] - for file_name in file_names: - if config.is_skipped(Path(file_name)): - skipped.append(file_name) - else: - filtered_files.append(file_name) - file_names = filtered_files - - file_names = iter_source_code(file_names, config, skipped) - num_skipped = 0 - if config.verbose or show_logo: - print(ASCII_ART) - - if jobs: - import multiprocessing - - executor = multiprocessing.Pool(jobs) - attempt_iterator = executor.imap( - functools.partial( - sort_imports, - check=check, - ask_to_apply=ask_to_apply, - write_to_stdout=write_to_stdout, - **config_dict, - ), - file_names, + # https://github.com/python/typeshed/pull/2814 + attempt_iterator = ( + sort_imports( # type: ignore + file_name, + config=config, + check=check, + ask_to_apply=ask_to_apply, + show_diff=show_diff, + write_to_stdout=write_to_stdout, + **config_dict, ) - else: - # https://github.com/python/typeshed/pull/2814 - attempt_iterator = ( - sort_imports( # type: ignore - file_name, - check=check, - ask_to_apply=ask_to_apply, - show_diff=show_diff, - write_to_stdout=write_to_stdout, - **config_dict, + for file_name in file_names + ) + + for sort_attempt in attempt_iterator: + if not sort_attempt: + continue + incorrectly_sorted = sort_attempt.incorrectly_sorted + if arguments.get("check", False) and incorrectly_sorted: + wrong_sorted_files = True + if sort_attempt.skipped: + num_skipped += 1 + + if wrong_sorted_files: + sys.exit(1) + + num_skipped += len(skipped) + if num_skipped and not arguments.get("quiet", False): + if config.verbose: + for was_skipped in skipped: + warn( + f"{was_skipped} was skipped as it's listed in 'skip' setting" + " or matches a glob in 'skip_glob' setting" ) - for file_name in file_names - ) - - for sort_attempt in attempt_iterator: - if not sort_attempt: - continue - incorrectly_sorted = sort_attempt.incorrectly_sorted - if arguments.get("check", False) and incorrectly_sorted: - wrong_sorted_files = True - if sort_attempt.skipped: - num_skipped += 1 - - if wrong_sorted_files: - sys.exit(1) - - num_skipped += len(skipped) - if num_skipped and not arguments.get("quiet", False): - if config.verbose: - for was_skipped in skipped: - warn( - f"{was_skipped} was skipped as it's listed in 'skip' setting" - " or matches a glob in 'skip_glob' setting" - ) - print(f"Skipped {num_skipped} files") + print(f"Skipped {num_skipped} files") if __name__ == "__main__": From 009c4c18441fec269d2177f6b6b7dee0b6e6f481 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 23 Feb 2020 07:32:41 -0800 Subject: [PATCH 0429/1439] Initial working sort_file implementation --- isort/api.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/isort/api.py b/isort/api.py index 01e97aaae..5599a3f77 100644 --- a/isort/api.py +++ b/isort/api.py @@ -411,3 +411,33 @@ def check_file( file_path=source_file.path, disregard_skip=disregard_skip, **config_kwargs) + + +def sort_file( + filename: Union[str, Path], + extension: str = "py", + config: Config = DEFAULT_CONFIG, + file_path: Optional[Path] = None, + disregard_skip: bool = False, + **config_kwargs, +): + with io.read_file(filename) as source_file: + tmp_file = source_file.path.with_suffix(source_file.path.suffix + ".isorted") + with tmp_file.open("w", encoding=source_file.encoding, newline="") as output_stream: + try: + changed = sorted_imports( + input_stream=source_file.stream, + output_stream=output_stream, + config=config, + file_path=source_file.path, + disregard_skip=disregard_skip, + **config_kwargs + ) + if changed: + tmp_file.replace(source_file.path) + + finally: + try: # Python 3.8+: use `missing_ok=True` instead of try except. + tmp_file.unlink() + except FileNotFoundError: + pass From 2d7a6dce45455f419508e95894b629d3f316b429 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 23 Feb 2020 07:33:15 -0800 Subject: [PATCH 0430/1439] Fix bug that occurs when stream is never created during try statemen --- isort/io.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/isort/io.py b/isort/io.py index 0dd104e4d..e24cd9bb7 100644 --- a/isort/io.py +++ b/isort/io.py @@ -46,11 +46,13 @@ def _open(filename): @contextmanager def read_file(filename: Union[str, Path]) -> Iterator["File"]: file_path = Path(filename).resolve() + stream = None try: stream = File._open(file_path) yield File(stream=stream, path=file_path, encoding=stream.encoding) finally: - stream.close() + if stream is not None: + stream.close() class _EmptyIO(StringIO): From bcdd4f850e188f9d3063936058f959497372e2de Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 23 Feb 2020 07:44:03 -0800 Subject: [PATCH 0431/1439] Add ask to apply support directly to sort_file API endpoint --- isort/api.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/isort/api.py b/isort/api.py index 5599a3f77..c0dc8983d 100644 --- a/isort/api.py +++ b/isort/api.py @@ -11,7 +11,7 @@ FileSkipSetting, IntroducedSyntaxErrors, ) -from .format import format_natural, remove_whitespace, show_unified_diff +from .format import format_natural, remove_whitespace, show_unified_diff, ask_whether_to_apply_changes_to_file from .io import Empty, File from .settings import DEFAULT_CONFIG, FILE_SKIP_COMMENTS, Config @@ -419,12 +419,14 @@ def sort_file( config: Config = DEFAULT_CONFIG, file_path: Optional[Path] = None, disregard_skip: bool = False, + ask_to_apply: bool = False, **config_kwargs, ): with io.read_file(filename) as source_file: tmp_file = source_file.path.with_suffix(source_file.path.suffix + ".isorted") - with tmp_file.open("w", encoding=source_file.encoding, newline="") as output_stream: - try: + changed: bool = False + try: + with tmp_file.open("w", encoding=source_file.encoding, newline="") as output_stream: changed = sorted_imports( input_stream=source_file.stream, output_stream=output_stream, @@ -433,11 +435,21 @@ def sort_file( disregard_skip=disregard_skip, **config_kwargs ) - if changed: + if changed: + if ask_to_apply: + source_file.stream.seek(0) + show_unified_diff( + file_input=source_file.stream.read(), + file_output=tmp_file.read_text(encoding=source_file.encoding), + file_path=source_file.path + ) + apply_changes = ask_whether_to_apply_changes_to_file(str(source_file.path)) + if not apply_changes: + return tmp_file.replace(source_file.path) - finally: - try: # Python 3.8+: use `missing_ok=True` instead of try except. - tmp_file.unlink() - except FileNotFoundError: - pass + finally: + try: # Python 3.8+: use `missing_ok=True` instead of try except. + tmp_file.unlink() + except FileNotFoundError: + pass From 8191601a49634a93ff1fadc2933b4f8a927f256b Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 24 Feb 2020 14:55:56 +0000 Subject: [PATCH 0432/1439] test for --version argument --- tests/test_main.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/test_main.py b/tests/test_main.py index 45fb0b8aa..f574a096d 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -5,6 +5,7 @@ from hypothesis_auto import auto_pytest_magic from isort import main +from isort._version import __version__ from isort.settings import DEFAULT_CONFIG auto_pytest_magic(main.sort_imports) @@ -25,6 +26,27 @@ def test_is_python_file(): assert not main.is_python_file("file.pex") +def test_ascii_art(capsys): + main.main(["--version"]) + out, error = capsys.readouterr() + assert ( + out + == f""" + _ _ + (_) ___ ___ _ __| |_ + | |/ _/ / _ \\/ '__ _/ + | |\\__ \\/\\_\\/| | | |_ + |_|\\___/\\___/\\_/ \\_/ + + isort your imports, so you don't have to. + + VERSION {__version__} + +""" + ) + assert error == "" + + @pytest.mark.skipif(sys.platform == "win32", reason="cannot create fifo file on Windows platform") def test_is_python_file_fifo(tmpdir): fifo_file = os.path.join(tmpdir, "fifo_file") From 49180cae7cd44a1b1c6b2152bffd017d6762afb5 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 24 Feb 2020 22:39:50 -0800 Subject: [PATCH 0433/1439] Add existing and introduced syntax error handling --- isort/api.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/isort/api.py b/isort/api.py index c0dc8983d..a492359ef 100644 --- a/isort/api.py +++ b/isort/api.py @@ -3,6 +3,7 @@ from itertools import chain from pathlib import Path from typing import List, Optional, TextIO, Union +from warnings import warn from . import output, parse, io from .exceptions import ( @@ -447,7 +448,10 @@ def sort_file( if not apply_changes: return tmp_file.replace(source_file.path) - + except ExistingSyntaxErrors: + warn("{file_path} unable to sort due to existing syntax errors") + except IntroducedSyntaxErrors: + warn("{file_path} unable to sort as isort introduces new syntax errors") finally: try: # Python 3.8+: use `missing_ok=True` instead of try except. tmp_file.unlink() From bccf7c3eb029c9449d68d3b47b1095efc75b785d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 25 Feb 2020 19:36:59 -0800 Subject: [PATCH 0434/1439] Add show diff flag --- isort/api.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/isort/api.py b/isort/api.py index a492359ef..17f4ca458 100644 --- a/isort/api.py +++ b/isort/api.py @@ -421,6 +421,7 @@ def sort_file( file_path: Optional[Path] = None, disregard_skip: bool = False, ask_to_apply: bool = False, + show_diff: bool = False, **config_kwargs, ): with io.read_file(filename) as source_file: @@ -437,17 +438,16 @@ def sort_file( **config_kwargs ) if changed: - if ask_to_apply: + if show_diff or ask_to_apply: source_file.stream.seek(0) show_unified_diff( file_input=source_file.stream.read(), file_output=tmp_file.read_text(encoding=source_file.encoding), file_path=source_file.path ) - apply_changes = ask_whether_to_apply_changes_to_file(str(source_file.path)) - if not apply_changes: + if ask_to_apply and not ask_whether_to_apply_changes_to_file(str(source_file.path)): return - tmp_file.replace(source_file.path) + tmp_file.replace(source_file.path) except ExistingSyntaxErrors: warn("{file_path} unable to sort due to existing syntax errors") except IntroducedSyntaxErrors: From 1920a6c47796bec4ddc2141dbaece6b44a93834e Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 26 Feb 2020 21:54:28 -0800 Subject: [PATCH 0435/1439] Move main to use new isort file api --- isort/main.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/isort/main.py b/isort/main.py index 447d4a15c..6517c1cea 100644 --- a/isort/main.py +++ b/isort/main.py @@ -72,9 +72,9 @@ def __init__(self, incorrectly_sorted: bool, skipped: bool) -> None: def sort_imports(file_name: str, config: Config, check: bool = False, **arguments: Any) -> Optional[SortAttempt]: try: + incorrectly_sorted: bool = False + skipped: bool = False if check: - incorrectly_sorted: bool = False - skipped: bool = False try: arguments.pop("ask_to_apply") arguments.pop("write_to_stdout") @@ -84,8 +84,11 @@ def sort_imports(file_name: str, config: Config, check: bool = False, **argument skipped = True return SortAttempt(incorrectly_sorted, skipped) else: - result = SortImports(file_name, **arguments) - return SortAttempt(result.incorrectly_sorted, result.skipped) + try: + incorrectly_sorted = not api.sort_file(file_name=file_name, config=config, **arguments) + except FileSkipped: + skipped = True + return SortAttempt(incorrectly_sorted, skipped) except (OSError, ValueError) as error: warn(f"Unable to parse file {file_name} due to {error}") return None From e735367a9a6c83c7660d2bcb9c29c8256af71031 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 28 Feb 2020 23:17:16 -0800 Subject: [PATCH 0436/1439] Add support for stdout output --- isort/api.py | 50 +++++++++++++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/isort/api.py b/isort/api.py index 17f4ca458..027cdd97f 100644 --- a/isort/api.py +++ b/isort/api.py @@ -422,38 +422,50 @@ def sort_file( disregard_skip: bool = False, ask_to_apply: bool = False, show_diff: bool = False, + output_to_stdout: bool = False **config_kwargs, ): with io.read_file(filename) as source_file: - tmp_file = source_file.path.with_suffix(source_file.path.suffix + ".isorted") changed: bool = False try: - with tmp_file.open("w", encoding=source_file.encoding, newline="") as output_stream: + if output_to_stdout: changed = sorted_imports( input_stream=source_file.stream, - output_stream=output_stream, + output_stream=sys.stdout, config=config, file_path=source_file.path, disregard_skip=disregard_skip, **config_kwargs ) - if changed: - if show_diff or ask_to_apply: - source_file.stream.seek(0) - show_unified_diff( - file_input=source_file.stream.read(), - file_output=tmp_file.read_text(encoding=source_file.encoding), - file_path=source_file.path - ) - if ask_to_apply and not ask_whether_to_apply_changes_to_file(str(source_file.path)): - return - tmp_file.replace(source_file.path) + else: + tmp_file = source_file.path.with_suffix(source_file.path.suffix + ".isorted") + try: + with tmp_file.open("w", encoding=source_file.encoding, newline="") as output_stream: + changed = sorted_imports( + input_stream=source_file.stream, + output_stream=output_stream, + config=config, + file_path=source_file.path, + disregard_skip=disregard_skip, + **config_kwargs + ) + if changed: + if show_diff or ask_to_apply: + source_file.stream.seek(0) + show_unified_diff( + file_input=source_file.stream.read(), + file_output=tmp_file.read_text(encoding=source_file.encoding), + file_path=source_file.path + ) + if ask_to_apply and not ask_whether_to_apply_changes_to_file(str(source_file.path)): + return + tmp_file.replace(source_file.path) + finally: + try: # Python 3.8+: use `missing_ok=True` instead of try except. + tmp_file.unlink() + except FileNotFoundError: + pass except ExistingSyntaxErrors: warn("{file_path} unable to sort due to existing syntax errors") except IntroducedSyntaxErrors: warn("{file_path} unable to sort as isort introduces new syntax errors") - finally: - try: # Python 3.8+: use `missing_ok=True` instead of try except. - tmp_file.unlink() - except FileNotFoundError: - pass From 0c8a6495ca90b16cfe40d64a29c511d5181d2cb1 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 29 Feb 2020 09:15:54 -0800 Subject: [PATCH 0437/1439] Add missing sys import --- isort/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/isort/api.py b/isort/api.py index 027cdd97f..cb37ad54b 100644 --- a/isort/api.py +++ b/isort/api.py @@ -1,4 +1,5 @@ import textwrap +import sys from io import StringIO from itertools import chain from pathlib import Path From dd8cb23d5e17be43e195e00bc8e42f16ed0c0372 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 1 Mar 2020 22:39:10 -0800 Subject: [PATCH 0438/1439] Formatting black --- isort/api.py | 27 ++++++++++++++++++--------- isort/main.py | 10 +++++++--- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/isort/api.py b/isort/api.py index cb37ad54b..879aa7fa8 100644 --- a/isort/api.py +++ b/isort/api.py @@ -13,7 +13,12 @@ FileSkipSetting, IntroducedSyntaxErrors, ) -from .format import format_natural, remove_whitespace, show_unified_diff, ask_whether_to_apply_changes_to_file +from .format import ( + format_natural, + remove_whitespace, + show_unified_diff, + ask_whether_to_apply_changes_to_file, +) from .io import Empty, File from .settings import DEFAULT_CONFIG, FILE_SKIP_COMMENTS, Config @@ -412,7 +417,8 @@ def check_file( config=config, file_path=source_file.path, disregard_skip=disregard_skip, - **config_kwargs) + **config_kwargs, + ) def sort_file( @@ -423,8 +429,7 @@ def sort_file( disregard_skip: bool = False, ask_to_apply: bool = False, show_diff: bool = False, - output_to_stdout: bool = False - **config_kwargs, + output_to_stdout: bool = False ** config_kwargs, ): with io.read_file(filename) as source_file: changed: bool = False @@ -436,19 +441,21 @@ def sort_file( config=config, file_path=source_file.path, disregard_skip=disregard_skip, - **config_kwargs + **config_kwargs, ) else: tmp_file = source_file.path.with_suffix(source_file.path.suffix + ".isorted") try: - with tmp_file.open("w", encoding=source_file.encoding, newline="") as output_stream: + with tmp_file.open( + "w", encoding=source_file.encoding, newline="" + ) as output_stream: changed = sorted_imports( input_stream=source_file.stream, output_stream=output_stream, config=config, file_path=source_file.path, disregard_skip=disregard_skip, - **config_kwargs + **config_kwargs, ) if changed: if show_diff or ask_to_apply: @@ -456,9 +463,11 @@ def sort_file( show_unified_diff( file_input=source_file.stream.read(), file_output=tmp_file.read_text(encoding=source_file.encoding), - file_path=source_file.path + file_path=source_file.path, ) - if ask_to_apply and not ask_whether_to_apply_changes_to_file(str(source_file.path)): + if ask_to_apply and not ask_whether_to_apply_changes_to_file( + str(source_file.path) + ): return tmp_file.replace(source_file.path) finally: diff --git a/isort/main.py b/isort/main.py index 6517c1cea..555fd70e2 100644 --- a/isort/main.py +++ b/isort/main.py @@ -70,7 +70,9 @@ def __init__(self, incorrectly_sorted: bool, skipped: bool) -> None: self.skipped = skipped -def sort_imports(file_name: str, config: Config, check: bool = False, **arguments: Any) -> Optional[SortAttempt]: +def sort_imports( + file_name: str, config: Config, check: bool = False, **arguments: Any +) -> Optional[SortAttempt]: try: incorrectly_sorted: bool = False skipped: bool = False @@ -85,7 +87,9 @@ def sort_imports(file_name: str, config: Config, check: bool = False, **argument return SortAttempt(incorrectly_sorted, skipped) else: try: - incorrectly_sorted = not api.sort_file(file_name=file_name, config=config, **arguments) + incorrectly_sorted = not api.sort_file( + file_name=file_name, config=config, **arguments + ) except FileSkipped: skipped = True return SortAttempt(incorrectly_sorted, skipped) @@ -623,7 +627,7 @@ def main(argv: Optional[Sequence[str]] = None) -> None: config=config, check=check, ask_to_apply=ask_to_apply, - write_to_stdout=write_to_stdout + write_to_stdout=write_to_stdout, ), file_names, ) From 115d2dd6a94037ebfe8bb449985181a1cd49d733 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 2 Mar 2020 22:35:25 -0800 Subject: [PATCH 0439/1439] Update to use new api.sort_file API endpoint --- isort/api.py | 3 ++- isort/main.py | 8 +++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/isort/api.py b/isort/api.py index 879aa7fa8..f9ddba04c 100644 --- a/isort/api.py +++ b/isort/api.py @@ -429,7 +429,8 @@ def sort_file( disregard_skip: bool = False, ask_to_apply: bool = False, show_diff: bool = False, - output_to_stdout: bool = False ** config_kwargs, + output_to_stdout: bool = False, + **config_kwargs, ): with io.read_file(filename) as source_file: changed: bool = False diff --git a/isort/main.py b/isort/main.py index 555fd70e2..7b80331b8 100644 --- a/isort/main.py +++ b/isort/main.py @@ -71,16 +71,14 @@ def __init__(self, incorrectly_sorted: bool, skipped: bool) -> None: def sort_imports( - file_name: str, config: Config, check: bool = False, **arguments: Any + file_name: str, config: Config, check: bool = False, ask_to_apply: bool=False, write_to_stdout:bool=False, **arguments: Any ) -> Optional[SortAttempt]: + arguments.pop("settings_path") try: incorrectly_sorted: bool = False skipped: bool = False if check: try: - arguments.pop("ask_to_apply") - arguments.pop("write_to_stdout") - arguments.pop("settings_path") incorrectly_sorted = not api.check_file(file_name, config=config, **arguments) except FileSkipped: skipped = True @@ -88,7 +86,7 @@ def sort_imports( else: try: incorrectly_sorted = not api.sort_file( - file_name=file_name, config=config, **arguments + file_name=file_name, config=config, ask_to_apply=ask_to_apply, write_to_stdout=write_to_stdout, **arguments ) except FileSkipped: skipped = True From 373bc3c1ac59ded179261042f05b079b0f9acba1 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 3 Mar 2020 23:05:32 -0800 Subject: [PATCH 0440/1439] Update to fix mypy and black linting errors --- isort/api.py | 13 ------------- isort/compat.py | 13 ++++++++----- isort/main.py | 13 +++++++++++-- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/isort/api.py b/isort/api.py index f9ddba04c..a16dff678 100644 --- a/isort/api.py +++ b/isort/api.py @@ -39,7 +39,6 @@ def _config( config_kwargs["settings_path"] = path if config_kwargs and config is not DEFAULT_CONFIG: - breakpoint() raise ValueError( "You can either specify custom configuration options using kwargs or " "passing in a Config object. Not Both!" @@ -137,18 +136,6 @@ def check_imports( return False -def sorted_file(filename: str, config: Config = DEFAULT_CONFIG, **config_kwargs) -> str: - file_data = File.read(filename) - config = _config(path=file_data.path.parent, config=config) - return sorted_imports( - file_contents=file_data.contents, - extension=file_data.extension, - config=config, - file_path=file_data.path, - **config_kwargs, - ) - - def sort_imports( input_stream: TextIO, output_stream: TextIO, diff --git a/isort/compat.py b/isort/compat.py index d3c0eb595..5a8a7176d 100644 --- a/isort/compat.py +++ b/isort/compat.py @@ -7,7 +7,7 @@ from . import api from .exceptions import ExistingSyntaxErrors, FileSkipped, IntroducedSyntaxErrors from .format import ask_whether_to_apply_changes_to_file, show_unified_diff -from .io import File +from .io import File, read_file from .settings import Config @@ -34,11 +34,14 @@ def __init__( if filename: if file_contents: file_data = File.from_contents(file_contents, filename=filename) + file_stream, file_path, file_encoding = file_data + if not extension: + extension = file_data.extension else: - file_data = File.read(filename) - file_stream, file_path, file_encoding = file_data - if not extension: - extension = file_data.extension + with read_file(filename) as file_data: + file_stream, file_path, file_encoding = file_data + if not extension: + extension = file_data.extension else: file_stream = StringIO(file_contents) diff --git a/isort/main.py b/isort/main.py index 7b80331b8..66b0b7125 100644 --- a/isort/main.py +++ b/isort/main.py @@ -71,7 +71,12 @@ def __init__(self, incorrectly_sorted: bool, skipped: bool) -> None: def sort_imports( - file_name: str, config: Config, check: bool = False, ask_to_apply: bool=False, write_to_stdout:bool=False, **arguments: Any + file_name: str, + config: Config, + check: bool = False, + ask_to_apply: bool = False, + write_to_stdout: bool = False, + **arguments: Any, ) -> Optional[SortAttempt]: arguments.pop("settings_path") try: @@ -86,7 +91,11 @@ def sort_imports( else: try: incorrectly_sorted = not api.sort_file( - file_name=file_name, config=config, ask_to_apply=ask_to_apply, write_to_stdout=write_to_stdout, **arguments + file_name=file_name, + config=config, + ask_to_apply=ask_to_apply, + write_to_stdout=write_to_stdout, + **arguments, ) except FileSkipped: skipped = True From 045057d9fe5b90d9e7b57c3dbe8bd4641781fc89 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 4 Mar 2020 23:05:20 -0800 Subject: [PATCH 0441/1439] Fix usage of read_file in compat to not use out of scope stream --- isort/compat.py | 1 + 1 file changed, 1 insertion(+) diff --git a/isort/compat.py b/isort/compat.py index 5a8a7176d..a7561b7a7 100644 --- a/isort/compat.py +++ b/isort/compat.py @@ -40,6 +40,7 @@ def __init__( else: with read_file(filename) as file_data: file_stream, file_path, file_encoding = file_data + file_stream = StringIO(file_stream.read()) if not extension: extension = file_data.extension else: From 333ffa3f7868a256581c1bf68ed1ed0c0c0a87b9 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 4 Mar 2020 23:14:43 -0800 Subject: [PATCH 0442/1439] Fix usage of sort_file --- isort/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/main.py b/isort/main.py index 66b0b7125..3b97df659 100644 --- a/isort/main.py +++ b/isort/main.py @@ -78,7 +78,7 @@ def sort_imports( write_to_stdout: bool = False, **arguments: Any, ) -> Optional[SortAttempt]: - arguments.pop("settings_path") + arguments.pop("settings_path", None) try: incorrectly_sorted: bool = False skipped: bool = False @@ -91,7 +91,7 @@ def sort_imports( else: try: incorrectly_sorted = not api.sort_file( - file_name=file_name, + file_name, config=config, ask_to_apply=ask_to_apply, write_to_stdout=write_to_stdout, From a6ada7f39449ddac91fa94bb7e56d326c4734aac Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 5 Mar 2020 23:41:26 -0800 Subject: [PATCH 0443/1439] Make write_to_stdout parameter consistent --- isort/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/api.py b/isort/api.py index a16dff678..b1ed88952 100644 --- a/isort/api.py +++ b/isort/api.py @@ -416,13 +416,13 @@ def sort_file( disregard_skip: bool = False, ask_to_apply: bool = False, show_diff: bool = False, - output_to_stdout: bool = False, + write_to_stdout: bool = False, **config_kwargs, ): with io.read_file(filename) as source_file: changed: bool = False try: - if output_to_stdout: + if write_to_stdout: changed = sorted_imports( input_stream=source_file.stream, output_stream=sys.stdout, From c321b2a6797e535c4e155bb9d5ed20ac6cbd1f2a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 6 Mar 2020 22:20:09 -0800 Subject: [PATCH 0444/1439] Report files as they are fixed --- isort/api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/isort/api.py b/isort/api.py index b1ed88952..6bd083fc6 100644 --- a/isort/api.py +++ b/isort/api.py @@ -458,6 +458,8 @@ def sort_file( ): return tmp_file.replace(source_file.path) + if not config.quiet: + print(f"Fixing {source_file.path}") finally: try: # Python 3.8+: use `missing_ok=True` instead of try except. tmp_file.unlink() From 1a7a967b0e3e5d9e6315f78e08e21ef9f6fc48a1 Mon Sep 17 00:00:00 2001 From: Hugo Date: Mon, 9 Mar 2020 11:34:49 +0200 Subject: [PATCH 0445/1439] use v2 of actions/checkout --- .github/workflows/lint.yml | 2 +- .github/workflows/test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index bb72eba27..06780574e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -10,7 +10,7 @@ jobs: python-version: [3.8] steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - name: pip cache uses: actions/cache@v1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0b18d96e5..54c1abce0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ jobs: os: [ubuntu-18.04, ubuntu-16.04, macos-latest] steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - name: Ubuntu cache uses: actions/cache@v1 From 42732ffefc00f4af7d92e1ee33cb309a4f362bab Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 9 Mar 2020 22:24:25 -0700 Subject: [PATCH 0446/1439] Switch stdin '-' endpoint to use streaming API --- isort/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/main.py b/isort/main.py index 3b97df659..e3bf69afe 100644 --- a/isort/main.py +++ b/isort/main.py @@ -584,7 +584,7 @@ def main(argv: Optional[Sequence[str]] = None) -> None: print(QUICK_GUIDE) return elif file_names == ["-"] and not show_config: - SortImports(file_contents=sys.stdin.read(), write_to_stdout=True, **arguments) + api.sorted_imports(input_stream=sys.stdin, output_stream=sys.stdout, **arguments) return if "settings_path" not in arguments: From e6ac8970ce1a5b43f943cac735a66b177e647ebe Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 10 Mar 2020 23:15:11 -0700 Subject: [PATCH 0447/1439] Add string sorting endpoint --- isort/api.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/isort/api.py b/isort/api.py index 6bd083fc6..01fbee323 100644 --- a/isort/api.py +++ b/isort/api.py @@ -49,6 +49,29 @@ def _config( return config +def sort_code_string( + code: str, + extension="py", + config=DEFAULT_CONFIG, + file_path: Optional[Path] = None, + disregard_skip: bool = False, + **config_kwargs, +): + input_stream = StringIO(code) + output_stream = StringIO() + sorted_imports( + input_stream, + output_stream, + extension=extension, + config=config, + file_path=file_path, + disregard_skip=disregard_skip, + **config_kwargs, + ) + output_stream.seek(0) + return output_stream.read() + + def sorted_imports( input_stream: TextIO, output_stream: TextIO, From e687078ce433299da125975fc5461a8d9dbe1035 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 11 Mar 2020 23:34:05 -0700 Subject: [PATCH 0448/1439] Start transition to using API directly from tests --- tests/test_isort.py | 755 ++++++++++++++++++++------------------------ 1 file changed, 337 insertions(+), 418 deletions(-) diff --git a/tests/test_isort.py b/tests/test_isort.py index 3e86b4487..45e5385ac 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -14,7 +14,7 @@ import py import pytest -from isort import finders, main, sections +from isort import finders, main, sections, api from isort.main import SortImports, is_python_file from isort.settings import WrapModes, Config from isort.utils import exists_case_sensitive @@ -64,7 +64,7 @@ def default_settings_path(tmpdir_factory) -> Iterator[str]: def test_happy_path() -> None: """Test the most basic use case, straight imports no code, simply not organized by category.""" test_input = "import sys\nimport os\nimport myproject.test\nimport django.settings" - test_output = SortImports(file_contents=test_input, known_third_party=["django"]).output + test_output = api.sort_code_string(test_input, known_third_party=["django"]) assert test_output == ( "import os\n" "import sys\n" "\n" "import django.settings\n" "\n" "import myproject.test\n" ) @@ -83,7 +83,7 @@ def test_code_intermixed() -> None: "print('I like to put code between imports cause I want stuff to break')\n" "import myproject.test\n" ) - test_output = SortImports(file_contents=test_input).output + test_output = api.sort_code_string(test_input) assert test_output == ( "import sys\n" "\n" @@ -101,45 +101,39 @@ def test_correct_space_between_imports() -> None: """ test_input_method = "import sys\ndef my_method():\n print('hello world')\n" - test_output_method = SortImports(file_contents=test_input_method).output + test_output_method = api.sort_code_string(test_input_method) assert test_output_method == ("import sys\n\n\ndef my_method():\n print('hello world')\n") test_input_decorator = ( "import sys\n" "@my_decorator\n" "def my_method():\n" " print('hello world')\n" ) - test_output_decorator = SortImports(file_contents=test_input_decorator).output + test_output_decorator = api.sort_code_string(test_input_decorator) assert test_output_decorator == ( "import sys\n" "\n" "\n" "@my_decorator\n" "def my_method():\n" " print('hello world')\n" ) test_input_class = "import sys\nclass MyClass(object):\n pass\n" - test_output_class = SortImports(file_contents=test_input_class).output + test_output_class = api.sort_code_string(test_input_class) assert test_output_class == "import sys\n\n\nclass MyClass(object):\n pass\n" test_input_other = "import sys\nprint('yo')\n" - test_output_other = SortImports(file_contents=test_input_other).output + test_output_other = api.sort_code_string(test_input_other) assert test_output_other == "import sys\n\nprint('yo')\n" def test_sort_on_number() -> None: """Ensure numbers get sorted logically (10 > 9 not the other way around)""" test_input = "import lib10\nimport lib9\n" - test_output = SortImports(file_contents=test_input).output + test_output = api.sort_code_string(test_input) assert test_output == "import lib9\nimport lib10\n" def test_line_length() -> None: """Ensure isort enforces the set line_length.""" - assert ( - len(SortImports(file_contents=REALLY_LONG_IMPORT, line_length=80).output.split("\n")[0]) - <= 80 - ) - assert ( - len(SortImports(file_contents=REALLY_LONG_IMPORT, line_length=120).output.split("\n")[0]) - <= 120 - ) + assert len(api.sort_code_string(REALLY_LONG_IMPORT, line_length=80).split("\n")[0]) <= 80 + assert len(api.sort_code_string(REALLY_LONG_IMPORT, line_length=120).split("\n")[0]) <= 120 - test_output = SortImports(file_contents=REALLY_LONG_IMPORT, line_length=42).output + test_output = api.sort_code_string(REALLY_LONG_IMPORT, line_length=42) assert test_output == ( "from third_party import (lib1, lib2, lib3,\n" " lib4, lib5, lib6,\n" @@ -165,13 +159,11 @@ def test_line_length() -> None: line_length=79, multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, balanced_wrapping=False, - ).output + ) == test_input ) - test_output = SortImports( - file_contents=REALLY_LONG_IMPORT, line_length=42, wrap_length=32 - ).output + test_output = SortImports(file_contents=REALLY_LONG_IMPORT, line_length=42, wrap_length=32) assert test_output == ( "from third_party import (lib1,\n" " lib2,\n" @@ -200,18 +192,15 @@ def test_line_length() -> None: "from .test import a_very_long_function_name_that_exceeds_the_normal_pep8_line_length\n" ) with pytest.raises(ValueError): - test_output = SortImports( - file_contents=REALLY_LONG_IMPORT, line_length=80, wrap_length=99 - ).output + test_output = SortImports(file_contents=REALLY_LONG_IMPORT, line_length=80, wrap_length=99) test_output = ( - SortImports(file_contents=REALLY_LONG_IMPORT, line_length=100, wrap_length=99).output - == test_input + api.sort_code_string(REALLY_LONG_IMPORT, line_length=100, wrap_length=99) == test_input ) # Test Case described in issue #1015 test_output = SortImports( file_contents=REALLY_LONG_IMPORT, line_length=25, multi_line_output=WrapModes.HANGING_INDENT - ).output + ) assert test_output == ( "from third_party import \\\n" " lib1, lib2, lib3, \\\n" @@ -230,7 +219,7 @@ def test_output_modes() -> None: """Test setting isort to use various output modes works as expected""" test_output_grid = SortImports( file_contents=REALLY_LONG_IMPORT, multi_line_output=WrapModes.GRID, line_length=40 - ).output + ) assert test_output_grid == ( "from third_party import (lib1, lib2,\n" " lib3, lib4,\n" @@ -247,7 +236,7 @@ def test_output_modes() -> None: test_output_vertical = SortImports( file_contents=REALLY_LONG_IMPORT, multi_line_output=WrapModes.VERTICAL, line_length=40 - ).output + ) assert test_output_vertical == ( "from third_party import (lib1,\n" " lib2,\n" @@ -276,7 +265,7 @@ def test_output_modes() -> None: file_contents=REALLY_LONG_IMPORT_WITH_COMMENT, multi_line_output=WrapModes.VERTICAL, line_length=40, - ).output + ) assert comment_output_vertical == ( "from third_party import (lib1, # comment\n" " lib2,\n" @@ -306,7 +295,7 @@ def test_output_modes() -> None: multi_line_output=WrapModes.HANGING_INDENT, line_length=40, indent=" ", - ).output + ) assert test_output_hanging_indent == ( "from third_party import lib1, lib2, \\\n" " lib3, lib4, lib5, lib6, lib7, \\\n" @@ -320,7 +309,7 @@ def test_output_modes() -> None: multi_line_output=WrapModes.HANGING_INDENT, line_length=40, indent=" ", - ).output + ) assert comment_output_hanging_indent == ( "from third_party import lib1, \\ # comment\n" " lib2, lib3, lib4, lib5, lib6, \\\n" @@ -334,7 +323,7 @@ def test_output_modes() -> None: multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, line_length=40, indent=" ", - ).output + ) assert test_output_vertical_indent == ( "from third_party import (\n" " lib1,\n" @@ -366,7 +355,7 @@ def test_output_modes() -> None: multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, line_length=40, indent=" ", - ).output + ) assert comment_output_vertical_indent == ( "from third_party import ( # comment\n" " lib1,\n" @@ -398,7 +387,7 @@ def test_output_modes() -> None: multi_line_output=WrapModes.VERTICAL_GRID, line_length=40, indent=" ", - ).output + ) assert test_output_vertical_grid == ( "from third_party import (\n" " lib1, lib2, lib3, lib4, lib5, lib6,\n" @@ -412,7 +401,7 @@ def test_output_modes() -> None: multi_line_output=WrapModes.VERTICAL_GRID, line_length=40, indent=" ", - ).output + ) assert comment_output_vertical_grid == ( "from third_party import ( # comment\n" " lib1, lib2, lib3, lib4, lib5, lib6,\n" @@ -426,7 +415,7 @@ def test_output_modes() -> None: multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, line_length=40, indent=" ", - ).output + ) assert test_output_vertical_grid_grouped == ( "from third_party import (\n" " lib1, lib2, lib3, lib4, lib5, lib6,\n" @@ -441,7 +430,7 @@ def test_output_modes() -> None: multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, line_length=40, indent=" ", - ).output + ) assert comment_output_vertical_grid_grouped == ( "from third_party import ( # comment\n" " lib1, lib2, lib3, lib4, lib5, lib6,\n" @@ -453,7 +442,7 @@ def test_output_modes() -> None: output_noqa = SortImports( file_contents=REALLY_LONG_IMPORT_WITH_COMMENT, multi_line_output=WrapModes.NOQA - ).output + ) assert output_noqa == ( "from third_party import lib1, lib2, lib3, lib4, lib5, lib6, lib7," " lib8, lib9, lib10, lib11," @@ -466,7 +455,7 @@ def test_output_modes() -> None: multi_line_output=WrapModes.VERTICAL_GRID_GROUPED_NO_COMMA, line_length=40, indent=" ", - ).output + ) test_output_vertical_grid_grouped_doesnt_wrap_early = test_case assert test_output_vertical_grid_grouped_doesnt_wrap_early == ( "from third_party import (\n lib1, lib2, lib3, lib4, lib5, lib5ab\n)\n" @@ -476,7 +465,7 @@ def test_output_modes() -> None: file_contents=REALLY_LONG_IMPORT, multi_line_output=WrapModes.VERTICAL_PREFIX_FROM_MODULE_IMPORT, line_length=40, - ).output + ) assert test_output_prefix_from_module == ( "from third_party import lib1, lib2\n" "from third_party import lib3, lib4\n" @@ -496,7 +485,7 @@ def test_output_modes() -> None: multi_line_output=WrapModes.VERTICAL_PREFIX_FROM_MODULE_IMPORT, line_length=40, indent=" ", - ).output + ) assert test_output_prefix_from_module_with_comment == ( "from third_party import lib1 # comment\n" "from third_party import lib2, lib3\n" @@ -516,7 +505,7 @@ def test_output_modes() -> None: " from allennlp.modules.text_field_embedders.basic_text_field_embedder" " import BasicTextFieldEmbedder" ) - test_output = SortImports(file_contents=test_input, line_length=100).output + test_output = api.sort_code_string(test_input, line_length=100) assert test_output == ( "def a():\n" " from allennlp.modules.text_field_embedders.basic_text_field_embedder import \\\n" @@ -534,7 +523,7 @@ def test_output_modes() -> None: " from allennlp.common.registrable import Registrable" " # import here to avoid circular imports\n" ) - test_output = SortImports(file_contents=test_input, line_length=100).output + test_output = api.sort_code_string(test_input, line_length=100) assert test_output == test_input @@ -542,13 +531,13 @@ def test_qa_comment_case() -> None: test_input = "from veryveryveryveryveryveryveryveryveryveryvery import X # NOQA" test_output = SortImports( file_contents=test_input, line_length=40, multi_line_output=WrapModes.NOQA - ).output + ) assert test_output == "from veryveryveryveryveryveryveryveryveryveryvery import X # NOQA\n" test_input = "import veryveryveryveryveryveryveryveryveryveryvery # NOQA" test_output = SortImports( file_contents=test_input, line_length=40, multi_line_output=WrapModes.NOQA - ).output + ) assert test_output == "import veryveryveryveryveryveryveryveryveryveryvery # NOQA\n" @@ -560,7 +549,7 @@ def test_length_sort() -> None: "import looooooooooooooooooooooooooooooooooooooong\n" "import medium_sizeeeeeeeeeeeeea\n" ) - test_output = SortImports(file_contents=test_input, length_sort=True).output + test_output = api.sort_code_string(test_input, length_sort=True) assert test_output == ( "import shortie\n" "import medium_sizeeeeeeeeeeeeea\n" @@ -579,7 +568,7 @@ def test_length_sort_section() -> None: "import looooooooooooooooooooooooooooooooooooooong\n" "import medium_sizeeeeeeeeeeeeea\n" ) - test_output = SortImports(file_contents=test_input, length_sort_sections=("stdlib",)).output + test_output = api.sort_code_string(test_input, length_sort_sections=("stdlib",)) assert test_output == ( "import os\n" "import sys\n" @@ -603,7 +592,7 @@ def test_convert_hanging() -> None: ) test_output = SortImports( file_contents=test_input, multi_line_output=WrapModes.GRID, line_length=40 - ).output + ) assert test_output == ( "from third_party import (lib1, lib2,\n" " lib3, lib4,\n" @@ -627,7 +616,7 @@ def test_custom_indent() -> None: line_length=40, indent=" ", balanced_wrapping=False, - ).output + ) assert test_output == ( "from third_party import lib1, lib2, \\\n" " lib3, lib4, lib5, lib6, lib7, lib8, \\\n" @@ -642,7 +631,7 @@ def test_custom_indent() -> None: line_length=40, indent="' '", balanced_wrapping=False, - ).output + ) assert test_output == ( "from third_party import lib1, lib2, \\\n" " lib3, lib4, lib5, lib6, lib7, lib8, \\\n" @@ -657,7 +646,7 @@ def test_custom_indent() -> None: line_length=40, indent="tab", balanced_wrapping=False, - ).output + ) assert test_output == ( "from third_party import lib1, lib2, \\\n" "\tlib3, lib4, lib5, lib6, lib7, lib8, \\\n" @@ -672,7 +661,7 @@ def test_custom_indent() -> None: line_length=40, indent=2, balanced_wrapping=False, - ).output + ) assert test_output == ( "from third_party import lib1, lib2, \\\n" " lib3, lib4, lib5, lib6, lib7, lib8, \\\n" @@ -687,7 +676,7 @@ def test_use_parentheses() -> None: "from fooooooooooooooooooooooooo.baaaaaaaaaaaaaaaaaaarrrrrrr import " " my_custom_function as my_special_function" ) - test_output = SortImports(file_contents=test_input, line_length=79, use_parentheses=True).output + test_output = api.sort_code_string(test_input, line_length=79, use_parentheses=True) assert test_output == ( "from fooooooooooooooooooooooooo.baaaaaaaaaaaaaaaaaaarrrrrrr import (\n" @@ -696,7 +685,7 @@ def test_use_parentheses() -> None: 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" @@ -708,7 +697,7 @@ def test_use_parentheses() -> None: line_length=79, use_parentheses=True, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, - ).output + ) assert test_output == ( "from fooooooooooooooooooooooooo.baaaaaaaaaaaaaaaaaaarrrrrrr import (\n" @@ -721,7 +710,7 @@ def test_use_parentheses() -> None: use_parentheses=True, multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, include_trailing_comma=True, - ).output + ) assert test_output == ( "from fooooooooooooooooooooooooo.baaaaaaaaaaaaaaaaaaarrrrrrr import (\n" @@ -738,7 +727,7 @@ def test_skip() -> None: "import sys # isort: skip this import needs to be placed here\n\n\n\n\n\n\n" ) - test_output = SortImports(file_contents=test_input, known_third_party=["django"]).output + test_output = api.sort_code_string(test_input, known_third_party=["django"]) assert test_output == ( "import django\n" "\n" @@ -763,7 +752,7 @@ def test_skip_with_file_name() -> None: def test_skip_within_file() -> None: """Ensure skipping a whole file works.""" test_input = "# isort: skip_file\nimport django\nimport myproject\n" - sort_imports = SortImports(file_contents=test_input, known_third_party=["django"]) + sort_imports = api.sort_code_string(test_input, known_third_party=["django"]) assert sort_imports.skipped assert sort_imports.output == "" @@ -771,16 +760,14 @@ def test_skip_within_file() -> None: def test_force_to_top() -> None: """Ensure forcing a single import to the top of its category works as expected.""" test_input = "import lib6\nimport lib2\nimport lib5\nimport lib1\n" - test_output = SortImports(file_contents=test_input, force_to_top=["lib5"]).output + test_output = api.sort_code_string(test_input, force_to_top=["lib5"]) assert test_output == "import lib5\nimport lib1\nimport lib2\nimport lib6\n" def test_add_imports() -> None: """Ensures adding imports works as expected.""" test_input = "import lib6\nimport lib2\nimport lib5\nimport lib1\n\n" - test_output = SortImports( - file_contents=test_input, add_imports=["import lib4", "import lib7"] - ).output + test_output = SortImports(file_contents=test_input, add_imports=["import lib4", "import lib7"]) assert test_output == ( "import lib1\n" "import lib2\n" @@ -792,9 +779,7 @@ def test_add_imports() -> None: # Using simplified syntax test_input = "import lib6\nimport lib2\nimport lib5\nimport lib1\n\n" - test_output = SortImports( - file_contents=test_input, add_imports=["lib4", "lib7", "lib8.a"] - ).output + test_output = SortImports(file_contents=test_input, add_imports=["lib4", "lib7", "lib8.a"]) assert test_output == ( "import lib1\n" "import lib2\n" @@ -809,7 +794,7 @@ def test_add_imports() -> None: test_input = '"""Module docstring"""\n' "\nclass MyClass(object):\n pass\n" test_output = SortImports( file_contents=test_input, add_imports=["from __future__ import print_function"] - ).output + ) assert test_output == ( '"""Module docstring"""\n' "from __future__ import print_function\n" @@ -823,28 +808,26 @@ def test_add_imports() -> None: test_input = "class MyClass(object):\n pass\n" test_output = SortImports( file_contents=test_input, add_imports=["from __future__ import print_function"] - ).output + ) assert test_output == ( "from __future__ import print_function\n" "\n" "\n" "class MyClass(object):\n" " pass\n" ) # On a file with no content what so ever test_input = "" - test_output = SortImports(file_contents=test_input, add_imports=["lib4"]).output + test_output = api.sort_code_string(test_input, add_imports=["lib4"]) assert test_output == ("") # On a file with no content what so ever, after force_adds is set to True test_input = "" - test_output = SortImports( - file_contents=test_input, add_imports=["lib4"], force_adds=True - ).output + test_output = SortImports(file_contents=test_input, add_imports=["lib4"], force_adds=True) assert test_output == ("import lib4\n") def test_remove_imports() -> None: """Ensures removing imports works as expected.""" test_input = "import lib6\nimport lib2\nimport lib5\nimport lib1" - test_output = SortImports(file_contents=test_input, remove_imports=["lib2", "lib6"]).output + test_output = api.sort_code_string(test_input, remove_imports=["lib2", "lib6"]) assert test_output == "import lib1\nimport lib5\n" # Using natural syntax @@ -854,14 +837,14 @@ def test_remove_imports() -> None: test_output = SortImports( file_contents=test_input, remove_imports=["import lib2", "import lib6", "from lib8 import a"], - ).output + ) assert test_output == "import lib1\nimport lib5\n" def test_explicitly_local_import() -> None: """Ensure that explicitly local imports are separated.""" test_input = "import lib1\nimport lib2\nimport .lib6\nfrom . import lib7" - assert SortImports(file_contents=test_input).output == ( + assert api.sort_code_string(test_input) == ( "import lib1\nimport lib2\n\nimport .lib6\nfrom . import lib7\n" ) @@ -869,24 +852,22 @@ def test_explicitly_local_import() -> None: def test_quotes_in_file() -> None: """Ensure imports within triple quotes don't get imported.""" test_input = "import os\n\n" '"""\n' "Let us\nimport foo\nokay?\n" '"""\n' - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input test_input = "import os\n\n" '\'"""\'\n' "import foo\n" - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input test_input = "import os\n\n" '"""Let us"""\n' "import foo\n\n" '"""okay?"""\n' - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input test_input = "import os\n\n" '#"""\n' "import foo\n" '#"""' - assert SortImports(file_contents=test_input).output == ( - 'import os\n\nimport foo\n\n#"""\n#"""\n' - ) + assert api.sort_code_string(test_input) == ('import os\n\nimport foo\n\n#"""\n#"""\n') test_input = "import os\n\n'\\\nimport foo'\n" - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input test_input = "import os\n\n'''\n\\'''\nimport junk\n'''\n" - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input def test_check_newline_in_imports(capsys) -> None: @@ -934,7 +915,7 @@ def test_forced_separate() -> None: known_third_party=["django"], line_length=120, order_by_type=False, - ).output + ) == test_input ) @@ -942,7 +923,7 @@ def test_forced_separate() -> None: assert ( SortImports( file_contents=test_input, forced_separate=[".y"], line_length=120, order_by_type=False - ).output + ) == test_input ) @@ -952,14 +933,14 @@ def test_default_section() -> None: test_input = "import sys\nimport os\nimport myproject.test\nimport django.settings" test_output = SortImports( file_contents=test_input, known_third_party=["django"], default_section="FIRSTPARTY" - ).output + ) assert test_output == ( "import os\n" "import sys\n" "\n" "import django.settings\n" "\n" "import myproject.test\n" ) test_output_custom = SortImports( file_contents=test_input, known_third_party=["django"], default_section="STDLIB" - ).output + ) assert test_output_custom == ( "import myproject.test\n" "import os\n" "import sys\n" "\n" "import django.settings\n" ) @@ -975,7 +956,7 @@ def test_first_party_overrides_standard_section() -> None: ) test_output = SortImports( file_contents=test_input, known_first_party=["profile"], py_version="27" - ).output + ) assert test_output == ( "import os\n" "import sys\n" @@ -988,7 +969,7 @@ def test_first_party_overrides_standard_section() -> None: def test_thirdy_party_overrides_standard_section() -> None: """Test to ensure changing the default section works as expected.""" test_input = "import sys\nimport os\nimport profile.test\n" - test_output = SortImports(file_contents=test_input, known_third_party=["profile"]).output + test_output = api.sort_code_string(test_input, known_third_party=["profile"]) assert test_output == "import os\nimport sys\n\nimport profile.test\n" @@ -1007,7 +988,7 @@ def test_known_pattern_path_expansion() -> None: file_contents=test_input, default_section="THIRDPARTY", known_first_party=["./", "this", "kate_plugin", "isort"], - ).output + ) assert test_output == ( "import os\n" "import sys\n" @@ -1032,7 +1013,7 @@ def test_force_single_line_imports() -> None: multi_line_output=WrapModes.GRID, line_length=40, force_single_line=True, - ).output + ) assert test_output == ( "from third_party import lib1\n" "from third_party import lib2\n" @@ -1065,7 +1046,7 @@ def test_force_single_line_imports() -> None: multi_line_output=WrapModes.GRID, line_length=40, force_single_line=True, - ).output + ) assert test_output == ( "from third_party import lib_a\n" "from third_party import lib_b\n" @@ -1081,7 +1062,7 @@ def test_force_single_line_long_imports() -> None: multi_line_output=WrapModes.NOQA, line_length=40, force_single_line=True, - ).output + ) assert test_output == ( "from veryveryveryveryveryvery import big\n" "from veryveryveryveryveryvery import small # NOQA\n" @@ -1098,7 +1079,7 @@ def test_force_single_line_imports_and_sort_within_sections() -> None: line_length=40, force_single_line=True, force_sort_within_sections=True, - ).output + ) assert test_output == ( "from third_party import lib_a\n" "from third_party import lib_b\n" @@ -1112,7 +1093,7 @@ def test_force_single_line_imports_and_sort_within_sections() -> None: force_single_line=True, force_sort_within_sections=True, lexicographical=True, - ).output + ) assert test_output == ( "from third_party import lib_a\n" "from third_party import lib_b\n" @@ -1128,9 +1109,7 @@ def test_force_single_line_imports_and_sort_within_sections() -> None: from matplotlib import pyplot as plt """ test_output = ( - SortImports( - file_contents=test_input, force_sort_within_sections=True, length_sort=True - ).output + SortImports(file_contents=test_input, force_sort_within_sections=True, length_sort=True) == test_input ) @@ -1150,7 +1129,7 @@ def test_titled_imports() -> None: known_third_party=["django"], import_heading_stdlib="Standard Library", import_heading_firstparty="My Stuff", - ).output + ) assert test_output == ( "# Standard Library\n" "import os\n" @@ -1168,7 +1147,7 @@ def test_titled_imports() -> None: known_third_party=["django"], import_heading_stdlib="Standard Library", import_heading_firstparty="My Stuff", - ).output + ) assert test_second_run == test_output @@ -1178,9 +1157,7 @@ def test_balanced_wrapping() -> None: "from __future__ import (absolute_import, division, print_function,\n" " unicode_literals)" ) - test_output = SortImports( - file_contents=test_input, line_length=70, balanced_wrapping=True - ).output + test_output = SortImports(file_contents=test_input, line_length=70, balanced_wrapping=True) assert test_output == ( "from __future__ import (absolute_import, division,\n" " print_function, unicode_literals)\n" @@ -1192,23 +1169,19 @@ def test_relative_import_with_space() -> None: with a space. """ test_input = "from ... fields.sproqet import SproqetCollection" - assert SortImports(file_contents=test_input).output == ( - "from ...fields.sproqet import SproqetCollection\n" - ) + assert api.sort_code_string(test_input) == ("from ...fields.sproqet import SproqetCollection\n") test_input = "from .import foo" test_output = "from . import foo\n" - assert SortImports(file_contents=test_input).output == test_output + assert api.sort_code_string(test_input) == test_output test_input = "from.import foo" test_output = "from . import foo\n" - assert SortImports(file_contents=test_input).output == test_output + assert api.sort_code_string(test_input) == test_output def test_multiline_import() -> None: """Test the case where import spawns multiple lines with inconsistent indentation.""" test_input = "from pkg \\\n import stuff, other_suff \\\n more_stuff" - assert SortImports(file_contents=test_input).output == ( - "from pkg import more_stuff, other_suff, stuff\n" - ) + assert api.sort_code_string(test_input) == ("from pkg import more_stuff, other_suff, stuff\n") # test again with a custom configuration custom_configuration = { @@ -1221,37 +1194,37 @@ def test_multiline_import() -> None: expected_output = ( "from pkg import more_stuff\n" "from pkg import other_suff\n" "from pkg import stuff\n" ) - assert SortImports(file_contents=test_input, **custom_configuration).output == expected_output + assert api.sort_code_string(test_input, **custom_configuration) == expected_output def test_single_multiline() -> None: """Test the case where a single import spawns multiple lines.""" test_input = "from os import\\\n getuid\n\nprint getuid()\n" - output = SortImports(file_contents=test_input).output + output = api.sort_code_string(test_input) assert output == ("from os import getuid\n\nprint getuid()\n") def test_atomic_mode() -> None: # without syntax error, everything works OK test_input = "from b import d, c\nfrom a import f, e\n" - assert SortImports(file_contents=test_input, atomic=True).output == ( + assert api.sort_code_string(test_input, atomic=True) == ( "from a import e, f\nfrom b import c, d\n" ) # with syntax error content is not changed test_input += "while True print 'Hello world'" # blatant syntax error - assert SortImports(file_contents=test_input, atomic=True).output == test_input + assert api.sort_code_string(test_input, atomic=True) == test_input def test_order_by_type() -> None: test_input = "from module import Class, CONSTANT, function" - assert SortImports(file_contents=test_input, order_by_type=True).output == ( + assert api.sort_code_string(test_input, order_by_type=True) == ( "from module import CONSTANT, Class, function\n" ) # More complex sample data test_input = "from module import Class, CONSTANT, function, BASIC, Apple" - assert SortImports(file_contents=test_input, order_by_type=True).output == ( + assert api.sort_code_string(test_input, order_by_type=True) == ( "from module import BASIC, CONSTANT, Apple, Class, function\n" ) @@ -1266,7 +1239,7 @@ def test_order_by_type() -> None: "from subprocess import PIPE, Popen, STDOUT\n" ) - assert SortImports(file_contents=test_input, order_by_type=True, py_version="27").output == ( + assert api.sort_code_string(test_input, order_by_type=True, py_version="27") == ( "import glob\n" "import os\n" "import shutil\n" @@ -1282,10 +1255,10 @@ def test_custom_lines_after_import_section() -> None: test_input = "from a import b\nfoo = 'bar'\n" # default case is one space if not method or class after imports - assert SortImports(file_contents=test_input).output == ("from a import b\n\nfoo = 'bar'\n") + assert api.sort_code_string(test_input) == ("from a import b\n\nfoo = 'bar'\n") # test again with a custom number of lines after the import section - assert SortImports(file_contents=test_input, lines_after_imports=2).output == ( + assert api.sort_code_string(test_input, lines_after_imports=2) == ( "from a import b\n\n\nfoo = 'bar'\n" ) @@ -1294,17 +1267,17 @@ def test_smart_lines_after_import_section() -> None: """Tests the default 'smart' behavior for dealing with lines after the import section""" # one space if not method or class after imports test_input = "from a import b\nfoo = 'bar'\n" - assert SortImports(file_contents=test_input).output == ("from a import b\n\nfoo = 'bar'\n") + assert api.sort_code_string(test_input) == ("from a import b\n\nfoo = 'bar'\n") # two spaces if a method or class after imports test_input = "from a import b\ndef my_function():\n pass\n" - assert SortImports(file_contents=test_input).output == ( + assert api.sort_code_string(test_input) == ( "from a import b\n\n\ndef my_function():\n pass\n" ) # two spaces if an async method after imports test_input = "from a import b\nasync def my_function():\n pass\n" - assert SortImports(file_contents=test_input).output == ( + assert api.sort_code_string(test_input) == ( "from a import b\n\n\nasync def my_function():\n pass\n" ) @@ -1312,7 +1285,7 @@ def test_smart_lines_after_import_section() -> None: test_input = ( "from a import b\n" "# comment should be ignored\n" "def my_function():\n" " pass\n" ) - assert SortImports(file_contents=test_input).output == ( + assert api.sort_code_string(test_input) == ( "from a import b\n" "\n" "\n" @@ -1330,7 +1303,7 @@ def test_smart_lines_after_import_section() -> None: "def my_function():\n" " pass\n" ) - assert SortImports(file_contents=test_input).output == ( + assert api.sort_code_string(test_input) == ( "from a import b\n" "\n" '"""\n' @@ -1342,7 +1315,7 @@ def test_smart_lines_after_import_section() -> None: # Ensure logic doesn't incorrectly skip over assignments to multi-line strings test_input = 'from a import b\nX = """test\n"""\ndef my_function():\n pass\n' - assert SortImports(file_contents=test_input).output == ( + assert api.sort_code_string(test_input) == ( "from a import b\n" "\n" 'X = """test\n' '"""\n' "def my_function():\n" " pass\n" ) @@ -1362,12 +1335,10 @@ def test_combined_from_and_as_imports() -> None: "from translate.storage import base, factory\n" "from translate.storage.placeables import general, parse as rich_parse\n" ) - assert SortImports(file_contents=test_input, combine_as_imports=True).output == test_input + assert api.sort_code_string(test_input, combine_as_imports=True) == test_input test_input = "import os \nimport os as _os" test_output = "import os\nimport os as _os\n" - assert ( - SortImports(file_contents=test_input, keep_direct_and_as_imports=True).output == test_output - ) + assert api.sort_code_string(test_input, keep_direct_and_as_imports=True) == test_output def test_as_imports_with_line_length() -> None: @@ -1376,9 +1347,7 @@ def test_as_imports_with_line_length() -> None: "from translate.storage import base as storage_base\n" "from translate.storage.placeables import general, parse as rich_parse\n" ) - assert SortImports( - file_contents=test_input, combine_as_imports=False, line_length=40 - ).output == ( + assert SortImports(file_contents=test_input, combine_as_imports=False, line_length=40) == ( "from translate.storage import \\\n base as storage_base\n" "from translate.storage.placeables import \\\n general\n" "from translate.storage.placeables import \\\n parse as rich_parse\n" @@ -1389,23 +1358,23 @@ def test_keep_comments() -> None: """Test to ensure isort properly keeps comments in tact after sorting.""" # Straight Import test_input = "import foo # bar\n" - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input # Star import test_input_star = "from foo import * # bar\n" - assert SortImports(file_contents=test_input_star).output == test_input_star + assert api.sort_code_string(test_input_star) == test_input_star # Force Single Line From Import test_input = "from foo import bar # comment\n" - assert SortImports(file_contents=test_input, force_single_line=True).output == test_input + assert api.sort_code_string(test_input, force_single_line=True) == test_input # From import test_input = "from foo import bar # My Comment\n" - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input # More complicated case test_input = "from a import b # My Comment1\nfrom a import c # My Comment2\n" - assert SortImports(file_contents=test_input).output == ( + assert api.sort_code_string(test_input) == ( "from a import b # My Comment1\nfrom a import c # My Comment2\n" ) @@ -1413,7 +1382,7 @@ def test_keep_comments() -> None: test_input = ( "from a import b # My Comment1\n" "from a import c # My Comment2\n" "from a import d\n" ) - assert SortImports(file_contents=test_input, line_length=45).output == ( + assert api.sort_code_string(test_input, line_length=45) == ( "from a import b # My Comment1\n" "from a import c # My Comment2\n" "from a import d\n" ) @@ -1422,14 +1391,14 @@ def test_keep_comments() -> None: "from a import b, c # My Comment1\n" "from a import c, d # My Comment2 is really really really really long\n" ) - assert SortImports(file_contents=test_input, line_length=45).output == ( + assert api.sort_code_string(test_input, line_length=45) == ( "from a import ( # My Comment1; My Comment2 is really really really really long\n" " b, c, d)\n" ) # Test that comments are not stripped from 'import ... as ...' by default test_input = "from a import b as bb # b comment\nfrom a import c as cc # c comment\n" - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input # Test that 'import ... as ...' comments are not collected inappropriately test_input = ( @@ -1437,8 +1406,8 @@ def test_keep_comments() -> None: "from a import c as cc # c comment\n" "from a import d\n" ) - assert SortImports(file_contents=test_input).output == test_input - assert SortImports(file_contents=test_input, combine_as_imports=True).output == ( + assert api.sort_code_string(test_input) == test_input + assert api.sort_code_string(test_input, combine_as_imports=True) == ( "from a import b as bb, c as cc, d # b comment; c comment\n" ) @@ -1451,7 +1420,7 @@ def test_multiline_split_on_dot() -> None: "from my_lib.my_package.test.level_1.level_2.level_3.level_4.level_5.\\\n" " my_module import my_function" ) - assert SortImports(file_contents=test_input, line_length=70).output == ( + assert api.sort_code_string(test_input, line_length=70) == ( "from my_lib.my_package.test.level_1.level_2.level_3.level_4.level_5.my_module import \\\n" " my_function\n" ) @@ -1460,12 +1429,8 @@ def test_multiline_split_on_dot() -> None: def test_import_star() -> None: """Test to ensure isort handles star imports correctly""" test_input = "from blah import *\nfrom blah import _potato\n" - assert SortImports(file_contents=test_input).output == ( - "from blah import *\nfrom blah import _potato\n" - ) - assert SortImports(file_contents=test_input, combine_star=True).output == ( - "from blah import *\n" - ) + assert api.sort_code_string(test_input) == ("from blah import *\nfrom blah import _potato\n") + assert api.sort_code_string(test_input, combine_star=True) == ("from blah import *\n") def test_include_trailing_comma() -> None: @@ -1475,7 +1440,7 @@ def test_include_trailing_comma() -> None: multi_line_output=WrapModes.GRID, line_length=40, include_trailing_comma=True, - ).output + ) assert test_output_grid == ( "from third_party import (lib1, lib2,\n" " lib3, lib4,)\n" ) @@ -1485,7 +1450,7 @@ def test_include_trailing_comma() -> None: multi_line_output=WrapModes.VERTICAL, line_length=40, include_trailing_comma=True, - ).output + ) assert test_output_vertical == ( "from third_party import (lib1,\n" " lib2,\n" @@ -1498,7 +1463,7 @@ def test_include_trailing_comma() -> None: multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, line_length=40, include_trailing_comma=True, - ).output + ) assert test_output_vertical_indent == ( "from third_party import (\n" " lib1,\n" " lib2,\n" " lib3,\n" " lib4,\n" ")\n" ) @@ -1508,7 +1473,7 @@ def test_include_trailing_comma() -> None: multi_line_output=WrapModes.VERTICAL_GRID, line_length=40, include_trailing_comma=True, - ).output + ) assert test_output_vertical_grid == ( "from third_party import (\n lib1, lib2, lib3, lib4,)\n" ) @@ -1518,7 +1483,7 @@ def test_include_trailing_comma() -> None: multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, line_length=40, include_trailing_comma=True, - ).output + ) assert test_output_vertical_grid_grouped == ( "from third_party import (\n lib1, lib2, lib3, lib4,\n)\n" ) @@ -1528,7 +1493,7 @@ def test_include_trailing_comma() -> None: line_length=25, include_trailing_comma=True, use_parentheses=True, - ).output + ) assert test_output_wrap_single_import_with_use_parentheses == ( "from third_party import (\n lib1,)\n" ) @@ -1539,7 +1504,7 @@ def test_include_trailing_comma() -> None: multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, include_trailing_comma=True, use_parentheses=True, - ).output + ) assert test_output_wrap_single_import_vertical_indent == ( "from third_party import (\n lib1,\n)\n" ) @@ -1559,7 +1524,7 @@ def test_include_trailing_comma() -> None: multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, include_trailing_comma=True, use_parentheses=True, - ).output + ) assert trailing_comma_with_comment == expected_trailing_comma_with_comment # The next time around, it should be equal trailing_comma_with_comment = SortImports( @@ -1568,7 +1533,7 @@ def test_include_trailing_comma() -> None: multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, include_trailing_comma=True, use_parentheses=True, - ).output + ) assert trailing_comma_with_comment == expected_trailing_comma_with_comment @@ -1577,19 +1542,16 @@ def test_similar_to_std_library() -> None: don't end up clobbered """ test_input = "import datetime\n\nimport requests\nimport times\n" - assert ( - SortImports(file_contents=test_input, known_third_party=["requests", "times"]).output - == test_input - ) + assert api.sort_code_string(test_input, known_third_party=["requests", "times"]) == test_input def test_correctly_placed_imports() -> None: """Test to ensure comments stay on correct placement after being sorted""" test_input = "from a import b # comment for b\nfrom a import c # comment for c\n" - assert SortImports(file_contents=test_input, force_single_line=True).output == ( + assert api.sort_code_string(test_input, force_single_line=True) == ( "from a import b # comment for b\nfrom a import c # comment for c\n" ) - assert SortImports(file_contents=test_input).output == ( + assert api.sort_code_string(test_input) == ( "from a import b # comment for b\nfrom a import c # comment for c\n" ) @@ -1666,7 +1628,7 @@ def test_correctly_placed_imports() -> None: force_single_line=True, line_length=140, known_third_party=["django", "model_mommy"], - ).output + ) == test_input ) @@ -1678,13 +1640,10 @@ def test_auto_detection() -> None: # Issue 157 test_input = "import binascii\nimport os\n\nimport cv2\nimport requests\n" - assert ( - SortImports(file_contents=test_input, known_third_party=["cv2", "requests"]).output - == test_input - ) + assert api.sort_code_string(test_input, known_third_party=["cv2", "requests"]) == test_input # alternative solution - assert SortImports(file_contents=test_input, default_section="THIRDPARTY").output == test_input + assert api.sort_code_string(test_input, default_section="THIRDPARTY") == test_input def test_same_line_statements() -> None: @@ -1692,10 +1651,10 @@ def test_same_line_statements() -> None: contains multiple statements including an import """ test_input = "import pdb; import nose\n" - assert SortImports(file_contents=test_input).output == ("import pdb\n\nimport nose\n") + assert api.sort_code_string(test_input) == ("import pdb\n\nimport nose\n") test_input = "import pdb; pdb.set_trace()\nimport nose; nose.run()\n" - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input def test_long_line_comments() -> None: @@ -1708,9 +1667,7 @@ def test_long_line_comments() -> None: "sync_stage_envdir, " "update_stage_app, update_stage_cron # noqa\n" ) - assert SortImports( - file_contents=test_input, line_length=100, balanced_wrapping=True - ).output == ( + assert SortImports(file_contents=test_input, line_length=100, balanced_wrapping=True) == ( "from foo.utils.fabric_stuff.live import (check_clean_live, deploy_live, # noqa\n" " sync_live_envdir, update_live_app, " "update_live_cron)\n" @@ -1725,7 +1682,7 @@ def test_tab_character_in_import() -> None: test_input = ( "from __future__ import print_function\n" "from __future__ import\tprint_function\n" ) - assert SortImports(file_contents=test_input).output == "from __future__ import print_function\n" + assert api.sort_code_string(test_input) == "from __future__ import print_function\n" def test_split_position() -> None: @@ -1734,7 +1691,7 @@ def test_split_position() -> None: "from p24.shared.exceptions.master.host_state_flag_unchanged " "import HostStateUnchangedException\n" ) - assert SortImports(file_contents=test_input, line_length=80).output == ( + assert api.sort_code_string(test_input, line_length=80) == ( "from p24.shared.exceptions.master.host_state_flag_unchanged import \\\n" " HostStateUnchangedException\n" ) @@ -1764,9 +1721,9 @@ def test_place_comments() -> None: "import os\n" "import sys\n" ) - test_output = SortImports(file_contents=test_input, known_third_party=["django"]).output + test_output = api.sort_code_string(test_input, known_third_party=["django"]) assert test_output == expected_output - test_output = SortImports(file_contents=test_output, known_third_party=["django"]).output + test_output = api.sort_code_string(test_output, known_third_party=["django"]) assert test_output == expected_output @@ -1787,7 +1744,7 @@ def test_placement_control() -> None: known_standard_library=["p24.imports", "os", "sys"], known_third_party=["bottle"], default_section="THIRDPARTY", - ).output + ) assert test_output == ( "import os\n" @@ -1839,7 +1796,7 @@ def test_custom_sections() -> None: "FIRSTPARTY", "LOCALFOLDER", ], - ).output + ) assert test_output == ( "# Standard Library\n" "import os\n" @@ -1893,7 +1850,7 @@ def test_glob_known() -> None: "FIRSTPARTY", "LOCALFOLDER", ], - ).output + ) assert test_output == ( "# Standard Library\n" "import os\n" @@ -1918,7 +1875,7 @@ def test_sticky_comments() -> None: "# Used for type-hinting (ref: https://github.com/davidhalter/jedi/issues/414).\n" "from selenium.webdriver.remote.webdriver import WebDriver # noqa\n" ) - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input test_input = ( "from django import forms\n" @@ -1927,26 +1884,26 @@ def test_sticky_comments() -> None: "from django.contrib.gis.geos import GEOSException, GEOSGeometry\n" "from django.utils.translation import ugettext_lazy as _\n" ) - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input def test_zipimport() -> None: """Imports ending in "import" shouldn't be clobbered""" test_input = "from zipimport import zipimport\n" - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input def test_from_ending() -> None: """Imports ending in "from" shouldn't be clobbered.""" test_input = "from foo import get_foo_from, get_foo\n" expected_output = "from foo import get_foo, get_foo_from\n" - assert SortImports(file_contents=test_input).output == expected_output + assert api.sort_code_string(test_input) == expected_output def test_from_first() -> None: """Tests the setting from_first works correctly""" test_input = "from os import path\nimport os\n" - assert SortImports(file_contents=test_input, from_first=True).output == test_input + assert api.sort_code_string(test_input, from_first=True) == test_input def test_top_comments() -> None: @@ -1957,26 +1914,26 @@ def test_top_comments() -> None: "#\n" "from __future__ import unicode_literals\n" ) - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input test_input = ( "# -*- coding: utf-8 -*-\n" "from django.db import models\n" "from django.utils.encoding import python_2_unicode_compatible\n" ) - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input test_input = "# Comment\nimport sys\n" - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input test_input = "# -*- coding\nimport sys\n" - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input def test_consistency() -> None: """Ensures consistency of handling even when dealing with non ordered-by-type imports""" test_input = "from sqlalchemy.dialects.postgresql import ARRAY, array\n" - assert SortImports(file_contents=test_input, order_by_type=True).output == test_input + assert api.sort_code_string(test_input, order_by_type=True) == test_input def test_force_grid_wrap() -> None: @@ -1986,7 +1943,7 @@ def test_force_grid_wrap() -> None: file_contents=test_input, force_grid_wrap=2, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, - ).output + ) assert ( test_output == """from bar import lib2 @@ -2000,7 +1957,7 @@ def test_force_grid_wrap() -> None: file_contents=test_input, force_grid_wrap=3, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, - ).output + ) assert test_output == test_input @@ -2016,7 +1973,7 @@ def test_force_grid_wrap_long() -> None: force_grid_wrap=2, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, line_length=9999, - ).output + ) assert ( test_output == """from babar import something_that_is_kind_of_long @@ -2036,7 +1993,7 @@ def test_uses_jinja_variables() -> None: ) test_output = SortImports( file_contents=test_input, known_third_party=["django"], known_first_party=["myproject"] - ).output + ) assert test_output == ( "import os\n" "import sys\n" @@ -2047,13 +2004,13 @@ def test_uses_jinja_variables() -> None: ) test_input = "import {{ cookiecutter.repo_name }}\n" "from foo import {{ cookiecutter.bar }}\n" - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input def test_fcntl() -> None: """Test to ensure fcntl gets correctly recognized as stdlib import""" test_input = "import fcntl\nimport os\nimport sys\n" - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input def test_import_split_is_word_boundary_aware() -> None: @@ -2066,7 +2023,7 @@ def test_import_split_is_word_boundary_aware() -> None: 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" @@ -2080,9 +2037,7 @@ def test_other_file_encodings(tmpdir) -> None: tmp_fname = tmpdir.join(f"test_{encoding}.py") file_contents = f"# coding: {encoding}\n\ns = u'ã'\n" tmp_fname.write_binary(file_contents.encode(encoding)) - assert ( - SortImports(filename=str(tmp_fname), settings_path=os.getcwd()).output == file_contents - ) + assert SortImports(filename=str(tmp_fname), settings_path=os.getcwd()) == file_contents def test_encoding_not_in_comment(tmpdir) -> None: @@ -2090,7 +2045,7 @@ def test_encoding_not_in_comment(tmpdir) -> None: tmp_fname = tmpdir.join("test_encoding.py") file_contents = "class Foo\n coding: latin1\n\ns = u'ã'\n" tmp_fname.write_binary(file_contents.encode("utf8")) - assert SortImports(filename=str(tmp_fname), settings_path=os.getcwd()).output == file_contents + assert SortImports(filename=str(tmp_fname), settings_path=os.getcwd()) == file_contents def test_encoding_not_in_first_two_lines(tmpdir) -> None: @@ -2098,7 +2053,7 @@ def test_encoding_not_in_first_two_lines(tmpdir) -> None: tmp_fname = tmpdir.join("test_encoding.py") file_contents = "\n\n# -*- coding: latin1\n\ns = u'ã'\n" tmp_fname.write_binary(file_contents.encode("utf8")) - assert SortImports(filename=str(tmp_fname), settings_path=os.getcwd()).output == file_contents + assert SortImports(filename=str(tmp_fname), settings_path=os.getcwd()) == file_contents def test_comment_at_top_of_file() -> None: @@ -2109,10 +2064,10 @@ def test_comment_at_top_of_file() -> None: "# Comment two\n" "from django.contrib.gis.geos import GEOSException\n" ) - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input test_input = "# -*- coding: utf-8 -*-\nfrom django.db import models\n" - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input def test_alphabetic_sorting() -> None: @@ -2133,11 +2088,11 @@ def test_alphabetic_sorting() -> None: "force_alphabetical_sort_within_sections": True, } # type: Dict[str, Any] - output = SortImports(file_contents=test_input, known_first_party=["django"], **options).output + output = api.sort_code_string(test_input, known_first_party=["django"], **options) assert output == test_input test_input = "# -*- coding: utf-8 -*-\nfrom django.db import models\n" - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input def test_alphabetic_sorting_multi_line() -> None: @@ -2147,7 +2102,7 @@ def test_alphabetic_sorting_multi_line() -> None: " CONSTANT_F, CONSTANT_G, CONSTANT_H, CONSTANT_I, CONSTANT_J)\n" ) options = {"force_alphabetical_sort_within_sections": True} # type: Dict[str, Any] - assert SortImports(file_contents=test_input, **options).output == test_input + assert api.sort_code_string(test_input, **options) == test_input def test_comments_not_duplicated() -> None: @@ -2158,7 +2113,7 @@ def test_comments_not_duplicated() -> None: "from service import demo # inline comment\n" "from service import settings\n" ) - output = SortImports(file_contents=test_input).output + output = api.sort_code_string(test_input) assert output.count("# Whole line comment\n") == 1 assert output.count("# inline comment\n") == 1 @@ -2175,7 +2130,7 @@ def test_top_of_line_comments() -> None: "\n" "import logging\n" ) - output = SortImports(file_contents=test_input).output + output = api.sort_code_string(test_input) print(output) assert output.startswith("# -*- coding: utf-8 -*-\n") @@ -2183,7 +2138,7 @@ def test_top_of_line_comments() -> None: def test_basic_comment() -> None: """Test to ensure a basic comment wont crash isort""" test_input = "import logging\n# Foo\nimport os\n" - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input def test_shouldnt_add_lines() -> None: @@ -2191,7 +2146,7 @@ def test_shouldnt_add_lines() -> None: See: issue #316 """ test_input = '"""Text"""\n' "# This is a comment\nimport pkg_resources\n" - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input def test_sections_parsed_correct(tmpdir) -> None: @@ -2216,7 +2171,7 @@ def test_sections_parsed_correct(tmpdir) -> None: "from nose import *\n" ) tmpdir.join(".isort.cfg").write(conf_file_data) - assert SortImports(file_contents=test_input, settings_path=str(tmpdir)).output == correct_output + assert api.sort_code_string(test_input, settings_path=str(tmpdir)) == correct_output @pytest.mark.skipif(toml is None, reason="Requires toml package to be installed.") @@ -2255,7 +2210,7 @@ def test_pyproject_conf_file(tmpdir) -> None: "from nose import *\n" ) tmpdir.join("pyproject.toml").write(conf_file_data) - assert SortImports(file_contents=test_input, settings_path=str(tmpdir)).output == correct_output + assert api.sort_code_string(test_input, settings_path=str(tmpdir)) == correct_output def test_alphabetic_sorting_no_newlines() -> None: @@ -2265,7 +2220,7 @@ def test_alphabetic_sorting_no_newlines() -> None: test_input = "import os\n" test_output = SortImports( file_contents=test_input, force_alphabetical_sort_within_sections=True - ).output + ) assert test_input == test_output test_input = "import os\n" "import unittest\n" "\n" "from a import b\n" "\n" "\n" "print(1)\n" @@ -2273,7 +2228,7 @@ def test_alphabetic_sorting_no_newlines() -> None: file_contents=test_input, force_alphabetical_sort_within_sections=True, lines_after_imports=2, - ).output + ) assert test_input == test_output @@ -2285,7 +2240,7 @@ def test_sort_within_section() -> None: "from foo import bar\n" "from foo.bar import Quux, baz\n" ) - test_output = SortImports(file_contents=test_input, force_sort_within_sections=True).output + test_output = api.sort_code_string(test_input, force_sort_within_sections=True) assert test_output == test_input test_input = ( @@ -2300,14 +2255,14 @@ def test_sort_within_section() -> None: force_sort_within_sections=True, order_by_type=False, force_single_line=True, - ).output + ) assert test_output == test_input def test_sorting_with_two_top_comments() -> None: """Test to ensure isort will sort files that contain 2 top comments""" test_input = "#! comment1\n''' comment2\n'''\nimport b\nimport a\n" - assert SortImports(file_contents=test_input).output == ( + assert api.sort_code_string(test_input) == ( "#! comment1\n''' comment2\n'''\nimport a\nimport b\n" ) @@ -2315,10 +2270,10 @@ def test_sorting_with_two_top_comments() -> None: def test_lines_between_sections() -> None: """Test to ensure lines_between_sections works""" test_input = "from bar import baz\nimport os\n" - assert SortImports(file_contents=test_input, lines_between_sections=0).output == ( + assert api.sort_code_string(test_input, lines_between_sections=0) == ( "import os\nfrom bar import baz\n" ) - assert SortImports(file_contents=test_input, lines_between_sections=2).output == ( + assert api.sort_code_string(test_input, lines_between_sections=2) == ( "import os\n\n\nfrom bar import baz\n" ) @@ -2338,7 +2293,7 @@ def test_forced_sepatate_globs() -> None: ) test_output = SortImports( file_contents=test_input, forced_separate=["*.models"], line_length=120 - ).output + ) assert test_output == ( "import os\n" @@ -2379,14 +2334,14 @@ def test_no_additional_lines_issue_358() -> None: file_contents=test_input, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, line_length=20, - ).output + ) assert test_output == expected_output test_output = SortImports( file_contents=test_output, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, line_length=20, - ).output + ) assert test_output == expected_output for _attempt in range(5): @@ -2394,7 +2349,7 @@ def test_no_additional_lines_issue_358() -> None: file_contents=test_output, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, line_length=20, - ).output + ) assert test_output == expected_output test_input = ( @@ -2423,14 +2378,14 @@ def test_no_additional_lines_issue_358() -> None: file_contents=test_input, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, line_length=20, - ).output + ) assert test_output == expected_output test_output = SortImports( file_contents=test_output, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, line_length=20, - ).output + ) assert test_output == expected_output for _attempt in range(5): @@ -2438,7 +2393,7 @@ def test_no_additional_lines_issue_358() -> None: file_contents=test_output, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, line_length=20, - ).output + ) assert test_output == expected_output @@ -2447,7 +2402,7 @@ def test_import_by_paren_issue_375() -> None: paren is directly by the import body """ test_input = "from .models import(\n Foo,\n Bar,\n)\n" - assert SortImports(file_contents=test_input).output == "from .models import Bar, Foo\n" + assert api.sort_code_string(test_input) == "from .models import Bar, Foo\n" def test_import_by_paren_issue_460() -> None: @@ -2459,7 +2414,7 @@ def test_import_by_paren_issue_460() -> None: import io import os """ - assert SortImports(file_contents=(test_input)).output == test_input + assert api.sort_code_string((test_input)) == test_input def test_function_with_docstring() -> None: @@ -2476,7 +2431,7 @@ def test_function_with_docstring() -> None: ' """ Single line triple quoted doctring """\n' " pass\n" ) - assert SortImports(file_contents=test_input, add_imports=add_imports).output == expected_output + assert api.sort_code_string(test_input, add_imports=add_imports) == expected_output def test_plone_style() -> None: @@ -2493,7 +2448,7 @@ def test_plone_style() -> None: "import Zope\n" ) options = {"force_single_line": True, "force_alphabetical_sort": True} # type: Dict[str, Any] - assert SortImports(file_contents=test_input, **options).output == test_input + assert api.sort_code_string(test_input, **options) == test_input def test_third_party_case_sensitive() -> None: @@ -2501,7 +2456,7 @@ def test_third_party_case_sensitive() -> None: test_input = "import thirdparty\nimport os\nimport ABC\n" expected_output = "import os\n\nimport ABC\nimport thirdparty\n" - assert SortImports(file_contents=test_input).output == expected_output + assert api.sort_code_string(test_input) == expected_output def test_exists_case_sensitive_file(tmpdir) -> None: @@ -2524,7 +2479,7 @@ def test_sys_path_mutation(tmpdir) -> None: test_input = "from myproject import test" options = {"virtual_env": str(tmpdir)} # type: Dict[str, Any] expected_length = len(sys.path) - SortImports(file_contents=test_input, **options).output + api.sort_code_string(test_input, **options) assert len(sys.path) == expected_length @@ -2535,7 +2490,7 @@ def test_long_single_line() -> None: " _a," "_xxxxxx_xxxxxxx_xxxxxxxx_xxx_xxxxxxx as xxxxxx_xxxxxxx_xxxxxxxx_xxx_xxxxxxx)", line_length=79, - ).output + ) for line in output.split("\n"): assert len(line) <= 79 @@ -2545,7 +2500,7 @@ def test_long_single_line() -> None: "_xxxxxx_xxxxxxx_xxxxxxxx_xxx_xxxxxxx as xxxxxx_xxxxxxx_xxxxxxxx_xxx_xxxxxxx)", line_length=76, combine_as_imports=True, - ).output + ) for line in output.split("\n"): assert len(line) <= 79 @@ -2564,34 +2519,32 @@ def test_import_inside_class_issue_432() -> None: " def bar(self):\n" " pass\n" ) - assert ( - SortImports(file_contents=test_input, add_imports=["import baz"]).output == expected_output - ) + assert api.sort_code_string(test_input, add_imports=["import baz"]) == expected_output def test_wildcard_import_without_space_issue_496() -> None: """Test to ensure issue #496: wildcard without space, is resolved""" test_input = "from findorserver.coupon.models import*" expected_output = "from findorserver.coupon.models import *\n" - assert SortImports(file_contents=test_input).output == expected_output + assert api.sort_code_string(test_input) == expected_output def test_import_line_mangles_issues_491() -> None: """Test to ensure comment on import with parens doesn't cause issues""" test_input = "import os # ([\n\n" 'print("hi")\n' - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input def test_import_line_mangles_issues_505() -> None: """Test to ensure comment on import with parens doesn't cause issues""" test_input = "from sys import * # (\n\n\ndef test():\n" ' print("Test print")\n' - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input def test_import_line_mangles_issues_439() -> None: """Test to ensure comment on import with parens doesn't cause issues""" test_input = "import a # () import\nfrom b import b\n" - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input def test_alias_using_paren_issue_466() -> None: @@ -2603,10 +2556,7 @@ def test_alias_using_paren_issue_466() -> None: "from django.db.backends.mysql.base import (\n" " DatabaseWrapper as MySQLDatabaseWrapper)\n" ) - assert ( - SortImports(file_contents=test_input, line_length=50, use_parentheses=True).output - == expected_output - ) + assert api.sort_code_string(test_input, line_length=50, use_parentheses=True) == expected_output test_input = ( "from django.db.backends.mysql.base import DatabaseWrapper as MySQLDatabaseWrapper\n" @@ -2622,7 +2572,7 @@ def test_alias_using_paren_issue_466() -> None: line_length=50, multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, use_parentheses=True, - ).output + ) == expected_output ) @@ -2642,7 +2592,7 @@ def test_long_alias_using_paren_issue_957() -> None: line_length=50, use_parentheses=True, multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, - ).output + ) assert out == expected_output test_input = ( @@ -2659,7 +2609,7 @@ def test_long_alias_using_paren_issue_957() -> None: line_length=50, use_parentheses=True, multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, - ).output + ) assert out == expected_output test_input = ( @@ -2678,27 +2628,27 @@ def test_long_alias_using_paren_issue_957() -> None: line_length=50, use_parentheses=True, multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, - ).output + ) assert out == expected_output def test_strict_whitespace_by_default(capsys) -> None: test_input = "import os\nfrom django.conf import settings\n" - SortImports(file_contents=test_input, check=True) + api.sort_code_string(test_input, check=True) out, err = capsys.readouterr() assert out == "ERROR: Imports are incorrectly sorted and/or formatted.\n" def test_strict_whitespace_no_closing_newline_issue_676(capsys) -> None: test_input = "import os\n\nfrom django.conf import settings\n\nprint(1)" - SortImports(file_contents=test_input, check=True) + api.sort_code_string(test_input, check=True) out, err = capsys.readouterr() assert out == "" def test_ignore_whitespace(capsys) -> None: test_input = "import os\nfrom django.conf import settings\n" - SortImports(file_contents=test_input, check=True, ignore_whitespace=True) + api.sort_code_string(test_input, check=True, ignore_whitespace=True) out, err = capsys.readouterr() assert out == "" @@ -2716,7 +2666,7 @@ def test_import_wraps_with_comment_issue_471() -> None: assert ( SortImports( file_contents=test_input, line_length=50, multi_line_output=1, use_parentheses=True - ).output + ) == expected_output ) @@ -2729,13 +2679,13 @@ def test_import_case_produces_inconsistent_results_issue_472() -> None: "from sqlalchemy.dialects.postgresql import ARRAY\n" "from sqlalchemy.dialects.postgresql import array\n" ) - assert SortImports(file_contents=test_input, force_single_line=True).output == test_input + assert api.sort_code_string(test_input, force_single_line=True) == test_input test_input = ( "from scrapy.core.downloader.handlers.http import " "HttpDownloadHandler, HTTPDownloadHandler\n" ) - assert SortImports(file_contents=test_input, line_length=100).output == test_input + assert api.sort_code_string(test_input, line_length=100) == test_input def test_inconsistent_behavior_in_python_2_and_3_issue_479() -> None: @@ -2744,7 +2694,7 @@ def test_inconsistent_behavior_in_python_2_and_3_issue_479() -> None: "from future.standard_library import hooks\n" "from workalendar.europe import UnitedKingdom\n" ) - assert SortImports(file_contents=test_input, known_first_party=["future"]).output == test_input + assert api.sort_code_string(test_input, known_first_party=["future"]) == test_input def test_sort_within_section_comments_issue_436() -> None: @@ -2758,18 +2708,14 @@ def test_sort_within_section_comments_issue_436() -> None: "# it must not be ... comment line 3\n" "import report\n" ) - assert ( - SortImports(file_contents=test_input, force_sort_within_sections=True).output == test_input - ) + assert api.sort_code_string(test_input, force_sort_within_sections=True) == test_input def test_sort_within_sections_with_force_to_top_issue_473() -> None: """Test to ensure it's possible to sort within sections with items forced to top""" test_input = "import z\nimport foo\nfrom foo import bar\n" assert ( - SortImports( - file_contents=test_input, force_sort_within_sections=True, force_to_top=["z"] - ).output + SortImports(file_contents=test_input, force_sort_within_sections=True, force_to_top=["z"]) == test_input ) @@ -2779,7 +2725,7 @@ def test_correct_number_of_new_lines_with_comment_issue_435() -> None: doesn't mess up the new line spacing """ test_input = "import foo\n\n# comment\n\n\ndef baz():\n pass\n" - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input def test_future_below_encoding_issue_545() -> None: @@ -2799,7 +2745,7 @@ def test_future_below_encoding_issue_545() -> None: "\n" 'print("hello")\n' ) - assert SortImports(file_contents=test_input).output == expected_output + assert api.sort_code_string(test_input) == expected_output def test_no_extra_lines_issue_557() -> None: @@ -2821,7 +2767,7 @@ def test_no_extra_lines_issue_557() -> None: force_alphabetical_sort=True, force_sort_within_sections=True, line_length=100, - ).output + ) == expected_output ) @@ -2835,7 +2781,7 @@ def test_long_import_wrap_support_with_mode_2() -> None: assert ( SortImports( file_contents=test_input, multi_line_output=WrapModes.HANGING_INDENT, line_length=80 - ).output + ) == test_input ) @@ -2850,7 +2796,7 @@ def test_pylint_comments_incorrectly_wrapped_issue_571() -> None: "from PyQt5.QtCore import \\\n" " QRegExp # @UnresolvedImport pylint: disable=import-error,useless-suppression\n" ) - assert SortImports(file_contents=test_input, line_length=60).output == expected_output + assert api.sort_code_string(test_input, line_length=60) == expected_output def test_ensure_async_methods_work_issue_537() -> None: @@ -2862,31 +2808,29 @@ def test_ensure_async_methods_work_issue_537() -> None: "async def test_myfunction(test_client, app):\n" " a = await myfunction(test_client, app)\n" ) - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input def test_ensure_as_imports_sort_correctly_within_from_imports_issue_590() -> None: """Test to ensure combination from and as import statements are sorted correct""" test_input = "from os import defpath\nfrom os import pathsep as separator\n" - assert ( - SortImports(file_contents=test_input, force_sort_within_sections=True).output == test_input - ) + assert api.sort_code_string(test_input, force_sort_within_sections=True) == test_input test_input = "from os import defpath\nfrom os import pathsep as separator\n" - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input test_input = "from os import defpath\nfrom os import pathsep as separator\n" - assert SortImports(file_contents=test_input, force_single_line=True).output == test_input + assert api.sort_code_string(test_input, force_single_line=True) == test_input def test_ensure_line_endings_are_preserved_issue_493() -> None: """Test to ensure line endings are not converted""" test_input = "from os import defpath\r\nfrom os import pathsep as separator\r\n" - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input test_input = "from os import defpath\rfrom os import pathsep as separator\r" - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input test_input = "from os import defpath\nfrom os import pathsep as separator\n" - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input def test_not_splitted_sections() -> None: @@ -2905,26 +2849,22 @@ def test_not_splitted_sections() -> None: + statement ) - assert SortImports(file_contents=test_input).output == test_input - assert SortImports(file_contents=test_input, no_lines_before=["LOCALFOLDER"]).output == ( + assert api.sort_code_string(test_input) == test_input + assert api.sort_code_string(test_input, no_lines_before=["LOCALFOLDER"]) == ( stdlib_section + whiteline + firstparty_section + local_section + whiteline + statement ) # by default STDLIB and FIRSTPARTY sections are split by THIRDPARTY section, # so don't merge them if THIRDPARTY imports aren't exist - assert ( - SortImports(file_contents=test_input, no_lines_before=["FIRSTPARTY"]).output == test_input - ) + assert api.sort_code_string(test_input, no_lines_before=["FIRSTPARTY"]) == test_input # in case when THIRDPARTY section is excluded from sections list, # it's ok to merge STDLIB and FIRSTPARTY assert SortImports( file_contents=test_input, sections=["STDLIB", "FIRSTPARTY", "LOCALFOLDER"], no_lines_before=["FIRSTPARTY"], - ).output == ( - stdlib_section + firstparty_section + whiteline + local_section + whiteline + statement - ) + ) == (stdlib_section + firstparty_section + whiteline + local_section + whiteline + statement) # it doesn't change output, because stdlib packages don't have any whitelines before them - assert SortImports(file_contents=test_input, no_lines_before=["STDLIB"]).output == test_input + assert api.sort_code_string(test_input, no_lines_before=["STDLIB"]) == test_input def test_no_lines_before_empty_section() -> None: @@ -2936,7 +2876,7 @@ def test_no_lines_before_empty_section() -> None: known_custom=["custom"], sections=["THIRDPARTY", "LOCALFOLDER", "CUSTOM"], no_lines_before=["THIRDPARTY", "LOCALFOLDER", "CUSTOM"], - ).output + ) == test_input ) @@ -2948,22 +2888,17 @@ def test_no_inline_sort() -> None: """ test_input = "from foo import a, c, b\n" assert ( - SortImports(file_contents=test_input, no_inline_sort=True, force_single_line=False).output - == test_input + api.sort_code_string(test_input, no_inline_sort=True, force_single_line=False) == test_input ) assert ( - SortImports(file_contents=test_input, no_inline_sort=False, force_single_line=False).output + api.sort_code_string(test_input, no_inline_sort=False, force_single_line=False) == "from foo import a, b, c\n" ) expected = "from foo import a\nfrom foo import b\nfrom foo import c\n" assert ( - SortImports(file_contents=test_input, no_inline_sort=False, force_single_line=True).output - == expected - ) - assert ( - SortImports(file_contents=test_input, no_inline_sort=True, force_single_line=True).output - == expected + api.sort_code_string(test_input, no_inline_sort=False, force_single_line=True) == expected ) + assert api.sort_code_string(test_input, no_inline_sort=True, force_single_line=True) == expected def test_relative_import_of_a_module() -> None: @@ -2990,14 +2925,14 @@ def test_relative_import_of_a_module() -> None: "from six.moves import asd\n" ) - sorted_result = SortImports(file_contents=test_input, force_single_line=True).output + sorted_result = api.sort_code_string(test_input, force_single_line=True) assert sorted_result == expected_results def test_escaped_parens_sort() -> None: test_input = "from foo import \\ \n(a,\nb,\nc)\n" expected = "from foo import a, b, c\n" - assert SortImports(file_contents=test_input).output == expected + assert api.sort_code_string(test_input) == expected def test_is_python_file_ioerror(tmpdir) -> None: @@ -3041,7 +2976,7 @@ def test_to_ensure_imports_are_brought_to_top_issue_651() -> None: "multiline text\n" '"""\n' ) - assert SortImports(file_contents=test_input).output == expected_output + assert api.sort_code_string(test_input) == expected_output def test_to_ensure_importing_from_imports_module_works_issue_662() -> None: @@ -3053,7 +2988,7 @@ def test_to_ensure_importing_from_imports_module_works_issue_662() -> None: " warn(description=description or qualname(fun), deprecation=deprecation, " "removal=removal)\n" ) - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input def test_to_ensure_no_unexpected_changes_issue_666() -> None: @@ -3072,12 +3007,12 @@ def test_to_ensure_no_unexpected_changes_issue_666() -> None: "from django.utils.translation import ugettext_lazy as _\n" '"""\n' ) - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input def test_to_ensure_tabs_dont_become_space_issue_665() -> None: test_input = "import os\n\n\ndef my_method():\n\tpass\n" - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input def test_new_lines_are_preserved() -> None: @@ -3177,10 +3112,7 @@ def test_forced_separate_is_deterministic_issue_774(tmpdir) -> None: "from separate4 import quux\n" ) - assert ( - SortImports(file_contents=test_input, settings_file=config_file.strpath).output - == test_input - ) + assert api.sort_code_string(test_input, settings_file=config_file.strpath) == test_input PIPFILE = """ @@ -3379,7 +3311,7 @@ def test_comments_not_removed_issue_576() -> None: "# this comment is important and should not be removed\n" "from sys import api_version as api_version\n" ) - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input def test_reverse_relative_imports_issue_417() -> None: @@ -3398,7 +3330,7 @@ def test_reverse_relative_imports_issue_417() -> None: "from ...ex import metus\n" ) assert ( - SortImports(file_contents=test_input, force_single_line=True, reverse_relative=True).output + api.sort_code_string(test_input, force_single_line=True, reverse_relative=True) == test_input ) @@ -3418,28 +3350,28 @@ def test_inconsistent_relative_imports_issue_577() -> None: "from .dolor import consecteur\n" "from .sit import apidiscing\n" ) - assert SortImports(file_contents=test_input, force_single_line=True).output == test_input + assert api.sort_code_string(test_input, force_single_line=True) == test_input def test_unwrap_issue_762() -> None: test_input = "from os.path \\\nimport (join, split)\n" - assert SortImports(file_contents=test_input).output == "from os.path import join, split\n" + assert api.sort_code_string(test_input) == "from os.path import join, split\n" test_input = "from os.\\\n path import (join, split)" - assert SortImports(file_contents=test_input).output == "from os.path import join, split\n" + assert api.sort_code_string(test_input) == "from os.path import join, split\n" def test_multiple_as_imports() -> None: test_input = "from a import b as b\nfrom a import b as bb\nfrom a import b as bb_\n" - test_output = SortImports(file_contents=test_input).output + test_output = api.sort_code_string(test_input) assert test_output == test_input - test_output = SortImports(file_contents=test_input, combine_as_imports=True).output + test_output = api.sort_code_string(test_input, combine_as_imports=True) assert test_output == "from a import b as b, b as bb, b as bb_\n" - test_output = SortImports(file_contents=test_input, keep_direct_and_as_imports=True).output + test_output = api.sort_code_string(test_input, keep_direct_and_as_imports=True) assert test_output == test_input test_output = SortImports( file_contents=test_input, combine_as_imports=True, keep_direct_and_as_imports=True - ).output + ) assert test_output == "from a import b as b, b as bb, b as bb_\n" test_input = ( @@ -3448,17 +3380,17 @@ def test_multiple_as_imports() -> None: "from a import b as bb\n" "from a import b as bb_\n" ) - test_output = SortImports(file_contents=test_input, keep_direct_and_as_imports=False).output + test_output = api.sort_code_string(test_input, keep_direct_and_as_imports=False) assert test_output == "from a import b as b\nfrom a import b as bb\nfrom a import b as bb_\n" test_output = SortImports( file_contents=test_input, combine_as_imports=True, keep_direct_and_as_imports=False - ).output + ) assert test_output == "from a import b as b, b as bb, b as bb_\n" - test_output = SortImports(file_contents=test_input, keep_direct_and_as_imports=True).output + test_output = api.sort_code_string(test_input, keep_direct_and_as_imports=True) assert test_output == test_input test_output = SortImports( file_contents=test_input, combine_as_imports=True, keep_direct_and_as_imports=True - ).output + ) assert test_output == "from a import b, b as b, b as bb, b as bb_\n" test_input = ( @@ -3467,61 +3399,61 @@ def test_multiple_as_imports() -> None: "from a import b\n" "from a import b as f\n" ) - test_output = SortImports(file_contents=test_input, keep_direct_and_as_imports=False).output + test_output = api.sort_code_string(test_input, keep_direct_and_as_imports=False) assert test_output == "from a import b as c\nfrom a import b as e\nfrom a import b as f\n" test_output = SortImports( file_contents=test_input, combine_as_imports=True, keep_direct_and_as_imports=False - ).output + ) assert test_output == "from a import b as c, b as e, b as f\n" - test_output = SortImports(file_contents=test_input, keep_direct_and_as_imports=True).output + test_output = api.sort_code_string(test_input, keep_direct_and_as_imports=True) assert ( test_output == "from a import b\nfrom a import b as c\nfrom a import b as e\nfrom a import b as f\n" ) test_output = SortImports( file_contents=test_input, no_inline_sort=True, keep_direct_and_as_imports=False - ).output + ) assert test_output == "from a import b as c\nfrom a import b as e\nfrom a import b as f\n" test_output = SortImports( file_contents=test_input, keep_direct_and_as_imports=True, no_inline_sort=True - ).output + ) assert ( test_output == "from a import b\nfrom a import b as c\nfrom a import b as e\nfrom a import b as f\n" ) test_output = SortImports( file_contents=test_input, combine_as_imports=True, keep_direct_and_as_imports=True - ).output + ) assert test_output == "from a import b, b as c, b as e, b as f\n" test_output = SortImports( file_contents=test_input, combine_as_imports=True, no_inline_sort=True, keep_direct_and_as_imports=False, - ).output + ) assert test_output == "from a import b as e, b as c, b as f\n" test_output = SortImports( file_contents=test_input, combine_as_imports=True, keep_direct_and_as_imports=True, no_inline_sort=True, - ).output + ) assert test_output == "from a import b, b as e, b as c, b as f\n" test_input = "import a as a\nimport a as aa\nimport a as aa_\n" - test_output = SortImports(file_contents=test_input, keep_direct_and_as_imports=False).output + test_output = api.sort_code_string(test_input, keep_direct_and_as_imports=False) assert test_output == test_input test_output = SortImports( file_contents=test_input, combine_as_imports=True, keep_direct_and_as_imports=True - ).output + ) assert test_output == test_input test_input = "import a\nimport a as a\nimport a as aa\nimport a as aa_\n" - test_output = SortImports(file_contents=test_input, keep_direct_and_as_imports=False).output + test_output = api.sort_code_string(test_input, keep_direct_and_as_imports=False) assert test_output == "import a as a\nimport a as aa\nimport a as aa_\n" test_output = SortImports( file_contents=test_input, combine_as_imports=True, keep_direct_and_as_imports=True - ).output + ) assert test_output == test_input @@ -3543,7 +3475,7 @@ def test_all_imports_from_single_module() -> None: keep_direct_and_as_imports=False, force_single_line=False, no_inline_sort=False, - ).output + ) assert test_output == ( "import a\n" "from a import *\n" @@ -3561,7 +3493,7 @@ def test_all_imports_from_single_module() -> None: keep_direct_and_as_imports=False, force_single_line=False, no_inline_sort=False, - ).output + ) assert test_output == "import a\nfrom a import *\n" test_output = SortImports( file_contents=test_input, @@ -3570,7 +3502,7 @@ def test_all_imports_from_single_module() -> None: keep_direct_and_as_imports=False, force_single_line=False, no_inline_sort=False, - ).output + ) assert test_output == ( "import a\n" "from a import *\n" @@ -3583,7 +3515,7 @@ def test_all_imports_from_single_module() -> None: keep_direct_and_as_imports=True, force_single_line=False, no_inline_sort=False, - ).output + ) assert test_output == ( "import a\n" "from a import *\n" @@ -3602,7 +3534,7 @@ def test_all_imports_from_single_module() -> None: keep_direct_and_as_imports=False, force_single_line=True, no_inline_sort=False, - ).output + ) assert test_output == ( "import a\n" "from a import *\n" @@ -3623,7 +3555,7 @@ def test_all_imports_from_single_module() -> None: keep_direct_and_as_imports=False, force_single_line=False, no_inline_sort=True, - ).output + ) assert test_output == ( "import a\n" "from a import *\n" @@ -3641,7 +3573,7 @@ def test_all_imports_from_single_module() -> None: keep_direct_and_as_imports=False, force_single_line=False, no_inline_sort=False, - ).output + ) assert test_output == "import a\nfrom a import *\n" test_output = SortImports( file_contents=test_input, @@ -3650,7 +3582,7 @@ def test_all_imports_from_single_module() -> None: keep_direct_and_as_imports=True, force_single_line=False, no_inline_sort=False, - ).output + ) assert test_output == "import a\nfrom a import *\n" test_output = SortImports( file_contents=test_input, @@ -3659,7 +3591,7 @@ def test_all_imports_from_single_module() -> None: keep_direct_and_as_imports=False, force_single_line=True, no_inline_sort=False, - ).output + ) assert test_output == "import a\nfrom a import *\n" test_output = SortImports( file_contents=test_input, @@ -3668,7 +3600,7 @@ def test_all_imports_from_single_module() -> None: keep_direct_and_as_imports=False, force_single_line=False, no_inline_sort=True, - ).output + ) assert test_output == "import a\nfrom a import *\n" test_output = SortImports( file_contents=test_input, @@ -3677,7 +3609,7 @@ def test_all_imports_from_single_module() -> None: keep_direct_and_as_imports=True, force_single_line=False, no_inline_sort=False, - ).output + ) assert test_output == ( "import a\n" "from a import *\n" @@ -3690,7 +3622,7 @@ def test_all_imports_from_single_module() -> None: keep_direct_and_as_imports=False, force_single_line=True, no_inline_sort=False, - ).output + ) assert test_output == ( "import a\n" "from a import *\n" @@ -3711,7 +3643,7 @@ def test_all_imports_from_single_module() -> None: keep_direct_and_as_imports=False, force_single_line=False, no_inline_sort=True, - ).output + ) assert test_output == ( "import a\n" "from a import *\n" @@ -3724,7 +3656,7 @@ def test_all_imports_from_single_module() -> None: keep_direct_and_as_imports=True, force_single_line=True, no_inline_sort=False, - ).output + ) assert test_output == ( "import a\n" "from a import *\n" @@ -3746,7 +3678,7 @@ def test_all_imports_from_single_module() -> None: keep_direct_and_as_imports=True, force_single_line=False, no_inline_sort=True, - ).output + ) assert test_output == ( "import a\n" "from a import *\n" @@ -3765,7 +3697,7 @@ def test_all_imports_from_single_module() -> None: keep_direct_and_as_imports=False, force_single_line=True, no_inline_sort=True, - ).output + ) assert test_output == ( "import a\n" "from a import *\n" @@ -3786,7 +3718,7 @@ def test_all_imports_from_single_module() -> None: keep_direct_and_as_imports=True, force_single_line=False, no_inline_sort=False, - ).output + ) assert test_output == "import a\nfrom a import *\n" test_output = SortImports( file_contents=test_input, @@ -3795,7 +3727,7 @@ def test_all_imports_from_single_module() -> None: keep_direct_and_as_imports=False, force_single_line=True, no_inline_sort=False, - ).output + ) assert test_output == "import a\nfrom a import *\n" test_output = SortImports( file_contents=test_input, @@ -3804,7 +3736,7 @@ def test_all_imports_from_single_module() -> None: keep_direct_and_as_imports=False, force_single_line=False, no_inline_sort=True, - ).output + ) assert test_output == "import a\nfrom a import *\n" test_output = SortImports( file_contents=test_input, @@ -3813,7 +3745,7 @@ def test_all_imports_from_single_module() -> None: keep_direct_and_as_imports=True, force_single_line=True, no_inline_sort=False, - ).output + ) assert test_output == "import a\nfrom a import *\n" test_output = SortImports( file_contents=test_input, @@ -3822,7 +3754,7 @@ def test_all_imports_from_single_module() -> None: keep_direct_and_as_imports=True, force_single_line=False, no_inline_sort=True, - ).output + ) assert test_output == "import a\nfrom a import *\n" test_output = SortImports( file_contents=test_input, @@ -3831,7 +3763,7 @@ def test_all_imports_from_single_module() -> None: keep_direct_and_as_imports=False, force_single_line=True, no_inline_sort=True, - ).output + ) assert test_output == "import a\nfrom a import *\n" test_output = SortImports( file_contents=test_input, @@ -3840,7 +3772,7 @@ def test_all_imports_from_single_module() -> None: keep_direct_and_as_imports=True, force_single_line=True, no_inline_sort=False, - ).output + ) assert test_output == ( "import a\n" "from a import *\n" @@ -3862,7 +3794,7 @@ def test_all_imports_from_single_module() -> None: keep_direct_and_as_imports=True, force_single_line=False, no_inline_sort=True, - ).output + ) assert test_output == ( "import a\n" "from a import *\n" @@ -3875,7 +3807,7 @@ def test_all_imports_from_single_module() -> None: keep_direct_and_as_imports=True, force_single_line=True, no_inline_sort=True, - ).output + ) assert test_output == ( "import a\n" "from a import *\n" @@ -3897,7 +3829,7 @@ def test_all_imports_from_single_module() -> None: keep_direct_and_as_imports=True, force_single_line=True, no_inline_sort=False, - ).output + ) assert test_output == "import a\nfrom a import *\n" @@ -3921,7 +3853,7 @@ def test_noqa_issue_679() -> None: "import zed # NOQA\n" "import ujson # NOQA\n" ) - assert SortImports(file_contents=test_input).output == test_output + assert api.sort_code_string(test_input) == test_output def test_extract_multiline_output_wrap_setting_from_a_config_file(tmpdir: py.path.local) -> None: @@ -3960,21 +3892,21 @@ def test_ensure_support_for_non_typed_but_cased_alphabetic_sort_issue_890() -> N 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() -> None: test_input = "# comment\nimport os\n# comment2\nimport sys\n" - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input def test_to_ensure_correctly_handling_of_whitespace_only_issue_811(capsys) -> None: 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) + api.sort_code_string(test_input, ignore_whitespace=True) out, err = capsys.readouterr() assert out == "" assert err == "" @@ -3982,7 +3914,7 @@ def test_to_ensure_correctly_handling_of_whitespace_only_issue_811(capsys) -> No def test_standard_library_deprecates_user_issue_778() -> None: test_input = "import os\n\nimport user\n" - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input def test_settings_path_skip_issue_909(tmpdir) -> None: @@ -4080,9 +4012,9 @@ def test_failing_file_check_916() -> None: "multi_line_output": 3, "lines_after_imports": 2, } # type: Dict[str, Any] - assert SortImports(file_contents=test_input, **settings).output == expected_output - assert SortImports(file_contents=expected_output, **settings).output == expected_output - assert not SortImports(file_contents=expected_output, check=True, **settings).incorrectly_sorted + assert api.sort_code_string(test_input, **settings) == expected_output + assert api.sort_code_string(expected_output, **settings) == expected_output + assert not api.sort_code_string(expected_output, check=True, **settings).incorrectly_sorted def test_import_heading_issue_905() -> None: @@ -4103,7 +4035,7 @@ def test_import_heading_issue_905() -> None: "# Local imports\n" "from oklib.plot_ok import imagesc\n" ) - assert SortImports(file_contents=test_input, **config).output == test_input + assert api.sort_code_string(test_input, **config) == test_input def test_isort_keeps_comments_issue_691() -> None: @@ -4130,7 +4062,7 @@ def test_isort_keeps_comments_issue_691() -> None: "def path(*subdirectories):\n" " return os.path.join(PROJECT_DIR, *subdirectories)\n" ) - assert SortImports(file_contents=test_input).output == expected_output + assert api.sort_code_string(test_input) == expected_output def test_isort_ensures_blank_line_between_import_and_comment() -> None: @@ -4197,7 +4129,7 @@ def test_isort_ensures_blank_line_between_import_and_comment() -> None: "# noinspection PyUnresolvedReferences\n" "from four.b import b as bb\n" ) - assert SortImports(file_contents=test_input, **config).output == expected_output + assert api.sort_code_string(test_input, **config) == expected_output def test_moving_comments_issue_726(): @@ -4207,7 +4139,7 @@ def test_moving_comments_issue_726(): "# comment for PlaidModel\n" "from Plaid.models import PlaidModel\n" ) - assert SortImports(file_contents=test_input, **config).output == test_input + assert api.sort_code_string(test_input, **config) == test_input test_input = ( "# comment for BlueModels\n" @@ -4216,26 +4148,23 @@ def test_moving_comments_issue_726(): "# another comment for PlaidModel\n" "from Plaid.models import PlaidModel\n" ) - assert SortImports(file_contents=test_input, **config).output == test_input + assert api.sort_code_string(test_input, **config) == test_input def test_pyi_formatting_issue_942(tmpdir) -> None: test_input = "import os\n\n\ndef my_method():\n" expected_py_output = test_input.splitlines() expected_pyi_output = "import os\n\ndef my_method():\n".splitlines() - assert SortImports(file_contents=test_input).output.splitlines() == expected_py_output - assert ( - SortImports(file_contents=test_input, extension="pyi").output.splitlines() - == expected_pyi_output - ) + assert api.sort_code_string(test_input).splitlines() == expected_py_output + assert api.sort_code_string(test_input, extension="pyi").splitlines() == expected_pyi_output source_py = tmpdir.join("source.py") source_py.write(test_input) - assert SortImports(filename=str(source_py)).output.splitlines() == expected_py_output + assert SortImports(filename=str(source_py)).splitlines() == expected_py_output source_pyi = tmpdir.join("source.pyi") source_pyi.write(test_input) - assert SortImports(filename=str(source_pyi)).output.splitlines() == expected_pyi_output + assert SortImports(filename=str(source_pyi)).splitlines() == expected_pyi_output def test_move_class_issue_751() -> None: @@ -4260,7 +4189,7 @@ def test_move_class_issue_751() -> None: " return item" "\n" ) - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input def test_python_version() -> None: @@ -4274,15 +4203,15 @@ def test_python_version() -> None: assert args["py_version"] == "3" test_input = "import os\n\nimport user\n" - assert SortImports(file_contents=test_input, py_version="3").output == test_input + assert api.sort_code_string(test_input, py_version="3") == test_input # user is part of the standard library in python 2 output_python_2 = "import os\nimport user\n" - assert SortImports(file_contents=test_input, py_version="27").output == output_python_2 + assert api.sort_code_string(test_input, py_version="27") == output_python_2 test_input = "import os\nimport xml" - print(SortImports(file_contents=test_input, py_version="all").output) + print(api.sort_code_string(test_input, py_version="all")) def test_isort_with_single_character_import() -> None: @@ -4292,7 +4221,7 @@ def test_isort_with_single_character_import() -> None: See Issue #376: https://github.com/timothycrosley/isort/issues/376 """ test_input = "from django.db.models import CASCADE, SET_NULL, Q\n" - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input def test_isort_nested_imports() -> None: @@ -4309,7 +4238,7 @@ def import_test(): return True """ assert ( - SortImports(file_contents=test_input).output + api.sort_code_string(test_input) == """ def import_test(): import os @@ -4334,7 +4263,7 @@ def test_isort_off() -> None: from . import local """ - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input def test_isort_split() -> None: @@ -4347,7 +4276,7 @@ def test_isort_split() -> None: import os import sys """ - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input def test_comment_look_alike(): @@ -4363,7 +4292,7 @@ def test_comment_look_alike(): import os ''' assert ( - SortImports(file_contents=test_input).output + api.sort_code_string(test_input) == ''' """This is a multi-line comment @@ -4731,7 +4660,7 @@ def test_cimport_support(): from string_visitor cimport * from web_request_client_cef3 cimport * """ - SortImports(file_contents=test_input).output == expected_output + api.sort_code_string(test_input) == expected_output def test_cdef_support(): @@ -4743,7 +4672,7 @@ def test_cdef_support(): cdef extern from *: ctypedef CefString ConstCefString "const CefString" """ - ).output + ) == """ from cpython.version cimport PY_MAJOR_VERSION @@ -4761,7 +4690,7 @@ def test_cdef_support(): cpdef extern from *: ctypedef CefString ConstCefString "const CefString" """ - ).output + ) == """ from cpython.version cimport PY_MAJOR_VERSION @@ -4777,9 +4706,7 @@ def test_top_level_import_order() -> None: "from rest_framework import throttling, viewsets\n" "from rest_framework.authentication import TokenAuthentication\n" ) - assert ( - SortImports(file_contents=test_input, force_sort_within_sections=True).output == test_input - ) + assert api.sort_code_string(test_input, force_sort_within_sections=True) == test_input def test_noqa_issue_1065() -> None: @@ -4808,7 +4735,7 @@ def test_noqa_issue_1065() -> None: from flask_security.signals import user_confirmed # noqa from flask_security.signals import user_registered # noqa """ - assert SortImports(file_contents=test_input).output == expected_output + assert api.sort_code_string(test_input) == expected_output def test_single_line_exclusions(): @@ -4826,7 +4753,7 @@ def test_single_line_exclusions(): assert ( SortImports( file_contents=test_input, force_single_line=True, single_line_exclusions=("typing",) - ).output + ) == expected_output ) @@ -4838,7 +4765,7 @@ def test_nested_comment_handling(): # comment for bar """ - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input # If comments appear inside import sections at same indentation they can be re-arranged. test_input = """ @@ -4854,7 +4781,7 @@ def test_nested_comment_handling(): import os import sys """ - assert SortImports(file_contents=test_input).output == expected_output + assert api.sort_code_string(test_input) == expected_output # Comments shouldn't be unexpectedly rearranged. See issue #1090. test_input = """ @@ -4868,7 +4795,7 @@ def f(): from b import b """ - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input # Whitespace shouldn't be adjusted for nested imports. See issue #1090. test_input = """ @@ -4877,7 +4804,7 @@ def f(): except ImportError: import bar """ - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input def test_comments_top_of_file(): @@ -4889,7 +4816,7 @@ def test_comments_top_of_file(): # comment 4 from foo import * """ - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input test_input = """# -*- coding: utf-8 -*- @@ -4912,7 +4839,7 @@ def _process_item(self, item, spider): item['inserted_at'] = datetime.now() return item """ - assert SortImports(file_contents=test_input).output == test_input + assert api.sort_code_string(test_input) == test_input def test_multiple_aliases(): @@ -4922,9 +4849,7 @@ def test_multiple_aliases(): import datetime as dt import datetime as dt2 """ - assert ( - SortImports(keep_direct_and_as_imports=True, file_contents=test_input).output == test_input - ) + assert SortImports(keep_direct_and_as_imports=True, file_contents=test_input) == test_input def test_parens_in_comment(): @@ -4934,7 +4859,7 @@ def test_parens_in_comment(): ) """ expected_output = "from foo import bar # (some text in brackets)\n" - assert SortImports(file_contents=test_input).output == expected_output + assert api.sort_code_string(test_input) == expected_output def test_as_imports_mixed(): @@ -4945,10 +4870,7 @@ def test_as_imports_mixed(): expected_output = """from datetime import datetime from datetime import datetime as dt """ - assert ( - SortImports(file_contents=test_input, keep_direct_and_as_imports=True).output - == expected_output - ) + assert api.sort_code_string(test_input, keep_direct_and_as_imports=True) == expected_output def test_no_sections_with_future(): @@ -4960,7 +4882,7 @@ def test_no_sections_with_future(): import os """ - assert SortImports(file_contents=test_input, no_sections=True).output == expected_output + assert api.sort_code_string(test_input, no_sections=True) == expected_output def test_no_sections_with_as_import(): @@ -4968,7 +4890,7 @@ def test_no_sections_with_as_import(): test_input = """import oumpy as np import sympy """ - assert SortImports(file_contents=test_input, no_sections=True).output == test_input + assert api.sort_code_string(test_input, no_sections=True) == test_input def test_no_lines_too_long(): @@ -4984,10 +4906,7 @@ def test_no_lines_too_long(): from package2 import \\ first_package """ - assert ( - SortImports(file_contents=test_input, line_length=25, multi_line_output=2).output - == expected_output - ) + assert api.sort_code_string(test_input, line_length=25, multi_line_output=2) == expected_output def test_python_future_category(): @@ -5056,6 +4975,6 @@ def test_python_future_category(): import_heading_localfolder="Explicitly Local", known_first_party=["katlogger"], known_future_thirdparty=["future"], - ).output + ) == expected_output ) From adc0df9a108c2cfc6e8c74f4b2530621d3b4983c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 12 Mar 2020 22:17:11 -0700 Subject: [PATCH 0449/1439] Fix test cases to use new API --- tests/test_isort.py | 542 ++++++++++++++++++++++---------------------- 1 file changed, 271 insertions(+), 271 deletions(-) diff --git a/tests/test_isort.py b/tests/test_isort.py index 45e5385ac..0af6728ac 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -153,8 +153,8 @@ def test_line_length() -> None: ")\n" ) # Test case described in issue #654 assert ( - SortImports( - file_contents=test_input, + api.sort_code_string( + code=test_input, include_trailing_comma=True, line_length=79, multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, @@ -163,7 +163,7 @@ def test_line_length() -> None: == test_input ) - test_output = SortImports(file_contents=REALLY_LONG_IMPORT, line_length=42, wrap_length=32) + test_output = api.sort_code_string(code=REALLY_LONG_IMPORT, line_length=42, wrap_length=32) assert test_output == ( "from third_party import (lib1,\n" " lib2,\n" @@ -192,14 +192,14 @@ def test_line_length() -> None: "from .test import a_very_long_function_name_that_exceeds_the_normal_pep8_line_length\n" ) with pytest.raises(ValueError): - test_output = SortImports(file_contents=REALLY_LONG_IMPORT, line_length=80, wrap_length=99) + test_output = api.sort_code_string(code=REALLY_LONG_IMPORT, line_length=80, wrap_length=99) test_output = ( api.sort_code_string(REALLY_LONG_IMPORT, line_length=100, wrap_length=99) == test_input ) # Test Case described in issue #1015 - test_output = SortImports( - file_contents=REALLY_LONG_IMPORT, line_length=25, multi_line_output=WrapModes.HANGING_INDENT + test_output = api.sort_code_string( + REALLY_LONG_IMPORT, line_length=25, multi_line_output=WrapModes.HANGING_INDENT ) assert test_output == ( "from third_party import \\\n" @@ -217,8 +217,8 @@ def test_line_length() -> None: def test_output_modes() -> None: """Test setting isort to use various output modes works as expected""" - test_output_grid = SortImports( - file_contents=REALLY_LONG_IMPORT, multi_line_output=WrapModes.GRID, line_length=40 + test_output_grid = api.sort_code_string( + code=REALLY_LONG_IMPORT, multi_line_output=WrapModes.GRID, line_length=40 ) assert test_output_grid == ( "from third_party import (lib1, lib2,\n" @@ -234,8 +234,8 @@ def test_output_modes() -> None: " lib22)\n" ) - test_output_vertical = SortImports( - file_contents=REALLY_LONG_IMPORT, multi_line_output=WrapModes.VERTICAL, line_length=40 + test_output_vertical = api.sort_code_string( + code=REALLY_LONG_IMPORT, multi_line_output=WrapModes.VERTICAL, line_length=40 ) assert test_output_vertical == ( "from third_party import (lib1,\n" @@ -261,8 +261,8 @@ def test_output_modes() -> None: " lib22)\n" ) - comment_output_vertical = SortImports( - file_contents=REALLY_LONG_IMPORT_WITH_COMMENT, + comment_output_vertical = api.sort_code_string( + code=REALLY_LONG_IMPORT_WITH_COMMENT, multi_line_output=WrapModes.VERTICAL, line_length=40, ) @@ -290,8 +290,8 @@ def test_output_modes() -> None: " lib22)\n" ) - test_output_hanging_indent = SortImports( - file_contents=REALLY_LONG_IMPORT, + test_output_hanging_indent = api.sort_code_string( + code=REALLY_LONG_IMPORT, multi_line_output=WrapModes.HANGING_INDENT, line_length=40, indent=" ", @@ -304,8 +304,8 @@ def test_output_modes() -> None: " lib18, lib20, lib21, lib22\n" ) - comment_output_hanging_indent = SortImports( - file_contents=REALLY_LONG_IMPORT_WITH_COMMENT, + comment_output_hanging_indent = api.sort_code_string( + code=REALLY_LONG_IMPORT_WITH_COMMENT, multi_line_output=WrapModes.HANGING_INDENT, line_length=40, indent=" ", @@ -318,8 +318,8 @@ def test_output_modes() -> None: " lib17, lib18, lib20, lib21, lib22\n" ) - test_output_vertical_indent = SortImports( - file_contents=REALLY_LONG_IMPORT, + test_output_vertical_indent = api.sort_code_string( + code=REALLY_LONG_IMPORT, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, line_length=40, indent=" ", @@ -350,8 +350,8 @@ def test_output_modes() -> None: ")\n" ) - comment_output_vertical_indent = SortImports( - file_contents=REALLY_LONG_IMPORT_WITH_COMMENT, + comment_output_vertical_indent = api.sort_code_string( + code=REALLY_LONG_IMPORT_WITH_COMMENT, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, line_length=40, indent=" ", @@ -382,8 +382,8 @@ def test_output_modes() -> None: ")\n" ) - test_output_vertical_grid = SortImports( - file_contents=REALLY_LONG_IMPORT, + test_output_vertical_grid = api.sort_code_string( + code=REALLY_LONG_IMPORT, multi_line_output=WrapModes.VERTICAL_GRID, line_length=40, indent=" ", @@ -396,8 +396,8 @@ def test_output_modes() -> None: " lib17, lib18, lib20, lib21, lib22)\n" ) - comment_output_vertical_grid = SortImports( - file_contents=REALLY_LONG_IMPORT_WITH_COMMENT, + comment_output_vertical_grid = api.sort_code_string( + code=REALLY_LONG_IMPORT_WITH_COMMENT, multi_line_output=WrapModes.VERTICAL_GRID, line_length=40, indent=" ", @@ -410,8 +410,8 @@ def test_output_modes() -> None: " lib17, lib18, lib20, lib21, lib22)\n" ) - test_output_vertical_grid_grouped = SortImports( - file_contents=REALLY_LONG_IMPORT, + test_output_vertical_grid_grouped = api.sort_code_string( + code=REALLY_LONG_IMPORT, multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, line_length=40, indent=" ", @@ -425,8 +425,8 @@ def test_output_modes() -> None: ")\n" ) - comment_output_vertical_grid_grouped = SortImports( - file_contents=REALLY_LONG_IMPORT_WITH_COMMENT, + comment_output_vertical_grid_grouped = api.sort_code_string( + code=REALLY_LONG_IMPORT_WITH_COMMENT, multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, line_length=40, indent=" ", @@ -440,8 +440,8 @@ def test_output_modes() -> None: ")\n" ) - output_noqa = SortImports( - file_contents=REALLY_LONG_IMPORT_WITH_COMMENT, multi_line_output=WrapModes.NOQA + output_noqa = api.sort_code_string( + code=REALLY_LONG_IMPORT_WITH_COMMENT, multi_line_output=WrapModes.NOQA ) assert output_noqa == ( "from third_party import lib1, lib2, lib3, lib4, lib5, lib6, lib7," @@ -450,8 +450,8 @@ def test_output_modes() -> None: "# NOQA comment\n" ) - test_case = SortImports( - file_contents=SINGLE_LINE_LONG_IMPORT, + test_case = api.sort_code_string( + code=SINGLE_LINE_LONG_IMPORT, multi_line_output=WrapModes.VERTICAL_GRID_GROUPED_NO_COMMA, line_length=40, indent=" ", @@ -461,8 +461,8 @@ def test_output_modes() -> None: "from third_party import (\n lib1, lib2, lib3, lib4, lib5, lib5ab\n)\n" ) - test_output_prefix_from_module = SortImports( - file_contents=REALLY_LONG_IMPORT, + test_output_prefix_from_module = api.sort_code_string( + code=REALLY_LONG_IMPORT, multi_line_output=WrapModes.VERTICAL_PREFIX_FROM_MODULE_IMPORT, line_length=40, ) @@ -480,8 +480,8 @@ def test_output_modes() -> None: "from third_party import lib22\n" ) - test_output_prefix_from_module_with_comment = SortImports( - file_contents=REALLY_LONG_IMPORT_WITH_COMMENT, + test_output_prefix_from_module_with_comment = api.sort_code_string( + code=REALLY_LONG_IMPORT_WITH_COMMENT, multi_line_output=WrapModes.VERTICAL_PREFIX_FROM_MODULE_IMPORT, line_length=40, indent=" ", @@ -529,14 +529,14 @@ def test_output_modes() -> None: def test_qa_comment_case() -> None: test_input = "from veryveryveryveryveryveryveryveryveryveryvery import X # NOQA" - test_output = SortImports( - file_contents=test_input, line_length=40, multi_line_output=WrapModes.NOQA + test_output = api.sort_code_string( + code=test_input, line_length=40, multi_line_output=WrapModes.NOQA ) assert test_output == "from veryveryveryveryveryveryveryveryveryveryvery import X # NOQA\n" test_input = "import veryveryveryveryveryveryveryveryveryveryvery # NOQA" - test_output = SortImports( - file_contents=test_input, line_length=40, multi_line_output=WrapModes.NOQA + test_output = api.sort_code_string( + code=test_input, line_length=40, multi_line_output=WrapModes.NOQA ) assert test_output == "import veryveryveryveryveryveryveryveryveryveryvery # NOQA\n" @@ -590,8 +590,8 @@ def test_convert_hanging() -> None: " lib13, lib14, lib15, lib16, lib17, \\\n" " lib18, lib20, lib21, lib22\n" ) - test_output = SortImports( - file_contents=test_input, multi_line_output=WrapModes.GRID, line_length=40 + test_output = api.sort_code_string( + code=test_input, multi_line_output=WrapModes.GRID, line_length=40 ) assert test_output == ( "from third_party import (lib1, lib2,\n" @@ -610,8 +610,8 @@ def test_convert_hanging() -> None: def test_custom_indent() -> None: """Ensure setting a custom indent will work as expected.""" - test_output = SortImports( - file_contents=REALLY_LONG_IMPORT, + test_output = api.sort_code_string( + code=REALLY_LONG_IMPORT, multi_line_output=WrapModes.HANGING_INDENT, line_length=40, indent=" ", @@ -625,8 +625,8 @@ def test_custom_indent() -> None: " lib20, lib21, lib22\n" ) - test_output = SortImports( - file_contents=REALLY_LONG_IMPORT, + test_output = api.sort_code_string( + code=REALLY_LONG_IMPORT, multi_line_output=WrapModes.HANGING_INDENT, line_length=40, indent="' '", @@ -640,8 +640,8 @@ def test_custom_indent() -> None: " lib20, lib21, lib22\n" ) - test_output = SortImports( - file_contents=REALLY_LONG_IMPORT, + test_output = api.sort_code_string( + code=REALLY_LONG_IMPORT, multi_line_output=WrapModes.HANGING_INDENT, line_length=40, indent="tab", @@ -655,8 +655,8 @@ def test_custom_indent() -> None: "\tlib20, lib21, lib22\n" ) - test_output = SortImports( - file_contents=REALLY_LONG_IMPORT, + test_output = api.sort_code_string( + code=REALLY_LONG_IMPORT, multi_line_output=WrapModes.HANGING_INDENT, line_length=40, indent=2, @@ -683,8 +683,8 @@ def test_use_parentheses() -> None: " 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 + test_output = api.sort_code_string( + code=test_input, line_length=79, use_parentheses=True, include_trailing_comma=True ) assert test_output == ( @@ -692,8 +692,8 @@ def test_use_parentheses() -> None: " my_custom_function as my_special_function,)\n" ) - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, line_length=79, use_parentheses=True, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, @@ -704,8 +704,8 @@ def test_use_parentheses() -> None: " my_custom_function as my_special_function\n)\n" ) - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, line_length=79, use_parentheses=True, multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, @@ -742,8 +742,8 @@ def test_skip_with_file_name() -> None: """Ensure skipping a file works even when file_contents is provided.""" test_input = "import django\nimport myproject\n" - sort_imports = SortImports( - filename="/baz.py", file_contents=test_input, settings_path=os.getcwd(), skip=["baz.py"] + sort_imports = api.sort_code_string( + filename="/baz.py", code=test_input, settings_path=os.getcwd(), skip=["baz.py"] ) assert sort_imports.skipped assert sort_imports.output == "" @@ -767,7 +767,7 @@ def test_force_to_top() -> None: def test_add_imports() -> None: """Ensures adding imports works as expected.""" test_input = "import lib6\nimport lib2\nimport lib5\nimport lib1\n\n" - test_output = SortImports(file_contents=test_input, add_imports=["import lib4", "import lib7"]) + test_output = api.sort_code_string(code=test_input, add_imports=["import lib4", "import lib7"]) assert test_output == ( "import lib1\n" "import lib2\n" @@ -779,7 +779,7 @@ def test_add_imports() -> None: # Using simplified syntax test_input = "import lib6\nimport lib2\nimport lib5\nimport lib1\n\n" - test_output = SortImports(file_contents=test_input, add_imports=["lib4", "lib7", "lib8.a"]) + test_output = api.sort_code_string(code=test_input, add_imports=["lib4", "lib7", "lib8.a"]) assert test_output == ( "import lib1\n" "import lib2\n" @@ -792,8 +792,8 @@ def test_add_imports() -> None: # On a file that has no pre-existing imports test_input = '"""Module docstring"""\n' "\nclass MyClass(object):\n pass\n" - test_output = SortImports( - file_contents=test_input, add_imports=["from __future__ import print_function"] + test_output = api.sort_code_string( + code=test_input, add_imports=["from __future__ import print_function"] ) assert test_output == ( '"""Module docstring"""\n' @@ -806,8 +806,8 @@ def test_add_imports() -> None: # On a file that has no pre-existing imports, and no doc-string test_input = "class MyClass(object):\n pass\n" - test_output = SortImports( - file_contents=test_input, add_imports=["from __future__ import print_function"] + test_output = api.sort_code_string( + code=test_input, add_imports=["from __future__ import print_function"] ) assert test_output == ( "from __future__ import print_function\n" "\n" "\n" "class MyClass(object):\n" " pass\n" @@ -820,7 +820,7 @@ def test_add_imports() -> None: # On a file with no content what so ever, after force_adds is set to True test_input = "" - test_output = SortImports(file_contents=test_input, add_imports=["lib4"], force_adds=True) + test_output = api.sort_code_string(code=test_input, add_imports=["lib4"], force_adds=True) assert test_output == ("import lib4\n") @@ -834,8 +834,8 @@ def test_remove_imports() -> None: test_input = ( "import lib6\n" "import lib2\n" "import lib5\n" "import lib1\n" "from lib8 import a" ) - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, remove_imports=["import lib2", "import lib6", "from lib8 import a"], ) assert test_output == "import lib1\nimport lib5\n" @@ -874,8 +874,8 @@ def test_check_newline_in_imports(capsys) -> None: """Ensure tests works correctly when new lines are in imports.""" test_input = "from lib1 import (\n sub1,\n sub2,\n sub3\n)\n" - SortImports( - file_contents=test_input, + api.sort_code_string( + code=test_input, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, line_length=20, check=True, @@ -909,8 +909,8 @@ def test_forced_separate() -> None: "TO_FIELD_VAR\n" ) assert ( - SortImports( - file_contents=test_input, + api.sort_code_string( + code=test_input, forced_separate=["django.contrib"], known_third_party=["django"], line_length=120, @@ -921,8 +921,8 @@ def test_forced_separate() -> None: test_input = "from .foo import bar\n\nfrom .y import ca\n" assert ( - SortImports( - file_contents=test_input, forced_separate=[".y"], line_length=120, order_by_type=False + api.sort_code_string( + code=test_input, forced_separate=[".y"], line_length=120, order_by_type=False ) == test_input ) @@ -931,15 +931,15 @@ def test_forced_separate() -> None: def test_default_section() -> None: """Test to ensure changing the default section works as expected.""" test_input = "import sys\nimport os\nimport myproject.test\nimport django.settings" - test_output = SortImports( - file_contents=test_input, known_third_party=["django"], default_section="FIRSTPARTY" + test_output = api.sort_code_string( + code=test_input, known_third_party=["django"], default_section="FIRSTPARTY" ) assert test_output == ( "import os\n" "import sys\n" "\n" "import django.settings\n" "\n" "import myproject.test\n" ) - test_output_custom = SortImports( - file_contents=test_input, known_third_party=["django"], default_section="STDLIB" + test_output_custom = api.sort_code_string( + code=test_input, known_third_party=["django"], default_section="STDLIB" ) assert test_output_custom == ( "import myproject.test\n" "import os\n" "import sys\n" "\n" "import django.settings\n" @@ -954,8 +954,8 @@ def test_first_party_overrides_standard_section() -> None: "import os\n" "import profile.test\n" ) - test_output = SortImports( - file_contents=test_input, known_first_party=["profile"], py_version="27" + test_output = api.sort_code_string( + code=test_input, known_first_party=["profile"], py_version="27" ) assert test_output == ( "import os\n" @@ -984,8 +984,8 @@ def test_known_pattern_path_expansion() -> None: "import this\n" "import os\n" ) - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, default_section="THIRDPARTY", known_first_party=["./", "this", "kate_plugin", "isort"], ) @@ -1008,8 +1008,8 @@ def test_force_single_line_imports() -> None: " lib13, lib14, lib15, lib16, lib17, \\\n" " lib18, lib20, lib21, lib22\n" ) - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, multi_line_output=WrapModes.GRID, line_length=40, force_single_line=True, @@ -1041,8 +1041,8 @@ def test_force_single_line_imports() -> None: test_input = ( "from third_party import lib_a, lib_b, lib_d\n" "from third_party.lib_c import lib1\n" ) - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, multi_line_output=WrapModes.GRID, line_length=40, force_single_line=True, @@ -1057,8 +1057,8 @@ def test_force_single_line_imports() -> None: def test_force_single_line_long_imports() -> None: test_input = "from veryveryveryveryveryvery import small, big\n" - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, multi_line_output=WrapModes.NOQA, line_length=40, force_single_line=True, @@ -1073,8 +1073,8 @@ def test_force_single_line_imports_and_sort_within_sections() -> None: test_input = ( "from third_party import lib_a, lib_b, lib_d\n" "from third_party.lib_c import lib1\n" ) - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, multi_line_output=WrapModes.GRID, line_length=40, force_single_line=True, @@ -1086,8 +1086,8 @@ def test_force_single_line_imports_and_sort_within_sections() -> None: "from third_party import lib_d\n" "from third_party.lib_c import lib1\n" ) - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, multi_line_output=WrapModes.GRID, line_length=40, force_single_line=True, @@ -1109,7 +1109,7 @@ def test_force_single_line_imports_and_sort_within_sections() -> None: from matplotlib import pyplot as plt """ test_output = ( - SortImports(file_contents=test_input, force_sort_within_sections=True, length_sort=True) + api.sort_code_string(code=test_input, force_sort_within_sections=True, length_sort=True) == test_input ) @@ -1124,8 +1124,8 @@ def test_titled_imports() -> None: "import myproject.test\n" "import django.settings" ) - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, known_third_party=["django"], import_heading_stdlib="Standard Library", import_heading_firstparty="My Stuff", @@ -1142,8 +1142,8 @@ def test_titled_imports() -> None: "# My Stuff\n" "import myproject.test\n" ) - test_second_run = SortImports( - file_contents=test_output, + test_second_run = api.sort_code_string( + code=test_output, known_third_party=["django"], import_heading_stdlib="Standard Library", import_heading_firstparty="My Stuff", @@ -1157,7 +1157,7 @@ def test_balanced_wrapping() -> None: "from __future__ import (absolute_import, division, print_function,\n" " unicode_literals)" ) - test_output = SortImports(file_contents=test_input, line_length=70, balanced_wrapping=True) + test_output = api.sort_code_string(code=test_input, line_length=70, balanced_wrapping=True) assert test_output == ( "from __future__ import (absolute_import, division,\n" " print_function, unicode_literals)\n" @@ -1347,7 +1347,7 @@ def test_as_imports_with_line_length() -> None: "from translate.storage import base as storage_base\n" "from translate.storage.placeables import general, parse as rich_parse\n" ) - assert SortImports(file_contents=test_input, combine_as_imports=False, line_length=40) == ( + assert api.sort_code_string(code=test_input, combine_as_imports=False, line_length=40) == ( "from translate.storage import \\\n base as storage_base\n" "from translate.storage.placeables import \\\n general\n" "from translate.storage.placeables import \\\n parse as rich_parse\n" @@ -1435,8 +1435,8 @@ def test_import_star() -> None: def test_include_trailing_comma() -> None: """Test for the include_trailing_comma option""" - test_output_grid = SortImports( - file_contents=SHORT_IMPORT, + test_output_grid = api.sort_code_string( + code=SHORT_IMPORT, multi_line_output=WrapModes.GRID, line_length=40, include_trailing_comma=True, @@ -1445,8 +1445,8 @@ def test_include_trailing_comma() -> None: "from third_party import (lib1, lib2,\n" " lib3, lib4,)\n" ) - test_output_vertical = SortImports( - file_contents=SHORT_IMPORT, + test_output_vertical = api.sort_code_string( + code=SHORT_IMPORT, multi_line_output=WrapModes.VERTICAL, line_length=40, include_trailing_comma=True, @@ -1458,8 +1458,8 @@ def test_include_trailing_comma() -> None: " lib4,)\n" ) - test_output_vertical_indent = SortImports( - file_contents=SHORT_IMPORT, + test_output_vertical_indent = api.sort_code_string( + code=SHORT_IMPORT, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, line_length=40, include_trailing_comma=True, @@ -1468,8 +1468,8 @@ def test_include_trailing_comma() -> None: "from third_party import (\n" " lib1,\n" " lib2,\n" " lib3,\n" " lib4,\n" ")\n" ) - test_output_vertical_grid = SortImports( - file_contents=SHORT_IMPORT, + test_output_vertical_grid = api.sort_code_string( + code=SHORT_IMPORT, multi_line_output=WrapModes.VERTICAL_GRID, line_length=40, include_trailing_comma=True, @@ -1478,8 +1478,8 @@ def test_include_trailing_comma() -> None: "from third_party import (\n lib1, lib2, lib3, lib4,)\n" ) - test_output_vertical_grid_grouped = SortImports( - file_contents=SHORT_IMPORT, + test_output_vertical_grid_grouped = api.sort_code_string( + code=SHORT_IMPORT, multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, line_length=40, include_trailing_comma=True, @@ -1488,8 +1488,8 @@ def test_include_trailing_comma() -> None: "from third_party import (\n lib1, lib2, lib3, lib4,\n)\n" ) - test_output_wrap_single_import_with_use_parentheses = SortImports( - file_contents=SINGLE_FROM_IMPORT, + test_output_wrap_single_import_with_use_parentheses = api.sort_code_string( + code=SINGLE_FROM_IMPORT, line_length=25, include_trailing_comma=True, use_parentheses=True, @@ -1498,8 +1498,8 @@ def test_include_trailing_comma() -> None: "from third_party import (\n lib1,)\n" ) - test_output_wrap_single_import_vertical_indent = SortImports( - file_contents=SINGLE_FROM_IMPORT, + test_output_wrap_single_import_vertical_indent = api.sort_code_string( + code=SINGLE_FROM_IMPORT, line_length=25, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, include_trailing_comma=True, @@ -1518,8 +1518,8 @@ def test_include_trailing_comma() -> None: " urlencode, # pylint: disable=no-n" "ame-in-module,import-error\n)\n" ) - trailing_comma_with_comment = SortImports( - file_contents=trailing_comma_with_comment, + trailing_comma_with_comment = api.sort_code_string( + code=trailing_comma_with_comment, line_length=80, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, include_trailing_comma=True, @@ -1527,8 +1527,8 @@ def test_include_trailing_comma() -> None: ) assert trailing_comma_with_comment == expected_trailing_comma_with_comment # The next time around, it should be equal - trailing_comma_with_comment = SortImports( - file_contents=trailing_comma_with_comment, + trailing_comma_with_comment = api.sort_code_string( + code=trailing_comma_with_comment, line_length=80, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, include_trailing_comma=True, @@ -1623,8 +1623,8 @@ def test_correctly_placed_imports() -> None: "get_right\n" ) assert ( - SortImports( - file_contents=test_input, + api.sort_code_string( + code=test_input, force_single_line=True, line_length=140, known_third_party=["django", "model_mommy"], @@ -1667,7 +1667,7 @@ def test_long_line_comments() -> None: "sync_stage_envdir, " "update_stage_app, update_stage_cron # noqa\n" ) - assert SortImports(file_contents=test_input, line_length=100, balanced_wrapping=True) == ( + assert api.sort_code_string(code=test_input, line_length=100, balanced_wrapping=True) == ( "from foo.utils.fabric_stuff.live import (check_clean_live, deploy_live, # noqa\n" " sync_live_envdir, update_live_app, " "update_live_cron)\n" @@ -1738,8 +1738,8 @@ def test_placement_control() -> None: "import p24.imports._VERSION as VERSION\n" "import p24.shared.media_wiki_syntax as syntax\n" ) - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, known_first_party=["p24", "p24.imports._VERSION"], known_standard_library=["p24.imports", "os", "sys"], known_third_party=["bottle"], @@ -1774,8 +1774,8 @@ def test_custom_sections() -> None: "import numpy as np\n" "import p24.shared.media_wiki_syntax as syntax\n" ) - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, known_first_party=["p24", "p24.imports._VERSION"], import_heading_stdlib="Standard Library", import_heading_thirdparty="Third Party", @@ -1830,8 +1830,8 @@ def test_glob_known() -> None: "from django.conf import settings\n" "from . import another\n" ) - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, import_heading_stdlib="Standard Library", import_heading_thirdparty="Third Party", import_heading_firstparty="First Party", @@ -1939,8 +1939,8 @@ def test_consistency() -> None: def test_force_grid_wrap() -> None: """Ensures removing imports works as expected.""" test_input = "from bar import lib2\nfrom foo import lib6, lib7\n" - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, force_grid_wrap=2, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, ) @@ -1953,8 +1953,8 @@ def test_force_grid_wrap() -> None: ) """ ) - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, force_grid_wrap=3, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, ) @@ -1968,8 +1968,8 @@ def test_force_grid_wrap_long() -> None: "from bar import lib2\n" "from babar import something_that_is_kind_of_long" ) - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, force_grid_wrap=2, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, line_length=9999, @@ -1991,8 +1991,8 @@ def test_uses_jinja_variables() -> None: test_input = ( "import sys\n" "import os\n" "import myproject.{ test }\n" "import django.{ settings }" ) - test_output = SortImports( - file_contents=test_input, known_third_party=["django"], known_first_party=["myproject"] + test_output = api.sort_code_string( + code=test_input, known_third_party=["django"], known_first_party=["myproject"] ) assert test_output == ( "import os\n" @@ -2019,8 +2019,8 @@ def test_import_split_is_word_boundary_aware() -> None: "from mycompany.model.size_value_array_import_func import \\\n" " get_size_value_array_import_func_jobs" ) - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, line_length=79, ) @@ -2037,7 +2037,7 @@ def test_other_file_encodings(tmpdir) -> None: tmp_fname = tmpdir.join(f"test_{encoding}.py") file_contents = f"# coding: {encoding}\n\ns = u'ã'\n" tmp_fname.write_binary(file_contents.encode(encoding)) - assert SortImports(filename=str(tmp_fname), settings_path=os.getcwd()) == file_contents + assert api.sort_code_string(filename=str(tmp_fname), settings_path=os.getcwd()) == file_contents def test_encoding_not_in_comment(tmpdir) -> None: @@ -2045,7 +2045,7 @@ def test_encoding_not_in_comment(tmpdir) -> None: tmp_fname = tmpdir.join("test_encoding.py") file_contents = "class Foo\n coding: latin1\n\ns = u'ã'\n" tmp_fname.write_binary(file_contents.encode("utf8")) - assert SortImports(filename=str(tmp_fname), settings_path=os.getcwd()) == file_contents + assert api.sort_code_string(filename=str(tmp_fname), settings_path=os.getcwd()) == file_contents def test_encoding_not_in_first_two_lines(tmpdir) -> None: @@ -2053,7 +2053,7 @@ def test_encoding_not_in_first_two_lines(tmpdir) -> None: tmp_fname = tmpdir.join("test_encoding.py") file_contents = "\n\n# -*- coding: latin1\n\ns = u'ã'\n" tmp_fname.write_binary(file_contents.encode("utf8")) - assert SortImports(filename=str(tmp_fname), settings_path=os.getcwd()) == file_contents + assert api.sort_code_string(filename=str(tmp_fname), settings_path=os.getcwd()) == file_contents def test_comment_at_top_of_file() -> None: @@ -2218,14 +2218,14 @@ def test_alphabetic_sorting_no_newlines() -> None: erroneously introduce new lines (issue #328) """ test_input = "import os\n" - test_output = SortImports( - file_contents=test_input, force_alphabetical_sort_within_sections=True + test_output = api.sort_code_string( + code=test_input, force_alphabetical_sort_within_sections=True ) assert test_input == test_output test_input = "import os\n" "import unittest\n" "\n" "from a import b\n" "\n" "\n" "print(1)\n" - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, force_alphabetical_sort_within_sections=True, lines_after_imports=2, ) @@ -2250,8 +2250,8 @@ def test_sort_within_section() -> None: "from foo.bar import Quux\n" "from Foob import ar\n" ) - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, force_sort_within_sections=True, order_by_type=False, force_single_line=True, @@ -2291,8 +2291,8 @@ def test_forced_sepatate_globs() -> None: "\n" "import sys\n" ) - test_output = SortImports( - file_contents=test_input, forced_separate=["*.models"], line_length=120 + test_output = api.sort_code_string( + code=test_input, forced_separate=["*.models"], line_length=120 ) assert test_output == ( @@ -2330,23 +2330,23 @@ def test_no_additional_lines_issue_358() -> None: " unicode_literals\n" ")\n" ) - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, line_length=20, ) assert test_output == expected_output - test_output = SortImports( - file_contents=test_output, + test_output = api.sort_code_string( + code=test_output, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, line_length=20, ) assert test_output == expected_output for _attempt in range(5): - test_output = SortImports( - file_contents=test_output, + test_output = api.sort_code_string( + code=test_output, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, line_length=20, ) @@ -2374,23 +2374,23 @@ def test_no_additional_lines_issue_358() -> None: " unicode_literals\n" ")\n" ) - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, line_length=20, ) assert test_output == expected_output - test_output = SortImports( - file_contents=test_output, + test_output = api.sort_code_string( + code=test_output, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, line_length=20, ) assert test_output == expected_output for _attempt in range(5): - test_output = SortImports( - file_contents=test_output, + test_output = api.sort_code_string( + code=test_output, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, line_length=20, ) @@ -2485,8 +2485,8 @@ def test_sys_path_mutation(tmpdir) -> None: def test_long_single_line() -> None: """Test to ensure long single lines get handled correctly""" - output = SortImports( - file_contents="from ..views import (" + output = api.sort_code_string( + code="from ..views import (" " _a," "_xxxxxx_xxxxxxx_xxxxxxxx_xxx_xxxxxxx as xxxxxx_xxxxxxx_xxxxxxxx_xxx_xxxxxxx)", line_length=79, @@ -2494,8 +2494,8 @@ def test_long_single_line() -> None: for line in output.split("\n"): assert len(line) <= 79 - output = SortImports( - file_contents="from ..views import (" + output = api.sort_code_string( + code="from ..views import (" " _a," "_xxxxxx_xxxxxxx_xxxxxxxx_xxx_xxxxxxx as xxxxxx_xxxxxxx_xxxxxxxx_xxx_xxxxxxx)", line_length=76, @@ -2567,8 +2567,8 @@ def test_alias_using_paren_issue_466() -> None: ")\n" ) assert ( - SortImports( - file_contents=test_input, + api.sort_code_string( + code=test_input, line_length=50, multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, use_parentheses=True, @@ -2587,8 +2587,8 @@ def test_long_alias_using_paren_issue_957() -> None: " module as very_very_very_very_very_very_very_very_very_very_long_alias\n" ")\n" ) - out = SortImports( - file_contents=test_input, + out = api.sort_code_string( + code=test_input, line_length=50, use_parentheses=True, multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, @@ -2604,8 +2604,8 @@ def test_long_alias_using_paren_issue_957() -> None: " module as very_very_very_very_very_very_very_very_very_very_long_alias\n" ")\n" ) - out = SortImports( - file_contents=test_input, + out = api.sort_code_string( + code=test_input, line_length=50, use_parentheses=True, multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, @@ -2623,8 +2623,8 @@ def test_long_alias_using_paren_issue_957() -> None: "_very_very_very_very_very_very_long_alias\n" ")\n" ) - out = SortImports( - file_contents=test_input, + out = api.sort_code_string( + code=test_input, line_length=50, use_parentheses=True, multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, @@ -2664,8 +2664,8 @@ def test_import_wraps_with_comment_issue_471() -> None: " SuperLongClassName) # @UnusedImport -- long string of comments which wrap over\n" ) assert ( - SortImports( - file_contents=test_input, line_length=50, multi_line_output=1, use_parentheses=True + api.sort_code_string( + code=test_input, line_length=50, multi_line_output=1, use_parentheses=True ) == expected_output ) @@ -2715,7 +2715,7 @@ def test_sort_within_sections_with_force_to_top_issue_473() -> None: """Test to ensure it's possible to sort within sections with items forced to top""" test_input = "import z\nimport foo\nfrom foo import bar\n" assert ( - SortImports(file_contents=test_input, force_sort_within_sections=True, force_to_top=["z"]) + api.sort_code_string(code=test_input, force_sort_within_sections=True, force_to_top=["z"]) == test_input ) @@ -2762,8 +2762,8 @@ def test_no_extra_lines_issue_557() -> None: "HTTPDownloadHandler\n" ) assert ( - SortImports( - file_contents=test_input, + api.sort_code_string( + code=test_input, force_alphabetical_sort=True, force_sort_within_sections=True, line_length=100, @@ -2779,8 +2779,8 @@ def test_long_import_wrap_support_with_mode_2() -> None: " an_even_longer_function_name_over_80_characters\n" ) assert ( - SortImports( - file_contents=test_input, multi_line_output=WrapModes.HANGING_INDENT, line_length=80 + api.sort_code_string( + code=test_input, multi_line_output=WrapModes.HANGING_INDENT, line_length=80 ) == test_input ) @@ -2858,8 +2858,8 @@ def test_not_splitted_sections() -> None: assert api.sort_code_string(test_input, no_lines_before=["FIRSTPARTY"]) == test_input # in case when THIRDPARTY section is excluded from sections list, # it's ok to merge STDLIB and FIRSTPARTY - assert SortImports( - file_contents=test_input, + assert api.sort_code_string( + code=test_input, sections=["STDLIB", "FIRSTPARTY", "LOCALFOLDER"], no_lines_before=["FIRSTPARTY"], ) == (stdlib_section + firstparty_section + whiteline + local_section + whiteline + statement) @@ -2870,8 +2870,8 @@ def test_not_splitted_sections() -> None: def test_no_lines_before_empty_section() -> None: test_input = "import first\nimport custom\n" assert ( - SortImports( - file_contents=test_input, + api.sort_code_string( + code=test_input, known_third_party=["first"], known_custom=["custom"], sections=["THIRDPARTY", "LOCALFOLDER", "CUSTOM"], @@ -3023,7 +3023,7 @@ def test_new_lines_are_preserved() -> None: with open(rn_newline.name, mode="w", newline="") as rn_newline_input: rn_newline_input.write("import sys\r\nimport os\r\n") - SortImports(rn_newline.name, settings_path=os.getcwd()) + api.sort_code_string(rn_newline.name, settings_path=os.getcwd()) with open(rn_newline.name) as new_line_file: print(new_line_file.read()) with open(rn_newline.name, newline="") as rn_newline_file: @@ -3039,7 +3039,7 @@ def test_new_lines_are_preserved() -> None: with open(r_newline.name, mode="w", newline="") as r_newline_input: r_newline_input.write("import sys\rimport os\r") - SortImports(r_newline.name, settings_path=os.getcwd()) + api.sort_code_string(r_newline.name, settings_path=os.getcwd()) with open(r_newline.name, newline="") as r_newline_file: r_newline_contents = r_newline_file.read() assert r_newline_contents == "import os\rimport sys\r" @@ -3053,7 +3053,7 @@ def test_new_lines_are_preserved() -> None: with open(n_newline.name, mode="w", newline="") as n_newline_input: n_newline_input.write("import sys\nimport os\n") - SortImports(n_newline.name, settings_path=os.getcwd()) + api.sort_code_string(n_newline.name, settings_path=os.getcwd()) with open(n_newline.name, newline="") as n_newline_file: n_newline_contents = n_newline_file.read() assert n_newline_contents == "import os\nimport sys\n" @@ -3369,8 +3369,8 @@ def test_multiple_as_imports() -> None: assert test_output == "from a import b as b, b as bb, b as bb_\n" test_output = api.sort_code_string(test_input, keep_direct_and_as_imports=True) assert test_output == test_input - test_output = SortImports( - file_contents=test_input, combine_as_imports=True, keep_direct_and_as_imports=True + test_output = api.sort_code_string( + code=test_input, combine_as_imports=True, keep_direct_and_as_imports=True ) assert test_output == "from a import b as b, b as bb, b as bb_\n" @@ -3382,14 +3382,14 @@ def test_multiple_as_imports() -> None: ) test_output = api.sort_code_string(test_input, keep_direct_and_as_imports=False) assert test_output == "from a import b as b\nfrom a import b as bb\nfrom a import b as bb_\n" - test_output = SortImports( - file_contents=test_input, combine_as_imports=True, keep_direct_and_as_imports=False + test_output = api.sort_code_string( + code=test_input, combine_as_imports=True, keep_direct_and_as_imports=False ) assert test_output == "from a import b as b, b as bb, b as bb_\n" test_output = api.sort_code_string(test_input, keep_direct_and_as_imports=True) assert test_output == test_input - test_output = SortImports( - file_contents=test_input, combine_as_imports=True, keep_direct_and_as_imports=True + test_output = api.sort_code_string( + code=test_input, combine_as_imports=True, keep_direct_and_as_imports=True ) assert test_output == "from a import b, b as b, b as bb, b as bb_\n" @@ -3401,8 +3401,8 @@ def test_multiple_as_imports() -> None: ) test_output = api.sort_code_string(test_input, keep_direct_and_as_imports=False) assert test_output == "from a import b as c\nfrom a import b as e\nfrom a import b as f\n" - test_output = SortImports( - file_contents=test_input, combine_as_imports=True, keep_direct_and_as_imports=False + test_output = api.sort_code_string( + code=test_input, combine_as_imports=True, keep_direct_and_as_imports=False ) assert test_output == "from a import b as c, b as e, b as f\n" test_output = api.sort_code_string(test_input, keep_direct_and_as_imports=True) @@ -3410,30 +3410,30 @@ def test_multiple_as_imports() -> None: test_output == "from a import b\nfrom a import b as c\nfrom a import b as e\nfrom a import b as f\n" ) - test_output = SortImports( - file_contents=test_input, no_inline_sort=True, keep_direct_and_as_imports=False + test_output = api.sort_code_string( + code=test_input, no_inline_sort=True, keep_direct_and_as_imports=False ) assert test_output == "from a import b as c\nfrom a import b as e\nfrom a import b as f\n" - test_output = SortImports( - file_contents=test_input, keep_direct_and_as_imports=True, no_inline_sort=True + test_output = api.sort_code_string( + code=test_input, keep_direct_and_as_imports=True, no_inline_sort=True ) assert ( test_output == "from a import b\nfrom a import b as c\nfrom a import b as e\nfrom a import b as f\n" ) - test_output = SortImports( - file_contents=test_input, combine_as_imports=True, keep_direct_and_as_imports=True + test_output = api.sort_code_string( + code=test_input, combine_as_imports=True, keep_direct_and_as_imports=True ) assert test_output == "from a import b, b as c, b as e, b as f\n" - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, combine_as_imports=True, no_inline_sort=True, keep_direct_and_as_imports=False, ) assert test_output == "from a import b as e, b as c, b as f\n" - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, combine_as_imports=True, keep_direct_and_as_imports=True, no_inline_sort=True, @@ -3443,16 +3443,16 @@ def test_multiple_as_imports() -> None: test_input = "import a as a\nimport a as aa\nimport a as aa_\n" test_output = api.sort_code_string(test_input, keep_direct_and_as_imports=False) assert test_output == test_input - test_output = SortImports( - file_contents=test_input, combine_as_imports=True, keep_direct_and_as_imports=True + test_output = api.sort_code_string( + code=test_input, combine_as_imports=True, keep_direct_and_as_imports=True ) assert test_output == test_input test_input = "import a\nimport a as a\nimport a as aa\nimport a as aa_\n" test_output = api.sort_code_string(test_input, keep_direct_and_as_imports=False) assert test_output == "import a as a\nimport a as aa\nimport a as aa_\n" - test_output = SortImports( - file_contents=test_input, combine_as_imports=True, keep_direct_and_as_imports=True + test_output = api.sort_code_string( + code=test_input, combine_as_imports=True, keep_direct_and_as_imports=True ) assert test_output == test_input @@ -3468,8 +3468,8 @@ def test_all_imports_from_single_module() -> None: "from a import b as c, g as h\n" "from a import e as f\n" ) - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, combine_star=False, combine_as_imports=False, keep_direct_and_as_imports=False, @@ -3486,8 +3486,8 @@ def test_all_imports_from_single_module() -> None: "from a import i as j\n" "from a import w, x, y, z\n" ) - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, combine_star=True, combine_as_imports=False, keep_direct_and_as_imports=False, @@ -3495,8 +3495,8 @@ def test_all_imports_from_single_module() -> None: no_inline_sort=False, ) assert test_output == "import a\nfrom a import *\n" - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, combine_star=False, combine_as_imports=True, keep_direct_and_as_imports=False, @@ -3508,8 +3508,8 @@ def test_all_imports_from_single_module() -> None: "from a import *\n" "from a import b as c, b as d, e as f, g as h, i as j, w, x, y, z\n" ) - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, combine_star=False, combine_as_imports=False, keep_direct_and_as_imports=True, @@ -3527,8 +3527,8 @@ def test_all_imports_from_single_module() -> None: "from a import i as j\n" "from a import w, x, y, z\n" ) - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, combine_star=False, combine_as_imports=False, keep_direct_and_as_imports=False, @@ -3548,8 +3548,8 @@ def test_all_imports_from_single_module() -> None: "from a import y\n" "from a import z\n" ) - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, combine_star=False, combine_as_imports=False, keep_direct_and_as_imports=False, @@ -3566,8 +3566,8 @@ def test_all_imports_from_single_module() -> None: "from a import g as h\n" "from a import e as f\n" ) - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, combine_star=True, combine_as_imports=True, keep_direct_and_as_imports=False, @@ -3575,8 +3575,8 @@ def test_all_imports_from_single_module() -> None: no_inline_sort=False, ) assert test_output == "import a\nfrom a import *\n" - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, combine_star=True, combine_as_imports=False, keep_direct_and_as_imports=True, @@ -3584,8 +3584,8 @@ def test_all_imports_from_single_module() -> None: no_inline_sort=False, ) assert test_output == "import a\nfrom a import *\n" - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, combine_star=True, combine_as_imports=False, keep_direct_and_as_imports=False, @@ -3593,8 +3593,8 @@ def test_all_imports_from_single_module() -> None: no_inline_sort=False, ) assert test_output == "import a\nfrom a import *\n" - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, combine_star=True, combine_as_imports=False, keep_direct_and_as_imports=False, @@ -3602,8 +3602,8 @@ def test_all_imports_from_single_module() -> None: no_inline_sort=True, ) assert test_output == "import a\nfrom a import *\n" - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, combine_star=False, combine_as_imports=True, keep_direct_and_as_imports=True, @@ -3615,8 +3615,8 @@ def test_all_imports_from_single_module() -> None: "from a import *\n" "from a import b, b as c, b as d, e as f, g as h, i as j, w, x, y, z\n" ) - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, combine_star=False, combine_as_imports=True, keep_direct_and_as_imports=False, @@ -3636,8 +3636,8 @@ def test_all_imports_from_single_module() -> None: "from a import y\n" "from a import z\n" ) - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, combine_star=False, combine_as_imports=True, keep_direct_and_as_imports=False, @@ -3649,8 +3649,8 @@ def test_all_imports_from_single_module() -> None: "from a import *\n" "from a import b as d, b as c, z, x, y, w, i as j, g as h, e as f\n" ) - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, combine_star=False, combine_as_imports=False, keep_direct_and_as_imports=True, @@ -3671,8 +3671,8 @@ def test_all_imports_from_single_module() -> None: "from a import y\n" "from a import z\n" ) - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, combine_star=False, combine_as_imports=False, keep_direct_and_as_imports=True, @@ -3690,8 +3690,8 @@ def test_all_imports_from_single_module() -> None: "from a import g as h\n" "from a import e as f\n" ) - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, combine_star=False, combine_as_imports=False, keep_direct_and_as_imports=False, @@ -3711,8 +3711,8 @@ def test_all_imports_from_single_module() -> None: "from a import y\n" "from a import z\n" ) - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, combine_star=True, combine_as_imports=True, keep_direct_and_as_imports=True, @@ -3720,8 +3720,8 @@ def test_all_imports_from_single_module() -> None: no_inline_sort=False, ) assert test_output == "import a\nfrom a import *\n" - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, combine_star=True, combine_as_imports=True, keep_direct_and_as_imports=False, @@ -3729,8 +3729,8 @@ def test_all_imports_from_single_module() -> None: no_inline_sort=False, ) assert test_output == "import a\nfrom a import *\n" - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, combine_star=True, combine_as_imports=True, keep_direct_and_as_imports=False, @@ -3738,8 +3738,8 @@ def test_all_imports_from_single_module() -> None: no_inline_sort=True, ) assert test_output == "import a\nfrom a import *\n" - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, combine_star=True, combine_as_imports=False, keep_direct_and_as_imports=True, @@ -3747,8 +3747,8 @@ def test_all_imports_from_single_module() -> None: no_inline_sort=False, ) assert test_output == "import a\nfrom a import *\n" - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, combine_star=True, combine_as_imports=False, keep_direct_and_as_imports=True, @@ -3756,8 +3756,8 @@ def test_all_imports_from_single_module() -> None: no_inline_sort=True, ) assert test_output == "import a\nfrom a import *\n" - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, combine_star=True, combine_as_imports=False, keep_direct_and_as_imports=False, @@ -3765,8 +3765,8 @@ def test_all_imports_from_single_module() -> None: no_inline_sort=True, ) assert test_output == "import a\nfrom a import *\n" - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, combine_star=False, combine_as_imports=True, keep_direct_and_as_imports=True, @@ -3787,8 +3787,8 @@ def test_all_imports_from_single_module() -> None: "from a import y\n" "from a import z\n" ) - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, combine_star=False, combine_as_imports=True, keep_direct_and_as_imports=True, @@ -3800,8 +3800,8 @@ def test_all_imports_from_single_module() -> None: "from a import *\n" "from a import b, b as d, b as c, z, x, y, w, i as j, g as h, e as f\n" ) - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, combine_star=False, combine_as_imports=False, keep_direct_and_as_imports=True, @@ -3822,8 +3822,8 @@ def test_all_imports_from_single_module() -> None: "from a import y\n" "from a import z\n" ) - test_output = SortImports( - file_contents=test_input, + test_output = api.sort_code_string( + code=test_input, combine_star=True, combine_as_imports=True, keep_direct_and_as_imports=True, @@ -3887,8 +3887,8 @@ def test_ensure_support_for_non_typed_but_cased_alphabetic_sort_issue_890() -> N "from pkg import recorder\n" ) assert ( - SortImports( - file_contents=test_input, + api.sort_code_string( + code=test_input, case_sensitive=True, order_by_type=False, force_single_line=True, @@ -4160,11 +4160,11 @@ def test_pyi_formatting_issue_942(tmpdir) -> None: source_py = tmpdir.join("source.py") source_py.write(test_input) - assert SortImports(filename=str(source_py)).splitlines() == expected_py_output + assert api.sort_code_string(filename=str(source_py)).splitlines() == expected_py_output source_pyi = tmpdir.join("source.pyi") source_pyi.write(test_input) - assert SortImports(filename=str(source_pyi)).splitlines() == expected_pyi_output + assert api.sort_code_string(filename=str(source_pyi)).splitlines() == expected_pyi_output def test_move_class_issue_751() -> None: @@ -4665,8 +4665,8 @@ def test_cimport_support(): def test_cdef_support(): assert ( - SortImports( - file_contents=""" + api.sort_code_string( + code=""" from cpython.version cimport PY_MAJOR_VERSION cdef extern from *: @@ -4683,8 +4683,8 @@ def test_cdef_support(): ) assert ( - SortImports( - file_contents=""" + api.sort_code_string( + code=""" from cpython.version cimport PY_MAJOR_VERSION cpdef extern from *: @@ -4751,8 +4751,8 @@ def test_single_line_exclusions(): from typing import List, TypeVar """ assert ( - SortImports( - file_contents=test_input, force_single_line=True, single_line_exclusions=("typing",) + api.sort_code_string( + code=test_input, force_single_line=True, single_line_exclusions=("typing",) ) == expected_output ) @@ -4849,7 +4849,7 @@ def test_multiple_aliases(): import datetime as dt import datetime as dt2 """ - assert SortImports(keep_direct_and_as_imports=True, file_contents=test_input) == test_input + assert api.sort_code_string(keep_direct_and_as_imports=True, code=test_input) == test_input def test_parens_in_comment(): @@ -4953,8 +4953,8 @@ def test_python_future_category(): from .query_elastic import QueryElastic """ assert ( - SortImports( - file_contents=test_input, + api.sort_code_string( + code=test_input, force_grid_wrap=False, include_trailing_comma=True, indent=4, From 8b4feb9acfd1143795871fa4aa7955c0575185e4 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 12 Mar 2020 22:24:31 -0700 Subject: [PATCH 0450/1439] Fix setting of file path --- tests/test_isort.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/test_isort.py b/tests/test_isort.py index 0af6728ac..ae41fa068 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -5,6 +5,7 @@ import importlib.machinery import os import os.path +from pathlib import Path import posixpath import subprocess import sys @@ -743,7 +744,7 @@ def test_skip_with_file_name() -> None: test_input = "import django\nimport myproject\n" sort_imports = api.sort_code_string( - filename="/baz.py", code=test_input, settings_path=os.getcwd(), skip=["baz.py"] + file_path=Path("/baz.py"), code=test_input, settings_path=os.getcwd(), skip=["baz.py"] ) assert sort_imports.skipped assert sort_imports.output == "" @@ -2037,7 +2038,7 @@ def test_other_file_encodings(tmpdir) -> None: tmp_fname = tmpdir.join(f"test_{encoding}.py") file_contents = f"# coding: {encoding}\n\ns = u'ã'\n" tmp_fname.write_binary(file_contents.encode(encoding)) - assert api.sort_code_string(filename=str(tmp_fname), settings_path=os.getcwd()) == file_contents + assert api.sort_code_string(file_path=Path(tmp_fname), settings_path=os.getcwd()) == file_contents def test_encoding_not_in_comment(tmpdir) -> None: @@ -2045,7 +2046,7 @@ def test_encoding_not_in_comment(tmpdir) -> None: tmp_fname = tmpdir.join("test_encoding.py") file_contents = "class Foo\n coding: latin1\n\ns = u'ã'\n" tmp_fname.write_binary(file_contents.encode("utf8")) - assert api.sort_code_string(filename=str(tmp_fname), settings_path=os.getcwd()) == file_contents + assert api.sort_code_string(file_path=Path(tmp_fname), settings_path=os.getcwd()) == file_contents def test_encoding_not_in_first_two_lines(tmpdir) -> None: @@ -2053,7 +2054,7 @@ def test_encoding_not_in_first_two_lines(tmpdir) -> None: tmp_fname = tmpdir.join("test_encoding.py") file_contents = "\n\n# -*- coding: latin1\n\ns = u'ã'\n" tmp_fname.write_binary(file_contents.encode("utf8")) - assert api.sort_code_string(filename=str(tmp_fname), settings_path=os.getcwd()) == file_contents + assert api.sort_code_string(file_path=Path(tmp_fname), settings_path=os.getcwd()) == file_contents def test_comment_at_top_of_file() -> None: @@ -4160,11 +4161,11 @@ def test_pyi_formatting_issue_942(tmpdir) -> None: source_py = tmpdir.join("source.py") source_py.write(test_input) - assert api.sort_code_string(filename=str(source_py)).splitlines() == expected_py_output + assert api.sort_code_string(file_path=Path(source_py)).splitlines() == expected_py_output source_pyi = tmpdir.join("source.pyi") source_pyi.write(test_input) - assert api.sort_code_string(filename=str(source_pyi)).splitlines() == expected_pyi_output + assert api.sort_code_string(file_path=Path(source_pyi)).splitlines() == expected_pyi_output def test_move_class_issue_751() -> None: From fadcd15d9117cc198b2edaf7957872a33efdff89 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 13 Mar 2020 23:45:36 -0700 Subject: [PATCH 0451/1439] Start associating extension directly in test --- tests/test_isort.py | 101 ++++++++++++++++++-------------------------- 1 file changed, 40 insertions(+), 61 deletions(-) diff --git a/tests/test_isort.py b/tests/test_isort.py index ae41fa068..79441f2ae 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -200,7 +200,7 @@ def test_line_length() -> None: # Test Case described in issue #1015 test_output = api.sort_code_string( - REALLY_LONG_IMPORT, line_length=25, multi_line_output=WrapModes.HANGING_INDENT + REALLY_LONG_IMPORT, line_length=25, multi_line_output=WrapModes.HANGING_INDENT ) assert test_output == ( "from third_party import \\\n" @@ -263,9 +263,7 @@ def test_output_modes() -> None: ) comment_output_vertical = api.sort_code_string( - code=REALLY_LONG_IMPORT_WITH_COMMENT, - multi_line_output=WrapModes.VERTICAL, - line_length=40, + code=REALLY_LONG_IMPORT_WITH_COMMENT, multi_line_output=WrapModes.VERTICAL, line_length=40 ) assert comment_output_vertical == ( "from third_party import (lib1, # comment\n" @@ -836,8 +834,7 @@ def test_remove_imports() -> None: "import lib6\n" "import lib2\n" "import lib5\n" "import lib1\n" "from lib8 import a" ) test_output = api.sort_code_string( - code=test_input, - remove_imports=["import lib2", "import lib6", "from lib8 import a"], + code=test_input, remove_imports=["import lib2", "import lib6", "from lib8 import a"] ) assert test_output == "import lib1\nimport lib5\n" @@ -1010,10 +1007,7 @@ def test_force_single_line_imports() -> None: " lib18, lib20, lib21, lib22\n" ) test_output = api.sort_code_string( - code=test_input, - multi_line_output=WrapModes.GRID, - line_length=40, - force_single_line=True, + code=test_input, multi_line_output=WrapModes.GRID, line_length=40, force_single_line=True ) assert test_output == ( "from third_party import lib1\n" @@ -1043,10 +1037,7 @@ def test_force_single_line_imports() -> None: "from third_party import lib_a, lib_b, lib_d\n" "from third_party.lib_c import lib1\n" ) test_output = api.sort_code_string( - code=test_input, - multi_line_output=WrapModes.GRID, - line_length=40, - force_single_line=True, + code=test_input, multi_line_output=WrapModes.GRID, line_length=40, force_single_line=True ) assert test_output == ( "from third_party import lib_a\n" @@ -1059,10 +1050,7 @@ def test_force_single_line_imports() -> None: def test_force_single_line_long_imports() -> None: test_input = "from veryveryveryveryveryvery import small, big\n" test_output = api.sort_code_string( - code=test_input, - multi_line_output=WrapModes.NOQA, - line_length=40, - force_single_line=True, + code=test_input, multi_line_output=WrapModes.NOQA, line_length=40, force_single_line=True ) assert test_output == ( "from veryveryveryveryveryvery import big\n" @@ -1490,10 +1478,7 @@ def test_include_trailing_comma() -> None: ) test_output_wrap_single_import_with_use_parentheses = api.sort_code_string( - code=SINGLE_FROM_IMPORT, - line_length=25, - include_trailing_comma=True, - use_parentheses=True, + code=SINGLE_FROM_IMPORT, line_length=25, include_trailing_comma=True, use_parentheses=True ) assert test_output_wrap_single_import_with_use_parentheses == ( "from third_party import (\n lib1,)\n" @@ -1941,9 +1926,7 @@ def test_force_grid_wrap() -> None: """Ensures removing imports works as expected.""" test_input = "from bar import lib2\nfrom foo import lib6, lib7\n" test_output = api.sort_code_string( - code=test_input, - force_grid_wrap=2, - multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, + code=test_input, force_grid_wrap=2, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT ) assert ( test_output @@ -1955,9 +1938,7 @@ def test_force_grid_wrap() -> None: """ ) test_output = api.sort_code_string( - code=test_input, - force_grid_wrap=3, - multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, + code=test_input, force_grid_wrap=3, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT ) assert test_output == test_input @@ -2021,9 +2002,7 @@ def test_import_split_is_word_boundary_aware() -> None: " get_size_value_array_import_func_jobs" ) test_output = api.sort_code_string( - code=test_input, - multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, - line_length=79, + code=test_input, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, line_length=79 ) assert test_output == ( "from mycompany.model.size_value_array_import_func import (\n" @@ -2038,7 +2017,10 @@ def test_other_file_encodings(tmpdir) -> None: tmp_fname = tmpdir.join(f"test_{encoding}.py") file_contents = f"# coding: {encoding}\n\ns = u'ã'\n" tmp_fname.write_binary(file_contents.encode(encoding)) - assert api.sort_code_string(file_path=Path(tmp_fname), settings_path=os.getcwd()) == file_contents + assert ( + api.sort_code_string(file_path=Path(tmp_fname), settings_path=os.getcwd()) + == file_contents + ) def test_encoding_not_in_comment(tmpdir) -> None: @@ -2046,7 +2028,9 @@ def test_encoding_not_in_comment(tmpdir) -> None: tmp_fname = tmpdir.join("test_encoding.py") file_contents = "class Foo\n coding: latin1\n\ns = u'ã'\n" tmp_fname.write_binary(file_contents.encode("utf8")) - assert api.sort_code_string(file_path=Path(tmp_fname), settings_path=os.getcwd()) == file_contents + assert ( + api.sort_code_string(file_path=Path(tmp_fname), settings_path=os.getcwd()) == file_contents + ) def test_encoding_not_in_first_two_lines(tmpdir) -> None: @@ -2054,7 +2038,9 @@ def test_encoding_not_in_first_two_lines(tmpdir) -> None: tmp_fname = tmpdir.join("test_encoding.py") file_contents = "\n\n# -*- coding: latin1\n\ns = u'ã'\n" tmp_fname.write_binary(file_contents.encode("utf8")) - assert api.sort_code_string(file_path=Path(tmp_fname), settings_path=os.getcwd()) == file_contents + assert ( + api.sort_code_string(file_path=Path(tmp_fname), settings_path=os.getcwd()) == file_contents + ) def test_comment_at_top_of_file() -> None: @@ -2226,9 +2212,7 @@ def test_alphabetic_sorting_no_newlines() -> None: test_input = "import os\n" "import unittest\n" "\n" "from a import b\n" "\n" "\n" "print(1)\n" test_output = api.sort_code_string( - code=test_input, - force_alphabetical_sort_within_sections=True, - lines_after_imports=2, + code=test_input, force_alphabetical_sort_within_sections=True, lines_after_imports=2 ) assert test_input == test_output @@ -2332,24 +2316,18 @@ def test_no_additional_lines_issue_358() -> None: ")\n" ) test_output = api.sort_code_string( - code=test_input, - multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, - line_length=20, + code=test_input, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, line_length=20 ) assert test_output == expected_output test_output = api.sort_code_string( - code=test_output, - multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, - line_length=20, + code=test_output, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, line_length=20 ) assert test_output == expected_output for _attempt in range(5): test_output = api.sort_code_string( - code=test_output, - multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, - line_length=20, + code=test_output, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, line_length=20 ) assert test_output == expected_output @@ -2376,24 +2354,18 @@ def test_no_additional_lines_issue_358() -> None: ")\n" ) test_output = api.sort_code_string( - code=test_input, - multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, - line_length=20, + code=test_input, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, line_length=20 ) assert test_output == expected_output test_output = api.sort_code_string( - code=test_output, - multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, - line_length=20, + code=test_output, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, line_length=20 ) assert test_output == expected_output for _attempt in range(5): test_output = api.sort_code_string( - code=test_output, - multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, - line_length=20, + code=test_output, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, line_length=20 ) assert test_output == expected_output @@ -3889,10 +3861,7 @@ def test_ensure_support_for_non_typed_but_cased_alphabetic_sort_issue_890() -> N ) assert ( api.sort_code_string( - code=test_input, - case_sensitive=True, - order_by_type=False, - force_single_line=True, + code=test_input, case_sensitive=True, order_by_type=False, force_single_line=True ) == expected_output ) @@ -4161,11 +4130,21 @@ def test_pyi_formatting_issue_942(tmpdir) -> None: source_py = tmpdir.join("source.py") source_py.write(test_input) - assert api.sort_code_string(file_path=Path(source_py)).splitlines() == expected_py_output + assert ( + api.sort_code_string( + code=Path(source_py).read_text(), file_path=Path(source_py) + ).splitlines() + == expected_py_output + ) source_pyi = tmpdir.join("source.pyi") source_pyi.write(test_input) - assert api.sort_code_string(file_path=Path(source_pyi)).splitlines() == expected_pyi_output + assert ( + api.sort_code_string( + code=Path("source.pyi").read_text(), file_path=Path(source_pyi) + ).splitlines() + == expected_pyi_output + ) def test_move_class_issue_751() -> None: From 5a37661dde839e88610fe4f2108e6ef69c12acb5 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 14 Mar 2020 22:00:16 -0700 Subject: [PATCH 0452/1439] Fix extension test --- tests/test_isort.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_isort.py b/tests/test_isort.py index 79441f2ae..e112af029 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -4141,7 +4141,7 @@ def test_pyi_formatting_issue_942(tmpdir) -> None: source_pyi.write(test_input) assert ( api.sort_code_string( - code=Path("source.pyi").read_text(), file_path=Path(source_pyi) + code=Path(source_pyi).read_text(), extension="pyi", file_path=Path(source_pyi) ).splitlines() == expected_pyi_output ) From 54c0262eec43b245497ad16e9d34f8d28ea4a96c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 15 Mar 2020 22:30:37 -0700 Subject: [PATCH 0453/1439] Fix test cases to include code --- tests/test_isort.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/test_isort.py b/tests/test_isort.py index e112af029..89f22e6e8 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -2018,7 +2018,9 @@ def test_other_file_encodings(tmpdir) -> None: file_contents = f"# coding: {encoding}\n\ns = u'ã'\n" tmp_fname.write_binary(file_contents.encode(encoding)) assert ( - api.sort_code_string(file_path=Path(tmp_fname), settings_path=os.getcwd()) + api.sort_code_string( + Path(tmp_fname).read_text(), file_path=Path(tmp_fname), settings_path=os.getcwd() + ) == file_contents ) @@ -2029,7 +2031,10 @@ def test_encoding_not_in_comment(tmpdir) -> None: file_contents = "class Foo\n coding: latin1\n\ns = u'ã'\n" tmp_fname.write_binary(file_contents.encode("utf8")) assert ( - api.sort_code_string(file_path=Path(tmp_fname), settings_path=os.getcwd()) == file_contents + api.sort_code_string( + Path(tmp_fname).read_text(), file_path=Path(tmp_fname), settings_path=os.getcwd() + ) + == file_contents ) @@ -2039,7 +2044,10 @@ def test_encoding_not_in_first_two_lines(tmpdir) -> None: file_contents = "\n\n# -*- coding: latin1\n\ns = u'ã'\n" tmp_fname.write_binary(file_contents.encode("utf8")) assert ( - api.sort_code_string(file_path=Path(tmp_fname), settings_path=os.getcwd()) == file_contents + api.sort_code_string( + Path(tmp_fname).read_text(), file_path=Path(tmp_fname), settings_path=os.getcwd() + ) + == file_contents ) From 617618faec6e1b0d7c7680758a91d8696bca04d4 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 16 Mar 2020 20:19:32 -0700 Subject: [PATCH 0454/1439] Handle skip exception in test --- tests/test_isort.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_isort.py b/tests/test_isort.py index 89f22e6e8..a56dd582f 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -741,9 +741,10 @@ def test_skip_with_file_name() -> None: """Ensure skipping a file works even when file_contents is provided.""" test_input = "import django\nimport myproject\n" - sort_imports = api.sort_code_string( - file_path=Path("/baz.py"), code=test_input, settings_path=os.getcwd(), skip=["baz.py"] - ) + with pytest.raises(Exception): + sort_imports = api.sort_code_string( + file_path=Path("/baz.py"), code=test_input, settings_path=os.getcwd(), skip=["baz.py"] + ) assert sort_imports.skipped assert sort_imports.output == "" From 3e1438d375e0cc04ebbeca93462439ca0f650f10 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 17 Mar 2020 21:32:06 -0700 Subject: [PATCH 0455/1439] Fix skip file tests --- tests/test_isort.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/tests/test_isort.py b/tests/test_isort.py index a56dd582f..0c60be9b2 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -19,6 +19,7 @@ from isort.main import SortImports, is_python_file from isort.settings import WrapModes, Config from isort.utils import exists_case_sensitive +from isort.exceptions import FileSkipped try: import toml @@ -740,21 +741,17 @@ def test_skip() -> None: def test_skip_with_file_name() -> None: """Ensure skipping a file works even when file_contents is provided.""" test_input = "import django\nimport myproject\n" - - with pytest.raises(Exception): - sort_imports = api.sort_code_string( + with pytest.raises(FileSkipped): + api.sort_code_string( file_path=Path("/baz.py"), code=test_input, settings_path=os.getcwd(), skip=["baz.py"] ) - assert sort_imports.skipped - assert sort_imports.output == "" def test_skip_within_file() -> None: """Ensure skipping a whole file works.""" test_input = "# isort: skip_file\nimport django\nimport myproject\n" - sort_imports = api.sort_code_string(test_input, known_third_party=["django"]) - assert sort_imports.skipped - assert sort_imports.output == "" + with pytest.raises(FileSkipped): + api.sort_code_string(test_input, known_third_party=["django"]) def test_force_to_top() -> None: From 32d46cc3581c01624e3c96ab4483e0a970b8d493 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 18 Mar 2020 20:53:52 -0700 Subject: [PATCH 0456/1439] Use isort.sort_file for file modifying test case --- tests/test_isort.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_isort.py b/tests/test_isort.py index 0c60be9b2..f86a764dc 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -3002,7 +3002,7 @@ def test_new_lines_are_preserved() -> None: with open(rn_newline.name, mode="w", newline="") as rn_newline_input: rn_newline_input.write("import sys\r\nimport os\r\n") - api.sort_code_string(rn_newline.name, settings_path=os.getcwd()) + api.sort_file(rn_newline.name, settings_path=os.getcwd()) with open(rn_newline.name) as new_line_file: print(new_line_file.read()) with open(rn_newline.name, newline="") as rn_newline_file: @@ -3018,7 +3018,7 @@ def test_new_lines_are_preserved() -> None: with open(r_newline.name, mode="w", newline="") as r_newline_input: r_newline_input.write("import sys\rimport os\r") - api.sort_code_string(r_newline.name, settings_path=os.getcwd()) + api.sort_file(r_newline.name, settings_path=os.getcwd()) with open(r_newline.name, newline="") as r_newline_file: r_newline_contents = r_newline_file.read() assert r_newline_contents == "import os\rimport sys\r" @@ -3032,7 +3032,7 @@ def test_new_lines_are_preserved() -> None: with open(n_newline.name, mode="w", newline="") as n_newline_input: n_newline_input.write("import sys\nimport os\n") - api.sort_code_string(n_newline.name, settings_path=os.getcwd()) + api.sort_file(n_newline.name, settings_path=os.getcwd()) with open(n_newline.name, newline="") as n_newline_file: n_newline_contents = n_newline_file.read() assert n_newline_contents == "import os\nimport sys\n" From 214d412dfa31ea4b54cac2ef28f85f35d9137181 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 19 Mar 2020 22:10:48 -0700 Subject: [PATCH 0457/1439] Update test case to flag existing syntax error --- tests/test_isort.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_isort.py b/tests/test_isort.py index f86a764dc..ed559c05b 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -19,7 +19,7 @@ from isort.main import SortImports, is_python_file from isort.settings import WrapModes, Config from isort.utils import exists_case_sensitive -from isort.exceptions import FileSkipped +from isort.exceptions import FileSkipped, ExistingSyntaxErrors try: import toml @@ -1200,7 +1200,8 @@ def test_atomic_mode() -> None: # with syntax error content is not changed test_input += "while True print 'Hello world'" # blatant syntax error - assert api.sort_code_string(test_input, atomic=True) == test_input + with pytest.raises(ExistingSyntaxErrors): + api.sort_code_string(test_input, atomic=True) def test_order_by_type() -> None: From 63d38c0b7affb950ee721036b22269842bc4f50b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 20 Mar 2020 23:33:23 -0700 Subject: [PATCH 0458/1439] Initial API command for check --- isort/api.py | 20 ++++++++++++++++++++ tests/test_isort.py | 9 ++++----- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/isort/api.py b/isort/api.py index 01fbee323..7c695f9d9 100644 --- a/isort/api.py +++ b/isort/api.py @@ -72,6 +72,26 @@ def sort_code_string( return output_stream.read() +def check_code_string( + code: str, + show_diff: bool = False, + extension: str = "py", + config: Config = DEFAULT_CONFIG, + file_path: Optional[Path] = None, + disregard_skip: bool = False, + **config_kwargs, +) -> bool: + return check_imports( + StringIO(code), + show_diff=show_diff, + extension=extension, + config=config, + file_path=file_path, + disregard_skip=disregard_skip, + **config_kwargs, + ) + + def sorted_imports( input_stream: TextIO, output_stream: TextIO, diff --git a/tests/test_isort.py b/tests/test_isort.py index ed559c05b..3cc906ca5 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -870,11 +870,10 @@ def test_check_newline_in_imports(capsys) -> None: """Ensure tests works correctly when new lines are in imports.""" test_input = "from lib1 import (\n sub1,\n sub2,\n sub3\n)\n" - api.sort_code_string( + assert api.check_code_string( code=test_input, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, line_length=20, - check=True, verbose=True, ) out, err = capsys.readouterr() @@ -2614,21 +2613,21 @@ def test_long_alias_using_paren_issue_957() -> None: def test_strict_whitespace_by_default(capsys) -> None: test_input = "import os\nfrom django.conf import settings\n" - api.sort_code_string(test_input, check=True) + assert not api.check_code_string(test_input) out, err = capsys.readouterr() assert out == "ERROR: Imports are incorrectly sorted and/or formatted.\n" def test_strict_whitespace_no_closing_newline_issue_676(capsys) -> None: test_input = "import os\n\nfrom django.conf import settings\n\nprint(1)" - api.sort_code_string(test_input, check=True) + assert api.check_code_string(test_input) out, err = capsys.readouterr() assert out == "" def test_ignore_whitespace(capsys) -> None: test_input = "import os\nfrom django.conf import settings\n" - api.sort_code_string(test_input, check=True, ignore_whitespace=True) + assert api.check_code_string(test_input, ignore_whitespace=True) out, err = capsys.readouterr() assert out == "" From 5e48e6242640ad478f5208d21618ea5e9b7806d3 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 21 Mar 2020 21:42:38 -0700 Subject: [PATCH 0459/1439] Fix config setting in test cases --- isort/api.py | 4 ++-- tests/test_isort.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/isort/api.py b/isort/api.py index 7c695f9d9..a656f88dd 100644 --- a/isort/api.py +++ b/isort/api.py @@ -59,6 +59,7 @@ def sort_code_string( ): input_stream = StringIO(code) output_stream = StringIO() + config = _config(config=config, **config_kwargs) sorted_imports( input_stream, output_stream, @@ -66,7 +67,6 @@ def sort_code_string( config=config, file_path=file_path, disregard_skip=disregard_skip, - **config_kwargs, ) output_stream.seek(0) return output_stream.read() @@ -81,6 +81,7 @@ def check_code_string( disregard_skip: bool = False, **config_kwargs, ) -> bool: + config = _config(config=config, **config_kwargs) return check_imports( StringIO(code), show_diff=show_diff, @@ -88,7 +89,6 @@ def check_code_string( config=config, file_path=file_path, disregard_skip=disregard_skip, - **config_kwargs, ) diff --git a/tests/test_isort.py b/tests/test_isort.py index 3cc906ca5..912a6bb1c 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -3990,7 +3990,7 @@ def test_failing_file_check_916() -> None: } # type: Dict[str, Any] assert api.sort_code_string(test_input, **settings) == expected_output assert api.sort_code_string(expected_output, **settings) == expected_output - assert not api.sort_code_string(expected_output, check=True, **settings).incorrectly_sorted + assert api.check_code_string(expected_output, **settings) def test_import_heading_issue_905() -> None: From ba78eb2d849bb5cac37793fe547861359ec0b6be Mon Sep 17 00:00:00 2001 From: Hongxin Liang Date: Thu, 19 Dec 2019 13:48:47 +0100 Subject: [PATCH 0460/1439] explicitly check for first-party modules under pwd could be identified as first-party instead of default configuration --- isort/finders.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/isort/finders.py b/isort/finders.py index 259ddbb48..e9b919a14 100644 --- a/isort/finders.py +++ b/isort/finders.py @@ -187,6 +187,8 @@ def find(self, module_name: str) -> Optional[str]: return sections.THIRDPARTY if os.path.normcase(prefix).startswith(self.stdlib_lib_prefix): return sections.STDLIB + if os.getcwd() in package_path: + return sections.FIRSTPARTY return self.config.default_section return None From c8be3c566dd29236993f1abd67012525f41d4820 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 22 Mar 2020 23:30:14 -0700 Subject: [PATCH 0461/1439] Fix test case to save and read from file --- tests/test_isort.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/test_isort.py b/tests/test_isort.py index 912a6bb1c..6deff666a 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -2015,12 +2015,8 @@ def test_other_file_encodings(tmpdir) -> None: tmp_fname = tmpdir.join(f"test_{encoding}.py") file_contents = f"# coding: {encoding}\n\ns = u'ã'\n" tmp_fname.write_binary(file_contents.encode(encoding)) - assert ( - api.sort_code_string( - Path(tmp_fname).read_text(), file_path=Path(tmp_fname), settings_path=os.getcwd() - ) - == file_contents - ) + api.sort_file(Path(tmp_fname), file_path=Path(tmp_fname), settings_path=os.getcwd()) + assert tmp_fname.read_text(encoding) == file_contents def test_encoding_not_in_comment(tmpdir) -> None: From c7378c3b77a115801200e7a20201075858997a28 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 23 Mar 2020 19:39:09 -0700 Subject: [PATCH 0462/1439] Remove SortImports --- isort/__init__.py | 1 - isort/compat.py | 118 ---------------------------------------------- isort/main.py | 2 +- 3 files changed, 1 insertion(+), 120 deletions(-) delete mode 100644 isort/compat.py diff --git a/isort/__init__.py b/isort/__init__.py index 5212cf2a6..e03cd0076 100644 --- a/isort/__init__.py +++ b/isort/__init__.py @@ -1,4 +1,3 @@ """Defines the public isort interface""" from . import settings # noqa: F401 from ._version import __version__ -from .compat import SortImports # noqa: F401 diff --git a/isort/compat.py b/isort/compat.py deleted file mode 100644 index a7561b7a7..000000000 --- a/isort/compat.py +++ /dev/null @@ -1,118 +0,0 @@ -import sys -from io import StringIO -from pathlib import Path -from typing import Any, Optional -from warnings import warn - -from . import api -from .exceptions import ExistingSyntaxErrors, FileSkipped, IntroducedSyntaxErrors -from .format import ask_whether_to_apply_changes_to_file, show_unified_diff -from .io import File, read_file -from .settings import Config - - -class SortImports: - incorrectly_sorted = False - skipped = False - - def __init__( - self, - filename: Optional[str] = None, - file_contents: str = "", - write_to_stdout: bool = False, - check: bool = False, - show_diff: bool = False, - settings_path: Optional[str] = None, - ask_to_apply: bool = False, - run_path: str = "", - check_skip: bool = True, - extension: str = "", - **setting_overrides: Any, - ): - file_encoding = "utf-8" - file_path: Optional[Path] = None - if filename: - if file_contents: - file_data = File.from_contents(file_contents, filename=filename) - file_stream, file_path, file_encoding = file_data - if not extension: - extension = file_data.extension - else: - with read_file(filename) as file_data: - file_stream, file_path, file_encoding = file_data - file_stream = StringIO(file_stream.read()) - if not extension: - extension = file_data.extension - else: - file_stream = StringIO(file_contents) - - if settings_path: - setting_overrides["settings_path"] = settings_path - elif file_path and "settings_file" not in setting_overrides: - setting_overrides["settings_path"] = file_path.parent - - config = Config(**setting_overrides) - - try: - if check: - self.incorrectly_sorted = not api.check_imports( - file_stream, - extension=extension, - config=config, - file_path=file_path, - show_diff=show_diff, - ) - self.output = "" - return - else: - output_stream = StringIO() - api.sorted_imports( - file_stream, - output_stream, - extension=extension, - config=config, - file_path=file_path, - ) - output_stream.seek(0) - self.output = output_stream.read() - except FileSkipped as error: - self.skipped = True - self.output = "" - if config.verbose: - warn(str(error)) - return - except ExistingSyntaxErrors: - warn("{file_path} unable to sort due to existing syntax errors") - self.output = file_contents - return - except IntroducedSyntaxErrors: - warn("{file_path} unable to sort as isort introduces new syntax errors") - self.output = file_contents - return - - if show_diff: - show_unified_diff( - file_input=file_contents, file_output=self.output, file_path=file_path - ) - - elif write_to_stdout: - sys.stdout.write(self.output) - - elif file_path and not check: - # if file_name resolves to True, file_path never None or '' - if self.output == file_contents: - return - - if ask_to_apply: - show_unified_diff( - file_input=file_contents, file_output=self.output, file_path=file_path - ) - apply_changes = ask_whether_to_apply_changes_to_file(str(file_path)) - if not apply_changes: - return - - with file_path.open("w", encoding=file_encoding, newline="") as output_file: - if not config.quiet: - print(f"Fixing {file_path}") - - output_file.write(self.output) diff --git a/isort/main.py b/isort/main.py index e3bf69afe..b7f4c5844 100644 --- a/isort/main.py +++ b/isort/main.py @@ -10,7 +10,7 @@ from typing import Any, Dict, Iterable, Iterator, List, Optional, Sequence from warnings import warn -from . import SortImports, __version__, sections, api +from . import __version__, sections, api from .exceptions import FileSkipped from .logo import ASCII_ART from .profiles import profiles From 611e97c78d85ba03dc45b0494e3f9ba2cec372d7 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 24 Mar 2020 20:49:38 -0700 Subject: [PATCH 0463/1439] Fix hooks module to use new API --- isort/hooks.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/isort/hooks.py b/isort/hooks.py index 207320eba..f0ce71056 100644 --- a/isort/hooks.py +++ b/isort/hooks.py @@ -5,8 +5,9 @@ """ import subprocess # nosec - Needed for hook from typing import List +from pathlib import Path -from isort import SortImports +from isort import api def get_output(command: List[str]) -> str: @@ -56,11 +57,11 @@ def git_hook(strict: bool = False, modify: bool = False) -> int: staged_cmd = ["git", "show", ":%s" % filename] staged_contents = get_output(staged_cmd) - sort = SortImports(filename=filename, file_contents=staged_contents, check=True) - - if sort.incorrectly_sorted: + if not ( + api.sort_file(filename) + if modify + else api.check_code_string(staged_contents, file_path=Path(filename)) + ): errors += 1 - if modify: - SortImports(filename=filename, file_contents=staged_contents, check=False) return errors if strict else 0 From 126deb0c7dfd4d37b73927a9b35de0e04376e5a1 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 25 Mar 2020 21:48:20 -0700 Subject: [PATCH 0464/1439] Fix setuptools command to use new API --- isort/setuptools_commands.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/isort/setuptools_commands.py b/isort/setuptools_commands.py index ea4840f2d..5a51a8623 100644 --- a/isort/setuptools_commands.py +++ b/isort/setuptools_commands.py @@ -6,7 +6,7 @@ import setuptools -from . import SortImports +from . import api from .settings import DEFAULT_CONFIG, Config @@ -55,8 +55,7 @@ def run(self) -> None: for python_file in glob.iglob(os.path.join(path, "*.py")): try: - incorrectly_sorted = SortImports(python_file, **arguments).incorrectly_sorted - if incorrectly_sorted: + if not api.check_file(python_file, **arguments): wrong_sorted_files = True except OSError as error: # pragma: no cover warn(f"Unable to parse file {python_file} due to {error}") From 6bfe82cafe8dc6a6e648586d632e34f21238b7b6 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 26 Mar 2020 23:00:59 -0700 Subject: [PATCH 0465/1439] Remove last references to SortImports legacy API --- isort/pylama_isort.py | 4 ++-- tests/test_isort.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/isort/pylama_isort.py b/isort/pylama_isort.py index e5ee01f3d..b9db79081 100644 --- a/isort/pylama_isort.py +++ b/isort/pylama_isort.py @@ -5,7 +5,7 @@ from pylama.lint import Linter as BaseLinter -from . import SortImports +from . import api @contextmanager @@ -25,7 +25,7 @@ def allow(self, path: str) -> bool: def run(self, path: str, **meta: Any) -> List[Dict[str, Any]]: """Lint the file. Return an array of error dicts if appropriate.""" with supress_stdout(): - if SortImports(path, check=True).incorrectly_sorted: + if api.check_file(path, check=True): return [ {"lnum": 0, "col": 0, "text": "Incorrectly sorted imports.", "type": "ISORT"} ] diff --git a/tests/test_isort.py b/tests/test_isort.py index 6deff666a..9e2ce0766 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -16,7 +16,7 @@ import py import pytest from isort import finders, main, sections, api -from isort.main import SortImports, is_python_file +from isort.main import is_python_file from isort.settings import WrapModes, Config from isort.utils import exists_case_sensitive from isort.exceptions import FileSkipped, ExistingSyntaxErrors From 2823efda9d6ec2a14606c9e22ef8085f111d8842 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 27 Mar 2020 23:32:40 -0700 Subject: [PATCH 0466/1439] Fix pylama isort integration not to include check=True redundly --- isort/pylama_isort.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/pylama_isort.py b/isort/pylama_isort.py index b9db79081..d507b49b2 100644 --- a/isort/pylama_isort.py +++ b/isort/pylama_isort.py @@ -25,7 +25,7 @@ def allow(self, path: str) -> bool: def run(self, path: str, **meta: Any) -> List[Dict[str, Any]]: """Lint the file. Return an array of error dicts if appropriate.""" with supress_stdout(): - if api.check_file(path, check=True): + if api.check_file(path): return [ {"lnum": 0, "col": 0, "text": "Incorrectly sorted imports.", "type": "ISORT"} ] From fc561301c3a3aea79043348a01e6a468a5693d3e Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 28 Mar 2020 22:07:39 -0700 Subject: [PATCH 0467/1439] Remove no longer needed import check --- tests/test_importable.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_importable.py b/tests/test_importable.py index 2dc79bcbb..4138738a8 100644 --- a/tests/test_importable.py +++ b/tests/test_importable.py @@ -10,7 +10,6 @@ def test_importable(): import isort._version import isort.api import isort.comments - import isort.compat import isort.exceptions import isort.finders import isort.format From a80cc9be4305cc0900b7bf4302b56037e2dfbfdd Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 29 Mar 2020 23:42:24 -0700 Subject: [PATCH 0468/1439] Remove no longer needed module / import --- isort/isort.py | 1 - tests/test_importable.py | 1 - 2 files changed, 2 deletions(-) delete mode 100644 isort/isort.py diff --git a/isort/isort.py b/isort/isort.py deleted file mode 100644 index 30e5a5c9d..000000000 --- a/isort/isort.py +++ /dev/null @@ -1 +0,0 @@ -from .compat import SortImports diff --git a/tests/test_importable.py b/tests/test_importable.py index 4138738a8..0fcf0300e 100644 --- a/tests/test_importable.py +++ b/tests/test_importable.py @@ -14,7 +14,6 @@ def test_importable(): import isort.finders import isort.format import isort.hooks - import isort.isort import isort.logo import isort.main import isort.output From 62ca16d355716c3baaf7a661269e54a517fef25d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 30 Mar 2020 23:07:55 -0700 Subject: [PATCH 0469/1439] Fix mock statement for new API --- tests/test_hooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_hooks.py b/tests/test_hooks.py index 9fb1e6ac4..639ac378f 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -21,5 +21,5 @@ class FakeProecssResponse(object): stdout = b"import b\nimport a" with patch("subprocess.run", MagicMock(return_value=FakeProecssResponse())) as run_mock: - with patch("isort.hooks.SortImports", MagicMock()): + with patch("isort.hooks.api", MagicMock()): hooks.git_hook(modify=True) From 0dab45f1bfc1ce983fc7a195ad13b9a8a2e60c24 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 31 Mar 2020 21:34:34 -0700 Subject: [PATCH 0470/1439] Fix setting of config values for check_file, fix test for file --- isort/api.py | 2 -- isort/pylama_isort.py | 2 +- tests/test_pylama_isort.py | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/isort/api.py b/isort/api.py index a656f88dd..ea4735715 100644 --- a/isort/api.py +++ b/isort/api.py @@ -149,7 +149,6 @@ def check_imports( config=config, file_path=file_path, disregard_skip=disregard_skip, - **config_kwargs, ) if not changed: @@ -169,7 +168,6 @@ def check_imports( config=config, file_path=file_path, disregard_skip=disregard_skip, - **config_kwargs, ) output_stream.seek(0) diff --git a/isort/pylama_isort.py b/isort/pylama_isort.py index d507b49b2..0e14d5696 100644 --- a/isort/pylama_isort.py +++ b/isort/pylama_isort.py @@ -25,7 +25,7 @@ def allow(self, path: str) -> bool: def run(self, path: str, **meta: Any) -> List[Dict[str, Any]]: """Lint the file. Return an array of error dicts if appropriate.""" with supress_stdout(): - if api.check_file(path): + if not api.check_file(path): return [ {"lnum": 0, "col": 0, "text": "Incorrectly sorted imports.", "type": "ISORT"} ] diff --git a/tests/test_pylama_isort.py b/tests/test_pylama_isort.py index 061517ced..b7b78c138 100644 --- a/tests/test_pylama_isort.py +++ b/tests/test_pylama_isort.py @@ -12,7 +12,7 @@ def test_allow(self): assert self.instance.allow("test_case.py") def test_run(self, src_dir, tmpdir): - assert not self.instance.run(os.path.join(src_dir, "isort.py")) + assert not self.instance.run(os.path.join(src_dir, "api.py")) incorrect = tmpdir.join("incorrect.py") incorrect.write("import b\nimport a\n") From df58905729555448b18650f85b09ca66d67f0d28 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 1 Apr 2020 23:11:06 -0700 Subject: [PATCH 0471/1439] Upgrade out of date dependencies --- poetry.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index 9d4bbc0b0..9ddbdd6c9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -956,8 +956,8 @@ category = "dev" description = "YAML parser and emitter for Python" name = "pyyaml" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "5.1.2" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "5.3.1" [[package]] category = "main" @@ -1118,8 +1118,8 @@ category = "main" description = "HTTP library with thread-safe connection pooling, file post, and more." name = "urllib3" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" -version = "1.25.6" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +version = "1.25.8" [[package]] category = "main" @@ -1296,7 +1296,7 @@ pytest = ["7e4800063ccfc306a53c461442526c5571e1462f61583506ce97e4da6a1d88c8", "c pytest-cov = ["cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b", "cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626"] pytest-mock = ["34520283d459cdf1d0dbb58a132df804697f1b966ecedf808bbf3d255af8f659", "f1ab8aefe795204efe7a015900296d1719e7bf0f4a0558d71e8599da1d1309d0"] python-dateutil = ["7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", "c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e"] -pyyaml = ["0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", "01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", "5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", "5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", "7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", "7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", "87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", "9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", "a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", "b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", "b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", "bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", "f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8"] +pyyaml = ["06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", "240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", "4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", "69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", "73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", "74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", "7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", "95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", "b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", "cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", "d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"] requests = ["11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", "9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"] requirementslib = ["50731ac1052473e4c7df59a44a1f3aa20f32e687110bc05d73c3b4109eebc23d", "8b594ab8b6280ee97cffd68fc766333345de150124d5b76061dd575c3a21fe5a"] safety = ["0a3a8a178a9c96242b224f033ee8d1d130c0448b0e6622d12deaf37f6c3b4e59", "5059f3ffab3648330548ea9c7403405bbfaf085b11235770825d14c58f24cb78"] @@ -1311,7 +1311,7 @@ traitlets = ["70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44", typed-ast = ["1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161", "18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e", "262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e", "2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0", "354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c", "48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47", "4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631", "630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4", "66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34", "71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b", "7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2", "838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e", "95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a", "bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233", "cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1", "d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36", "d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d", "d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", "fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66", "ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12"] typing = ["91dfe6f3f706ee8cc32d38edbbf304e9b7583fb37108fef38229617f8b3eba23", "c8cabb5ab8945cd2f54917be357d134db9cc1eb039e59d1606dc1e60cb1d9d36", "f38d83c5a7a7086543a0f649564d661859c5146a85775ab90c0d2f93ffaa9714"] typing-extensions = ["2ed632b30bb54fc3941c382decfd0ee4148f5c591651c9272473fea2c6397d95", "b1edbbf0652660e32ae780ac9433f4231e7339c7f9a8057d0f042fcbcea49b87", "d8179012ec2c620d3791ca6fe2bf7979d979acdbef1fca0bc56b37411db682ed"] -urllib3 = ["3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398", "9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86"] +urllib3 = ["2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc", "87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"] vistir = ["2166e3148a67c438c9e3edbba0cde153d42dec6e3bf5d8f4624feb27686c0990", "3a0529b4b6c2e842fd19b5ceaa95b6c9201321314825c110406d4af3331a0709"] vulture = ["20e73154efc9c6d2445663bc2f7fbf79b6c562f2b78cafc0ec0463b38610f42d", "a72834a8c9cf254f6ba0a64e9053b800e05df8391b1724702a19b00d7d849a43"] wcwidth = ["3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", "f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c"] From d08aaa3c29e6823cc4c6c80db541128663cbf567 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 2 Apr 2020 22:49:20 -0700 Subject: [PATCH 0472/1439] Use hug profile --- isort/pylama_isort.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/pylama_isort.py b/isort/pylama_isort.py index 0e14d5696..98faa93e2 100644 --- a/isort/pylama_isort.py +++ b/isort/pylama_isort.py @@ -25,7 +25,7 @@ def allow(self, path: str) -> bool: def run(self, path: str, **meta: Any) -> List[Dict[str, Any]]: """Lint the file. Return an array of error dicts if appropriate.""" with supress_stdout(): - if not api.check_file(path): + if not api.check_file(path, profile="hug"): return [ {"lnum": 0, "col": 0, "text": "Incorrectly sorted imports.", "type": "ISORT"} ] From aa67cd0101737fd6d9046538676ca45e59e267c6 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 3 Apr 2020 23:12:33 -0700 Subject: [PATCH 0473/1439] Use isort.cfg to set hug profile, sort imports --- .isort.cfg | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .isort.cfg diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 000000000..b3b28cbc2 --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,2 @@ +[settings] +profile=hug From cc3b0f0e3dd53b339feb344bf7c886c181b6b2da Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 4 Apr 2020 21:47:23 -0700 Subject: [PATCH 0474/1439] Fix passing of file_path in API --- isort/api.py | 22 +++++++++++----------- isort/pylama_isort.py | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/isort/api.py b/isort/api.py index ea4735715..f67096a6c 100644 --- a/isort/api.py +++ b/isort/api.py @@ -1,12 +1,12 @@ -import textwrap import sys +import textwrap from io import StringIO from itertools import chain from pathlib import Path from typing import List, Optional, TextIO, Union from warnings import warn -from . import output, parse, io +from . import io, output, parse from .exceptions import ( ExistingSyntaxErrors, FileSkipComment, @@ -14,10 +14,10 @@ IntroducedSyntaxErrors, ) from .format import ( + ask_whether_to_apply_changes_to_file, format_natural, remove_whitespace, show_unified_diff, - ask_whether_to_apply_changes_to_file, ) from .io import Empty, File from .settings import DEFAULT_CONFIG, FILE_SKIP_COMMENTS, Config @@ -59,7 +59,7 @@ def sort_code_string( ): input_stream = StringIO(code) output_stream = StringIO() - config = _config(config=config, **config_kwargs) + config = _config(path=file_path, config=config, **config_kwargs) sorted_imports( input_stream, output_stream, @@ -81,7 +81,7 @@ def check_code_string( disregard_skip: bool = False, **config_kwargs, ) -> bool: - config = _config(config=config, **config_kwargs) + config = _config(path=file_path, config=config, **config_kwargs) return check_imports( StringIO(code), show_diff=show_diff, @@ -101,7 +101,7 @@ def sorted_imports( disregard_skip: bool = False, **config_kwargs, ): - config = _config(config=config, **config_kwargs) + config = _config(path=file_path, config=config, **config_kwargs) content_source = str(file_path or "Passed in content") if not disregard_skip: if file_path and config.is_skipped(file_path): @@ -140,7 +140,7 @@ def check_imports( disregard_skip: bool = False, **config_kwargs, ) -> bool: - config = _config(config=config, **config_kwargs) + config = _config(path=file_path, config=config, **config_kwargs) changed: bool = sorted_imports( input_stream=input_stream, @@ -443,7 +443,7 @@ def check_file( show_diff=show_diff, extension=source_file.extension or "py", config=config, - file_path=source_file.path, + file_path=file_path or source_file.path, disregard_skip=disregard_skip, **config_kwargs, ) @@ -468,7 +468,7 @@ def sort_file( input_stream=source_file.stream, output_stream=sys.stdout, config=config, - file_path=source_file.path, + file_path=file_path or source_file.path, disregard_skip=disregard_skip, **config_kwargs, ) @@ -482,7 +482,7 @@ def sort_file( input_stream=source_file.stream, output_stream=output_stream, config=config, - file_path=source_file.path, + file_path=file_path or source_file.path, disregard_skip=disregard_skip, **config_kwargs, ) @@ -492,7 +492,7 @@ def sort_file( show_unified_diff( file_input=source_file.stream.read(), file_output=tmp_file.read_text(encoding=source_file.encoding), - file_path=source_file.path, + file_path=file_path or source_file.path, ) if ask_to_apply and not ask_whether_to_apply_changes_to_file( str(source_file.path) diff --git a/isort/pylama_isort.py b/isort/pylama_isort.py index 98faa93e2..0e14d5696 100644 --- a/isort/pylama_isort.py +++ b/isort/pylama_isort.py @@ -25,7 +25,7 @@ def allow(self, path: str) -> bool: def run(self, path: str, **meta: Any) -> List[Dict[str, Any]]: """Lint the file. Return an array of error dicts if appropriate.""" with supress_stdout(): - if not api.check_file(path, profile="hug"): + if not api.check_file(path): return [ {"lnum": 0, "col": 0, "text": "Incorrectly sorted imports.", "type": "ISORT"} ] From e17f5d21f1f858d2f6822bf148e66ae44781e117 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 5 Apr 2020 23:17:35 -0700 Subject: [PATCH 0475/1439] Sort imports and fix sorting of imports via CLI --- isort/hooks.py | 2 +- isort/io.py | 2 +- isort/main.py | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/isort/hooks.py b/isort/hooks.py index f0ce71056..07b92f898 100644 --- a/isort/hooks.py +++ b/isort/hooks.py @@ -4,8 +4,8 @@ exit_code = git_hook(strict=True|False, modify=True|False) """ import subprocess # nosec - Needed for hook -from typing import List from pathlib import Path +from typing import List from isort import api diff --git a/isort/io.py b/isort/io.py index e24cd9bb7..f8f72f08b 100644 --- a/isort/io.py +++ b/isort/io.py @@ -5,7 +5,7 @@ from contextlib import contextmanager from io import BytesIO, StringIO, TextIOWrapper from pathlib import Path -from typing import List, NamedTuple, Optional, TextIO, Tuple, Union, Iterator +from typing import Iterator, List, NamedTuple, Optional, TextIO, Tuple, Union from .exceptions import UnableToDetermineEncoding diff --git a/isort/main.py b/isort/main.py index b7f4c5844..2e442e824 100644 --- a/isort/main.py +++ b/isort/main.py @@ -10,7 +10,7 @@ from typing import Any, Dict, Iterable, Iterator, List, Optional, Sequence from warnings import warn -from . import __version__, sections, api +from . import __version__, api, sections from .exceptions import FileSkipped from .logo import ASCII_ART from .profiles import profiles @@ -84,7 +84,7 @@ def sort_imports( skipped: bool = False if check: try: - incorrectly_sorted = not api.check_file(file_name, config=config, **arguments) + incorrectly_sorted = not api.check_file(file_name, config=config) except FileSkipped: skipped = True return SortAttempt(incorrectly_sorted, skipped) @@ -95,7 +95,6 @@ def sort_imports( config=config, ask_to_apply=ask_to_apply, write_to_stdout=write_to_stdout, - **arguments, ) except FileSkipped: skipped = True From c3da8bb2fe72ce3d631a631d4dc6a3590ab0b15a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 6 Apr 2020 20:00:00 -0700 Subject: [PATCH 0476/1439] Disregard skip except for directly passed in files --- isort/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/api.py b/isort/api.py index f67096a6c..c0967b18e 100644 --- a/isort/api.py +++ b/isort/api.py @@ -434,7 +434,7 @@ def check_file( show_diff: bool = False, config: Config = DEFAULT_CONFIG, file_path: Optional[Path] = None, - disregard_skip: bool = False, + disregard_skip: bool = True, **config_kwargs, ) -> bool: with io.read_file(filename) as source_file: @@ -454,7 +454,7 @@ def sort_file( extension: str = "py", config: Config = DEFAULT_CONFIG, file_path: Optional[Path] = None, - disregard_skip: bool = False, + disregard_skip: bool = True, ask_to_apply: bool = False, show_diff: bool = False, write_to_stdout: bool = False, From e1218f79fcae0b032c3372578f0745961f901f26 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 7 Apr 2020 21:51:05 -0700 Subject: [PATCH 0477/1439] Close stream --- isort/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/isort/api.py b/isort/api.py index c0967b18e..f88a95cec 100644 --- a/isort/api.py +++ b/isort/api.py @@ -486,6 +486,7 @@ def sort_file( disregard_skip=disregard_skip, **config_kwargs, ) + source_file.stream.close() if changed: if show_diff or ask_to_apply: source_file.stream.seek(0) From 1926af486bfc1886786442ce26a46e14f2cd1c07 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 8 Apr 2020 21:37:05 -0700 Subject: [PATCH 0478/1439] Specify encoding --- tests/test_isort.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_isort.py b/tests/test_isort.py index 9e2ce0766..0b9336067 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -2026,7 +2026,7 @@ def test_encoding_not_in_comment(tmpdir) -> None: tmp_fname.write_binary(file_contents.encode("utf8")) assert ( api.sort_code_string( - Path(tmp_fname).read_text(), file_path=Path(tmp_fname), settings_path=os.getcwd() + Path(tmp_fname).read_text("utf8"), file_path=Path(tmp_fname), settings_path=os.getcwd() ) == file_contents ) @@ -2039,7 +2039,7 @@ def test_encoding_not_in_first_two_lines(tmpdir) -> None: tmp_fname.write_binary(file_contents.encode("utf8")) assert ( api.sort_code_string( - Path(tmp_fname).read_text(), file_path=Path(tmp_fname), settings_path=os.getcwd() + Path(tmp_fname).read_text("utf8"), file_path=Path(tmp_fname), settings_path=os.getcwd() ) == file_contents ) From 1621e19c5bf6deacc54a017115c47175a6e52765 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 9 Apr 2020 20:29:25 -0700 Subject: [PATCH 0479/1439] Add initial testing module for api.py --- tests/test_api.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 tests/test_api.py diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 000000000..88b1fcc67 --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,12 @@ +"""Tests the isort API module""" +import pytest + +from isort import api, exceptions + + +def test_sort_file_invalid_syntax(tmpdir) -> None: + """Test to ensure file encoding is respected""" + tmp_file = tmpdir.join(f"test_bad_syntax.py") + tmp_file.write_text("""print('mismathing quotes")""", "utf8") + with pytest.warns(UserWarning): + api.sort_file(tmp_file, atomic=True) From 3b8c76aaee54e0d49656f640c3f18d2a6c6fbe13 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 10 Apr 2020 23:03:06 -0700 Subject: [PATCH 0480/1439] Add initial check file check --- tests/test_api.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_api.py b/tests/test_api.py index 88b1fcc67..94a26f2c1 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -10,3 +10,9 @@ def test_sort_file_invalid_syntax(tmpdir) -> None: tmp_file.write_text("""print('mismathing quotes")""", "utf8") with pytest.warns(UserWarning): api.sort_file(tmp_file, atomic=True) + + +def test_check_file(tmpdir) -> None: + perfect = tmpdir.join(f"test_no_changes.py") + perfect.write_text("import a\nimport b\n", "utf8") + assert api.check_file(perfect, show_diff=True) From d2c50272ec4509e40562f441e534ba7457a493f3 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 11 Apr 2020 23:03:18 -0700 Subject: [PATCH 0481/1439] Add test for imperfect imports as well --- tests/test_api.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_api.py b/tests/test_api.py index 94a26f2c1..d20e0f5b5 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -16,3 +16,7 @@ def test_check_file(tmpdir) -> None: perfect = tmpdir.join(f"test_no_changes.py") perfect.write_text("import a\nimport b\n", "utf8") assert api.check_file(perfect, show_diff=True) + + imperfect = tmpdir.join(f"test_needs_changes.py") + imperfect.write_text("import b\nimport a\n", "utf8") + assert not api.check_file(imperfect, show_diff=True) From 9a7d11e577e6576ce0eba5708e9cf8588f368fdf Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 12 Apr 2020 23:19:23 -0700 Subject: [PATCH 0482/1439] add test for showing diff and writing to stdout when sorting file --- tests/test_api.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index d20e0f5b5..44db6e621 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -4,19 +4,24 @@ from isort import api, exceptions -def test_sort_file_invalid_syntax(tmpdir) -> None: - """Test to ensure file encoding is respected""" +def test_sort_file(tmpdir) -> None: tmp_file = tmpdir.join(f"test_bad_syntax.py") tmp_file.write_text("""print('mismathing quotes")""", "utf8") with pytest.warns(UserWarning): api.sort_file(tmp_file, atomic=True) + with pytest.warns(UserWarning): + api.sort_file(tmp_file, atomic=True, write_to_stdout=True) + + imperfect = tmpdir.join(f"test_needs_changes.py") + imperfect.write_text("import b\nimport a\n", "utf8") + api.sort_file(imperfect, write_to_stdout=True, show_diff=True) def test_check_file(tmpdir) -> None: perfect = tmpdir.join(f"test_no_changes.py") perfect.write_text("import a\nimport b\n", "utf8") assert api.check_file(perfect, show_diff=True) - + imperfect = tmpdir.join(f"test_needs_changes.py") imperfect.write_text("import b\nimport a\n", "utf8") assert not api.check_file(imperfect, show_diff=True) From 92ae90caeeb9e33428d07165348ceadc87ad94a8 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 13 Apr 2020 23:32:48 -0700 Subject: [PATCH 0483/1439] Skip introduce syntax errors, as they should never be reached as isort should never introduce any syntax errors --- isort/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/api.py b/isort/api.py index f88a95cec..14099d839 100644 --- a/isort/api.py +++ b/isort/api.py @@ -126,7 +126,7 @@ def sorted_imports( compile(output_stream.read(), content_source, "exec", 0, 1) output_stream.seek(0) except SyntaxError: - raise IntroducedSyntaxErrors(content_source) + raise IntroducedSyntaxErrors(content_source) # pragma: no cover return changed @@ -509,5 +509,5 @@ def sort_file( pass except ExistingSyntaxErrors: warn("{file_path} unable to sort due to existing syntax errors") - except IntroducedSyntaxErrors: + except IntroducedSyntaxErrors: # pragma: no cover warn("{file_path} unable to sort as isort introduces new syntax errors") From 8e026f3db6cbacd1806d96410695d920137460ac Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 14 Apr 2020 21:23:03 -0700 Subject: [PATCH 0484/1439] Fix pragma location --- isort/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/api.py b/isort/api.py index 14099d839..abb5f6694 100644 --- a/isort/api.py +++ b/isort/api.py @@ -125,8 +125,8 @@ def sorted_imports( try: compile(output_stream.read(), content_source, "exec", 0, 1) output_stream.seek(0) - except SyntaxError: - raise IntroducedSyntaxErrors(content_source) # pragma: no cover + except SyntaxError: # pragma: no cover + raise IntroducedSyntaxErrors(content_source) return changed From 57e1691862722ea54b2ced7998964396691b602f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 15 Apr 2020 22:22:33 -0700 Subject: [PATCH 0485/1439] Update cimport test to test swapping back and forth from cimports to normal imports --- isort/api.py | 4 ++-- tests/test_isort.py | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/isort/api.py b/isort/api.py index abb5f6694..4648d77a6 100644 --- a/isort/api.py +++ b/isort/api.py @@ -125,8 +125,8 @@ def sorted_imports( try: compile(output_stream.read(), content_source, "exec", 0, 1) output_stream.seek(0) - except SyntaxError: # pragma: no cover - raise IntroducedSyntaxErrors(content_source) + except SyntaxError: # pragma: no cover + raise IntroducedSyntaxErrors(content_source) return changed diff --git a/tests/test_isort.py b/tests/test_isort.py index 0b9336067..83290de23 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -4416,6 +4416,8 @@ def test_cimport_support(): from cef_time cimport * from cef_drag cimport * +import os + IF CEF_VERSION == 1: from cef_v8 cimport * cimport cef_v8_static @@ -4592,6 +4594,8 @@ def test_cimport_support(): from cef_drag cimport * from cef_time cimport * +import os + IF CEF_VERSION == 1: cimport cef_cookie_manager_namespace cimport cef_stream_static From 64ba8e99ec1cc82f99fb9ad8ab1da10b25fe6eb1 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 16 Apr 2020 22:51:08 -0700 Subject: [PATCH 0486/1439] Initial show_diff coverage --- isort/api.py | 2 +- tests/test_api.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/isort/api.py b/isort/api.py index 4648d77a6..8e3e20189 100644 --- a/isort/api.py +++ b/isort/api.py @@ -486,7 +486,6 @@ def sort_file( disregard_skip=disregard_skip, **config_kwargs, ) - source_file.stream.close() if changed: if show_diff or ask_to_apply: source_file.stream.seek(0) @@ -499,6 +498,7 @@ def sort_file( str(source_file.path) ): return + source_file.stream.close() tmp_file.replace(source_file.path) if not config.quiet: print(f"Fixing {source_file.path}") diff --git a/tests/test_api.py b/tests/test_api.py index 44db6e621..bc0a4ffa7 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -15,6 +15,7 @@ def test_sort_file(tmpdir) -> None: imperfect = tmpdir.join(f"test_needs_changes.py") imperfect.write_text("import b\nimport a\n", "utf8") api.sort_file(imperfect, write_to_stdout=True, show_diff=True) + api.sort_file(imperfect, show_diff=True) def test_check_file(tmpdir) -> None: From 2531d68c7747d1ae5aeac56609842888fe815263 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 17 Apr 2020 14:42:26 -0700 Subject: [PATCH 0487/1439] Add test case for applying change --- tests/test_api.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_api.py b/tests/test_api.py index bc0a4ffa7..5458dfbed 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,4 +1,6 @@ """Tests the isort API module""" +from unittest.mock import MagicMock, patch + import pytest from isort import api, exceptions @@ -15,6 +17,13 @@ def test_sort_file(tmpdir) -> None: imperfect = tmpdir.join(f"test_needs_changes.py") imperfect.write_text("import b\nimport a\n", "utf8") api.sort_file(imperfect, write_to_stdout=True, show_diff=True) + + # First show diff, but ensure change wont get written by asking to apply + # and ensuring answer is no. + with patch("isort.format.input", MagicMock(return_value="n")): + api.sort_file(imperfect, show_diff=True, ask_to_apply=True) + + # Then run again, but apply the change without asking api.sort_file(imperfect, show_diff=True) From 58db70aaa43372535554f060609806fe43c60409 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 18 Apr 2020 15:01:30 -0700 Subject: [PATCH 0488/1439] Add test for duplicate config infromation --- tests/test_api.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_api.py b/tests/test_api.py index 5458dfbed..6e0795f63 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -4,6 +4,7 @@ import pytest from isort import api, exceptions +from isort.settings import Config def test_sort_file(tmpdir) -> None: @@ -35,3 +36,8 @@ def test_check_file(tmpdir) -> None: imperfect = tmpdir.join(f"test_needs_changes.py") imperfect.write_text("import b\nimport a\n", "utf8") assert not api.check_file(imperfect, show_diff=True) + + +def test_sorted_imports_multiple_configs() -> None: + with pytest.raises(ValueError): + api.sort_code_string("import os", config=Config(line_length=80), line_length=80) From 9f623514f4fb8de3c90cce7234724d5d8369af61 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 18 Apr 2020 15:07:49 -0700 Subject: [PATCH 0489/1439] Add pragma skip --- isort/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/api.py b/isort/api.py index 8e3e20189..770109dd1 100644 --- a/isort/api.py +++ b/isort/api.py @@ -344,7 +344,7 @@ def sort_imports( contains_imports = True add_imports = [] - if next_import_section and not import_section: + if next_import_section and not import_section: # pragma: no cover import_section = next_import_section next_import_section = "" From bb31300342b0135099d75c30c9e1cf1554297433 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 18 Apr 2020 15:13:39 -0700 Subject: [PATCH 0490/1439] Remove tests from coverage report --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 2f5f9fad9..31b649c8e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -2,6 +2,7 @@ omit = isort/_future/* except ImportError: + tests/* include = isort/* From a9f91373e3877d0ad4827dc7e98863aeceda7e03 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 18 Apr 2020 15:41:18 -0700 Subject: [PATCH 0491/1439] Achieve 100% test coverage for wrap.py --- isort/wrap.py | 4 ++-- tests/test_wrap.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 tests/test_wrap.py diff --git a/isort/wrap.py b/isort/wrap.py index 37bf6ec26..a6dbeee16 100644 --- a/isort/wrap.py +++ b/isort/wrap.py @@ -10,8 +10,8 @@ def import_statement( import_start: str, from_imports: List[str], - comments: Sequence[str], - line_separator: str, + comments: Sequence[str] = (), + line_separator: str = "\n", config: Config = DEFAULT_CONFIG, multi_line_output: Optional[Modes] = None, ) -> str: diff --git a/tests/test_wrap.py b/tests/test_wrap.py new file mode 100644 index 000000000..2b8ec6fad --- /dev/null +++ b/tests/test_wrap.py @@ -0,0 +1,15 @@ +from isort import wrap +from isort.settings import Config + + +def test_import_statement(): + assert wrap.import_statement("", [], []) == "" + assert ( + wrap.import_statement("from x import ", ["y"], [], config=Config(balanced_wrapping=True)) + == "from x import (y)" + ) + assert ( + wrap.import_statement("from long_import ", ["verylong"] * 10, []) + == """from long_import (verylong, verylong, verylong, verylong, verylong, verylong, + verylong, verylong, verylong, verylong)""" + ) From 90d46ed5a65e2cee7cc05556c6a427b05bc5a0b9 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 18 Apr 2020 15:56:15 -0700 Subject: [PATCH 0492/1439] 100% test coverage for wrap_modes, adding auto NOQA test case --- tests/test_wrap_modes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_wrap_modes.py b/tests/test_wrap_modes.py index fa35c5a86..31742b2a8 100644 --- a/tests/test_wrap_modes.py +++ b/tests/test_wrap_modes.py @@ -10,6 +10,7 @@ auto_pytest_magic(wrap_modes.vertical_grid_grouped, auto_allow_exceptions_=(ValueError,)) auto_pytest_magic(wrap_modes.vertical_grid_grouped_no_comma, auto_allow_exceptions_=(ValueError,)) auto_pytest_magic(wrap_modes.noqa, auto_allow_exceptions_=(ValueError,)) +auto_pytest_magic(wrap_modes.noqa, auto_allow_exceptions_=(ValueError,), comments=["NOQA"]) auto_pytest_magic( wrap_modes.vertical_prefix_from_module_import, auto_allow_exceptions_=(ValueError,) ) From 171da44884873c432f3cee576bf6cf0420a1ecf8 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 19 Apr 2020 13:00:59 -0700 Subject: [PATCH 0493/1439] Add some pragmatic no covs to setuptool command --- isort/setuptools_commands.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/setuptools_commands.py b/isort/setuptools_commands.py index 5a51a8623..1af8df8c6 100644 --- a/isort/setuptools_commands.py +++ b/isort/setuptools_commands.py @@ -56,8 +56,8 @@ def run(self) -> None: try: if not api.check_file(python_file, **arguments): - wrong_sorted_files = True + wrong_sorted_files = True # pragma: no cover except OSError as error: # pragma: no cover warn(f"Unable to parse file {python_file} due to {error}") if wrong_sorted_files: - sys.exit(1) + sys.exit(1) # pragma: no cover From b0d33b86ed383478ac23ed14ef6c1ad398ce243a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 20 Apr 2020 20:25:56 -0700 Subject: [PATCH 0494/1439] Add test for removing from imports --- tests/test_isort.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_isort.py b/tests/test_isort.py index 83290de23..ae9426ac9 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -836,6 +836,15 @@ def test_remove_imports() -> None: ) assert test_output == "import lib1\nimport lib5\n" + # From imports + test_input = "from x import y" + test_output = api.sort_code_string(test_input, remove_imports=["x"]) + assert test_output == "" + + test_input = "from x import y" + test_output = api.sort_code_string(test_input, remove_imports=["x.y"]) + assert test_output == "" + def test_explicitly_local_import() -> None: """Ensure that explicitly local imports are separated.""" From 7bbb5cbe281cba308cba521b05e42b83d65f7f8a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 21 Apr 2020 21:24:26 -0700 Subject: [PATCH 0495/1439] Add test for comments above --- tests/test_isort.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_isort.py b/tests/test_isort.py index ae9426ac9..efcac4aa0 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -846,6 +846,12 @@ def test_remove_imports() -> None: assert test_output == "" +def test_comments_above(): + """Test to ensure comments above an import will stay in place""" + test_input = "import os\n\nfrom x import y\n\n# comment\nfrom z import __version__, api\n" + assert api.sort_code_string(test_input, ensure_newline_before_comments=True) == test_input + + def test_explicitly_local_import() -> None: """Ensure that explicitly local imports are separated.""" test_input = "import lib1\nimport lib2\nimport .lib6\nfrom . import lib7" From e762d48fce82492b9c092653fc2f5cbf00197dcc Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 22 Apr 2020 21:34:48 -0700 Subject: [PATCH 0496/1439] Additional test for comments above import sections --- tests/test_isort.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_isort.py b/tests/test_isort.py index efcac4aa0..09d9c9e31 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -4917,6 +4917,7 @@ def test_python_future_category(): """ test_input = """from __future__ import absolute_import +# my future comment from future import standard_library standard_library.install_aliases() @@ -4935,6 +4936,7 @@ def test_python_future_category(): """ expected_output = """from __future__ import absolute_import +# my future comment from future import standard_library standard_library.install_aliases() From 6c1d7284f59f2842beaafb139c33802e471bfa66 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 23 Apr 2020 20:34:10 -0700 Subject: [PATCH 0497/1439] 100% code coverage for output.py --- isort/output.py | 2 +- tests/test_isort.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/isort/output.py b/isort/output.py index 1efbe4ed9..a20632288 100644 --- a/isort/output.py +++ b/isort/output.py @@ -159,7 +159,7 @@ def sorted_imports( pending_lines_before = pending_lines_before or not no_lines_before while output and output[-1].strip() == "": - output.pop() + output.pop() # pragma: no cover while output and output[0].strip() == "": output.pop(0) diff --git a/tests/test_isort.py b/tests/test_isort.py index 09d9c9e31..e4f3e439f 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -4981,3 +4981,17 @@ def test_python_future_category(): ) == expected_output ) + + +def test_combine_star_comments_above(): + input_text = """from __future__ import absolute_import + +# my future comment +from future import *, something +""" + expected_output = """from __future__ import absolute_import + +# my future comment +from future import * +""" + assert api.sort_code_string(input_text, combine_star=True) == expected_output From e451339092427a7c145d2d173c68e1c2b6d28372 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 24 Apr 2020 21:04:53 -0700 Subject: [PATCH 0498/1439] Ensure both forms of section comments are tested --- tests/test_isort.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_isort.py b/tests/test_isort.py index e4f3e439f..02966c12f 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -1706,7 +1706,7 @@ def test_place_comments() -> None: "\n" "# isort: imports-thirdparty\n" "# isort: imports-firstparty\n" - "# isort: imports-stdlib\n" + "# isort:imports-stdlib\n" "\n" ) expected_output = ( @@ -1716,7 +1716,7 @@ def test_place_comments() -> None: "# isort: imports-firstparty\n" "import myproject.test\n" "\n" - "# isort: imports-stdlib\n" + "# isort:imports-stdlib\n" "import os\n" "import sys\n" ) From 18a55badac8c42b63c2fcf46215095705ced6d1c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 25 Apr 2020 21:27:29 -0700 Subject: [PATCH 0499/1439] Pragramatically ignore coverage for environment specific lines --- isort/settings.py | 4 ++-- tests/test_isort.py | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/isort/settings.py b/isort/settings.py index 8df49461f..d25b99f55 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -35,7 +35,7 @@ import appdirs if appdirs.system == "darwin": - appdirs.system = "linux2" + appdirs.system = "linux2" # pragma: no cover except ImportError: appdirs = None @@ -164,7 +164,7 @@ class _Config: def __post_init__(self): py_version = self.py_version - if py_version == "auto": + if py_version == "auto": # pragma: no cover if sys.version_info.major == 2 and sys.version_info.minor <= 6: py_version = "2" elif sys.version_info.major == 3 and ( diff --git a/tests/test_isort.py b/tests/test_isort.py index 02966c12f..8f10e9bb5 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -4993,5 +4993,9 @@ def test_combine_star_comments_above(): # my future comment from future import * +from future import something """ - assert api.sort_code_string(input_text, combine_star=True) == expected_output + assert ( + api.sort_code_string(input_text, combine_star=False, ensure_newline_before_comments=True, force_single_line=True) + == expected_output + ) From eeba640d7d6343814303a838a671ed0c833f5b27 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 26 Apr 2020 00:43:18 -0700 Subject: [PATCH 0500/1439] Fix test case --- tests/test_isort.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/test_isort.py b/tests/test_isort.py index 8f10e9bb5..02966c12f 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -4993,9 +4993,5 @@ def test_combine_star_comments_above(): # my future comment from future import * -from future import something """ - assert ( - api.sort_code_string(input_text, combine_star=False, ensure_newline_before_comments=True, force_single_line=True) - == expected_output - ) + assert api.sort_code_string(input_text, combine_star=True) == expected_output From 9bce480b245e20f6c6ef93d34e92c27cea9f6d77 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 27 Apr 2020 23:56:32 -0700 Subject: [PATCH 0501/1439] Add initial settings test file --- tests/test_settings.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 tests/test_settings.py diff --git a/tests/test_settings.py b/tests/test_settings.py new file mode 100644 index 000000000..7fce9cfa5 --- /dev/null +++ b/tests/test_settings.py @@ -0,0 +1,17 @@ +import pytest + +from isort import exceptions +from isort.settings import Config + + +class TestConfig: + def test_init(self): + assert Config() + + def test_invalid_pyversion(self): + with pytest.raises(ValueError): + Config(py_version=10) + + def test_invalid_profile(self): + with pytest.raises(exceptions.ProfileDoesNotExist): + Config(profile="blackandwhitestylemixedwithpep8") From 3e656c34559a9dc09c85e66a2a9417f73ef8afed Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 28 Apr 2020 23:39:36 -0700 Subject: [PATCH 0502/1439] 100% hooks coverage --- tests/test_hooks.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/test_hooks.py b/tests/test_hooks.py index 639ac378f..49ba8b45b 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -1,10 +1,11 @@ +import os from io import BytesIO from unittest.mock import MagicMock, patch from isort import hooks -def test_git_hook(): +def test_git_hook(src_dir): """Simple smoke level testing of git hooks""" # Ensure correct subprocess command is called @@ -15,11 +16,13 @@ def test_git_hook(): ) # Test with incorrectly sorted file returned from git - with patch("isort.hooks.get_lines", MagicMock(return_value=["isort/isort.py"])) as run_mock: + with patch( + "isort.hooks.get_lines", MagicMock(return_value=[os.path.join(src_dir, "main.py")]) + ) as run_mock: - class FakeProecssResponse(object): + class FakeProcessResponse(object): stdout = b"import b\nimport a" - with patch("subprocess.run", MagicMock(return_value=FakeProecssResponse())) as run_mock: - with patch("isort.hooks.api", MagicMock()): + with patch("subprocess.run", MagicMock(return_value=FakeProcessResponse())) as run_mock: + with patch("isort.api", MagicMock(return_value=False)): hooks.git_hook(modify=True) From 61e628501992840e9a3f7155950007098510d8be Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 29 Apr 2020 18:45:17 -0700 Subject: [PATCH 0503/1439] Initial test for is_skipped --- tests/test_settings.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_settings.py b/tests/test_settings.py index 7fce9cfa5..be4232d13 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -1,3 +1,5 @@ +from pathlib import Path + import pytest from isort import exceptions @@ -15,3 +17,7 @@ def test_invalid_pyversion(self): def test_invalid_profile(self): with pytest.raises(exceptions.ProfileDoesNotExist): Config(profile="blackandwhitestylemixedwithpep8") + + def test_is_skipped(self): + assert Config().is_skipped(Path("C:\\path\\isort.py")) + assert Config(skip=["/path/isort.py"]).is_skipped(Path("C:\\path\\isort.py")) From 5617a9e8a3977584d4764662b44dcbbb6870e6bb Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 30 Apr 2020 21:56:12 -0700 Subject: [PATCH 0504/1439] Add initial test for main.sort_imports --- tests/test_main.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/test_main.py b/tests/test_main.py index f574a096d..e2f3e9300 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -6,7 +6,7 @@ from isort import main from isort._version import __version__ -from isort.settings import DEFAULT_CONFIG +from isort.settings import DEFAULT_CONFIG, Config auto_pytest_magic(main.sort_imports) @@ -17,6 +17,15 @@ def test_iter_source_code(tmpdir): assert tuple(main.iter_source_code((tmp_file,), DEFAULT_CONFIG, [])) == (tmp_file,) +def test_sort_imports(tmpdir): + tmp_file = tmpdir.join("file.py") + tmp_file.write("import os, sys\n") + assert main.sort_imports(str(tmp_file), DEFAULT_CONFIG, check=True).incorrectly_sorted + main.sort_imports(str(tmp_file), DEFAULT_CONFIG) + assert not main.sort_imports(str(tmp_file), DEFAULT_CONFIG, check=True).incorrectly_sorted + assert main.sort_imports(str(tmp_file), Config(skip=str(tmp_file)), check=True) + + def test_is_python_file(): assert main.is_python_file("file.py") assert main.is_python_file("file.pyi") From fc63effd219e3c0e88826c75dab20427a1a559f6 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 1 May 2020 23:52:37 -0700 Subject: [PATCH 0505/1439] skip fallback in coverage --- isort/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/settings.py b/isort/settings.py index d25b99f55..f86e23a40 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -71,7 +71,7 @@ appdirs.user_config_dir(".isort.cfg"), appdirs.user_config_dir(".editorconfig"), ) -else: +else: # pragma: no cover FALLBACK_CONFIGS = ("~/.isort.cfg", "~/.editorconfig") IMPORT_HEADING_PREFIX = "import_heading_" From 86368f475ec2fa7d8988f247264e54889ab11b8e Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 1 May 2020 23:56:29 -0700 Subject: [PATCH 0506/1439] Add test for settings._as_list --- tests/test_settings.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test_settings.py b/tests/test_settings.py index be4232d13..66d1b944c 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -2,10 +2,15 @@ import pytest -from isort import exceptions +from isort import exceptions, settings from isort.settings import Config +def test_as_list(): + assert settings._as_list([" one "]) == ["one"] + assert settings._as_list("one,two") == ["one", "two"] + + class TestConfig: def test_init(self): assert Config() From efd9b2a0670689db5bd13b62e784d2749833f000 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 2 May 2020 01:22:41 -0700 Subject: [PATCH 0507/1439] Full test coverage of settings module --- isort/settings.py | 4 +-- tests/test_settings.py | 67 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 64 insertions(+), 7 deletions(-) diff --git a/isort/settings.py b/isort/settings.py index f86e23a40..0e531627f 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -394,7 +394,7 @@ def _find_config(path: str) -> Dict[str, Any]: @lru_cache() -def _get_config_data(file_path: str, sections: Iterable[str]) -> Dict[str, Any]: +def _get_config_data(file_path: str, sections: Tuple[str]) -> Dict[str, Any]: settings: Dict[str, Any] = {} with open(file_path) as config_file: @@ -406,7 +406,7 @@ def _get_config_data(file_path: str, sections: Iterable[str]) -> Dict[str, Any]: for key in section.split("."): config_section = config_section.get(key, {}) settings.update(config_section) - else: + else: # pragma: no cover if "[tool.isort]" in config_file.read(): warnings.warn( f"Found {file_path} with [tool.isort] section, but toml package is not " diff --git a/tests/test_settings.py b/tests/test_settings.py index 66d1b944c..648da100d 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -1,4 +1,5 @@ from pathlib import Path +from unittest.mock import MagicMock, patch import pytest @@ -6,11 +7,6 @@ from isort.settings import Config -def test_as_list(): - assert settings._as_list([" one "]) == ["one"] - assert settings._as_list("one,two") == ["one", "two"] - - class TestConfig: def test_init(self): assert Config() @@ -26,3 +22,64 @@ def test_invalid_profile(self): def test_is_skipped(self): assert Config().is_skipped(Path("C:\\path\\isort.py")) assert Config(skip=["/path/isort.py"]).is_skipped(Path("C:\\path\\isort.py")) + + +def test_as_list(): + assert settings._as_list([" one "]) == ["one"] + assert settings._as_list("one,two") == ["one", "two"] + + +def test_find_config(tmpdir): + tmp_config = tmpdir.join(".isort.cfg") + + # can't find config if it has no relevant section + settings._find_config.cache_clear() + settings._get_config_data.cache_clear() + tmp_config.write_text( + """ +[section] +force_grid_wrap=true +""", + "utf8", + ) + assert not settings._find_config(str(tmpdir)) + + # or if it is malformed + settings._find_config.cache_clear() + settings._get_config_data.cache_clear() + tmp_config.write_text("""arstoyrsyan arienrsaeinrastyngpuywnlguyn354q^%$)(%_)@$""", "utf8") + assert not settings._find_config(str(tmpdir)) + + # can when it has either a file format, or generic relevant section + settings._find_config.cache_clear() + settings._get_config_data.cache_clear() + tmp_config.write_text( + """ +[isort] +force_grid_wrap=true +""", + "utf8", + ) + assert settings._find_config(str(tmpdir)) + + +def test_get_config_data(tmpdir): + test_config = tmpdir.join("test_config.editorconfig") + test_config.write_text( + """ +root = true + +[*.py] +indent_style=tab +indent_size=tab +force_grid_wrap=false +comment_prefix="text" +""", + "utf8", + ) + loaded_settings = settings._get_config_data(str(test_config), sections=("*.py",)) + assert loaded_settings + assert loaded_settings["comment_prefix"] == "text" + assert loaded_settings["force_grid_wrap"] == 0 + assert loaded_settings["indent"] == "\t" + assert str(tmpdir) in loaded_settings["source"] From 62ffabb43f550d4a860bff09016332343983b191 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 2 May 2020 02:04:24 -0700 Subject: [PATCH 0508/1439] Full test coverage for io module --- tests/test_io.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/tests/test_io.py b/tests/test_io.py index 6b6fa685d..5fc161084 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -11,7 +11,31 @@ class TestFile: def test_read(self, tmpdir): test_file_content = """# -*- encoding: ascii -*- -import ☺ +import Ὡ """ test_file = tmpdir.join("file.py") test_file.write(test_file_content) + with pytest.raises(Exception): + with io.read_file(str(test_file)) as file_handler: + file_handler.stream.read() + + def test_from_content(self, tmpdir): + test_file = tmpdir.join("file.py") + test_file.write_text("import os", "utf8") + assert io.File.from_contents("import os", filename=str(test_file)) + + def test_open(self, tmpdir): + with pytest.raises(Exception): + io.File._open("THISCANTBEAREALFILEὩὩὩὩὩὩὩὩὩὩὩὩ.ὩὩὩὩὩ") + + def raise_arbitrary_exception(*args, **kwargs): + raise RuntimeError("test") + + test_file = tmpdir.join("file.py") + test_file.write("import os") + assert io.File._open(str(test_file)) + + # correctly responds to error determining encoding + with patch("tokenize.detect_encoding", raise_arbitrary_exception): + with pytest.raises(Exception): + io.File._open(str(test_file)) From a8514d2aa4a889cdbb91040cd1a40814ac619a6c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 3 May 2020 02:51:44 -0700 Subject: [PATCH 0509/1439] Add testing for skip exceptions in main.sort_imports --- isort/main.py | 7 +++---- tests/test_main.py | 7 ++++++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/isort/main.py b/isort/main.py index 2e442e824..32031f99d 100644 --- a/isort/main.py +++ b/isort/main.py @@ -76,15 +76,14 @@ def sort_imports( check: bool = False, ask_to_apply: bool = False, write_to_stdout: bool = False, - **arguments: Any, + **kwargs: Any, ) -> Optional[SortAttempt]: - arguments.pop("settings_path", None) try: incorrectly_sorted: bool = False skipped: bool = False if check: try: - incorrectly_sorted = not api.check_file(file_name, config=config) + incorrectly_sorted = not api.check_file(file_name, config=config, **kwargs) except FileSkipped: skipped = True return SortAttempt(incorrectly_sorted, skipped) @@ -95,6 +94,7 @@ def sort_imports( config=config, ask_to_apply=ask_to_apply, write_to_stdout=write_to_stdout, + **kwargs, ) except FileSkipped: skipped = True @@ -647,7 +647,6 @@ def main(argv: Optional[Sequence[str]] = None) -> None: ask_to_apply=ask_to_apply, show_diff=show_diff, write_to_stdout=write_to_stdout, - **config_dict, ) for file_name in file_names ) diff --git a/tests/test_main.py b/tests/test_main.py index e2f3e9300..744db472e 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -23,7 +23,12 @@ def test_sort_imports(tmpdir): assert main.sort_imports(str(tmp_file), DEFAULT_CONFIG, check=True).incorrectly_sorted main.sort_imports(str(tmp_file), DEFAULT_CONFIG) assert not main.sort_imports(str(tmp_file), DEFAULT_CONFIG, check=True).incorrectly_sorted - assert main.sort_imports(str(tmp_file), Config(skip=str(tmp_file)), check=True) + + skip_config = Config(skip=[str(tmp_file)]) + assert main.sort_imports( + str(tmp_file), config=skip_config, check=True, disregard_skip=False + ).skipped + assert main.sort_imports(str(tmp_file), config=skip_config, disregard_skip=False).skipped def test_is_python_file(): From 9ff06415aade9f072816e2b7405d251b69a3b20d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 3 May 2020 03:07:40 -0700 Subject: [PATCH 0510/1439] Add initial test for main.parse_args --- tests/test_main.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_main.py b/tests/test_main.py index 744db472e..d874d6144 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -7,6 +7,7 @@ from isort import main from isort._version import __version__ from isort.settings import DEFAULT_CONFIG, Config +from isort.wrap_modes import WrapModes auto_pytest_magic(main.sort_imports) @@ -40,6 +41,12 @@ def test_is_python_file(): assert not main.is_python_file("file.pex") +def test_parse_args(): + assert main.parse_args([]) == {} + assert main.parse_args(["--multi-line", "1"]) == {"multi_line_output": WrapModes.VERTICAL} + assert main.parse_args(["--multi-line", "GRID"]) == {"multi_line_output": WrapModes.GRID} + + def test_ascii_art(capsys): main.main(["--version"]) out, error = capsys.readouterr() From ac95fc4f14aa3db67838268471763ceab60fd312 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 3 May 2020 03:10:11 -0700 Subject: [PATCH 0511/1439] Skip if __name__ == "__main__": --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 31b649c8e..0763398b8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -16,6 +16,7 @@ exclude_lines = except ImportError: if TYPE_CHECKING: raise NotImplementedError + if __name__ == "__main__": [paths] source = isort/ From 3e09facb2311f5b5b8bff8da90f4992e57f4bc9e Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 3 May 2020 12:01:11 -0700 Subject: [PATCH 0512/1439] Output config as JSON to make it easier to use from programatic scripts, add test to ensure desired behaviour --- isort/main.py | 13 +++++++++++-- tests/test_main.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/isort/main.py b/isort/main.py index 32031f99d..ed853704f 100644 --- a/isort/main.py +++ b/isort/main.py @@ -1,12 +1,12 @@ """Tool for sorting imports alphabetically, and automatically separated into sections.""" import argparse import functools +import json import os import re import stat import sys from pathlib import Path -from pprint import pprint from typing import Any, Dict, Iterable, Iterator, List, Optional, Sequence from warnings import warn @@ -603,7 +603,16 @@ def main(argv: Optional[Sequence[str]] = None) -> None: write_to_stdout = config_dict.pop("write_to_stdout", False) config = Config(**config_dict) if show_config: - pprint(config.__dict__) + + def _preconvert(item): + if isinstance(item, frozenset): + return list(item) + elif isinstance(item, WrapModes): + return item.name + else: + raise TypeError("Unserializable object {} of type {}".format(item, type(item))) + + print(json.dumps(config.__dict__, indent=4, separators=(",", ": "), default=_preconvert)) return wrong_sorted_files = False diff --git a/tests/test_main.py b/tests/test_main.py index d874d6144..b90359f0e 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,3 +1,4 @@ +import json import os import sys @@ -75,6 +76,35 @@ def test_is_python_file_fifo(tmpdir): assert not main.is_python_file(fifo_file) +def test_main(capsys, tmpdir): + base_args = ["--settings-path", str(tmpdir), "--virtual-env", str(tmpdir)] + + # If no files are passed in the quick guide is returned + main.main(base_args) + out, error = capsys.readouterr() + assert main.QUICK_GUIDE in out + assert not error + + # Unless the config is requested, in which case it will be returned alone as JSON + main.main(base_args + ["--show-config"]) + out, error = capsys.readouterr() + returned_config = json.loads(out) + assert returned_config + assert returned_config["virtual_env"] == str(tmpdir) + + # This should work even if settings path is non-existent or not provided + main.main(base_args[2:] + ["--show-config"]) + out, error = capsys.readouterr() + assert json.loads(out)["virtual_env"] == str(tmpdir) + main.main( + base_args[2:] + + ["--show-config"] + + ["--settings-path", "/random-root-folder-that-cant-exist-right?"] + ) + out, error = capsys.readouterr() + assert json.loads(out)["virtual_env"] == str(tmpdir) + + def test_isort_command(): """Ensure ISortCommand got registered, otherwise setuptools error must have occured""" assert main.ISortCommand From 5de8d50075f1dbe33a7e74172a6b6c27795a0c8d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 4 May 2020 20:44:39 -0700 Subject: [PATCH 0513/1439] Update test.main --- isort/main.py | 28 +++++++++++++++++----------- tests/test_main.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 11 deletions(-) diff --git a/isort/main.py b/isort/main.py index ed853704f..8d8429569 100644 --- a/isort/main.py +++ b/isort/main.py @@ -6,6 +6,7 @@ import re import stat import sys +from io import TextIOWrapper from pathlib import Path from typing import Any, Dict, Iterable, Iterator, List, Optional, Sequence from warnings import warn @@ -555,7 +556,17 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: return arguments -def main(argv: Optional[Sequence[str]] = None) -> None: +def _preconvert(item): + """Preconverts objects from native types into JSONifyiable types""" + if isinstance(item, frozenset): + return list(item) + elif isinstance(item, WrapModes): + return item.name + else: + raise TypeError("Unserializable object {} of type {}".format(item, type(item))) + + +def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = None) -> None: arguments = parse_args(argv) if arguments.get("show_version"): print(ASCII_ART) @@ -583,7 +594,11 @@ def main(argv: Optional[Sequence[str]] = None) -> None: print(QUICK_GUIDE) return elif file_names == ["-"] and not show_config: - api.sorted_imports(input_stream=sys.stdin, output_stream=sys.stdout, **arguments) + api.sorted_imports( + input_stream=sys.stdin if stdin is None else stdin, + output_stream=sys.stdout, + **arguments, + ) return if "settings_path" not in arguments: @@ -603,15 +618,6 @@ def main(argv: Optional[Sequence[str]] = None) -> None: write_to_stdout = config_dict.pop("write_to_stdout", False) config = Config(**config_dict) if show_config: - - def _preconvert(item): - if isinstance(item, frozenset): - return list(item) - elif isinstance(item, WrapModes): - return item.name - else: - raise TypeError("Unserializable object {} of type {}".format(item, type(item))) - print(json.dumps(config.__dict__, indent=4, separators=(",", ": "), default=_preconvert)) return diff --git a/tests/test_main.py b/tests/test_main.py index b90359f0e..e7b1bbe36 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,6 +1,7 @@ import json import os import sys +from io import BytesIO, TextIOWrapper import pytest from hypothesis_auto import auto_pytest_magic @@ -104,6 +105,44 @@ def test_main(capsys, tmpdir): out, error = capsys.readouterr() assert json.loads(out)["virtual_env"] == str(tmpdir) + # Should be able to set settings path to a file + config_file = tmpdir.join(".isort.cfg") + config_file.write( + """ +[settings] +profile=hug +verbose=true +""" + ) + config_args = ["--settings-path", str(config_file)] + main.main( + config_args + + ["--virtual-env", "/random-root-folder-that-cant-exist-right?"] + + ["--show-config"] + ) + out, error = capsys.readouterr() + assert json.loads(out)["profile"] == "hug" + + # Should be able to stream in content to sort + input_content = TextIOWrapper( + BytesIO( + b""" +import b +import a +""" + ) + ) + main.main(config_args + ["-"], stdin=input_content) + out, error = capsys.readouterr() + assert ( + out + == """else-type place_module for b returned FIRSTPARTY +else-type place_module for a returned FIRSTPARTY +import a +import b +""" + ) + def test_isort_command(): """Ensure ISortCommand got registered, otherwise setuptools error must have occured""" From 394738e2a21737c23fdc67be399691341ca422fa Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 4 May 2020 20:50:05 -0700 Subject: [PATCH 0514/1439] Add test for JSON preconvert step --- tests/test_main.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_main.py b/tests/test_main.py index e7b1bbe36..a480f1ecf 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,6 +1,7 @@ import json import os import sys +from datetime import datetime from io import BytesIO, TextIOWrapper import pytest @@ -70,6 +71,13 @@ def test_ascii_art(capsys): assert error == "" +def test_preconvert(): + assert main._preconvert(frozenset([1, 1, 2])) == [1, 2] + assert main._preconvert(WrapModes.GRID) == "GRID" + with pytest.raises(TypeError): + main._preconvert(datetime.now()) + + @pytest.mark.skipif(sys.platform == "win32", reason="cannot create fifo file on Windows platform") def test_is_python_file_fifo(tmpdir): fifo_file = os.path.join(tmpdir, "fifo_file") From e4c145c497028cb096eeab90b776447297d95a47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 5 May 2020 18:16:53 +0300 Subject: [PATCH 0515/1439] Add changelog project URL Background info at https://github.com/pypa/warehouse/pull/7882#issue-412444446 --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index b6d9f171c..6fd460cca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ classifiers = [ "Topic :: Software Development :: Libraries", "Topic :: Utilities", ] +urls = { Changelog = "https://github.com/timothycrosley/isort/blob/master/CHANGELOG.md" } [tool.poetry.dependencies] python = "^3.6" From d2490ab6dc2f314bbe9d7273a002cb9e08a3e456 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 5 May 2020 23:55:53 -0700 Subject: [PATCH 0516/1439] Increase main test coverage adding significantly more sub test cases --- isort/main.py | 3 +-- tests/test_main.py | 50 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/isort/main.py b/isort/main.py index 8d8429569..8e4628ea8 100644 --- a/isort/main.py +++ b/isort/main.py @@ -611,7 +611,6 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = config_dict = arguments.copy() ask_to_apply = config_dict.pop("ask_to_apply", False) jobs = config_dict.pop("jobs", ()) - show_logo = config_dict.pop("show_logo", False) filter_files = config_dict.pop("filter_files", False) check = config_dict.pop("check", False) show_diff = config_dict.pop("show_diff", False) @@ -635,7 +634,7 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = file_names = iter_source_code(file_names, config, skipped) num_skipped = 0 - if config.verbose or show_logo: + if config.verbose: print(ASCII_ART) if jobs: diff --git a/tests/test_main.py b/tests/test_main.py index a480f1ecf..f2ba65abc 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -151,6 +151,56 @@ def test_main(capsys, tmpdir): """ ) + # Should be able to run with just a file + python_file = tmpdir.join("has_imports.py") + python_file.write( + """ +import b +import a +""" + ) + main.main([str(python_file), "--filter-files", "--verbose"]) + assert ( + python_file.read() + == """import a +import b +""" + ) + + # Add a file to skip + should_skip = tmpdir.join("should_skip.py") + should_skip.write("import nothing") + main.main( + [ + str(python_file), + str(should_skip), + "--filter-files", + "--verbose", + "--skip", + str(should_skip), + ] + ) + + # Should raise a system exit if check only, with broken file + python_file.write( + """ +import b +import a +""" + ) + with pytest.raises(SystemExit): + main.main( + [ + str(python_file), + str(should_skip), + "--filter-files", + "--verbose", + "--check-only", + "--skip", + str(should_skip), + ] + ) + def test_isort_command(): """Ensure ISortCommand got registered, otherwise setuptools error must have occured""" From 8681d114dafd3a2ca8f9ddefdc72236c112c51df Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 5 May 2020 23:56:29 -0700 Subject: [PATCH 0517/1439] Allow --check to be used in addition to --check-only --- isort/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/isort/main.py b/isort/main.py index 8e4628ea8..c1c68bdd6 100644 --- a/isort/main.py +++ b/isort/main.py @@ -166,6 +166,7 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: parser.add_argument( "-c", "--check-only", + "--check", action="store_true", dest="check", help="Checks the file for unsorted / unformatted imports and prints them to the " From cf93f354610e26c47124e00ac1c4fd5b4e376ad2 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 6 May 2020 00:46:20 -0700 Subject: [PATCH 0518/1439] 100% test coverage for main.py --- isort/main.py | 4 ++-- tests/test_main.py | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/isort/main.py b/isort/main.py index c1c68bdd6..0c1a29c1f 100644 --- a/isort/main.py +++ b/isort/main.py @@ -668,12 +668,12 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = for sort_attempt in attempt_iterator: if not sort_attempt: - continue + continue # pragma: no cover - shouldn't happen, satisfies type constraint incorrectly_sorted = sort_attempt.incorrectly_sorted if arguments.get("check", False) and incorrectly_sorted: wrong_sorted_files = True if sort_attempt.skipped: - num_skipped += 1 + num_skipped += 1 # pragma: no cover - shouldn't happen, due to skip in iter_source_code if wrong_sorted_files: sys.exit(1) diff --git a/tests/test_main.py b/tests/test_main.py index f2ba65abc..ff0fe4dbf 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -201,6 +201,23 @@ def test_main(capsys, tmpdir): ] ) + # Should have same behavior if full directory is skipped + with pytest.raises(SystemExit): + main.main( + [str(tmpdir), "--filter-files", "--verbose", "--check-only", "--skip", str(should_skip)] + ) + + # Nested files should be skipped without needing --filter-files + nested_file = tmpdir.mkdir("nested_dir").join("skip.py") + nested_file.write("import b;import a") + python_file.write( + """ +import a +import b +""" + ) + main.main([str(tmpdir), "--skip", str(nested_file), "--check"]) + def test_isort_command(): """Ensure ISortCommand got registered, otherwise setuptools error must have occured""" From 8a44f151633152deaf8636956cce53498c8a5bb6 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 7 May 2020 23:45:58 -0700 Subject: [PATCH 0519/1439] Add additional tests; achieve 100% test coverage for parse module --- isort/parse.py | 3 ++- tests/test_isort.py | 18 ++++++++++++++++++ tests/test_parse.py | 13 +++++++++++-- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/isort/parse.py b/isort/parse.py index 8d8f56df3..c171525cc 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -405,9 +405,10 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte print(f"else-type place_module for {module} returned {placed_module}") if placed_module == "": warn( - f"could not place module {import_from} of line {line} --" + f"could not place module {module} of line {line} --" " Do you need to define a default section?" ) + imports.setdefault("", {"straight": OrderedDict(), "from": OrderedDict()}) straight_import |= imports[placed_module][type_of_import].get( # type: ignore module, False ) diff --git a/tests/test_isort.py b/tests/test_isort.py index 02966c12f..e3ce46108 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -2925,6 +2925,24 @@ def test_escaped_parens_sort() -> None: assert api.sort_code_string(test_input) == expected +def test_escaped_parens_sort_with_comment() -> None: + test_input = "from foo import \\ \n(a,\nb,# comment\nc)\n" + expected = "from foo import b # comment\nfrom foo import a, c\n" + assert api.sort_code_string(test_input) == expected + + +def test_escaped_parens_sort_with_first_comment() -> None: + test_input = "from foo import \\ \n(a,# comment\nb,\nc)\n" + expected = "from foo import a # comment\nfrom foo import b, c\n" + assert api.sort_code_string(test_input) == expected + + +def test_escaped_no_parens_sort_with_first_comment() -> None: + test_input = "from foo import a, \\\nb, \\\nc # comment\n" + expected = "from foo import c # comment\nfrom foo import a, b\n" + assert api.sort_code_string(test_input) == expected + + def test_is_python_file_ioerror(tmpdir) -> None: does_not_exist = tmpdir.join("fake.txt") assert is_python_file(str(does_not_exist)) is False diff --git a/tests/test_parse.py b/tests/test_parse.py index efc3a22a4..4948f9ce9 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -6,6 +6,15 @@ TEST_CONTENTS = """ import xyz import abc +import (\\ # one + one as \\ # two + three) +import \\ + zebra as \\ # one + not_bacon +from x import (\\ # one + one as \\ # two + three) def function(): @@ -28,11 +37,11 @@ def test_file_contents(): line_separator, sections, section_comments, - ) = parse.file_contents(TEST_CONTENTS, config=Config()) + ) = parse.file_contents(TEST_CONTENTS, config=Config(default_section="")) assert "\n".join(in_lines) == TEST_CONTENTS assert "import" not in "\n".join(out_lines) assert import_index == 1 - assert change_count == -2 + assert change_count == -11 assert original_line_count == len(in_lines) From 4f0e9508e40ec1f9da27dcfa622e19745a6a0e07 Mon Sep 17 00:00:00 2001 From: Jeremy Paige Date: Fri, 8 May 2020 11:12:14 -0700 Subject: [PATCH 0520/1439] speed up docker build times --- .dockerignore | 15 +++++++++++++++ Dockerfile | 9 ++++++--- docs/contributing/1.-contributing-guide.md | 5 ++++- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/.dockerignore b/.dockerignore index 3e4b57aa7..54671a6b0 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,17 @@ +# project build medadata that can't be used in docker +Dockerfile +.appveyor.yml +.dockerignore +.pre-commit-hooks.yaml +.travis.yml .git* + +# documentation not needed +CHANGELOG.md +LICENSE +MANIFEST.in +.undertake +example.gif +logo.png +art/ docs/ diff --git a/Dockerfile b/Dockerfile index bf4ae2bfc..ab01b3200 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,8 +3,11 @@ FROM python:$VERSION RUN mkdir /isort WORKDIR /isort -COPY . /isort -RUN python3 -m pip install poetry && poetry install +COPY pyproject.toml poetry.lock /isort/ +RUN python -m pip install --upgrade pip && python -m pip install poetry && poetry install -CMD /isort/scripts/done.sh +COPY . /isort/ +RUN poetry install + +CMD /isort/scripts/test.sh diff --git a/docs/contributing/1.-contributing-guide.md b/docs/contributing/1.-contributing-guide.md index 75727c38e..04b3bb9b4 100644 --- a/docs/contributing/1.-contributing-guide.md +++ b/docs/contributing/1.-contributing-guide.md @@ -46,7 +46,10 @@ A local test cycle might look like the following: 1. `docker build ./ -t isort:latest` 2. `docker run isort` -3. if #2 fails, debug, save, and goto #1; `docker run -it isort bash` will get you into the failed environment +3. if #2 fails, debug, save, and goto #1 + * `docker run -it isort bash` will get you into the failed environment + * `docker run -v $(git rev-parse --show-toplevel):/isort` will make changes made in the docker environment persist on your local checkout. + **TIP**: combine both to get an interacive docker shell that loads changes made locally, even after build, to quickly rerun that pesky failing test 4. `./scripts/docker.sh` 5. if #4 fails, debug, save and goto #1; you may need to specify a different `--build-arg VERSION=$VER` 6. congrats! you are probably ready to push a contribution From 5b38a697ee5ab2fa6876029e5810c2bad623c867 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 9 May 2020 16:04:22 -0700 Subject: [PATCH 0521/1439] Fix test failures by upgrading pip-shims version --- poetry.lock | 848 ++++++++++++++++++++++++++++++++++++++++++------- pyproject.toml | 2 +- 2 files changed, 731 insertions(+), 119 deletions(-) diff --git a/poetry.lock b/poetry.lock index 9ddbdd6c9..6960a04aa 100644 --- a/poetry.lock +++ b/poetry.lock @@ -42,6 +42,12 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "19.3.0" +[package.extras] +azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"] +dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"] +docs = ["sphinx", "zope.interface"] +tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] + [[package]] category = "dev" description = "Specifications for callback functions passed in to an API" @@ -90,6 +96,9 @@ attrs = ">=17.4.0" click = ">=6.5" toml = ">=0.9.4" +[package.extras] +d = ["aiohttp (>=3.3.2)"] + [[package]] category = "main" description = "A decorator for caching properties in classes." @@ -98,6 +107,17 @@ optional = false python-versions = "*" version = "1.5.1" +[[package]] +category = "main" +description = "Lightweight, extensible schema and data validation tool for Python dictionaries." +name = "cerberus" +optional = false +python-versions = ">=2.7" +version = "1.3.2" + +[package.dependencies] +setuptools = "*" + [[package]] category = "main" description = "Python package for providing Mozilla's CA Bundle." @@ -170,6 +190,9 @@ examples = ">=1.0,<2.0" gitpython = ">=3.0,<4.0" hug = ">=2.6,<3.0" +[package.extras] +pyproject = ["toml (>=0.10,<0.11)"] + [[package]] category = "dev" description = "A backport of the dataclasses module for Python 3.6" @@ -216,6 +239,9 @@ packaging = "*" pyyaml = "*" six = "*" +[package.extras] +pipenv = ["pipenv"] + [[package]] category = "dev" description = "Discover and load entry points from installed packages." @@ -349,6 +375,17 @@ version = "4.41.2" [package.dependencies] attrs = ">=19.2.0" +[package.extras] +all = ["django (>=1.11)", "dpcontracts (>=0.4)", "lark-parser (>=0.6.5)", "numpy (>=1.9.0)", "pandas (>=0.19)", "pytest (>=4.3)", "python-dateutil (>=1.4)", "pytz", "pytz (>=2014.1)"] +dateutil = ["python-dateutil (>=1.4)"] +django = ["pytz", "django (>=1.11)"] +dpcontracts = ["dpcontracts (>=0.4)"] +lark = ["lark-parser (>=0.6.5)"] +numpy = ["numpy (>=1.9.0)"] +pandas = ["pandas (>=0.19)"] +pytest = ["pytest (>=4.3)"] +pytz = ["pytz (>=2014.1)"] + [[package]] category = "dev" description = "Extends Hypothesis to add fully automatic testing of type annotated functions" @@ -361,6 +398,9 @@ version = "1.1.4" hypothesis = ">=4.36" pydantic = ">=0.32.2" +[package.extras] +pytest = ["pytest (>=4.0.0,<5.0.0)"] + [[package]] category = "main" description = "Internationalized Domain Names in Applications (IDNA)" @@ -380,6 +420,10 @@ version = "0.23" [package.dependencies] zipp = ">=0.5" +[package.extras] +docs = ["sphinx", "rst.linker"] +testing = ["packaging", "importlib-resources"] + [[package]] category = "dev" description = "IPython: Productive Interactive Computing" @@ -401,6 +445,17 @@ pygments = "*" setuptools = ">=18.5" traitlets = ">=4.2" +[package.extras] +all = ["nbconvert", "notebook", "ipywidgets", "testpath", "qtconsole", "ipyparallel", "nose (>=0.10.1)", "nbformat", "ipykernel", "pygments", "requests", "numpy", "Sphinx (>=1.3)"] +doc = ["Sphinx (>=1.3)"] +kernel = ["ipykernel"] +nbconvert = ["nbconvert"] +nbformat = ["nbformat"] +notebook = ["notebook", "ipywidgets"] +parallel = ["ipyparallel"] +qtconsole = ["qtconsole"] +test = ["nose (>=0.10.1)", "requests", "testpath", "pygments", "nbformat", "ipykernel", "numpy"] + [[package]] category = "dev" description = "Vestigial utilities from IPython" @@ -420,6 +475,9 @@ version = "0.15.1" [package.dependencies] parso = ">=0.5.0" +[package.extras] +testing = ["colorama", "docopt", "pytest (>=3.1.0,<5.0.0)"] + [[package]] category = "dev" description = "A very fast and expressive template engine." @@ -431,6 +489,9 @@ version = "2.10.3" [package.dependencies] MarkupSafe = ">=0.23" +[package.extras] +i18n = ["Babel (>=0.8)"] + [[package]] category = "dev" description = "Jinja2 Extension for Dates and Times" @@ -485,6 +546,9 @@ version = "3.1.1" [package.dependencies] setuptools = ">=36" +[package.extras] +testing = ["coverage", "pyyaml"] + [[package]] category = "dev" description = "Safely add untrusted strings to HTML/XML markup." @@ -565,6 +629,9 @@ mypy-extensions = ">=0.4.3,<0.5.0" typed-ast = ">=1.4.0,<1.5.0" typing-extensions = ">=3.7.4" +[package.extras] +dmypy = ["psutil (>=4.0)"] + [[package]] category = "dev" description = "Experimental type system extensions for programs checked with the mypy typechecker." @@ -612,6 +679,9 @@ optional = false python-versions = "*" version = "0.5.1" +[package.extras] +testing = ["docopt", "pytest (>=3.0.7)"] + [[package]] category = "dev" description = "Python Build Reasonableness" @@ -701,15 +771,20 @@ category = "main" description = "Compatibility shims for pip versions 8 thru current." name = "pip-shims" optional = false -python-versions = ">=2.6,!=3.0,!=3.1,!=3.2,!=3.3" -version = "0.3.3" +python-versions = "!=3.0,!=3.1,!=3.2,!=3.3,!=3.4,>=2.7" +version = "0.5.2" [package.dependencies] +packaging = "*" pip = "*" setuptools = "*" six = "*" wheel = "*" +[package.extras] +dev = ["pre-commit", "isort", "flake8", "rope", "invoke", "parver", "towncrier", "wheel", "mypy", "flake8-bugbear", "black"] +tests = ["pytest-timeout", "pytest (<5.0)", "pytest-xdist", "pytest-cov", "twine", "readme-renderer"] + [[package]] category = "dev" description = "" @@ -745,6 +820,14 @@ version = "0.2.2" six = "*" tomlkit = "*" +[package.dependencies.cerberus] +optional = true +version = "*" + +[package.extras] +tests = ["pytest", "pytest-xdist", "pytest-cov"] +validation = ["cerberus"] + [[package]] category = "dev" description = "plugin and hook calling mechanisms for python" @@ -758,6 +841,9 @@ version = "0.13.0" python = "<3.8" version = ">=0.12" +[package.extras] +dev = ["pre-commit", "tox"] + [[package]] category = "dev" description = "Your Project with Great Documentation" @@ -834,6 +920,11 @@ version = "0.32.2" python = "<3.7" version = ">=0.6" +[package.extras] +email = ["email-validator (>=1.0.3)"] +typing_extensions = ["typing-extensions (>=3.7.2)"] +ujson = ["ujson (>=1.35)"] + [[package]] category = "dev" description = "Python docstring style checker" @@ -917,6 +1008,9 @@ wcwidth = "*" python = "<3.8" version = ">=0.12" +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + [[package]] category = "dev" description = "Pytest plugin for measuring coverage." @@ -929,6 +1023,9 @@ version = "2.8.1" coverage = ">=4.4" pytest = ">=3.6" +[package.extras] +testing = ["fields", "hunter", "process-tests (2.0.2)", "six", "virtualenv"] + [[package]] category = "dev" description = "Thin-wrapper around the mock package for easier use with py.test" @@ -940,6 +1037,9 @@ version = "1.11.1" [package.dependencies] pytest = ">=2.7" +[package.extras] +dev = ["pre-commit", "tox"] + [[package]] category = "dev" description = "Extensions to the standard Python datetime module" @@ -973,6 +1073,10 @@ chardet = ">=3.0.2,<3.1.0" idna = ">=2.5,<2.9" urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" +[package.extras] +security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)"] +socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] + [[package]] category = "main" description = "A tool for converting between pip-style and pipfile requirements." @@ -991,13 +1095,21 @@ orderedmultidict = "*" packaging = ">=19.0" pep517 = ">=0.5.0" pip-shims = ">=0.3.2" -plette = "*" requests = "*" setuptools = ">=40.8" six = ">=1.11.0" tomlkit = ">=0.5.3" vistir = ">=0.3.1" +[package.dependencies.plette] +extras = ["validation"] +version = "*" + +[package.extras] +dev = ["vulture", "flake8", "rope", "isort", "invoke", "twine", "pre-commit", "lxml", "towncrier", "parver", "flake8-bugbear", "black"] +tests = ["pytest", "twine", "readme-renderer", "pytest-xdist", "pytest-cov", "pytest-timeout", "pytest-sugar", "coverage (<5.0a5)", "hypothesis"] +typing = ["mypy", "mypy-extensions", "mypytools", "pytype", "typed-ast", "monkeytype"] + [[package]] category = "dev" description = "Safety checks your installed dependencies for known security vulnerabilities." @@ -1086,6 +1198,9 @@ decorator = "*" ipython-genutils = "*" six = "*" +[package.extras] +test = ["pytest", "mock"] + [[package]] category = "dev" description = "a fork of Python 2 and 3 ast modules with type comment support" @@ -1121,6 +1236,11 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" version = "1.25.8" +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] + [[package]] category = "main" description = "Miscellaneous utilities for dealing with filesystems, paths, projects, subprocesses, and more." @@ -1134,6 +1254,12 @@ colorama = ">=0.3.4" requests = "*" six = "*" +[package.extras] +dev = ["pre-commit", "coverage (<5.0)", "isort", "flake8", "rope", "invoke", "parver", "sphinx", "sphinx-rtd-theme", "flake8-bugbear", "black"] +spinner = ["yaspin"] +tests = ["twine", "readme-renderer", "pytest", "pytest-xdist", "pytest-cov", "pytest-timeout", "hypothesis-fspaths", "hypothesis"] +typing = ["mypy", "mypy-extensions", "mypytools", "pytype", "typed-ast"] + [[package]] category = "dev" description = "Find dead code" @@ -1158,6 +1284,9 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "0.33.6" +[package.extras] +test = ["pytest (>=3.0.0)", "pytest-cov"] + [[package]] category = "dev" description = "This package provides cross-platform cross-python shutil.which functionality." @@ -1196,6 +1325,10 @@ version = "0.6.0" [package.dependencies] more-itertools = "*" +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] +testing = ["pathlib2", "contextlib2", "unittest2"] + [extras] pipfile = ["pipreqs", "tomlkit", "requirementslib"] pyproject = ["toml"] @@ -1203,120 +1336,599 @@ requirements = ["pipreqs", "pip-api"] xdg_home = ["appdirs"] [metadata] -content-hash = "3447cba840ff626794cdabd519349e9c13499276018784dc7739c57aae0356e5" +content-hash = "310311d313b5820d3cf1f6f8d446e14cbb7a1be118e1b0d5c1abaee1708bd412" python-versions = "^3.6" -[metadata.hashes] -appdirs = ["9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", "d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"] -appnope = ["5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0", "8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71"] -arrow = ["10257c5daba1a88db34afa284823382f4963feca7733b9107956bed041aff24f", "c2325911fcd79972cf493cfd957072f9644af8ad25456201ae1ede3316576eb4"] -atomicwrites = ["03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", "75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"] -attrs = ["08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", "f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"] -backcall = ["38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4", "bbbf4b1e5cd2bdb08f915895b51081c041bac22394fdfcfdfbe9f14b77c08bf2"] -bandit = ["336620e220cf2d3115877685e264477ff9d9abaeb0afe3dc7264f55fa17a3952", "41e75315853507aa145d62a78a2a6c5e3240fe14ee7c601459d0df9418196065"] -binaryornot = ["359501dfc9d40632edc9fac890e19542db1a287bbcfa58175b66658392018061", "b8b71173c917bddcd2c16070412e369c3ed7f0528926f70cac18a6c97fd563e4"] -black = ["817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739", "e030a9a28f542debc08acceb273f228ac422798e5215ba2a791a6ddeaaca22a5"] -cached-property = ["3a026f1a54135677e7da5ce819b0c690f156f37976f3e30c5430740725203d7f", "9217a59f14a5682da7c4b8829deadbfc194ac22e9908ccf7c8820234e80a1504"] -certifi = ["e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50", "fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef"] -chardet = ["84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"] -click = ["2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", "5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"] -colorama = ["05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", "f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"] -cookiecutter = ["1316a52e1c1f08db0c9efbf7d876dbc01463a74b155a0d83e722be88beda9a3e", "ed8f54a8fc79b6864020d773ce11539b5f08e4617f353de1f22d23226f6a0d36"] -coverage = ["08907593569fe59baca0bf152c43f3863201efb6113ecb38ce7e97ce339805a6", "0be0f1ed45fc0c185cfd4ecc19a1d6532d72f86a2bac9de7e24541febad72650", "141f08ed3c4b1847015e2cd62ec06d35e67a3ac185c26f7635f4406b90afa9c5", "19e4df788a0581238e9390c85a7a09af39c7b539b29f25c89209e6c3e371270d", "23cc09ed395b03424d1ae30dcc292615c1372bfba7141eb85e11e50efaa6b351", "245388cda02af78276b479f299bbf3783ef0a6a6273037d7c60dc73b8d8d7755", "331cb5115673a20fb131dadd22f5bcaf7677ef758741312bee4937d71a14b2ef", "386e2e4090f0bc5df274e720105c342263423e77ee8826002dcffe0c9533dbca", "3a794ce50daee01c74a494919d5ebdc23d58873747fa0e288318728533a3e1ca", "60851187677b24c6085248f0a0b9b98d49cba7ecc7ec60ba6b9d2e5574ac1ee9", "63a9a5fc43b58735f65ed63d2cf43508f462dc49857da70b8980ad78d41d52fc", "6b62544bb68106e3f00b21c8930e83e584fdca005d4fffd29bb39fb3ffa03cb5", "6ba744056423ef8d450cf627289166da65903885272055fb4b5e113137cfa14f", "7494b0b0274c5072bddbfd5b4a6c6f18fbbe1ab1d22a41e99cd2d00c8f96ecfe", "826f32b9547c8091679ff292a82aca9c7b9650f9fda3e2ca6bf2ac905b7ce888", "93715dffbcd0678057f947f496484e906bf9509f5c1c38fc9ba3922893cda5f5", "9a334d6c83dfeadae576b4d633a71620d40d1c379129d587faa42ee3e2a85cce", "af7ed8a8aa6957aac47b4268631fa1df984643f07ef00acd374e456364b373f5", "bf0a7aed7f5521c7ca67febd57db473af4762b9622254291fbcbb8cd0ba5e33e", "bf1ef9eb901113a9805287e090452c05547578eaab1b62e4ad456fcc049a9b7e", "c0afd27bc0e307a1ffc04ca5ec010a290e49e3afbe841c5cafc5c5a80ecd81c9", "dd579709a87092c6dbee09d1b7cfa81831040705ffa12a1b248935274aee0437", "df6712284b2e44a065097846488f66840445eb987eb81b3cc6e4149e7b6982e1", "e07d9f1a23e9e93ab5c62902833bf3e4b1f65502927379148b6622686223125c", "e2ede7c1d45e65e209d6093b762e98e8318ddeff95317d07a27a2140b80cfd24", "e4ef9c164eb55123c62411f5936b5c2e521b12356037b6e1c2617cef45523d47", "eca2b7343524e7ba246cab8ff00cab47a2d6d54ada3b02772e908a45675722e2", "eee64c616adeff7db37cc37da4180a3a5b6177f5c46b187894e633f088fb5b28", "ef824cad1f980d27f26166f86856efe11eff9912c4fed97d3804820d43fa550c", "efc89291bd5a08855829a3c522df16d856455297cf35ae827a37edac45f466a7", "fa964bae817babece5aa2e8c1af841bebb6d0b9add8e637548809d040443fee0", "ff37757e068ae606659c28c3bd0d923f9d29a85de79bf25b2b34b148473b5025"] -cruft = ["05a224047a5c705a1f8e51ea6cab5cc42570a1ee3cdad36eef5a1611c9f9876e", "3864a6775381a69b36720401a9db1714c20af51190c8378fcd5de4560aeb9282"] -dataclasses = ["454a69d788c7fda44efd71e259be79577822f5e3f53f029a22d08004e951dc9f", "6988bd2b895eef432d562370bb707d540f32f7360ab13da45340101bc2307d84"] -decorator = ["86156361c50488b84a3f148056ea716ca587df2f0de1d34750d35c21312725de", "f069f3a01830ca754ba5258fde2278454a0b5b79e0d7f5c13b3b97e57d4acff6"] -distlib = ["ecb3d0e4f71d0fa7f38db6bcc276c7c9a1c6638a516d726495934a553eb3fbe0"] -docopt = ["49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"] -dparse = ["00a5fdfa900629e5159bf3600d44905b333f4059a3366f28e0dbd13eeab17b19", "cef95156fa0adedaf042cd42f9990974bec76f25dfeca4dc01f381a243d5aa5b"] -entrypoints = ["589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", "c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"] -examples = ["9dba261e3929f3274328beeadcb2212180e12dbdc033eb0f35c3666708055fc4", "bf3c16a072e186815a78ebf85177bd1313cdaf34298baf0a8be30957147dd47d"] -falcon = ["18157af2a4fc3feedf2b5dcc6196f448639acf01c68bc33d4d5a04c3ef87f494", "24adcd2b29a8ffa9d552dc79638cd21736a3fb04eda7d102c6cebafdaadb88ad", "54f2cb4b687035b2a03206dbfc538055cc48b59a953187b0458aa1b574d47b53", "59d1e8c993b9a37ea06df9d72cf907a46cc8063b30717cdac2f34d1658b6f936", "733033ec80c896e30a43ab3e776856096836787197a44eb21022320a61311983", "74cf1d18207381c665b9e6292d65100ce146d958707793174b03869dc6e614f4", "95bf6ce986c1119aef12c9b348f4dee9c6dcc58391bdd0bc2b0bf353c2b15986", "9712975adcf8c6e12876239085ad757b8fdeba223d46d23daef82b47658f83a9", "a5ebb22a04c9cc65081938ee7651b4e3b4d2a28522ea8ec04c7bdd2b3e9e8cd8", "aa184895d1ad4573fbfaaf803563d02f019ebdf4790e41cc568a330607eae439", "e3782b7b92fefd46a6ad1fd8fe63fe6c6f1b7740a95ca56957f48d1aee34b357", "e9efa0791b5d9f9dd9689015ea6bce0a27fcd5ecbcd30e6d940bffa4f7f03389", "eea593cf466b9c126ce667f6d30503624ef24459f118c75594a69353b6c3d5fc", "f93351459f110b4c1ee28556aef9a791832df6f910bea7b3f616109d534df06b"] -first = ["8d8e46e115ea8ac652c76123c0865e3ff18372aef6f03c22809ceefcea9dec86", "ff285b08c55f8c97ce4ea7012743af2495c9f1291785f163722bd36f6af6d3bf"] -flake8 = ["19241c1cbc971b9962473e4438a2ca19749a7dd002dd1a946eaba171b4114548", "8e9dfa3cecb2400b3738a42c54c3043e821682b9c840b0448c0503f781130696"] -flake8-bugbear = ["d8c466ea79d5020cb20bf9f11cf349026e09517a42264f313d3f6fddb83e0571", "ded4d282778969b5ab5530ceba7aa1a9f1b86fa7618fc96a19a1d512331640f8"] -flake8-polyfill = ["12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9", "e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda"] -future = ["858e38522e8fd0d3ce8f0c1feaf0603358e366d5403209674c7b617fa0c24093"] -gitdb2 = ["1b6df1433567a51a4a9c1a5a0de977aa351a405cc56d7d35f3388bad1f630350", "96bbb507d765a7f51eb802554a9cfe194a174582f772e0d89f4e87288c288b7b"] -gitpython = ["631263cc670aa56ce3d3c414cf0fe2e840f2e913514b138ea28d88a477bbcd21", "6e97b9f0954807f30c2dd8e3165731ed6c477a1b365f194b69d81d7940a08332"] -htmlmin = ["50c1ef4630374a5d723900096a961cff426dff46b48f34d194a81bbe14eca178"] -hug = ["a04cd002614e8c788d58e5dcd5b932a62e2d2c33d4bd69fd8a3d7ce4e5cf5545", "bbe1390642e324130f60e4f7978bd6ea152d6957ed6e769ad6bc314ddaed5b9b"] -hypothesis = ["6847df3ffb4aa52798621dd007e6b61dbcf2d76c30ba37dc2699720e2c734b7a", "acd47600deb55e9c2c98de6deef23384160ed0fdaafb6753146e556c077d3c78"] -hypothesis-auto = ["5e2c2fb09dc09842512d80630bb792359a1d33d2c0473ad47ee23da0be9e32b1", "fea8560c4522c0fd490ed8cc17e420b95dabebb11b9b334c59bf2d768839015f"] -idna = ["c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", "ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"] -importlib-metadata = ["aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26", "d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af"] -ipython = ["c4ab005921641e40a68e405e286e7a1fcc464497e14d81b6914b4fd95e5dee9b", "dd76831f065f17bddd7eaa5c781f5ea32de5ef217592cf019e34043b56895aa1"] -ipython-genutils = ["72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8", "eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"] -jedi = ["786b6c3d80e2f06fd77162a07fed81b8baa22dde5d62896a790a331d6ac21a27", "ba859c74fa3c966a22f2aeebe1b74ee27e2a462f56d3f5f7ca4a59af61bfe42e"] -jinja2 = ["74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f", "9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"] -jinja2-time = ["d14eaa4d315e7688daa4969f616f226614350c48730bfa1692d2caebd8c90d40", "d3eab6605e3ec8b7a0863df09cc1d23714908fa61aa6986a845c20ba488b4efa"] -jsmin = ["b6df99b2cd1c75d9d342e4335b535789b8da9107ec748212706ef7bbe5c2553b"] -livereload = ["78d55f2c268a8823ba499305dcac64e28ddeb9a92571e12d543cd304faf5817b", "89254f78d7529d7ea0a3417d224c34287ebfe266b05e67e51facaf82c27f0f66"] -mako = ["a36919599a9b7dc5d86a7a8988f23a9a3a3d083070023bab23d64f7f1d1e0a4b"] -markdown = ["2e50876bcdd74517e7b71f3e7a76102050edec255b3983403f1a63e7c8a41e7a", "56a46ac655704b91e5b7e6326ce43d5ef72411376588afa1dd90e881b83c7e8c"] -markupsafe = ["00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", "09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", "09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", "1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", "24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", "29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", "43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", "46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", "500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", "535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", "62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", "6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", "717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", "79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", "7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", "88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", "8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", "98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", "9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", "9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", "ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", "b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", "b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", "b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", "ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", "c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", "cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", "e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"] -mccabe = ["ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", "dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"] -mkdocs = ["17d34329aad75d5de604b9ed4e31df3a4d235afefdc46ce7b1964fddb2e1e939", "8cc8b38325456b9e942c981a209eaeb1e9f3f77b493ad755bfef889b9c8d356a"] -mkdocs-material = ["a5246b550299d00a135a3f739e70ac6db73d7127480f0fecbda113d0095a674a", "e4a9ac73db7c65fdae1dbd248091e4b0a3f5db3e6bf87a46bb457db013a045e4"] -mkdocs-minify-plugin = ["3000a5069dd0f42f56a8aaf7fd5ea1222c67487949617e39585d6b6434b074b6", "d54fdd5be6843dd29fd7af2f7fdd20a9eb4db46f1f6bed914e03b2f58d2d488e"] -more-itertools = ["409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", "92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4"] -mypy = ["0a9a45157e532da06fe56adcfef8a74629566b607fa2c1ac0122d1ff995c748a", "2c35cae79ceb20d47facfad51f952df16c2ae9f45db6cb38405a3da1cf8fc0a7", "4b9365ade157794cef9685791032521233729cb00ce76b0ddc78749abea463d2", "53ea810ae3f83f9c9b452582261ea859828a9ed666f2e1ca840300b69322c474", "634aef60b4ff0f650d3e59d4374626ca6153fcaff96ec075b215b568e6ee3cb0", "7e396ce53cacd5596ff6d191b47ab0ea18f8e0ec04e15d69728d530e86d4c217", "7eadc91af8270455e0d73565b8964da1642fe226665dd5c9560067cd64d56749", "7f672d02fffcbace4db2b05369142e0506cdcde20cea0e07c7c2171c4fd11dd6", "85baab8d74ec601e86134afe2bcccd87820f79d2f8d5798c889507d1088287bf", "87c556fb85d709dacd4b4cb6167eecc5bbb4f0a9864b69136a0d4640fdc76a36", "a6bd44efee4dc8c3324c13785a9dc3519b3ee3a92cada42d2b57762b7053b49b", "c6d27bd20c3ba60d5b02f20bd28e20091d6286a699174dfad515636cb09b5a72", "e2bb577d10d09a2d8822a042a23b8d62bc3b269667c9eb8e60a6edfa000211b1", "f97a605d7c8bc2c6d1172c2f0d5a65b24142e11a58de689046e62c2d632ca8c1"] -mypy-extensions = ["090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", "2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"] -numpy = ["0b0dd8f47fb177d00fa6ef2d58783c4f41ad3126b139c91dd2f7c4b3fdf5e9a5", "25ffe71f96878e1da7e014467e19e7db90ae7d4e12affbc73101bcf61785214e", "26efd7f7d755e6ca966a5c0ac5a930a87dbbaab1c51716ac26a38f42ecc9bc4b", "28b1180c758abf34a5c3fea76fcee66a87def1656724c42bb14a6f9717a5bdf7", "2e418f0a59473dac424f888dd57e85f77502a593b207809211c76e5396ae4f5c", "30c84e3a62cfcb9e3066f25226e131451312a044f1fe2040e69ce792cb7de418", "4650d94bb9c947151737ee022b934b7d9a845a7c76e476f3e460f09a0c8c6f39", "4dd830a11e8724c9c9379feed1d1be43113f8bcce55f47ea7186d3946769ce26", "4f2a2b279efde194877aff1f76cf61c68e840db242a5c7169f1ff0fd59a2b1e2", "62d22566b3e3428dfc9ec972014c38ed9a4db4f8969c78f5414012ccd80a149e", "669795516d62f38845c7033679c648903200980d68935baaa17ac5c7ae03ae0c", "75fcd60d682db3e1f8fbe2b8b0c6761937ad56d01c1dc73edf4ef2748d5b6bc4", "9395b0a41e8b7e9a284e3be7060db9d14ad80273841c952c83a5afc241d2bd98", "9e37c35fc4e9410093b04a77d11a34c64bf658565e30df7cbe882056088a91c1", "a0678793096205a4d784bd99f32803ba8100f639cf3b932dc63b21621390ea7e", "b46554ad4dafb2927f88de5a1d207398c5385edbb5c84d30b3ef187c4a3894d8", "c867eeccd934920a800f65c6068acdd6b87e80d45cd8c8beefff783b23cdc462", "dd0667f5be56fb1b570154c2c0516a528e02d50da121bbbb2cbb0b6f87f59bc2", "de2b1c20494bdf47f0160bd88ed05f5e48ae5dc336b8de7cfade71abcc95c0b9", "f1df7b2b7740dd777571c732f98adb5aad5450aee32772f1b39249c8a50386f6", "ffca69e29079f7880c5392bf675eb8b4146479d976ae1924d01cd92b04cccbcc"] -orderedmultidict = ["04070bbb5e87291cc9bfa51df413677faf2141c73c61d2a5f7b26bea3cd882ad", "43c839a17ee3cdd62234c47deca1a8508a3f2ca1d0678a3bf791c87cf84adbf3"] -packaging = ["28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47", "d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108"] -parso = ["63854233e1fadb5da97f2744b6b24346d2750b85965e7e399bec1620232797dc", "666b0ee4a7a1220f65d367617f2cd3ffddff3e205f3f16a0284df30e774c2a9c"] -pbr = ["2c8e420cd4ed4cec4e7999ee47409e876af575d4c35a45840d59e8b5f3155ab8", "b32c8ccaac7b1a20c0ce00ce317642e6cf231cf038f9875e0280e28af5bf7ac9"] -pdocs = ["23a0346f56c08ab5701ca9b14630aa1f0f32f1c29ee7efcfc8bc512cd272f89b", "9c0d24fdc0e0c537be8f2418edb4f1075da46a0d749b17ea20b74e7e60124f49"] -pep517 = ["d283181fdb83fb698556cd3a4ebde5bb352b59242574e6ca56a95118774da374", "fac83aa4c3b73adc84cb2a295f1f5bd5b9a13946ebd1339ba3b33ce287165c88"] -pep562 = ["58cb1cc9ee63d93e62b4905a50357618d526d289919814bea1f0da8f53b79395", "d2a48b178ebf5f8dd31709cc26a19808ef794561fa2fe50ea01ea2bad4d667ef"] -pep8-naming = ["01cb1dab2f3ce9045133d08449f1b6b93531dceacb9ef04f67087c11c723cea9", "0ec891e59eea766efd3059c3d81f1da304d858220678bdc351aab73c533f2fbb"] -pexpect = ["2094eefdfcf37a1fdbfb9aa090862c1a4878e5c7e0e7e7088bdb511c558e5cd1", "9e2c1fd0e6ee3a49b28f95d4b33bc389c89b20af6a1255906e90ff1262ce62eb"] -pickleshare = ["87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca", "9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"] -pip-api = ["5266d9c8c9585e6fdeaf5d78f17b4e68dd2d657103fb24b80b629f70fac26712", "6eeb656fb1a4df791c40c16e22e6107025b6a1ee912e2dbc66d684bafc8ef57c"] -pip-shims = ["0162d846bd60c7b1feb4e1336541a3e661eb6a1eff4b9ea0d759780f8e0a2936", "d73372b9fa3a10e73f057fced70d39bb82df16a1ee3ded027fed0bb8d7c0ff97"] -pipfile = ["f7d9f15de8b660986557eb3cc5391aa1a16207ac41bc378d03f414762d36c984"] -pipreqs = ["2dfa21631cb68a97515e222d6f0033b5bfb75823567ca2195d528efb18c97990", "cec6eecc4685967b27eb386037565a737d036045f525b9eb314631a68d60e4bc"] -plette = ["c0e3553c1e581d8423daccbd825789c6e7f29b7d9e00e5331b12e1642a1a26d3", "dde5d525cf5f0cbad4d938c83b93db17887918daf63c13eafed257c4f61b07b4"] -pluggy = ["0db4b7601aae1d35b4a033282da476845aa19185c1e6964b25cf324b5e4ec3e6", "fa5fa1622fa6dd5c030e9cad086fa19ef6a0cf6d7a2d12318e10cb49d6d68f34"] -portray = ["28e0b21ad611cd460369c89cdfe67054e20354c6c5c03950c250edab6d7a99d7", "ac1a651f97ab04556732b64fdb10dcba4f9a006dd023471039743b16d28a7a61"] -poyo = ["3e2ca8e33fdc3c411cd101ca395668395dd5dc7ac775b8e809e3def9f9fe041a", "e26956aa780c45f011ca9886f044590e2d8fd8b61db7b1c1cf4e0869f48ed4dd"] -prompt-toolkit = ["46642344ce457641f28fc9d1c9ca939b63dadf8df128b86f1b9860e59c73a5e4", "e7f8af9e3d70f514373bf41aa51bc33af12a6db3f71461ea47fea985defb2c31", "f15af68f66e664eaa559d4ac8a928111eebd5feda0c11738b5998045224829db"] -ptyprocess = ["923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0", "d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"] -py = ["64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", "dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"] -pycodestyle = ["95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", "e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"] -pydantic = ["18598557f0d9ab46173045910ed50458c4fb4d16153c23346b504d7a5b679f77", "6a9335c968e13295430a208487e74d69fef40168b72dea8d975765d14e2da660", "6f5eb88fe4c21380aa064b7d249763fc6306f0b001d7e7d52d80866d1afc9ed3", "bc6c6a78647d7a65a493e1107572d993f26a652c49183201e3c7d23924bf7311", "e1a63b4e6bf8820833cb6fa239ffbe8eec57ccdd7d66359eff20e68a83c1deeb", "ede2d65ae33788d4e26e12b330b4a32c53cb14131c65bca3a59f037c73f6ee7a"] -pydocstyle = ["da7831660b7355307b32778c4a0dbfb137d89254ef31a2b2978f50fc0b4d7586", "f4f5d210610c2d153fae39093d44224c17429e2ad7da12a8b419aba5c2f614b5"] -pyflakes = ["17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", "d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"] -pygments = ["71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127", "881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297"] -pylama = ["9bae53ef9c1a431371d6a8dca406816a60d547147b60a4934721898f553b7d8f", "fd61c11872d6256b019ef1235be37b77c922ef37ac9797df6bd489996dddeb15"] -pymdown-extensions = ["24c1a0afbae101c4e2b2675ff4dd936470a90133f93398b9cbe0c855e2d2ec10", "960486dea995f1759dfd517aa140b3d851cd7b44d4c48d276fd2c74fc4e1bce9"] -pyparsing = ["6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80", "d9338df12903bbf5d65a0e4e87c2161968b10d2e489652bb47001d82a9b028b4"] -pytest = ["7e4800063ccfc306a53c461442526c5571e1462f61583506ce97e4da6a1d88c8", "ca563435f4941d0cb34767301c27bc65c510cb82e90b9ecf9cb52dc2c63caaa0"] -pytest-cov = ["cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b", "cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626"] -pytest-mock = ["34520283d459cdf1d0dbb58a132df804697f1b966ecedf808bbf3d255af8f659", "f1ab8aefe795204efe7a015900296d1719e7bf0f4a0558d71e8599da1d1309d0"] -python-dateutil = ["7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", "c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e"] -pyyaml = ["06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", "240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", "4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", "69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", "73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", "74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", "7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", "95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", "b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", "cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", "d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"] -requests = ["11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", "9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"] -requirementslib = ["50731ac1052473e4c7df59a44a1f3aa20f32e687110bc05d73c3b4109eebc23d", "8b594ab8b6280ee97cffd68fc766333345de150124d5b76061dd575c3a21fe5a"] -safety = ["0a3a8a178a9c96242b224f033ee8d1d130c0448b0e6622d12deaf37f6c3b4e59", "5059f3ffab3648330548ea9c7403405bbfaf085b11235770825d14c58f24cb78"] -six = ["3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", "d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"] -smmap2 = ["0555a7bf4df71d1ef4218e4807bbf9b201f910174e6e08af2e138d4e517b4dde", "29a9ffa0497e7f2be94ca0ed1ca1aa3cd4cf25a1f6b4f5f87f74b46ed91d609a"] -snowballstemmer = ["209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0", "df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"] -stevedore = ["01d9f4beecf0fbd070ddb18e5efb10567801ba7ef3ddab0074f54e3cd4e91730", "e0739f9739a681c7a1fda76a102b65295e96a144ccdb552f2ae03c5f0abe8a14"] -toml = ["229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", "235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", "f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"] -tomlkit = ["d6506342615d051bc961f70bfcfa3d29b6616cc08a3ddfd4bc24196f16fd4ec2", "f077456d35303e7908cc233b340f71e0bec96f63429997f38ca9272b7d64029e"] -tornado = ["349884248c36801afa19e342a77cc4458caca694b0eda633f5878e458a44cb2c", "398e0d35e086ba38a0427c3b37f4337327231942e731edaa6e9fd1865bbd6f60", "4e73ef678b1a859f0cb29e1d895526a20ea64b5ffd510a2307b5998c7df24281", "559bce3d31484b665259f50cd94c5c28b961b09315ccd838f284687245f416e5", "abbe53a39734ef4aba061fca54e30c6b4639d3e1f59653f0da37a0003de148c7", "c845db36ba616912074c5b1ee897f8e0124df269468f25e4fe21fe72f6edd7a9", "c9399267c926a4e7c418baa5cbe91c7d1cf362d505a1ef898fde44a07c9dd8a5"] -traitlets = ["70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44", "d023ee369ddd2763310e4c3eae1ff649689440d4ae59d7485eb4cfbbe3e359f7"] -typed-ast = ["1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161", "18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e", "262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e", "2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0", "354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c", "48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47", "4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631", "630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4", "66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34", "71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b", "7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2", "838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e", "95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a", "bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233", "cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1", "d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36", "d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d", "d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", "fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66", "ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12"] -typing = ["91dfe6f3f706ee8cc32d38edbbf304e9b7583fb37108fef38229617f8b3eba23", "c8cabb5ab8945cd2f54917be357d134db9cc1eb039e59d1606dc1e60cb1d9d36", "f38d83c5a7a7086543a0f649564d661859c5146a85775ab90c0d2f93ffaa9714"] -typing-extensions = ["2ed632b30bb54fc3941c382decfd0ee4148f5c591651c9272473fea2c6397d95", "b1edbbf0652660e32ae780ac9433f4231e7339c7f9a8057d0f042fcbcea49b87", "d8179012ec2c620d3791ca6fe2bf7979d979acdbef1fca0bc56b37411db682ed"] -urllib3 = ["2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc", "87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"] -vistir = ["2166e3148a67c438c9e3edbba0cde153d42dec6e3bf5d8f4624feb27686c0990", "3a0529b4b6c2e842fd19b5ceaa95b6c9201321314825c110406d4af3331a0709"] -vulture = ["20e73154efc9c6d2445663bc2f7fbf79b6c562f2b78cafc0ec0463b38610f42d", "a72834a8c9cf254f6ba0a64e9053b800e05df8391b1724702a19b00d7d849a43"] -wcwidth = ["3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", "f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c"] -wheel = ["10c9da68765315ed98850f8e048347c3eb06dd81822dc2ab1d4fde9dc9702646", "f4da1763d3becf2e2cd92a14a7c920f0f00eca30fdde9ea992c836685b9faf28"] -whichcraft = ["acdbb91b63d6a15efbd6430d1d7b2d36e44a71697e93e19b7ded477afd9fce87", "deda9266fbb22b8c64fd3ee45c050d61139cd87419765f588e37c8d23e236dd9"] -yarg = ["4f9cebdc00fac946c9bf2783d634e538a71c7d280a4d806d45fd4dc0ef441492", "55695bf4d1e3e7f756496c36a69ba32c40d18f821e38f61d028f6049e5e15911"] -yaspin = ["0ee4668936d0053de752c9a4963929faa3a832bd0ba823877d27855592dc80aa", "5a938bdc7bab353fd8942d0619d56c6b5159a80997dc1c387a479b39e6dc9391"] -zipp = ["3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", "f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335"] +[metadata.files] +appdirs = [ + {file = "appdirs-1.4.3-py2.py3-none-any.whl", hash = "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"}, + {file = "appdirs-1.4.3.tar.gz", hash = "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92"}, +] +appnope = [ + {file = "appnope-0.1.0-py2.py3-none-any.whl", hash = "sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0"}, + {file = "appnope-0.1.0.tar.gz", hash = "sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71"}, +] +arrow = [ + {file = "arrow-0.15.2-py2.py3-none-any.whl", hash = "sha256:c2325911fcd79972cf493cfd957072f9644af8ad25456201ae1ede3316576eb4"}, + {file = "arrow-0.15.2.tar.gz", hash = "sha256:10257c5daba1a88db34afa284823382f4963feca7733b9107956bed041aff24f"}, +] +atomicwrites = [ + {file = "atomicwrites-1.3.0-py2.py3-none-any.whl", hash = "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4"}, + {file = "atomicwrites-1.3.0.tar.gz", hash = "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"}, +] +attrs = [ + {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, + {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, +] +backcall = [ + {file = "backcall-0.1.0.tar.gz", hash = "sha256:38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4"}, + {file = "backcall-0.1.0.zip", hash = "sha256:bbbf4b1e5cd2bdb08f915895b51081c041bac22394fdfcfdfbe9f14b77c08bf2"}, +] +bandit = [ + {file = "bandit-1.6.2-py2.py3-none-any.whl", hash = "sha256:336620e220cf2d3115877685e264477ff9d9abaeb0afe3dc7264f55fa17a3952"}, + {file = "bandit-1.6.2.tar.gz", hash = "sha256:41e75315853507aa145d62a78a2a6c5e3240fe14ee7c601459d0df9418196065"}, +] +binaryornot = [ + {file = "binaryornot-0.4.4-py2.py3-none-any.whl", hash = "sha256:b8b71173c917bddcd2c16070412e369c3ed7f0528926f70cac18a6c97fd563e4"}, + {file = "binaryornot-0.4.4.tar.gz", hash = "sha256:359501dfc9d40632edc9fac890e19542db1a287bbcfa58175b66658392018061"}, +] +black = [ + {file = "black-18.9b0-py36-none-any.whl", hash = "sha256:817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739"}, + {file = "black-18.9b0.tar.gz", hash = "sha256:e030a9a28f542debc08acceb273f228ac422798e5215ba2a791a6ddeaaca22a5"}, +] +cached-property = [ + {file = "cached-property-1.5.1.tar.gz", hash = "sha256:9217a59f14a5682da7c4b8829deadbfc194ac22e9908ccf7c8820234e80a1504"}, + {file = "cached_property-1.5.1-py2.py3-none-any.whl", hash = "sha256:3a026f1a54135677e7da5ce819b0c690f156f37976f3e30c5430740725203d7f"}, +] +cerberus = [ + {file = "Cerberus-1.3.2.tar.gz", hash = "sha256:302e6694f206dd85cb63f13fd5025b31ab6d38c99c50c6d769f8fa0b0f299589"}, +] +certifi = [ + {file = "certifi-2019.9.11-py2.py3-none-any.whl", hash = "sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef"}, + {file = "certifi-2019.9.11.tar.gz", hash = "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50"}, +] +chardet = [ + {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, + {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, +] +click = [ + {file = "Click-7.0-py2.py3-none-any.whl", hash = "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13"}, + {file = "Click-7.0.tar.gz", hash = "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"}, +] +colorama = [ + {file = "colorama-0.4.1-py2.py3-none-any.whl", hash = "sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"}, + {file = "colorama-0.4.1.tar.gz", hash = "sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d"}, +] +cookiecutter = [ + {file = "cookiecutter-1.6.0-py2.py3-none-any.whl", hash = "sha256:ed8f54a8fc79b6864020d773ce11539b5f08e4617f353de1f22d23226f6a0d36"}, + {file = "cookiecutter-1.6.0.tar.gz", hash = "sha256:1316a52e1c1f08db0c9efbf7d876dbc01463a74b155a0d83e722be88beda9a3e"}, +] +coverage = [ + {file = "coverage-4.5.4-cp26-cp26m-macosx_10_12_x86_64.whl", hash = "sha256:eee64c616adeff7db37cc37da4180a3a5b6177f5c46b187894e633f088fb5b28"}, + {file = "coverage-4.5.4-cp27-cp27m-macosx_10_12_x86_64.whl", hash = "sha256:ef824cad1f980d27f26166f86856efe11eff9912c4fed97d3804820d43fa550c"}, + {file = "coverage-4.5.4-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:9a334d6c83dfeadae576b4d633a71620d40d1c379129d587faa42ee3e2a85cce"}, + {file = "coverage-4.5.4-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:7494b0b0274c5072bddbfd5b4a6c6f18fbbe1ab1d22a41e99cd2d00c8f96ecfe"}, + {file = "coverage-4.5.4-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:826f32b9547c8091679ff292a82aca9c7b9650f9fda3e2ca6bf2ac905b7ce888"}, + {file = "coverage-4.5.4-cp27-cp27m-win32.whl", hash = "sha256:63a9a5fc43b58735f65ed63d2cf43508f462dc49857da70b8980ad78d41d52fc"}, + {file = "coverage-4.5.4-cp27-cp27m-win_amd64.whl", hash = "sha256:e2ede7c1d45e65e209d6093b762e98e8318ddeff95317d07a27a2140b80cfd24"}, + {file = "coverage-4.5.4-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:dd579709a87092c6dbee09d1b7cfa81831040705ffa12a1b248935274aee0437"}, + {file = "coverage-4.5.4-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:08907593569fe59baca0bf152c43f3863201efb6113ecb38ce7e97ce339805a6"}, + {file = "coverage-4.5.4-cp33-cp33m-macosx_10_10_x86_64.whl", hash = "sha256:6b62544bb68106e3f00b21c8930e83e584fdca005d4fffd29bb39fb3ffa03cb5"}, + {file = "coverage-4.5.4-cp34-cp34m-macosx_10_12_x86_64.whl", hash = "sha256:331cb5115673a20fb131dadd22f5bcaf7677ef758741312bee4937d71a14b2ef"}, + {file = "coverage-4.5.4-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:bf1ef9eb901113a9805287e090452c05547578eaab1b62e4ad456fcc049a9b7e"}, + {file = "coverage-4.5.4-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:386e2e4090f0bc5df274e720105c342263423e77ee8826002dcffe0c9533dbca"}, + {file = "coverage-4.5.4-cp34-cp34m-win32.whl", hash = "sha256:fa964bae817babece5aa2e8c1af841bebb6d0b9add8e637548809d040443fee0"}, + {file = "coverage-4.5.4-cp34-cp34m-win_amd64.whl", hash = "sha256:df6712284b2e44a065097846488f66840445eb987eb81b3cc6e4149e7b6982e1"}, + {file = "coverage-4.5.4-cp35-cp35m-macosx_10_12_x86_64.whl", hash = "sha256:efc89291bd5a08855829a3c522df16d856455297cf35ae827a37edac45f466a7"}, + {file = "coverage-4.5.4-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:e4ef9c164eb55123c62411f5936b5c2e521b12356037b6e1c2617cef45523d47"}, + {file = "coverage-4.5.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:ff37757e068ae606659c28c3bd0d923f9d29a85de79bf25b2b34b148473b5025"}, + {file = "coverage-4.5.4-cp35-cp35m-win32.whl", hash = "sha256:bf0a7aed7f5521c7ca67febd57db473af4762b9622254291fbcbb8cd0ba5e33e"}, + {file = "coverage-4.5.4-cp35-cp35m-win_amd64.whl", hash = "sha256:19e4df788a0581238e9390c85a7a09af39c7b539b29f25c89209e6c3e371270d"}, + {file = "coverage-4.5.4-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:60851187677b24c6085248f0a0b9b98d49cba7ecc7ec60ba6b9d2e5574ac1ee9"}, + {file = "coverage-4.5.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:245388cda02af78276b479f299bbf3783ef0a6a6273037d7c60dc73b8d8d7755"}, + {file = "coverage-4.5.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:c0afd27bc0e307a1ffc04ca5ec010a290e49e3afbe841c5cafc5c5a80ecd81c9"}, + {file = "coverage-4.5.4-cp36-cp36m-win32.whl", hash = "sha256:6ba744056423ef8d450cf627289166da65903885272055fb4b5e113137cfa14f"}, + {file = "coverage-4.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:af7ed8a8aa6957aac47b4268631fa1df984643f07ef00acd374e456364b373f5"}, + {file = "coverage-4.5.4-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:3a794ce50daee01c74a494919d5ebdc23d58873747fa0e288318728533a3e1ca"}, + {file = "coverage-4.5.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0be0f1ed45fc0c185cfd4ecc19a1d6532d72f86a2bac9de7e24541febad72650"}, + {file = "coverage-4.5.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:eca2b7343524e7ba246cab8ff00cab47a2d6d54ada3b02772e908a45675722e2"}, + {file = "coverage-4.5.4-cp37-cp37m-win32.whl", hash = "sha256:93715dffbcd0678057f947f496484e906bf9509f5c1c38fc9ba3922893cda5f5"}, + {file = "coverage-4.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:23cc09ed395b03424d1ae30dcc292615c1372bfba7141eb85e11e50efaa6b351"}, + {file = "coverage-4.5.4-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:141f08ed3c4b1847015e2cd62ec06d35e67a3ac185c26f7635f4406b90afa9c5"}, + {file = "coverage-4.5.4.tar.gz", hash = "sha256:e07d9f1a23e9e93ab5c62902833bf3e4b1f65502927379148b6622686223125c"}, +] +cruft = [ + {file = "cruft-1.1.2-py3-none-any.whl", hash = "sha256:3864a6775381a69b36720401a9db1714c20af51190c8378fcd5de4560aeb9282"}, + {file = "cruft-1.1.2.tar.gz", hash = "sha256:05a224047a5c705a1f8e51ea6cab5cc42570a1ee3cdad36eef5a1611c9f9876e"}, +] +dataclasses = [ + {file = "dataclasses-0.6-py3-none-any.whl", hash = "sha256:454a69d788c7fda44efd71e259be79577822f5e3f53f029a22d08004e951dc9f"}, + {file = "dataclasses-0.6.tar.gz", hash = "sha256:6988bd2b895eef432d562370bb707d540f32f7360ab13da45340101bc2307d84"}, +] +decorator = [ + {file = "decorator-4.4.0-py2.py3-none-any.whl", hash = "sha256:f069f3a01830ca754ba5258fde2278454a0b5b79e0d7f5c13b3b97e57d4acff6"}, + {file = "decorator-4.4.0.tar.gz", hash = "sha256:86156361c50488b84a3f148056ea716ca587df2f0de1d34750d35c21312725de"}, +] +distlib = [ + {file = "distlib-0.2.9.post0.zip", hash = "sha256:ecb3d0e4f71d0fa7f38db6bcc276c7c9a1c6638a516d726495934a553eb3fbe0"}, +] +docopt = [ + {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, +] +dparse = [ + {file = "dparse-0.4.1-py2-none-any.whl", hash = "sha256:cef95156fa0adedaf042cd42f9990974bec76f25dfeca4dc01f381a243d5aa5b"}, + {file = "dparse-0.4.1.tar.gz", hash = "sha256:00a5fdfa900629e5159bf3600d44905b333f4059a3366f28e0dbd13eeab17b19"}, +] +entrypoints = [ + {file = "entrypoints-0.3-py2.py3-none-any.whl", hash = "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19"}, + {file = "entrypoints-0.3.tar.gz", hash = "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"}, +] +examples = [ + {file = "examples-1.0.1-py3-none-any.whl", hash = "sha256:bf3c16a072e186815a78ebf85177bd1313cdaf34298baf0a8be30957147dd47d"}, + {file = "examples-1.0.1.tar.gz", hash = "sha256:9dba261e3929f3274328beeadcb2212180e12dbdc033eb0f35c3666708055fc4"}, +] +falcon = [ + {file = "falcon-2.0.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:733033ec80c896e30a43ab3e776856096836787197a44eb21022320a61311983"}, + {file = "falcon-2.0.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f93351459f110b4c1ee28556aef9a791832df6f910bea7b3f616109d534df06b"}, + {file = "falcon-2.0.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:e9efa0791b5d9f9dd9689015ea6bce0a27fcd5ecbcd30e6d940bffa4f7f03389"}, + {file = "falcon-2.0.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:59d1e8c993b9a37ea06df9d72cf907a46cc8063b30717cdac2f34d1658b6f936"}, + {file = "falcon-2.0.0-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:a5ebb22a04c9cc65081938ee7651b4e3b4d2a28522ea8ec04c7bdd2b3e9e8cd8"}, + {file = "falcon-2.0.0-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:95bf6ce986c1119aef12c9b348f4dee9c6dcc58391bdd0bc2b0bf353c2b15986"}, + {file = "falcon-2.0.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:aa184895d1ad4573fbfaaf803563d02f019ebdf4790e41cc568a330607eae439"}, + {file = "falcon-2.0.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:74cf1d18207381c665b9e6292d65100ce146d958707793174b03869dc6e614f4"}, + {file = "falcon-2.0.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24adcd2b29a8ffa9d552dc79638cd21736a3fb04eda7d102c6cebafdaadb88ad"}, + {file = "falcon-2.0.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:18157af2a4fc3feedf2b5dcc6196f448639acf01c68bc33d4d5a04c3ef87f494"}, + {file = "falcon-2.0.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:e3782b7b92fefd46a6ad1fd8fe63fe6c6f1b7740a95ca56957f48d1aee34b357"}, + {file = "falcon-2.0.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:9712975adcf8c6e12876239085ad757b8fdeba223d46d23daef82b47658f83a9"}, + {file = "falcon-2.0.0-py2.py3-none-any.whl", hash = "sha256:54f2cb4b687035b2a03206dbfc538055cc48b59a953187b0458aa1b574d47b53"}, + {file = "falcon-2.0.0.tar.gz", hash = "sha256:eea593cf466b9c126ce667f6d30503624ef24459f118c75594a69353b6c3d5fc"}, +] +first = [ + {file = "first-2.0.2-py2.py3-none-any.whl", hash = "sha256:8d8e46e115ea8ac652c76123c0865e3ff18372aef6f03c22809ceefcea9dec86"}, + {file = "first-2.0.2.tar.gz", hash = "sha256:ff285b08c55f8c97ce4ea7012743af2495c9f1291785f163722bd36f6af6d3bf"}, +] +flake8 = [ + {file = "flake8-3.7.8-py2.py3-none-any.whl", hash = "sha256:8e9dfa3cecb2400b3738a42c54c3043e821682b9c840b0448c0503f781130696"}, + {file = "flake8-3.7.8.tar.gz", hash = "sha256:19241c1cbc971b9962473e4438a2ca19749a7dd002dd1a946eaba171b4114548"}, +] +flake8-bugbear = [ + {file = "flake8-bugbear-19.8.0.tar.gz", hash = "sha256:d8c466ea79d5020cb20bf9f11cf349026e09517a42264f313d3f6fddb83e0571"}, + {file = "flake8_bugbear-19.8.0-py35.py36.py37-none-any.whl", hash = "sha256:ded4d282778969b5ab5530ceba7aa1a9f1b86fa7618fc96a19a1d512331640f8"}, +] +flake8-polyfill = [ + {file = "flake8-polyfill-1.0.2.tar.gz", hash = "sha256:e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda"}, + {file = "flake8_polyfill-1.0.2-py2.py3-none-any.whl", hash = "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9"}, +] +future = [ + {file = "future-0.18.1.tar.gz", hash = "sha256:858e38522e8fd0d3ce8f0c1feaf0603358e366d5403209674c7b617fa0c24093"}, +] +gitdb2 = [ + {file = "gitdb2-2.0.6-py2.py3-none-any.whl", hash = "sha256:96bbb507d765a7f51eb802554a9cfe194a174582f772e0d89f4e87288c288b7b"}, + {file = "gitdb2-2.0.6.tar.gz", hash = "sha256:1b6df1433567a51a4a9c1a5a0de977aa351a405cc56d7d35f3388bad1f630350"}, +] +gitpython = [ + {file = "GitPython-3.0.3-py3-none-any.whl", hash = "sha256:6e97b9f0954807f30c2dd8e3165731ed6c477a1b365f194b69d81d7940a08332"}, + {file = "GitPython-3.0.3.tar.gz", hash = "sha256:631263cc670aa56ce3d3c414cf0fe2e840f2e913514b138ea28d88a477bbcd21"}, +] +htmlmin = [ + {file = "htmlmin-0.1.12.tar.gz", hash = "sha256:50c1ef4630374a5d723900096a961cff426dff46b48f34d194a81bbe14eca178"}, +] +hug = [ + {file = "hug-2.6.0-py2.py3-none-any.whl", hash = "sha256:bbe1390642e324130f60e4f7978bd6ea152d6957ed6e769ad6bc314ddaed5b9b"}, + {file = "hug-2.6.0.tar.gz", hash = "sha256:a04cd002614e8c788d58e5dcd5b932a62e2d2c33d4bd69fd8a3d7ce4e5cf5545"}, +] +hypothesis = [ + {file = "hypothesis-4.41.2-py3-none-any.whl", hash = "sha256:acd47600deb55e9c2c98de6deef23384160ed0fdaafb6753146e556c077d3c78"}, + {file = "hypothesis-4.41.2.tar.gz", hash = "sha256:6847df3ffb4aa52798621dd007e6b61dbcf2d76c30ba37dc2699720e2c734b7a"}, +] +hypothesis-auto = [ + {file = "hypothesis-auto-1.1.4.tar.gz", hash = "sha256:5e2c2fb09dc09842512d80630bb792359a1d33d2c0473ad47ee23da0be9e32b1"}, + {file = "hypothesis_auto-1.1.4-py3-none-any.whl", hash = "sha256:fea8560c4522c0fd490ed8cc17e420b95dabebb11b9b334c59bf2d768839015f"}, +] +idna = [ + {file = "idna-2.8-py2.py3-none-any.whl", hash = "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"}, + {file = "idna-2.8.tar.gz", hash = "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407"}, +] +importlib-metadata = [ + {file = "importlib_metadata-0.23-py2.py3-none-any.whl", hash = "sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af"}, + {file = "importlib_metadata-0.23.tar.gz", hash = "sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26"}, +] +ipython = [ + {file = "ipython-7.8.0-py3-none-any.whl", hash = "sha256:c4ab005921641e40a68e405e286e7a1fcc464497e14d81b6914b4fd95e5dee9b"}, + {file = "ipython-7.8.0.tar.gz", hash = "sha256:dd76831f065f17bddd7eaa5c781f5ea32de5ef217592cf019e34043b56895aa1"}, +] +ipython-genutils = [ + {file = "ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8"}, + {file = "ipython_genutils-0.2.0.tar.gz", hash = "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"}, +] +jedi = [ + {file = "jedi-0.15.1-py2.py3-none-any.whl", hash = "sha256:786b6c3d80e2f06fd77162a07fed81b8baa22dde5d62896a790a331d6ac21a27"}, + {file = "jedi-0.15.1.tar.gz", hash = "sha256:ba859c74fa3c966a22f2aeebe1b74ee27e2a462f56d3f5f7ca4a59af61bfe42e"}, +] +jinja2 = [ + {file = "Jinja2-2.10.3-py2.py3-none-any.whl", hash = "sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f"}, + {file = "Jinja2-2.10.3.tar.gz", hash = "sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"}, +] +jinja2-time = [ + {file = "jinja2-time-0.2.0.tar.gz", hash = "sha256:d14eaa4d315e7688daa4969f616f226614350c48730bfa1692d2caebd8c90d40"}, + {file = "jinja2_time-0.2.0-py2.py3-none-any.whl", hash = "sha256:d3eab6605e3ec8b7a0863df09cc1d23714908fa61aa6986a845c20ba488b4efa"}, +] +jsmin = [ + {file = "jsmin-2.2.2.tar.gz", hash = "sha256:b6df99b2cd1c75d9d342e4335b535789b8da9107ec748212706ef7bbe5c2553b"}, +] +livereload = [ + {file = "livereload-2.6.1-py2.py3-none-any.whl", hash = "sha256:78d55f2c268a8823ba499305dcac64e28ddeb9a92571e12d543cd304faf5817b"}, + {file = "livereload-2.6.1.tar.gz", hash = "sha256:89254f78d7529d7ea0a3417d224c34287ebfe266b05e67e51facaf82c27f0f66"}, +] +mako = [ + {file = "Mako-1.1.0.tar.gz", hash = "sha256:a36919599a9b7dc5d86a7a8988f23a9a3a3d083070023bab23d64f7f1d1e0a4b"}, +] +markdown = [ + {file = "Markdown-3.1.1-py2.py3-none-any.whl", hash = "sha256:56a46ac655704b91e5b7e6326ce43d5ef72411376588afa1dd90e881b83c7e8c"}, + {file = "Markdown-3.1.1.tar.gz", hash = "sha256:2e50876bcdd74517e7b71f3e7a76102050edec255b3983403f1a63e7c8a41e7a"}, +] +markupsafe = [ + {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, + {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +mkdocs = [ + {file = "mkdocs-1.0.4-py2.py3-none-any.whl", hash = "sha256:8cc8b38325456b9e942c981a209eaeb1e9f3f77b493ad755bfef889b9c8d356a"}, + {file = "mkdocs-1.0.4.tar.gz", hash = "sha256:17d34329aad75d5de604b9ed4e31df3a4d235afefdc46ce7b1964fddb2e1e939"}, +] +mkdocs-material = [ + {file = "mkdocs-material-4.4.3.tar.gz", hash = "sha256:e4a9ac73db7c65fdae1dbd248091e4b0a3f5db3e6bf87a46bb457db013a045e4"}, + {file = "mkdocs_material-4.4.3-py2.py3-none-any.whl", hash = "sha256:a5246b550299d00a135a3f739e70ac6db73d7127480f0fecbda113d0095a674a"}, +] +mkdocs-minify-plugin = [ + {file = "mkdocs-minify-plugin-0.2.1.tar.gz", hash = "sha256:3000a5069dd0f42f56a8aaf7fd5ea1222c67487949617e39585d6b6434b074b6"}, + {file = "mkdocs_minify_plugin-0.2.1-py2-none-any.whl", hash = "sha256:d54fdd5be6843dd29fd7af2f7fdd20a9eb4db46f1f6bed914e03b2f58d2d488e"}, +] +more-itertools = [ + {file = "more-itertools-7.2.0.tar.gz", hash = "sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832"}, + {file = "more_itertools-7.2.0-py3-none-any.whl", hash = "sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4"}, +] +mypy = [ + {file = "mypy-0.761-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:7f672d02fffcbace4db2b05369142e0506cdcde20cea0e07c7c2171c4fd11dd6"}, + {file = "mypy-0.761-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:87c556fb85d709dacd4b4cb6167eecc5bbb4f0a9864b69136a0d4640fdc76a36"}, + {file = "mypy-0.761-cp35-cp35m-win_amd64.whl", hash = "sha256:c6d27bd20c3ba60d5b02f20bd28e20091d6286a699174dfad515636cb09b5a72"}, + {file = "mypy-0.761-cp36-cp36m-macosx_10_6_x86_64.whl", hash = "sha256:4b9365ade157794cef9685791032521233729cb00ce76b0ddc78749abea463d2"}, + {file = "mypy-0.761-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:634aef60b4ff0f650d3e59d4374626ca6153fcaff96ec075b215b568e6ee3cb0"}, + {file = "mypy-0.761-cp36-cp36m-win_amd64.whl", hash = "sha256:53ea810ae3f83f9c9b452582261ea859828a9ed666f2e1ca840300b69322c474"}, + {file = "mypy-0.761-cp37-cp37m-macosx_10_6_x86_64.whl", hash = "sha256:0a9a45157e532da06fe56adcfef8a74629566b607fa2c1ac0122d1ff995c748a"}, + {file = "mypy-0.761-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:7eadc91af8270455e0d73565b8964da1642fe226665dd5c9560067cd64d56749"}, + {file = "mypy-0.761-cp37-cp37m-win_amd64.whl", hash = "sha256:e2bb577d10d09a2d8822a042a23b8d62bc3b269667c9eb8e60a6edfa000211b1"}, + {file = "mypy-0.761-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c35cae79ceb20d47facfad51f952df16c2ae9f45db6cb38405a3da1cf8fc0a7"}, + {file = "mypy-0.761-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:f97a605d7c8bc2c6d1172c2f0d5a65b24142e11a58de689046e62c2d632ca8c1"}, + {file = "mypy-0.761-cp38-cp38-win_amd64.whl", hash = "sha256:a6bd44efee4dc8c3324c13785a9dc3519b3ee3a92cada42d2b57762b7053b49b"}, + {file = "mypy-0.761-py3-none-any.whl", hash = "sha256:7e396ce53cacd5596ff6d191b47ab0ea18f8e0ec04e15d69728d530e86d4c217"}, + {file = "mypy-0.761.tar.gz", hash = "sha256:85baab8d74ec601e86134afe2bcccd87820f79d2f8d5798c889507d1088287bf"}, +] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] +numpy = [ + {file = "numpy-1.17.3-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:4dd830a11e8724c9c9379feed1d1be43113f8bcce55f47ea7186d3946769ce26"}, + {file = "numpy-1.17.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:30c84e3a62cfcb9e3066f25226e131451312a044f1fe2040e69ce792cb7de418"}, + {file = "numpy-1.17.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9395b0a41e8b7e9a284e3be7060db9d14ad80273841c952c83a5afc241d2bd98"}, + {file = "numpy-1.17.3-cp35-cp35m-win32.whl", hash = "sha256:9e37c35fc4e9410093b04a77d11a34c64bf658565e30df7cbe882056088a91c1"}, + {file = "numpy-1.17.3-cp35-cp35m-win_amd64.whl", hash = "sha256:de2b1c20494bdf47f0160bd88ed05f5e48ae5dc336b8de7cfade71abcc95c0b9"}, + {file = "numpy-1.17.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:669795516d62f38845c7033679c648903200980d68935baaa17ac5c7ae03ae0c"}, + {file = "numpy-1.17.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:4650d94bb9c947151737ee022b934b7d9a845a7c76e476f3e460f09a0c8c6f39"}, + {file = "numpy-1.17.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4f2a2b279efde194877aff1f76cf61c68e840db242a5c7169f1ff0fd59a2b1e2"}, + {file = "numpy-1.17.3-cp36-cp36m-win32.whl", hash = "sha256:ffca69e29079f7880c5392bf675eb8b4146479d976ae1924d01cd92b04cccbcc"}, + {file = "numpy-1.17.3-cp36-cp36m-win_amd64.whl", hash = "sha256:2e418f0a59473dac424f888dd57e85f77502a593b207809211c76e5396ae4f5c"}, + {file = "numpy-1.17.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:75fcd60d682db3e1f8fbe2b8b0c6761937ad56d01c1dc73edf4ef2748d5b6bc4"}, + {file = "numpy-1.17.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:28b1180c758abf34a5c3fea76fcee66a87def1656724c42bb14a6f9717a5bdf7"}, + {file = "numpy-1.17.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:dd0667f5be56fb1b570154c2c0516a528e02d50da121bbbb2cbb0b6f87f59bc2"}, + {file = "numpy-1.17.3-cp37-cp37m-win32.whl", hash = "sha256:25ffe71f96878e1da7e014467e19e7db90ae7d4e12affbc73101bcf61785214e"}, + {file = "numpy-1.17.3-cp37-cp37m-win_amd64.whl", hash = "sha256:0b0dd8f47fb177d00fa6ef2d58783c4f41ad3126b139c91dd2f7c4b3fdf5e9a5"}, + {file = "numpy-1.17.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:62d22566b3e3428dfc9ec972014c38ed9a4db4f8969c78f5414012ccd80a149e"}, + {file = "numpy-1.17.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:26efd7f7d755e6ca966a5c0ac5a930a87dbbaab1c51716ac26a38f42ecc9bc4b"}, + {file = "numpy-1.17.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:b46554ad4dafb2927f88de5a1d207398c5385edbb5c84d30b3ef187c4a3894d8"}, + {file = "numpy-1.17.3-cp38-cp38-win32.whl", hash = "sha256:c867eeccd934920a800f65c6068acdd6b87e80d45cd8c8beefff783b23cdc462"}, + {file = "numpy-1.17.3-cp38-cp38-win_amd64.whl", hash = "sha256:f1df7b2b7740dd777571c732f98adb5aad5450aee32772f1b39249c8a50386f6"}, + {file = "numpy-1.17.3.zip", hash = "sha256:a0678793096205a4d784bd99f32803ba8100f639cf3b932dc63b21621390ea7e"}, +] +orderedmultidict = [ + {file = "orderedmultidict-1.0.1-py2.py3-none-any.whl", hash = "sha256:43c839a17ee3cdd62234c47deca1a8508a3f2ca1d0678a3bf791c87cf84adbf3"}, + {file = "orderedmultidict-1.0.1.tar.gz", hash = "sha256:04070bbb5e87291cc9bfa51df413677faf2141c73c61d2a5f7b26bea3cd882ad"}, +] +packaging = [ + {file = "packaging-19.2-py2.py3-none-any.whl", hash = "sha256:d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108"}, + {file = "packaging-19.2.tar.gz", hash = "sha256:28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47"}, +] +parso = [ + {file = "parso-0.5.1-py2.py3-none-any.whl", hash = "sha256:63854233e1fadb5da97f2744b6b24346d2750b85965e7e399bec1620232797dc"}, + {file = "parso-0.5.1.tar.gz", hash = "sha256:666b0ee4a7a1220f65d367617f2cd3ffddff3e205f3f16a0284df30e774c2a9c"}, +] +pbr = [ + {file = "pbr-5.4.3-py2.py3-none-any.whl", hash = "sha256:b32c8ccaac7b1a20c0ce00ce317642e6cf231cf038f9875e0280e28af5bf7ac9"}, + {file = "pbr-5.4.3.tar.gz", hash = "sha256:2c8e420cd4ed4cec4e7999ee47409e876af575d4c35a45840d59e8b5f3155ab8"}, +] +pdocs = [ + {file = "pdocs-1.0.1-py3-none-any.whl", hash = "sha256:9c0d24fdc0e0c537be8f2418edb4f1075da46a0d749b17ea20b74e7e60124f49"}, + {file = "pdocs-1.0.1.tar.gz", hash = "sha256:23a0346f56c08ab5701ca9b14630aa1f0f32f1c29ee7efcfc8bc512cd272f89b"}, +] +pep517 = [ + {file = "pep517-0.7.0-py2.py3-none-any.whl", hash = "sha256:fac83aa4c3b73adc84cb2a295f1f5bd5b9a13946ebd1339ba3b33ce287165c88"}, + {file = "pep517-0.7.0.tar.gz", hash = "sha256:d283181fdb83fb698556cd3a4ebde5bb352b59242574e6ca56a95118774da374"}, +] +pep562 = [ + {file = "pep562-1.0-py2.py3-none-any.whl", hash = "sha256:d2a48b178ebf5f8dd31709cc26a19808ef794561fa2fe50ea01ea2bad4d667ef"}, + {file = "pep562-1.0.tar.gz", hash = "sha256:58cb1cc9ee63d93e62b4905a50357618d526d289919814bea1f0da8f53b79395"}, +] +pep8-naming = [ + {file = "pep8-naming-0.8.2.tar.gz", hash = "sha256:01cb1dab2f3ce9045133d08449f1b6b93531dceacb9ef04f67087c11c723cea9"}, + {file = "pep8_naming-0.8.2-py2.py3-none-any.whl", hash = "sha256:0ec891e59eea766efd3059c3d81f1da304d858220678bdc351aab73c533f2fbb"}, +] +pexpect = [ + {file = "pexpect-4.7.0-py2.py3-none-any.whl", hash = "sha256:2094eefdfcf37a1fdbfb9aa090862c1a4878e5c7e0e7e7088bdb511c558e5cd1"}, + {file = "pexpect-4.7.0.tar.gz", hash = "sha256:9e2c1fd0e6ee3a49b28f95d4b33bc389c89b20af6a1255906e90ff1262ce62eb"}, +] +pickleshare = [ + {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, + {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, +] +pip-api = [ + {file = "pip-api-0.0.12.tar.gz", hash = "sha256:6eeb656fb1a4df791c40c16e22e6107025b6a1ee912e2dbc66d684bafc8ef57c"}, + {file = "pip_api-0.0.12-py3-none-any.whl", hash = "sha256:5266d9c8c9585e6fdeaf5d78f17b4e68dd2d657103fb24b80b629f70fac26712"}, +] +pip-shims = [ + {file = "pip_shims-0.5.2-py2.py3-none-any.whl", hash = "sha256:39193b8c4aa5e4cb82e250be58df4d5eaebe931a33b0df43b369f4ae92ee5753"}, + {file = "pip_shims-0.5.2.tar.gz", hash = "sha256:423978c27d0e24e8ecb3e82b4a6c1f607e2e364153e73d0803c671d48b23195e"}, +] +pipfile = [ + {file = "pipfile-0.0.2.tar.gz", hash = "sha256:f7d9f15de8b660986557eb3cc5391aa1a16207ac41bc378d03f414762d36c984"}, +] +pipreqs = [ + {file = "pipreqs-0.4.9-py2.py3-none-any.whl", hash = "sha256:2dfa21631cb68a97515e222d6f0033b5bfb75823567ca2195d528efb18c97990"}, + {file = "pipreqs-0.4.9.tar.gz", hash = "sha256:cec6eecc4685967b27eb386037565a737d036045f525b9eb314631a68d60e4bc"}, +] +plette = [ + {file = "plette-0.2.2-py2.py3-none-any.whl", hash = "sha256:c0e3553c1e581d8423daccbd825789c6e7f29b7d9e00e5331b12e1642a1a26d3"}, + {file = "plette-0.2.2.tar.gz", hash = "sha256:dde5d525cf5f0cbad4d938c83b93db17887918daf63c13eafed257c4f61b07b4"}, +] +pluggy = [ + {file = "pluggy-0.13.0-py2.py3-none-any.whl", hash = "sha256:0db4b7601aae1d35b4a033282da476845aa19185c1e6964b25cf324b5e4ec3e6"}, + {file = "pluggy-0.13.0.tar.gz", hash = "sha256:fa5fa1622fa6dd5c030e9cad086fa19ef6a0cf6d7a2d12318e10cb49d6d68f34"}, +] +portray = [ + {file = "portray-1.3.1-py3-none-any.whl", hash = "sha256:ac1a651f97ab04556732b64fdb10dcba4f9a006dd023471039743b16d28a7a61"}, + {file = "portray-1.3.1.tar.gz", hash = "sha256:28e0b21ad611cd460369c89cdfe67054e20354c6c5c03950c250edab6d7a99d7"}, +] +poyo = [ + {file = "poyo-0.5.0-py2.py3-none-any.whl", hash = "sha256:3e2ca8e33fdc3c411cd101ca395668395dd5dc7ac775b8e809e3def9f9fe041a"}, + {file = "poyo-0.5.0.tar.gz", hash = "sha256:e26956aa780c45f011ca9886f044590e2d8fd8b61db7b1c1cf4e0869f48ed4dd"}, +] +prompt-toolkit = [ + {file = "prompt_toolkit-2.0.10-py2-none-any.whl", hash = "sha256:e7f8af9e3d70f514373bf41aa51bc33af12a6db3f71461ea47fea985defb2c31"}, + {file = "prompt_toolkit-2.0.10-py3-none-any.whl", hash = "sha256:46642344ce457641f28fc9d1c9ca939b63dadf8df128b86f1b9860e59c73a5e4"}, + {file = "prompt_toolkit-2.0.10.tar.gz", hash = "sha256:f15af68f66e664eaa559d4ac8a928111eebd5feda0c11738b5998045224829db"}, +] +ptyprocess = [ + {file = "ptyprocess-0.6.0-py2.py3-none-any.whl", hash = "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"}, + {file = "ptyprocess-0.6.0.tar.gz", hash = "sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0"}, +] +py = [ + {file = "py-1.8.0-py2.py3-none-any.whl", hash = "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa"}, + {file = "py-1.8.0.tar.gz", hash = "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"}, +] +pycodestyle = [ + {file = "pycodestyle-2.5.0-py2.py3-none-any.whl", hash = "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56"}, + {file = "pycodestyle-2.5.0.tar.gz", hash = "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"}, +] +pydantic = [ + {file = "pydantic-0.32.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:18598557f0d9ab46173045910ed50458c4fb4d16153c23346b504d7a5b679f77"}, + {file = "pydantic-0.32.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ede2d65ae33788d4e26e12b330b4a32c53cb14131c65bca3a59f037c73f6ee7a"}, + {file = "pydantic-0.32.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:bc6c6a78647d7a65a493e1107572d993f26a652c49183201e3c7d23924bf7311"}, + {file = "pydantic-0.32.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:6f5eb88fe4c21380aa064b7d249763fc6306f0b001d7e7d52d80866d1afc9ed3"}, + {file = "pydantic-0.32.2-py36.py37.py38-none-any.whl", hash = "sha256:e1a63b4e6bf8820833cb6fa239ffbe8eec57ccdd7d66359eff20e68a83c1deeb"}, + {file = "pydantic-0.32.2.tar.gz", hash = "sha256:6a9335c968e13295430a208487e74d69fef40168b72dea8d975765d14e2da660"}, +] +pydocstyle = [ + {file = "pydocstyle-5.0.2-py3-none-any.whl", hash = "sha256:da7831660b7355307b32778c4a0dbfb137d89254ef31a2b2978f50fc0b4d7586"}, + {file = "pydocstyle-5.0.2.tar.gz", hash = "sha256:f4f5d210610c2d153fae39093d44224c17429e2ad7da12a8b419aba5c2f614b5"}, +] +pyflakes = [ + {file = "pyflakes-2.1.1-py2.py3-none-any.whl", hash = "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0"}, + {file = "pyflakes-2.1.1.tar.gz", hash = "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"}, +] +pygments = [ + {file = "Pygments-2.4.2-py2.py3-none-any.whl", hash = "sha256:71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127"}, + {file = "Pygments-2.4.2.tar.gz", hash = "sha256:881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297"}, +] +pylama = [ + {file = "pylama-7.7.1-py2.py3-none-any.whl", hash = "sha256:fd61c11872d6256b019ef1235be37b77c922ef37ac9797df6bd489996dddeb15"}, + {file = "pylama-7.7.1.tar.gz", hash = "sha256:9bae53ef9c1a431371d6a8dca406816a60d547147b60a4934721898f553b7d8f"}, +] +pymdown-extensions = [ + {file = "pymdown-extensions-6.1.tar.gz", hash = "sha256:960486dea995f1759dfd517aa140b3d851cd7b44d4c48d276fd2c74fc4e1bce9"}, + {file = "pymdown_extensions-6.1-py2.py3-none-any.whl", hash = "sha256:24c1a0afbae101c4e2b2675ff4dd936470a90133f93398b9cbe0c855e2d2ec10"}, +] +pyparsing = [ + {file = "pyparsing-2.4.2-py2.py3-none-any.whl", hash = "sha256:d9338df12903bbf5d65a0e4e87c2161968b10d2e489652bb47001d82a9b028b4"}, + {file = "pyparsing-2.4.2.tar.gz", hash = "sha256:6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80"}, +] +pytest = [ + {file = "pytest-5.2.1-py3-none-any.whl", hash = "sha256:7e4800063ccfc306a53c461442526c5571e1462f61583506ce97e4da6a1d88c8"}, + {file = "pytest-5.2.1.tar.gz", hash = "sha256:ca563435f4941d0cb34767301c27bc65c510cb82e90b9ecf9cb52dc2c63caaa0"}, +] +pytest-cov = [ + {file = "pytest-cov-2.8.1.tar.gz", hash = "sha256:cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b"}, + {file = "pytest_cov-2.8.1-py2.py3-none-any.whl", hash = "sha256:cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626"}, +] +pytest-mock = [ + {file = "pytest-mock-1.11.1.tar.gz", hash = "sha256:f1ab8aefe795204efe7a015900296d1719e7bf0f4a0558d71e8599da1d1309d0"}, + {file = "pytest_mock-1.11.1-py2.py3-none-any.whl", hash = "sha256:34520283d459cdf1d0dbb58a132df804697f1b966ecedf808bbf3d255af8f659"}, +] +python-dateutil = [ + {file = "python-dateutil-2.8.0.tar.gz", hash = "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e"}, + {file = "python_dateutil-2.8.0-py2.py3-none-any.whl", hash = "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb"}, +] +pyyaml = [ + {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, + {file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"}, + {file = "PyYAML-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2"}, + {file = "PyYAML-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c"}, + {file = "PyYAML-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2"}, + {file = "PyYAML-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648"}, + {file = "PyYAML-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"}, + {file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"}, + {file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"}, + {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"}, + {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, +] +requests = [ + {file = "requests-2.22.0-py2.py3-none-any.whl", hash = "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"}, + {file = "requests-2.22.0.tar.gz", hash = "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4"}, +] +requirementslib = [ + {file = "requirementslib-1.5.3-py2.py3-none-any.whl", hash = "sha256:8b594ab8b6280ee97cffd68fc766333345de150124d5b76061dd575c3a21fe5a"}, + {file = "requirementslib-1.5.3.tar.gz", hash = "sha256:50731ac1052473e4c7df59a44a1f3aa20f32e687110bc05d73c3b4109eebc23d"}, +] +safety = [ + {file = "safety-1.8.5-py2.py3-none-any.whl", hash = "sha256:0a3a8a178a9c96242b224f033ee8d1d130c0448b0e6622d12deaf37f6c3b4e59"}, + {file = "safety-1.8.5.tar.gz", hash = "sha256:5059f3ffab3648330548ea9c7403405bbfaf085b11235770825d14c58f24cb78"}, +] +six = [ + {file = "six-1.12.0-py2.py3-none-any.whl", hash = "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c"}, + {file = "six-1.12.0.tar.gz", hash = "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"}, +] +smmap2 = [ + {file = "smmap2-2.0.5-py2.py3-none-any.whl", hash = "sha256:0555a7bf4df71d1ef4218e4807bbf9b201f910174e6e08af2e138d4e517b4dde"}, + {file = "smmap2-2.0.5.tar.gz", hash = "sha256:29a9ffa0497e7f2be94ca0ed1ca1aa3cd4cf25a1f6b4f5f87f74b46ed91d609a"}, +] +snowballstemmer = [ + {file = "snowballstemmer-2.0.0-py2.py3-none-any.whl", hash = "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0"}, + {file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"}, +] +stevedore = [ + {file = "stevedore-1.31.0-py2.py3-none-any.whl", hash = "sha256:01d9f4beecf0fbd070ddb18e5efb10567801ba7ef3ddab0074f54e3cd4e91730"}, + {file = "stevedore-1.31.0.tar.gz", hash = "sha256:e0739f9739a681c7a1fda76a102b65295e96a144ccdb552f2ae03c5f0abe8a14"}, +] +toml = [ + {file = "toml-0.10.0-py2.7.egg", hash = "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"}, + {file = "toml-0.10.0-py2.py3-none-any.whl", hash = "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"}, + {file = "toml-0.10.0.tar.gz", hash = "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c"}, +] +tomlkit = [ + {file = "tomlkit-0.5.3-py2.py3-none-any.whl", hash = "sha256:f077456d35303e7908cc233b340f71e0bec96f63429997f38ca9272b7d64029e"}, + {file = "tomlkit-0.5.3.tar.gz", hash = "sha256:d6506342615d051bc961f70bfcfa3d29b6616cc08a3ddfd4bc24196f16fd4ec2"}, +] +tornado = [ + {file = "tornado-6.0.3-cp35-cp35m-win32.whl", hash = "sha256:c9399267c926a4e7c418baa5cbe91c7d1cf362d505a1ef898fde44a07c9dd8a5"}, + {file = "tornado-6.0.3-cp35-cp35m-win_amd64.whl", hash = "sha256:398e0d35e086ba38a0427c3b37f4337327231942e731edaa6e9fd1865bbd6f60"}, + {file = "tornado-6.0.3-cp36-cp36m-win32.whl", hash = "sha256:4e73ef678b1a859f0cb29e1d895526a20ea64b5ffd510a2307b5998c7df24281"}, + {file = "tornado-6.0.3-cp36-cp36m-win_amd64.whl", hash = "sha256:349884248c36801afa19e342a77cc4458caca694b0eda633f5878e458a44cb2c"}, + {file = "tornado-6.0.3-cp37-cp37m-win32.whl", hash = "sha256:559bce3d31484b665259f50cd94c5c28b961b09315ccd838f284687245f416e5"}, + {file = "tornado-6.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:abbe53a39734ef4aba061fca54e30c6b4639d3e1f59653f0da37a0003de148c7"}, + {file = "tornado-6.0.3.tar.gz", hash = "sha256:c845db36ba616912074c5b1ee897f8e0124df269468f25e4fe21fe72f6edd7a9"}, +] +traitlets = [ + {file = "traitlets-4.3.3-py2.py3-none-any.whl", hash = "sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44"}, + {file = "traitlets-4.3.3.tar.gz", hash = "sha256:d023ee369ddd2763310e4c3eae1ff649689440d4ae59d7485eb4cfbbe3e359f7"}, +] +typed-ast = [ + {file = "typed_ast-1.4.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e"}, + {file = "typed_ast-1.4.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b"}, + {file = "typed_ast-1.4.0-cp35-cp35m-win32.whl", hash = "sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4"}, + {file = "typed_ast-1.4.0-cp35-cp35m-win_amd64.whl", hash = "sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12"}, + {file = "typed_ast-1.4.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631"}, + {file = "typed_ast-1.4.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233"}, + {file = "typed_ast-1.4.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1"}, + {file = "typed_ast-1.4.0-cp36-cp36m-win32.whl", hash = "sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a"}, + {file = "typed_ast-1.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c"}, + {file = "typed_ast-1.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a"}, + {file = "typed_ast-1.4.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e"}, + {file = "typed_ast-1.4.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d"}, + {file = "typed_ast-1.4.0-cp37-cp37m-win32.whl", hash = "sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36"}, + {file = "typed_ast-1.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0"}, + {file = "typed_ast-1.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66"}, + {file = "typed_ast-1.4.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2"}, + {file = "typed_ast-1.4.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47"}, + {file = "typed_ast-1.4.0-cp38-cp38-win32.whl", hash = "sha256:1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161"}, + {file = "typed_ast-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e"}, + {file = "typed_ast-1.4.0.tar.gz", hash = "sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34"}, +] +typing = [ + {file = "typing-3.7.4.1-py2-none-any.whl", hash = "sha256:c8cabb5ab8945cd2f54917be357d134db9cc1eb039e59d1606dc1e60cb1d9d36"}, + {file = "typing-3.7.4.1-py3-none-any.whl", hash = "sha256:f38d83c5a7a7086543a0f649564d661859c5146a85775ab90c0d2f93ffaa9714"}, + {file = "typing-3.7.4.1.tar.gz", hash = "sha256:91dfe6f3f706ee8cc32d38edbbf304e9b7583fb37108fef38229617f8b3eba23"}, +] +typing-extensions = [ + {file = "typing_extensions-3.7.4-py2-none-any.whl", hash = "sha256:b1edbbf0652660e32ae780ac9433f4231e7339c7f9a8057d0f042fcbcea49b87"}, + {file = "typing_extensions-3.7.4-py3-none-any.whl", hash = "sha256:d8179012ec2c620d3791ca6fe2bf7979d979acdbef1fca0bc56b37411db682ed"}, + {file = "typing_extensions-3.7.4.tar.gz", hash = "sha256:2ed632b30bb54fc3941c382decfd0ee4148f5c591651c9272473fea2c6397d95"}, +] +urllib3 = [ + {file = "urllib3-1.25.8-py2.py3-none-any.whl", hash = "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc"}, + {file = "urllib3-1.25.8.tar.gz", hash = "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"}, +] +vistir = [ + {file = "vistir-0.4.3-py2.py3-none-any.whl", hash = "sha256:3a0529b4b6c2e842fd19b5ceaa95b6c9201321314825c110406d4af3331a0709"}, + {file = "vistir-0.4.3.tar.gz", hash = "sha256:2166e3148a67c438c9e3edbba0cde153d42dec6e3bf5d8f4624feb27686c0990"}, +] +vulture = [ + {file = "vulture-1.1-py2.py3-none-any.whl", hash = "sha256:20e73154efc9c6d2445663bc2f7fbf79b6c562f2b78cafc0ec0463b38610f42d"}, + {file = "vulture-1.1.tar.gz", hash = "sha256:a72834a8c9cf254f6ba0a64e9053b800e05df8391b1724702a19b00d7d849a43"}, +] +wcwidth = [ + {file = "wcwidth-0.1.7-py2.py3-none-any.whl", hash = "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c"}, + {file = "wcwidth-0.1.7.tar.gz", hash = "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e"}, +] +wheel = [ + {file = "wheel-0.33.6-py2.py3-none-any.whl", hash = "sha256:f4da1763d3becf2e2cd92a14a7c920f0f00eca30fdde9ea992c836685b9faf28"}, + {file = "wheel-0.33.6.tar.gz", hash = "sha256:10c9da68765315ed98850f8e048347c3eb06dd81822dc2ab1d4fde9dc9702646"}, +] +whichcraft = [ + {file = "whichcraft-0.6.1-py2.py3-none-any.whl", hash = "sha256:deda9266fbb22b8c64fd3ee45c050d61139cd87419765f588e37c8d23e236dd9"}, + {file = "whichcraft-0.6.1.tar.gz", hash = "sha256:acdbb91b63d6a15efbd6430d1d7b2d36e44a71697e93e19b7ded477afd9fce87"}, +] +yarg = [ + {file = "yarg-0.1.9-py2.py3-none-any.whl", hash = "sha256:4f9cebdc00fac946c9bf2783d634e538a71c7d280a4d806d45fd4dc0ef441492"}, + {file = "yarg-0.1.9.tar.gz", hash = "sha256:55695bf4d1e3e7f756496c36a69ba32c40d18f821e38f61d028f6049e5e15911"}, +] +yaspin = [ + {file = "yaspin-0.15.0-py2.py3-none-any.whl", hash = "sha256:0ee4668936d0053de752c9a4963929faa3a832bd0ba823877d27855592dc80aa"}, + {file = "yaspin-0.15.0.tar.gz", hash = "sha256:5a938bdc7bab353fd8942d0619d56c6b5159a80997dc1c387a479b39e6dc9391"}, +] +zipp = [ + {file = "zipp-0.6.0-py2.py3-none-any.whl", hash = "sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335"}, + {file = "zipp-0.6.0.tar.gz", hash = "sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e"}, +] diff --git a/pyproject.toml b/pyproject.toml index b6d9f171c..f12101994 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,7 +67,7 @@ pip_api = "^0.0.12" numpy = "^1.16.0" pylama = "^7.7" pip = "^20.0.2" -pip-shims = "<=0.3.4" +pip-shims = "^0.5.2" [tool.poetry.scripts] isort = "isort.main:main" From 758a703f9854bb8eb15ae097e3ed4d647da912f2 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 9 May 2020 17:18:09 -0700 Subject: [PATCH 0522/1439] Add test coverage for finders manager --- isort/finders.py | 4 ++-- isort/output.py | 2 +- tests/test_finders.py | 31 +++++++++++++++++++++++++++++++ tests/test_io.py | 2 +- 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/isort/finders.py b/isort/finders.py index 259ddbb48..8555c370b 100644 --- a/isort/finders.py +++ b/isort/finders.py @@ -390,6 +390,8 @@ def find(self, module_name: str) -> Optional[str]: for finder in self.finders: try: section = finder.find(module_name) + if section is not None: + return section except Exception as exception: # isort has to be able to keep trying to identify the correct # import section even if one approach fails @@ -398,6 +400,4 @@ def find(self, module_name: str) -> Optional[str]: f"{finder.__class__.__name__} encountered an error ({exception}) while " f"trying to identify the {module_name} module" ) - if section is not None: - return section return None diff --git a/isort/output.py b/isort/output.py index a20632288..ce687ff27 100644 --- a/isort/output.py +++ b/isort/output.py @@ -475,7 +475,7 @@ def _with_from_imports( if import_statement: above_comments = parsed.categorized_comments["above"]["from"].pop(module, None) - if above_comments: + if above_comments: # pragma: no cover if new_section_output and config.ensure_newline_before_comments: new_section_output.append("") new_section_output.extend(above_comments) diff --git a/tests/test_finders.py b/tests/test_finders.py index 02b3ba4c1..391a2205f 100644 --- a/tests/test_finders.py +++ b/tests/test_finders.py @@ -1,4 +1,35 @@ +from unittest.mock import patch + from isort import finders, settings +from isort.finders import FindersManager + + +class TestFindersManager: + def test_init(self): + assert FindersManager(settings.DEFAULT_CONFIG) + + class ExceptionOnInit(finders.BaseFinder): + def __init__(*args, **kwargs): + super().__init__(*args, **kwargs) + raise ValueError("test") + + with patch( + "isort.finders.FindersManager._default_finders_classes", + FindersManager._default_finders_classes + (ExceptionOnInit,), + ): + assert FindersManager(settings.Config(verbose=True)) + + def test_no_finders(self): + assert FindersManager(settings.DEFAULT_CONFIG, []).find("isort") is None + + def test_find_broken_finder(self): + class ExceptionOnFind(finders.BaseFinder): + def find(*args, **kwargs): + raise ValueError("test") + + assert ( + FindersManager(settings.Config(verbose=True), [ExceptionOnFind]).find("isort") is None + ) class AbstractTestFinder: diff --git a/tests/test_io.py b/tests/test_io.py index 5fc161084..9b2637fb2 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -1,5 +1,5 @@ import sys -from unittest.mock import MagicMock, patch +from unittest.mock import patch import pytest From d6c6d3cf687beb0f9b19acbe08cf494b26372fdf Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 9 May 2020 17:24:41 -0700 Subject: [PATCH 0523/1439] Add test for no pipreqs --- tests/test_finders.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_finders.py b/tests/test_finders.py index 391a2205f..8c37dd9b5 100644 --- a/tests/test_finders.py +++ b/tests/test_finders.py @@ -71,3 +71,8 @@ class TestPipfileFinder(AbstractTestFinder): class TestRequirementsFinder(AbstractTestFinder): kind = finders.RequirementsFinder + + def test_no_pipreqs(self): + with patch("isort.finders.pipreqs", None): + assert not self.instance.find("isort") + From 8df261cb9183e5933f49c441f3af8940f8449059 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 9 May 2020 17:41:31 -0700 Subject: [PATCH 0524/1439] Improve finders code coverage, fix tests not running due to inheretince approach with __init__ --- tests/test_finders.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/test_finders.py b/tests/test_finders.py index 8c37dd9b5..a5763d9c4 100644 --- a/tests/test_finders.py +++ b/tests/test_finders.py @@ -1,5 +1,7 @@ from unittest.mock import patch +import pytest + from isort import finders, settings from isort.finders import FindersManager @@ -35,14 +37,16 @@ def find(*args, **kwargs): class AbstractTestFinder: kind = finders.BaseFinder - def __init__(self): - self.instance = self.kind(settings.DEFAULT_CONFIG) + @classmethod + def setup_class(cls): + cls.instance = cls.kind(settings.DEFAULT_CONFIG) def test_create(self): assert self.kind(settings.DEFAULT_CONFIG) def test_find(self): self.instance.find("isort") + self.instance.find("") class TestForcedSeparateFinder(AbstractTestFinder): @@ -74,5 +78,9 @@ class TestRequirementsFinder(AbstractTestFinder): def test_no_pipreqs(self): with patch("isort.finders.pipreqs", None): - assert not self.instance.find("isort") + assert not self.kind(settings.DEFAULT_CONFIG).find("isort") + def test_not_enabled(self): + test_finder = self.kind(settings.DEFAULT_CONFIG) + test_finder.enabled = False + assert not test_finder.find("isort") From d119abe9153d892e568b84242d44cea5fa7920b8 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 10 May 2020 00:42:51 -0700 Subject: [PATCH 0525/1439] Add test for nested requirements --- tests/test_finders.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_finders.py b/tests/test_finders.py index a5763d9c4..9668fc334 100644 --- a/tests/test_finders.py +++ b/tests/test_finders.py @@ -84,3 +84,8 @@ def test_not_enabled(self): test_finder = self.kind(settings.DEFAULT_CONFIG) test_finder.enabled = False assert not test_finder.find("isort") + + def test_requirements_dir(self, tmpdir): + tmpdir.mkdir("requirements").join("development.txt").write("x==1.00") + test_finder = self.kind(settings.DEFAULT_CONFIG, str(tmpdir)) + assert test_finder.find("x") From cf4ce177e2db66c2b6642aba41f95c27e1e723d8 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 10 May 2020 01:55:29 -0700 Subject: [PATCH 0526/1439] Add initial tests for virtual env loading --- tests/test_finders.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_finders.py b/tests/test_finders.py index 9668fc334..71ced962f 100644 --- a/tests/test_finders.py +++ b/tests/test_finders.py @@ -68,6 +68,16 @@ class TestLocalFinder(AbstractTestFinder): class TestPathFinder(AbstractTestFinder): kind = finders.PathFinder + def test_conda_and_virtual_env(self, tmpdir): + python3lib = tmpdir.mkdir("lib").mkdir("python3") + python3lib.mkdir("site-packages").mkdir("y") + python3lib.mkdir("n").mkdir("site-packages").mkdir("x") + + conda = self.kind(settings.Config(conda_env=str(tmpdir))) + venv = self.kind(settings.Config(virtual_env=str(tmpdir))) + assert conda.find("y") == venv.find("y") == "THIRDPARTY" + assert conda.find("x") == venv.find("x") == "THIRDPARTY" + class TestPipfileFinder(AbstractTestFinder): kind = finders.PipfileFinder From 353060161769a4688a7c20e05c671d9d47b04ba5 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 10 May 2020 02:37:03 -0700 Subject: [PATCH 0527/1439] 100% test coverage achieved --- isort/finders.py | 22 +++++++++++----------- tests/test_finders.py | 8 ++++++-- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/isort/finders.py b/isort/finders.py index 8555c370b..4a138a3a7 100644 --- a/isort/finders.py +++ b/isort/finders.py @@ -116,11 +116,11 @@ def find(self, module_name: str) -> Optional[str]: class PathFinder(BaseFinder): - def __init__(self, config: Config) -> None: + def __init__(self, config: Config, path: str = ".") -> None: super().__init__(config) # restore the original import path (i.e. not the path to bin/isort) - root_dir = os.getcwd() + root_dir = os.path.abspath(path) src_dir = f"{root_dir}/src" self.paths = [root_dir, src_dir] @@ -175,18 +175,18 @@ def find(self, module_name: str) -> Optional[str]: ) is_package = exists_case_sensitive(package_path) and os.path.isdir(package_path) if is_module or is_package: - if "site-packages" in prefix: - return sections.THIRDPARTY - if "dist-packages" in prefix: + if ( + "site-packages" in prefix + or "dist-packages" in prefix + or (self.virtual_env and self.virtual_env_src in prefix) + ): return sections.THIRDPARTY - if self.virtual_env and self.virtual_env_src in prefix: - return sections.THIRDPARTY - if os.path.normcase(prefix) == self.stdlib_lib_prefix: + elif os.path.normcase(prefix) == self.stdlib_lib_prefix: return sections.STDLIB - if self.conda_env and self.conda_env in prefix: + elif self.conda_env and self.conda_env in prefix: return sections.THIRDPARTY - if os.path.normcase(prefix).startswith(self.stdlib_lib_prefix): - return sections.STDLIB + elif os.path.normcase(prefix).startswith(self.stdlib_lib_prefix): + return sections.STDLIB # pragma: no cover - edge case for one OS. Hard to test. return self.config.default_section return None diff --git a/tests/test_finders.py b/tests/test_finders.py index 71ced962f..2e54317d6 100644 --- a/tests/test_finders.py +++ b/tests/test_finders.py @@ -72,11 +72,15 @@ def test_conda_and_virtual_env(self, tmpdir): python3lib = tmpdir.mkdir("lib").mkdir("python3") python3lib.mkdir("site-packages").mkdir("y") python3lib.mkdir("n").mkdir("site-packages").mkdir("x") + tmpdir.mkdir("z").join("__init__.py").write("__version__ = '1.0.0'") + tmpdir.chdir() - conda = self.kind(settings.Config(conda_env=str(tmpdir))) - venv = self.kind(settings.Config(virtual_env=str(tmpdir))) + conda = self.kind(settings.Config(conda_env=str(tmpdir)), str(tmpdir)) + venv = self.kind(settings.Config(virtual_env=str(tmpdir)), str(tmpdir)) assert conda.find("y") == venv.find("y") == "THIRDPARTY" assert conda.find("x") == venv.find("x") == "THIRDPARTY" + assert conda.find("z") == "THIRDPARTY" + assert conda.find("os") == venv.find("os") == "STDLIB" class TestPipfileFinder(AbstractTestFinder): From f87fce3be0abd80bc5be898f83f66dc2a565cebc Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 11 May 2020 22:37:34 -0700 Subject: [PATCH 0528/1439] Update dependencies --- poetry.lock | 1007 +++++++++++++++++++++++++-------------------- pyproject.toml | 6 +- tests/test_api.py | 8 +- 3 files changed, 567 insertions(+), 454 deletions(-) diff --git a/poetry.lock b/poetry.lock index 6960a04aa..005f3469c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -4,7 +4,7 @@ description = "A small Python module for determining appropriate platform-specif name = "appdirs" optional = false python-versions = "*" -version = "1.4.3" +version = "1.4.4" [[package]] category = "dev" @@ -21,7 +21,7 @@ description = "Better dates & times for Python" name = "arrow" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "0.15.2" +version = "0.15.6" [package.dependencies] python-dateutil = "*" @@ -29,10 +29,11 @@ python-dateutil = "*" [[package]] category = "dev" description = "Atomic file writes." +marker = "sys_platform == \"win32\"" name = "atomicwrites" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.3.0" +version = "1.4.0" [[package]] category = "main" @@ -124,7 +125,7 @@ description = "Python package for providing Mozilla's CA Bundle." name = "certifi" optional = false python-versions = "*" -version = "2019.9.11" +version = "2020.4.5.1" [[package]] category = "main" @@ -139,42 +140,46 @@ category = "dev" description = "Composable command line interface toolkit" name = "click" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "7.0" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "7.1.2" [[package]] category = "main" description = "Cross-platform colored terminal text." name = "colorama" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.4.1" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.4.3" [[package]] category = "dev" description = "A command-line utility that creates projects from project templates, e.g. creating a Python package project from a Python package project template." name = "cookiecutter" optional = false -python-versions = "*" -version = "1.6.0" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "1.7.2" [package.dependencies] -binaryornot = ">=0.2.0" -click = ">=5.0" -future = ">=0.15.2" -jinja2 = ">=2.7" -jinja2-time = ">=0.1.0" -poyo = ">=0.1.0" -requests = ">=2.18.0" -whichcraft = ">=0.4.0" +Jinja2 = "<3.0.0" +MarkupSafe = "<2.0.0" +binaryornot = ">=0.4.4" +click = ">=7.0" +jinja2-time = ">=0.2.0" +poyo = ">=0.5.0" +python-slugify = ">=4.0.0" +requests = ">=2.23.0" +six = ">=1.10" [[package]] category = "dev" description = "Code coverage measurement for Python" name = "coverage" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, <4" -version = "4.5.4" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +version = "5.1" + +[package.extras] +toml = ["toml"] [[package]] category = "dev" @@ -204,11 +209,11 @@ version = "0.6" [[package]] category = "dev" -description = "Better living through Python with decorators" +description = "Decorators for Humans" name = "decorator" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*" -version = "4.4.0" +version = "4.4.2" [[package]] category = "main" @@ -216,7 +221,7 @@ description = "Distribution utilities" name = "distlib" optional = false python-versions = "*" -version = "0.2.9.post0" +version = "0.3.0" [[package]] category = "main" @@ -231,35 +236,27 @@ category = "dev" description = "A parser for Python dependency files" name = "dparse" optional = false -python-versions = "*" -version = "0.4.1" +python-versions = ">=3.5" +version = "0.5.1" [package.dependencies] packaging = "*" pyyaml = "*" -six = "*" +toml = "*" [package.extras] pipenv = ["pipenv"] -[[package]] -category = "dev" -description = "Discover and load entry points from installed packages." -name = "entrypoints" -optional = false -python-versions = ">=2.7" -version = "0.3" - [[package]] category = "dev" description = "Tests and Documentation Done by Example." name = "examples" optional = false python-versions = ">=3.6,<4.0" -version = "1.0.1" +version = "1.0.2" [package.dependencies] -pydantic = ">=0.32.2,<0.33.0" +pydantic = ">=0.32.2" [[package]] category = "dev" @@ -269,27 +266,22 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "2.0.0" -[[package]] -category = "main" -description = "Return the first true value of an iterable." -name = "first" -optional = false -python-versions = "*" -version = "2.0.2" - [[package]] category = "dev" -description = "the modular source code checker: pep8, pyflakes and co" +description = "the modular source code checker: pep8 pyflakes and co" name = "flake8" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "3.7.8" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +version = "3.8.0" [package.dependencies] -entrypoints = ">=0.3.0,<0.4.0" mccabe = ">=0.6.0,<0.7.0" -pycodestyle = ">=2.5.0,<2.6.0" -pyflakes = ">=2.1.0,<2.2.0" +pycodestyle = ">=2.6.0a1,<2.7.0" +pyflakes = ">=2.2.0,<2.3.0" + +[package.dependencies.importlib-metadata] +python = "<3.8" +version = "*" [[package]] category = "dev" @@ -320,37 +312,40 @@ description = "Clean single-source support for Python 3 and 2" name = "future" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -version = "0.18.1" +version = "0.18.2" [[package]] category = "dev" description = "Git Object Database" -name = "gitdb2" +name = "gitdb" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.0.6" +python-versions = ">=3.4" +version = "4.0.5" [package.dependencies] -smmap2 = ">=2.0.0" +smmap = ">=3.0.1,<4" [[package]] category = "dev" -description = "Python Git Library" -name = "gitpython" +description = "A mirror package for gitdb" +name = "gitdb2" optional = false -python-versions = ">=3.0, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "3.0.3" +python-versions = "*" +version = "4.0.2" [package.dependencies] -gitdb2 = ">=2.0.0" +gitdb = ">=4.0.1" [[package]] category = "dev" -description = "An HTML Minifier" -name = "htmlmin" +description = "Python Git Library" +name = "gitpython" optional = false -python-versions = "*" -version = "0.1.12" +python-versions = ">=3.4" +version = "3.1.2" + +[package.dependencies] +gitdb = ">=4.0.1,<5" [[package]] category = "dev" @@ -358,7 +353,7 @@ description = "A Python framework that makes developing APIs as simple as possib name = "hug" optional = false python-versions = ">=3.5" -version = "2.6.0" +version = "2.6.1" [package.dependencies] falcon = "2.0.0" @@ -366,19 +361,20 @@ requests = "*" [[package]] category = "dev" -description = "A library for property based testing" +description = "A library for property-based testing" name = "hypothesis" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "4.41.2" +python-versions = ">=3.5.2" +version = "5.12.1" [package.dependencies] attrs = ">=19.2.0" +sortedcontainers = ">=2.1.0,<3.0.0" [package.extras] -all = ["django (>=1.11)", "dpcontracts (>=0.4)", "lark-parser (>=0.6.5)", "numpy (>=1.9.0)", "pandas (>=0.19)", "pytest (>=4.3)", "python-dateutil (>=1.4)", "pytz", "pytz (>=2014.1)"] +all = ["django (>=2.2)", "dpcontracts (>=0.4)", "lark-parser (>=0.6.5)", "numpy (>=1.9.0)", "pandas (>=0.19)", "pytest (>=4.3)", "python-dateutil (>=1.4)", "pytz (>=2014.1)"] dateutil = ["python-dateutil (>=1.4)"] -django = ["pytz", "django (>=1.11)"] +django = ["pytz (>=2014.1)", "django (>=2.2)"] dpcontracts = ["dpcontracts (>=0.4)"] lark = ["lark-parser (>=0.6.5)"] numpy = ["numpy (>=1.9.0)"] @@ -407,15 +403,16 @@ description = "Internationalized Domain Names in Applications (IDNA)" name = "idna" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.8" +version = "2.9" [[package]] category = "main" description = "Read metadata from Python packages" +marker = "python_version < \"3.8\"" name = "importlib-metadata" optional = false -python-versions = ">=2.7,!=3.0,!=3.1,!=3.2,!=3.3" -version = "0.23" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +version = "1.6.0" [package.dependencies] zipp = ">=0.5" @@ -429,8 +426,8 @@ category = "dev" description = "IPython: Productive Interactive Computing" name = "ipython" optional = false -python-versions = ">=3.5" -version = "7.8.0" +python-versions = ">=3.6" +version = "7.14.0" [package.dependencies] appnope = "*" @@ -440,13 +437,13 @@ decorator = "*" jedi = ">=0.10" pexpect = "*" pickleshare = "*" -prompt-toolkit = ">=2.0.0,<2.1.0" +prompt-toolkit = ">=2.0.0,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.1.0" pygments = "*" setuptools = ">=18.5" traitlets = ">=4.2" [package.extras] -all = ["nbconvert", "notebook", "ipywidgets", "testpath", "qtconsole", "ipyparallel", "nose (>=0.10.1)", "nbformat", "ipykernel", "pygments", "requests", "numpy", "Sphinx (>=1.3)"] +all = ["nose (>=0.10.1)", "Sphinx (>=1.3)", "testpath", "nbformat", "ipywidgets", "qtconsole", "numpy (>=1.14)", "notebook", "ipyparallel", "ipykernel", "pygments", "requests", "nbconvert"] doc = ["Sphinx (>=1.3)"] kernel = ["ipykernel"] nbconvert = ["nbconvert"] @@ -454,7 +451,7 @@ nbformat = ["nbformat"] notebook = ["notebook", "ipywidgets"] parallel = ["ipyparallel"] qtconsole = ["qtconsole"] -test = ["nose (>=0.10.1)", "requests", "testpath", "pygments", "nbformat", "ipykernel", "numpy"] +test = ["nose (>=0.10.1)", "requests", "testpath", "pygments", "nbformat", "ipykernel", "numpy (>=1.14)"] [[package]] category = "dev" @@ -469,22 +466,23 @@ category = "dev" description = "An autocompletion tool for Python that can be used for text editors." name = "jedi" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.15.1" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.17.0" [package.dependencies] -parso = ">=0.5.0" +parso = ">=0.7.0" [package.extras] -testing = ["colorama", "docopt", "pytest (>=3.1.0,<5.0.0)"] +qa = ["flake8 (3.7.9)"] +testing = ["colorama", "docopt", "pytest (>=3.9.0,<5.0.0)"] [[package]] category = "dev" description = "A very fast and expressive template engine." name = "jinja2" optional = false -python-versions = "*" -version = "2.10.3" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.11.2" [package.dependencies] MarkupSafe = ">=0.23" @@ -506,11 +504,11 @@ jinja2 = "*" [[package]] category = "dev" -description = "JavaScript minifier." -name = "jsmin" +description = "Lightweight pipelining: using Python functions as pipeline jobs." +name = "joblib" optional = false python-versions = "*" -version = "2.2.2" +version = "0.14.1" [[package]] category = "dev" @@ -524,27 +522,52 @@ version = "2.6.1" six = "*" tornado = "*" +[[package]] +category = "dev" +description = "A Python implementation of Lunr.js" +name = "lunr" +optional = false +python-versions = "*" +version = "0.5.6" + +[package.dependencies] +future = ">=0.16.0" +six = ">=1.11.0" + +[package.dependencies.nltk] +optional = true +version = ">=3.2.5" + +[package.extras] +languages = ["nltk (>=3.2.5)"] + [[package]] category = "dev" description = "A super-fast templating language that borrows the best ideas from the existing templating languages." name = "mako" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.1.0" +version = "1.1.2" [package.dependencies] MarkupSafe = ">=0.9.2" +[package.extras] +babel = ["babel"] +lingua = ["lingua"] + [[package]] category = "dev" description = "Python implementation of Markdown." name = "markdown" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" -version = "3.1.1" +python-versions = ">=3.5" +version = "3.2.2" [package.dependencies] -setuptools = ">=36" +[package.dependencies.importlib-metadata] +python = "<3.8" +version = "*" [package.extras] testing = ["coverage", "pyyaml"] @@ -570,51 +593,42 @@ category = "dev" description = "Project documentation with Markdown." name = "mkdocs" optional = false -python-versions = ">=2.7.9,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" -version = "1.0.4" +python-versions = ">=3.5" +version = "1.1" [package.dependencies] -Jinja2 = ">=2.7.1" -Markdown = ">=2.3.1" +Jinja2 = ">=2.10.1" +Markdown = ">=3.2.1" PyYAML = ">=3.10" click = ">=3.3" livereload = ">=2.5.1" tornado = ">=5.0" +[package.dependencies.lunr] +extras = ["languages"] +version = "0.5.6" + [[package]] category = "dev" description = "A Material Design theme for MkDocs" name = "mkdocs-material" optional = false python-versions = "*" -version = "4.4.3" +version = "4.6.3" [package.dependencies] -Pygments = ">=2.2" -mkdocs = ">=1" -mkdocs-minify-plugin = ">=0.2" -pymdown-extensions = ">=4.11" +Pygments = ">=2.4" +markdown = ">=3.2" +mkdocs = ">=1.0" +pymdown-extensions = ">=6.3" [[package]] category = "dev" -description = "An MkDocs plugin to minify HTML and/or JS files prior to being written to disk" -name = "mkdocs-minify-plugin" -optional = false -python-versions = ">=2.7" -version = "0.2.1" - -[package.dependencies] -htmlmin = ">=0.1.4" -jsmin = ">=2.2.2" -mkdocs = ">=1.0.4" - -[[package]] -category = "main" description = "More routines for operating on iterables, beyond itertools" name = "more-itertools" optional = false -python-versions = ">=3.4" -version = "7.2.0" +python-versions = ">=3.5" +version = "8.2.0" [[package]] category = "dev" @@ -640,13 +654,35 @@ optional = false python-versions = "*" version = "0.4.3" +[[package]] +category = "dev" +description = "Natural Language Toolkit" +name = "nltk" +optional = false +python-versions = "*" +version = "3.5" + +[package.dependencies] +click = "*" +joblib = "*" +regex = "*" +tqdm = "*" + +[package.extras] +all = ["requests", "numpy", "python-crfsuite", "scikit-learn", "twython", "pyparsing", "scipy", "matplotlib", "gensim"] +corenlp = ["requests"] +machine_learning = ["gensim", "numpy", "python-crfsuite", "scikit-learn", "scipy"] +plot = ["matplotlib"] +tgrep = ["pyparsing"] +twitter = ["twython"] + [[package]] category = "dev" description = "NumPy is the fundamental package for array computing with Python." name = "numpy" optional = false python-versions = ">=3.5" -version = "1.17.3" +version = "1.18.4" [[package]] category = "main" @@ -665,7 +701,7 @@ description = "Core utilities for Python packages" name = "packaging" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "19.2" +version = "20.3" [package.dependencies] pyparsing = ">=2.0.2" @@ -676,8 +712,8 @@ category = "dev" description = "A Python Parser" name = "parso" optional = false -python-versions = "*" -version = "0.5.1" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.7.0" [package.extras] testing = ["docopt", "pytest (>=3.0.7)"] @@ -688,7 +724,7 @@ description = "Python Build Reasonableness" name = "pbr" optional = false python-versions = "*" -version = "5.4.3" +version = "5.4.5" [[package]] category = "dev" @@ -709,20 +745,18 @@ description = "Wrappers to build Python packages using PEP 517 hooks" name = "pep517" optional = false python-versions = "*" -version = "0.7.0" +version = "0.8.2" [package.dependencies] -importlib_metadata = "*" toml = "*" -zipp = "*" -[[package]] -category = "dev" -description = "Backport of PEP 562." -name = "pep562" -optional = false -python-versions = "*" -version = "1.0" +[package.dependencies.importlib_metadata] +python = "<3.8" +version = "*" + +[package.dependencies.zipp] +python = "<3.8" +version = "*" [[package]] category = "dev" @@ -742,7 +776,7 @@ marker = "sys_platform != \"win32\"" name = "pexpect" optional = false python-versions = "*" -version = "4.7.0" +version = "4.8.0" [package.dependencies] ptyprocess = ">=0.5" @@ -802,7 +836,7 @@ description = "Pip requirements.txt generator based on imports in project" name = "pipreqs" optional = false python-versions = "*" -version = "0.4.9" +version = "0.4.10" [package.dependencies] docopt = "*" @@ -814,7 +848,7 @@ description = "Structured Pipfile and Pipfile.lock models." name = "plette" optional = false python-versions = ">=2.6,!=3.0,!=3.1,!=3.2,!=3.3" -version = "0.2.2" +version = "0.2.3" [package.dependencies] six = "*" @@ -834,7 +868,7 @@ description = "plugin and hook calling mechanisms for python" name = "pluggy" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.13.0" +version = "0.13.1" [package.dependencies] [package.dependencies.importlib-metadata] @@ -875,11 +909,10 @@ category = "dev" description = "Library for building powerful interactive command lines in Python" name = "prompt-toolkit" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -version = "2.0.10" +python-versions = ">=3.6" +version = "3.0.3" [package.dependencies] -six = ">=1.9.0" wcwidth = "*" [[package]] @@ -897,7 +930,7 @@ description = "library with cross-python path, ini-parsing, io, code, log facili name = "py" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.8.0" +version = "1.8.1" [[package]] category = "dev" @@ -905,7 +938,7 @@ description = "Python style guide checker" name = "pycodestyle" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.5.0" +version = "2.6.0" [[package]] category = "dev" @@ -913,7 +946,7 @@ description = "Data validation and settings management using python 3.6 type hin name = "pydantic" optional = false python-versions = ">=3.6" -version = "0.32.2" +version = "1.5.1" [package.dependencies] [package.dependencies.dataclasses] @@ -921,9 +954,9 @@ python = "<3.7" version = ">=0.6" [package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] email = ["email-validator (>=1.0.3)"] typing_extensions = ["typing-extensions (>=3.7.2)"] -ujson = ["ujson (>=1.35)"] [[package]] category = "dev" @@ -942,15 +975,15 @@ description = "passive checker of Python programs" name = "pyflakes" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.1.1" +version = "2.2.0" [[package]] category = "dev" description = "Pygments is a syntax highlighting package written in Python." name = "pygments" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "2.4.2" +python-versions = ">=3.5" +version = "2.6.1" [[package]] category = "dev" @@ -972,11 +1005,10 @@ description = "Extension pack for Python Markdown." name = "pymdown-extensions" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" -version = "6.1" +version = "6.3" [package.dependencies] -Markdown = ">=3.0.1" -pep562 = "*" +Markdown = ">=3.2" [[package]] category = "main" @@ -984,7 +1016,7 @@ description = "Python parsing module" name = "pyparsing" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -version = "2.4.2" +version = "2.4.7" [[package]] category = "dev" @@ -992,7 +1024,7 @@ description = "pytest: simple powerful testing with Python" name = "pytest" optional = false python-versions = ">=3.5" -version = "5.2.1" +version = "5.4.2" [package.dependencies] atomicwrites = ">=1.0" @@ -1009,6 +1041,7 @@ python = "<3.8" version = ">=0.12" [package.extras] +checkqa-mypy = ["mypy (v0.761)"] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] [[package]] @@ -1032,7 +1065,7 @@ description = "Thin-wrapper around the mock package for easier use with py.test" name = "pytest-mock" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.11.1" +version = "1.13.0" [package.dependencies] pytest = ">=2.7" @@ -1041,16 +1074,30 @@ pytest = ">=2.7" dev = ["pre-commit", "tox"] [[package]] -category = "dev" +category = "main" description = "Extensions to the standard Python datetime module" name = "python-dateutil" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -version = "2.8.0" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +version = "2.8.1" [package.dependencies] six = ">=1.5" +[[package]] +category = "dev" +description = "A Python Slugify application that handles Unicode" +name = "python-slugify" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "4.0.0" + +[package.dependencies] +text-unidecode = ">=1.3" + +[package.extras] +unidecode = ["Unidecode (>=1.1.1)"] + [[package]] category = "dev" description = "YAML parser and emitter for Python" @@ -1059,22 +1106,30 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" version = "5.3.1" +[[package]] +category = "dev" +description = "Alternative regular expression module, to replace re." +name = "regex" +optional = false +python-versions = "*" +version = "2020.5.7" + [[package]] category = "main" description = "Python HTTP for Humans." name = "requests" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "2.22.0" +version = "2.23.0" [package.dependencies] certifi = ">=2017.4.17" -chardet = ">=3.0.2,<3.1.0" -idna = ">=2.5,<2.9" +chardet = ">=3.0.2,<4" +idna = ">=2.5,<3" urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" [package.extras] -security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)"] +security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] [[package]] @@ -1082,19 +1137,19 @@ category = "main" description = "A tool for converting between pip-style and pipfile requirements." name = "requirementslib" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" -version = "1.5.3" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +version = "1.5.7" [package.dependencies] appdirs = "*" attrs = ">=18.2" cached-property = "*" distlib = ">=0.2.8" -first = "*" orderedmultidict = "*" packaging = ">=19.0" pep517 = ">=0.5.0" pip-shims = ">=0.3.2" +python-dateutil = "*" requests = "*" setuptools = ">=40.8" six = ">=1.11.0" @@ -1107,20 +1162,20 @@ version = "*" [package.extras] dev = ["vulture", "flake8", "rope", "isort", "invoke", "twine", "pre-commit", "lxml", "towncrier", "parver", "flake8-bugbear", "black"] -tests = ["pytest", "twine", "readme-renderer", "pytest-xdist", "pytest-cov", "pytest-timeout", "pytest-sugar", "coverage (<5.0a5)", "hypothesis"] -typing = ["mypy", "mypy-extensions", "mypytools", "pytype", "typed-ast", "monkeytype"] +tests = ["mock", "pytest", "twine", "readme-renderer", "pytest-xdist", "pytest-cov", "pytest-timeout", "coverage", "hypothesis"] +typing = ["typing", "mypy", "mypy-extensions", "mypytools", "pytype", "typed-ast", "monkeytype"] [[package]] category = "dev" -description = "Safety checks your installed dependencies for known security vulnerabilities." +description = "Checks installed dependencies for known vulnerabilities." name = "safety" optional = false -python-versions = "*" -version = "1.8.5" +python-versions = ">=3.5" +version = "1.9.0" [package.dependencies] Click = ">=6.0" -dparse = ">=0.4.1" +dparse = ">=0.5.1" packaging = "*" requests = "*" setuptools = "*" @@ -1130,16 +1185,27 @@ category = "main" description = "Python 2 and 3 compatibility utilities" name = "six" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*" -version = "1.12.0" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +version = "1.14.0" [[package]] category = "dev" description = "A pure Python implementation of a sliding window memory map manager" -name = "smmap2" +name = "smmap" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.0.5" +version = "3.0.4" + +[[package]] +category = "dev" +description = "A mirror package for smmap" +name = "smmap2" +optional = false +python-versions = "*" +version = "3.0.1" + +[package.dependencies] +smmap = ">=3.0.1" [[package]] category = "dev" @@ -1149,18 +1215,34 @@ optional = false python-versions = "*" version = "2.0.0" +[[package]] +category = "dev" +description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +name = "sortedcontainers" +optional = false +python-versions = "*" +version = "2.1.0" + [[package]] category = "dev" description = "Manage dynamic plugins for Python applications" name = "stevedore" optional = false python-versions = "*" -version = "1.31.0" +version = "1.32.0" [package.dependencies] pbr = ">=2.0.0,<2.1.0 || >2.1.0" six = ">=1.10.0" +[[package]] +category = "dev" +description = "The most basic Text::Unidecode port" +name = "text-unidecode" +optional = false +python-versions = "*" +version = "1.3" + [[package]] category = "main" description = "Python Library for Tom's Obvious, Minimal Language" @@ -1175,7 +1257,7 @@ description = "Style preserving TOML library" name = "tomlkit" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.5.3" +version = "0.6.0" [[package]] category = "dev" @@ -1183,7 +1265,18 @@ description = "Tornado is a Python web framework and asynchronous networking lib name = "tornado" optional = false python-versions = ">= 3.5" -version = "6.0.3" +version = "6.0.4" + +[[package]] +category = "dev" +description = "Fast, Extensible Progress Meter" +name = "tqdm" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*" +version = "4.46.0" + +[package.extras] +dev = ["py-make (>=0.1.0)", "twine", "argopt", "pydoc-markdown"] [[package]] category = "dev" @@ -1207,15 +1300,7 @@ description = "a fork of Python 2 and 3 ast modules with type comment support" name = "typed-ast" optional = false python-versions = "*" -version = "1.4.0" - -[[package]] -category = "dev" -description = "Type Hints for Python" -name = "typing" -optional = false -python-versions = "*" -version = "3.7.4.1" +version = "1.4.1" [[package]] category = "dev" @@ -1223,10 +1308,7 @@ description = "Backported and Experimental Type Hints for Python 3.5+" name = "typing-extensions" optional = false python-versions = "*" -version = "3.7.4" - -[package.dependencies] -typing = ">=3.7.4" +version = "3.7.4.2" [[package]] category = "main" @@ -1234,11 +1316,11 @@ description = "HTTP library with thread-safe connection pooling, file post, and name = "urllib3" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" -version = "1.25.8" +version = "1.25.9" [package.extras] brotli = ["brotlipy (>=0.6.0)"] -secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)", "ipaddress"] socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] [[package]] @@ -1246,27 +1328,27 @@ category = "main" description = "Miscellaneous utilities for dealing with filesystems, paths, projects, subprocesses, and more." name = "vistir" optional = false -python-versions = ">=2.7,!=3.0,!=3.1,!=3.2,!=3.3" -version = "0.4.3" +python-versions = "!=3.0,!=3.1,!=3.2,!=3.3,>=2.7" +version = "0.5.0" [package.dependencies] -colorama = ">=0.3.4" -requests = "*" +colorama = ">=0.3.4,<0.4.2 || >0.4.2" six = "*" [package.extras] dev = ["pre-commit", "coverage (<5.0)", "isort", "flake8", "rope", "invoke", "parver", "sphinx", "sphinx-rtd-theme", "flake8-bugbear", "black"] +requests = ["requests"] spinner = ["yaspin"] -tests = ["twine", "readme-renderer", "pytest", "pytest-xdist", "pytest-cov", "pytest-timeout", "hypothesis-fspaths", "hypothesis"] -typing = ["mypy", "mypy-extensions", "mypytools", "pytype", "typed-ast"] +tests = ["hypothesis", "hypothesis-fspaths", "pytest", "pytest-xdist", "pytest-cov", "pytest-timeout", "readme-renderer", "twine", "mock"] +typing = ["typing", "mypy", "mypy-extensions", "mypytools", "pytype", "typed-ast"] [[package]] category = "dev" description = "Find dead code" name = "vulture" optional = false -python-versions = "*" -version = "1.1" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "1.4" [[package]] category = "dev" @@ -1274,27 +1356,19 @@ description = "Measures number of Terminal column cells of wide-character codes" name = "wcwidth" optional = false python-versions = "*" -version = "0.1.7" +version = "0.1.9" [[package]] category = "main" -description = "A built-package format for Python." +description = "A built-package format for Python" name = "wheel" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.33.6" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.34.2" [package.extras] test = ["pytest (>=3.0.0)", "pytest-cov"] -[[package]] -category = "dev" -description = "This package provides cross-platform cross-python shutil.which functionality." -name = "whichcraft" -optional = false -python-versions = "*" -version = "0.6.1" - [[package]] category = "main" description = "A semi hard Cornish cheese, also queries PyPI (PyPI client)" @@ -1317,17 +1391,15 @@ version = "0.15.0" [[package]] category = "main" description = "Backport of pathlib-compatible object wrapper for zip files" +marker = "python_version < \"3.8\"" name = "zipp" optional = false -python-versions = ">=2.7" -version = "0.6.0" - -[package.dependencies] -more-itertools = "*" +python-versions = ">=3.6" +version = "3.1.0" [package.extras] docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] -testing = ["pathlib2", "contextlib2", "unittest2"] +testing = ["jaraco.itertools", "func-timeout"] [extras] pipfile = ["pipreqs", "tomlkit", "requirementslib"] @@ -1336,25 +1408,25 @@ requirements = ["pipreqs", "pip-api"] xdg_home = ["appdirs"] [metadata] -content-hash = "310311d313b5820d3cf1f6f8d446e14cbb7a1be118e1b0d5c1abaee1708bd412" +content-hash = "983dd12d7389a2267be62bd2f32082e7a7f66b2ef89c3298f5a688df6e70a715" python-versions = "^3.6" [metadata.files] appdirs = [ - {file = "appdirs-1.4.3-py2.py3-none-any.whl", hash = "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"}, - {file = "appdirs-1.4.3.tar.gz", hash = "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92"}, + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, ] appnope = [ {file = "appnope-0.1.0-py2.py3-none-any.whl", hash = "sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0"}, {file = "appnope-0.1.0.tar.gz", hash = "sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71"}, ] arrow = [ - {file = "arrow-0.15.2-py2.py3-none-any.whl", hash = "sha256:c2325911fcd79972cf493cfd957072f9644af8ad25456201ae1ede3316576eb4"}, - {file = "arrow-0.15.2.tar.gz", hash = "sha256:10257c5daba1a88db34afa284823382f4963feca7733b9107956bed041aff24f"}, + {file = "arrow-0.15.6-py2.py3-none-any.whl", hash = "sha256:a24c1de90850f6fb2033fd6bf8a11f281e84cb54825e5eabdda219e673b52aac"}, + {file = "arrow-0.15.6.tar.gz", hash = "sha256:eb5d339f00072cc297d7de252a2e75f272085d1231a3723f1026d1fa91367118"}, ] atomicwrites = [ - {file = "atomicwrites-1.3.0-py2.py3-none-any.whl", hash = "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4"}, - {file = "atomicwrites-1.3.0.tar.gz", hash = "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"}, + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, ] attrs = [ {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, @@ -1384,58 +1456,57 @@ cerberus = [ {file = "Cerberus-1.3.2.tar.gz", hash = "sha256:302e6694f206dd85cb63f13fd5025b31ab6d38c99c50c6d769f8fa0b0f299589"}, ] certifi = [ - {file = "certifi-2019.9.11-py2.py3-none-any.whl", hash = "sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef"}, - {file = "certifi-2019.9.11.tar.gz", hash = "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50"}, + {file = "certifi-2020.4.5.1-py2.py3-none-any.whl", hash = "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304"}, + {file = "certifi-2020.4.5.1.tar.gz", hash = "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519"}, ] chardet = [ {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, ] click = [ - {file = "Click-7.0-py2.py3-none-any.whl", hash = "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13"}, - {file = "Click-7.0.tar.gz", hash = "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"}, + {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, + {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, ] colorama = [ - {file = "colorama-0.4.1-py2.py3-none-any.whl", hash = "sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"}, - {file = "colorama-0.4.1.tar.gz", hash = "sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d"}, + {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, + {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, ] cookiecutter = [ - {file = "cookiecutter-1.6.0-py2.py3-none-any.whl", hash = "sha256:ed8f54a8fc79b6864020d773ce11539b5f08e4617f353de1f22d23226f6a0d36"}, - {file = "cookiecutter-1.6.0.tar.gz", hash = "sha256:1316a52e1c1f08db0c9efbf7d876dbc01463a74b155a0d83e722be88beda9a3e"}, + {file = "cookiecutter-1.7.2-py2.py3-none-any.whl", hash = "sha256:430eb882d028afb6102c084bab6cf41f6559a77ce9b18dc6802e3bc0cc5f4a30"}, + {file = "cookiecutter-1.7.2.tar.gz", hash = "sha256:efb6b2d4780feda8908a873e38f0e61778c23f6a2ea58215723bcceb5b515dac"}, ] coverage = [ - {file = "coverage-4.5.4-cp26-cp26m-macosx_10_12_x86_64.whl", hash = "sha256:eee64c616adeff7db37cc37da4180a3a5b6177f5c46b187894e633f088fb5b28"}, - {file = "coverage-4.5.4-cp27-cp27m-macosx_10_12_x86_64.whl", hash = "sha256:ef824cad1f980d27f26166f86856efe11eff9912c4fed97d3804820d43fa550c"}, - {file = "coverage-4.5.4-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:9a334d6c83dfeadae576b4d633a71620d40d1c379129d587faa42ee3e2a85cce"}, - {file = "coverage-4.5.4-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:7494b0b0274c5072bddbfd5b4a6c6f18fbbe1ab1d22a41e99cd2d00c8f96ecfe"}, - {file = "coverage-4.5.4-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:826f32b9547c8091679ff292a82aca9c7b9650f9fda3e2ca6bf2ac905b7ce888"}, - {file = "coverage-4.5.4-cp27-cp27m-win32.whl", hash = "sha256:63a9a5fc43b58735f65ed63d2cf43508f462dc49857da70b8980ad78d41d52fc"}, - {file = "coverage-4.5.4-cp27-cp27m-win_amd64.whl", hash = "sha256:e2ede7c1d45e65e209d6093b762e98e8318ddeff95317d07a27a2140b80cfd24"}, - {file = "coverage-4.5.4-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:dd579709a87092c6dbee09d1b7cfa81831040705ffa12a1b248935274aee0437"}, - {file = "coverage-4.5.4-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:08907593569fe59baca0bf152c43f3863201efb6113ecb38ce7e97ce339805a6"}, - {file = "coverage-4.5.4-cp33-cp33m-macosx_10_10_x86_64.whl", hash = "sha256:6b62544bb68106e3f00b21c8930e83e584fdca005d4fffd29bb39fb3ffa03cb5"}, - {file = "coverage-4.5.4-cp34-cp34m-macosx_10_12_x86_64.whl", hash = "sha256:331cb5115673a20fb131dadd22f5bcaf7677ef758741312bee4937d71a14b2ef"}, - {file = "coverage-4.5.4-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:bf1ef9eb901113a9805287e090452c05547578eaab1b62e4ad456fcc049a9b7e"}, - {file = "coverage-4.5.4-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:386e2e4090f0bc5df274e720105c342263423e77ee8826002dcffe0c9533dbca"}, - {file = "coverage-4.5.4-cp34-cp34m-win32.whl", hash = "sha256:fa964bae817babece5aa2e8c1af841bebb6d0b9add8e637548809d040443fee0"}, - {file = "coverage-4.5.4-cp34-cp34m-win_amd64.whl", hash = "sha256:df6712284b2e44a065097846488f66840445eb987eb81b3cc6e4149e7b6982e1"}, - {file = "coverage-4.5.4-cp35-cp35m-macosx_10_12_x86_64.whl", hash = "sha256:efc89291bd5a08855829a3c522df16d856455297cf35ae827a37edac45f466a7"}, - {file = "coverage-4.5.4-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:e4ef9c164eb55123c62411f5936b5c2e521b12356037b6e1c2617cef45523d47"}, - {file = "coverage-4.5.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:ff37757e068ae606659c28c3bd0d923f9d29a85de79bf25b2b34b148473b5025"}, - {file = "coverage-4.5.4-cp35-cp35m-win32.whl", hash = "sha256:bf0a7aed7f5521c7ca67febd57db473af4762b9622254291fbcbb8cd0ba5e33e"}, - {file = "coverage-4.5.4-cp35-cp35m-win_amd64.whl", hash = "sha256:19e4df788a0581238e9390c85a7a09af39c7b539b29f25c89209e6c3e371270d"}, - {file = "coverage-4.5.4-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:60851187677b24c6085248f0a0b9b98d49cba7ecc7ec60ba6b9d2e5574ac1ee9"}, - {file = "coverage-4.5.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:245388cda02af78276b479f299bbf3783ef0a6a6273037d7c60dc73b8d8d7755"}, - {file = "coverage-4.5.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:c0afd27bc0e307a1ffc04ca5ec010a290e49e3afbe841c5cafc5c5a80ecd81c9"}, - {file = "coverage-4.5.4-cp36-cp36m-win32.whl", hash = "sha256:6ba744056423ef8d450cf627289166da65903885272055fb4b5e113137cfa14f"}, - {file = "coverage-4.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:af7ed8a8aa6957aac47b4268631fa1df984643f07ef00acd374e456364b373f5"}, - {file = "coverage-4.5.4-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:3a794ce50daee01c74a494919d5ebdc23d58873747fa0e288318728533a3e1ca"}, - {file = "coverage-4.5.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0be0f1ed45fc0c185cfd4ecc19a1d6532d72f86a2bac9de7e24541febad72650"}, - {file = "coverage-4.5.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:eca2b7343524e7ba246cab8ff00cab47a2d6d54ada3b02772e908a45675722e2"}, - {file = "coverage-4.5.4-cp37-cp37m-win32.whl", hash = "sha256:93715dffbcd0678057f947f496484e906bf9509f5c1c38fc9ba3922893cda5f5"}, - {file = "coverage-4.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:23cc09ed395b03424d1ae30dcc292615c1372bfba7141eb85e11e50efaa6b351"}, - {file = "coverage-4.5.4-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:141f08ed3c4b1847015e2cd62ec06d35e67a3ac185c26f7635f4406b90afa9c5"}, - {file = "coverage-4.5.4.tar.gz", hash = "sha256:e07d9f1a23e9e93ab5c62902833bf3e4b1f65502927379148b6622686223125c"}, + {file = "coverage-5.1-cp27-cp27m-macosx_10_12_x86_64.whl", hash = "sha256:0cb4be7e784dcdc050fc58ef05b71aa8e89b7e6636b99967fadbdba694cf2b65"}, + {file = "coverage-5.1-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:c317eaf5ff46a34305b202e73404f55f7389ef834b8dbf4da09b9b9b37f76dd2"}, + {file = "coverage-5.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b83835506dfc185a319031cf853fa4bb1b3974b1f913f5bb1a0f3d98bdcded04"}, + {file = "coverage-5.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5f2294dbf7875b991c381e3d5af2bcc3494d836affa52b809c91697449d0eda6"}, + {file = "coverage-5.1-cp27-cp27m-win32.whl", hash = "sha256:de807ae933cfb7f0c7d9d981a053772452217df2bf38e7e6267c9cbf9545a796"}, + {file = "coverage-5.1-cp27-cp27m-win_amd64.whl", hash = "sha256:bf9cb9a9fd8891e7efd2d44deb24b86d647394b9705b744ff6f8261e6f29a730"}, + {file = "coverage-5.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:acf3763ed01af8410fc36afea23707d4ea58ba7e86a8ee915dfb9ceff9ef69d0"}, + {file = "coverage-5.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:dec5202bfe6f672d4511086e125db035a52b00f1648d6407cc8e526912c0353a"}, + {file = "coverage-5.1-cp35-cp35m-macosx_10_12_x86_64.whl", hash = "sha256:7a5bdad4edec57b5fb8dae7d3ee58622d626fd3a0be0dfceda162a7035885ecf"}, + {file = "coverage-5.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:1601e480b9b99697a570cea7ef749e88123c04b92d84cedaa01e117436b4a0a9"}, + {file = "coverage-5.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:dbe8c6ae7534b5b024296464f387d57c13caa942f6d8e6e0346f27e509f0f768"}, + {file = "coverage-5.1-cp35-cp35m-win32.whl", hash = "sha256:a027ef0492ede1e03a8054e3c37b8def89a1e3c471482e9f046906ba4f2aafd2"}, + {file = "coverage-5.1-cp35-cp35m-win_amd64.whl", hash = "sha256:0e61d9803d5851849c24f78227939c701ced6704f337cad0a91e0972c51c1ee7"}, + {file = "coverage-5.1-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:2d27a3f742c98e5c6b461ee6ef7287400a1956c11421eb574d843d9ec1f772f0"}, + {file = "coverage-5.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:66460ab1599d3cf894bb6baee8c684788819b71a5dc1e8fa2ecc152e5d752019"}, + {file = "coverage-5.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5c542d1e62eece33c306d66fe0a5c4f7f7b3c08fecc46ead86d7916684b36d6c"}, + {file = "coverage-5.1-cp36-cp36m-win32.whl", hash = "sha256:2742c7515b9eb368718cd091bad1a1b44135cc72468c731302b3d641895b83d1"}, + {file = "coverage-5.1-cp36-cp36m-win_amd64.whl", hash = "sha256:dead2ddede4c7ba6cb3a721870f5141c97dc7d85a079edb4bd8d88c3ad5b20c7"}, + {file = "coverage-5.1-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:01333e1bd22c59713ba8a79f088b3955946e293114479bbfc2e37d522be03355"}, + {file = "coverage-5.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:e1ea316102ea1e1770724db01998d1603ed921c54a86a2efcb03428d5417e489"}, + {file = "coverage-5.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:adeb4c5b608574a3d647011af36f7586811a2c1197c861aedb548dd2453b41cd"}, + {file = "coverage-5.1-cp37-cp37m-win32.whl", hash = "sha256:782caea581a6e9ff75eccda79287daefd1d2631cc09d642b6ee2d6da21fc0a4e"}, + {file = "coverage-5.1-cp37-cp37m-win_amd64.whl", hash = "sha256:00f1d23f4336efc3b311ed0d807feb45098fc86dee1ca13b3d6768cdab187c8a"}, + {file = "coverage-5.1-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:402e1744733df483b93abbf209283898e9f0d67470707e3c7516d84f48524f55"}, + {file = "coverage-5.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:a3f3654d5734a3ece152636aad89f58afc9213c6520062db3978239db122f03c"}, + {file = "coverage-5.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6402bd2fdedabbdb63a316308142597534ea8e1895f4e7d8bf7476c5e8751fef"}, + {file = "coverage-5.1-cp38-cp38-win32.whl", hash = "sha256:8fa0cbc7ecad630e5b0f4f35b0f6ad419246b02bc750de7ac66db92667996d24"}, + {file = "coverage-5.1-cp38-cp38-win_amd64.whl", hash = "sha256:79a3cfd6346ce6c13145731d39db47b7a7b859c0272f02cdb89a3bdcbae233a0"}, + {file = "coverage-5.1-cp39-cp39-win32.whl", hash = "sha256:a82b92b04a23d3c8a581fc049228bafde988abacba397d57ce95fe95e0338ab4"}, + {file = "coverage-5.1-cp39-cp39-win_amd64.whl", hash = "sha256:bb28a7245de68bf29f6fb199545d072d1036a1917dca17a1e75bbb919e14ee8e"}, + {file = "coverage-5.1.tar.gz", hash = "sha256:f90bfc4ad18450c80b024036eaf91e4a246ae287701aaa88eaebebf150868052"}, ] cruft = [ {file = "cruft-1.1.2-py3-none-any.whl", hash = "sha256:3864a6775381a69b36720401a9db1714c20af51190c8378fcd5de4560aeb9282"}, @@ -1446,26 +1517,22 @@ dataclasses = [ {file = "dataclasses-0.6.tar.gz", hash = "sha256:6988bd2b895eef432d562370bb707d540f32f7360ab13da45340101bc2307d84"}, ] decorator = [ - {file = "decorator-4.4.0-py2.py3-none-any.whl", hash = "sha256:f069f3a01830ca754ba5258fde2278454a0b5b79e0d7f5c13b3b97e57d4acff6"}, - {file = "decorator-4.4.0.tar.gz", hash = "sha256:86156361c50488b84a3f148056ea716ca587df2f0de1d34750d35c21312725de"}, + {file = "decorator-4.4.2-py2.py3-none-any.whl", hash = "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760"}, + {file = "decorator-4.4.2.tar.gz", hash = "sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7"}, ] distlib = [ - {file = "distlib-0.2.9.post0.zip", hash = "sha256:ecb3d0e4f71d0fa7f38db6bcc276c7c9a1c6638a516d726495934a553eb3fbe0"}, + {file = "distlib-0.3.0.zip", hash = "sha256:2e166e231a26b36d6dfe35a48c4464346620f8645ed0ace01ee31822b288de21"}, ] docopt = [ {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, ] dparse = [ - {file = "dparse-0.4.1-py2-none-any.whl", hash = "sha256:cef95156fa0adedaf042cd42f9990974bec76f25dfeca4dc01f381a243d5aa5b"}, - {file = "dparse-0.4.1.tar.gz", hash = "sha256:00a5fdfa900629e5159bf3600d44905b333f4059a3366f28e0dbd13eeab17b19"}, -] -entrypoints = [ - {file = "entrypoints-0.3-py2.py3-none-any.whl", hash = "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19"}, - {file = "entrypoints-0.3.tar.gz", hash = "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"}, + {file = "dparse-0.5.1-py3-none-any.whl", hash = "sha256:e953a25e44ebb60a5c6efc2add4420c177f1d8404509da88da9729202f306994"}, + {file = "dparse-0.5.1.tar.gz", hash = "sha256:a1b5f169102e1c894f9a7d5ccf6f9402a836a5d24be80a986c7ce9eaed78f367"}, ] examples = [ - {file = "examples-1.0.1-py3-none-any.whl", hash = "sha256:bf3c16a072e186815a78ebf85177bd1313cdaf34298baf0a8be30957147dd47d"}, - {file = "examples-1.0.1.tar.gz", hash = "sha256:9dba261e3929f3274328beeadcb2212180e12dbdc033eb0f35c3666708055fc4"}, + {file = "examples-1.0.2-py3-none-any.whl", hash = "sha256:372fefd15d5a17bda3b003cf26edbc2d29632bc63f29c816b55ed33dcccb3e65"}, + {file = "examples-1.0.2.tar.gz", hash = "sha256:f29ba443f158bb47913ac21f098306a9749ed459a2290540ff1f86baac074597"}, ] falcon = [ {file = "falcon-2.0.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:733033ec80c896e30a43ab3e776856096836787197a44eb21022320a61311983"}, @@ -1483,13 +1550,9 @@ falcon = [ {file = "falcon-2.0.0-py2.py3-none-any.whl", hash = "sha256:54f2cb4b687035b2a03206dbfc538055cc48b59a953187b0458aa1b574d47b53"}, {file = "falcon-2.0.0.tar.gz", hash = "sha256:eea593cf466b9c126ce667f6d30503624ef24459f118c75594a69353b6c3d5fc"}, ] -first = [ - {file = "first-2.0.2-py2.py3-none-any.whl", hash = "sha256:8d8e46e115ea8ac652c76123c0865e3ff18372aef6f03c22809ceefcea9dec86"}, - {file = "first-2.0.2.tar.gz", hash = "sha256:ff285b08c55f8c97ce4ea7012743af2495c9f1291785f163722bd36f6af6d3bf"}, -] flake8 = [ - {file = "flake8-3.7.8-py2.py3-none-any.whl", hash = "sha256:8e9dfa3cecb2400b3738a42c54c3043e821682b9c840b0448c0503f781130696"}, - {file = "flake8-3.7.8.tar.gz", hash = "sha256:19241c1cbc971b9962473e4438a2ca19749a7dd002dd1a946eaba171b4114548"}, + {file = "flake8-3.8.0-py2.py3-none-any.whl", hash = "sha256:bcf5163890bb01f11f04f0f444f01004d0f9ad5fab10c51104f770acf532008f"}, + {file = "flake8-3.8.0.tar.gz", hash = "sha256:e2f33066fb92ac0a3a30ea509f61e325f2110b2e84644333a3ff8e9e98a2beab"}, ] flake8-bugbear = [ {file = "flake8-bugbear-19.8.0.tar.gz", hash = "sha256:d8c466ea79d5020cb20bf9f11cf349026e09517a42264f313d3f6fddb83e0571"}, @@ -1500,72 +1563,79 @@ flake8-polyfill = [ {file = "flake8_polyfill-1.0.2-py2.py3-none-any.whl", hash = "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9"}, ] future = [ - {file = "future-0.18.1.tar.gz", hash = "sha256:858e38522e8fd0d3ce8f0c1feaf0603358e366d5403209674c7b617fa0c24093"}, + {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, +] +gitdb = [ + {file = "gitdb-4.0.5-py3-none-any.whl", hash = "sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac"}, + {file = "gitdb-4.0.5.tar.gz", hash = "sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9"}, ] gitdb2 = [ - {file = "gitdb2-2.0.6-py2.py3-none-any.whl", hash = "sha256:96bbb507d765a7f51eb802554a9cfe194a174582f772e0d89f4e87288c288b7b"}, - {file = "gitdb2-2.0.6.tar.gz", hash = "sha256:1b6df1433567a51a4a9c1a5a0de977aa351a405cc56d7d35f3388bad1f630350"}, + {file = "gitdb2-4.0.2-py3-none-any.whl", hash = "sha256:a1c974e5fab8c2c90192c1367c81cbc54baec04244bda1816e9c8ab377d1cba3"}, + {file = "gitdb2-4.0.2.tar.gz", hash = "sha256:0986cb4003de743f2b3aba4c828edd1ab58ce98e1c4a8acf72ef02760d4beb4e"}, ] gitpython = [ - {file = "GitPython-3.0.3-py3-none-any.whl", hash = "sha256:6e97b9f0954807f30c2dd8e3165731ed6c477a1b365f194b69d81d7940a08332"}, - {file = "GitPython-3.0.3.tar.gz", hash = "sha256:631263cc670aa56ce3d3c414cf0fe2e840f2e913514b138ea28d88a477bbcd21"}, -] -htmlmin = [ - {file = "htmlmin-0.1.12.tar.gz", hash = "sha256:50c1ef4630374a5d723900096a961cff426dff46b48f34d194a81bbe14eca178"}, + {file = "GitPython-3.1.2-py3-none-any.whl", hash = "sha256:da3b2cf819974789da34f95ac218ef99f515a928685db141327c09b73dd69c09"}, + {file = "GitPython-3.1.2.tar.gz", hash = "sha256:864a47472548f3ba716ca202e034c1900f197c0fb3a08f641c20c3cafd15ed94"}, ] hug = [ - {file = "hug-2.6.0-py2.py3-none-any.whl", hash = "sha256:bbe1390642e324130f60e4f7978bd6ea152d6957ed6e769ad6bc314ddaed5b9b"}, - {file = "hug-2.6.0.tar.gz", hash = "sha256:a04cd002614e8c788d58e5dcd5b932a62e2d2c33d4bd69fd8a3d7ce4e5cf5545"}, + {file = "hug-2.6.1-py2.py3-none-any.whl", hash = "sha256:31c8fc284f81377278629a4b94cbb619ae9ce829cdc2da9564ccc66a121046b4"}, + {file = "hug-2.6.1.tar.gz", hash = "sha256:b0edace2acb618873779c9ce6ecf9165db54fef95c22262f5700fcdd9febaec9"}, ] hypothesis = [ - {file = "hypothesis-4.41.2-py3-none-any.whl", hash = "sha256:acd47600deb55e9c2c98de6deef23384160ed0fdaafb6753146e556c077d3c78"}, - {file = "hypothesis-4.41.2.tar.gz", hash = "sha256:6847df3ffb4aa52798621dd007e6b61dbcf2d76c30ba37dc2699720e2c734b7a"}, + {file = "hypothesis-5.12.1-py3-none-any.whl", hash = "sha256:41a4548876cd24d45ee917809dfd2dc36fd26ffe62330b9c8f837f1be0c92737"}, + {file = "hypothesis-5.12.1.tar.gz", hash = "sha256:b7ddd5f0920eec97c534c3dbce9f96055fc7332107b73b7c75ef07335a47329d"}, ] hypothesis-auto = [ {file = "hypothesis-auto-1.1.4.tar.gz", hash = "sha256:5e2c2fb09dc09842512d80630bb792359a1d33d2c0473ad47ee23da0be9e32b1"}, {file = "hypothesis_auto-1.1.4-py3-none-any.whl", hash = "sha256:fea8560c4522c0fd490ed8cc17e420b95dabebb11b9b334c59bf2d768839015f"}, ] idna = [ - {file = "idna-2.8-py2.py3-none-any.whl", hash = "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"}, - {file = "idna-2.8.tar.gz", hash = "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407"}, + {file = "idna-2.9-py2.py3-none-any.whl", hash = "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"}, + {file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"}, ] importlib-metadata = [ - {file = "importlib_metadata-0.23-py2.py3-none-any.whl", hash = "sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af"}, - {file = "importlib_metadata-0.23.tar.gz", hash = "sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26"}, + {file = "importlib_metadata-1.6.0-py2.py3-none-any.whl", hash = "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f"}, + {file = "importlib_metadata-1.6.0.tar.gz", hash = "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e"}, ] ipython = [ - {file = "ipython-7.8.0-py3-none-any.whl", hash = "sha256:c4ab005921641e40a68e405e286e7a1fcc464497e14d81b6914b4fd95e5dee9b"}, - {file = "ipython-7.8.0.tar.gz", hash = "sha256:dd76831f065f17bddd7eaa5c781f5ea32de5ef217592cf019e34043b56895aa1"}, + {file = "ipython-7.14.0-py3-none-any.whl", hash = "sha256:5b241b84bbf0eb085d43ae9d46adf38a13b45929ca7774a740990c2c242534bb"}, + {file = "ipython-7.14.0.tar.gz", hash = "sha256:f0126781d0f959da852fb3089e170ed807388e986a8dd4e6ac44855845b0fb1c"}, ] ipython-genutils = [ {file = "ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8"}, {file = "ipython_genutils-0.2.0.tar.gz", hash = "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"}, ] jedi = [ - {file = "jedi-0.15.1-py2.py3-none-any.whl", hash = "sha256:786b6c3d80e2f06fd77162a07fed81b8baa22dde5d62896a790a331d6ac21a27"}, - {file = "jedi-0.15.1.tar.gz", hash = "sha256:ba859c74fa3c966a22f2aeebe1b74ee27e2a462f56d3f5f7ca4a59af61bfe42e"}, + {file = "jedi-0.17.0-py2.py3-none-any.whl", hash = "sha256:cd60c93b71944d628ccac47df9a60fec53150de53d42dc10a7fc4b5ba6aae798"}, + {file = "jedi-0.17.0.tar.gz", hash = "sha256:df40c97641cb943661d2db4c33c2e1ff75d491189423249e989bcea4464f3030"}, ] jinja2 = [ - {file = "Jinja2-2.10.3-py2.py3-none-any.whl", hash = "sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f"}, - {file = "Jinja2-2.10.3.tar.gz", hash = "sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"}, + {file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"}, + {file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"}, ] jinja2-time = [ {file = "jinja2-time-0.2.0.tar.gz", hash = "sha256:d14eaa4d315e7688daa4969f616f226614350c48730bfa1692d2caebd8c90d40"}, {file = "jinja2_time-0.2.0-py2.py3-none-any.whl", hash = "sha256:d3eab6605e3ec8b7a0863df09cc1d23714908fa61aa6986a845c20ba488b4efa"}, ] -jsmin = [ - {file = "jsmin-2.2.2.tar.gz", hash = "sha256:b6df99b2cd1c75d9d342e4335b535789b8da9107ec748212706ef7bbe5c2553b"}, +joblib = [ + {file = "joblib-0.14.1-py2.py3-none-any.whl", hash = "sha256:bdb4fd9b72915ffb49fde2229ce482dd7ae79d842ed8c2b4c932441495af1403"}, + {file = "joblib-0.14.1.tar.gz", hash = "sha256:0630eea4f5664c463f23fbf5dcfc54a2bc6168902719fa8e19daf033022786c8"}, ] livereload = [ {file = "livereload-2.6.1-py2.py3-none-any.whl", hash = "sha256:78d55f2c268a8823ba499305dcac64e28ddeb9a92571e12d543cd304faf5817b"}, {file = "livereload-2.6.1.tar.gz", hash = "sha256:89254f78d7529d7ea0a3417d224c34287ebfe266b05e67e51facaf82c27f0f66"}, ] +lunr = [ + {file = "lunr-0.5.6-py2.py3-none-any.whl", hash = "sha256:1208622930c915a07e6f8e8640474357826bad48534c0f57969b6fca9bffc88e"}, + {file = "lunr-0.5.6.tar.gz", hash = "sha256:7be69d7186f65784a4f2adf81e5c58efd6a9921aa95966babcb1f2f2ada75c20"}, +] mako = [ - {file = "Mako-1.1.0.tar.gz", hash = "sha256:a36919599a9b7dc5d86a7a8988f23a9a3a3d083070023bab23d64f7f1d1e0a4b"}, + {file = "Mako-1.1.2-py2.py3-none-any.whl", hash = "sha256:8e8b53c71c7e59f3de716b6832c4e401d903af574f6962edbbbf6ecc2a5fe6c9"}, + {file = "Mako-1.1.2.tar.gz", hash = "sha256:3139c5d64aa5d175dbafb95027057128b5fbd05a40c53999f3905ceb53366d9d"}, ] markdown = [ - {file = "Markdown-3.1.1-py2.py3-none-any.whl", hash = "sha256:56a46ac655704b91e5b7e6326ce43d5ef72411376588afa1dd90e881b83c7e8c"}, - {file = "Markdown-3.1.1.tar.gz", hash = "sha256:2e50876bcdd74517e7b71f3e7a76102050edec255b3983403f1a63e7c8a41e7a"}, + {file = "Markdown-3.2.2-py3-none-any.whl", hash = "sha256:c467cd6233885534bf0fe96e62e3cf46cfc1605112356c4f9981512b8174de59"}, + {file = "Markdown-3.2.2.tar.gz", hash = "sha256:1fafe3f1ecabfb514a5285fca634a53c1b32a81cb0feb154264d55bf2ff22c17"}, ] markupsafe = [ {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, @@ -1607,20 +1677,16 @@ mccabe = [ {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] mkdocs = [ - {file = "mkdocs-1.0.4-py2.py3-none-any.whl", hash = "sha256:8cc8b38325456b9e942c981a209eaeb1e9f3f77b493ad755bfef889b9c8d356a"}, - {file = "mkdocs-1.0.4.tar.gz", hash = "sha256:17d34329aad75d5de604b9ed4e31df3a4d235afefdc46ce7b1964fddb2e1e939"}, + {file = "mkdocs-1.1-py2.py3-none-any.whl", hash = "sha256:1e385a70aea8a9dedb731aea4fd5f3704b2074801c4f96f06b2920999babda8a"}, + {file = "mkdocs-1.1.tar.gz", hash = "sha256:9243291392f59e20b655e4e46210233453faf97787c2cf72176510e868143174"}, ] mkdocs-material = [ - {file = "mkdocs-material-4.4.3.tar.gz", hash = "sha256:e4a9ac73db7c65fdae1dbd248091e4b0a3f5db3e6bf87a46bb457db013a045e4"}, - {file = "mkdocs_material-4.4.3-py2.py3-none-any.whl", hash = "sha256:a5246b550299d00a135a3f739e70ac6db73d7127480f0fecbda113d0095a674a"}, -] -mkdocs-minify-plugin = [ - {file = "mkdocs-minify-plugin-0.2.1.tar.gz", hash = "sha256:3000a5069dd0f42f56a8aaf7fd5ea1222c67487949617e39585d6b6434b074b6"}, - {file = "mkdocs_minify_plugin-0.2.1-py2-none-any.whl", hash = "sha256:d54fdd5be6843dd29fd7af2f7fdd20a9eb4db46f1f6bed914e03b2f58d2d488e"}, + {file = "mkdocs-material-4.6.3.tar.gz", hash = "sha256:1d486635b03f5a2ec87325842f7b10c7ae7daa0eef76b185572eece6a6ea212c"}, + {file = "mkdocs_material-4.6.3-py2.py3-none-any.whl", hash = "sha256:7f3afa0a09c07d0b89a6a9755fdb00513aee8f0cec3538bb903325c80f66f444"}, ] more-itertools = [ - {file = "more-itertools-7.2.0.tar.gz", hash = "sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832"}, - {file = "more_itertools-7.2.0-py3-none-any.whl", hash = "sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4"}, + {file = "more-itertools-8.2.0.tar.gz", hash = "sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507"}, + {file = "more_itertools-8.2.0-py3-none-any.whl", hash = "sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c"}, ] mypy = [ {file = "mypy-0.761-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:7f672d02fffcbace4db2b05369142e0506cdcde20cea0e07c7c2171c4fd11dd6"}, @@ -1642,64 +1708,63 @@ mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] +nltk = [ + {file = "nltk-3.5.zip", hash = "sha256:845365449cd8c5f9731f7cb9f8bd6fd0767553b9d53af9eb1b3abf7700936b35"}, +] numpy = [ - {file = "numpy-1.17.3-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:4dd830a11e8724c9c9379feed1d1be43113f8bcce55f47ea7186d3946769ce26"}, - {file = "numpy-1.17.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:30c84e3a62cfcb9e3066f25226e131451312a044f1fe2040e69ce792cb7de418"}, - {file = "numpy-1.17.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9395b0a41e8b7e9a284e3be7060db9d14ad80273841c952c83a5afc241d2bd98"}, - {file = "numpy-1.17.3-cp35-cp35m-win32.whl", hash = "sha256:9e37c35fc4e9410093b04a77d11a34c64bf658565e30df7cbe882056088a91c1"}, - {file = "numpy-1.17.3-cp35-cp35m-win_amd64.whl", hash = "sha256:de2b1c20494bdf47f0160bd88ed05f5e48ae5dc336b8de7cfade71abcc95c0b9"}, - {file = "numpy-1.17.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:669795516d62f38845c7033679c648903200980d68935baaa17ac5c7ae03ae0c"}, - {file = "numpy-1.17.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:4650d94bb9c947151737ee022b934b7d9a845a7c76e476f3e460f09a0c8c6f39"}, - {file = "numpy-1.17.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4f2a2b279efde194877aff1f76cf61c68e840db242a5c7169f1ff0fd59a2b1e2"}, - {file = "numpy-1.17.3-cp36-cp36m-win32.whl", hash = "sha256:ffca69e29079f7880c5392bf675eb8b4146479d976ae1924d01cd92b04cccbcc"}, - {file = "numpy-1.17.3-cp36-cp36m-win_amd64.whl", hash = "sha256:2e418f0a59473dac424f888dd57e85f77502a593b207809211c76e5396ae4f5c"}, - {file = "numpy-1.17.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:75fcd60d682db3e1f8fbe2b8b0c6761937ad56d01c1dc73edf4ef2748d5b6bc4"}, - {file = "numpy-1.17.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:28b1180c758abf34a5c3fea76fcee66a87def1656724c42bb14a6f9717a5bdf7"}, - {file = "numpy-1.17.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:dd0667f5be56fb1b570154c2c0516a528e02d50da121bbbb2cbb0b6f87f59bc2"}, - {file = "numpy-1.17.3-cp37-cp37m-win32.whl", hash = "sha256:25ffe71f96878e1da7e014467e19e7db90ae7d4e12affbc73101bcf61785214e"}, - {file = "numpy-1.17.3-cp37-cp37m-win_amd64.whl", hash = "sha256:0b0dd8f47fb177d00fa6ef2d58783c4f41ad3126b139c91dd2f7c4b3fdf5e9a5"}, - {file = "numpy-1.17.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:62d22566b3e3428dfc9ec972014c38ed9a4db4f8969c78f5414012ccd80a149e"}, - {file = "numpy-1.17.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:26efd7f7d755e6ca966a5c0ac5a930a87dbbaab1c51716ac26a38f42ecc9bc4b"}, - {file = "numpy-1.17.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:b46554ad4dafb2927f88de5a1d207398c5385edbb5c84d30b3ef187c4a3894d8"}, - {file = "numpy-1.17.3-cp38-cp38-win32.whl", hash = "sha256:c867eeccd934920a800f65c6068acdd6b87e80d45cd8c8beefff783b23cdc462"}, - {file = "numpy-1.17.3-cp38-cp38-win_amd64.whl", hash = "sha256:f1df7b2b7740dd777571c732f98adb5aad5450aee32772f1b39249c8a50386f6"}, - {file = "numpy-1.17.3.zip", hash = "sha256:a0678793096205a4d784bd99f32803ba8100f639cf3b932dc63b21621390ea7e"}, + {file = "numpy-1.18.4-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:efdba339fffb0e80fcc19524e4fdbda2e2b5772ea46720c44eaac28096d60720"}, + {file = "numpy-1.18.4-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2b573fcf6f9863ce746e4ad00ac18a948978bb3781cffa4305134d31801f3e26"}, + {file = "numpy-1.18.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:3f0dae97e1126f529ebb66f3c63514a0f72a177b90d56e4bce8a0b5def34627a"}, + {file = "numpy-1.18.4-cp35-cp35m-win32.whl", hash = "sha256:dccd380d8e025c867ddcb2f84b439722cf1f23f3a319381eac45fd077dee7170"}, + {file = "numpy-1.18.4-cp35-cp35m-win_amd64.whl", hash = "sha256:02ec9582808c4e48be4e93cd629c855e644882faf704bc2bd6bbf58c08a2a897"}, + {file = "numpy-1.18.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:904b513ab8fbcbdb062bed1ce2f794ab20208a1b01ce9bd90776c6c7e7257032"}, + {file = "numpy-1.18.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:e22cd0f72fc931d6abc69dc7764484ee20c6a60b0d0fee9ce0426029b1c1bdae"}, + {file = "numpy-1.18.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2466fbcf23711ebc5daa61d28ced319a6159b260a18839993d871096d66b93f7"}, + {file = "numpy-1.18.4-cp36-cp36m-win32.whl", hash = "sha256:00d7b54c025601e28f468953d065b9b121ddca7fff30bed7be082d3656dd798d"}, + {file = "numpy-1.18.4-cp36-cp36m-win_amd64.whl", hash = "sha256:7d59f21e43bbfd9a10953a7e26b35b6849d888fc5a331fa84a2d9c37bd9fe2a2"}, + {file = "numpy-1.18.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:efb7ac5572c9a57159cf92c508aad9f856f1cb8e8302d7fdb99061dbe52d712c"}, + {file = "numpy-1.18.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0e6f72f7bb08f2f350ed4408bb7acdc0daba637e73bce9f5ea2b207039f3af88"}, + {file = "numpy-1.18.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:9933b81fecbe935e6a7dc89cbd2b99fea1bf362f2790daf9422a7bb1dc3c3085"}, + {file = "numpy-1.18.4-cp37-cp37m-win32.whl", hash = "sha256:96dd36f5cdde152fd6977d1bbc0f0561bccffecfde63cd397c8e6033eb66baba"}, + {file = "numpy-1.18.4-cp37-cp37m-win_amd64.whl", hash = "sha256:57aea170fb23b1fd54fa537359d90d383d9bf5937ee54ae8045a723caa5e0961"}, + {file = "numpy-1.18.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ed722aefb0ebffd10b32e67f48e8ac4c5c4cf5d3a785024fdf0e9eb17529cd9d"}, + {file = "numpy-1.18.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:50fb72bcbc2cf11e066579cb53c4ca8ac0227abb512b6cbc1faa02d1595a2a5d"}, + {file = "numpy-1.18.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:709c2999b6bd36cdaf85cf888d8512da7433529f14a3689d6e37ab5242e7add5"}, + {file = "numpy-1.18.4-cp38-cp38-win32.whl", hash = "sha256:f22273dd6a403ed870207b853a856ff6327d5cbce7a835dfa0645b3fc00273ec"}, + {file = "numpy-1.18.4-cp38-cp38-win_amd64.whl", hash = "sha256:1be2e96314a66f5f1ce7764274327fd4fb9da58584eaff00b5a5221edefee7d6"}, + {file = "numpy-1.18.4.zip", hash = "sha256:bbcc85aaf4cd84ba057decaead058f43191cc0e30d6bc5d44fe336dc3d3f4509"}, ] orderedmultidict = [ {file = "orderedmultidict-1.0.1-py2.py3-none-any.whl", hash = "sha256:43c839a17ee3cdd62234c47deca1a8508a3f2ca1d0678a3bf791c87cf84adbf3"}, {file = "orderedmultidict-1.0.1.tar.gz", hash = "sha256:04070bbb5e87291cc9bfa51df413677faf2141c73c61d2a5f7b26bea3cd882ad"}, ] packaging = [ - {file = "packaging-19.2-py2.py3-none-any.whl", hash = "sha256:d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108"}, - {file = "packaging-19.2.tar.gz", hash = "sha256:28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47"}, + {file = "packaging-20.3-py2.py3-none-any.whl", hash = "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752"}, + {file = "packaging-20.3.tar.gz", hash = "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3"}, ] parso = [ - {file = "parso-0.5.1-py2.py3-none-any.whl", hash = "sha256:63854233e1fadb5da97f2744b6b24346d2750b85965e7e399bec1620232797dc"}, - {file = "parso-0.5.1.tar.gz", hash = "sha256:666b0ee4a7a1220f65d367617f2cd3ffddff3e205f3f16a0284df30e774c2a9c"}, + {file = "parso-0.7.0-py2.py3-none-any.whl", hash = "sha256:158c140fc04112dc45bca311633ae5033c2c2a7b732fa33d0955bad8152a8dd0"}, + {file = "parso-0.7.0.tar.gz", hash = "sha256:908e9fae2144a076d72ae4e25539143d40b8e3eafbaeae03c1bfe226f4cdf12c"}, ] pbr = [ - {file = "pbr-5.4.3-py2.py3-none-any.whl", hash = "sha256:b32c8ccaac7b1a20c0ce00ce317642e6cf231cf038f9875e0280e28af5bf7ac9"}, - {file = "pbr-5.4.3.tar.gz", hash = "sha256:2c8e420cd4ed4cec4e7999ee47409e876af575d4c35a45840d59e8b5f3155ab8"}, + {file = "pbr-5.4.5-py2.py3-none-any.whl", hash = "sha256:579170e23f8e0c2f24b0de612f71f648eccb79fb1322c814ae6b3c07b5ba23e8"}, + {file = "pbr-5.4.5.tar.gz", hash = "sha256:07f558fece33b05caf857474a366dfcc00562bca13dd8b47b2b3e22d9f9bf55c"}, ] pdocs = [ {file = "pdocs-1.0.1-py3-none-any.whl", hash = "sha256:9c0d24fdc0e0c537be8f2418edb4f1075da46a0d749b17ea20b74e7e60124f49"}, {file = "pdocs-1.0.1.tar.gz", hash = "sha256:23a0346f56c08ab5701ca9b14630aa1f0f32f1c29ee7efcfc8bc512cd272f89b"}, ] pep517 = [ - {file = "pep517-0.7.0-py2.py3-none-any.whl", hash = "sha256:fac83aa4c3b73adc84cb2a295f1f5bd5b9a13946ebd1339ba3b33ce287165c88"}, - {file = "pep517-0.7.0.tar.gz", hash = "sha256:d283181fdb83fb698556cd3a4ebde5bb352b59242574e6ca56a95118774da374"}, -] -pep562 = [ - {file = "pep562-1.0-py2.py3-none-any.whl", hash = "sha256:d2a48b178ebf5f8dd31709cc26a19808ef794561fa2fe50ea01ea2bad4d667ef"}, - {file = "pep562-1.0.tar.gz", hash = "sha256:58cb1cc9ee63d93e62b4905a50357618d526d289919814bea1f0da8f53b79395"}, + {file = "pep517-0.8.2-py2.py3-none-any.whl", hash = "sha256:576c480be81f3e1a70a16182c762311eb80d1f8a7b0d11971e5234967d7a342c"}, + {file = "pep517-0.8.2.tar.gz", hash = "sha256:8e6199cf1288d48a0c44057f112acf18aa5ebabbf73faa242f598fbe145ba29e"}, ] pep8-naming = [ {file = "pep8-naming-0.8.2.tar.gz", hash = "sha256:01cb1dab2f3ce9045133d08449f1b6b93531dceacb9ef04f67087c11c723cea9"}, {file = "pep8_naming-0.8.2-py2.py3-none-any.whl", hash = "sha256:0ec891e59eea766efd3059c3d81f1da304d858220678bdc351aab73c533f2fbb"}, ] pexpect = [ - {file = "pexpect-4.7.0-py2.py3-none-any.whl", hash = "sha256:2094eefdfcf37a1fdbfb9aa090862c1a4878e5c7e0e7e7088bdb511c558e5cd1"}, - {file = "pexpect-4.7.0.tar.gz", hash = "sha256:9e2c1fd0e6ee3a49b28f95d4b33bc389c89b20af6a1255906e90ff1262ce62eb"}, + {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, + {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, ] pickleshare = [ {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, @@ -1717,16 +1782,16 @@ pipfile = [ {file = "pipfile-0.0.2.tar.gz", hash = "sha256:f7d9f15de8b660986557eb3cc5391aa1a16207ac41bc378d03f414762d36c984"}, ] pipreqs = [ - {file = "pipreqs-0.4.9-py2.py3-none-any.whl", hash = "sha256:2dfa21631cb68a97515e222d6f0033b5bfb75823567ca2195d528efb18c97990"}, - {file = "pipreqs-0.4.9.tar.gz", hash = "sha256:cec6eecc4685967b27eb386037565a737d036045f525b9eb314631a68d60e4bc"}, + {file = "pipreqs-0.4.10-py2.py3-none-any.whl", hash = "sha256:cafe42ab70628d408c147fb8944bc303355ea8f91fddca4a98d273e572e39905"}, + {file = "pipreqs-0.4.10.tar.gz", hash = "sha256:9e351d644b28b98d7386b046a73806cbb3bb66b23a30e74feeb95ed9571db939"}, ] plette = [ - {file = "plette-0.2.2-py2.py3-none-any.whl", hash = "sha256:c0e3553c1e581d8423daccbd825789c6e7f29b7d9e00e5331b12e1642a1a26d3"}, - {file = "plette-0.2.2.tar.gz", hash = "sha256:dde5d525cf5f0cbad4d938c83b93db17887918daf63c13eafed257c4f61b07b4"}, + {file = "plette-0.2.3-py2.py3-none-any.whl", hash = "sha256:d6c9b96981b347bddd333910b753b6091a2c1eb2ef85bb373b4a67c9d91dca16"}, + {file = "plette-0.2.3.tar.gz", hash = "sha256:46402c03e36d6eadddad2a5125990e322dd74f98160c8f2dcd832b2291858a26"}, ] pluggy = [ - {file = "pluggy-0.13.0-py2.py3-none-any.whl", hash = "sha256:0db4b7601aae1d35b4a033282da476845aa19185c1e6964b25cf324b5e4ec3e6"}, - {file = "pluggy-0.13.0.tar.gz", hash = "sha256:fa5fa1622fa6dd5c030e9cad086fa19ef6a0cf6d7a2d12318e10cb49d6d68f34"}, + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, ] portray = [ {file = "portray-1.3.1-py3-none-any.whl", hash = "sha256:ac1a651f97ab04556732b64fdb10dcba4f9a006dd023471039743b16d28a7a61"}, @@ -1737,69 +1802,82 @@ poyo = [ {file = "poyo-0.5.0.tar.gz", hash = "sha256:e26956aa780c45f011ca9886f044590e2d8fd8b61db7b1c1cf4e0869f48ed4dd"}, ] prompt-toolkit = [ - {file = "prompt_toolkit-2.0.10-py2-none-any.whl", hash = "sha256:e7f8af9e3d70f514373bf41aa51bc33af12a6db3f71461ea47fea985defb2c31"}, - {file = "prompt_toolkit-2.0.10-py3-none-any.whl", hash = "sha256:46642344ce457641f28fc9d1c9ca939b63dadf8df128b86f1b9860e59c73a5e4"}, - {file = "prompt_toolkit-2.0.10.tar.gz", hash = "sha256:f15af68f66e664eaa559d4ac8a928111eebd5feda0c11738b5998045224829db"}, + {file = "prompt_toolkit-3.0.3-py3-none-any.whl", hash = "sha256:c93e53af97f630f12f5f62a3274e79527936ed466f038953dfa379d4941f651a"}, + {file = "prompt_toolkit-3.0.3.tar.gz", hash = "sha256:a402e9bf468b63314e37460b68ba68243d55b2f8c4d0192f85a019af3945050e"}, ] ptyprocess = [ {file = "ptyprocess-0.6.0-py2.py3-none-any.whl", hash = "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"}, {file = "ptyprocess-0.6.0.tar.gz", hash = "sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0"}, ] py = [ - {file = "py-1.8.0-py2.py3-none-any.whl", hash = "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa"}, - {file = "py-1.8.0.tar.gz", hash = "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"}, + {file = "py-1.8.1-py2.py3-none-any.whl", hash = "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"}, + {file = "py-1.8.1.tar.gz", hash = "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa"}, ] pycodestyle = [ - {file = "pycodestyle-2.5.0-py2.py3-none-any.whl", hash = "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56"}, - {file = "pycodestyle-2.5.0.tar.gz", hash = "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"}, + {file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"}, + {file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"}, ] pydantic = [ - {file = "pydantic-0.32.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:18598557f0d9ab46173045910ed50458c4fb4d16153c23346b504d7a5b679f77"}, - {file = "pydantic-0.32.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ede2d65ae33788d4e26e12b330b4a32c53cb14131c65bca3a59f037c73f6ee7a"}, - {file = "pydantic-0.32.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:bc6c6a78647d7a65a493e1107572d993f26a652c49183201e3c7d23924bf7311"}, - {file = "pydantic-0.32.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:6f5eb88fe4c21380aa064b7d249763fc6306f0b001d7e7d52d80866d1afc9ed3"}, - {file = "pydantic-0.32.2-py36.py37.py38-none-any.whl", hash = "sha256:e1a63b4e6bf8820833cb6fa239ffbe8eec57ccdd7d66359eff20e68a83c1deeb"}, - {file = "pydantic-0.32.2.tar.gz", hash = "sha256:6a9335c968e13295430a208487e74d69fef40168b72dea8d975765d14e2da660"}, + {file = "pydantic-1.5.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2a6904e9f18dea58f76f16b95cba6a2f20b72d787abd84ecd67ebc526e61dce6"}, + {file = "pydantic-1.5.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:da8099fca5ee339d5572cfa8af12cf0856ae993406f0b1eb9bb38c8a660e7416"}, + {file = "pydantic-1.5.1-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:68dece67bff2b3a5cc188258e46b49f676a722304f1c6148ae08e9291e284d98"}, + {file = "pydantic-1.5.1-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:ab863853cb502480b118187d670f753be65ec144e1654924bec33d63bc8b3ce2"}, + {file = "pydantic-1.5.1-cp36-cp36m-win_amd64.whl", hash = "sha256:2007eb062ed0e57875ce8ead12760a6e44bf5836e6a1a7ea81d71eeecf3ede0f"}, + {file = "pydantic-1.5.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:20a15a303ce1e4d831b4e79c17a4a29cb6740b12524f5bba3ea363bff65732bc"}, + {file = "pydantic-1.5.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:473101121b1bd454c8effc9fe66d54812fdc128184d9015c5aaa0d4e58a6d338"}, + {file = "pydantic-1.5.1-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:9be755919258d5d168aeffbe913ed6e8bd562e018df7724b68cabdee3371e331"}, + {file = "pydantic-1.5.1-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:b96ce81c4b5ca62ab81181212edfd057beaa41411cd9700fbcb48a6ba6564b4e"}, + {file = "pydantic-1.5.1-cp37-cp37m-win_amd64.whl", hash = "sha256:93b9f265329d9827f39f0fca68f5d72cc8321881cdc519a1304fa73b9f8a75bd"}, + {file = "pydantic-1.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e2c753d355126ddd1eefeb167fa61c7037ecd30b98e7ebecdc0d1da463b4ea09"}, + {file = "pydantic-1.5.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:8433dbb87246c0f562af75d00fa80155b74e4f6924b0db6a2078a3cd2f11c6c4"}, + {file = "pydantic-1.5.1-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:0a1cdf24e567d42dc762d3fed399bd211a13db2e8462af9dfa93b34c41648efb"}, + {file = "pydantic-1.5.1-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:8be325fc9da897029ee48d1b5e40df817d97fe969f3ac3fd2434ba7e198c55d5"}, + {file = "pydantic-1.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:3714a4056f5bdbecf3a41e0706ec9b228c9513eee2ad884dc2c568c4dfa540e9"}, + {file = "pydantic-1.5.1-py36.py37.py38-none-any.whl", hash = "sha256:70f27d2f0268f490fe3de0a9b6fca7b7492b8fd6623f9fecd25b221ebee385e3"}, + {file = "pydantic-1.5.1.tar.gz", hash = "sha256:f0018613c7a0d19df3240c2a913849786f21b6539b9f23d85ce4067489dfacfa"}, ] pydocstyle = [ {file = "pydocstyle-5.0.2-py3-none-any.whl", hash = "sha256:da7831660b7355307b32778c4a0dbfb137d89254ef31a2b2978f50fc0b4d7586"}, {file = "pydocstyle-5.0.2.tar.gz", hash = "sha256:f4f5d210610c2d153fae39093d44224c17429e2ad7da12a8b419aba5c2f614b5"}, ] pyflakes = [ - {file = "pyflakes-2.1.1-py2.py3-none-any.whl", hash = "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0"}, - {file = "pyflakes-2.1.1.tar.gz", hash = "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"}, + {file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"}, + {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"}, ] pygments = [ - {file = "Pygments-2.4.2-py2.py3-none-any.whl", hash = "sha256:71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127"}, - {file = "Pygments-2.4.2.tar.gz", hash = "sha256:881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297"}, + {file = "Pygments-2.6.1-py3-none-any.whl", hash = "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324"}, + {file = "Pygments-2.6.1.tar.gz", hash = "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44"}, ] pylama = [ {file = "pylama-7.7.1-py2.py3-none-any.whl", hash = "sha256:fd61c11872d6256b019ef1235be37b77c922ef37ac9797df6bd489996dddeb15"}, {file = "pylama-7.7.1.tar.gz", hash = "sha256:9bae53ef9c1a431371d6a8dca406816a60d547147b60a4934721898f553b7d8f"}, ] pymdown-extensions = [ - {file = "pymdown-extensions-6.1.tar.gz", hash = "sha256:960486dea995f1759dfd517aa140b3d851cd7b44d4c48d276fd2c74fc4e1bce9"}, - {file = "pymdown_extensions-6.1-py2.py3-none-any.whl", hash = "sha256:24c1a0afbae101c4e2b2675ff4dd936470a90133f93398b9cbe0c855e2d2ec10"}, + {file = "pymdown-extensions-6.3.tar.gz", hash = "sha256:cb879686a586b22292899771f5e5bc3382808e92aa938f71b550ecdea709419f"}, + {file = "pymdown_extensions-6.3-py2.py3-none-any.whl", hash = "sha256:66fae2683c7a1dac53184f7de57f51f8dad73f9ead2f453e94e85096cb811335"}, ] pyparsing = [ - {file = "pyparsing-2.4.2-py2.py3-none-any.whl", hash = "sha256:d9338df12903bbf5d65a0e4e87c2161968b10d2e489652bb47001d82a9b028b4"}, - {file = "pyparsing-2.4.2.tar.gz", hash = "sha256:6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80"}, + {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, + {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, ] pytest = [ - {file = "pytest-5.2.1-py3-none-any.whl", hash = "sha256:7e4800063ccfc306a53c461442526c5571e1462f61583506ce97e4da6a1d88c8"}, - {file = "pytest-5.2.1.tar.gz", hash = "sha256:ca563435f4941d0cb34767301c27bc65c510cb82e90b9ecf9cb52dc2c63caaa0"}, + {file = "pytest-5.4.2-py3-none-any.whl", hash = "sha256:95c710d0a72d91c13fae35dce195633c929c3792f54125919847fdcdf7caa0d3"}, + {file = "pytest-5.4.2.tar.gz", hash = "sha256:eb2b5e935f6a019317e455b6da83dd8650ac9ffd2ee73a7b657a30873d67a698"}, ] pytest-cov = [ {file = "pytest-cov-2.8.1.tar.gz", hash = "sha256:cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b"}, {file = "pytest_cov-2.8.1-py2.py3-none-any.whl", hash = "sha256:cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626"}, ] pytest-mock = [ - {file = "pytest-mock-1.11.1.tar.gz", hash = "sha256:f1ab8aefe795204efe7a015900296d1719e7bf0f4a0558d71e8599da1d1309d0"}, - {file = "pytest_mock-1.11.1-py2.py3-none-any.whl", hash = "sha256:34520283d459cdf1d0dbb58a132df804697f1b966ecedf808bbf3d255af8f659"}, + {file = "pytest-mock-1.13.0.tar.gz", hash = "sha256:e24a911ec96773022ebcc7030059b57cd3480b56d4f5d19b7c370ec635e6aed5"}, + {file = "pytest_mock-1.13.0-py2.py3-none-any.whl", hash = "sha256:67e414b3caef7bff6fc6bd83b22b5bc39147e4493f483c2679bc9d4dc485a94d"}, ] python-dateutil = [ - {file = "python-dateutil-2.8.0.tar.gz", hash = "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e"}, - {file = "python_dateutil-2.8.0-py2.py3-none-any.whl", hash = "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb"}, + {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, + {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, +] +python-slugify = [ + {file = "python-slugify-4.0.0.tar.gz", hash = "sha256:a8fc3433821140e8f409a9831d13ae5deccd0b033d4744d94b31fea141bdd84c"}, ] pyyaml = [ {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, @@ -1814,33 +1892,68 @@ pyyaml = [ {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"}, {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, ] +regex = [ + {file = "regex-2020.5.7-cp27-cp27m-win32.whl", hash = "sha256:5493a02c1882d2acaaf17be81a3b65408ff541c922bfd002535c5f148aa29f74"}, + {file = "regex-2020.5.7-cp27-cp27m-win_amd64.whl", hash = "sha256:021a0ae4d2baeeb60a3014805a2096cb329bd6d9f30669b7ad0da51a9cb73349"}, + {file = "regex-2020.5.7-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:4df91094ced6f53e71f695c909d9bad1cca8761d96fd9f23db12245b5521136e"}, + {file = "regex-2020.5.7-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:7ce4a213a96d6c25eeae2f7d60d4dad89ac2b8134ec3e69db9bc522e2c0f9388"}, + {file = "regex-2020.5.7-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:3b059e2476b327b9794c792c855aa05531a3f3044737e455d283c7539bd7534d"}, + {file = "regex-2020.5.7-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:652ab4836cd5531d64a34403c00ada4077bb91112e8bcdae933e2eae232cf4a8"}, + {file = "regex-2020.5.7-cp36-cp36m-win32.whl", hash = "sha256:1e2255ae938a36e9bd7db3b93618796d90c07e5f64dd6a6750c55f51f8b76918"}, + {file = "regex-2020.5.7-cp36-cp36m-win_amd64.whl", hash = "sha256:8127ca2bf9539d6a64d03686fd9e789e8c194fc19af49b69b081f8c7e6ecb1bc"}, + {file = "regex-2020.5.7-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f7f2f4226db6acd1da228adf433c5c3792858474e49d80668ea82ac87cf74a03"}, + {file = "regex-2020.5.7-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2bc6a17a7fa8afd33c02d51b6f417fc271538990297167f68a98cae1c9e5c945"}, + {file = "regex-2020.5.7-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:b7c9f65524ff06bf70c945cd8d8d1fd90853e27ccf86026af2afb4d9a63d06b1"}, + {file = "regex-2020.5.7-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:fa09da4af4e5b15c0e8b4986a083f3fd159302ea115a6cc0649cd163435538b8"}, + {file = "regex-2020.5.7-cp37-cp37m-win32.whl", hash = "sha256:669a8d46764a09f198f2e91fc0d5acdac8e6b620376757a04682846ae28879c4"}, + {file = "regex-2020.5.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b5b5b2e95f761a88d4c93691716ce01dc55f288a153face1654f868a8034f494"}, + {file = "regex-2020.5.7-cp38-cp38-manylinux1_i686.whl", hash = "sha256:0ff50843535593ee93acab662663cb2f52af8e31c3f525f630f1dc6156247938"}, + {file = "regex-2020.5.7-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:1b17bf37c2aefc4cac8436971fe6ee52542ae4225cfc7762017f7e97a63ca998"}, + {file = "regex-2020.5.7-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:04d6e948ef34d3eac133bedc0098364a9e635a7914f050edb61272d2ddae3608"}, + {file = "regex-2020.5.7-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:5b741ecc3ad3e463d2ba32dce512b412c319993c1bb3d999be49e6092a769fb2"}, + {file = "regex-2020.5.7-cp38-cp38-win32.whl", hash = "sha256:099568b372bda492be09c4f291b398475587d49937c659824f891182df728cdf"}, + {file = "regex-2020.5.7-cp38-cp38-win_amd64.whl", hash = "sha256:3ab5e41c4ed7cd4fa426c50add2892eb0f04ae4e73162155cd668257d02259dd"}, + {file = "regex-2020.5.7.tar.gz", hash = "sha256:73a10404867b835f1b8a64253e4621908f0d71150eb4e97ab2e7e441b53e9451"}, +] requests = [ - {file = "requests-2.22.0-py2.py3-none-any.whl", hash = "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"}, - {file = "requests-2.22.0.tar.gz", hash = "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4"}, + {file = "requests-2.23.0-py2.py3-none-any.whl", hash = "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee"}, + {file = "requests-2.23.0.tar.gz", hash = "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"}, ] requirementslib = [ - {file = "requirementslib-1.5.3-py2.py3-none-any.whl", hash = "sha256:8b594ab8b6280ee97cffd68fc766333345de150124d5b76061dd575c3a21fe5a"}, - {file = "requirementslib-1.5.3.tar.gz", hash = "sha256:50731ac1052473e4c7df59a44a1f3aa20f32e687110bc05d73c3b4109eebc23d"}, + {file = "requirementslib-1.5.7-py2.py3-none-any.whl", hash = "sha256:b9989e4815ada8ed71f5d4059e4e6be6f864fb57de744c04ac3d0c744df52304"}, + {file = "requirementslib-1.5.7.tar.gz", hash = "sha256:4999223a26504e0a3cedf9b58def69eae3a93d39db945a85e2135e0239e28fa8"}, ] safety = [ - {file = "safety-1.8.5-py2.py3-none-any.whl", hash = "sha256:0a3a8a178a9c96242b224f033ee8d1d130c0448b0e6622d12deaf37f6c3b4e59"}, - {file = "safety-1.8.5.tar.gz", hash = "sha256:5059f3ffab3648330548ea9c7403405bbfaf085b11235770825d14c58f24cb78"}, + {file = "safety-1.9.0-py2.py3-none-any.whl", hash = "sha256:86c1c4a031fe35bd624fce143fbe642a0234d29f7cbf7a9aa269f244a955b087"}, + {file = "safety-1.9.0.tar.gz", hash = "sha256:23bf20690d4400edc795836b0c983c2b4cbbb922233108ff925b7dd7750f00c9"}, ] six = [ - {file = "six-1.12.0-py2.py3-none-any.whl", hash = "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c"}, - {file = "six-1.12.0.tar.gz", hash = "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"}, + {file = "six-1.14.0-py2.py3-none-any.whl", hash = "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"}, + {file = "six-1.14.0.tar.gz", hash = "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a"}, +] +smmap = [ + {file = "smmap-3.0.4-py2.py3-none-any.whl", hash = "sha256:54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4"}, + {file = "smmap-3.0.4.tar.gz", hash = "sha256:9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24"}, ] smmap2 = [ - {file = "smmap2-2.0.5-py2.py3-none-any.whl", hash = "sha256:0555a7bf4df71d1ef4218e4807bbf9b201f910174e6e08af2e138d4e517b4dde"}, - {file = "smmap2-2.0.5.tar.gz", hash = "sha256:29a9ffa0497e7f2be94ca0ed1ca1aa3cd4cf25a1f6b4f5f87f74b46ed91d609a"}, + {file = "smmap2-3.0.1-py3-none-any.whl", hash = "sha256:0cb6ea470b1ad9a65a02ca7f4c7ae601861f7dd24a43812ca51cfca2892bb524"}, + {file = "smmap2-3.0.1.tar.gz", hash = "sha256:44cc8bdaf96442dbb9a8e2e14377d074b3d0eea292eee3c95c8c449b6c92c557"}, ] snowballstemmer = [ {file = "snowballstemmer-2.0.0-py2.py3-none-any.whl", hash = "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0"}, {file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"}, ] +sortedcontainers = [ + {file = "sortedcontainers-2.1.0-py2.py3-none-any.whl", hash = "sha256:d9e96492dd51fae31e60837736b38fe42a187b5404c16606ff7ee7cd582d4c60"}, + {file = "sortedcontainers-2.1.0.tar.gz", hash = "sha256:974e9a32f56b17c1bac2aebd9dcf197f3eb9cd30553c5852a3187ad162e1a03a"}, +] stevedore = [ - {file = "stevedore-1.31.0-py2.py3-none-any.whl", hash = "sha256:01d9f4beecf0fbd070ddb18e5efb10567801ba7ef3ddab0074f54e3cd4e91730"}, - {file = "stevedore-1.31.0.tar.gz", hash = "sha256:e0739f9739a681c7a1fda76a102b65295e96a144ccdb552f2ae03c5f0abe8a14"}, + {file = "stevedore-1.32.0-py2.py3-none-any.whl", hash = "sha256:a4e7dc759fb0f2e3e2f7d8ffe2358c19d45b9b8297f393ef1256858d82f69c9b"}, + {file = "stevedore-1.32.0.tar.gz", hash = "sha256:18afaf1d623af5950cc0f7e75e70f917784c73b652a34a12d90b309451b5500b"}, +] +text-unidecode = [ + {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, + {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, ] toml = [ {file = "toml-0.10.0-py2.7.egg", hash = "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"}, @@ -1848,77 +1961,75 @@ toml = [ {file = "toml-0.10.0.tar.gz", hash = "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c"}, ] tomlkit = [ - {file = "tomlkit-0.5.3-py2.py3-none-any.whl", hash = "sha256:f077456d35303e7908cc233b340f71e0bec96f63429997f38ca9272b7d64029e"}, - {file = "tomlkit-0.5.3.tar.gz", hash = "sha256:d6506342615d051bc961f70bfcfa3d29b6616cc08a3ddfd4bc24196f16fd4ec2"}, + {file = "tomlkit-0.6.0-py2.py3-none-any.whl", hash = "sha256:e5d5f20809c2b09276a6c5d98fb0202325aee441a651db84ac12e0812ab7e569"}, + {file = "tomlkit-0.6.0.tar.gz", hash = "sha256:74f976908030ff164c0aa1edabe3bf83ea004b3daa5b0940b9c86a060c004e9a"}, ] tornado = [ - {file = "tornado-6.0.3-cp35-cp35m-win32.whl", hash = "sha256:c9399267c926a4e7c418baa5cbe91c7d1cf362d505a1ef898fde44a07c9dd8a5"}, - {file = "tornado-6.0.3-cp35-cp35m-win_amd64.whl", hash = "sha256:398e0d35e086ba38a0427c3b37f4337327231942e731edaa6e9fd1865bbd6f60"}, - {file = "tornado-6.0.3-cp36-cp36m-win32.whl", hash = "sha256:4e73ef678b1a859f0cb29e1d895526a20ea64b5ffd510a2307b5998c7df24281"}, - {file = "tornado-6.0.3-cp36-cp36m-win_amd64.whl", hash = "sha256:349884248c36801afa19e342a77cc4458caca694b0eda633f5878e458a44cb2c"}, - {file = "tornado-6.0.3-cp37-cp37m-win32.whl", hash = "sha256:559bce3d31484b665259f50cd94c5c28b961b09315ccd838f284687245f416e5"}, - {file = "tornado-6.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:abbe53a39734ef4aba061fca54e30c6b4639d3e1f59653f0da37a0003de148c7"}, - {file = "tornado-6.0.3.tar.gz", hash = "sha256:c845db36ba616912074c5b1ee897f8e0124df269468f25e4fe21fe72f6edd7a9"}, + {file = "tornado-6.0.4-cp35-cp35m-win32.whl", hash = "sha256:5217e601700f24e966ddab689f90b7ea4bd91ff3357c3600fa1045e26d68e55d"}, + {file = "tornado-6.0.4-cp35-cp35m-win_amd64.whl", hash = "sha256:c98232a3ac391f5faea6821b53db8db461157baa788f5d6222a193e9456e1740"}, + {file = "tornado-6.0.4-cp36-cp36m-win32.whl", hash = "sha256:5f6a07e62e799be5d2330e68d808c8ac41d4a259b9cea61da4101b83cb5dc673"}, + {file = "tornado-6.0.4-cp36-cp36m-win_amd64.whl", hash = "sha256:c952975c8ba74f546ae6de2e226ab3cc3cc11ae47baf607459a6728585bb542a"}, + {file = "tornado-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:2c027eb2a393d964b22b5c154d1a23a5f8727db6fda837118a776b29e2b8ebc6"}, + {file = "tornado-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:5618f72e947533832cbc3dec54e1dffc1747a5cb17d1fd91577ed14fa0dc081b"}, + {file = "tornado-6.0.4-cp38-cp38-win32.whl", hash = "sha256:22aed82c2ea340c3771e3babc5ef220272f6fd06b5108a53b4976d0d722bcd52"}, + {file = "tornado-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:c58d56003daf1b616336781b26d184023ea4af13ae143d9dda65e31e534940b9"}, + {file = "tornado-6.0.4.tar.gz", hash = "sha256:0fe2d45ba43b00a41cd73f8be321a44936dc1aba233dee979f17a042b83eb6dc"}, +] +tqdm = [ + {file = "tqdm-4.46.0-py2.py3-none-any.whl", hash = "sha256:acdafb20f51637ca3954150d0405ff1a7edde0ff19e38fb99a80a66210d2a28f"}, + {file = "tqdm-4.46.0.tar.gz", hash = "sha256:4733c4a10d0f2a4d098d801464bdaf5240c7dadd2a7fde4ee93b0a0efd9fb25e"}, ] traitlets = [ {file = "traitlets-4.3.3-py2.py3-none-any.whl", hash = "sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44"}, {file = "traitlets-4.3.3.tar.gz", hash = "sha256:d023ee369ddd2763310e4c3eae1ff649689440d4ae59d7485eb4cfbbe3e359f7"}, ] typed-ast = [ - {file = "typed_ast-1.4.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e"}, - {file = "typed_ast-1.4.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b"}, - {file = "typed_ast-1.4.0-cp35-cp35m-win32.whl", hash = "sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4"}, - {file = "typed_ast-1.4.0-cp35-cp35m-win_amd64.whl", hash = "sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12"}, - {file = "typed_ast-1.4.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631"}, - {file = "typed_ast-1.4.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233"}, - {file = "typed_ast-1.4.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1"}, - {file = "typed_ast-1.4.0-cp36-cp36m-win32.whl", hash = "sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a"}, - {file = "typed_ast-1.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c"}, - {file = "typed_ast-1.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a"}, - {file = "typed_ast-1.4.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e"}, - {file = "typed_ast-1.4.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d"}, - {file = "typed_ast-1.4.0-cp37-cp37m-win32.whl", hash = "sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36"}, - {file = "typed_ast-1.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0"}, - {file = "typed_ast-1.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66"}, - {file = "typed_ast-1.4.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2"}, - {file = "typed_ast-1.4.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47"}, - {file = "typed_ast-1.4.0-cp38-cp38-win32.whl", hash = "sha256:1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161"}, - {file = "typed_ast-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e"}, - {file = "typed_ast-1.4.0.tar.gz", hash = "sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34"}, -] -typing = [ - {file = "typing-3.7.4.1-py2-none-any.whl", hash = "sha256:c8cabb5ab8945cd2f54917be357d134db9cc1eb039e59d1606dc1e60cb1d9d36"}, - {file = "typing-3.7.4.1-py3-none-any.whl", hash = "sha256:f38d83c5a7a7086543a0f649564d661859c5146a85775ab90c0d2f93ffaa9714"}, - {file = "typing-3.7.4.1.tar.gz", hash = "sha256:91dfe6f3f706ee8cc32d38edbbf304e9b7583fb37108fef38229617f8b3eba23"}, + {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, + {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, + {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, + {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, + {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, + {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, + {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, + {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, + {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, + {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, + {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, + {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, + {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, + {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, + {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, + {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, + {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, + {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, + {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, + {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, + {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, ] typing-extensions = [ - {file = "typing_extensions-3.7.4-py2-none-any.whl", hash = "sha256:b1edbbf0652660e32ae780ac9433f4231e7339c7f9a8057d0f042fcbcea49b87"}, - {file = "typing_extensions-3.7.4-py3-none-any.whl", hash = "sha256:d8179012ec2c620d3791ca6fe2bf7979d979acdbef1fca0bc56b37411db682ed"}, - {file = "typing_extensions-3.7.4.tar.gz", hash = "sha256:2ed632b30bb54fc3941c382decfd0ee4148f5c591651c9272473fea2c6397d95"}, + {file = "typing_extensions-3.7.4.2-py2-none-any.whl", hash = "sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392"}, + {file = "typing_extensions-3.7.4.2-py3-none-any.whl", hash = "sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5"}, + {file = "typing_extensions-3.7.4.2.tar.gz", hash = "sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae"}, ] urllib3 = [ - {file = "urllib3-1.25.8-py2.py3-none-any.whl", hash = "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc"}, - {file = "urllib3-1.25.8.tar.gz", hash = "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"}, + {file = "urllib3-1.25.9-py2.py3-none-any.whl", hash = "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115"}, + {file = "urllib3-1.25.9.tar.gz", hash = "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527"}, ] vistir = [ - {file = "vistir-0.4.3-py2.py3-none-any.whl", hash = "sha256:3a0529b4b6c2e842fd19b5ceaa95b6c9201321314825c110406d4af3331a0709"}, - {file = "vistir-0.4.3.tar.gz", hash = "sha256:2166e3148a67c438c9e3edbba0cde153d42dec6e3bf5d8f4624feb27686c0990"}, + {file = "vistir-0.5.0-py2.py3-none-any.whl", hash = "sha256:e47afdec8baf35032a8d17116765f751ecd2f2146d47e5af457c5de1fe5a334e"}, + {file = "vistir-0.5.0.tar.gz", hash = "sha256:33f8e905d40a77276b3d5310c8b57c1479a4e46930042b4894fcf7ed60ad76c4"}, ] vulture = [ - {file = "vulture-1.1-py2.py3-none-any.whl", hash = "sha256:20e73154efc9c6d2445663bc2f7fbf79b6c562f2b78cafc0ec0463b38610f42d"}, - {file = "vulture-1.1.tar.gz", hash = "sha256:a72834a8c9cf254f6ba0a64e9053b800e05df8391b1724702a19b00d7d849a43"}, + {file = "vulture-1.4-py2.py3-none-any.whl", hash = "sha256:89f977c83043c9e01336eedc1b86346a82264d6c85498b7fab4722d914a5f171"}, + {file = "vulture-1.4.tar.gz", hash = "sha256:7ed28f87e6b08e62675946c96788f30f44da6882ad07f4af50485b65a0fac77a"}, ] wcwidth = [ - {file = "wcwidth-0.1.7-py2.py3-none-any.whl", hash = "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c"}, - {file = "wcwidth-0.1.7.tar.gz", hash = "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e"}, + {file = "wcwidth-0.1.9-py2.py3-none-any.whl", hash = "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1"}, + {file = "wcwidth-0.1.9.tar.gz", hash = "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1"}, ] wheel = [ - {file = "wheel-0.33.6-py2.py3-none-any.whl", hash = "sha256:f4da1763d3becf2e2cd92a14a7c920f0f00eca30fdde9ea992c836685b9faf28"}, - {file = "wheel-0.33.6.tar.gz", hash = "sha256:10c9da68765315ed98850f8e048347c3eb06dd81822dc2ab1d4fde9dc9702646"}, -] -whichcraft = [ - {file = "whichcraft-0.6.1-py2.py3-none-any.whl", hash = "sha256:deda9266fbb22b8c64fd3ee45c050d61139cd87419765f588e37c8d23e236dd9"}, - {file = "whichcraft-0.6.1.tar.gz", hash = "sha256:acdbb91b63d6a15efbd6430d1d7b2d36e44a71697e93e19b7ded477afd9fce87"}, + {file = "wheel-0.34.2-py2.py3-none-any.whl", hash = "sha256:df277cb51e61359aba502208d680f90c0493adec6f0e848af94948778aed386e"}, + {file = "wheel-0.34.2.tar.gz", hash = "sha256:8788e9155fe14f54164c1b9eb0a319d98ef02c160725587ad60f14ddc57b6f96"}, ] yarg = [ {file = "yarg-0.1.9-py2.py3-none-any.whl", hash = "sha256:4f9cebdc00fac946c9bf2783d634e538a71c7d280a4d806d45fd4dc0ef441492"}, @@ -1929,6 +2040,6 @@ yaspin = [ {file = "yaspin-0.15.0.tar.gz", hash = "sha256:5a938bdc7bab353fd8942d0619d56c6b5159a80997dc1c387a479b39e6dc9391"}, ] zipp = [ - {file = "zipp-0.6.0-py2.py3-none-any.whl", hash = "sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335"}, - {file = "zipp-0.6.0.tar.gz", hash = "sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e"}, + {file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"}, + {file = "zipp-3.1.0.tar.gz", hash = "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"}, ] diff --git a/pyproject.toml b/pyproject.toml index f12101994..52c11bb17 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ python = "^3.6" appdirs = {version = "^1.4.0", optional = true} pipreqs = {version = "*", optional = true} requirementslib = {version = "*", optional = true} -tomlkit = {version = "0.5.3", optional = true} +tomlkit = {version = ">=0.5.3", optional = true} toml = {version = "*", optional = true} pip-api = {version = "*", optional = true} @@ -62,12 +62,14 @@ appdirs = "^1.4" pipfile = "^0.0.2" requirementslib = "^1.5" pipreqs = "^0.4.9" -tomlkit = "0.5.3" +tomlkit = ">=0.5.3" pip_api = "^0.0.12" numpy = "^1.16.0" pylama = "^7.7" pip = "^20.0.2" pip-shims = "^0.5.2" +smmap2 = "^3.0.1" +gitdb2 = "^4.0.2" [tool.poetry.scripts] isort = "isort.main:main" diff --git a/tests/test_api.py b/tests/test_api.py index 6e0795f63..e4984ceb1 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -8,14 +8,14 @@ def test_sort_file(tmpdir) -> None: - tmp_file = tmpdir.join(f"test_bad_syntax.py") + tmp_file = tmpdir.join("test_bad_syntax.py") tmp_file.write_text("""print('mismathing quotes")""", "utf8") with pytest.warns(UserWarning): api.sort_file(tmp_file, atomic=True) with pytest.warns(UserWarning): api.sort_file(tmp_file, atomic=True, write_to_stdout=True) - imperfect = tmpdir.join(f"test_needs_changes.py") + imperfect = tmpdir.join("test_needs_changes.py") imperfect.write_text("import b\nimport a\n", "utf8") api.sort_file(imperfect, write_to_stdout=True, show_diff=True) @@ -29,11 +29,11 @@ def test_sort_file(tmpdir) -> None: def test_check_file(tmpdir) -> None: - perfect = tmpdir.join(f"test_no_changes.py") + perfect = tmpdir.join("test_no_changes.py") perfect.write_text("import a\nimport b\n", "utf8") assert api.check_file(perfect, show_diff=True) - imperfect = tmpdir.join(f"test_needs_changes.py") + imperfect = tmpdir.join("test_needs_changes.py") imperfect.write_text("import b\nimport a\n", "utf8") assert not api.check_file(imperfect, show_diff=True) From 1eb187b911248ae0b30b8f8489fe56b313246f86 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 11 May 2020 22:57:22 -0700 Subject: [PATCH 0529/1439] Update tests to :fingers-crossed: work on windows --- tests/test_main.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/tests/test_main.py b/tests/test_main.py index ff0fe4dbf..609c45cae 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -28,7 +28,7 @@ def test_sort_imports(tmpdir): main.sort_imports(str(tmp_file), DEFAULT_CONFIG) assert not main.sort_imports(str(tmp_file), DEFAULT_CONFIG, check=True).incorrectly_sorted - skip_config = Config(skip=[str(tmp_file)]) + skip_config = Config(skip=["file.py"]) assert main.sort_imports( str(tmp_file), config=skip_config, check=True, disregard_skip=False ).skipped @@ -160,12 +160,7 @@ def test_main(capsys, tmpdir): """ ) main.main([str(python_file), "--filter-files", "--verbose"]) - assert ( - python_file.read() - == """import a -import b -""" - ) + assert python_file.read().lstrip() == "import a\nimport b\n" # Add a file to skip should_skip = tmpdir.join("should_skip.py") From d4b55a79b09db94f6e0615a1436ee0285d21861f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 11 May 2020 23:04:03 -0700 Subject: [PATCH 0530/1439] Update tests to :fingers-crossed: work on windows --- tests/test_main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_main.py b/tests/test_main.py index 609c45cae..f9ef13acc 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -211,7 +211,7 @@ def test_main(capsys, tmpdir): import b """ ) - main.main([str(tmpdir), "--skip", str(nested_file), "--check"]) + main.main([str(tmpdir), "--skip", "skip.py", "--check"]) def test_isort_command(): From 2ec774bff1544ee69a2b74222cd1987ce53b5e96 Mon Sep 17 00:00:00 2001 From: DeepSource Bot Date: Tue, 12 May 2020 07:23:07 +0000 Subject: [PATCH 0531/1439] Add .deepsource.toml --- .deepsource.toml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .deepsource.toml diff --git a/.deepsource.toml b/.deepsource.toml new file mode 100644 index 000000000..9bcfbf002 --- /dev/null +++ b/.deepsource.toml @@ -0,0 +1,10 @@ +version = 1 + +test_patterns = ["*/tests**/"] + +[[analyzers]] +name = "python" +enabled = true + + [analyzers.meta] + runtime_version = "3.x.x" \ No newline at end of file From 459b3885b74869fdbb970699498c739de7538a8b Mon Sep 17 00:00:00 2001 From: DeepSource Bot Date: Tue, 12 May 2020 07:26:34 +0000 Subject: [PATCH 0532/1439] Update .deepsource.toml --- .deepsource.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.deepsource.toml b/.deepsource.toml index 9bcfbf002..80173f5da 100644 --- a/.deepsource.toml +++ b/.deepsource.toml @@ -1,6 +1,6 @@ version = 1 -test_patterns = ["*/tests**/"] +test_patterns = ["*/tests/**/*.py"] [[analyzers]] name = "python" From c490956361124943a2d91f8884692e117807f5ba Mon Sep 17 00:00:00 2001 From: DeepSource Bot Date: Tue, 12 May 2020 07:30:30 +0000 Subject: [PATCH 0533/1439] Update .deepsource.toml --- .deepsource.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.deepsource.toml b/.deepsource.toml index 80173f5da..b6fabbc9f 100644 --- a/.deepsource.toml +++ b/.deepsource.toml @@ -1,6 +1,6 @@ version = 1 -test_patterns = ["*/tests/**/*.py"] +test_patterns = ["test/**/test_*.py"] [[analyzers]] name = "python" From 035a287a96470a7b5a29f9f4cd4ef75e83a777f9 Mon Sep 17 00:00:00 2001 From: Timothy Edmund Crosley Date: Tue, 12 May 2020 00:30:53 -0700 Subject: [PATCH 0534/1439] Update .deepsource.toml --- .deepsource.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.deepsource.toml b/.deepsource.toml index b6fabbc9f..07c27e3d0 100644 --- a/.deepsource.toml +++ b/.deepsource.toml @@ -1,10 +1,10 @@ version = 1 -test_patterns = ["test/**/test_*.py"] +test_patterns = ["tests/**/test_*.py"] [[analyzers]] name = "python" enabled = true [analyzers.meta] - runtime_version = "3.x.x" \ No newline at end of file + runtime_version = "3.x.x" From da1c5935de465fdb409717d465c4d03e0528bea5 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 13 May 2020 23:41:38 -0700 Subject: [PATCH 0535/1439] Fix outlines conditional to not needlesly check length twice --- isort/parse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/parse.py b/isort/parse.py index c171525cc..2be462127 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -392,7 +392,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte categorized_comments["above"]["straight"].setdefault(module, []).insert( 0, out_lines.pop(-1) ) - if len(out_lines) > 0 and len(out_lines): + if out_lines: last = out_lines[-1].rstrip() else: last = "" From fd7d8c14213834a1e5f83e170ae26e8410b69ab0 Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Thu, 14 May 2020 06:42:16 +0000 Subject: [PATCH 0536/1439] Remove unnecessary comprehension --- isort/_future/_dataclasses.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/_future/_dataclasses.py b/isort/_future/_dataclasses.py index 8e2ff41a0..a7b113fe2 100644 --- a/isort/_future/_dataclasses.py +++ b/isort/_future/_dataclasses.py @@ -361,7 +361,7 @@ def _tuple_str(obj_name, fields): if not fields: return "()" # Note the trailing comma, needed if this turns out to be a 1-tuple. - return f'({",".join([f"{obj_name}.{f.name}" for f in fields])},)' + return f'({",".join(f"{obj_name}.{f.name}" for f in fields)},)' def _create_fn(name, args, body, *, globals=None, locals=None, return_type=MISSING): @@ -520,7 +520,7 @@ def _repr_fn(fields): ("self",), [ 'return self.__class__.__qualname__ + f"(' - + ", ".join([f"{f.name}={{self.{f.name}!r}}" for f in fields]) + + ", ".join(f"{f.name}={{self.{f.name}!r}}" for f in fields) + ')"' ], ) From 3f9fa9f838e566fdda938defc7ee9ae89ee5eb75 Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Thu, 14 May 2020 06:53:24 +0000 Subject: [PATCH 0537/1439] Remove unused variables --- isort/io.py | 4 ++-- tests/test_isort.py | 8 ++++---- tests/test_parse.py | 16 ++++++++-------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/isort/io.py b/isort/io.py index f8f72f08b..b7c661c8c 100644 --- a/isort/io.py +++ b/isort/io.py @@ -19,7 +19,7 @@ class File(NamedTuple): @staticmethod def from_contents(contents: str, filename: str) -> "File": - encoding, lines = tokenize.detect_encoding(BytesIO(contents.encode("utf-8")).readline) + encoding, _ = tokenize.detect_encoding(BytesIO(contents.encode("utf-8")).readline) return File(StringIO(contents), path=Path(filename).resolve(), encoding=encoding) @property @@ -33,7 +33,7 @@ def _open(filename): """ buffer = open(filename, "rb") try: - encoding, lines = tokenize.detect_encoding(buffer.readline) + encoding, _ = tokenize.detect_encoding(buffer.readline) buffer.seek(0) text = TextIOWrapper(buffer, encoding, line_buffering=True, newline="") text.mode = "r" # type: ignore diff --git a/tests/test_isort.py b/tests/test_isort.py index e3ce46108..5859a5702 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -891,7 +891,7 @@ def test_check_newline_in_imports(capsys) -> None: line_length=20, verbose=True, ) - out, err = capsys.readouterr() + out, _ = capsys.readouterr() assert "SUCCESS" in out @@ -2625,21 +2625,21 @@ def test_long_alias_using_paren_issue_957() -> None: def test_strict_whitespace_by_default(capsys) -> None: test_input = "import os\nfrom django.conf import settings\n" assert not api.check_code_string(test_input) - out, err = capsys.readouterr() + out, _ = capsys.readouterr() assert out == "ERROR: Imports are incorrectly sorted and/or formatted.\n" def test_strict_whitespace_no_closing_newline_issue_676(capsys) -> None: test_input = "import os\n\nfrom django.conf import settings\n\nprint(1)" assert api.check_code_string(test_input) - out, err = capsys.readouterr() + out, _ = capsys.readouterr() assert out == "" def test_ignore_whitespace(capsys) -> None: test_input = "import os\nfrom django.conf import settings\n" assert api.check_code_string(test_input, ignore_whitespace=True) - out, err = capsys.readouterr() + out, _ = capsys.readouterr() assert out == "" diff --git a/tests/test_parse.py b/tests/test_parse.py index 4948f9ce9..f3a24d668 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -27,16 +27,16 @@ def test_file_contents(): in_lines, out_lines, import_index, - place_imports, - import_placements, - as_map, - imports, - categorized_comments, + _, + _, + _, + _, + _, change_count, original_line_count, - line_separator, - sections, - section_comments, + _, + _, + _, ) = parse.file_contents(TEST_CONTENTS, config=Config(default_section="")) assert "\n".join(in_lines) == TEST_CONTENTS assert "import" not in "\n".join(out_lines) From d91f45286ca8f7a05e17ff0edc20f35d54a80c7d Mon Sep 17 00:00:00 2001 From: Brad Solomon Date: Thu, 14 May 2020 09:22:21 -0400 Subject: [PATCH 0538/1439] Docs: mention isort[pyproject] extra in README --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index af4d2a5c5..f127f3a2e 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,12 @@ Install isort with both formats support: pip install isort[requirements,pipfile] ``` +Install isort with support for reading configuration from `pyproject.toml`: + +```bash +pip install isort[pyproject] +``` + Using isort =========== @@ -272,7 +278,8 @@ Or, if you prefer, you can add an `isort` or `tool:isort` section to your project's `setup.cfg` or `tox.ini` file with any desired settings. You can also add your desired settings under a `[tool.isort]` section in -your `pyproject.toml` file. +your `pyproject.toml` file. For `pyproject.toml` support, use +`pip install isort[pyproject]`. You can then override any of these settings by using command line arguments, or by passing in override values to the SortImports class. From cf6e644c5055a773bbddb0b881d204da43a4b943 Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Fri, 15 May 2020 05:55:33 +0000 Subject: [PATCH 0539/1439] Use literal syntax to create data structure --- tests/test_isort.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/test_isort.py b/tests/test_isort.py index 5859a5702..2e0ee6e40 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -3172,13 +3172,9 @@ def test_path_finder(monkeypatch) -> None: finder = finders.PathFinder(config=config) third_party_prefix = next(path for path in finder.paths if "site-packages" in path) ext_suffixes = importlib.machinery.EXTENSION_SUFFIXES - imaginary_paths = set( - [ - posixpath.join(finder.stdlib_lib_prefix, "example_1.py"), + imaginary_paths = {posixpath.join(finder.stdlib_lib_prefix, "example_1.py"), posixpath.join(third_party_prefix, "example_2.py"), - posixpath.join(os.getcwd(), "example_3.py"), - ] - ) + posixpath.join(os.getcwd(), "example_3.py"),} imaginary_paths.update( { posixpath.join(third_party_prefix, "example_" + str(i) + ext_suffix) From cdb92fee4e8dec934887e9778e68bcae024a2cf8 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 15 May 2020 21:02:53 -0700 Subject: [PATCH 0540/1439] Switch to set comprehension for better performance --- isort/settings.py | 10 ++++------ tests/test_isort.py | 8 +++++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/isort/settings.py b/isort/settings.py index 0e531627f..35425f00a 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -350,12 +350,10 @@ def _as_list(value: str) -> List[str]: def _abspaths(cwd: str, values: Iterable[str]) -> Set[str]: paths = set( - [ - os.path.join(cwd, value) - if not value.startswith(os.path.sep) and value.endswith(os.path.sep) - else value - for value in values - ] + os.path.join(cwd, value) + if not value.startswith(os.path.sep) and value.endswith(os.path.sep) + else value + for value in values ) return paths diff --git a/tests/test_isort.py b/tests/test_isort.py index 2e0ee6e40..33e482b4f 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -3172,9 +3172,11 @@ def test_path_finder(monkeypatch) -> None: finder = finders.PathFinder(config=config) third_party_prefix = next(path for path in finder.paths if "site-packages" in path) ext_suffixes = importlib.machinery.EXTENSION_SUFFIXES - imaginary_paths = {posixpath.join(finder.stdlib_lib_prefix, "example_1.py"), - posixpath.join(third_party_prefix, "example_2.py"), - posixpath.join(os.getcwd(), "example_3.py"),} + imaginary_paths = { + posixpath.join(finder.stdlib_lib_prefix, "example_1.py"), + posixpath.join(third_party_prefix, "example_2.py"), + posixpath.join(os.getcwd(), "example_3.py"), + } imaginary_paths.update( { posixpath.join(third_party_prefix, "example_" + str(i) + ext_suffix) From 4a82d87916ece67042153b0481a34e99d78b4144 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 16 May 2020 16:11:25 -0700 Subject: [PATCH 0541/1439] Fix missing asserts --- tests/test_exceptions.py | 2 +- tests/test_isort.py | 9 ++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 4067f638e..af877917b 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -41,7 +41,7 @@ def setup_class(self): def test_variables(self): assert self.instance.file_path == "file_path" - str(self.instance) == "message" + assert str(self.instance) == "message" class TestFileSkipComment(TestISortError): diff --git a/tests/test_isort.py b/tests/test_isort.py index 33e482b4f..7d830a42a 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -4519,13 +4519,11 @@ def test_cimport_support(): if sys.version_info.major == 2: import urlparse - else: from urllib import parse as urlparse if sys.version_info.major == 2: from urllib import pathname2url as urllib_pathname2url - else: from urllib.request import pathname2url as urllib_pathname2url @@ -4580,10 +4578,8 @@ def test_cimport_support(): IF UNAME_SYSNAME == "Windows": from dpi_aware_win cimport * from windows cimport * - ELIF UNAME_SYSNAME == "Linux": from linux cimport * - ELIF UNAME_SYSNAME == "Darwin": from mac cimport * @@ -4591,6 +4587,7 @@ def test_cimport_support(): from cpp_utils cimport * from task cimport * + cdef extern from *: ctypedef CefString ConstCefString "const CefString" @@ -4615,10 +4612,8 @@ def test_cimport_support(): # cannot cimport *, name conflicts IF UNAME_SYSNAME == "Windows": cimport cef_types_win - ELIF UNAME_SYSNAME == "Darwin": cimport cef_types_mac - ELIF UNAME_SYSNAME == "Linux": cimport cef_types_linux @@ -4677,7 +4672,7 @@ def test_cimport_support(): from string_visitor cimport * from web_request_client_cef3 cimport * """ - api.sort_code_string(test_input) == expected_output + assert api.sort_code_string(test_input).strip() == expected_output.strip() def test_cdef_support(): From 18a5cf0b69ffa33d44827f574ac922aa6514df06 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 16 May 2020 16:14:12 -0700 Subject: [PATCH 0542/1439] Make it explicit that nothing is returned in init, just used as short circuit --- isort/settings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/isort/settings.py b/isort/settings.py index 35425f00a..ed99cbe86 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -216,7 +216,8 @@ def __init__( config_vars = vars(config).copy() config_vars.update(config_overrides) config_vars["py_version"] = config_vars["py_version"].replace("py", "") - return super().__init__(**config_vars) # type: ignore + super().__init__(**config_vars) # type: ignore + return sources: List[Dict[str, Any]] = [_DEFAULT_SETTINGS] From 09e68edaf903e800ea8615d25fa69b8a68e3737f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 16 May 2020 16:19:54 -0700 Subject: [PATCH 0543/1439] Fix exclusions --- .deepsource.toml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.deepsource.toml b/.deepsource.toml index 07c27e3d0..a62405693 100644 --- a/.deepsource.toml +++ b/.deepsource.toml @@ -1,6 +1,11 @@ version = 1 -test_patterns = ["tests/**/test_*.py"] +test_patterns = ["tests/**"] +exclude_patterns = [ + "tests/**", + "isort/_future/**", +] + [[analyzers]] name = "python" From e7d067e703cf766da015ca198276877aeb481b33 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 16 May 2020 16:21:10 -0700 Subject: [PATCH 0544/1439] Add deep source badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index af4d2a5c5..740f57724 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ [![PyPI version](https://badge.fury.io/py/isort.svg)](https://badge.fury.io/py/isort) [![Build Status](https://travis-ci.org/timothycrosley/isort.svg?branch=develop)](https://travis-ci.org/timothycrosley/isort) [![Code coverage Status](https://codecov.io/gh/timothycrosley/isort/branch/develop/graph/badge.svg)](https://codecov.io/gh/timothycrosley/isort) +[![DeepSource](https://static.deepsource.io/deepsource-badge-light-mini.svg)](https://deepsource.io/gh/timothycrosley/isort/?ref=repository-badge) [![License](https://img.shields.io/github/license/mashape/apistatus.svg)](https://pypi.org/project/hug/) [![Join the chat at https://gitter.im/timothycrosley/isort](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/timothycrosley/isort?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Downloads](https://pepy.tech/badge/isort)](https://pepy.tech/project/isort) From 8f76297eaeab85060ad20da48b50a8791e318a85 Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Sun, 17 May 2020 02:57:29 +0000 Subject: [PATCH 0545/1439] Change methods not using its bound instance to ... ...staticmethods --- isort/finders.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/isort/finders.py b/isort/finders.py index 4a138a3a7..8c3367fd1 100644 --- a/isort/finders.py +++ b/isort/finders.py @@ -91,7 +91,8 @@ def __init__(self, config: Config) -> None: regexp = "^" + known_pattern.replace("*", ".*").replace("?", ".?") + "$" self.known_patterns.append((re.compile(regexp), placement)) - def _parse_known_pattern(self, pattern: str) -> List[str]: + @staticmethod + def _parse_known_pattern(pattern: str) -> List[str]: """Expand pattern if identified as a directory and return found sub packages""" if pattern.endswith(os.path.sep): patterns = [ From 4b6040902e8e9460e4734639e0b7de8cc29cda1f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 16 May 2020 20:09:18 -0700 Subject: [PATCH 0546/1439] Mode read_file into File class --- isort/api.py | 4 ++-- isort/io.py | 22 +++++++++++----------- tests/test_io.py | 2 +- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/isort/api.py b/isort/api.py index 770109dd1..d2554429b 100644 --- a/isort/api.py +++ b/isort/api.py @@ -437,7 +437,7 @@ def check_file( disregard_skip: bool = True, **config_kwargs, ) -> bool: - with io.read_file(filename) as source_file: + with io.File.read(filename) as source_file: return check_imports( source_file.stream, show_diff=show_diff, @@ -460,7 +460,7 @@ def sort_file( write_to_stdout: bool = False, **config_kwargs, ): - with io.read_file(filename) as source_file: + with io.File.read(filename) as source_file: changed: bool = False try: if write_to_stdout: diff --git a/isort/io.py b/isort/io.py index b7c661c8c..43a348677 100644 --- a/isort/io.py +++ b/isort/io.py @@ -42,17 +42,17 @@ def _open(filename): buffer.close() raise - -@contextmanager -def read_file(filename: Union[str, Path]) -> Iterator["File"]: - file_path = Path(filename).resolve() - stream = None - try: - stream = File._open(file_path) - yield File(stream=stream, path=file_path, encoding=stream.encoding) - finally: - if stream is not None: - stream.close() + @staticmethod + @contextmanager + def read(filename: Union[str, Path]) -> Iterator["File"]: + file_path = Path(filename).resolve() + stream = None + try: + stream = File._open(file_path) + yield File(stream=stream, path=file_path, encoding=stream.encoding) + finally: + if stream is not None: + stream.close() class _EmptyIO(StringIO): diff --git a/tests/test_io.py b/tests/test_io.py index 9b2637fb2..59a86faca 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -16,7 +16,7 @@ def test_read(self, tmpdir): test_file = tmpdir.join("file.py") test_file.write(test_file_content) with pytest.raises(Exception): - with io.read_file(str(test_file)) as file_handler: + with io.File.read(str(test_file)) as file_handler: file_handler.stream.read() def test_from_content(self, tmpdir): From 6e5e2597a86f123bab0a64249d02d8ffa00133d5 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 16 May 2020 20:16:10 -0700 Subject: [PATCH 0547/1439] Remove unused imports --- isort/api.py | 2 +- isort/exceptions.py | 13 ------------- isort/io.py | 3 --- isort/main.py | 2 +- isort/setuptools_commands.py | 2 +- tests/test_exceptions.py | 10 ---------- 6 files changed, 3 insertions(+), 29 deletions(-) diff --git a/isort/api.py b/isort/api.py index d2554429b..721fd6c7c 100644 --- a/isort/api.py +++ b/isort/api.py @@ -19,7 +19,7 @@ remove_whitespace, show_unified_diff, ) -from .io import Empty, File +from .io import Empty from .settings import DEFAULT_CONFIG, FILE_SKIP_COMMENTS, Config CIMPORT_IDENTIFIERS = ("cimport ", "cimport*", "from.cimport") diff --git a/isort/exceptions.py b/isort/exceptions.py index 73a747cb6..cc202cb1e 100644 --- a/isort/exceptions.py +++ b/isort/exceptions.py @@ -8,19 +8,6 @@ class ISortError(Exception): """Base isort exception object from which all isort sourced exceptions should inherit""" -class UnableToDetermineEncoding(ISortError): - """Raised when isort is unable to determine a provided file's encoding""" - - def __init__(self, file_path: Path, encoding: str, fallback_encoding: str): - super().__init__( - f"Couldn't open {file_path} with any of the attempted encodings." - f"Given {encoding} encoding or {fallback_encoding} fallback encoding both failed." - ) - self.file_path = file_path - self.encoding = encoding - self.fallback_encoding = fallback_encoding - - class ExistingSyntaxErrors(ISortError): """Raised when isort is told to sort imports within code that has existing syntax errors""" diff --git a/isort/io.py b/isort/io.py index 43a348677..753a6a6b5 100644 --- a/isort/io.py +++ b/isort/io.py @@ -1,5 +1,4 @@ """Defines any IO utilities used by isort""" -import locale import re import tokenize from contextlib import contextmanager @@ -7,8 +6,6 @@ from pathlib import Path from typing import Iterator, List, NamedTuple, Optional, TextIO, Tuple, Union -from .exceptions import UnableToDetermineEncoding - _ENCODING_PATTERN = re.compile(br"^[ \t\f]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)") diff --git a/isort/main.py b/isort/main.py index 0c1a29c1f..90b54d203 100644 --- a/isort/main.py +++ b/isort/main.py @@ -18,7 +18,7 @@ from .settings import SUPPORTED_EXTENSIONS, VALID_PY_TARGETS, Config, WrapModes try: - from .setuptools_commands import ISortCommand + from .setuptools_commands import ISortCommand # skipcq: PYL-W0611 except ImportError: pass diff --git a/isort/setuptools_commands.py b/isort/setuptools_commands.py index 1af8df8c6..62c595fbe 100644 --- a/isort/setuptools_commands.py +++ b/isort/setuptools_commands.py @@ -7,7 +7,7 @@ import setuptools from . import api -from .settings import DEFAULT_CONFIG, Config +from .settings import DEFAULT_CONFIG class ISortCommand(setuptools.Command): diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index af877917b..93e4e226e 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -9,16 +9,6 @@ def test_init(self): assert isinstance(self.instance, exceptions.ISortError) -class TestUnableToDetermineEncoding(TestISortError): - def setup_class(self): - self.instance = exceptions.UnableToDetermineEncoding("file_path", "encoding", "fallback") - - def test_variables(self): - assert self.instance.file_path == "file_path" - assert self.instance.encoding == "encoding" - assert self.instance.fallback_encoding == "fallback" - - class TestExistingSyntaxErrors(TestISortError): def setup_class(self): self.instance = exceptions.ExistingSyntaxErrors("file_path") From cb260e6fc878bb9e39a9a00c8b6eb0956a23739d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 16 May 2020 20:18:18 -0700 Subject: [PATCH 0548/1439] Remove unused import --- isort/exceptions.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/isort/exceptions.py b/isort/exceptions.py index cc202cb1e..64545e6eb 100644 --- a/isort/exceptions.py +++ b/isort/exceptions.py @@ -1,6 +1,4 @@ """All isort specific exception classes should be defined here""" -from pathlib import Path - from .profiles import profiles From 28bbd0e703e1b6124cb2866082fa9fcfb5b9d921 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 16 May 2020 23:38:45 -0700 Subject: [PATCH 0549/1439] Make sure outer scope variables aren't redefined --- isort/parse.py | 12 +++++------ isort/wrap.py | 52 +++++++++++++++++++++++---------------------- isort/wrap_modes.py | 22 +++++++++---------- 3 files changed, 44 insertions(+), 42 deletions(-) diff --git a/isort/parse.py b/isort/parse.py index 2be462127..0266d23f1 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -27,10 +27,10 @@ ) -def _infer_line_separator(file_contents: str) -> str: - if "\r\n" in file_contents: +def _infer_line_separator(contents: str) -> str: + if "\r\n" in contents: return "\r\n" - elif "\r" in file_contents: + elif "\r" in contents: return "\r" else: return "\n" @@ -86,7 +86,7 @@ def skip_line( (skip_line: bool, in_quote: str,) """ - skip_line = bool(in_quote) + should_skip = bool(in_quote) if '"' in line or "'" in line: char_index = 0 while char_index < len(line): @@ -113,9 +113,9 @@ def skip_line( and not part.startswith("from ") and not part.startswith(("import ", "cimport ")) ): - skip_line = True + should_skip = True - return (bool(skip_line or in_quote), in_quote) + return (bool(should_skip or in_quote), in_quote) class ParsedContent(NamedTuple): diff --git a/isort/wrap.py b/isort/wrap.py index a6dbeee16..2265218ad 100644 --- a/isort/wrap.py +++ b/isort/wrap.py @@ -20,7 +20,7 @@ def import_statement( dynamic_indent = " " * (len(import_start) + 1) indent = config.indent line_length = config.wrap_length or config.line_length - import_statement = formatter( + statement = formatter( statement=import_start, imports=copy.copy(from_imports), white_space=dynamic_indent, @@ -33,15 +33,15 @@ def import_statement( remove_comments=config.ignore_comments, ) if config.balanced_wrapping: - lines = import_statement.split(line_separator) + lines = statement.split(line_separator) line_count = len(lines) if len(lines) > 1: minimum_length = min(len(line) for line in lines[:-1]) else: minimum_length = 0 - new_import_statement = import_statement + new_import_statement = statement while len(lines[-1]) < minimum_length and len(lines) == line_count and line_length > 10: - import_statement = new_import_statement + statement = new_import_statement line_length -= 1 new_import_statement = formatter( statement=import_start, @@ -56,19 +56,19 @@ def import_statement( remove_comments=config.ignore_comments, ) lines = new_import_statement.split(line_separator) - if import_statement.count(line_separator) == 0: - return _wrap_line(import_statement, line_separator, config) - return import_statement + if statement.count(line_separator) == 0: + return _wrap_line(statement, line_separator, config) + return statement -def line(line: str, line_separator: str, config: Config = DEFAULT_CONFIG) -> str: +def line(content: str, line_separator: str, config: Config = DEFAULT_CONFIG) -> str: """Returns a line wrapped to the specified line-length, if possible.""" wrap_mode = config.multi_line_output - if len(line) > config.line_length and wrap_mode != Modes.NOQA: # type: ignore - line_without_comment = line + if len(content) > config.line_length and wrap_mode != Modes.NOQA: # type: ignore + line_without_comment = content comment = None - if "#" in line: - line_without_comment, comment = line.split("#", 1) + if "#" in content: + line_without_comment, comment = content.split("#", 1) for splitter in ("import ", ".", "as "): exp = r"\b" + re.escape(splitter) + r"\b" if re.search(exp, line_without_comment) and not line_without_comment.strip().startswith( @@ -79,18 +79,20 @@ def line(line: str, line_separator: str, config: Config = DEFAULT_CONFIG) -> str _comma_maybe = "," if config.include_trailing_comma else "" line_parts[-1] = f"{line_parts[-1].strip()}{_comma_maybe} #{comment}" next_line = [] - while (len(line) + 2) > (config.wrap_length or config.line_length) and line_parts: + while (len(content) + 2) > ( + config.wrap_length or config.line_length + ) and line_parts: next_line.append(line_parts.pop()) - line = splitter.join(line_parts) - if not line: - line = next_line.pop() + content = splitter.join(line_parts) + if not content: + content = next_line.pop() cont_line = _wrap_line( config.indent + splitter.join(next_line).lstrip(), line_separator, config ) if config.use_parentheses: if splitter == "as ": - output = f"{line}{splitter}{cont_line.lstrip()}" + output = f"{content}{splitter}{cont_line.lstrip()}" else: _comma = "," if config.include_trailing_comma and not comment else "" if wrap_mode in ( @@ -101,19 +103,19 @@ def line(line: str, line_separator: str, config: Config = DEFAULT_CONFIG) -> str else: _separator = "" output = ( - f"{line}{splitter}({line_separator}{cont_line}{_comma}{_separator})" + f"{content}{splitter}({line_separator}{cont_line}{_comma}{_separator})" ) lines = output.split(line_separator) if config.comment_prefix in lines[-1] and lines[-1].endswith(")"): - line, comment = lines[-1].split(config.comment_prefix, 1) - lines[-1] = line + ")" + config.comment_prefix + comment[:-1] + content, comment = lines[-1].split(config.comment_prefix, 1) + lines[-1] = content + ")" + config.comment_prefix + comment[:-1] return line_separator.join(lines) - return f"{line}{splitter}\\{line_separator}{cont_line}" - elif len(line) > config.line_length and wrap_mode == Modes.NOQA: # type: ignore - if "# NOQA" not in line: - return f"{line}{config.comment_prefix} NOQA" + return f"{content}{splitter}\\{line_separator}{cont_line}" + elif len(content) > config.line_length and wrap_mode == Modes.NOQA: # type: ignore + if "# NOQA" not in content: + return f"{content}{config.comment_prefix} NOQA" - return line + return content _wrap_line = line diff --git a/isort/wrap_modes.py b/isort/wrap_modes.py index a42d48a97..b9ba875d6 100644 --- a/isort/wrap_modes.py +++ b/isort/wrap_modes.py @@ -3,7 +3,7 @@ from inspect import signature from typing import Any, Callable, Dict, List -from . import comments +import isort.comments _wrap_modes: Dict[str, Callable[[Any], str]] = {} @@ -50,7 +50,7 @@ def grid(**interface): interface["statement"] += "(" + interface["imports"].pop(0) while interface["imports"]: next_import = interface["imports"].pop(0) - next_statement = comments.add_to_line( + next_statement = isort.comments.add_to_line( interface["comments"], interface["statement"] + ", " + next_import, removed=interface["remove_comments"], @@ -69,7 +69,7 @@ def grid(**interface): lines[-1] = new_line next_import = interface["line_separator"].join(lines) interface["statement"] = ( - comments.add_to_line( + isort.comments.add_to_line( interface["comments"], f"{interface['statement']},", removed=interface["remove_comments"], @@ -89,7 +89,7 @@ def vertical(**interface): return "" first_import = ( - comments.add_to_line( + isort.comments.add_to_line( interface["comments"], interface["imports"].pop(0) + ",", removed=interface["remove_comments"], @@ -115,7 +115,7 @@ def hanging_indent(**interface): # Check for first import if len(next_statement) + 3 > interface["line_length"]: next_statement = ( - comments.add_to_line( + isort.comments.add_to_line( interface["comments"], f"{interface['statement']}\\", removed=interface["remove_comments"], @@ -127,7 +127,7 @@ def hanging_indent(**interface): interface["statement"] = next_statement while interface["imports"]: next_import = interface["imports"].pop(0) - next_statement = comments.add_to_line( + next_statement = isort.comments.add_to_line( interface["comments"], interface["statement"] + ", " + next_import, removed=interface["remove_comments"], @@ -138,7 +138,7 @@ def hanging_indent(**interface): > interface["line_length"] ): next_statement = ( - comments.add_to_line( + isort.comments.add_to_line( interface["comments"], f"{interface['statement']}, \\", removed=interface["remove_comments"], @@ -153,7 +153,7 @@ def hanging_indent(**interface): @_wrap_mode def vertical_hanging_indent(**interface): - _line_with_comments = comments.add_to_line( + _line_with_comments = isort.comments.add_to_line( interface["comments"], "", removed=interface["remove_comments"], @@ -172,7 +172,7 @@ def vertical_grid_common(need_trailing_char: bool, **interface): return "" interface["statement"] += ( - comments.add_to_line( + isort.comments.add_to_line( interface["comments"], "(", removed=interface["remove_comments"], @@ -302,7 +302,7 @@ def vertical_prefix_from_module_import(**interface): interface["statement"] += interface["imports"].pop(0) while interface["imports"]: next_import = interface["imports"].pop(0) - next_statement = comments.add_to_line( + next_statement = isort.comments.add_to_line( interface["comments"], interface["statement"] + ", " + next_import, removed=interface["remove_comments"], @@ -313,7 +313,7 @@ def vertical_prefix_from_module_import(**interface): > interface["line_length"] ): next_statement = ( - comments.add_to_line( + isort.comments.add_to_line( interface["comments"], f"{interface['statement']}", removed=interface["remove_comments"], From 9f0ea27cdfe56f20ae8c78be414d89cf07852342 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 17 May 2020 01:33:37 -0700 Subject: [PATCH 0550/1439] Remove no longer relevant undertake config file --- .undertake | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 .undertake diff --git a/.undertake b/.undertake deleted file mode 100644 index c43281fbf..000000000 --- a/.undertake +++ /dev/null @@ -1,4 +0,0 @@ -name=Add isort settings -color=#3C79AB -font_color=white -format=instantly \ No newline at end of file From 640bcf7a130bce040d7c318201244b99f6efea5e Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 17 May 2020 01:39:35 -0700 Subject: [PATCH 0551/1439] Attempt to fix MacOS github action testing --- tests/test_isort.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_isort.py b/tests/test_isort.py index 7d830a42a..0aa59ea5e 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -3222,7 +3222,7 @@ def test_command_line(tmpdir, capfd, multiprocess: bool) -> None: tmpdir.join("file2.py").read() == "import abc\nimport collections\nimport time\n\nimport isort\n" ) - if not sys.platform.startswith("win"): + if not (sys.platform.startswith("win") or sys.platform.startswith("mac")): out, err = capfd.readouterr() assert not [error for error in err.split("\n") if error and "warning:" not in error] # it informs us about fixing the files: From 33d1145571431ae791f7592cfe7f41e131a87b2d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 17 May 2020 01:47:57 -0700 Subject: [PATCH 0552/1439] Attempt to fix MacOS github action testing --- tests/test_isort.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_isort.py b/tests/test_isort.py index 0aa59ea5e..9d5b1a9b9 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -3222,7 +3222,7 @@ def test_command_line(tmpdir, capfd, multiprocess: bool) -> None: tmpdir.join("file2.py").read() == "import abc\nimport collections\nimport time\n\nimport isort\n" ) - if not (sys.platform.startswith("win") or sys.platform.startswith("mac")): + if not (sys.platform.startswith("win") or sys.platform.startswith("darwin")): out, err = capfd.readouterr() assert not [error for error in err.split("\n") if error and "warning:" not in error] # it informs us about fixing the files: From ca2b986354efc1f3131d6dfb44f42c0808a9fe8f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 17 May 2020 01:54:32 -0700 Subject: [PATCH 0553/1439] Expose Python API cleanly --- isort/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/isort/__init__.py b/isort/__init__.py index e03cd0076..e731a81e4 100644 --- a/isort/__init__.py +++ b/isort/__init__.py @@ -1,3 +1,9 @@ """Defines the public isort interface""" from . import settings # noqa: F401 from ._version import __version__ +from .api import check_code_string as check_code +from .api import check_file as check_file +from .api import check_imports as check_stream +from .api import sort_code_string as code +from .api import sort_file as file +from .api import sort_imports as stream From 586109d4aa06ce3045766467bedb1f6fb4bc189d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 17 May 2020 01:56:18 -0700 Subject: [PATCH 0554/1439] Minor README fixes --- README.md | 40 ++++------------------------------------ 1 file changed, 4 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 740f57724..47c6f8790 100644 --- a/README.md +++ b/README.md @@ -148,51 +148,19 @@ run against code written using a different version of Python) **From within Python**: ```bash -from isort import SortImports +import isort -SortImports("pythonfile.py") +isort.file("pythonfile.py") ``` or: ```bash -from isort import SortImports +import isort -new_contents = SortImports(file_contents=old_contents).output +sorted_code = isort.code("import b\nimport a\n") ``` -**From within Kate:** - -```bash -ctrl+[ -``` - -or: - -```bash -menu > Python > Sort Imports -``` - -Installing isort's Kate plugin -=============================== - -For KDE 4.13+ / Pate 2.0+: - -```bash -wget https://raw.github.com/timothycrosley/isort/master/kate_plugin/isort_plugin.py --output-document ~/.kde/share/apps/kate/pate/isort_plugin.py -wget https://raw.github.com/timothycrosley/isort/master/kate_plugin/isort_plugin_ui.rc --output-document ~/.kde/share/apps/kate/pate/isort_plugin_ui.rc -wget https://raw.github.com/timothycrosley/isort/master/kate_plugin/katepart_isort.desktop --output-document ~/.kde/share/kde4/services/katepart_isort.desktop -``` - -For all older versions: - -```bash -wget https://raw.github.com/timothycrosley/isort/master/kate_plugin/isort_plugin_old.py --output-document ~/.kde/share/apps/kate/pate/isort_plugin.py -``` - -You will then need to restart kate and enable Python Plugins as well as -the isort plugin itself. - Installing isort's for your preferred text editor ================================================== From b2339d5e6ecf688145a0a328384d08ffaadbaf88 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 17 May 2020 01:56:50 -0700 Subject: [PATCH 0555/1439] Minor README fixes --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 47c6f8790..db9b09548 100644 --- a/README.md +++ b/README.md @@ -244,7 +244,8 @@ You can also add your desired settings under a `[tool.isort]` section in your `pyproject.toml` file. You can then override any of these settings by using command line -arguments, or by passing in override values to the SortImports class. +arguments, or by passing in override values to any of the public Python API +functions. Finally, as of version 3.0 isort supports editorconfig files using the standard syntax defined here: From 093aeabc8f3ebabb98d51db22063f0a050a050ca Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 17 May 2020 02:17:24 -0700 Subject: [PATCH 0556/1439] Improvements suggested by deep source --- isort/__init__.py | 2 +- isort/api.py | 13 +++++++------ isort/finders.py | 36 ++++++++++++++++++------------------ isort/settings.py | 8 ++++---- 4 files changed, 30 insertions(+), 29 deletions(-) diff --git a/isort/__init__.py b/isort/__init__.py index e731a81e4..6d818e99f 100644 --- a/isort/__init__.py +++ b/isort/__init__.py @@ -2,7 +2,7 @@ from . import settings # noqa: F401 from ._version import __version__ from .api import check_code_string as check_code -from .api import check_file as check_file +from .api import check_file from .api import check_imports as check_stream from .api import sort_code_string as code from .api import sort_file as file diff --git a/isort/api.py b/isort/api.py index 721fd6c7c..46d543565 100644 --- a/isort/api.py +++ b/isort/api.py @@ -38,12 +38,13 @@ def _config( ): config_kwargs["settings_path"] = path - if config_kwargs and config is not DEFAULT_CONFIG: - raise ValueError( - "You can either specify custom configuration options using kwargs or " - "passing in a Config object. Not Both!" - ) - elif config_kwargs: + if config_kwargs: + if config is not DEFAULT_CONFIG: + raise ValueError( + "You can either specify custom configuration options using kwargs or " + "passing in a Config object. Not Both!" + ) + config = Config(**config_kwargs) return config diff --git a/isort/finders.py b/isort/finders.py index 8c3367fd1..210ddfd5e 100644 --- a/isort/finders.py +++ b/isort/finders.py @@ -132,26 +132,26 @@ def __init__(self, config: Config, path: str = ".") -> None: self.virtual_env_src = "" if self.virtual_env: self.virtual_env_src = f"{self.virtual_env}/src/" - for path in glob(f"{self.virtual_env}/lib/python*/site-packages"): - if path not in self.paths: - self.paths.append(path) - for path in glob(f"{self.virtual_env}/lib/python*/*/site-packages"): - if path not in self.paths: - self.paths.append(path) - for path in glob(f"{self.virtual_env}/src/*"): - if os.path.isdir(path): - self.paths.append(path) + for venv_path in glob(f"{self.virtual_env}/lib/python*/site-packages"): + if venv_path not in self.paths: + self.paths.append(venv_path) + for nested_venv_path in glob(f"{self.virtual_env}/lib/python*/*/site-packages"): + if nested_venv_path not in self.paths: + self.paths.append(nested_venv_path) + for venv_src_path in glob(f"{self.virtual_env}/src/*"): + if os.path.isdir(venv_src_path): + self.paths.append(venv_src_path) # conda self.conda_env = self.config.conda_env or os.environ.get("CONDA_PREFIX") or "" if self.conda_env: self.conda_env = os.path.realpath(self.conda_env) - for path in glob(f"{self.conda_env}/lib/python*/site-packages"): - if path not in self.paths: - self.paths.append(path) - for path in glob(f"{self.conda_env}/lib/python*/*/site-packages"): - if path not in self.paths: - self.paths.append(path) + for conda_path in glob(f"{self.conda_env}/lib/python*/site-packages"): + if conda_path not in self.paths: + self.paths.append(conda_path) + for nested_conda_path in glob(f"{self.conda_env}/lib/python*/*/site-packages"): + if nested_conda_path not in self.paths: + self.paths.append(nested_conda_path) # handle case-insensitive paths on windows self.stdlib_lib_prefix = os.path.normcase(sysconfig.get_paths()["stdlib"]) @@ -159,9 +159,9 @@ def __init__(self, config: Config, path: str = ".") -> None: self.paths.append(self.stdlib_lib_prefix) # add system paths - for path in sys.path[1:]: - if path not in self.paths: - self.paths.append(path) + for system_path in sys.path[1:]: + if system_path not in self.paths: + self.paths.append(system_path) def find(self, module_name: str) -> Optional[str]: for prefix in self.paths: diff --git a/isort/settings.py b/isort/settings.py index ed99cbe86..a61e000a0 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -290,11 +290,11 @@ def __init__( combined_config.pop("source", None) combined_config.pop("sources", None) if known_other: - for known_key in known_other.keys(): + for known_key in known_other: combined_config.pop(f"{KNOWN_PREFIX}{known_key}", None) combined_config["known_other"] = known_other if import_headings: - for import_heading_key in import_headings.keys(): + for import_heading_key in import_headings: combined_config.pop(f"{IMPORT_HEADING_PREFIX}{import_heading_key}") combined_config["import_headings"] = import_headings @@ -350,12 +350,12 @@ def _as_list(value: str) -> List[str]: def _abspaths(cwd: str, values: Iterable[str]) -> Set[str]: - paths = set( + paths = { os.path.join(cwd, value) if not value.startswith(os.path.sep) and value.endswith(os.path.sep) else value for value in values - ) + } return paths From 8624d30da0a107b2b94b06d8509c6211778b6153 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 17 May 2020 02:32:37 -0700 Subject: [PATCH 0557/1439] Add maintainability badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index db9b09548..bcab8c9a7 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ [![Build Status](https://travis-ci.org/timothycrosley/isort.svg?branch=develop)](https://travis-ci.org/timothycrosley/isort) [![Code coverage Status](https://codecov.io/gh/timothycrosley/isort/branch/develop/graph/badge.svg)](https://codecov.io/gh/timothycrosley/isort) [![DeepSource](https://static.deepsource.io/deepsource-badge-light-mini.svg)](https://deepsource.io/gh/timothycrosley/isort/?ref=repository-badge) +[![Maintainability](https://api.codeclimate.com/v1/badges/060372d3e77573072609/maintainability)](https://codeclimate.com/github/timothycrosley/isort/maintainability) [![License](https://img.shields.io/github/license/mashape/apistatus.svg)](https://pypi.org/project/hug/) [![Join the chat at https://gitter.im/timothycrosley/isort](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/timothycrosley/isort?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Downloads](https://pepy.tech/badge/isort)](https://pepy.tech/project/isort) From b7e6f98c2a9d01999e7b9e5ff2067a0d12f40e09 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 17 May 2020 02:33:44 -0700 Subject: [PATCH 0558/1439] Rearrange badges --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bcab8c9a7..115a35937 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,11 @@ [![PyPI version](https://badge.fury.io/py/isort.svg)](https://badge.fury.io/py/isort) [![Build Status](https://travis-ci.org/timothycrosley/isort.svg?branch=develop)](https://travis-ci.org/timothycrosley/isort) [![Code coverage Status](https://codecov.io/gh/timothycrosley/isort/branch/develop/graph/badge.svg)](https://codecov.io/gh/timothycrosley/isort) -[![DeepSource](https://static.deepsource.io/deepsource-badge-light-mini.svg)](https://deepsource.io/gh/timothycrosley/isort/?ref=repository-badge) [![Maintainability](https://api.codeclimate.com/v1/badges/060372d3e77573072609/maintainability)](https://codeclimate.com/github/timothycrosley/isort/maintainability) [![License](https://img.shields.io/github/license/mashape/apistatus.svg)](https://pypi.org/project/hug/) [![Join the chat at https://gitter.im/timothycrosley/isort](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/timothycrosley/isort?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Downloads](https://pepy.tech/badge/isort)](https://pepy.tech/project/isort) +[![DeepSource](https://static.deepsource.io/deepsource-badge-light-mini.svg)](https://deepsource.io/gh/timothycrosley/isort/?ref=repository-badge) _________________ [Read Latest Documentation](https://timothycrosley.github.io/isort/) - [Browse GitHub Code Repository](https://github.com/timothycrosley/isort/) From f5d670dc24723f2b3909efe063eb57c269a29658 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 17 May 2020 22:32:05 -0700 Subject: [PATCH 0559/1439] Attempt to move coverage reporting to github actions --- .github/workflows/test.yml | 3 +++ .travis.yml | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 54c1abce0..681d85e4f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -49,3 +49,6 @@ jobs: shell: bash run: | poetry run pytest tests/ -s --cov=isort/ --cov=tests/ --cov-report=term-missing ${@-} --cov-report html + - name: Report Coverage + if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == '3.8' + run: bash <(curl -s https://codecov.io/bash) diff --git a/.travis.yml b/.travis.yml index 7e5309fbe..4fb2f1988 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,8 +17,6 @@ matrix: language: generic script: - bash scripts/test.sh -after_script: -- bash <(curl -s https://codecov.io/bash) deploy: provider: pypi user: timothycrosley From 395727df33812690bf24ad99cfd50d6d86a7468a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 17 May 2020 22:37:46 -0700 Subject: [PATCH 0560/1439] Add coverage developer dependency --- poetry.lock | 2 +- pyproject.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 005f3469c..5d57246f1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1408,7 +1408,7 @@ requirements = ["pipreqs", "pip-api"] xdg_home = ["appdirs"] [metadata] -content-hash = "983dd12d7389a2267be62bd2f32082e7a7f66b2ef89c3298f5a688df6e70a715" +content-hash = "799667179d478618849d8fd57653d36e73e4b9176f6b88e783fc0f6e0adce3f7" python-versions = "^3.6" [metadata.files] diff --git a/pyproject.toml b/pyproject.toml index 2c91b9c1f..936be418b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,6 +71,7 @@ pip = "^20.0.2" pip-shims = "^0.5.2" smmap2 = "^3.0.1" gitdb2 = "^4.0.2" +coverage = "^5.1" [tool.poetry.scripts] isort = "isort.main:main" From 4e4d92931cc814a6544db102343910eb8002055a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 17 May 2020 22:46:05 -0700 Subject: [PATCH 0561/1439] Add coverage developer dependency --- poetry.lock | 2 +- pyproject.toml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index 5d57246f1..005f3469c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1408,7 +1408,7 @@ requirements = ["pipreqs", "pip-api"] xdg_home = ["appdirs"] [metadata] -content-hash = "799667179d478618849d8fd57653d36e73e4b9176f6b88e783fc0f6e0adce3f7" +content-hash = "983dd12d7389a2267be62bd2f32082e7a7f66b2ef89c3298f5a688df6e70a715" python-versions = "^3.6" [metadata.files] diff --git a/pyproject.toml b/pyproject.toml index 936be418b..2c91b9c1f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,7 +71,6 @@ pip = "^20.0.2" pip-shims = "^0.5.2" smmap2 = "^3.0.1" gitdb2 = "^4.0.2" -coverage = "^5.1" [tool.poetry.scripts] isort = "isort.main:main" From 99a27ea77857bac432c84e9dd5db923201b129a5 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 17 May 2020 22:50:49 -0700 Subject: [PATCH 0562/1439] Attempt different way to run codecov --- .github/workflows/test.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 681d85e4f..44b6a6f21 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -51,4 +51,6 @@ jobs: poetry run pytest tests/ -s --cov=isort/ --cov=tests/ --cov-report=term-missing ${@-} --cov-report html - name: Report Coverage if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == '3.8' - run: bash <(curl -s https://codecov.io/bash) + run: | + poetry run pip install codecov + poetry run codecov From e5b6e996e591ecfcee13eeeab04c9fa609dba34d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 17 May 2020 22:58:57 -0700 Subject: [PATCH 0563/1439] Annother attempt --- .github/workflows/test.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 44b6a6f21..c9ffff104 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -49,8 +49,4 @@ jobs: shell: bash run: | poetry run pytest tests/ -s --cov=isort/ --cov=tests/ --cov-report=term-missing ${@-} --cov-report html - - name: Report Coverage - if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == '3.8' - run: | - poetry run pip install codecov - poetry run codecov + bash <(curl -s https://codecov.io/bash) From 5f8df660cdc9d358c68d48d47d1572a8a05a0c87 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 17 May 2020 23:04:47 -0700 Subject: [PATCH 0564/1439] Annother attempt --- .github/workflows/test.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c9ffff104..b5c0ed580 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -50,3 +50,8 @@ jobs: run: | poetry run pytest tests/ -s --cov=isort/ --cov=tests/ --cov-report=term-missing ${@-} --cov-report html bash <(curl -s https://codecov.io/bash) + - name: Report Coverage + if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == '3.8' + run: | + poetry run install codecov coverage + bash <(curl -s https://codecov.io/bash) From 6cfbb84950a40112c3888bf01c7be6a2625fd107 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 17 May 2020 23:09:27 -0700 Subject: [PATCH 0565/1439] Annother attempt --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b5c0ed580..5a99587c4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -53,5 +53,5 @@ jobs: - name: Report Coverage if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == '3.8' run: | - poetry run install codecov coverage + poetry run pip install codecov coverage bash <(curl -s https://codecov.io/bash) From 91c0da918de312738fe1275b755d196031719b52 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 18 May 2020 23:23:07 -0700 Subject: [PATCH 0566/1439] Switch to official code cov action --- .github/workflows/test.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5a99587c4..c264412af 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -51,7 +51,5 @@ jobs: poetry run pytest tests/ -s --cov=isort/ --cov=tests/ --cov-report=term-missing ${@-} --cov-report html bash <(curl -s https://codecov.io/bash) - name: Report Coverage - if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == '3.8' - run: | - poetry run pip install codecov coverage - bash <(curl -s https://codecov.io/bash) + if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.8' + uses: codecov/codecov-action@v1.0.6 From 7d4a3715eb0ef6c668c6733626753934a1fd91bd Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 18 May 2020 23:28:08 -0700 Subject: [PATCH 0567/1439] Bump ubuntu versions --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c264412af..6549970df 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,7 +9,7 @@ jobs: fail-fast: false matrix: python-version: [3.6, 3.7, 3.8] - os: [ubuntu-18.04, ubuntu-16.04, macos-latest] + os: [ubuntu-latest, ubuntu-18.04, macos-latest] steps: - uses: actions/checkout@v2 From 6a68002b3a18624228148aad23a69f9c2c4c77d0 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 18 May 2020 23:44:45 -0700 Subject: [PATCH 0568/1439] Report in xml not html --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6549970df..2cc08446b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -48,7 +48,7 @@ jobs: - name: Test shell: bash run: | - poetry run pytest tests/ -s --cov=isort/ --cov=tests/ --cov-report=term-missing ${@-} --cov-report html + poetry run pytest tests/ -s --cov=isort/ --cov=tests/ --cov-report=term-missing ${@-} --cov-report xml bash <(curl -s https://codecov.io/bash) - name: Report Coverage if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.8' From bce81fb5b33d1fef5d400744d6be86e3b635cee6 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 19 May 2020 00:17:58 -0700 Subject: [PATCH 0569/1439] Report coverage to xml --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2cc08446b..c0cad2e9e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -48,8 +48,8 @@ jobs: - name: Test shell: bash run: | - poetry run pytest tests/ -s --cov=isort/ --cov=tests/ --cov-report=term-missing ${@-} --cov-report xml - bash <(curl -s https://codecov.io/bash) + poetry run pytest tests/ -s --cov=isort/ --cov=tests/ --cov-report=term-missing ${@-} + coverage xml - name: Report Coverage if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.8' uses: codecov/codecov-action@v1.0.6 From a3b32166c1c11714f1722477c97de41e79fddafb Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 19 May 2020 00:22:16 -0700 Subject: [PATCH 0570/1439] Report coverage to xml --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c0cad2e9e..3bed42198 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -49,7 +49,7 @@ jobs: shell: bash run: | poetry run pytest tests/ -s --cov=isort/ --cov=tests/ --cov-report=term-missing ${@-} - coverage xml + poetry run coverage xml - name: Report Coverage if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.8' uses: codecov/codecov-action@v1.0.6 From 48eb4c64c5d561f351b0791d7a338e08cdd399af Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 19 May 2020 00:31:58 -0700 Subject: [PATCH 0571/1439] Clean up coverage reporting config --- .coveragerc | 6 ------ .github/workflows/test.yml | 2 +- scripts/test.sh | 3 ++- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/.coveragerc b/.coveragerc index 0763398b8..e4734ab3e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -4,12 +4,6 @@ omit = except ImportError: tests/* -include = - isort/* - test_*.py - .tox/*/lib/python*/site-packages/isort/* - .tox\*\Lib\site-packages\isort\* - [report] exclude_lines = pragma: no cover diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3bed42198..3ae65a266 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -48,7 +48,7 @@ jobs: - name: Test shell: bash run: | - poetry run pytest tests/ -s --cov=isort/ --cov=tests/ --cov-report=term-missing ${@-} + poetry run pytest tests/ -s --cov=isort/ --cov-report=term-missing ${@-} poetry run coverage xml - name: Report Coverage if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.8' diff --git a/scripts/test.sh b/scripts/test.sh index eab951dad..a06598520 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -2,4 +2,5 @@ set -euxo pipefail ./scripts/lint.sh -poetry run pytest tests/ -s --cov=isort/ --cov=tests/ --cov-report=term-missing ${@-} --cov-report html +poetry run pytest tests/ -s --cov=isort/ --cov-report=term-missing ${@-} +poetry run coverage html From 34d3419d1622880989b2cb8cfb93257f76bb515e Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 19 May 2020 00:43:14 -0700 Subject: [PATCH 0572/1439] Officially move from travis to github actions --- .travis.yml | 30 ------------------------------ README.md | 3 ++- 2 files changed, 2 insertions(+), 31 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 4fb2f1988..000000000 --- a/.travis.yml +++ /dev/null @@ -1,30 +0,0 @@ -dist: xenial -language: python -cache: pip -install: -- pip3 install poetry -- poetry install -matrix: - include: - - os: linux - python: 3.6 - - os: linux - python: 3.7 - env: DEPLOY=yes - - os: linux - python: 3.8 - - os: osx - language: generic -script: -- bash scripts/test.sh -deploy: - provider: pypi - user: timothycrosley - distributions: sdist bdist_wheel - skip_existing: true - on: - tags: false - branch: master - condition: "$DEPLOY = yes" - password: - secure: SSFcjBL3dhWvSbo21icmnHQFV7mXfv/eDzxrefHUDMk37MWrvtNKchH8zz7wjAsf2PH1VYL1zkEFwnzuzHgs2aFCK7HDUwAaDSIcvPmJg9Oty+o2WQw16m7UnUac9MIZGmBHQaZuUTw0VJpm3GuPSXtdFJwFq3Tk3TIyUipEwg8= diff --git a/README.md b/README.md index 115a35937..1141280eb 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@ ------------------------------------------------------------------------ [![PyPI version](https://badge.fury.io/py/isort.svg)](https://badge.fury.io/py/isort) -[![Build Status](https://travis-ci.org/timothycrosley/isort.svg?branch=develop)](https://travis-ci.org/timothycrosley/isort) +[![Test Status](https://github.com/timothycrosley/isort/workflows/Test/badge.svg?branch=develop)](https://github.com/timothycrosley/isort/actions?query=workflow%3ATest) +[![Lint Status](https://github.com/timothycrosley/isort/workflows/Lint/badge.svg?branch=develop)](https://github.com/timothycrosley/isort/actions?query=workflow%3ALint) [![Code coverage Status](https://codecov.io/gh/timothycrosley/isort/branch/develop/graph/badge.svg)](https://codecov.io/gh/timothycrosley/isort) [![Maintainability](https://api.codeclimate.com/v1/badges/060372d3e77573072609/maintainability)](https://codeclimate.com/github/timothycrosley/isort/maintainability) [![License](https://img.shields.io/github/license/mashape/apistatus.svg)](https://pypi.org/project/hug/) From ee588dbb35e986a2f859ff75504331cdf5e0b5f2 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 19 May 2020 01:02:01 -0700 Subject: [PATCH 0573/1439] Attempt to install poetry on windows --- .github/workflows/test.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3ae65a266..6f76561e6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,11 +9,16 @@ jobs: fail-fast: false matrix: python-version: [3.6, 3.7, 3.8] - os: [ubuntu-latest, ubuntu-18.04, macos-latest] + os: [ubuntu-latest, ubuntu-18.04, macos-latest, windows-latest] steps: - uses: actions/checkout@v2 + - name: Install Poetry Windows + if: matrix.os == 'windows-latest' + run: | + curl -O -sSL https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py + python get-poetry.py - name: Ubuntu cache uses: actions/cache@v1 if: startsWith(matrix.os, 'ubuntu') From e4dfeba87437d7ae129357a2c2ea2f30109b305d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 19 May 2020 01:11:41 -0700 Subject: [PATCH 0574/1439] Check if windows install special step is needed --- .github/workflows/test.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6f76561e6..ae8edb547 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,12 +13,6 @@ jobs: steps: - uses: actions/checkout@v2 - - - name: Install Poetry Windows - if: matrix.os == 'windows-latest' - run: | - curl -O -sSL https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py - python get-poetry.py - name: Ubuntu cache uses: actions/cache@v1 if: startsWith(matrix.os, 'ubuntu') From adc49634c7a95d3b356cc37b4f4d30598c9cee76 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 19 May 2020 01:18:50 -0700 Subject: [PATCH 0575/1439] Attempt to add windows cache --- .github/workflows/test.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ae8edb547..7244bb5c4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,6 +33,16 @@ jobs: restore-keys: | ${{ matrix.os }}-${{ matrix.python-version }}- + - name: Windows cache + uses: actions/cache@v1 + if: startsWith(matrix.os, 'windows') + with: + path: ~\AppData\Local\pip\cache + key: + ${{ matrix.os }}-${{ matrix.python-version }}-${{ hashFiles('**/pyproject.toml') }} + restore-keys: | + ${{ matrix.os }}-${{ matrix.python-version }}- + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 with: From 4e5ccd33f91496c3a114db3afa8b4f891228caed Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 19 May 2020 01:26:29 -0700 Subject: [PATCH 0576/1439] Remove app veyor --- .appveyor.yml | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 .appveyor.yml diff --git a/.appveyor.yml b/.appveyor.yml deleted file mode 100644 index 3e729d36e..000000000 --- a/.appveyor.yml +++ /dev/null @@ -1,13 +0,0 @@ -init: - - "SET PATH=C:\\Python38\\Scripts;%PATH%" - - "SET PYTEST_ADDOPTS=-vv" - - "ECHO PATH=%PATH%" - -build: off - -install: - - pip3 install poetry - - poetry install - -test_script: - - poetry run pytest tests/ -s --cov=isort/ --cov=tests/ --cov-report=term-missing --cov-report html From 6d1f1751ee780eff9ec61cf69132d2256328ee5c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 19 May 2020 01:35:33 -0700 Subject: [PATCH 0577/1439] Print cache dir --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7244bb5c4..fbb74521c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -51,6 +51,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip + echo `pip cache dir` python -m pip install --upgrade poetry poetry install From 12979180af4c6ec4dea9d908f1cc2db9d7054ae3 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 19 May 2020 01:41:53 -0700 Subject: [PATCH 0578/1439] Try harder to see cache dir --- .github/workflows/test.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fbb74521c..802ca27d8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -51,9 +51,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - echo `pip cache dir` - python -m pip install --upgrade poetry - poetry install + pip cache dir - name: Test shell: bash From eac2fe171742b0e75d1ee180efe8a61736530ea5 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 19 May 2020 01:45:11 -0700 Subject: [PATCH 0579/1439] Try more direct cache --- .github/workflows/test.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 802ca27d8..71788b54b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -37,7 +37,7 @@ jobs: uses: actions/cache@v1 if: startsWith(matrix.os, 'windows') with: - path: ~\AppData\Local\pip\cache + path: c:\users\runneradmin\appdata\local\pip\cache key: ${{ matrix.os }}-${{ matrix.python-version }}-${{ hashFiles('**/pyproject.toml') }} restore-keys: | @@ -51,7 +51,8 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip cache dir + python -m pip install --upgrade poetry + poetry install - name: Test shell: bash From 6ebf42d31d307a2d3790d3e7a372ba9d7e69f47a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 19 May 2020 02:26:08 -0700 Subject: [PATCH 0580/1439] Improve test coverage fix noqa interface --- isort/wrap_modes.py | 11 +++++------ tests/test_wrap_modes.py | 5 +++++ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/isort/wrap_modes.py b/isort/wrap_modes.py index b9ba875d6..17e8fc743 100644 --- a/isort/wrap_modes.py +++ b/isort/wrap_modes.py @@ -274,16 +274,15 @@ def noqa(**interface): <= interface["line_length"] ): return f"{retval}{interface['comment_prefix']} {comment_str}" - else: - if len(retval) <= interface["line_length"]: - return retval - if interface["comments"]: - if "NOQA" in interface["comments"]: + elif "NOQA" in interface["comments"]: return f"{retval}{interface['comment_prefix']} {comment_str}" else: return f"{retval}{interface['comment_prefix']} NOQA {comment_str}" else: - return f"{retval}{interface['comment_prefix']} NOQA" + if len(retval) <= interface["line_length"]: + return retval + else: + return f"{retval}{interface['comment_prefix']} NOQA" @_wrap_mode diff --git a/tests/test_wrap_modes.py b/tests/test_wrap_modes.py index 31742b2a8..13ac45cdb 100644 --- a/tests/test_wrap_modes.py +++ b/tests/test_wrap_modes.py @@ -15,6 +15,11 @@ wrap_modes.vertical_prefix_from_module_import, auto_allow_exceptions_=(ValueError,) ) auto_pytest_magic(wrap_modes.vertical_hanging_indent_bracket, auto_allow_exceptions_=(ValueError,)) +auto_pytest_magic( + wrap_modes.vertical_hanging_indent_bracket, + auto_allow_exceptions_=(ValueError,), + imports=["one", "two"], +) def test_wrap_mode_interface(): From d1623bab6797aaf18dc82813572cab122b5bba55 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 19 May 2020 21:53:25 -0700 Subject: [PATCH 0581/1439] Add - Brad Solomon (@bsolomon1124) to documentation contributor acknowledgements --- docs/contributing/4.-acknowledgements.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index 88d190dfa..0f71f497f 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -104,6 +104,7 @@ Documenters - Sarah Beth Tracy (@sbtries) - Aaron Brown (@aaronvbrown) - Harutaka Kawamura (@harupy) +- Brad Solomon (@bsolomon1124) -------------------------------------------- From dfc60dcfc225e018cdf77b5c7e9329a697347960 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 20 May 2020 21:48:43 -0700 Subject: [PATCH 0582/1439] Add - Honnix (@honnix) to code contributors --- docs/contributing/4.-acknowledgements.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index 0f71f497f..b346708d0 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -91,6 +91,7 @@ Code Contributors - Chris St. Pierre (@stpierre) - Sebastian Rittau (@srittau) - João M.C. Teixeira (@joaomcteixeira) +- Honnix (@honnix) Documenters =================== From b52b0a09dabdd0bf265c40f8b941303fa5a2812b Mon Sep 17 00:00:00 2001 From: Anders Kaseorg Date: Fri, 22 May 2020 13:55:07 -0700 Subject: [PATCH 0583/1439] sort_file: Preserve permission bits isort 4 would overwrite the old file in place, so its permission bits would not be modified. But as of commit 1920a6c47796bec4ddc2141dbaece6b44a93834e or so, isort instead writes a new file with default permission bits and renames it into place. So we now need to explicitly copy the permission bits. Fixes #1194. Signed-off-by: Anders Kaseorg --- isort/api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/isort/api.py b/isort/api.py index 46d543565..cc8d7cd20 100644 --- a/isort/api.py +++ b/isort/api.py @@ -1,3 +1,4 @@ +import shutil import sys import textwrap from io import StringIO @@ -479,6 +480,7 @@ def sort_file( with tmp_file.open( "w", encoding=source_file.encoding, newline="" ) as output_stream: + shutil.copymode(filename, tmp_file) changed = sorted_imports( input_stream=source_file.stream, output_stream=output_stream, From 30296c75932ccfb17f9b9008eeb4423520884fcc Mon Sep 17 00:00:00 2001 From: Timothy Edmund Crosley Date: Fri, 22 May 2020 21:46:16 -0700 Subject: [PATCH 0584/1439] Add Anders Kaseorg (@andersk) to contributors list --- docs/contributing/4.-acknowledgements.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index b346708d0..dc5f5108d 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -92,6 +92,8 @@ Code Contributors - Sebastian Rittau (@srittau) - João M.C. Teixeira (@joaomcteixeira) - Honnix (@honnix) +- Anders Kaseorg (@andersk) + Documenters =================== From 696d168a8962b3785881f5fa98b2b616035e54c1 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 23 May 2020 23:58:01 -0700 Subject: [PATCH 0585/1439] Start saving some hypothesis auto test cases to ensure consistent test coverage --- tests/test_wrap_modes.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/test_wrap_modes.py b/tests/test_wrap_modes.py index 13ac45cdb..2c841d99e 100644 --- a/tests/test_wrap_modes.py +++ b/tests/test_wrap_modes.py @@ -26,3 +26,24 @@ def test_wrap_mode_interface(): assert ( wrap_modes._wrap_mode_interface("statement", [], "", "", 80, [], "", "", True, True) == "" ) + + +def test_auto_saved(): + """hypothesis_auto tests cases that have been saved to ensure they run each test cycle""" + assert ( + wrap_modes.noqa( + **{ + "comment_prefix": "-\U000bf82c\x0c\U0004608f\x10%", + "comments": [], + "imports": [], + "include_trailing_comma": False, + "indent": "0\x19", + "line_length": -19659, + "line_separator": "\x15\x0b\U00086494\x1d\U000e00a2\U000ee216\U0006708a\x03\x1f", + "remove_comments": False, + "statement": "\U00092452", + "white_space": "\U000a7322\U000c20e3-\U0010eae4\x07\x14\U0007d486", + } + ) + == "\U00092452-\U000bf82c\x0c\U0004608f\x10% NOQA" + ) From 80fec4d92b7b155e37e4ecac6711c35df98f297c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 24 May 2020 00:07:57 -0700 Subject: [PATCH 0586/1439] Add additional finder test case --- tests/test_finders.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_finders.py b/tests/test_finders.py index 2e54317d6..94acfdfd4 100644 --- a/tests/test_finders.py +++ b/tests/test_finders.py @@ -82,6 +82,10 @@ def test_conda_and_virtual_env(self, tmpdir): assert conda.find("z") == "THIRDPARTY" assert conda.find("os") == venv.find("os") == "STDLIB" + def test_default_section(self, tmpdir): + tmpdir.join("file.py").write("import b\nimport a\n") + assert self.kind(settings.Config(default_section="CUSTOM"), tmpdir).find("file") == "CUSTOM" + class TestPipfileFinder(AbstractTestFinder): kind = finders.PipfileFinder From 8ddf2c6da31e2f1d2dc0eff8016a17d3cdba533f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 25 May 2020 23:52:48 -0700 Subject: [PATCH 0587/1439] Split up creation and usage of parser to enable introspection --- isort/main.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/isort/main.py b/isort/main.py index 90b54d203..0dc198574 100644 --- a/isort/main.py +++ b/isort/main.py @@ -126,7 +126,7 @@ def iter_source_code(paths: Iterable[str], config: Config, skipped: List[str]) - yield path -def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: +def _build_arg_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( description="Sort Python import definitions alphabetically " "within logical sections. Run with no arguments to run " @@ -544,7 +544,11 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: action="store_true", help="See isort's determined config, as well as sources of config options.", ) + return parser + +def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: + parser = _build_arg_parser() arguments = {key: value for key, value in vars(parser.parse_args(argv)).items() if value} if "dont_order_by_type" in arguments: arguments["order_by_type"] = False From 509bcd4cf3a51c9d0d9e45f41f5b6ba3183bef21 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 25 May 2020 23:53:06 -0700 Subject: [PATCH 0588/1439] Add script to build config option docs --- scripts/build_config_option_docs.py | 76 +++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100755 scripts/build_config_option_docs.py diff --git a/scripts/build_config_option_docs.py b/scripts/build_config_option_docs.py new file mode 100755 index 000000000..0dd20d65f --- /dev/null +++ b/scripts/build_config_option_docs.py @@ -0,0 +1,76 @@ +#! /bin/env python +import os + +from isort.settings import _DEFAULT_SETTINGS as config +from isort.main import _build_arg_parser +from typing import Generator + +OUTPUT_FILE = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), + "../docs/configuration/options.md")) +HUMAN_NAME = {"py_version": "Python Version"} +DESCRIPTIONS = {} +IGNORED = {"source", "help"} +COLUMNS = ["Name", "Type", "Default", "Python / Config file", "CLI", "Description"] +HEADER = f"""Configuration options for isort +======== + +As a code formatter isort has opinions. However, it also allows you to also have your own. If your opinions disagree with those of isort, +isort will disagree but commit to your way of formatting. To enable this, isort exposes a plethora of options to specify +how you want your imports sorted. + +| {' | '.join(COLUMNS)} | +""" +parser = _build_arg_parser() + + +def human(name: str) -> str: + if name in HUMAN_NAME: + return HUMAN_NAME[name] + + return " ".join([part.capitalize() for part in name.replace("-", "_").split("_")]) + + +def config_options() -> Generator[str, None, None]: + cli_actions = {} + for action in parser._actions: + cli_actions[action.dest] = action + + for name, default in config.items(): + if name in IGNORED: + continue + + description = DESCRIPTIONS.get(name, "") + + cli = cli_actions.pop(name, None) + if cli: + cli_options = ','.join(cli.option_strings) + description = description or cli.help + else: + cli_options = "**Not Supported**" + + yield f"|{human(name)}|{human(type(default).__name__)}|{default}|{name}|{cli_options}|{description}|\n" + + for name, cli in cli_actions.items(): + if name in IGNORED: + continue + + if cli.type: + type_name = human(cli.type.__name__) + elif cli.default is not None: + type_name = human(type(cli.default).__name__) + else: + type_name = "*Not Typed*" + yield f"|{human(name)}|{type_name}|{cli.default}|**Not Supported**|{','.join(cli.option_strings)}|{DESCRIPTIONS.get(name, cli.help)}|\n" + + +def document_text() -> str: + return f"{HEADER}{''.join(config_options())}" + + +def write_document(): + with open(OUTPUT_FILE, "w") as output_file: + output_file.write(document_text()) + + +if __name__ == "__main__": + write_document() From e42438224744649f5157c1d6fbd768cd9ed0463a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 25 May 2020 23:53:17 -0700 Subject: [PATCH 0589/1439] Add initiail config option docs --- docs/configuration/options.md | 81 +++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 docs/configuration/options.md diff --git a/docs/configuration/options.md b/docs/configuration/options.md new file mode 100644 index 000000000..829dfa73a --- /dev/null +++ b/docs/configuration/options.md @@ -0,0 +1,81 @@ +Configuration options for isort +======== + +As a code formatter isort has opinions. However, it also allows you to also have your own. If your opinions disagree with those of isort, +isort will disagree but commit to your way of formatting. To enable this, isort exposes a plethora of options to specify +how you want your imports sorted. + +| Name | Type | Default | Python / Config file | CLI | Description | +| ---- | ---- | ------- | -------------------- | --- | ----------- | +|Python Version|Str|py3|py_version|--py,--python-version|Tells isort to set the known standard library based on the the specified Python version. Default is to assume any Python 3 version could be the target, and use a union off all stdlib modules across versions. If auto is specified, the version of the interpreter used to run isort (currently: 38) will be used.| +|Force To Top|Frozenset|frozenset()|force_to_top|-t,--top|Force specific imports to the top of their appropriate section.| +|Skip|Frozenset|frozenset({'build', '.eggs', 'dist', '.nox', '_build', '.hg', '.venv', '.git', '.pants.d', 'node_modules', 'buck-out', '.mypy_cache', '.tox', 'venv'})|skip|-s,--skip|Files that sort imports should skip over. If you want to skip multiple files you should specify twice: --skip file1 --skip file2.| +|Skip Glob|Frozenset|frozenset()|skip_glob|--sg,--skip-glob|Files that sort imports should skip over.| +|Line Length|Int|79|line_length|-l,-w,--line-length,--line-width|The max length of an import line (used for wrapping long imports).| +|Wrap Length|Int|0|wrap_length|--wl,--wrap-length|Specifies how long lines that are wrapped should be, if not set line_length is used. +NOTE: wrap_length must be LOWER than or equal to line_length.| +|Line Ending|Str||line_ending|--le,--line-ending|Forces line endings to the specified value. If not set, values will be guessed per-file.| +|Sections|Tuple|('FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER')|sections|**Not Supported**|| +|No Sections|Bool|False|no_sections|--ds,--no-sections|Put all imports into the same section bucket| +|Known Future Library|Frozenset|frozenset({'__future__'})|known_future_library|-f,--future|Force sortImports to recognize a module as part of the future compatibility libraries.| +|Known Third Party|Frozenset|frozenset({'google.appengine.api'})|known_third_party|-o,--thirdparty|Force sortImports to recognize a module as being part of a third party library.| +|Known First Party|Frozenset|frozenset()|known_first_party|-p,--project|Force sortImports to recognize a module as being part of the current python project.| +|Known Standard Library|Frozenset|frozenset({'contextlib', 'shutil', 'urllib', 'unicodedata', 'mimetypes', 'msvcrt', 'calendar', 'lzma', 'builtins', 'concurrent', 'ssl', 'sched', 'turtledemo', 'shlex', 'spwd', 'zipfile', 'uuid', 'msilib', 'cProfile', 'grp', 'contextvars', 'copyreg', 'imaplib', 'fpectl', 'traceback', 'bdb', 'tty', 'mailbox', 'sys', 'xmlrpc', 'subprocess', 'rlcompleter', 'zlib', 'uu', 'filecmp', 'asyncore', 'symbol', 'argparse', 'html', 'pydoc', 'ipaddress', 'pstats', 'sqlite3', 'functools', 'turtle', 'json', 'tracemalloc', 'getpass', 'hashlib', '_dummy_thread', 'cmath', 'pkgutil', 'dbm', 'asyncio', 'fractions', 'decimal', 'resource', 'inspect', 'socketserver', 'sndhdr', 'faulthandler', 'syslog', 'xdrlib', 'py_compile', 'stringprep', 'datetime', 'shelve', 'webbrowser', 'poplib', 'optparse', 'socket', 'imp', 'telnetlib', 'ast', 'atexit', 'ossaudiodev', 'bisect', 'fileinput', 'dis', 'colorsys', 'pdb', 'cgi', 'formatter', 'tempfile', 'cmd', 'ctypes', 'errno', 'profile', 'signal', 'tabnanny', 'chunk', 'pwd', 'asynchat', 'platform', 'zipimport', 'termios', 'ftplib', 'marshal', 'email', 'token', 'array', 'crypt', 'statistics', 'binhex', 'codeop', 'doctest', 'pickletools', 'plistlib', 'runpy', 'macpath', 'readline', 'locale', 'wave', 'fcntl', 'pprint', 'random', 'keyword', 'difflib', 'compileall', 'modulefinder', 'string', 'copy', 'mmap', 'audioop', 'csv', 'linecache', 'quopri', 'abc', 'multiprocessing', 'collections', 'configparser', 'enum', 'imghdr', 'smtpd', 'nis', 'secrets', 'logging', 'unittest', 'sysconfig', 'zipapp', 'warnings', 'pyclbr', 'symtable', 'dummy_threading', 'codecs', 'types', 'cgitb', 'mailcap', 'code', 'tkinter', 'operator', 'lib2to3', 'os', 'threading', 'bz2', 'trace', '_thread', 'pty', 'distutils', 'gzip', 'gettext', 'parser', 'pipes', 'reprlib', 'heapq', 'getopt', 'encodings', 'time', 'io', 'textwrap', 'struct', 'wsgiref', 'binascii', 'sunau', 'smtplib', 'pickle', 'selectors', 'tarfile', 'math', 'weakref', 'stat', 'queue', 'venv', 'glob', 'fnmatch', 'base64', 'select', 'gc', 'dataclasses', 're', 'http', 'itertools', 'xml', 'ensurepip', 'importlib', 'hmac', 'typing', 'nntplib', 'winsound', 'test', 'numbers', 'posix', 'timeit', 'pathlib', 'tokenize', 'site', 'winreg', 'netrc', 'aifc', 'curses'})|known_standard_library|-b,--builtin|Force sortImports to recognize a module as part of the python standard library.| +|Known Other|Dict|{}|known_other|**Not Supported**|| +|Multi Line Output|Wrapmodes|WrapModes.GRID|multi_line_output|-m,--multi-line|Multi line output (0-grid, 1-vertical, 2-hanging, 3-vert-hanging, 4-vert-grid, 5-vert-grid-grouped, 6-vert-grid-grouped-no-comma).| +|Forced Separate|Tuple|()|forced_separate|**Not Supported**|| +|Indent|Str| |indent|-i,--indent|String to place for indents defaults to " " (4 spaces).| +|Comment Prefix|Str| #|comment_prefix|**Not Supported**|| +|Length Sort|Bool|False|length_sort|--ls,--length-sort|Sort imports by their string length.| +|Length Sort Sections|Frozenset|frozenset()|length_sort_sections|**Not Supported**|| +|Add Imports|Frozenset|frozenset()|add_imports|-a,--add-import|Adds the specified import line to all files, automatically determining correct placement.| +|Remove Imports|Frozenset|frozenset()|remove_imports|--rm,--remove-import|Removes the specified import from all files.| +|Reverse Relative|Bool|False|reverse_relative|--rr,--reverse-relative|Reverse order of relative imports.| +|Force Single Line|Bool|False|force_single_line|--sl,--force-single-line-imports|Forces all from imports to appear on their own line| +|Single Line Exclusions|Tuple|()|single_line_exclusions|--nsl,--single-line-exclusions|One or more modules to exclude from the single line rule.| +|Default Section|Str|FIRSTPARTY|default_section|--sd,--section-default|Sets the default section for imports (by default FIRSTPARTY) options: ('FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER')| +|Import Headings|Dict|{}|import_headings|**Not Supported**|| +|Balanced Wrapping|Bool|False|balanced_wrapping|-e,--balanced|Balances wrapping to produce the most consistent line length possible| +|Use Parentheses|Bool|False|use_parentheses|--up,--use-parentheses|Use parenthesis for line continuation on length limit instead of slashes.| +|Order By Type|Bool|True|order_by_type|--ot,--order-by-type|Order imports by type in addition to alphabetically| +|Atomic|Bool|False|atomic|--ac,--atomic|Ensures the output doesn't save if the resulting file contains syntax errors.| +|Lines After Imports|Int|-1|lines_after_imports|--lai,--lines-after-imports|None| +|Lines Between Sections|Int|1|lines_between_sections|**Not Supported**|| +|Lines Between Types|Int|0|lines_between_types|--lbt,--lines-between-types|None| +|Combine As Imports|Bool|False|combine_as_imports|--ca,--combine-as|Combines as imports on the same line.| +|Combine Star|Bool|False|combine_star|--cs,--combine-star|Ensures that if a star import is present, nothing else is imported from that namespace.| +|Keep Direct And As Imports|Bool|True|keep_direct_and_as_imports|-k,--keep-direct-and-as|Turns off default behavior that removes direct imports when as imports exist.| +|Include Trailing Comma|Bool|False|include_trailing_comma|--tc,--trailing-comma|Includes a trailing comma on multi line imports that include parentheses.| +|From First|Bool|False|from_first|--ff,--from-first|Switches the typical ordering preference, showing from imports first then straight ones.| +|Verbose|Bool|False|verbose|-v,--verbose|Shows verbose output, such as when files are skipped or when a check is successful.| +|Quiet|Bool|False|quiet|-q,--quiet|Shows extra quiet output, only errors are outputted.| +|Force Adds|Bool|False|force_adds|--af,--force-adds|Forces import adds even if the original file is empty.| +|Force Alphabetical Sort Within Sections|Bool|False|force_alphabetical_sort_within_sections|**Not Supported**|| +|Force Alphabetical Sort|Bool|False|force_alphabetical_sort|--fass,--force-alphabetical-sort-within-sections|Force all imports to be sorted alphabetically within a section| +|Force Grid Wrap|Int|0|force_grid_wrap|--fgw,--force-grid-wrap|Force number of from imports (defaults to 2) to be grid wrapped regardless of line length| +|Force Sort Within Sections|Bool|False|force_sort_within_sections|--fss,--force-sort-within-sections|Force imports to be sorted by module, independent of import_type| +|Lexicographical|Bool|False|lexicographical|**Not Supported**|| +|Ignore Whitespace|Bool|False|ignore_whitespace|--ws,--ignore-whitespace|Tells isort to ignore whitespace differences when --check-only is being used.| +|No Lines Before|Frozenset|frozenset()|no_lines_before|--nlb,--no-lines-before|Sections which should not be split with previous by empty lines| +|No Inline Sort|Bool|False|no_inline_sort|--nis,--no-inline-sort|Leaves `from` imports with multiple imports 'as-is' (e.g. `from foo import a, c ,b`).| +|Ignore Comments|Bool|False|ignore_comments|**Not Supported**|| +|Case Sensitive|Bool|False|case_sensitive|--case-sensitive|Tells isort to include casing when sorting module names| +|Sources|Tuple|()|sources|**Not Supported**|| +|Virtual Env|Str||virtual_env|--virtual-env|Virtual environment to use for determining whether a package is third-party| +|Conda Env|Str||conda_env|--conda-env|Conda environment to use for determining whether a package is third-party| +|Ensure Newline Before Comments|Bool|False|ensure_newline_before_comments|-n,--ensure-newline-before-comments|Inserts a blank line before a comment following an import.| +|Directory|Str||directory|**Not Supported**|| +|Profile|Str||profile|--profile|Base profile type to use for configuration.| +|Check|Bool|False|**Not Supported**|-c,--check-only,--check|Checks the file for unsorted / unformatted imports and prints them to the command line without modifying the file.| +|Write To Stdout|Bool|False|**Not Supported**|-d,--stdout|Force resulting output to stdout, instead of in-place.| +|Show Diff|Bool|False|**Not Supported**|--df,--diff|Prints a diff of all the changes isort would make to a file, instead of changing it in place| +|Jobs|Int|None|**Not Supported**|-j,--jobs|Number of files to process in parallel.| +|Dont Order By Type|Bool|False|**Not Supported**|--dt,--dont-order-by-type|Don't order imports by type in addition to alphabetically| +|Settings Path|*Not Typed*|None|**Not Supported**|--sp,--settings-path,--settings-file,--settings|Explicitly set the settings path or file instead of auto determining based on file location.| +|Show Version|Bool|False|**Not Supported**|-V,--version|Displays the currently installed version of isort.| +|Vn|Str|==SUPPRESS==|**Not Supported**|--vn,--version-number|Returns just the current version number without the logo| +|Filter Files|Bool|False|**Not Supported**|--filter-files|Tells isort to filter files even when they are explicitly passed in as part of the command| +|Files|*Not Typed*|None|**Not Supported**||One or more Python source files that need their imports sorted.| +|Ask To Apply|Bool|False|**Not Supported**|--interactive|Tells isort to apply changes interactively.| +|Show Config|Bool|False|**Not Supported**|--show-config|See isort's determined config, as well as sources of config options.| + From 7d7a49dd69c2657b60950712fe5f407133af6ee1 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 25 May 2020 23:59:34 -0700 Subject: [PATCH 0590/1439] Auto clean scripts --- scripts/clean.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/clean.sh b/scripts/clean.sh index 1a5b493e2..d3aa1e6b4 100755 --- a/scripts/clean.sh +++ b/scripts/clean.sh @@ -1,5 +1,5 @@ #!/bin/bash set -euxo pipefail -poetry run isort --profile hug isort/ tests/ -poetry run black isort/ tests/ -l 100 +poetry run isort --profile hug isort/ tests/ scripts/ +poetry run black isort/ tests/ scripts/ -l 100 From c6220552a38c0acb0d75f040c0961179a2d83ae6 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 26 May 2020 00:00:01 -0700 Subject: [PATCH 0591/1439] Cleaned build_config_option_docs, improved handling of descriptions --- scripts/build_config_option_docs.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/scripts/build_config_option_docs.py b/scripts/build_config_option_docs.py index 0dd20d65f..b60f8e892 100755 --- a/scripts/build_config_option_docs.py +++ b/scripts/build_config_option_docs.py @@ -1,12 +1,13 @@ #! /bin/env python import os +from typing import Generator -from isort.settings import _DEFAULT_SETTINGS as config from isort.main import _build_arg_parser -from typing import Generator +from isort.settings import _DEFAULT_SETTINGS as config -OUTPUT_FILE = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), - "../docs/configuration/options.md")) +OUTPUT_FILE = os.path.abspath( + os.path.join(os.path.dirname(os.path.abspath(__file__)), "../docs/configuration/options.md") +) HUMAN_NAME = {"py_version": "Python Version"} DESCRIPTIONS = {} IGNORED = {"source", "help"} @@ -43,10 +44,11 @@ def config_options() -> Generator[str, None, None]: cli = cli_actions.pop(name, None) if cli: - cli_options = ','.join(cli.option_strings) + cli_options = ",".join(cli.option_strings) description = description or cli.help else: cli_options = "**Not Supported**" + description = description.replace("\n", " ") yield f"|{human(name)}|{human(type(default).__name__)}|{default}|{name}|{cli_options}|{description}|\n" @@ -60,7 +62,9 @@ def config_options() -> Generator[str, None, None]: type_name = human(type(cli.default).__name__) else: type_name = "*Not Typed*" - yield f"|{human(name)}|{type_name}|{cli.default}|**Not Supported**|{','.join(cli.option_strings)}|{DESCRIPTIONS.get(name, cli.help)}|\n" + + description = DESCRIPTIONS.get(name, cli.help).replace("\n", " ") + yield f"|{human(name)}|{type_name}|{cli.default}|**Not Supported**|{','.join(cli.option_strings)}|{description}|\n" def document_text() -> str: From 174026a15368742f5a18f0cbdbec7cbcc3657d70 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 26 May 2020 00:01:02 -0700 Subject: [PATCH 0592/1439] Update config option documentation --- docs/configuration/options.md | 13 +++++-------- scripts/build_config_option_docs.py | 4 ++-- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/docs/configuration/options.md b/docs/configuration/options.md index 829dfa73a..be6bb59b2 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -6,21 +6,19 @@ isort will disagree but commit to your way of formatting. To enable this, isort how you want your imports sorted. | Name | Type | Default | Python / Config file | CLI | Description | -| ---- | ---- | ------- | -------------------- | --- | ----------- | |Python Version|Str|py3|py_version|--py,--python-version|Tells isort to set the known standard library based on the the specified Python version. Default is to assume any Python 3 version could be the target, and use a union off all stdlib modules across versions. If auto is specified, the version of the interpreter used to run isort (currently: 38) will be used.| |Force To Top|Frozenset|frozenset()|force_to_top|-t,--top|Force specific imports to the top of their appropriate section.| -|Skip|Frozenset|frozenset({'build', '.eggs', 'dist', '.nox', '_build', '.hg', '.venv', '.git', '.pants.d', 'node_modules', 'buck-out', '.mypy_cache', '.tox', 'venv'})|skip|-s,--skip|Files that sort imports should skip over. If you want to skip multiple files you should specify twice: --skip file1 --skip file2.| +|Skip|Frozenset|frozenset({'.venv', '.mypy_cache', 'build', '_build', '.nox', '.git', 'dist', 'node_modules', '.pants.d', '.tox', '.eggs', 'buck-out', '.hg', 'venv'})|skip|-s,--skip|Files that sort imports should skip over. If you want to skip multiple files you should specify twice: --skip file1 --skip file2.| |Skip Glob|Frozenset|frozenset()|skip_glob|--sg,--skip-glob|Files that sort imports should skip over.| |Line Length|Int|79|line_length|-l,-w,--line-length,--line-width|The max length of an import line (used for wrapping long imports).| -|Wrap Length|Int|0|wrap_length|--wl,--wrap-length|Specifies how long lines that are wrapped should be, if not set line_length is used. -NOTE: wrap_length must be LOWER than or equal to line_length.| +|Wrap Length|Int|0|wrap_length|--wl,--wrap-length|Specifies how long lines that are wrapped should be, if not set line_length is used. NOTE: wrap_length must be LOWER than or equal to line_length.| |Line Ending|Str||line_ending|--le,--line-ending|Forces line endings to the specified value. If not set, values will be guessed per-file.| |Sections|Tuple|('FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER')|sections|**Not Supported**|| |No Sections|Bool|False|no_sections|--ds,--no-sections|Put all imports into the same section bucket| |Known Future Library|Frozenset|frozenset({'__future__'})|known_future_library|-f,--future|Force sortImports to recognize a module as part of the future compatibility libraries.| |Known Third Party|Frozenset|frozenset({'google.appengine.api'})|known_third_party|-o,--thirdparty|Force sortImports to recognize a module as being part of a third party library.| |Known First Party|Frozenset|frozenset()|known_first_party|-p,--project|Force sortImports to recognize a module as being part of the current python project.| -|Known Standard Library|Frozenset|frozenset({'contextlib', 'shutil', 'urllib', 'unicodedata', 'mimetypes', 'msvcrt', 'calendar', 'lzma', 'builtins', 'concurrent', 'ssl', 'sched', 'turtledemo', 'shlex', 'spwd', 'zipfile', 'uuid', 'msilib', 'cProfile', 'grp', 'contextvars', 'copyreg', 'imaplib', 'fpectl', 'traceback', 'bdb', 'tty', 'mailbox', 'sys', 'xmlrpc', 'subprocess', 'rlcompleter', 'zlib', 'uu', 'filecmp', 'asyncore', 'symbol', 'argparse', 'html', 'pydoc', 'ipaddress', 'pstats', 'sqlite3', 'functools', 'turtle', 'json', 'tracemalloc', 'getpass', 'hashlib', '_dummy_thread', 'cmath', 'pkgutil', 'dbm', 'asyncio', 'fractions', 'decimal', 'resource', 'inspect', 'socketserver', 'sndhdr', 'faulthandler', 'syslog', 'xdrlib', 'py_compile', 'stringprep', 'datetime', 'shelve', 'webbrowser', 'poplib', 'optparse', 'socket', 'imp', 'telnetlib', 'ast', 'atexit', 'ossaudiodev', 'bisect', 'fileinput', 'dis', 'colorsys', 'pdb', 'cgi', 'formatter', 'tempfile', 'cmd', 'ctypes', 'errno', 'profile', 'signal', 'tabnanny', 'chunk', 'pwd', 'asynchat', 'platform', 'zipimport', 'termios', 'ftplib', 'marshal', 'email', 'token', 'array', 'crypt', 'statistics', 'binhex', 'codeop', 'doctest', 'pickletools', 'plistlib', 'runpy', 'macpath', 'readline', 'locale', 'wave', 'fcntl', 'pprint', 'random', 'keyword', 'difflib', 'compileall', 'modulefinder', 'string', 'copy', 'mmap', 'audioop', 'csv', 'linecache', 'quopri', 'abc', 'multiprocessing', 'collections', 'configparser', 'enum', 'imghdr', 'smtpd', 'nis', 'secrets', 'logging', 'unittest', 'sysconfig', 'zipapp', 'warnings', 'pyclbr', 'symtable', 'dummy_threading', 'codecs', 'types', 'cgitb', 'mailcap', 'code', 'tkinter', 'operator', 'lib2to3', 'os', 'threading', 'bz2', 'trace', '_thread', 'pty', 'distutils', 'gzip', 'gettext', 'parser', 'pipes', 'reprlib', 'heapq', 'getopt', 'encodings', 'time', 'io', 'textwrap', 'struct', 'wsgiref', 'binascii', 'sunau', 'smtplib', 'pickle', 'selectors', 'tarfile', 'math', 'weakref', 'stat', 'queue', 'venv', 'glob', 'fnmatch', 'base64', 'select', 'gc', 'dataclasses', 're', 'http', 'itertools', 'xml', 'ensurepip', 'importlib', 'hmac', 'typing', 'nntplib', 'winsound', 'test', 'numbers', 'posix', 'timeit', 'pathlib', 'tokenize', 'site', 'winreg', 'netrc', 'aifc', 'curses'})|known_standard_library|-b,--builtin|Force sortImports to recognize a module as part of the python standard library.| +|Known Standard Library|Frozenset|frozenset({'quopri', 'gc', 'sqlite3', 'zipfile', 'enum', 'posix', 'fileinput', 'io', 'pty', 'sched', 'heapq', 'crypt', 'doctest', 'bdb', 'venv', 'symbol', 'operator', 'cgitb', 'typing', 'shelve', 'ipaddress', 'nis', 'gzip', 'zipimport', 'fpectl', 'compileall', 'aifc', 'smtplib', 'difflib', 'grp', 'selectors', 'faulthandler', 'code', 'xml', 'time', 'distutils', 'cmath', 'importlib', 'textwrap', 'pyclbr', 'itertools', 'zipapp', 'pstats', 'keyword', 'pwd', 'inspect', 'csv', 'warnings', 'stringprep', 'cmd', 'decimal', 'termios', 'codeop', 'reprlib', 'os', 'tabnanny', 'syslog', 'errno', 'string', 'types', 'hmac', 'profile', 'fractions', 'formatter', 'curses', 'nntplib', 'pkgutil', 'locale', 'trace', 'telnetlib', 'sndhdr', 'smtpd', 'functools', 'stat', 'binhex', 're', 'tracemalloc', 'getpass', 'signal', 'xdrlib', 'builtins', 'lib2to3', 'pydoc', 'atexit', 'fnmatch', '_dummy_thread', 'msvcrt', 'audioop', 'configparser', 'abc', 'timeit', 'contextlib', 'optparse', 'marshal', 'fcntl', 'tokenize', 'getopt', 'mailbox', 'queue', 'asyncio', 'math', 'subprocess', 'calendar', 'tempfile', 'datetime', 'unittest', 'logging', 'modulefinder', 'select', 'mailcap', 'platform', 'macpath', 'asynchat', 'pathlib', 'wsgiref', 'threading', 'shutil', 'multiprocessing', 'pickletools', 'encodings', 'symtable', 'spwd', 'struct', 'chunk', 'parser', 'copy', 'uuid', 'socketserver', 'cProfile', 'sysconfig', 'netrc', 'tarfile', 'mmap', 'resource', 'bz2', 'concurrent', 'glob', 'linecache', 'email', 'weakref', 'sys', 'collections', 'poplib', 'imghdr', 'sunau', 'site', 'wave', 'tkinter', 'http', 'random', 'pprint', 'ssl', 'dis', 'imaplib', 'winreg', 'filecmp', 'test', 'statistics', 'hashlib', 'plistlib', 'msilib', 'cgi', 'array', 'dbm', 'turtle', 'gettext', 'html', 'traceback', 'numbers', 'base64', 'copyreg', 'mimetypes', 'asyncore', 'zlib', 'shlex', 'tty', 'webbrowser', 'pipes', 'ftplib', 'unicodedata', 'json', 'colorsys', 'xmlrpc', 'ast', 'bisect', '_thread', 'contextvars', 'urllib', 'secrets', 'imp', 'dataclasses', 'ctypes', 'argparse', 'binascii', 'py_compile', 'rlcompleter', 'codecs', 'lzma', 'ossaudiodev', 'turtledemo', 'runpy', 'winsound', 'dummy_threading', 'readline', 'socket', 'uu', 'pdb', 'token', 'pickle', 'ensurepip'})|known_standard_library|-b,--builtin|Force sortImports to recognize a module as part of the python standard library.| |Known Other|Dict|{}|known_other|**Not Supported**|| |Multi Line Output|Wrapmodes|WrapModes.GRID|multi_line_output|-m,--multi-line|Multi line output (0-grid, 1-vertical, 2-hanging, 3-vert-hanging, 4-vert-grid, 5-vert-grid-grouped, 6-vert-grid-grouped-no-comma).| |Forced Separate|Tuple|()|forced_separate|**Not Supported**|| @@ -39,9 +37,9 @@ NOTE: wrap_length must be LOWER than or equal to line_length.| |Use Parentheses|Bool|False|use_parentheses|--up,--use-parentheses|Use parenthesis for line continuation on length limit instead of slashes.| |Order By Type|Bool|True|order_by_type|--ot,--order-by-type|Order imports by type in addition to alphabetically| |Atomic|Bool|False|atomic|--ac,--atomic|Ensures the output doesn't save if the resulting file contains syntax errors.| -|Lines After Imports|Int|-1|lines_after_imports|--lai,--lines-after-imports|None| +|Lines After Imports|Int|-1|lines_after_imports|--lai,--lines-after-imports|| |Lines Between Sections|Int|1|lines_between_sections|**Not Supported**|| -|Lines Between Types|Int|0|lines_between_types|--lbt,--lines-between-types|None| +|Lines Between Types|Int|0|lines_between_types|--lbt,--lines-between-types|| |Combine As Imports|Bool|False|combine_as_imports|--ca,--combine-as|Combines as imports on the same line.| |Combine Star|Bool|False|combine_star|--cs,--combine-star|Ensures that if a star import is present, nothing else is imported from that namespace.| |Keep Direct And As Imports|Bool|True|keep_direct_and_as_imports|-k,--keep-direct-and-as|Turns off default behavior that removes direct imports when as imports exist.| @@ -78,4 +76,3 @@ NOTE: wrap_length must be LOWER than or equal to line_length.| |Files|*Not Typed*|None|**Not Supported**||One or more Python source files that need their imports sorted.| |Ask To Apply|Bool|False|**Not Supported**|--interactive|Tells isort to apply changes interactively.| |Show Config|Bool|False|**Not Supported**|--show-config|See isort's determined config, as well as sources of config options.| - diff --git a/scripts/build_config_option_docs.py b/scripts/build_config_option_docs.py index b60f8e892..05265656c 100755 --- a/scripts/build_config_option_docs.py +++ b/scripts/build_config_option_docs.py @@ -45,7 +45,7 @@ def config_options() -> Generator[str, None, None]: cli = cli_actions.pop(name, None) if cli: cli_options = ",".join(cli.option_strings) - description = description or cli.help + description = description or cli.help or "" else: cli_options = "**Not Supported**" description = description.replace("\n", " ") @@ -63,7 +63,7 @@ def config_options() -> Generator[str, None, None]: else: type_name = "*Not Typed*" - description = DESCRIPTIONS.get(name, cli.help).replace("\n", " ") + description = (DESCRIPTIONS.get(name, cli.help) or "").replace("\n", " ") yield f"|{human(name)}|{type_name}|{cli.default}|**Not Supported**|{','.join(cli.option_strings)}|{description}|\n" From 084b2750f977db3cf1477b8ac1ccdc9d06d2ba6c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 26 May 2020 00:02:50 -0700 Subject: [PATCH 0593/1439] Update config option documentation --- docs/configuration/options.md | 6 +++--- scripts/build_config_option_docs.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/configuration/options.md b/docs/configuration/options.md index be6bb59b2..693bc3062 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -8,7 +8,7 @@ how you want your imports sorted. | Name | Type | Default | Python / Config file | CLI | Description | |Python Version|Str|py3|py_version|--py,--python-version|Tells isort to set the known standard library based on the the specified Python version. Default is to assume any Python 3 version could be the target, and use a union off all stdlib modules across versions. If auto is specified, the version of the interpreter used to run isort (currently: 38) will be used.| |Force To Top|Frozenset|frozenset()|force_to_top|-t,--top|Force specific imports to the top of their appropriate section.| -|Skip|Frozenset|frozenset({'.venv', '.mypy_cache', 'build', '_build', '.nox', '.git', 'dist', 'node_modules', '.pants.d', '.tox', '.eggs', 'buck-out', '.hg', 'venv'})|skip|-s,--skip|Files that sort imports should skip over. If you want to skip multiple files you should specify twice: --skip file1 --skip file2.| +|Skip|Frozenset|frozenset({'_build', 'dist', '.eggs', '.pants.d', '.hg', 'build', '.nox', 'buck-out', '.git', '.venv', '.tox', 'venv', '.mypy_cache', 'node_modules'})|skip|-s,--skip|Files that sort imports should skip over. If you want to skip multiple files you should specify twice: --skip file1 --skip file2.| |Skip Glob|Frozenset|frozenset()|skip_glob|--sg,--skip-glob|Files that sort imports should skip over.| |Line Length|Int|79|line_length|-l,-w,--line-length,--line-width|The max length of an import line (used for wrapping long imports).| |Wrap Length|Int|0|wrap_length|--wl,--wrap-length|Specifies how long lines that are wrapped should be, if not set line_length is used. NOTE: wrap_length must be LOWER than or equal to line_length.| @@ -18,7 +18,7 @@ how you want your imports sorted. |Known Future Library|Frozenset|frozenset({'__future__'})|known_future_library|-f,--future|Force sortImports to recognize a module as part of the future compatibility libraries.| |Known Third Party|Frozenset|frozenset({'google.appengine.api'})|known_third_party|-o,--thirdparty|Force sortImports to recognize a module as being part of a third party library.| |Known First Party|Frozenset|frozenset()|known_first_party|-p,--project|Force sortImports to recognize a module as being part of the current python project.| -|Known Standard Library|Frozenset|frozenset({'quopri', 'gc', 'sqlite3', 'zipfile', 'enum', 'posix', 'fileinput', 'io', 'pty', 'sched', 'heapq', 'crypt', 'doctest', 'bdb', 'venv', 'symbol', 'operator', 'cgitb', 'typing', 'shelve', 'ipaddress', 'nis', 'gzip', 'zipimport', 'fpectl', 'compileall', 'aifc', 'smtplib', 'difflib', 'grp', 'selectors', 'faulthandler', 'code', 'xml', 'time', 'distutils', 'cmath', 'importlib', 'textwrap', 'pyclbr', 'itertools', 'zipapp', 'pstats', 'keyword', 'pwd', 'inspect', 'csv', 'warnings', 'stringprep', 'cmd', 'decimal', 'termios', 'codeop', 'reprlib', 'os', 'tabnanny', 'syslog', 'errno', 'string', 'types', 'hmac', 'profile', 'fractions', 'formatter', 'curses', 'nntplib', 'pkgutil', 'locale', 'trace', 'telnetlib', 'sndhdr', 'smtpd', 'functools', 'stat', 'binhex', 're', 'tracemalloc', 'getpass', 'signal', 'xdrlib', 'builtins', 'lib2to3', 'pydoc', 'atexit', 'fnmatch', '_dummy_thread', 'msvcrt', 'audioop', 'configparser', 'abc', 'timeit', 'contextlib', 'optparse', 'marshal', 'fcntl', 'tokenize', 'getopt', 'mailbox', 'queue', 'asyncio', 'math', 'subprocess', 'calendar', 'tempfile', 'datetime', 'unittest', 'logging', 'modulefinder', 'select', 'mailcap', 'platform', 'macpath', 'asynchat', 'pathlib', 'wsgiref', 'threading', 'shutil', 'multiprocessing', 'pickletools', 'encodings', 'symtable', 'spwd', 'struct', 'chunk', 'parser', 'copy', 'uuid', 'socketserver', 'cProfile', 'sysconfig', 'netrc', 'tarfile', 'mmap', 'resource', 'bz2', 'concurrent', 'glob', 'linecache', 'email', 'weakref', 'sys', 'collections', 'poplib', 'imghdr', 'sunau', 'site', 'wave', 'tkinter', 'http', 'random', 'pprint', 'ssl', 'dis', 'imaplib', 'winreg', 'filecmp', 'test', 'statistics', 'hashlib', 'plistlib', 'msilib', 'cgi', 'array', 'dbm', 'turtle', 'gettext', 'html', 'traceback', 'numbers', 'base64', 'copyreg', 'mimetypes', 'asyncore', 'zlib', 'shlex', 'tty', 'webbrowser', 'pipes', 'ftplib', 'unicodedata', 'json', 'colorsys', 'xmlrpc', 'ast', 'bisect', '_thread', 'contextvars', 'urllib', 'secrets', 'imp', 'dataclasses', 'ctypes', 'argparse', 'binascii', 'py_compile', 'rlcompleter', 'codecs', 'lzma', 'ossaudiodev', 'turtledemo', 'runpy', 'winsound', 'dummy_threading', 'readline', 'socket', 'uu', 'pdb', 'token', 'pickle', 'ensurepip'})|known_standard_library|-b,--builtin|Force sortImports to recognize a module as part of the python standard library.| +|Known Standard Library|Frozenset|frozenset({'telnetlib', 'functools', 'itertools', 'tabnanny', 'pkgutil', 'runpy', 'cgitb', 'uu', 'multiprocessing', 'pickle', 'binhex', 'syslog', 'site', 'quopri', 'faulthandler', 'msilib', 'queue', 'symtable', 'asyncore', 'locale', 'sys', 'spwd', 'pwd', 'uuid', 'zipapp', 'codeop', 'tkinter', 'zipfile', 'tempfile', 'dbm', 'ipaddress', 'code', 'math', 'termios', 'difflib', 'fractions', 'plistlib', 'bdb', 'json', 'select', 'zlib', 'fileinput', '_dummy_thread', 'logging', 'fnmatch', 'unittest', 'zipimport', 'subprocess', 'imghdr', 'trace', 'aifc', 'turtledemo', 'shelve', 'grp', 'colorsys', 'xdrlib', 'pyclbr', 'dummy_threading', 'posix', 'urllib', 'pathlib', 'audioop', 'msvcrt', 'sched', 'poplib', 'netrc', 'sunau', 'venv', 'ensurepip', 'typing', 'xml', 'tokenize', 'datetime', 'mailbox', 'weakref', 'struct', 'test', 'dataclasses', 'tty', 'wave', 'winreg', 'nis', 'pydoc', 'pdb', 'macpath', 'linecache', 'parser', 'shutil', 'tarfile', 'crypt', 'array', 'html', 'fpectl', 're', 'rlcompleter', 'socket', 'email', 'mimetypes', 'chunk', 'lzma', 'pprint', 'pstats', 'sysconfig', 'gzip', 'sndhdr', 'collections', 'mailcap', 'stringprep', 'doctest', 'curses', 'textwrap', 'pickletools', 'token', '_thread', 'signal', 'cProfile', 'statistics', 'base64', 'ast', 'shlex', 'wsgiref', 'cmath', 'binascii', 'bisect', 'timeit', 'getpass', 'importlib', 'smtplib', 'pipes', 'stat', 'csv', 'types', 'inspect', 'distutils', 'filecmp', 'sqlite3', 'abc', 'fcntl', 'keyword', 'hashlib', 'decimal', 'warnings', 'os', 'io', 'contextlib', 'winsound', 'asynchat', 'nntplib', 'tracemalloc', 'formatter', 'profile', 'enum', 'contextvars', 'atexit', 'glob', 'mmap', 'http', 'random', 'concurrent', 'xmlrpc', 'lib2to3', 'turtle', 'gettext', 'selectors', 'smtpd', 'optparse', 'getopt', 'builtins', 'secrets', 'hmac', 'gc', 'encodings', 'cmd', 'argparse', 'operator', 'ossaudiodev', 'asyncio', 'socketserver', 'codecs', 'traceback', 'symbol', 'imp', 'numbers', 'py_compile', 'imaplib', 'marshal', 'threading', 'pty', 'copy', 'reprlib', 'bz2', 'calendar', 'ctypes', 'configparser', 'modulefinder', 'heapq', 'errno', 'webbrowser', 'string', 'cgi', 'readline', 'unicodedata', 'copyreg', 'ftplib', 'platform', 'resource', 'ssl', 'dis', 'time', 'compileall'})|known_standard_library|-b,--builtin|Force sortImports to recognize a module as part of the python standard library.| |Known Other|Dict|{}|known_other|**Not Supported**|| |Multi Line Output|Wrapmodes|WrapModes.GRID|multi_line_output|-m,--multi-line|Multi line output (0-grid, 1-vertical, 2-hanging, 3-vert-hanging, 4-vert-grid, 5-vert-grid-grouped, 6-vert-grid-grouped-no-comma).| |Forced Separate|Tuple|()|forced_separate|**Not Supported**|| @@ -71,7 +71,7 @@ how you want your imports sorted. |Dont Order By Type|Bool|False|**Not Supported**|--dt,--dont-order-by-type|Don't order imports by type in addition to alphabetically| |Settings Path|*Not Typed*|None|**Not Supported**|--sp,--settings-path,--settings-file,--settings|Explicitly set the settings path or file instead of auto determining based on file location.| |Show Version|Bool|False|**Not Supported**|-V,--version|Displays the currently installed version of isort.| -|Vn|Str|==SUPPRESS==|**Not Supported**|--vn,--version-number|Returns just the current version number without the logo| +|Version Number|Str|==SUPPRESS==|**Not Supported**|--vn,--version-number|Returns just the current version number without the logo| |Filter Files|Bool|False|**Not Supported**|--filter-files|Tells isort to filter files even when they are explicitly passed in as part of the command| |Files|*Not Typed*|None|**Not Supported**||One or more Python source files that need their imports sorted.| |Ask To Apply|Bool|False|**Not Supported**|--interactive|Tells isort to apply changes interactively.| diff --git a/scripts/build_config_option_docs.py b/scripts/build_config_option_docs.py index 05265656c..dc2291c78 100755 --- a/scripts/build_config_option_docs.py +++ b/scripts/build_config_option_docs.py @@ -8,7 +8,7 @@ OUTPUT_FILE = os.path.abspath( os.path.join(os.path.dirname(os.path.abspath(__file__)), "../docs/configuration/options.md") ) -HUMAN_NAME = {"py_version": "Python Version"} +HUMAN_NAME = {"py_version": "Python Version", "vn": "Version Number"} DESCRIPTIONS = {} IGNORED = {"source", "help"} COLUMNS = ["Name", "Type", "Default", "Python / Config file", "CLI", "Description"] From b09715e89551a7b8303a08ee44345ab1854b2b38 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 26 May 2020 00:04:15 -0700 Subject: [PATCH 0594/1439] Update config option documentation --- docs/configuration/options.md | 5 +++-- scripts/build_config_option_docs.py | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/configuration/options.md b/docs/configuration/options.md index 693bc3062..1be23b9c0 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -6,9 +6,10 @@ isort will disagree but commit to your way of formatting. To enable this, isort how you want your imports sorted. | Name | Type | Default | Python / Config file | CLI | Description | +| ---- | ---- | ------- | -------------------- | --- | ----------- | |Python Version|Str|py3|py_version|--py,--python-version|Tells isort to set the known standard library based on the the specified Python version. Default is to assume any Python 3 version could be the target, and use a union off all stdlib modules across versions. If auto is specified, the version of the interpreter used to run isort (currently: 38) will be used.| |Force To Top|Frozenset|frozenset()|force_to_top|-t,--top|Force specific imports to the top of their appropriate section.| -|Skip|Frozenset|frozenset({'_build', 'dist', '.eggs', '.pants.d', '.hg', 'build', '.nox', 'buck-out', '.git', '.venv', '.tox', 'venv', '.mypy_cache', 'node_modules'})|skip|-s,--skip|Files that sort imports should skip over. If you want to skip multiple files you should specify twice: --skip file1 --skip file2.| +|Skip|Frozenset|frozenset({'dist', '.git', 'build', 'venv', '.tox', 'node_modules', '.venv', '.eggs', '.pants.d', '.nox', '.mypy_cache', '_build', 'buck-out', '.hg'})|skip|-s,--skip|Files that sort imports should skip over. If you want to skip multiple files you should specify twice: --skip file1 --skip file2.| |Skip Glob|Frozenset|frozenset()|skip_glob|--sg,--skip-glob|Files that sort imports should skip over.| |Line Length|Int|79|line_length|-l,-w,--line-length,--line-width|The max length of an import line (used for wrapping long imports).| |Wrap Length|Int|0|wrap_length|--wl,--wrap-length|Specifies how long lines that are wrapped should be, if not set line_length is used. NOTE: wrap_length must be LOWER than or equal to line_length.| @@ -18,7 +19,7 @@ how you want your imports sorted. |Known Future Library|Frozenset|frozenset({'__future__'})|known_future_library|-f,--future|Force sortImports to recognize a module as part of the future compatibility libraries.| |Known Third Party|Frozenset|frozenset({'google.appengine.api'})|known_third_party|-o,--thirdparty|Force sortImports to recognize a module as being part of a third party library.| |Known First Party|Frozenset|frozenset()|known_first_party|-p,--project|Force sortImports to recognize a module as being part of the current python project.| -|Known Standard Library|Frozenset|frozenset({'telnetlib', 'functools', 'itertools', 'tabnanny', 'pkgutil', 'runpy', 'cgitb', 'uu', 'multiprocessing', 'pickle', 'binhex', 'syslog', 'site', 'quopri', 'faulthandler', 'msilib', 'queue', 'symtable', 'asyncore', 'locale', 'sys', 'spwd', 'pwd', 'uuid', 'zipapp', 'codeop', 'tkinter', 'zipfile', 'tempfile', 'dbm', 'ipaddress', 'code', 'math', 'termios', 'difflib', 'fractions', 'plistlib', 'bdb', 'json', 'select', 'zlib', 'fileinput', '_dummy_thread', 'logging', 'fnmatch', 'unittest', 'zipimport', 'subprocess', 'imghdr', 'trace', 'aifc', 'turtledemo', 'shelve', 'grp', 'colorsys', 'xdrlib', 'pyclbr', 'dummy_threading', 'posix', 'urllib', 'pathlib', 'audioop', 'msvcrt', 'sched', 'poplib', 'netrc', 'sunau', 'venv', 'ensurepip', 'typing', 'xml', 'tokenize', 'datetime', 'mailbox', 'weakref', 'struct', 'test', 'dataclasses', 'tty', 'wave', 'winreg', 'nis', 'pydoc', 'pdb', 'macpath', 'linecache', 'parser', 'shutil', 'tarfile', 'crypt', 'array', 'html', 'fpectl', 're', 'rlcompleter', 'socket', 'email', 'mimetypes', 'chunk', 'lzma', 'pprint', 'pstats', 'sysconfig', 'gzip', 'sndhdr', 'collections', 'mailcap', 'stringprep', 'doctest', 'curses', 'textwrap', 'pickletools', 'token', '_thread', 'signal', 'cProfile', 'statistics', 'base64', 'ast', 'shlex', 'wsgiref', 'cmath', 'binascii', 'bisect', 'timeit', 'getpass', 'importlib', 'smtplib', 'pipes', 'stat', 'csv', 'types', 'inspect', 'distutils', 'filecmp', 'sqlite3', 'abc', 'fcntl', 'keyword', 'hashlib', 'decimal', 'warnings', 'os', 'io', 'contextlib', 'winsound', 'asynchat', 'nntplib', 'tracemalloc', 'formatter', 'profile', 'enum', 'contextvars', 'atexit', 'glob', 'mmap', 'http', 'random', 'concurrent', 'xmlrpc', 'lib2to3', 'turtle', 'gettext', 'selectors', 'smtpd', 'optparse', 'getopt', 'builtins', 'secrets', 'hmac', 'gc', 'encodings', 'cmd', 'argparse', 'operator', 'ossaudiodev', 'asyncio', 'socketserver', 'codecs', 'traceback', 'symbol', 'imp', 'numbers', 'py_compile', 'imaplib', 'marshal', 'threading', 'pty', 'copy', 'reprlib', 'bz2', 'calendar', 'ctypes', 'configparser', 'modulefinder', 'heapq', 'errno', 'webbrowser', 'string', 'cgi', 'readline', 'unicodedata', 'copyreg', 'ftplib', 'platform', 'resource', 'ssl', 'dis', 'time', 'compileall'})|known_standard_library|-b,--builtin|Force sortImports to recognize a module as part of the python standard library.| +|Known Standard Library|Frozenset|frozenset({'time', 'binascii', 'encodings', 'webbrowser', 'enum', 'subprocess', 'uu', 'functools', 'statistics', 'base64', 'modulefinder', 'nis', 'pyclbr', 'sched', 'linecache', 'test', 'bisect', 'imaplib', 'xmlrpc', 'mailcap', 'zipimport', 'ftplib', 'trace', 'turtledemo', 'getpass', 'math', 'doctest', 'py_compile', 'sndhdr', 'tarfile', 'builtins', 'string', 're', 'email', 'tty', 'operator', 'csv', 'atexit', 'plistlib', 'textwrap', 'asynchat', 'imghdr', 'html', 'glob', 'copy', 'fpectl', 'mmap', 'compileall', 'heapq', 'chunk', 'logging', 'errno', 'types', 'importlib', 'ensurepip', 'difflib', 'marshal', 'curses', 'platform', 'stringprep', 'readline', 'venv', 'wave', 'pipes', 'fileinput', 'bdb', 'fcntl', 'zipfile', 'xml', 'json', 'symtable', 'msvcrt', '_thread', 'weakref', 'profile', 'zlib', 'pty', 'imp', 'dis', 'socketserver', 'quopri', 'abc', 'shutil', 'select', 'cmd', 'codecs', 'os', 'calendar', 'turtle', 'gzip', 'warnings', 'concurrent', 'mimetypes', 'nntplib', 'getopt', 'posix', 'site', 'struct', 'zipapp', 'spwd', 'fractions', 'copyreg', 'syslog', 'cgitb', 'tkinter', 'hashlib', 'pydoc', 'aifc', 'contextlib', 'ssl', 'rlcompleter', 'smtpd', 'ipaddress', 'tokenize', 'selectors', 'traceback', 'binhex', 'reprlib', 'tabnanny', 'locale', 'formatter', 'contextvars', 'lzma', 'optparse', 'mailbox', 'pkgutil', 'telnetlib', 'array', 'runpy', 'ossaudiodev', 'netrc', 'multiprocessing', 'filecmp', 'dbm', 'http', 'signal', 'codeop', 'inspect', 'collections', 'winreg', 'grp', 'symbol', 'decimal', 'fnmatch', 'cProfile', 'pprint', 'bz2', 'macpath', 'pathlib', 'faulthandler', 'distutils', 'unittest', 'ctypes', 'uuid', 'random', 'shelve', '_dummy_thread', 'cmath', 'colorsys', 'ast', 'stat', 'code', 'wsgiref', 'dataclasses', 'tempfile', 'gettext', 'lib2to3', 'parser', 'pwd', 'sunau', 'pstats', 'numbers', 'socket', 'datetime', 'tracemalloc', 'msilib', 'sqlite3', 'itertools', 'keyword', 'xdrlib', 'asyncore', 'crypt', 'sys', 'urllib', 'queue', 'poplib', 'gc', 'pickle', 'token', 'secrets', 'io', 'pdb', 'dummy_threading', 'pickletools', 'timeit', 'cgi', 'winsound', 'termios', 'typing', 'hmac', 'configparser', 'asyncio', 'shlex', 'smtplib', 'unicodedata', 'resource', 'sysconfig', 'argparse', 'threading', 'audioop'})|known_standard_library|-b,--builtin|Force sortImports to recognize a module as part of the python standard library.| |Known Other|Dict|{}|known_other|**Not Supported**|| |Multi Line Output|Wrapmodes|WrapModes.GRID|multi_line_output|-m,--multi-line|Multi line output (0-grid, 1-vertical, 2-hanging, 3-vert-hanging, 4-vert-grid, 5-vert-grid-grouped, 6-vert-grid-grouped-no-comma).| |Forced Separate|Tuple|()|forced_separate|**Not Supported**|| diff --git a/scripts/build_config_option_docs.py b/scripts/build_config_option_docs.py index dc2291c78..53f9aa3fc 100755 --- a/scripts/build_config_option_docs.py +++ b/scripts/build_config_option_docs.py @@ -20,6 +20,7 @@ how you want your imports sorted. | {' | '.join(COLUMNS)} | +| {' | '.join(["-" * len(column) for column in COLUMNS])} | """ parser = _build_arg_parser() From 9d6b83e815d430652568b5876caec6c21fa83f4c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 26 May 2020 01:14:49 -0700 Subject: [PATCH 0595/1439] Fix text formatting --- docs/configuration/options.md | 8 ++++---- scripts/build_config_option_docs.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/configuration/options.md b/docs/configuration/options.md index 1be23b9c0..d17dfa5b5 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -1,15 +1,15 @@ Configuration options for isort ======== -As a code formatter isort has opinions. However, it also allows you to also have your own. If your opinions disagree with those of isort, +As a code formatter isort has opinions. However, it also allows you to have your own. If your opinions disagree with those of isort, isort will disagree but commit to your way of formatting. To enable this, isort exposes a plethora of options to specify -how you want your imports sorted. +how you want your imports sorted, organized, and formatted. | Name | Type | Default | Python / Config file | CLI | Description | | ---- | ---- | ------- | -------------------- | --- | ----------- | |Python Version|Str|py3|py_version|--py,--python-version|Tells isort to set the known standard library based on the the specified Python version. Default is to assume any Python 3 version could be the target, and use a union off all stdlib modules across versions. If auto is specified, the version of the interpreter used to run isort (currently: 38) will be used.| |Force To Top|Frozenset|frozenset()|force_to_top|-t,--top|Force specific imports to the top of their appropriate section.| -|Skip|Frozenset|frozenset({'dist', '.git', 'build', 'venv', '.tox', 'node_modules', '.venv', '.eggs', '.pants.d', '.nox', '.mypy_cache', '_build', 'buck-out', '.hg'})|skip|-s,--skip|Files that sort imports should skip over. If you want to skip multiple files you should specify twice: --skip file1 --skip file2.| +|Skip|Frozenset|frozenset({'.git', 'venv', 'buck-out', '.nox', 'build', '.eggs', '.pants.d', '.mypy_cache', 'node_modules', '.tox', 'dist', '.hg', '_build', '.venv'})|skip|-s,--skip|Files that sort imports should skip over. If you want to skip multiple files you should specify twice: --skip file1 --skip file2.| |Skip Glob|Frozenset|frozenset()|skip_glob|--sg,--skip-glob|Files that sort imports should skip over.| |Line Length|Int|79|line_length|-l,-w,--line-length,--line-width|The max length of an import line (used for wrapping long imports).| |Wrap Length|Int|0|wrap_length|--wl,--wrap-length|Specifies how long lines that are wrapped should be, if not set line_length is used. NOTE: wrap_length must be LOWER than or equal to line_length.| @@ -19,7 +19,7 @@ how you want your imports sorted. |Known Future Library|Frozenset|frozenset({'__future__'})|known_future_library|-f,--future|Force sortImports to recognize a module as part of the future compatibility libraries.| |Known Third Party|Frozenset|frozenset({'google.appengine.api'})|known_third_party|-o,--thirdparty|Force sortImports to recognize a module as being part of a third party library.| |Known First Party|Frozenset|frozenset()|known_first_party|-p,--project|Force sortImports to recognize a module as being part of the current python project.| -|Known Standard Library|Frozenset|frozenset({'time', 'binascii', 'encodings', 'webbrowser', 'enum', 'subprocess', 'uu', 'functools', 'statistics', 'base64', 'modulefinder', 'nis', 'pyclbr', 'sched', 'linecache', 'test', 'bisect', 'imaplib', 'xmlrpc', 'mailcap', 'zipimport', 'ftplib', 'trace', 'turtledemo', 'getpass', 'math', 'doctest', 'py_compile', 'sndhdr', 'tarfile', 'builtins', 'string', 're', 'email', 'tty', 'operator', 'csv', 'atexit', 'plistlib', 'textwrap', 'asynchat', 'imghdr', 'html', 'glob', 'copy', 'fpectl', 'mmap', 'compileall', 'heapq', 'chunk', 'logging', 'errno', 'types', 'importlib', 'ensurepip', 'difflib', 'marshal', 'curses', 'platform', 'stringprep', 'readline', 'venv', 'wave', 'pipes', 'fileinput', 'bdb', 'fcntl', 'zipfile', 'xml', 'json', 'symtable', 'msvcrt', '_thread', 'weakref', 'profile', 'zlib', 'pty', 'imp', 'dis', 'socketserver', 'quopri', 'abc', 'shutil', 'select', 'cmd', 'codecs', 'os', 'calendar', 'turtle', 'gzip', 'warnings', 'concurrent', 'mimetypes', 'nntplib', 'getopt', 'posix', 'site', 'struct', 'zipapp', 'spwd', 'fractions', 'copyreg', 'syslog', 'cgitb', 'tkinter', 'hashlib', 'pydoc', 'aifc', 'contextlib', 'ssl', 'rlcompleter', 'smtpd', 'ipaddress', 'tokenize', 'selectors', 'traceback', 'binhex', 'reprlib', 'tabnanny', 'locale', 'formatter', 'contextvars', 'lzma', 'optparse', 'mailbox', 'pkgutil', 'telnetlib', 'array', 'runpy', 'ossaudiodev', 'netrc', 'multiprocessing', 'filecmp', 'dbm', 'http', 'signal', 'codeop', 'inspect', 'collections', 'winreg', 'grp', 'symbol', 'decimal', 'fnmatch', 'cProfile', 'pprint', 'bz2', 'macpath', 'pathlib', 'faulthandler', 'distutils', 'unittest', 'ctypes', 'uuid', 'random', 'shelve', '_dummy_thread', 'cmath', 'colorsys', 'ast', 'stat', 'code', 'wsgiref', 'dataclasses', 'tempfile', 'gettext', 'lib2to3', 'parser', 'pwd', 'sunau', 'pstats', 'numbers', 'socket', 'datetime', 'tracemalloc', 'msilib', 'sqlite3', 'itertools', 'keyword', 'xdrlib', 'asyncore', 'crypt', 'sys', 'urllib', 'queue', 'poplib', 'gc', 'pickle', 'token', 'secrets', 'io', 'pdb', 'dummy_threading', 'pickletools', 'timeit', 'cgi', 'winsound', 'termios', 'typing', 'hmac', 'configparser', 'asyncio', 'shlex', 'smtplib', 'unicodedata', 'resource', 'sysconfig', 'argparse', 'threading', 'audioop'})|known_standard_library|-b,--builtin|Force sortImports to recognize a module as part of the python standard library.| +|Known Standard Library|Frozenset|frozenset({'plistlib', 'pwd', 'email', 'poplib', '_dummy_thread', 'msvcrt', 'shelve', 'resource', 'threading', 'inspect', 'aifc', 'nis', 'importlib', 'timeit', 'queue', 'contextlib', 'dis', 'formatter', 'asynchat', 'os', 'winsound', 'dummy_threading', 'html', 'socketserver', 'chunk', 'ast', 'selectors', 'quopri', 'faulthandler', 'base64', 'multiprocessing', 'stat', 'urllib', 'gettext', 'mimetypes', 'xml', 'cgi', 'mailcap', 'modulefinder', 'turtledemo', 'trace', 'syslog', 'colorsys', 're', 'shutil', 'array', 'sndhdr', 'json', 'runpy', 'collections', 'bisect', 'curses', 'fpectl', 'textwrap', 'readline', 'winreg', 'fcntl', 'itertools', 'filecmp', 'optparse', 'marshal', 'dbm', 'stringprep', 'tarfile', 'xdrlib', 'parser', 'smtplib', 'socket', 'distutils', 'decimal', 'pyclbr', 'signal', 'contextvars', 'compileall', 'binhex', 'time', 'random', 'tokenize', 'lib2to3', 'datetime', 'gc', 'enum', 'uuid', 'pickletools', 'unicodedata', 'pdb', 'symtable', 'heapq', 'wsgiref', 'sunau', 'configparser', 'zipapp', 'types', 'ipaddress', 'token', 'logging', 'grp', 'operator', 'tkinter', 'py_compile', 'pydoc', 'binascii', 'cgitb', 'getopt', 'select', 'hmac', 'unittest', 'sysconfig', 'difflib', 'sqlite3', 'http', 'imp', 'ftplib', 'pathlib', 'pprint', 'concurrent', 'reprlib', 'sys', 'fnmatch', 'test', 'cmd', 'pipes', 'argparse', 'shlex', 'ssl', 'imghdr', 'bdb', 'weakref', 'tempfile', 'turtle', 'glob', 'string', 'zlib', 'fractions', '_thread', 'spwd', 'codeop', 'crypt', 'rlcompleter', 'codecs', 'atexit', 'ossaudiodev', 'code', 'encodings', 'profile', 'macpath', 'site', 'telnetlib', 'doctest', 'statistics', 'pickle', 'copy', 'io', 'getpass', 'asyncore', 'posix', 'cmath', 'csv', 'asyncio', 'traceback', 'ctypes', 'symbol', 'webbrowser', 'copyreg', 'pstats', 'numbers', 'pty', 'zipfile', 'bz2', 'lzma', 'gzip', 'errno', 'imaplib', 'mmap', 'calendar', 'tty', 'math', 'secrets', 'subprocess', 'functools', 'keyword', 'dataclasses', 'ensurepip', 'warnings', 'nntplib', 'fileinput', 'linecache', 'struct', 'msilib', 'wave', 'typing', 'pkgutil', 'abc', 'audioop', 'builtins', 'venv', 'zipimport', 'xmlrpc', 'locale', 'uu', 'netrc', 'platform', 'tracemalloc', 'tabnanny', 'mailbox', 'termios', 'cProfile', 'sched', 'hashlib', 'smtpd'})|known_standard_library|-b,--builtin|Force sortImports to recognize a module as part of the python standard library.| |Known Other|Dict|{}|known_other|**Not Supported**|| |Multi Line Output|Wrapmodes|WrapModes.GRID|multi_line_output|-m,--multi-line|Multi line output (0-grid, 1-vertical, 2-hanging, 3-vert-hanging, 4-vert-grid, 5-vert-grid-grouped, 6-vert-grid-grouped-no-comma).| |Forced Separate|Tuple|()|forced_separate|**Not Supported**|| diff --git a/scripts/build_config_option_docs.py b/scripts/build_config_option_docs.py index 53f9aa3fc..eaad08838 100755 --- a/scripts/build_config_option_docs.py +++ b/scripts/build_config_option_docs.py @@ -15,9 +15,9 @@ HEADER = f"""Configuration options for isort ======== -As a code formatter isort has opinions. However, it also allows you to also have your own. If your opinions disagree with those of isort, +As a code formatter isort has opinions. However, it also allows you to have your own. If your opinions disagree with those of isort, isort will disagree but commit to your way of formatting. To enable this, isort exposes a plethora of options to specify -how you want your imports sorted. +how you want your imports sorted, organized, and formatted. | {' | '.join(COLUMNS)} | | {' | '.join(["-" * len(column) for column in COLUMNS])} | From fa4d8c54a0e8ad9b33b4f65af75c9c1a4270af35 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 26 May 2020 23:56:23 -0700 Subject: [PATCH 0596/1439] Refactor options doc to separate display from config option identifcation --- docs/configuration/options.md | 36 ++++++++--------- scripts/build_config_option_docs.py | 62 +++++++++++++++++++---------- 2 files changed, 58 insertions(+), 40 deletions(-) diff --git a/docs/configuration/options.md b/docs/configuration/options.md index d17dfa5b5..ff913713b 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -9,38 +9,38 @@ how you want your imports sorted, organized, and formatted. | ---- | ---- | ------- | -------------------- | --- | ----------- | |Python Version|Str|py3|py_version|--py,--python-version|Tells isort to set the known standard library based on the the specified Python version. Default is to assume any Python 3 version could be the target, and use a union off all stdlib modules across versions. If auto is specified, the version of the interpreter used to run isort (currently: 38) will be used.| |Force To Top|Frozenset|frozenset()|force_to_top|-t,--top|Force specific imports to the top of their appropriate section.| -|Skip|Frozenset|frozenset({'.git', 'venv', 'buck-out', '.nox', 'build', '.eggs', '.pants.d', '.mypy_cache', 'node_modules', '.tox', 'dist', '.hg', '_build', '.venv'})|skip|-s,--skip|Files that sort imports should skip over. If you want to skip multiple files you should specify twice: --skip file1 --skip file2.| +|Skip|Frozenset|frozenset({'buck-out', 'venv', '.hg', '.mypy_cache', '.eggs', '.nox', 'build', '.pants.d', '.tox', 'dist', '.git', '.venv', 'node_modules', '_build'})|skip|-s,--skip|Files that sort imports should skip over. If you want to skip multiple files you should specify twice: --skip file1 --skip file2.| |Skip Glob|Frozenset|frozenset()|skip_glob|--sg,--skip-glob|Files that sort imports should skip over.| |Line Length|Int|79|line_length|-l,-w,--line-length,--line-width|The max length of an import line (used for wrapping long imports).| |Wrap Length|Int|0|wrap_length|--wl,--wrap-length|Specifies how long lines that are wrapped should be, if not set line_length is used. NOTE: wrap_length must be LOWER than or equal to line_length.| |Line Ending|Str||line_ending|--le,--line-ending|Forces line endings to the specified value. If not set, values will be guessed per-file.| -|Sections|Tuple|('FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER')|sections|**Not Supported**|| +|Sections|Tuple|('FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER')|sections|*,*,N,o,t, ,S,u,p,p,o,r,t,e,d,*,*|**No Description**| |No Sections|Bool|False|no_sections|--ds,--no-sections|Put all imports into the same section bucket| |Known Future Library|Frozenset|frozenset({'__future__'})|known_future_library|-f,--future|Force sortImports to recognize a module as part of the future compatibility libraries.| |Known Third Party|Frozenset|frozenset({'google.appengine.api'})|known_third_party|-o,--thirdparty|Force sortImports to recognize a module as being part of a third party library.| |Known First Party|Frozenset|frozenset()|known_first_party|-p,--project|Force sortImports to recognize a module as being part of the current python project.| -|Known Standard Library|Frozenset|frozenset({'plistlib', 'pwd', 'email', 'poplib', '_dummy_thread', 'msvcrt', 'shelve', 'resource', 'threading', 'inspect', 'aifc', 'nis', 'importlib', 'timeit', 'queue', 'contextlib', 'dis', 'formatter', 'asynchat', 'os', 'winsound', 'dummy_threading', 'html', 'socketserver', 'chunk', 'ast', 'selectors', 'quopri', 'faulthandler', 'base64', 'multiprocessing', 'stat', 'urllib', 'gettext', 'mimetypes', 'xml', 'cgi', 'mailcap', 'modulefinder', 'turtledemo', 'trace', 'syslog', 'colorsys', 're', 'shutil', 'array', 'sndhdr', 'json', 'runpy', 'collections', 'bisect', 'curses', 'fpectl', 'textwrap', 'readline', 'winreg', 'fcntl', 'itertools', 'filecmp', 'optparse', 'marshal', 'dbm', 'stringprep', 'tarfile', 'xdrlib', 'parser', 'smtplib', 'socket', 'distutils', 'decimal', 'pyclbr', 'signal', 'contextvars', 'compileall', 'binhex', 'time', 'random', 'tokenize', 'lib2to3', 'datetime', 'gc', 'enum', 'uuid', 'pickletools', 'unicodedata', 'pdb', 'symtable', 'heapq', 'wsgiref', 'sunau', 'configparser', 'zipapp', 'types', 'ipaddress', 'token', 'logging', 'grp', 'operator', 'tkinter', 'py_compile', 'pydoc', 'binascii', 'cgitb', 'getopt', 'select', 'hmac', 'unittest', 'sysconfig', 'difflib', 'sqlite3', 'http', 'imp', 'ftplib', 'pathlib', 'pprint', 'concurrent', 'reprlib', 'sys', 'fnmatch', 'test', 'cmd', 'pipes', 'argparse', 'shlex', 'ssl', 'imghdr', 'bdb', 'weakref', 'tempfile', 'turtle', 'glob', 'string', 'zlib', 'fractions', '_thread', 'spwd', 'codeop', 'crypt', 'rlcompleter', 'codecs', 'atexit', 'ossaudiodev', 'code', 'encodings', 'profile', 'macpath', 'site', 'telnetlib', 'doctest', 'statistics', 'pickle', 'copy', 'io', 'getpass', 'asyncore', 'posix', 'cmath', 'csv', 'asyncio', 'traceback', 'ctypes', 'symbol', 'webbrowser', 'copyreg', 'pstats', 'numbers', 'pty', 'zipfile', 'bz2', 'lzma', 'gzip', 'errno', 'imaplib', 'mmap', 'calendar', 'tty', 'math', 'secrets', 'subprocess', 'functools', 'keyword', 'dataclasses', 'ensurepip', 'warnings', 'nntplib', 'fileinput', 'linecache', 'struct', 'msilib', 'wave', 'typing', 'pkgutil', 'abc', 'audioop', 'builtins', 'venv', 'zipimport', 'xmlrpc', 'locale', 'uu', 'netrc', 'platform', 'tracemalloc', 'tabnanny', 'mailbox', 'termios', 'cProfile', 'sched', 'hashlib', 'smtpd'})|known_standard_library|-b,--builtin|Force sortImports to recognize a module as part of the python standard library.| -|Known Other|Dict|{}|known_other|**Not Supported**|| +|Known Standard Library|Frozenset|frozenset({'argparse', 'turtledemo', 'chunk', 'binhex', 'cmath', 'profile', 'itertools', 'atexit', 'fractions', 'socketserver', 'audioop', 'telnetlib', 'wave', 'struct', 'crypt', 'tty', 'logging', 'cmd', 'gc', 'select', 'hashlib', 'cgi', 'mimetypes', 'pty', 'signal', 'test', 'symbol', 'fcntl', 'termios', 'imaplib', 'copyreg', 'parser', 'secrets', 'posix', 'fnmatch', 'optparse', 'tracemalloc', 'json', 'macpath', 'ssl', 'uuid', 'threading', 'mailcap', 'cgitb', 'lib2to3', 'imghdr', 'os', 'queue', 'codecs', 'sys', 'pipes', 'email', 'resource', 'quopri', 'selectors', 'syslog', 'types', 'textwrap', 'abc', 'operator', 'uu', 'wsgiref', 'weakref', 'ctypes', 'pprint', 'fpectl', 'concurrent', 'compileall', 'gettext', 'collections', 'glob', 'contextvars', 'multiprocessing', 'getopt', 'heapq', 'netrc', 'zipimport', 'sndhdr', 'subprocess', 'grp', 'string', 'symtable', 'pwd', 'zlib', 'py_compile', 'sqlite3', 'poplib', 'tempfile', 'contextlib', 'ast', 'ensurepip', 'getpass', 'urllib', 'lzma', 'calendar', 'ipaddress', 'plistlib', 'builtins', 'keyword', 'pstats', 'ossaudiodev', 'pyclbr', 'msilib', 'unittest', 'nis', 'unicodedata', 'functools', 'numbers', 'pathlib', 'smtpd', 'typing', 'rlcompleter', 'dis', 'pkgutil', 'mmap', 'winreg', 'shlex', 'array', 'venv', 'bz2', 'hmac', 'pickletools', 're', 'site', 'tkinter', 'asyncio', 'io', 'traceback', 'pydoc', 'inspect', 'token', 'distutils', 'enum', 'marshal', 'aifc', 'stringprep', 'dataclasses', 'sunau', 'curses', 'platform', 'random', 'tarfile', 'timeit', 'mailbox', 'runpy', 'tokenize', 'imp', 'winsound', 'filecmp', 'pickle', 'socket', 'doctest', 'spwd', 'nntplib', 'stat', 'importlib', 'xdrlib', 'gzip', 'cProfile', 'errno', 'warnings', 'shelve', 'configparser', '_thread', 'ftplib', 'encodings', 'sched', 'difflib', 'html', 'pdb', 'smtplib', 'base64', 'webbrowser', 'datetime', 'colorsys', 'trace', 'readline', 'copy', 'reprlib', 'codeop', 'xmlrpc', 'fileinput', 'decimal', 'zipfile', 'bdb', 'asyncore', 'dummy_threading', 'turtle', 'statistics', '_dummy_thread', 'csv', 'linecache', 'modulefinder', 'zipapp', 'bisect', 'formatter', 'asynchat', 'sysconfig', 'xml', 'math', 'dbm', 'faulthandler', 'shutil', 'http', 'locale', 'tabnanny', 'code', 'time', 'binascii', 'msvcrt'})|known_standard_library|-b,--builtin|Force sortImports to recognize a module as part of the python standard library.| +|Known Other|Dict|{}|known_other|*,*,N,o,t, ,S,u,p,p,o,r,t,e,d,*,*|**No Description**| |Multi Line Output|Wrapmodes|WrapModes.GRID|multi_line_output|-m,--multi-line|Multi line output (0-grid, 1-vertical, 2-hanging, 3-vert-hanging, 4-vert-grid, 5-vert-grid-grouped, 6-vert-grid-grouped-no-comma).| -|Forced Separate|Tuple|()|forced_separate|**Not Supported**|| +|Forced Separate|Tuple|()|forced_separate|*,*,N,o,t, ,S,u,p,p,o,r,t,e,d,*,*|**No Description**| |Indent|Str| |indent|-i,--indent|String to place for indents defaults to " " (4 spaces).| -|Comment Prefix|Str| #|comment_prefix|**Not Supported**|| +|Comment Prefix|Str| #|comment_prefix|*,*,N,o,t, ,S,u,p,p,o,r,t,e,d,*,*|**No Description**| |Length Sort|Bool|False|length_sort|--ls,--length-sort|Sort imports by their string length.| -|Length Sort Sections|Frozenset|frozenset()|length_sort_sections|**Not Supported**|| +|Length Sort Sections|Frozenset|frozenset()|length_sort_sections|*,*,N,o,t, ,S,u,p,p,o,r,t,e,d,*,*|**No Description**| |Add Imports|Frozenset|frozenset()|add_imports|-a,--add-import|Adds the specified import line to all files, automatically determining correct placement.| |Remove Imports|Frozenset|frozenset()|remove_imports|--rm,--remove-import|Removes the specified import from all files.| |Reverse Relative|Bool|False|reverse_relative|--rr,--reverse-relative|Reverse order of relative imports.| |Force Single Line|Bool|False|force_single_line|--sl,--force-single-line-imports|Forces all from imports to appear on their own line| |Single Line Exclusions|Tuple|()|single_line_exclusions|--nsl,--single-line-exclusions|One or more modules to exclude from the single line rule.| |Default Section|Str|FIRSTPARTY|default_section|--sd,--section-default|Sets the default section for imports (by default FIRSTPARTY) options: ('FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER')| -|Import Headings|Dict|{}|import_headings|**Not Supported**|| +|Import Headings|Dict|{}|import_headings|*,*,N,o,t, ,S,u,p,p,o,r,t,e,d,*,*|**No Description**| |Balanced Wrapping|Bool|False|balanced_wrapping|-e,--balanced|Balances wrapping to produce the most consistent line length possible| |Use Parentheses|Bool|False|use_parentheses|--up,--use-parentheses|Use parenthesis for line continuation on length limit instead of slashes.| |Order By Type|Bool|True|order_by_type|--ot,--order-by-type|Order imports by type in addition to alphabetically| |Atomic|Bool|False|atomic|--ac,--atomic|Ensures the output doesn't save if the resulting file contains syntax errors.| -|Lines After Imports|Int|-1|lines_after_imports|--lai,--lines-after-imports|| -|Lines Between Sections|Int|1|lines_between_sections|**Not Supported**|| -|Lines Between Types|Int|0|lines_between_types|--lbt,--lines-between-types|| +|Lines After Imports|Int|-1|lines_after_imports|--lai,--lines-after-imports|**No Description**| +|Lines Between Sections|Int|1|lines_between_sections|*,*,N,o,t, ,S,u,p,p,o,r,t,e,d,*,*|**No Description**| +|Lines Between Types|Int|0|lines_between_types|--lbt,--lines-between-types|**No Description**| |Combine As Imports|Bool|False|combine_as_imports|--ca,--combine-as|Combines as imports on the same line.| |Combine Star|Bool|False|combine_star|--cs,--combine-star|Ensures that if a star import is present, nothing else is imported from that namespace.| |Keep Direct And As Imports|Bool|True|keep_direct_and_as_imports|-k,--keep-direct-and-as|Turns off default behavior that removes direct imports when as imports exist.| @@ -49,31 +49,31 @@ how you want your imports sorted, organized, and formatted. |Verbose|Bool|False|verbose|-v,--verbose|Shows verbose output, such as when files are skipped or when a check is successful.| |Quiet|Bool|False|quiet|-q,--quiet|Shows extra quiet output, only errors are outputted.| |Force Adds|Bool|False|force_adds|--af,--force-adds|Forces import adds even if the original file is empty.| -|Force Alphabetical Sort Within Sections|Bool|False|force_alphabetical_sort_within_sections|**Not Supported**|| +|Force Alphabetical Sort Within Sections|Bool|False|force_alphabetical_sort_within_sections|*,*,N,o,t, ,S,u,p,p,o,r,t,e,d,*,*|**No Description**| |Force Alphabetical Sort|Bool|False|force_alphabetical_sort|--fass,--force-alphabetical-sort-within-sections|Force all imports to be sorted alphabetically within a section| |Force Grid Wrap|Int|0|force_grid_wrap|--fgw,--force-grid-wrap|Force number of from imports (defaults to 2) to be grid wrapped regardless of line length| |Force Sort Within Sections|Bool|False|force_sort_within_sections|--fss,--force-sort-within-sections|Force imports to be sorted by module, independent of import_type| -|Lexicographical|Bool|False|lexicographical|**Not Supported**|| +|Lexicographical|Bool|False|lexicographical|*,*,N,o,t, ,S,u,p,p,o,r,t,e,d,*,*|**No Description**| |Ignore Whitespace|Bool|False|ignore_whitespace|--ws,--ignore-whitespace|Tells isort to ignore whitespace differences when --check-only is being used.| |No Lines Before|Frozenset|frozenset()|no_lines_before|--nlb,--no-lines-before|Sections which should not be split with previous by empty lines| |No Inline Sort|Bool|False|no_inline_sort|--nis,--no-inline-sort|Leaves `from` imports with multiple imports 'as-is' (e.g. `from foo import a, c ,b`).| -|Ignore Comments|Bool|False|ignore_comments|**Not Supported**|| +|Ignore Comments|Bool|False|ignore_comments|*,*,N,o,t, ,S,u,p,p,o,r,t,e,d,*,*|**No Description**| |Case Sensitive|Bool|False|case_sensitive|--case-sensitive|Tells isort to include casing when sorting module names| -|Sources|Tuple|()|sources|**Not Supported**|| +|Sources|Tuple|()|sources|*,*,N,o,t, ,S,u,p,p,o,r,t,e,d,*,*|**No Description**| |Virtual Env|Str||virtual_env|--virtual-env|Virtual environment to use for determining whether a package is third-party| |Conda Env|Str||conda_env|--conda-env|Conda environment to use for determining whether a package is third-party| |Ensure Newline Before Comments|Bool|False|ensure_newline_before_comments|-n,--ensure-newline-before-comments|Inserts a blank line before a comment following an import.| -|Directory|Str||directory|**Not Supported**|| +|Directory|Str||directory|*,*,N,o,t, ,S,u,p,p,o,r,t,e,d,*,*|**No Description**| |Profile|Str||profile|--profile|Base profile type to use for configuration.| |Check|Bool|False|**Not Supported**|-c,--check-only,--check|Checks the file for unsorted / unformatted imports and prints them to the command line without modifying the file.| |Write To Stdout|Bool|False|**Not Supported**|-d,--stdout|Force resulting output to stdout, instead of in-place.| |Show Diff|Bool|False|**Not Supported**|--df,--diff|Prints a diff of all the changes isort would make to a file, instead of changing it in place| |Jobs|Int|None|**Not Supported**|-j,--jobs|Number of files to process in parallel.| |Dont Order By Type|Bool|False|**Not Supported**|--dt,--dont-order-by-type|Don't order imports by type in addition to alphabetically| -|Settings Path|*Not Typed*|None|**Not Supported**|--sp,--settings-path,--settings-file,--settings|Explicitly set the settings path or file instead of auto determining based on file location.| +|Settings Path|Str|None|**Not Supported**|--sp,--settings-path,--settings-file,--settings|Explicitly set the settings path or file instead of auto determining based on file location.| |Show Version|Bool|False|**Not Supported**|-V,--version|Displays the currently installed version of isort.| |Version Number|Str|==SUPPRESS==|**Not Supported**|--vn,--version-number|Returns just the current version number without the logo| |Filter Files|Bool|False|**Not Supported**|--filter-files|Tells isort to filter files even when they are explicitly passed in as part of the command| -|Files|*Not Typed*|None|**Not Supported**||One or more Python source files that need their imports sorted.| +|Files|Str|None|**Not Supported**||One or more Python source files that need their imports sorted.| |Ask To Apply|Bool|False|**Not Supported**|--interactive|Tells isort to apply changes interactively.| |Show Config|Bool|False|**Not Supported**|--show-config|See isort's determined config, as well as sources of config options.| diff --git a/scripts/build_config_option_docs.py b/scripts/build_config_option_docs.py index eaad08838..d9a69c737 100755 --- a/scripts/build_config_option_docs.py +++ b/scripts/build_config_option_docs.py @@ -1,7 +1,8 @@ #! /bin/env python import os -from typing import Generator +from typing import Any, Generator, Type, Iterable +from isort._future import dataclass, field from isort.main import _build_arg_parser from isort.settings import _DEFAULT_SETTINGS as config @@ -25,6 +26,26 @@ parser = _build_arg_parser() +@dataclass(frozen=True) +class ConfigOption: + name: str + type: Type = str + default: Any = "" + config_name: str = "**Not Supported**" + cli_options: Iterable[str] = ("**Not Supported**") + description: str = "**No Description**" + + def __str__(self): + if self.name in IGNORED: + return "" + + description = DESCRIPTIONS.get(self.name, self.description).replace("\n", " ") + return ( + f"|{human(self.name)}|{human(self.type.__name__)}|{self.default}|{self.config_name}" + f"|{','.join(self.cli_options)}|{description}|\n" + ) + + def human(name: str) -> str: if name in HUMAN_NAME: return HUMAN_NAME[name] @@ -32,44 +53,41 @@ def human(name: str) -> str: return " ".join([part.capitalize() for part in name.replace("-", "_").split("_")]) -def config_options() -> Generator[str, None, None]: +def config_options() -> Generator[ConfigOption, None, None]: cli_actions = {} for action in parser._actions: cli_actions[action.dest] = action for name, default in config.items(): - if name in IGNORED: - continue - - description = DESCRIPTIONS.get(name, "") + extra_kwargs = {} cli = cli_actions.pop(name, None) if cli: - cli_options = ",".join(cli.option_strings) - description = description or cli.help or "" - else: - cli_options = "**Not Supported**" - description = description.replace("\n", " ") + extra_kwargs["cli_options"] = cli.option_strings + if cli.help: + extra_kwargs["description"] = cli.help - yield f"|{human(name)}|{human(type(default).__name__)}|{default}|{name}|{cli_options}|{description}|\n" + yield ConfigOption( + name=name, type=type(default), default=default, config_name=name, **extra_kwargs + ) for name, cli in cli_actions.items(): - if name in IGNORED: - continue - + extra_kwargs = {} if cli.type: - type_name = human(cli.type.__name__) + extra_kwargs["type"] = cli.type elif cli.default is not None: - type_name = human(type(cli.default).__name__) - else: - type_name = "*Not Typed*" + extra_kwargs["type"] = type(cli.default) + + if cli.help: + extra_kwargs["description"] = cli.help - description = (DESCRIPTIONS.get(name, cli.help) or "").replace("\n", " ") - yield f"|{human(name)}|{type_name}|{cli.default}|**Not Supported**|{','.join(cli.option_strings)}|{description}|\n" + yield ConfigOption( + name=name, default=cli.default, cli_options=cli.option_strings, **extra_kwargs + ) def document_text() -> str: - return f"{HEADER}{''.join(config_options())}" + return f"{HEADER}{''.join(str(config_option) for config_option in config_options())}" def write_document(): From 5d12a0b1927e6144f4c0db1c8d306c7cef66eb0a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 27 May 2020 00:13:10 -0700 Subject: [PATCH 0597/1439] Improve documentation format for arguments --- docs/configuration/options.md | 828 +++++++++++++++++++++++++--- scripts/build_config_option_docs.py | 23 +- 2 files changed, 771 insertions(+), 80 deletions(-) diff --git a/docs/configuration/options.md b/docs/configuration/options.md index ff913713b..1c62abf85 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -5,75 +5,759 @@ As a code formatter isort has opinions. However, it also allows you to have your isort will disagree but commit to your way of formatting. To enable this, isort exposes a plethora of options to specify how you want your imports sorted, organized, and formatted. -| Name | Type | Default | Python / Config file | CLI | Description | -| ---- | ---- | ------- | -------------------- | --- | ----------- | -|Python Version|Str|py3|py_version|--py,--python-version|Tells isort to set the known standard library based on the the specified Python version. Default is to assume any Python 3 version could be the target, and use a union off all stdlib modules across versions. If auto is specified, the version of the interpreter used to run isort (currently: 38) will be used.| -|Force To Top|Frozenset|frozenset()|force_to_top|-t,--top|Force specific imports to the top of their appropriate section.| -|Skip|Frozenset|frozenset({'buck-out', 'venv', '.hg', '.mypy_cache', '.eggs', '.nox', 'build', '.pants.d', '.tox', 'dist', '.git', '.venv', 'node_modules', '_build'})|skip|-s,--skip|Files that sort imports should skip over. If you want to skip multiple files you should specify twice: --skip file1 --skip file2.| -|Skip Glob|Frozenset|frozenset()|skip_glob|--sg,--skip-glob|Files that sort imports should skip over.| -|Line Length|Int|79|line_length|-l,-w,--line-length,--line-width|The max length of an import line (used for wrapping long imports).| -|Wrap Length|Int|0|wrap_length|--wl,--wrap-length|Specifies how long lines that are wrapped should be, if not set line_length is used. NOTE: wrap_length must be LOWER than or equal to line_length.| -|Line Ending|Str||line_ending|--le,--line-ending|Forces line endings to the specified value. If not set, values will be guessed per-file.| -|Sections|Tuple|('FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER')|sections|*,*,N,o,t, ,S,u,p,p,o,r,t,e,d,*,*|**No Description**| -|No Sections|Bool|False|no_sections|--ds,--no-sections|Put all imports into the same section bucket| -|Known Future Library|Frozenset|frozenset({'__future__'})|known_future_library|-f,--future|Force sortImports to recognize a module as part of the future compatibility libraries.| -|Known Third Party|Frozenset|frozenset({'google.appengine.api'})|known_third_party|-o,--thirdparty|Force sortImports to recognize a module as being part of a third party library.| -|Known First Party|Frozenset|frozenset()|known_first_party|-p,--project|Force sortImports to recognize a module as being part of the current python project.| -|Known Standard Library|Frozenset|frozenset({'argparse', 'turtledemo', 'chunk', 'binhex', 'cmath', 'profile', 'itertools', 'atexit', 'fractions', 'socketserver', 'audioop', 'telnetlib', 'wave', 'struct', 'crypt', 'tty', 'logging', 'cmd', 'gc', 'select', 'hashlib', 'cgi', 'mimetypes', 'pty', 'signal', 'test', 'symbol', 'fcntl', 'termios', 'imaplib', 'copyreg', 'parser', 'secrets', 'posix', 'fnmatch', 'optparse', 'tracemalloc', 'json', 'macpath', 'ssl', 'uuid', 'threading', 'mailcap', 'cgitb', 'lib2to3', 'imghdr', 'os', 'queue', 'codecs', 'sys', 'pipes', 'email', 'resource', 'quopri', 'selectors', 'syslog', 'types', 'textwrap', 'abc', 'operator', 'uu', 'wsgiref', 'weakref', 'ctypes', 'pprint', 'fpectl', 'concurrent', 'compileall', 'gettext', 'collections', 'glob', 'contextvars', 'multiprocessing', 'getopt', 'heapq', 'netrc', 'zipimport', 'sndhdr', 'subprocess', 'grp', 'string', 'symtable', 'pwd', 'zlib', 'py_compile', 'sqlite3', 'poplib', 'tempfile', 'contextlib', 'ast', 'ensurepip', 'getpass', 'urllib', 'lzma', 'calendar', 'ipaddress', 'plistlib', 'builtins', 'keyword', 'pstats', 'ossaudiodev', 'pyclbr', 'msilib', 'unittest', 'nis', 'unicodedata', 'functools', 'numbers', 'pathlib', 'smtpd', 'typing', 'rlcompleter', 'dis', 'pkgutil', 'mmap', 'winreg', 'shlex', 'array', 'venv', 'bz2', 'hmac', 'pickletools', 're', 'site', 'tkinter', 'asyncio', 'io', 'traceback', 'pydoc', 'inspect', 'token', 'distutils', 'enum', 'marshal', 'aifc', 'stringprep', 'dataclasses', 'sunau', 'curses', 'platform', 'random', 'tarfile', 'timeit', 'mailbox', 'runpy', 'tokenize', 'imp', 'winsound', 'filecmp', 'pickle', 'socket', 'doctest', 'spwd', 'nntplib', 'stat', 'importlib', 'xdrlib', 'gzip', 'cProfile', 'errno', 'warnings', 'shelve', 'configparser', '_thread', 'ftplib', 'encodings', 'sched', 'difflib', 'html', 'pdb', 'smtplib', 'base64', 'webbrowser', 'datetime', 'colorsys', 'trace', 'readline', 'copy', 'reprlib', 'codeop', 'xmlrpc', 'fileinput', 'decimal', 'zipfile', 'bdb', 'asyncore', 'dummy_threading', 'turtle', 'statistics', '_dummy_thread', 'csv', 'linecache', 'modulefinder', 'zipapp', 'bisect', 'formatter', 'asynchat', 'sysconfig', 'xml', 'math', 'dbm', 'faulthandler', 'shutil', 'http', 'locale', 'tabnanny', 'code', 'time', 'binascii', 'msvcrt'})|known_standard_library|-b,--builtin|Force sortImports to recognize a module as part of the python standard library.| -|Known Other|Dict|{}|known_other|*,*,N,o,t, ,S,u,p,p,o,r,t,e,d,*,*|**No Description**| -|Multi Line Output|Wrapmodes|WrapModes.GRID|multi_line_output|-m,--multi-line|Multi line output (0-grid, 1-vertical, 2-hanging, 3-vert-hanging, 4-vert-grid, 5-vert-grid-grouped, 6-vert-grid-grouped-no-comma).| -|Forced Separate|Tuple|()|forced_separate|*,*,N,o,t, ,S,u,p,p,o,r,t,e,d,*,*|**No Description**| -|Indent|Str| |indent|-i,--indent|String to place for indents defaults to " " (4 spaces).| -|Comment Prefix|Str| #|comment_prefix|*,*,N,o,t, ,S,u,p,p,o,r,t,e,d,*,*|**No Description**| -|Length Sort|Bool|False|length_sort|--ls,--length-sort|Sort imports by their string length.| -|Length Sort Sections|Frozenset|frozenset()|length_sort_sections|*,*,N,o,t, ,S,u,p,p,o,r,t,e,d,*,*|**No Description**| -|Add Imports|Frozenset|frozenset()|add_imports|-a,--add-import|Adds the specified import line to all files, automatically determining correct placement.| -|Remove Imports|Frozenset|frozenset()|remove_imports|--rm,--remove-import|Removes the specified import from all files.| -|Reverse Relative|Bool|False|reverse_relative|--rr,--reverse-relative|Reverse order of relative imports.| -|Force Single Line|Bool|False|force_single_line|--sl,--force-single-line-imports|Forces all from imports to appear on their own line| -|Single Line Exclusions|Tuple|()|single_line_exclusions|--nsl,--single-line-exclusions|One or more modules to exclude from the single line rule.| -|Default Section|Str|FIRSTPARTY|default_section|--sd,--section-default|Sets the default section for imports (by default FIRSTPARTY) options: ('FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER')| -|Import Headings|Dict|{}|import_headings|*,*,N,o,t, ,S,u,p,p,o,r,t,e,d,*,*|**No Description**| -|Balanced Wrapping|Bool|False|balanced_wrapping|-e,--balanced|Balances wrapping to produce the most consistent line length possible| -|Use Parentheses|Bool|False|use_parentheses|--up,--use-parentheses|Use parenthesis for line continuation on length limit instead of slashes.| -|Order By Type|Bool|True|order_by_type|--ot,--order-by-type|Order imports by type in addition to alphabetically| -|Atomic|Bool|False|atomic|--ac,--atomic|Ensures the output doesn't save if the resulting file contains syntax errors.| -|Lines After Imports|Int|-1|lines_after_imports|--lai,--lines-after-imports|**No Description**| -|Lines Between Sections|Int|1|lines_between_sections|*,*,N,o,t, ,S,u,p,p,o,r,t,e,d,*,*|**No Description**| -|Lines Between Types|Int|0|lines_between_types|--lbt,--lines-between-types|**No Description**| -|Combine As Imports|Bool|False|combine_as_imports|--ca,--combine-as|Combines as imports on the same line.| -|Combine Star|Bool|False|combine_star|--cs,--combine-star|Ensures that if a star import is present, nothing else is imported from that namespace.| -|Keep Direct And As Imports|Bool|True|keep_direct_and_as_imports|-k,--keep-direct-and-as|Turns off default behavior that removes direct imports when as imports exist.| -|Include Trailing Comma|Bool|False|include_trailing_comma|--tc,--trailing-comma|Includes a trailing comma on multi line imports that include parentheses.| -|From First|Bool|False|from_first|--ff,--from-first|Switches the typical ordering preference, showing from imports first then straight ones.| -|Verbose|Bool|False|verbose|-v,--verbose|Shows verbose output, such as when files are skipped or when a check is successful.| -|Quiet|Bool|False|quiet|-q,--quiet|Shows extra quiet output, only errors are outputted.| -|Force Adds|Bool|False|force_adds|--af,--force-adds|Forces import adds even if the original file is empty.| -|Force Alphabetical Sort Within Sections|Bool|False|force_alphabetical_sort_within_sections|*,*,N,o,t, ,S,u,p,p,o,r,t,e,d,*,*|**No Description**| -|Force Alphabetical Sort|Bool|False|force_alphabetical_sort|--fass,--force-alphabetical-sort-within-sections|Force all imports to be sorted alphabetically within a section| -|Force Grid Wrap|Int|0|force_grid_wrap|--fgw,--force-grid-wrap|Force number of from imports (defaults to 2) to be grid wrapped regardless of line length| -|Force Sort Within Sections|Bool|False|force_sort_within_sections|--fss,--force-sort-within-sections|Force imports to be sorted by module, independent of import_type| -|Lexicographical|Bool|False|lexicographical|*,*,N,o,t, ,S,u,p,p,o,r,t,e,d,*,*|**No Description**| -|Ignore Whitespace|Bool|False|ignore_whitespace|--ws,--ignore-whitespace|Tells isort to ignore whitespace differences when --check-only is being used.| -|No Lines Before|Frozenset|frozenset()|no_lines_before|--nlb,--no-lines-before|Sections which should not be split with previous by empty lines| -|No Inline Sort|Bool|False|no_inline_sort|--nis,--no-inline-sort|Leaves `from` imports with multiple imports 'as-is' (e.g. `from foo import a, c ,b`).| -|Ignore Comments|Bool|False|ignore_comments|*,*,N,o,t, ,S,u,p,p,o,r,t,e,d,*,*|**No Description**| -|Case Sensitive|Bool|False|case_sensitive|--case-sensitive|Tells isort to include casing when sorting module names| -|Sources|Tuple|()|sources|*,*,N,o,t, ,S,u,p,p,o,r,t,e,d,*,*|**No Description**| -|Virtual Env|Str||virtual_env|--virtual-env|Virtual environment to use for determining whether a package is third-party| -|Conda Env|Str||conda_env|--conda-env|Conda environment to use for determining whether a package is third-party| -|Ensure Newline Before Comments|Bool|False|ensure_newline_before_comments|-n,--ensure-newline-before-comments|Inserts a blank line before a comment following an import.| -|Directory|Str||directory|*,*,N,o,t, ,S,u,p,p,o,r,t,e,d,*,*|**No Description**| -|Profile|Str||profile|--profile|Base profile type to use for configuration.| -|Check|Bool|False|**Not Supported**|-c,--check-only,--check|Checks the file for unsorted / unformatted imports and prints them to the command line without modifying the file.| -|Write To Stdout|Bool|False|**Not Supported**|-d,--stdout|Force resulting output to stdout, instead of in-place.| -|Show Diff|Bool|False|**Not Supported**|--df,--diff|Prints a diff of all the changes isort would make to a file, instead of changing it in place| -|Jobs|Int|None|**Not Supported**|-j,--jobs|Number of files to process in parallel.| -|Dont Order By Type|Bool|False|**Not Supported**|--dt,--dont-order-by-type|Don't order imports by type in addition to alphabetically| -|Settings Path|Str|None|**Not Supported**|--sp,--settings-path,--settings-file,--settings|Explicitly set the settings path or file instead of auto determining based on file location.| -|Show Version|Bool|False|**Not Supported**|-V,--version|Displays the currently installed version of isort.| -|Version Number|Str|==SUPPRESS==|**Not Supported**|--vn,--version-number|Returns just the current version number without the logo| -|Filter Files|Bool|False|**Not Supported**|--filter-files|Tells isort to filter files even when they are explicitly passed in as part of the command| -|Files|Str|None|**Not Supported**||One or more Python source files that need their imports sorted.| -|Ask To Apply|Bool|False|**Not Supported**|--interactive|Tells isort to apply changes interactively.| -|Show Config|Bool|False|**Not Supported**|--show-config|See isort's determined config, as well as sources of config options.| + +## Python Version +Tells isort to set the known standard library based on the the specified Python version. Default is to assume any Python 3 version could be the target, and use a union off all stdlib modules across versions. If auto is specified, the version of the interpreter used to run isort (currently: 38) will be used. + +**Type:** String +**Default:** `py3` +**Python & Config File Name:** py_version +**CLI Flags:** + + - --py + - --python-version + +## Force To Top +Force specific imports to the top of their appropriate section. + +**Type:** Frozenset +**Default:** `frozenset()` +**Python & Config File Name:** force_to_top +**CLI Flags:** + + - -t + - --top + +## Skip +Files that sort imports should skip over. If you want to skip multiple files you should specify twice: --skip file1 --skip file2. + +**Type:** Frozenset +**Default:** `frozenset({'.tox', 'venv', 'build', '.pants.d', 'node_modules', '.nox', 'buck-out', '_build', 'dist', '.git', '.eggs', '.mypy_cache', '.hg', '.venv'})` +**Python & Config File Name:** skip +**CLI Flags:** + + - -s + - --skip + +## Skip Glob +Files that sort imports should skip over. + +**Type:** Frozenset +**Default:** `frozenset()` +**Python & Config File Name:** skip_glob +**CLI Flags:** + + - --sg + - --skip-glob + +## Line Length +The max length of an import line (used for wrapping long imports). + +**Type:** Int +**Default:** `79` +**Python & Config File Name:** line_length +**CLI Flags:** + + - -l + - -w + - --line-length + - --line-width + +## Wrap Length +Specifies how long lines that are wrapped should be, if not set line_length is used. +NOTE: wrap_length must be LOWER than or equal to line_length. + +**Type:** Int +**Default:** `0` +**Python & Config File Name:** wrap_length +**CLI Flags:** + + - --wl + - --wrap-length + +## Line Ending +Forces line endings to the specified value. If not set, values will be guessed per-file. + +**Type:** String +**Default:** `` +**Python & Config File Name:** line_ending +**CLI Flags:** + + - --le + - --line-ending + +## Sections +**No Description** + +**Type:** Tuple +**Default:** `('FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER')` +**Python & Config File Name:** sections +**CLI Flags:** + + - **Not Supported** + +## No Sections +Put all imports into the same section bucket + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** no_sections +**CLI Flags:** + + - --ds + - --no-sections + +## Known Future Library +Force sortImports to recognize a module as part of the future compatibility libraries. + +**Type:** Frozenset +**Default:** `frozenset({'__future__'})` +**Python & Config File Name:** known_future_library +**CLI Flags:** + + - -f + - --future + +## Known Third Party +Force sortImports to recognize a module as being part of a third party library. + +**Type:** Frozenset +**Default:** `frozenset({'google.appengine.api'})` +**Python & Config File Name:** known_third_party +**CLI Flags:** + + - -o + - --thirdparty + +## Known First Party +Force sortImports to recognize a module as being part of the current python project. + +**Type:** Frozenset +**Default:** `frozenset()` +**Python & Config File Name:** known_first_party +**CLI Flags:** + + - -p + - --project + +## Known Standard Library +Force sortImports to recognize a module as part of the python standard library. + +**Type:** Frozenset +**Default:** `frozenset({'gettext', 'calendar', 'sunau', 're', 'urllib', 'test', 'resource', 'html', 'platform', 'quopri', 'uu', 'shelve', 'zipfile', 'chunk', 'tokenize', 'turtle', 'imghdr', 'keyword', 'cmd', 'plistlib', 'struct', 'string', 'tarfile', 'ipaddress', 'encodings', 'webbrowser', 'difflib', 'copyreg', 'modulefinder', 'faulthandler', 'abc', 'code', 'netrc', 'gzip', 'binhex', 'fileinput', 'contextvars', 'smtplib', 'io', 'statistics', 'secrets', 'os', 'cgitb', 'grp', 'mmap', 'pyclbr', 'pathlib', 'heapq', 'winsound', 'cgi', 'pydoc', 'hmac', 'pkgutil', 'zlib', 'smtpd', 'distutils', 'pickle', 'codecs', 'math', 'lzma', 'unittest', 'email', 'dis', 'socket', 'tty', 'ssl', 'sysconfig', 'numbers', 'pdb', 'ossaudiodev', 'http', 'multiprocessing', 'pwd', 'shlex', 'socketserver', 'ctypes', 'mailbox', 'collections', 'sched', 'optparse', 'venv', 'builtins', 'site', 'dummy_threading', 'zipapp', 'imaplib', 'compileall', 'shutil', 'errno', 'cmath', 'argparse', 'pprint', 'telnetlib', 'enum', 'fpectl', 'poplib', 'time', 'csv', 'gc', 'select', 'codeop', 'nis', 'cProfile', 'warnings', 'asyncio', 'msilib', 'spwd', 'audioop', 'doctest', 'asyncore', 'ftplib', 'symbol', 'array', 'winreg', 'pipes', 'ensurepip', 'mailcap', 'typing', 'atexit', 'pstats', 'xdrlib', 'itertools', 'mimetypes', 'zipimport', 'aifc', 'rlcompleter', 'stringprep', 'textwrap', 'formatter', 'posix', 'tkinter', 'dbm', 'syslog', 'weakref', 'functools', 'linecache', 'datetime', 'curses', 'trace', 'pty', 'ast', 'nntplib', 'xml', 'decimal', 'turtledemo', 'profile', 'base64', 'filecmp', 'locale', 'readline', 'crypt', 'tabnanny', 'msvcrt', 'stat', 'glob', 'types', 'reprlib', 'wsgiref', 'json', 'lib2to3', 'py_compile', 'sndhdr', 'queue', 'uuid', 'threading', 'selectors', 'imp', 'asynchat', 'inspect', 'timeit', 'concurrent', 'bdb', 'getopt', 'unicodedata', 'termios', 'importlib', 'macpath', 'parser', 'wave', 'signal', 'sys', 'bisect', 'dataclasses', 'hashlib', 'fnmatch', 'pickletools', 'logging', 'sqlite3', 'tempfile', 'subprocess', 'traceback', 'tracemalloc', '_thread', 'xmlrpc', 'fractions', 'token', 'runpy', 'random', 'binascii', 'getpass', 'copy', 'configparser', 'fcntl', 'marshal', 'symtable', 'colorsys', 'operator', 'bz2', '_dummy_thread', 'contextlib'})` +**Python & Config File Name:** known_standard_library +**CLI Flags:** + + - -b + - --builtin + +## Known Other +**No Description** + +**Type:** Dict +**Default:** `{}` +**Python & Config File Name:** known_other +**CLI Flags:** + + - **Not Supported** + +## Multi Line Output +Multi line output (0-grid, 1-vertical, 2-hanging, 3-vert-hanging, 4-vert-grid, 5-vert-grid-grouped, 6-vert-grid-grouped-no-comma). + +**Type:** Wrapmodes +**Default:** `WrapModes.GRID` +**Python & Config File Name:** multi_line_output +**CLI Flags:** + + - -m + - --multi-line + +## Forced Separate +**No Description** + +**Type:** Tuple +**Default:** `()` +**Python & Config File Name:** forced_separate +**CLI Flags:** + + - **Not Supported** + +## Indent +String to place for indents defaults to " " (4 spaces). + +**Type:** String +**Default:** ` ` +**Python & Config File Name:** indent +**CLI Flags:** + + - -i + - --indent + +## Comment Prefix +**No Description** + +**Type:** String +**Default:** ` #` +**Python & Config File Name:** comment_prefix +**CLI Flags:** + + - **Not Supported** + +## Length Sort +Sort imports by their string length. + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** length_sort +**CLI Flags:** + + - --ls + - --length-sort + +## Length Sort Sections +**No Description** + +**Type:** Frozenset +**Default:** `frozenset()` +**Python & Config File Name:** length_sort_sections +**CLI Flags:** + + - **Not Supported** + +## Add Imports +Adds the specified import line to all files, automatically determining correct placement. + +**Type:** Frozenset +**Default:** `frozenset()` +**Python & Config File Name:** add_imports +**CLI Flags:** + + - -a + - --add-import + +## Remove Imports +Removes the specified import from all files. + +**Type:** Frozenset +**Default:** `frozenset()` +**Python & Config File Name:** remove_imports +**CLI Flags:** + + - --rm + - --remove-import + +## Reverse Relative +Reverse order of relative imports. + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** reverse_relative +**CLI Flags:** + + - --rr + - --reverse-relative + +## Force Single Line +Forces all from imports to appear on their own line + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** force_single_line +**CLI Flags:** + + - --sl + - --force-single-line-imports + +## Single Line Exclusions +One or more modules to exclude from the single line rule. + +**Type:** Tuple +**Default:** `()` +**Python & Config File Name:** single_line_exclusions +**CLI Flags:** + + - --nsl + - --single-line-exclusions + +## Default Section +Sets the default section for imports (by default FIRSTPARTY) options: ('FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER') + +**Type:** String +**Default:** `FIRSTPARTY` +**Python & Config File Name:** default_section +**CLI Flags:** + + - --sd + - --section-default + +## Import Headings +**No Description** + +**Type:** Dict +**Default:** `{}` +**Python & Config File Name:** import_headings +**CLI Flags:** + + - **Not Supported** + +## Balanced Wrapping +Balances wrapping to produce the most consistent line length possible + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** balanced_wrapping +**CLI Flags:** + + - -e + - --balanced + +## Use Parentheses +Use parenthesis for line continuation on length limit instead of slashes. + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** use_parentheses +**CLI Flags:** + + - --up + - --use-parentheses + +## Order By Type +Order imports by type in addition to alphabetically + +**Type:** Bool +**Default:** `True` +**Python & Config File Name:** order_by_type +**CLI Flags:** + + - --ot + - --order-by-type + +## Atomic +Ensures the output doesn't save if the resulting file contains syntax errors. + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** atomic +**CLI Flags:** + + - --ac + - --atomic + +## Lines After Imports +**No Description** + +**Type:** Int +**Default:** `-1` +**Python & Config File Name:** lines_after_imports +**CLI Flags:** + + - --lai + - --lines-after-imports + +## Lines Between Sections +**No Description** + +**Type:** Int +**Default:** `1` +**Python & Config File Name:** lines_between_sections +**CLI Flags:** + + - **Not Supported** + +## Lines Between Types +**No Description** + +**Type:** Int +**Default:** `0` +**Python & Config File Name:** lines_between_types +**CLI Flags:** + + - --lbt + - --lines-between-types + +## Combine As Imports +Combines as imports on the same line. + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** combine_as_imports +**CLI Flags:** + + - --ca + - --combine-as + +## Combine Star +Ensures that if a star import is present, nothing else is imported from that namespace. + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** combine_star +**CLI Flags:** + + - --cs + - --combine-star + +## Keep Direct And As Imports +Turns off default behavior that removes direct imports when as imports exist. + +**Type:** Bool +**Default:** `True` +**Python & Config File Name:** keep_direct_and_as_imports +**CLI Flags:** + + - -k + - --keep-direct-and-as + +## Include Trailing Comma +Includes a trailing comma on multi line imports that include parentheses. + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** include_trailing_comma +**CLI Flags:** + + - --tc + - --trailing-comma + +## From First +Switches the typical ordering preference, showing from imports first then straight ones. + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** from_first +**CLI Flags:** + + - --ff + - --from-first + +## Verbose +Shows verbose output, such as when files are skipped or when a check is successful. + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** verbose +**CLI Flags:** + + - -v + - --verbose + +## Quiet +Shows extra quiet output, only errors are outputted. + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** quiet +**CLI Flags:** + + - -q + - --quiet + +## Force Adds +Forces import adds even if the original file is empty. + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** force_adds +**CLI Flags:** + + - --af + - --force-adds + +## Force Alphabetical Sort Within Sections +**No Description** + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** force_alphabetical_sort_within_sections +**CLI Flags:** + + - **Not Supported** + +## Force Alphabetical Sort +Force all imports to be sorted alphabetically within a section + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** force_alphabetical_sort +**CLI Flags:** + + - --fass + - --force-alphabetical-sort-within-sections + +## Force Grid Wrap +Force number of from imports (defaults to 2) to be grid wrapped regardless of line length + +**Type:** Int +**Default:** `0` +**Python & Config File Name:** force_grid_wrap +**CLI Flags:** + + - --fgw + - --force-grid-wrap + +## Force Sort Within Sections +Force imports to be sorted by module, independent of import_type + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** force_sort_within_sections +**CLI Flags:** + + - --fss + - --force-sort-within-sections + +## Lexicographical +**No Description** + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** lexicographical +**CLI Flags:** + + - **Not Supported** + +## Ignore Whitespace +Tells isort to ignore whitespace differences when --check-only is being used. + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** ignore_whitespace +**CLI Flags:** + + - --ws + - --ignore-whitespace + +## No Lines Before +Sections which should not be split with previous by empty lines + +**Type:** Frozenset +**Default:** `frozenset()` +**Python & Config File Name:** no_lines_before +**CLI Flags:** + + - --nlb + - --no-lines-before + +## No Inline Sort +Leaves `from` imports with multiple imports 'as-is' (e.g. `from foo import a, c ,b`). + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** no_inline_sort +**CLI Flags:** + + - --nis + - --no-inline-sort + +## Ignore Comments +**No Description** + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** ignore_comments +**CLI Flags:** + + - **Not Supported** + +## Case Sensitive +Tells isort to include casing when sorting module names + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** case_sensitive +**CLI Flags:** + + - --case-sensitive + +## Sources +**No Description** + +**Type:** Tuple +**Default:** `()` +**Python & Config File Name:** sources +**CLI Flags:** + + - **Not Supported** + +## Virtual Env +Virtual environment to use for determining whether a package is third-party + +**Type:** String +**Default:** `` +**Python & Config File Name:** virtual_env +**CLI Flags:** + + - --virtual-env + +## Conda Env +Conda environment to use for determining whether a package is third-party + +**Type:** String +**Default:** `` +**Python & Config File Name:** conda_env +**CLI Flags:** + + - --conda-env + +## Ensure Newline Before Comments +Inserts a blank line before a comment following an import. + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** ensure_newline_before_comments +**CLI Flags:** + + - -n + - --ensure-newline-before-comments + +## Directory +**No Description** + +**Type:** String +**Default:** `` +**Python & Config File Name:** directory +**CLI Flags:** + + - **Not Supported** + +## Profile +Base profile type to use for configuration. + +**Type:** String +**Default:** `` +**Python & Config File Name:** profile +**CLI Flags:** + + - --profile + +## Check +Checks the file for unsorted / unformatted imports and prints them to the command line without modifying the file. + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** **Not Supported** +**CLI Flags:** + + - -c + - --check-only + - --check + +## Write To Stdout +Force resulting output to stdout, instead of in-place. + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** **Not Supported** +**CLI Flags:** + + - -d + - --stdout + +## Show Diff +Prints a diff of all the changes isort would make to a file, instead of changing it in place + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** **Not Supported** +**CLI Flags:** + + - --df + - --diff + +## Jobs +Number of files to process in parallel. + +**Type:** Int +**Default:** `None` +**Python & Config File Name:** **Not Supported** +**CLI Flags:** + + - -j + - --jobs + +## Dont Order By Type +Don't order imports by type in addition to alphabetically + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** **Not Supported** +**CLI Flags:** + + - --dt + - --dont-order-by-type + +## Settings Path +Explicitly set the settings path or file instead of auto determining based on file location. + +**Type:** String +**Default:** `None` +**Python & Config File Name:** **Not Supported** +**CLI Flags:** + + - --sp + - --settings-path + - --settings-file + - --settings + +## Show Version +Displays the currently installed version of isort. + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** **Not Supported** +**CLI Flags:** + + - -V + - --version + +## Version Number +Returns just the current version number without the logo + +**Type:** String +**Default:** `==SUPPRESS==` +**Python & Config File Name:** **Not Supported** +**CLI Flags:** + + - --vn + - --version-number + +## Filter Files +Tells isort to filter files even when they are explicitly passed in as part of the command + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** **Not Supported** +**CLI Flags:** + + - --filter-files + +## Files +One or more Python source files that need their imports sorted. + +**Type:** String +**Default:** `None` +**Python & Config File Name:** **Not Supported** +**CLI Flags:** + + - + +## Ask To Apply +Tells isort to apply changes interactively. + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** **Not Supported** +**CLI Flags:** + + - --interactive + +## Show Config +See isort's determined config, as well as sources of config options. + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** **Not Supported** +**CLI Flags:** + + - --show-config diff --git a/scripts/build_config_option_docs.py b/scripts/build_config_option_docs.py index d9a69c737..22e2f639c 100755 --- a/scripts/build_config_option_docs.py +++ b/scripts/build_config_option_docs.py @@ -9,7 +9,8 @@ OUTPUT_FILE = os.path.abspath( os.path.join(os.path.dirname(os.path.abspath(__file__)), "../docs/configuration/options.md") ) -HUMAN_NAME = {"py_version": "Python Version", "vn": "Version Number"} +MD_NEWLINE = " " +HUMAN_NAME = {"py_version": "Python Version", "vn": "Version Number", "str": "String"} DESCRIPTIONS = {} IGNORED = {"source", "help"} COLUMNS = ["Name", "Type", "Default", "Python / Config file", "CLI", "Description"] @@ -20,8 +21,6 @@ isort will disagree but commit to your way of formatting. To enable this, isort exposes a plethora of options to specify how you want your imports sorted, organized, and formatted. -| {' | '.join(COLUMNS)} | -| {' | '.join(["-" * len(column) for column in COLUMNS])} | """ parser = _build_arg_parser() @@ -32,7 +31,7 @@ class ConfigOption: type: Type = str default: Any = "" config_name: str = "**Not Supported**" - cli_options: Iterable[str] = ("**Not Supported**") + cli_options: Iterable[str] = ("**Not Supported**", ) description: str = "**No Description**" def __str__(self): @@ -40,10 +39,18 @@ def __str__(self): return "" description = DESCRIPTIONS.get(self.name, self.description).replace("\n", " ") - return ( - f"|{human(self.name)}|{human(self.type.__name__)}|{self.default}|{self.config_name}" - f"|{','.join(self.cli_options)}|{description}|\n" - ) + cli_options = "\n - ".join(self.cli_options) + return f""" +## {human(self.name)} +{self.description} + +**Type:** {human(self.type.__name__)}{MD_NEWLINE} +**Default:** `{self.default}`{MD_NEWLINE} +**Python & Config File Name:** {self.config_name}{MD_NEWLINE} +**CLI Flags:** + + - {cli_options} +""" def human(name: str) -> str: From 685fd59585943cf02385998e76c6b34eaa56b12a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 28 May 2020 22:42:04 -0700 Subject: [PATCH 0598/1439] Fix issues uncovered by deepsource --- scripts/build_config_option_docs.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/build_config_option_docs.py b/scripts/build_config_option_docs.py index 22e2f639c..79e641ad4 100755 --- a/scripts/build_config_option_docs.py +++ b/scripts/build_config_option_docs.py @@ -14,7 +14,7 @@ DESCRIPTIONS = {} IGNORED = {"source", "help"} COLUMNS = ["Name", "Type", "Default", "Python / Config file", "CLI", "Description"] -HEADER = f"""Configuration options for isort +HEADER = """Configuration options for isort ======== As a code formatter isort has opinions. However, it also allows you to have your own. If your opinions disagree with those of isort, @@ -38,7 +38,6 @@ def __str__(self): if self.name in IGNORED: return "" - description = DESCRIPTIONS.get(self.name, self.description).replace("\n", " ") cli_options = "\n - ".join(self.cli_options) return f""" ## {human(self.name)} From 0ebce23875a5ccac6b1fd17f65bfeb2dcbec7ce9 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 29 May 2020 23:59:38 -0700 Subject: [PATCH 0599/1439] Add initial doc string for sort_code_string --- isort/api.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/isort/api.py b/isort/api.py index cc8d7cd20..2a781dd11 100644 --- a/isort/api.py +++ b/isort/api.py @@ -59,6 +59,15 @@ def sort_code_string( disregard_skip: bool = False, **config_kwargs, ): + """Sorts any imports within the provided code string, returning a new string with them sorted. + + - **code**: The string of code with imports that need to be sorted. + - **extension**: The file extension that contains the code. + - **config**: The config object to use when sorting imports. + - **file_path**: The disk location where the code string was pulled from. + - **disregard_skip**: set to `True` if you want to ignore a skip set in config for this file. + - ** **config_kwargs**: Any config modifications. + """ input_stream = StringIO(code) output_stream = StringIO() config = _config(path=file_path, config=config, **config_kwargs) From b0d1f58c7f71b0d1ec7e4279335bac6ed0aca5d0 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 30 May 2020 00:16:41 -0700 Subject: [PATCH 0600/1439] Slight refactor of api commands, additional doc string --- isort/__init__.py | 2 +- isort/api.py | 29 ++++++++++++++++++++--------- isort/main.py | 2 +- scripts/build_config_option_docs.py | 4 ++-- 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/isort/__init__.py b/isort/__init__.py index 6d818e99f..7b334a9ae 100644 --- a/isort/__init__.py +++ b/isort/__init__.py @@ -6,4 +6,4 @@ from .api import check_imports as check_stream from .api import sort_code_string as code from .api import sort_file as file -from .api import sort_imports as stream +from .api import sort_stream as stream diff --git a/isort/api.py b/isort/api.py index 2a781dd11..243f98b22 100644 --- a/isort/api.py +++ b/isort/api.py @@ -66,12 +66,12 @@ def sort_code_string( - **config**: The config object to use when sorting imports. - **file_path**: The disk location where the code string was pulled from. - **disregard_skip**: set to `True` if you want to ignore a skip set in config for this file. - - ** **config_kwargs**: Any config modifications. + - ****config_kwargs**: Any config modifications. """ input_stream = StringIO(code) output_stream = StringIO() config = _config(path=file_path, config=config, **config_kwargs) - sorted_imports( + sort_stream( input_stream, output_stream, extension=extension, @@ -92,6 +92,17 @@ def check_code_string( disregard_skip: bool = False, **config_kwargs, ) -> bool: + """Checks the order, format, and categorization of imports within the provided code string. + Returns `True` if everything is correct, otherwise `False`. + + - **code**: The string of code with imports that need to be sorted. + - **show_diff**: If `True` the changes that need to be done will be printed to stdout. + - **extension**: The file extension that contains the code. + - **config**: The config object to use when sorting imports. + - **file_path**: The disk location where the code string was pulled from. + - **disregard_skip**: set to `True` if you want to ignore a skip set in config for this file. + - ****config_kwargs**: Any config modifications. + """ config = _config(path=file_path, config=config, **config_kwargs) return check_imports( StringIO(code), @@ -103,7 +114,7 @@ def check_code_string( ) -def sorted_imports( +def sort_stream( input_stream: TextIO, output_stream: TextIO, extension: str = "py", @@ -127,7 +138,7 @@ def sorted_imports( raise ExistingSyntaxErrors(content_source) try: - changed = sort_imports(input_stream, output_stream, extension=extension, config=config) + changed = _sort_imports(input_stream, output_stream, extension=extension, config=config) except FileSkipComment: raise FileSkipComment(content_source) @@ -153,7 +164,7 @@ def check_imports( ) -> bool: config = _config(path=file_path, config=config, **config_kwargs) - changed: bool = sorted_imports( + changed: bool = sort_stream( input_stream=input_stream, output_stream=Empty, extension=extension, @@ -172,7 +183,7 @@ def check_imports( output_stream = StringIO() input_stream.seek(0) file_contents = input_stream.read() - sorted_imports( + sort_stream( input_stream=StringIO(file_contents), output_stream=output_stream, extension=extension, @@ -188,7 +199,7 @@ def check_imports( return False -def sort_imports( +def _sort_imports( input_stream: TextIO, output_stream: TextIO, extension: str = "py", @@ -475,7 +486,7 @@ def sort_file( changed: bool = False try: if write_to_stdout: - changed = sorted_imports( + changed = sort_stream( input_stream=source_file.stream, output_stream=sys.stdout, config=config, @@ -490,7 +501,7 @@ def sort_file( "w", encoding=source_file.encoding, newline="" ) as output_stream: shutil.copymode(filename, tmp_file) - changed = sorted_imports( + changed = sort_stream( input_stream=source_file.stream, output_stream=output_stream, config=config, diff --git a/isort/main.py b/isort/main.py index 0dc198574..04b6c2170 100644 --- a/isort/main.py +++ b/isort/main.py @@ -599,7 +599,7 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = print(QUICK_GUIDE) return elif file_names == ["-"] and not show_config: - api.sorted_imports( + api.sort_stream( input_stream=sys.stdin if stdin is None else stdin, output_stream=sys.stdout, **arguments, diff --git a/scripts/build_config_option_docs.py b/scripts/build_config_option_docs.py index 79e641ad4..75b46d294 100755 --- a/scripts/build_config_option_docs.py +++ b/scripts/build_config_option_docs.py @@ -1,6 +1,6 @@ #! /bin/env python import os -from typing import Any, Generator, Type, Iterable +from typing import Any, Generator, Iterable, Type from isort._future import dataclass, field from isort.main import _build_arg_parser @@ -31,7 +31,7 @@ class ConfigOption: type: Type = str default: Any = "" config_name: str = "**Not Supported**" - cli_options: Iterable[str] = ("**Not Supported**", ) + cli_options: Iterable[str] = ("**Not Supported**",) description: str = "**No Description**" def __str__(self): From 2caaf4f778813d7868d4e486390a5479ae806904 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 30 May 2020 00:22:27 -0700 Subject: [PATCH 0601/1439] Save addition hypothesis auto test --- tests/test_wrap_modes.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/test_wrap_modes.py b/tests/test_wrap_modes.py index 2c841d99e..bf0d698e8 100644 --- a/tests/test_wrap_modes.py +++ b/tests/test_wrap_modes.py @@ -47,3 +47,20 @@ def test_auto_saved(): ) == "\U00092452-\U000bf82c\x0c\U0004608f\x10% NOQA" ) + assert ( + wrap_modes.noqa( + **{ + "comment_prefix": '\x12\x07\U0009e994🁣"\U000ae787\x0e', + "comments": ["\x00\U0001ae99\U0005c3e7\U0004d08e", "\x1e", "", ""], + "imports": ["*"], + "include_trailing_comma": True, + "indent": "", + "line_length": 31492, + "line_separator": "\U00071610\U0005bfbc", + "remove_comments": False, + "statement": "", + "white_space": "\x08\x01ⷓ\x16%\U0006cd8c", + } + ) + == '*\x12\x07\U0009e994🁣"\U000ae787\x0e \x00\U0001ae99\U0005c3e7\U0004d08e \x1e ' + ) From 227c3675f3bd72b063d68c7d4ef56d56e447b1b3 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 30 May 2020 01:25:12 -0700 Subject: [PATCH 0602/1439] check_imports -> check_stream --- isort/__init__.py | 3 +-- isort/api.py | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/isort/__init__.py b/isort/__init__.py index 7b334a9ae..66ce7dcb4 100644 --- a/isort/__init__.py +++ b/isort/__init__.py @@ -2,8 +2,7 @@ from . import settings # noqa: F401 from ._version import __version__ from .api import check_code_string as check_code -from .api import check_file -from .api import check_imports as check_stream +from .api import check_file, check_stream from .api import sort_code_string as code from .api import sort_file as file from .api import sort_stream as stream diff --git a/isort/api.py b/isort/api.py index 243f98b22..a16c9a806 100644 --- a/isort/api.py +++ b/isort/api.py @@ -104,7 +104,7 @@ def check_code_string( - ****config_kwargs**: Any config modifications. """ config = _config(path=file_path, config=config, **config_kwargs) - return check_imports( + return check_stream( StringIO(code), show_diff=show_diff, extension=extension, @@ -123,6 +123,17 @@ def sort_stream( disregard_skip: bool = False, **config_kwargs, ): + """Sorts any imports within the provided code stream, outputs to the provided output stream. + Directly returns nothing. + + - **input_stream**: The stream of code with imports that need to be sorted. + - **output_stream**: The stream where sorted imports should be written to. + - **extension**: The file extension that contains the code. + - **config**: The config object to use when sorting imports. + - **file_path**: The disk location where the code string was pulled from. + - **disregard_skip**: set to `True` if you want to ignore a skip set in config for this file. + - ****config_kwargs**: Any config modifications. + """ config = _config(path=file_path, config=config, **config_kwargs) content_source = str(file_path or "Passed in content") if not disregard_skip: @@ -153,7 +164,7 @@ def sort_stream( return changed -def check_imports( +def check_stream( input_stream: TextIO, show_diff: bool = False, extension: str = "py", @@ -460,7 +471,7 @@ def check_file( **config_kwargs, ) -> bool: with io.File.read(filename) as source_file: - return check_imports( + return check_stream( source_file.stream, show_diff=show_diff, extension=source_file.extension or "py", From 4e90b5462eba2a4d9d1fed7ad8f7c047bcde10c5 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 31 May 2020 22:35:39 -0700 Subject: [PATCH 0603/1439] Provide doc strings for all public API functions --- isort/api.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/isort/api.py b/isort/api.py index a16c9a806..5bdae0c67 100644 --- a/isort/api.py +++ b/isort/api.py @@ -173,6 +173,17 @@ def check_stream( disregard_skip: bool = False, **config_kwargs, ) -> bool: + """Checks any imports within the provided code stream, returning `False` if any unsorted or + incorrectly imports are found or `True` if no problems are identified. + + - **input_stream**: The stream of code with imports that need to be sorted. + - **show_diff**: If `True` the changes that need to be done will be printed to stdout. + - **extension**: The file extension that contains the code. + - **config**: The config object to use when sorting imports. + - **file_path**: The disk location where the code string was pulled from. + - **disregard_skip**: set to `True` if you want to ignore a skip set in config for this file. + - ****config_kwargs**: Any config modifications. + """ config = _config(path=file_path, config=config, **config_kwargs) changed: bool = sort_stream( @@ -470,6 +481,17 @@ def check_file( disregard_skip: bool = True, **config_kwargs, ) -> bool: + """Checks any imports within the provided file, returning `False` if any unsorted or + incorrectly imports are found or `True` if no problems are identified. + + - **filename**: The name or Path of the file to check. + - **show_diff**: If `True` the changes that need to be done will be printed to stdout. + - **extension**: The file extension that contains the code. + - **config**: The config object to use when sorting imports. + - **file_path**: The disk location where the code string was pulled from. + - **disregard_skip**: set to `True` if you want to ignore a skip set in config for this file. + - ****config_kwargs**: Any config modifications. + """ with io.File.read(filename) as source_file: return check_stream( source_file.stream, @@ -493,6 +515,18 @@ def sort_file( write_to_stdout: bool = False, **config_kwargs, ): + """Sorts and formats any groups of imports imports within the provided file or Path. + + - **filename**: The name or Path of the file to format. + - **extension**: The file extension that contains the code. + - **config**: The config object to use when sorting imports. + - **file_path**: The disk location where the code string was pulled from. + - **disregard_skip**: set to `True` if you want to ignore a skip set in config for this file. + - **ask_to_apply**: If `True`, prompt before applying any changes. + - **show_diff**: If `True` the changes that need to be done will be printed to stdout. + - **write_to_stdout**: If `True`, write to stdout instead of the input file. + - ****config_kwargs**: Any config modifications. + """ with io.File.read(filename) as source_file: changed: bool = False try: From 4d16c443de74793bf413f08d427dce6ac3ec9461 Mon Sep 17 00:00:00 2001 From: Maciej Gawinecki Date: Mon, 1 Jun 2020 16:40:00 +0200 Subject: [PATCH 0604/1439] Bug fix for #1221 Bug fix for improper handling of packages with - (dash) in name --- isort/finders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/finders.py b/isort/finders.py index 9fc98b5fb..05ba09bd9 100644 --- a/isort/finders.py +++ b/isort/finders.py @@ -265,7 +265,7 @@ def _normalize_name(self, name: str) -> str: Flask-RESTFul -> flask_restful """ if self.mapping: - name = self.mapping.get(name, name) + name = self.mapping.get(name.replace("-","_"), name) return name.lower().replace("-", "_") def find(self, module_name: str) -> Optional[str]: From 938392d1c94f469e25ec79c176de98e67bc921da Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 1 Jun 2020 19:40:53 -0700 Subject: [PATCH 0605/1439] Move API helper methods to below the field --- isort/api.py | 262 +++++++++++++++++++++++++-------------------------- 1 file changed, 131 insertions(+), 131 deletions(-) diff --git a/isort/api.py b/isort/api.py index 5bdae0c67..817799807 100644 --- a/isort/api.py +++ b/isort/api.py @@ -28,29 +28,6 @@ COMMENT_INDICATORS = ('"""', "'''", "'", '"', "#") -def _config( - path: Optional[Path] = None, config: Config = DEFAULT_CONFIG, **config_kwargs -) -> Config: - if path: - if ( - config is DEFAULT_CONFIG - and "settings_path" not in config_kwargs - and "settings_file" not in config_kwargs - ): - config_kwargs["settings_path"] = path - - if config_kwargs: - if config is not DEFAULT_CONFIG: - raise ValueError( - "You can either specify custom configuration options using kwargs or " - "passing in a Config object. Not Both!" - ) - - config = Config(**config_kwargs) - - return config - - def sort_code_string( code: str, extension="py", @@ -221,6 +198,137 @@ def check_stream( return False +def check_file( + filename: Union[str, Path], + show_diff: bool = False, + config: Config = DEFAULT_CONFIG, + file_path: Optional[Path] = None, + disregard_skip: bool = True, + **config_kwargs, +) -> bool: + """Checks any imports within the provided file, returning `False` if any unsorted or + incorrectly imports are found or `True` if no problems are identified. + + - **filename**: The name or Path of the file to check. + - **show_diff**: If `True` the changes that need to be done will be printed to stdout. + - **extension**: The file extension that contains the code. + - **config**: The config object to use when sorting imports. + - **file_path**: The disk location where the code string was pulled from. + - **disregard_skip**: set to `True` if you want to ignore a skip set in config for this file. + - ****config_kwargs**: Any config modifications. + """ + with io.File.read(filename) as source_file: + return check_stream( + source_file.stream, + show_diff=show_diff, + extension=source_file.extension or "py", + config=config, + file_path=file_path or source_file.path, + disregard_skip=disregard_skip, + **config_kwargs, + ) + + +def sort_file( + filename: Union[str, Path], + extension: str = "py", + config: Config = DEFAULT_CONFIG, + file_path: Optional[Path] = None, + disregard_skip: bool = True, + ask_to_apply: bool = False, + show_diff: bool = False, + write_to_stdout: bool = False, + **config_kwargs, +): + """Sorts and formats any groups of imports imports within the provided file or Path. + + - **filename**: The name or Path of the file to format. + - **extension**: The file extension that contains the code. + - **config**: The config object to use when sorting imports. + - **file_path**: The disk location where the code string was pulled from. + - **disregard_skip**: set to `True` if you want to ignore a skip set in config for this file. + - **ask_to_apply**: If `True`, prompt before applying any changes. + - **show_diff**: If `True` the changes that need to be done will be printed to stdout. + - **write_to_stdout**: If `True`, write to stdout instead of the input file. + - ****config_kwargs**: Any config modifications. + """ + with io.File.read(filename) as source_file: + changed: bool = False + try: + if write_to_stdout: + changed = sort_stream( + input_stream=source_file.stream, + output_stream=sys.stdout, + config=config, + file_path=file_path or source_file.path, + disregard_skip=disregard_skip, + **config_kwargs, + ) + else: + tmp_file = source_file.path.with_suffix(source_file.path.suffix + ".isorted") + try: + with tmp_file.open( + "w", encoding=source_file.encoding, newline="" + ) as output_stream: + shutil.copymode(filename, tmp_file) + changed = sort_stream( + input_stream=source_file.stream, + output_stream=output_stream, + config=config, + file_path=file_path or source_file.path, + disregard_skip=disregard_skip, + **config_kwargs, + ) + if changed: + if show_diff or ask_to_apply: + source_file.stream.seek(0) + show_unified_diff( + file_input=source_file.stream.read(), + file_output=tmp_file.read_text(encoding=source_file.encoding), + file_path=file_path or source_file.path, + ) + if ask_to_apply and not ask_whether_to_apply_changes_to_file( + str(source_file.path) + ): + return + source_file.stream.close() + tmp_file.replace(source_file.path) + if not config.quiet: + print(f"Fixing {source_file.path}") + finally: + try: # Python 3.8+: use `missing_ok=True` instead of try except. + tmp_file.unlink() + except FileNotFoundError: + pass + except ExistingSyntaxErrors: + warn("{file_path} unable to sort due to existing syntax errors") + except IntroducedSyntaxErrors: # pragma: no cover + warn("{file_path} unable to sort as isort introduces new syntax errors") + + +def _config( + path: Optional[Path] = None, config: Config = DEFAULT_CONFIG, **config_kwargs +) -> Config: + if path: + if ( + config is DEFAULT_CONFIG + and "settings_path" not in config_kwargs + and "settings_file" not in config_kwargs + ): + config_kwargs["settings_path"] = path + + if config_kwargs: + if config is not DEFAULT_CONFIG: + raise ValueError( + "You can either specify custom configuration options using kwargs or " + "passing in a Config object. Not Both!" + ) + + config = Config(**config_kwargs) + + return config + + def _sort_imports( input_stream: TextIO, output_stream: TextIO, @@ -471,111 +579,3 @@ def _sort_imports( not_imports = False return made_changes - - -def check_file( - filename: Union[str, Path], - show_diff: bool = False, - config: Config = DEFAULT_CONFIG, - file_path: Optional[Path] = None, - disregard_skip: bool = True, - **config_kwargs, -) -> bool: - """Checks any imports within the provided file, returning `False` if any unsorted or - incorrectly imports are found or `True` if no problems are identified. - - - **filename**: The name or Path of the file to check. - - **show_diff**: If `True` the changes that need to be done will be printed to stdout. - - **extension**: The file extension that contains the code. - - **config**: The config object to use when sorting imports. - - **file_path**: The disk location where the code string was pulled from. - - **disregard_skip**: set to `True` if you want to ignore a skip set in config for this file. - - ****config_kwargs**: Any config modifications. - """ - with io.File.read(filename) as source_file: - return check_stream( - source_file.stream, - show_diff=show_diff, - extension=source_file.extension or "py", - config=config, - file_path=file_path or source_file.path, - disregard_skip=disregard_skip, - **config_kwargs, - ) - - -def sort_file( - filename: Union[str, Path], - extension: str = "py", - config: Config = DEFAULT_CONFIG, - file_path: Optional[Path] = None, - disregard_skip: bool = True, - ask_to_apply: bool = False, - show_diff: bool = False, - write_to_stdout: bool = False, - **config_kwargs, -): - """Sorts and formats any groups of imports imports within the provided file or Path. - - - **filename**: The name or Path of the file to format. - - **extension**: The file extension that contains the code. - - **config**: The config object to use when sorting imports. - - **file_path**: The disk location where the code string was pulled from. - - **disregard_skip**: set to `True` if you want to ignore a skip set in config for this file. - - **ask_to_apply**: If `True`, prompt before applying any changes. - - **show_diff**: If `True` the changes that need to be done will be printed to stdout. - - **write_to_stdout**: If `True`, write to stdout instead of the input file. - - ****config_kwargs**: Any config modifications. - """ - with io.File.read(filename) as source_file: - changed: bool = False - try: - if write_to_stdout: - changed = sort_stream( - input_stream=source_file.stream, - output_stream=sys.stdout, - config=config, - file_path=file_path or source_file.path, - disregard_skip=disregard_skip, - **config_kwargs, - ) - else: - tmp_file = source_file.path.with_suffix(source_file.path.suffix + ".isorted") - try: - with tmp_file.open( - "w", encoding=source_file.encoding, newline="" - ) as output_stream: - shutil.copymode(filename, tmp_file) - changed = sort_stream( - input_stream=source_file.stream, - output_stream=output_stream, - config=config, - file_path=file_path or source_file.path, - disregard_skip=disregard_skip, - **config_kwargs, - ) - if changed: - if show_diff or ask_to_apply: - source_file.stream.seek(0) - show_unified_diff( - file_input=source_file.stream.read(), - file_output=tmp_file.read_text(encoding=source_file.encoding), - file_path=file_path or source_file.path, - ) - if ask_to_apply and not ask_whether_to_apply_changes_to_file( - str(source_file.path) - ): - return - source_file.stream.close() - tmp_file.replace(source_file.path) - if not config.quiet: - print(f"Fixing {source_file.path}") - finally: - try: # Python 3.8+: use `missing_ok=True` instead of try except. - tmp_file.unlink() - except FileNotFoundError: - pass - except ExistingSyntaxErrors: - warn("{file_path} unable to sort due to existing syntax errors") - except IntroducedSyntaxErrors: # pragma: no cover - warn("{file_path} unable to sort as isort introduces new syntax errors") From 158b22e223dd13ca144237353b07947fd8e055ed Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 2 Jun 2020 06:03:12 -0700 Subject: [PATCH 0606/1439] Auto format --- isort/finders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/finders.py b/isort/finders.py index 05ba09bd9..3b86900fe 100644 --- a/isort/finders.py +++ b/isort/finders.py @@ -265,7 +265,7 @@ def _normalize_name(self, name: str) -> str: Flask-RESTFul -> flask_restful """ if self.mapping: - name = self.mapping.get(name.replace("-","_"), name) + name = self.mapping.get(name.replace("-", "_"), name) return name.lower().replace("-", "_") def find(self, module_name: str) -> Optional[str]: From 2b10d3f85232523bb38af7fabcd19d182567ac63 Mon Sep 17 00:00:00 2001 From: Nikolaus Wittenstein Date: Wed, 3 Jun 2020 12:28:40 -0400 Subject: [PATCH 0607/1439] Change sortImports to isort in CLI help --- isort/main.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/isort/main.py b/isort/main.py index 04b6c2170..2744fdeef 100644 --- a/isort/main.py +++ b/isort/main.py @@ -161,7 +161,7 @@ def _build_arg_parser() -> argparse.ArgumentParser: "--builtin", dest="known_standard_library", action="append", - help="Force sortImports to recognize a module as part of the python standard library.", + help="Force isort to recognize a module as part of the python standard library.", ) parser.add_argument( "-c", @@ -221,7 +221,7 @@ def _build_arg_parser() -> argparse.ArgumentParser: "--future", dest="known_future_library", action="append", - help="Force sortImports to recognize a module as part " + help="Force isort to recognize a module as part " "of the future compatibility libraries.", ) parser.add_argument( @@ -332,7 +332,7 @@ def _build_arg_parser() -> argparse.ArgumentParser: "--thirdparty", dest="known_third_party", action="append", - help="Force sortImports to recognize a module as being part of a third party library.", + help="Force isort to recognize a module as being part of a third party library.", ) parser.add_argument( "--ot", @@ -353,7 +353,7 @@ def _build_arg_parser() -> argparse.ArgumentParser: "--project", dest="known_first_party", action="append", - help="Force sortImports to recognize a module as being part of the current python project.", + help="Force isort to recognize a module as being part of the current python project.", ) parser.add_argument( "-q", From 2d8ada319f2b7bd68d78df7827519ba4067e0600 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 3 Jun 2020 23:08:39 -0700 Subject: [PATCH 0608/1439] Small formatting fix --- isort/setuptools_commands.py | 1 - 1 file changed, 1 deletion(-) diff --git a/isort/setuptools_commands.py b/isort/setuptools_commands.py index 62c595fbe..9a3966a0f 100644 --- a/isort/setuptools_commands.py +++ b/isort/setuptools_commands.py @@ -14,7 +14,6 @@ class ISortCommand(setuptools.Command): """The :class:`ISortCommand` class is used by setuptools to perform imports checks on registered modules. """ - description = "Run isort on modules registered in setuptools" user_options: List[Any] = [] From 7c066127eb7894d89eefc19a8945f32b74ad3064 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 3 Jun 2020 23:49:21 -0700 Subject: [PATCH 0609/1439] Add config option --- isort/main.py | 2 ++ isort/settings.py | 4 ++++ isort/setuptools_commands.py | 1 + 3 files changed, 7 insertions(+) diff --git a/isort/main.py b/isort/main.py index 04b6c2170..69eee5cf1 100644 --- a/isort/main.py +++ b/isort/main.py @@ -567,6 +567,8 @@ def _preconvert(item): return list(item) elif isinstance(item, WrapModes): return item.name + elif isinstance(item, Path): + return str(item) else: raise TypeError("Unserializable object {} of type {}".format(item, type(item))) diff --git a/isort/settings.py b/isort/settings.py index a61e000a0..b3d129801 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -161,6 +161,7 @@ class _Config: ensure_newline_before_comments: bool = False directory: str = "" profile: str = "" + src_paths: FrozenSet[Path] = frozenset() def __post_init__(self): py_version = self.py_version @@ -298,6 +299,9 @@ def __init__( combined_config.pop(f"{IMPORT_HEADING_PREFIX}{import_heading_key}") combined_config["import_headings"] = import_headings + if "src_paths" not in combined_config: + combined_config["src_paths"] = frozenset((Path.cwd(),)) + super().__init__(sources=tuple(sources), **combined_config) # type: ignore def is_skipped(self, file_path: Path) -> bool: diff --git a/isort/setuptools_commands.py b/isort/setuptools_commands.py index 9a3966a0f..62c595fbe 100644 --- a/isort/setuptools_commands.py +++ b/isort/setuptools_commands.py @@ -14,6 +14,7 @@ class ISortCommand(setuptools.Command): """The :class:`ISortCommand` class is used by setuptools to perform imports checks on registered modules. """ + description = "Run isort on modules registered in setuptools" user_options: List[Any] = [] From 6df7ef4f2044a7d0eadff657e6e29ba0e6fee081 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 4 Jun 2020 23:51:22 -0700 Subject: [PATCH 0610/1439] Add support for src_paths inside of finders --- isort/finders.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/isort/finders.py b/isort/finders.py index 3b86900fe..b5dbd557c 100644 --- a/isort/finders.py +++ b/isort/finders.py @@ -186,10 +186,14 @@ def find(self, module_name: str) -> Optional[str]: return sections.STDLIB elif self.conda_env and self.conda_env in prefix: return sections.THIRDPARTY - if os.getcwd() in package_path: - return sections.FIRSTPARTY - elif os.path.normcase(prefix).startswith(self.stdlib_lib_prefix): + + for src_path in self.config.src_paths: + if str(src_path.absolute) in os.path.abspath(package_path): + return sections.FIRSTPARTY + + if os.path.normcase(prefix).startswith(self.stdlib_lib_prefix): return sections.STDLIB # pragma: no cover - edge case for one OS. Hard to test. + return self.config.default_section return None From a58781fdeb0b2da6fd97c05e2adf935bcb1b55d1 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 5 Jun 2020 21:50:57 -0700 Subject: [PATCH 0611/1439] Add test case for src_paths --- tests/test_finders.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_finders.py b/tests/test_finders.py index 94acfdfd4..ce7ecb73e 100644 --- a/tests/test_finders.py +++ b/tests/test_finders.py @@ -1,3 +1,4 @@ +from pathlib import Path from unittest.mock import patch import pytest @@ -86,6 +87,13 @@ def test_default_section(self, tmpdir): tmpdir.join("file.py").write("import b\nimport a\n") assert self.kind(settings.Config(default_section="CUSTOM"), tmpdir).find("file") == "CUSTOM" + def test_src_paths(self, tmpdir): + tmpdir.join("file.py").write("import b\nimport a\n") + assert ( + self.kind(settings.Config(src_paths=[Path(str(tmpdir))]), tmpdir).find("file") + == "FIRSTPARTY" + ) + class TestPipfileFinder(AbstractTestFinder): kind = finders.PipfileFinder From 0c9fa56439d153b25db81ed59cc88baa4f7837b5 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 6 Jun 2020 23:23:50 -0700 Subject: [PATCH 0612/1439] Fix src_path check, reaching 100% coverage again --- isort/finders.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/isort/finders.py b/isort/finders.py index b5dbd557c..489739cf6 100644 --- a/isort/finders.py +++ b/isort/finders.py @@ -186,9 +186,8 @@ def find(self, module_name: str) -> Optional[str]: return sections.STDLIB elif self.conda_env and self.conda_env in prefix: return sections.THIRDPARTY - for src_path in self.config.src_paths: - if str(src_path.absolute) in os.path.abspath(package_path): + if str(src_path.absolute()) in os.path.abspath(package_path): return sections.FIRSTPARTY if os.path.normcase(prefix).startswith(self.stdlib_lib_prefix): From f2fc6befaeed3209620cd7f7f8148ddf8f06a1b6 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 8 Jun 2020 21:55:04 -0700 Subject: [PATCH 0613/1439] Add source paths --- isort/main.py | 8 ++ poetry.lock | 283 +++++++++++++++++++++++++------------------------- 2 files changed, 152 insertions(+), 139 deletions(-) diff --git a/isort/main.py b/isort/main.py index 69eee5cf1..e54f68f9a 100644 --- a/isort/main.py +++ b/isort/main.py @@ -134,6 +134,14 @@ def _build_arg_parser() -> argparse.ArgumentParser: "stdin. Otherwise provide a list of files to sort." ) inline_args_group = parser.add_mutually_exclusive_group() + parser.add_argument( + "--src", + "--src-path", + dest="source_paths", + action="append", + help="Add an explicitly defined source path " + "(modules within src paths have their imports automatically catorgorized as first_party).", + ) parser.add_argument( "-a", "--add-import", diff --git a/poetry.lock b/poetry.lock index 005f3469c..b64bd119c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -125,7 +125,7 @@ description = "Python package for providing Mozilla's CA Bundle." name = "certifi" optional = false python-versions = "*" -version = "2020.4.5.1" +version = "2020.4.5.2" [[package]] category = "main" @@ -272,7 +272,7 @@ description = "the modular source code checker: pep8 pyflakes and co" name = "flake8" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" -version = "3.8.0" +version = "3.8.3" [package.dependencies] mccabe = ">=0.6.0,<0.7.0" @@ -342,7 +342,7 @@ description = "Python Git Library" name = "gitpython" optional = false python-versions = ">=3.4" -version = "3.1.2" +version = "3.1.3" [package.dependencies] gitdb = ">=4.0.1,<5" @@ -365,7 +365,7 @@ description = "A library for property-based testing" name = "hypothesis" optional = false python-versions = ">=3.5.2" -version = "5.12.1" +version = "5.16.0" [package.dependencies] attrs = ">=19.2.0" @@ -412,14 +412,14 @@ marker = "python_version < \"3.8\"" name = "importlib-metadata" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -version = "1.6.0" +version = "1.6.1" [package.dependencies] zipp = ">=0.5" [package.extras] docs = ["sphinx", "rst.linker"] -testing = ["packaging", "importlib-resources"] +testing = ["packaging", "pep517", "importlib-resources (>=1.3)"] [[package]] category = "dev" @@ -427,7 +427,7 @@ description = "IPython: Productive Interactive Computing" name = "ipython" optional = false python-versions = ">=3.6" -version = "7.14.0" +version = "7.15.0" [package.dependencies] appnope = "*" @@ -443,7 +443,7 @@ setuptools = ">=18.5" traitlets = ">=4.2" [package.extras] -all = ["nose (>=0.10.1)", "Sphinx (>=1.3)", "testpath", "nbformat", "ipywidgets", "qtconsole", "numpy (>=1.14)", "notebook", "ipyparallel", "ipykernel", "pygments", "requests", "nbconvert"] +all = ["Sphinx (>=1.3)", "ipykernel", "ipyparallel", "ipywidgets", "nbconvert", "nbformat", "nose (>=0.10.1)", "notebook", "numpy (>=1.14)", "pygments", "qtconsole", "requests", "testpath"] doc = ["Sphinx (>=1.3)"] kernel = ["ipykernel"] nbconvert = ["nbconvert"] @@ -505,10 +505,11 @@ jinja2 = "*" [[package]] category = "dev" description = "Lightweight pipelining: using Python functions as pipeline jobs." +marker = "python_version > \"2.7\"" name = "joblib" optional = false -python-versions = "*" -version = "0.14.1" +python-versions = ">=3.6" +version = "0.15.1" [[package]] category = "dev" @@ -516,11 +517,14 @@ description = "Python LiveReload is an awesome tool for web developers" name = "livereload" optional = false python-versions = "*" -version = "2.6.1" +version = "2.6.2" [package.dependencies] six = "*" -tornado = "*" + +[package.dependencies.tornado] +python = ">=2.8" +version = "*" [[package]] category = "dev" @@ -528,7 +532,7 @@ description = "A Python implementation of Lunr.js" name = "lunr" optional = false python-versions = "*" -version = "0.5.6" +version = "0.5.8" [package.dependencies] future = ">=0.16.0" @@ -536,10 +540,11 @@ six = ">=1.11.0" [package.dependencies.nltk] optional = true +python = ">=2.8" version = ">=3.2.5" [package.extras] -languages = ["nltk (>=3.2.5)"] +languages = ["nltk (>=3.2.5,<3.5)", "nltk (>=3.2.5)"] [[package]] category = "dev" @@ -547,7 +552,7 @@ description = "A super-fast templating language that borrows the best ideas fro name = "mako" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.1.2" +version = "1.1.3" [package.dependencies] MarkupSafe = ">=0.9.2" @@ -594,7 +599,7 @@ description = "Project documentation with Markdown." name = "mkdocs" optional = false python-versions = ">=3.5" -version = "1.1" +version = "1.1.2" [package.dependencies] Jinja2 = ">=2.10.1" @@ -606,7 +611,7 @@ tornado = ">=5.0" [package.dependencies.lunr] extras = ["languages"] -version = "0.5.6" +version = "0.5.8" [[package]] category = "dev" @@ -628,7 +633,7 @@ description = "More routines for operating on iterables, beyond itertools" name = "more-itertools" optional = false python-versions = ">=3.5" -version = "8.2.0" +version = "8.3.0" [[package]] category = "dev" @@ -657,6 +662,7 @@ version = "0.4.3" [[package]] category = "dev" description = "Natural Language Toolkit" +marker = "python_version > \"2.7\"" name = "nltk" optional = false python-versions = "*" @@ -682,7 +688,7 @@ description = "NumPy is the fundamental package for array computing with Python. name = "numpy" optional = false python-versions = ">=3.5" -version = "1.18.4" +version = "1.18.5" [[package]] category = "main" @@ -701,7 +707,7 @@ description = "Core utilities for Python packages" name = "packaging" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "20.3" +version = "20.4" [package.dependencies] pyparsing = ">=2.0.2" @@ -732,7 +738,7 @@ description = "A simple program and library to auto generate API documentation f name = "pdocs" optional = false python-versions = ">=3.6,<4.0" -version = "1.0.1" +version = "1.0.2" [package.dependencies] Mako = ">=1.1,<2.0" @@ -884,17 +890,17 @@ description = "Your Project with Great Documentation" name = "portray" optional = false python-versions = ">=3.6,<4.0" -version = "1.3.1" +version = "1.3.2" [package.dependencies] GitPython = ">=3.0,<4.0" hug = ">=2.6,<3.0" mkdocs = ">=1.0,<2.0" mkdocs-material = ">=4.4,<5.0" -pdocs = ">=1.0,<2.0" +pdocs = ">=1.0.2,<2.0.0" pymdown-extensions = ">=6.0,<7.0" toml = ">=0.10.0,<0.11.0" -yaspin = "0.15.0" +yaspin = ">=0.15.0,<0.16.0" [[package]] category = "dev" @@ -1024,7 +1030,7 @@ description = "pytest: simple powerful testing with Python" name = "pytest" optional = false python-versions = ">=3.5" -version = "5.4.2" +version = "5.4.3" [package.dependencies] atomicwrites = ">=1.0" @@ -1049,15 +1055,15 @@ category = "dev" description = "Pytest plugin for measuring coverage." name = "pytest-cov" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.8.1" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.9.0" [package.dependencies] coverage = ">=4.4" pytest = ">=3.6" [package.extras] -testing = ["fields", "hunter", "process-tests (2.0.2)", "six", "virtualenv"] +testing = ["fields", "hunter", "process-tests (2.0.2)", "six", "pytest-xdist", "virtualenv"] [[package]] category = "dev" @@ -1109,10 +1115,11 @@ version = "5.3.1" [[package]] category = "dev" description = "Alternative regular expression module, to replace re." +marker = "python_version > \"2.7\"" name = "regex" optional = false python-versions = "*" -version = "2020.5.7" +version = "2020.6.8" [[package]] category = "main" @@ -1138,7 +1145,7 @@ description = "A tool for converting between pip-style and pipfile requirements. name = "requirementslib" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -version = "1.5.7" +version = "1.5.11" [package.dependencies] appdirs = "*" @@ -1148,7 +1155,7 @@ distlib = ">=0.2.8" orderedmultidict = "*" packaging = ">=19.0" pep517 = ">=0.5.0" -pip-shims = ">=0.3.2" +pip-shims = ">=0.5.2" python-dateutil = "*" requests = "*" setuptools = ">=40.8" @@ -1186,7 +1193,7 @@ description = "Python 2 and 3 compatibility utilities" name = "six" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -version = "1.14.0" +version = "1.15.0" [[package]] category = "dev" @@ -1221,19 +1228,18 @@ description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" name = "sortedcontainers" optional = false python-versions = "*" -version = "2.1.0" +version = "2.2.2" [[package]] category = "dev" description = "Manage dynamic plugins for Python applications" name = "stevedore" optional = false -python-versions = "*" -version = "1.32.0" +python-versions = ">=3.6" +version = "2.0.0" [package.dependencies] pbr = ">=2.0.0,<2.1.0 || >2.1.0" -six = ">=1.10.0" [[package]] category = "dev" @@ -1249,7 +1255,7 @@ description = "Python Library for Tom's Obvious, Minimal Language" name = "toml" optional = false python-versions = "*" -version = "0.10.0" +version = "0.10.1" [[package]] category = "main" @@ -1270,10 +1276,11 @@ version = "6.0.4" [[package]] category = "dev" description = "Fast, Extensible Progress Meter" +marker = "python_version > \"2.7\"" name = "tqdm" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*" -version = "4.46.0" +version = "4.46.1" [package.extras] dev = ["py-make (>=0.1.0)", "twine", "argopt", "pydoc-markdown"] @@ -1329,17 +1336,17 @@ description = "Miscellaneous utilities for dealing with filesystems, paths, proj name = "vistir" optional = false python-versions = "!=3.0,!=3.1,!=3.2,!=3.3,>=2.7" -version = "0.5.0" +version = "0.5.2" [package.dependencies] colorama = ">=0.3.4,<0.4.2 || >0.4.2" six = "*" [package.extras] -dev = ["pre-commit", "coverage (<5.0)", "isort", "flake8", "rope", "invoke", "parver", "sphinx", "sphinx-rtd-theme", "flake8-bugbear", "black"] +dev = ["pre-commit", "coverage", "isort", "flake8", "rope", "invoke", "parver", "sphinx", "sphinx-rtd-theme", "flake8-bugbear", "black"] requests = ["requests"] spinner = ["yaspin"] -tests = ["hypothesis", "hypothesis-fspaths", "pytest", "pytest-xdist", "pytest-cov", "pytest-timeout", "readme-renderer", "twine", "mock"] +tests = ["hypothesis", "hypothesis-fspaths", "pytest", "pytest-rerunfailures (<9.0)", "pytest-xdist", "pytest-timeout", "readme-renderer", "twine", "mock"] typing = ["typing", "mypy", "mypy-extensions", "mypytools", "pytype", "typed-ast"] [[package]] @@ -1348,15 +1355,15 @@ description = "Find dead code" name = "vulture" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "1.4" +version = "1.5" [[package]] category = "dev" -description = "Measures number of Terminal column cells of wide-character codes" +description = "Measures the displayed width of unicode strings in a terminal" name = "wcwidth" optional = false python-versions = "*" -version = "0.1.9" +version = "0.2.4" [[package]] category = "main" @@ -1456,8 +1463,8 @@ cerberus = [ {file = "Cerberus-1.3.2.tar.gz", hash = "sha256:302e6694f206dd85cb63f13fd5025b31ab6d38c99c50c6d769f8fa0b0f299589"}, ] certifi = [ - {file = "certifi-2020.4.5.1-py2.py3-none-any.whl", hash = "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304"}, - {file = "certifi-2020.4.5.1.tar.gz", hash = "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519"}, + {file = "certifi-2020.4.5.2-py2.py3-none-any.whl", hash = "sha256:9cd41137dc19af6a5e03b630eefe7d1f458d964d406342dd3edf625839b944cc"}, + {file = "certifi-2020.4.5.2.tar.gz", hash = "sha256:5ad7e9a056d25ffa5082862e36f119f7f7cec6457fa07ee2f8c339814b80c9b1"}, ] chardet = [ {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, @@ -1551,8 +1558,8 @@ falcon = [ {file = "falcon-2.0.0.tar.gz", hash = "sha256:eea593cf466b9c126ce667f6d30503624ef24459f118c75594a69353b6c3d5fc"}, ] flake8 = [ - {file = "flake8-3.8.0-py2.py3-none-any.whl", hash = "sha256:bcf5163890bb01f11f04f0f444f01004d0f9ad5fab10c51104f770acf532008f"}, - {file = "flake8-3.8.0.tar.gz", hash = "sha256:e2f33066fb92ac0a3a30ea509f61e325f2110b2e84644333a3ff8e9e98a2beab"}, + {file = "flake8-3.8.3-py2.py3-none-any.whl", hash = "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c"}, + {file = "flake8-3.8.3.tar.gz", hash = "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208"}, ] flake8-bugbear = [ {file = "flake8-bugbear-19.8.0.tar.gz", hash = "sha256:d8c466ea79d5020cb20bf9f11cf349026e09517a42264f313d3f6fddb83e0571"}, @@ -1574,16 +1581,16 @@ gitdb2 = [ {file = "gitdb2-4.0.2.tar.gz", hash = "sha256:0986cb4003de743f2b3aba4c828edd1ab58ce98e1c4a8acf72ef02760d4beb4e"}, ] gitpython = [ - {file = "GitPython-3.1.2-py3-none-any.whl", hash = "sha256:da3b2cf819974789da34f95ac218ef99f515a928685db141327c09b73dd69c09"}, - {file = "GitPython-3.1.2.tar.gz", hash = "sha256:864a47472548f3ba716ca202e034c1900f197c0fb3a08f641c20c3cafd15ed94"}, + {file = "GitPython-3.1.3-py3-none-any.whl", hash = "sha256:ef1d60b01b5ce0040ad3ec20bc64f783362d41fa0822a2742d3586e1f49bb8ac"}, + {file = "GitPython-3.1.3.tar.gz", hash = "sha256:e107af4d873daed64648b4f4beb89f89f0cfbe3ef558fc7821ed2331c2f8da1a"}, ] hug = [ {file = "hug-2.6.1-py2.py3-none-any.whl", hash = "sha256:31c8fc284f81377278629a4b94cbb619ae9ce829cdc2da9564ccc66a121046b4"}, {file = "hug-2.6.1.tar.gz", hash = "sha256:b0edace2acb618873779c9ce6ecf9165db54fef95c22262f5700fcdd9febaec9"}, ] hypothesis = [ - {file = "hypothesis-5.12.1-py3-none-any.whl", hash = "sha256:41a4548876cd24d45ee917809dfd2dc36fd26ffe62330b9c8f837f1be0c92737"}, - {file = "hypothesis-5.12.1.tar.gz", hash = "sha256:b7ddd5f0920eec97c534c3dbce9f96055fc7332107b73b7c75ef07335a47329d"}, + {file = "hypothesis-5.16.0-py3-none-any.whl", hash = "sha256:21bb5fbe456f775233fe20bcbeb26f648d68025bce554c94c0698fb4c33e7008"}, + {file = "hypothesis-5.16.0.tar.gz", hash = "sha256:7c819501a26ff82ccb4bee8a96b1ff4fff187e041d79ed176964ca550b77098b"}, ] hypothesis-auto = [ {file = "hypothesis-auto-1.1.4.tar.gz", hash = "sha256:5e2c2fb09dc09842512d80630bb792359a1d33d2c0473ad47ee23da0be9e32b1"}, @@ -1594,12 +1601,12 @@ idna = [ {file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"}, ] importlib-metadata = [ - {file = "importlib_metadata-1.6.0-py2.py3-none-any.whl", hash = "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f"}, - {file = "importlib_metadata-1.6.0.tar.gz", hash = "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e"}, + {file = "importlib_metadata-1.6.1-py2.py3-none-any.whl", hash = "sha256:15ec6c0fd909e893e3a08b3a7c76ecb149122fb14b7efe1199ddd4c7c57ea958"}, + {file = "importlib_metadata-1.6.1.tar.gz", hash = "sha256:0505dd08068cfec00f53a74a0ad927676d7757da81b7436a6eefe4c7cf75c545"}, ] ipython = [ - {file = "ipython-7.14.0-py3-none-any.whl", hash = "sha256:5b241b84bbf0eb085d43ae9d46adf38a13b45929ca7774a740990c2c242534bb"}, - {file = "ipython-7.14.0.tar.gz", hash = "sha256:f0126781d0f959da852fb3089e170ed807388e986a8dd4e6ac44855845b0fb1c"}, + {file = "ipython-7.15.0-py3-none-any.whl", hash = "sha256:1b85d65632211bf5d3e6f1406f3393c8c429a47d7b947b9a87812aa5bce6595c"}, + {file = "ipython-7.15.0.tar.gz", hash = "sha256:0ef1433879816a960cd3ae1ae1dc82c64732ca75cec8dab5a4e29783fb571d0e"}, ] ipython-genutils = [ {file = "ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8"}, @@ -1618,20 +1625,19 @@ jinja2-time = [ {file = "jinja2_time-0.2.0-py2.py3-none-any.whl", hash = "sha256:d3eab6605e3ec8b7a0863df09cc1d23714908fa61aa6986a845c20ba488b4efa"}, ] joblib = [ - {file = "joblib-0.14.1-py2.py3-none-any.whl", hash = "sha256:bdb4fd9b72915ffb49fde2229ce482dd7ae79d842ed8c2b4c932441495af1403"}, - {file = "joblib-0.14.1.tar.gz", hash = "sha256:0630eea4f5664c463f23fbf5dcfc54a2bc6168902719fa8e19daf033022786c8"}, + {file = "joblib-0.15.1-py3-none-any.whl", hash = "sha256:6825784ffda353cc8a1be573118085789e5b5d29401856b35b756645ab5aecb5"}, + {file = "joblib-0.15.1.tar.gz", hash = "sha256:61e49189c84b3c5d99a969d314853f4d1d263316cc694bec17548ebaa9c47b6e"}, ] livereload = [ - {file = "livereload-2.6.1-py2.py3-none-any.whl", hash = "sha256:78d55f2c268a8823ba499305dcac64e28ddeb9a92571e12d543cd304faf5817b"}, - {file = "livereload-2.6.1.tar.gz", hash = "sha256:89254f78d7529d7ea0a3417d224c34287ebfe266b05e67e51facaf82c27f0f66"}, + {file = "livereload-2.6.2.tar.gz", hash = "sha256:d1eddcb5c5eb8d2ca1fa1f750e580da624c0f7fcb734aa5780dc81b7dcbd89be"}, ] lunr = [ - {file = "lunr-0.5.6-py2.py3-none-any.whl", hash = "sha256:1208622930c915a07e6f8e8640474357826bad48534c0f57969b6fca9bffc88e"}, - {file = "lunr-0.5.6.tar.gz", hash = "sha256:7be69d7186f65784a4f2adf81e5c58efd6a9921aa95966babcb1f2f2ada75c20"}, + {file = "lunr-0.5.8-py2.py3-none-any.whl", hash = "sha256:aab3f489c4d4fab4c1294a257a30fec397db56f0a50273218ccc3efdbf01d6ca"}, + {file = "lunr-0.5.8.tar.gz", hash = "sha256:c4fb063b98eff775dd638b3df380008ae85e6cb1d1a24d1cd81a10ef6391c26e"}, ] mako = [ - {file = "Mako-1.1.2-py2.py3-none-any.whl", hash = "sha256:8e8b53c71c7e59f3de716b6832c4e401d903af574f6962edbbbf6ecc2a5fe6c9"}, - {file = "Mako-1.1.2.tar.gz", hash = "sha256:3139c5d64aa5d175dbafb95027057128b5fbd05a40c53999f3905ceb53366d9d"}, + {file = "Mako-1.1.3-py2.py3-none-any.whl", hash = "sha256:93729a258e4ff0747c876bd9e20df1b9758028946e976324ccd2d68245c7b6a9"}, + {file = "Mako-1.1.3.tar.gz", hash = "sha256:8195c8c1400ceb53496064314c6736719c6f25e7479cd24c77be3d9361cddc27"}, ] markdown = [ {file = "Markdown-3.2.2-py3-none-any.whl", hash = "sha256:c467cd6233885534bf0fe96e62e3cf46cfc1605112356c4f9981512b8174de59"}, @@ -1677,16 +1683,16 @@ mccabe = [ {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] mkdocs = [ - {file = "mkdocs-1.1-py2.py3-none-any.whl", hash = "sha256:1e385a70aea8a9dedb731aea4fd5f3704b2074801c4f96f06b2920999babda8a"}, - {file = "mkdocs-1.1.tar.gz", hash = "sha256:9243291392f59e20b655e4e46210233453faf97787c2cf72176510e868143174"}, + {file = "mkdocs-1.1.2-py3-none-any.whl", hash = "sha256:096f52ff52c02c7e90332d2e53da862fde5c062086e1b5356a6e392d5d60f5e9"}, + {file = "mkdocs-1.1.2.tar.gz", hash = "sha256:f0b61e5402b99d7789efa032c7a74c90a20220a9c81749da06dbfbcbd52ffb39"}, ] mkdocs-material = [ {file = "mkdocs-material-4.6.3.tar.gz", hash = "sha256:1d486635b03f5a2ec87325842f7b10c7ae7daa0eef76b185572eece6a6ea212c"}, {file = "mkdocs_material-4.6.3-py2.py3-none-any.whl", hash = "sha256:7f3afa0a09c07d0b89a6a9755fdb00513aee8f0cec3538bb903325c80f66f444"}, ] more-itertools = [ - {file = "more-itertools-8.2.0.tar.gz", hash = "sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507"}, - {file = "more_itertools-8.2.0-py3-none-any.whl", hash = "sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c"}, + {file = "more-itertools-8.3.0.tar.gz", hash = "sha256:558bb897a2232f5e4f8e2399089e35aecb746e1f9191b6584a151647e89267be"}, + {file = "more_itertools-8.3.0-py3-none-any.whl", hash = "sha256:7818f596b1e87be009031c7653d01acc46ed422e6656b394b0f765ce66ed4982"}, ] mypy = [ {file = "mypy-0.761-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:7f672d02fffcbace4db2b05369142e0506cdcde20cea0e07c7c2171c4fd11dd6"}, @@ -1712,35 +1718,35 @@ nltk = [ {file = "nltk-3.5.zip", hash = "sha256:845365449cd8c5f9731f7cb9f8bd6fd0767553b9d53af9eb1b3abf7700936b35"}, ] numpy = [ - {file = "numpy-1.18.4-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:efdba339fffb0e80fcc19524e4fdbda2e2b5772ea46720c44eaac28096d60720"}, - {file = "numpy-1.18.4-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2b573fcf6f9863ce746e4ad00ac18a948978bb3781cffa4305134d31801f3e26"}, - {file = "numpy-1.18.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:3f0dae97e1126f529ebb66f3c63514a0f72a177b90d56e4bce8a0b5def34627a"}, - {file = "numpy-1.18.4-cp35-cp35m-win32.whl", hash = "sha256:dccd380d8e025c867ddcb2f84b439722cf1f23f3a319381eac45fd077dee7170"}, - {file = "numpy-1.18.4-cp35-cp35m-win_amd64.whl", hash = "sha256:02ec9582808c4e48be4e93cd629c855e644882faf704bc2bd6bbf58c08a2a897"}, - {file = "numpy-1.18.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:904b513ab8fbcbdb062bed1ce2f794ab20208a1b01ce9bd90776c6c7e7257032"}, - {file = "numpy-1.18.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:e22cd0f72fc931d6abc69dc7764484ee20c6a60b0d0fee9ce0426029b1c1bdae"}, - {file = "numpy-1.18.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2466fbcf23711ebc5daa61d28ced319a6159b260a18839993d871096d66b93f7"}, - {file = "numpy-1.18.4-cp36-cp36m-win32.whl", hash = "sha256:00d7b54c025601e28f468953d065b9b121ddca7fff30bed7be082d3656dd798d"}, - {file = "numpy-1.18.4-cp36-cp36m-win_amd64.whl", hash = "sha256:7d59f21e43bbfd9a10953a7e26b35b6849d888fc5a331fa84a2d9c37bd9fe2a2"}, - {file = "numpy-1.18.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:efb7ac5572c9a57159cf92c508aad9f856f1cb8e8302d7fdb99061dbe52d712c"}, - {file = "numpy-1.18.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0e6f72f7bb08f2f350ed4408bb7acdc0daba637e73bce9f5ea2b207039f3af88"}, - {file = "numpy-1.18.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:9933b81fecbe935e6a7dc89cbd2b99fea1bf362f2790daf9422a7bb1dc3c3085"}, - {file = "numpy-1.18.4-cp37-cp37m-win32.whl", hash = "sha256:96dd36f5cdde152fd6977d1bbc0f0561bccffecfde63cd397c8e6033eb66baba"}, - {file = "numpy-1.18.4-cp37-cp37m-win_amd64.whl", hash = "sha256:57aea170fb23b1fd54fa537359d90d383d9bf5937ee54ae8045a723caa5e0961"}, - {file = "numpy-1.18.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ed722aefb0ebffd10b32e67f48e8ac4c5c4cf5d3a785024fdf0e9eb17529cd9d"}, - {file = "numpy-1.18.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:50fb72bcbc2cf11e066579cb53c4ca8ac0227abb512b6cbc1faa02d1595a2a5d"}, - {file = "numpy-1.18.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:709c2999b6bd36cdaf85cf888d8512da7433529f14a3689d6e37ab5242e7add5"}, - {file = "numpy-1.18.4-cp38-cp38-win32.whl", hash = "sha256:f22273dd6a403ed870207b853a856ff6327d5cbce7a835dfa0645b3fc00273ec"}, - {file = "numpy-1.18.4-cp38-cp38-win_amd64.whl", hash = "sha256:1be2e96314a66f5f1ce7764274327fd4fb9da58584eaff00b5a5221edefee7d6"}, - {file = "numpy-1.18.4.zip", hash = "sha256:bbcc85aaf4cd84ba057decaead058f43191cc0e30d6bc5d44fe336dc3d3f4509"}, + {file = "numpy-1.18.5-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:e91d31b34fc7c2c8f756b4e902f901f856ae53a93399368d9a0dc7be17ed2ca0"}, + {file = "numpy-1.18.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7d42ab8cedd175b5ebcb39b5208b25ba104842489ed59fbb29356f671ac93583"}, + {file = "numpy-1.18.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:a78e438db8ec26d5d9d0e584b27ef25c7afa5a182d1bf4d05e313d2d6d515271"}, + {file = "numpy-1.18.5-cp35-cp35m-win32.whl", hash = "sha256:a87f59508c2b7ceb8631c20630118cc546f1f815e034193dc72390db038a5cb3"}, + {file = "numpy-1.18.5-cp35-cp35m-win_amd64.whl", hash = "sha256:965df25449305092b23d5145b9bdaeb0149b6e41a77a7d728b1644b3c99277c1"}, + {file = "numpy-1.18.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ac792b385d81151bae2a5a8adb2b88261ceb4976dbfaaad9ce3a200e036753dc"}, + {file = "numpy-1.18.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:ef627986941b5edd1ed74ba89ca43196ed197f1a206a3f18cc9faf2fb84fd675"}, + {file = "numpy-1.18.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:f718a7949d1c4f622ff548c572e0c03440b49b9531ff00e4ed5738b459f011e8"}, + {file = "numpy-1.18.5-cp36-cp36m-win32.whl", hash = "sha256:4064f53d4cce69e9ac613256dc2162e56f20a4e2d2086b1956dd2fcf77b7fac5"}, + {file = "numpy-1.18.5-cp36-cp36m-win_amd64.whl", hash = "sha256:b03b2c0badeb606d1232e5f78852c102c0a7989d3a534b3129e7856a52f3d161"}, + {file = "numpy-1.18.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a7acefddf994af1aeba05bbbafe4ba983a187079f125146dc5859e6d817df824"}, + {file = "numpy-1.18.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:cd49930af1d1e49a812d987c2620ee63965b619257bd76eaaa95870ca08837cf"}, + {file = "numpy-1.18.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:b39321f1a74d1f9183bf1638a745b4fd6fe80efbb1f6b32b932a588b4bc7695f"}, + {file = "numpy-1.18.5-cp37-cp37m-win32.whl", hash = "sha256:cae14a01a159b1ed91a324722d746523ec757357260c6804d11d6147a9e53e3f"}, + {file = "numpy-1.18.5-cp37-cp37m-win_amd64.whl", hash = "sha256:0172304e7d8d40e9e49553901903dc5f5a49a703363ed756796f5808a06fc233"}, + {file = "numpy-1.18.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e15b382603c58f24265c9c931c9a45eebf44fe2e6b4eaedbb0d025ab3255228b"}, + {file = "numpy-1.18.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:3676abe3d621fc467c4c1469ee11e395c82b2d6b5463a9454e37fe9da07cd0d7"}, + {file = "numpy-1.18.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:4674f7d27a6c1c52a4d1aa5f0881f1eff840d2206989bae6acb1c7668c02ebfb"}, + {file = "numpy-1.18.5-cp38-cp38-win32.whl", hash = "sha256:9c9d6531bc1886454f44aa8f809268bc481295cf9740827254f53c30104f074a"}, + {file = "numpy-1.18.5-cp38-cp38-win_amd64.whl", hash = "sha256:3dd6823d3e04b5f223e3e265b4a1eae15f104f4366edd409e5a5e413a98f911f"}, + {file = "numpy-1.18.5.zip", hash = "sha256:34e96e9dae65c4839bd80012023aadd6ee2ccb73ce7fdf3074c62f301e63120b"}, ] orderedmultidict = [ {file = "orderedmultidict-1.0.1-py2.py3-none-any.whl", hash = "sha256:43c839a17ee3cdd62234c47deca1a8508a3f2ca1d0678a3bf791c87cf84adbf3"}, {file = "orderedmultidict-1.0.1.tar.gz", hash = "sha256:04070bbb5e87291cc9bfa51df413677faf2141c73c61d2a5f7b26bea3cd882ad"}, ] packaging = [ - {file = "packaging-20.3-py2.py3-none-any.whl", hash = "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752"}, - {file = "packaging-20.3.tar.gz", hash = "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3"}, + {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"}, + {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, ] parso = [ {file = "parso-0.7.0-py2.py3-none-any.whl", hash = "sha256:158c140fc04112dc45bca311633ae5033c2c2a7b732fa33d0955bad8152a8dd0"}, @@ -1751,8 +1757,8 @@ pbr = [ {file = "pbr-5.4.5.tar.gz", hash = "sha256:07f558fece33b05caf857474a366dfcc00562bca13dd8b47b2b3e22d9f9bf55c"}, ] pdocs = [ - {file = "pdocs-1.0.1-py3-none-any.whl", hash = "sha256:9c0d24fdc0e0c537be8f2418edb4f1075da46a0d749b17ea20b74e7e60124f49"}, - {file = "pdocs-1.0.1.tar.gz", hash = "sha256:23a0346f56c08ab5701ca9b14630aa1f0f32f1c29ee7efcfc8bc512cd272f89b"}, + {file = "pdocs-1.0.2-py3-none-any.whl", hash = "sha256:4d5ff87babcd0c46f12b76c887d53225bddb389dee7c6b338dbe281c729fc035"}, + {file = "pdocs-1.0.2.tar.gz", hash = "sha256:2e32432bd2736fd678ac1ce4447cd508deb62b5a12f7ba3bf0e3a374063221e2"}, ] pep517 = [ {file = "pep517-0.8.2-py2.py3-none-any.whl", hash = "sha256:576c480be81f3e1a70a16182c762311eb80d1f8a7b0d11971e5234967d7a342c"}, @@ -1794,8 +1800,8 @@ pluggy = [ {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, ] portray = [ - {file = "portray-1.3.1-py3-none-any.whl", hash = "sha256:ac1a651f97ab04556732b64fdb10dcba4f9a006dd023471039743b16d28a7a61"}, - {file = "portray-1.3.1.tar.gz", hash = "sha256:28e0b21ad611cd460369c89cdfe67054e20354c6c5c03950c250edab6d7a99d7"}, + {file = "portray-1.3.2-py3-none-any.whl", hash = "sha256:5f108eb4c6efc74901a138a1fe44384e18e5fdc4c8de6deda64b416ba015e52c"}, + {file = "portray-1.3.2.tar.gz", hash = "sha256:4c2f3ef64278877cbcfc4756d9a70edb0e4fc7875e0000d0ad9f1412d213eb49"}, ] poyo = [ {file = "poyo-0.5.0-py2.py3-none-any.whl", hash = "sha256:3e2ca8e33fdc3c411cd101ca395668395dd5dc7ac775b8e809e3def9f9fe041a"}, @@ -1861,12 +1867,12 @@ pyparsing = [ {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, ] pytest = [ - {file = "pytest-5.4.2-py3-none-any.whl", hash = "sha256:95c710d0a72d91c13fae35dce195633c929c3792f54125919847fdcdf7caa0d3"}, - {file = "pytest-5.4.2.tar.gz", hash = "sha256:eb2b5e935f6a019317e455b6da83dd8650ac9ffd2ee73a7b657a30873d67a698"}, + {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"}, + {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, ] pytest-cov = [ - {file = "pytest-cov-2.8.1.tar.gz", hash = "sha256:cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b"}, - {file = "pytest_cov-2.8.1-py2.py3-none-any.whl", hash = "sha256:cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626"}, + {file = "pytest-cov-2.9.0.tar.gz", hash = "sha256:b6a814b8ed6247bd81ff47f038511b57fe1ce7f4cc25b9106f1a4b106f1d9322"}, + {file = "pytest_cov-2.9.0-py2.py3-none-any.whl", hash = "sha256:c87dfd8465d865655a8213859f1b4749b43448b5fae465cb981e16d52a811424"}, ] pytest-mock = [ {file = "pytest-mock-1.13.0.tar.gz", hash = "sha256:e24a911ec96773022ebcc7030059b57cd3480b56d4f5d19b7c370ec635e6aed5"}, @@ -1893,43 +1899,43 @@ pyyaml = [ {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, ] regex = [ - {file = "regex-2020.5.7-cp27-cp27m-win32.whl", hash = "sha256:5493a02c1882d2acaaf17be81a3b65408ff541c922bfd002535c5f148aa29f74"}, - {file = "regex-2020.5.7-cp27-cp27m-win_amd64.whl", hash = "sha256:021a0ae4d2baeeb60a3014805a2096cb329bd6d9f30669b7ad0da51a9cb73349"}, - {file = "regex-2020.5.7-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:4df91094ced6f53e71f695c909d9bad1cca8761d96fd9f23db12245b5521136e"}, - {file = "regex-2020.5.7-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:7ce4a213a96d6c25eeae2f7d60d4dad89ac2b8134ec3e69db9bc522e2c0f9388"}, - {file = "regex-2020.5.7-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:3b059e2476b327b9794c792c855aa05531a3f3044737e455d283c7539bd7534d"}, - {file = "regex-2020.5.7-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:652ab4836cd5531d64a34403c00ada4077bb91112e8bcdae933e2eae232cf4a8"}, - {file = "regex-2020.5.7-cp36-cp36m-win32.whl", hash = "sha256:1e2255ae938a36e9bd7db3b93618796d90c07e5f64dd6a6750c55f51f8b76918"}, - {file = "regex-2020.5.7-cp36-cp36m-win_amd64.whl", hash = "sha256:8127ca2bf9539d6a64d03686fd9e789e8c194fc19af49b69b081f8c7e6ecb1bc"}, - {file = "regex-2020.5.7-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f7f2f4226db6acd1da228adf433c5c3792858474e49d80668ea82ac87cf74a03"}, - {file = "regex-2020.5.7-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2bc6a17a7fa8afd33c02d51b6f417fc271538990297167f68a98cae1c9e5c945"}, - {file = "regex-2020.5.7-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:b7c9f65524ff06bf70c945cd8d8d1fd90853e27ccf86026af2afb4d9a63d06b1"}, - {file = "regex-2020.5.7-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:fa09da4af4e5b15c0e8b4986a083f3fd159302ea115a6cc0649cd163435538b8"}, - {file = "regex-2020.5.7-cp37-cp37m-win32.whl", hash = "sha256:669a8d46764a09f198f2e91fc0d5acdac8e6b620376757a04682846ae28879c4"}, - {file = "regex-2020.5.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b5b5b2e95f761a88d4c93691716ce01dc55f288a153face1654f868a8034f494"}, - {file = "regex-2020.5.7-cp38-cp38-manylinux1_i686.whl", hash = "sha256:0ff50843535593ee93acab662663cb2f52af8e31c3f525f630f1dc6156247938"}, - {file = "regex-2020.5.7-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:1b17bf37c2aefc4cac8436971fe6ee52542ae4225cfc7762017f7e97a63ca998"}, - {file = "regex-2020.5.7-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:04d6e948ef34d3eac133bedc0098364a9e635a7914f050edb61272d2ddae3608"}, - {file = "regex-2020.5.7-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:5b741ecc3ad3e463d2ba32dce512b412c319993c1bb3d999be49e6092a769fb2"}, - {file = "regex-2020.5.7-cp38-cp38-win32.whl", hash = "sha256:099568b372bda492be09c4f291b398475587d49937c659824f891182df728cdf"}, - {file = "regex-2020.5.7-cp38-cp38-win_amd64.whl", hash = "sha256:3ab5e41c4ed7cd4fa426c50add2892eb0f04ae4e73162155cd668257d02259dd"}, - {file = "regex-2020.5.7.tar.gz", hash = "sha256:73a10404867b835f1b8a64253e4621908f0d71150eb4e97ab2e7e441b53e9451"}, + {file = "regex-2020.6.8-cp27-cp27m-win32.whl", hash = "sha256:fbff901c54c22425a5b809b914a3bfaf4b9570eee0e5ce8186ac71eb2025191c"}, + {file = "regex-2020.6.8-cp27-cp27m-win_amd64.whl", hash = "sha256:112e34adf95e45158c597feea65d06a8124898bdeac975c9087fe71b572bd938"}, + {file = "regex-2020.6.8-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:92d8a043a4241a710c1cf7593f5577fbb832cf6c3a00ff3fc1ff2052aff5dd89"}, + {file = "regex-2020.6.8-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:bae83f2a56ab30d5353b47f9b2a33e4aac4de9401fb582b55c42b132a8ac3868"}, + {file = "regex-2020.6.8-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:b2ba0f78b3ef375114856cbdaa30559914d081c416b431f2437f83ce4f8b7f2f"}, + {file = "regex-2020.6.8-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:95fa7726d073c87141f7bbfb04c284901f8328e2d430eeb71b8ffdd5742a5ded"}, + {file = "regex-2020.6.8-cp36-cp36m-win32.whl", hash = "sha256:e3cdc9423808f7e1bb9c2e0bdb1c9dc37b0607b30d646ff6faf0d4e41ee8fee3"}, + {file = "regex-2020.6.8-cp36-cp36m-win_amd64.whl", hash = "sha256:c78e66a922de1c95a208e4ec02e2e5cf0bb83a36ceececc10a72841e53fbf2bd"}, + {file = "regex-2020.6.8-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:08997a37b221a3e27d68ffb601e45abfb0093d39ee770e4257bd2f5115e8cb0a"}, + {file = "regex-2020.6.8-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2f6f211633ee8d3f7706953e9d3edc7ce63a1d6aad0be5dcee1ece127eea13ae"}, + {file = "regex-2020.6.8-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:55b4c25cbb3b29f8d5e63aeed27b49fa0f8476b0d4e1b3171d85db891938cc3a"}, + {file = "regex-2020.6.8-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:89cda1a5d3e33ec9e231ece7307afc101b5217523d55ef4dc7fb2abd6de71ba3"}, + {file = "regex-2020.6.8-cp37-cp37m-win32.whl", hash = "sha256:690f858d9a94d903cf5cada62ce069b5d93b313d7d05456dbcd99420856562d9"}, + {file = "regex-2020.6.8-cp37-cp37m-win_amd64.whl", hash = "sha256:1700419d8a18c26ff396b3b06ace315b5f2a6e780dad387e4c48717a12a22c29"}, + {file = "regex-2020.6.8-cp38-cp38-manylinux1_i686.whl", hash = "sha256:654cb773b2792e50151f0e22be0f2b6e1c3a04c5328ff1d9d59c0398d37ef610"}, + {file = "regex-2020.6.8-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:52e1b4bef02f4040b2fd547357a170fc1146e60ab310cdbdd098db86e929b387"}, + {file = "regex-2020.6.8-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:cf59bbf282b627130f5ba68b7fa3abdb96372b24b66bdf72a4920e8153fc7910"}, + {file = "regex-2020.6.8-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:5aaa5928b039ae440d775acea11d01e42ff26e1561c0ffcd3d805750973c6baf"}, + {file = "regex-2020.6.8-cp38-cp38-win32.whl", hash = "sha256:97712e0d0af05febd8ab63d2ef0ab2d0cd9deddf4476f7aa153f76feef4b2754"}, + {file = "regex-2020.6.8-cp38-cp38-win_amd64.whl", hash = "sha256:6ad8663c17db4c5ef438141f99e291c4d4edfeaacc0ce28b5bba2b0bf273d9b5"}, + {file = "regex-2020.6.8.tar.gz", hash = "sha256:e9b64e609d37438f7d6e68c2546d2cb8062f3adb27e6336bc129b51be20773ac"}, ] requests = [ {file = "requests-2.23.0-py2.py3-none-any.whl", hash = "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee"}, {file = "requests-2.23.0.tar.gz", hash = "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"}, ] requirementslib = [ - {file = "requirementslib-1.5.7-py2.py3-none-any.whl", hash = "sha256:b9989e4815ada8ed71f5d4059e4e6be6f864fb57de744c04ac3d0c744df52304"}, - {file = "requirementslib-1.5.7.tar.gz", hash = "sha256:4999223a26504e0a3cedf9b58def69eae3a93d39db945a85e2135e0239e28fa8"}, + {file = "requirementslib-1.5.11-py2.py3-none-any.whl", hash = "sha256:3dea8f577be1f7f5aafb66007cb49ef659f9249649aa11312a903fa4205a7350"}, + {file = "requirementslib-1.5.11.tar.gz", hash = "sha256:6acae5ba27c9a1a45d120d74e0b922b202e0c614591880ae060d06c1e77eff66"}, ] safety = [ {file = "safety-1.9.0-py2.py3-none-any.whl", hash = "sha256:86c1c4a031fe35bd624fce143fbe642a0234d29f7cbf7a9aa269f244a955b087"}, {file = "safety-1.9.0.tar.gz", hash = "sha256:23bf20690d4400edc795836b0c983c2b4cbbb922233108ff925b7dd7750f00c9"}, ] six = [ - {file = "six-1.14.0-py2.py3-none-any.whl", hash = "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"}, - {file = "six-1.14.0.tar.gz", hash = "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a"}, + {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, + {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, ] smmap = [ {file = "smmap-3.0.4-py2.py3-none-any.whl", hash = "sha256:54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4"}, @@ -1944,21 +1950,20 @@ snowballstemmer = [ {file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"}, ] sortedcontainers = [ - {file = "sortedcontainers-2.1.0-py2.py3-none-any.whl", hash = "sha256:d9e96492dd51fae31e60837736b38fe42a187b5404c16606ff7ee7cd582d4c60"}, - {file = "sortedcontainers-2.1.0.tar.gz", hash = "sha256:974e9a32f56b17c1bac2aebd9dcf197f3eb9cd30553c5852a3187ad162e1a03a"}, + {file = "sortedcontainers-2.2.2-py2.py3-none-any.whl", hash = "sha256:c633ebde8580f241f274c1f8994a665c0e54a17724fecd0cae2f079e09c36d3f"}, + {file = "sortedcontainers-2.2.2.tar.gz", hash = "sha256:4e73a757831fc3ca4de2859c422564239a31d8213d09a2a666e375807034d2ba"}, ] stevedore = [ - {file = "stevedore-1.32.0-py2.py3-none-any.whl", hash = "sha256:a4e7dc759fb0f2e3e2f7d8ffe2358c19d45b9b8297f393ef1256858d82f69c9b"}, - {file = "stevedore-1.32.0.tar.gz", hash = "sha256:18afaf1d623af5950cc0f7e75e70f917784c73b652a34a12d90b309451b5500b"}, + {file = "stevedore-2.0.0-py3-none-any.whl", hash = "sha256:471c920412265cc809540ae6fb01f3f02aba89c79bbc7091372f4745a50f9691"}, + {file = "stevedore-2.0.0.tar.gz", hash = "sha256:001e90cd704be6470d46cc9076434e2d0d566c1379187e7013eb296d3a6032d9"}, ] text-unidecode = [ {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, ] toml = [ - {file = "toml-0.10.0-py2.7.egg", hash = "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"}, - {file = "toml-0.10.0-py2.py3-none-any.whl", hash = "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"}, - {file = "toml-0.10.0.tar.gz", hash = "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c"}, + {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"}, + {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"}, ] tomlkit = [ {file = "tomlkit-0.6.0-py2.py3-none-any.whl", hash = "sha256:e5d5f20809c2b09276a6c5d98fb0202325aee441a651db84ac12e0812ab7e569"}, @@ -1976,8 +1981,8 @@ tornado = [ {file = "tornado-6.0.4.tar.gz", hash = "sha256:0fe2d45ba43b00a41cd73f8be321a44936dc1aba233dee979f17a042b83eb6dc"}, ] tqdm = [ - {file = "tqdm-4.46.0-py2.py3-none-any.whl", hash = "sha256:acdafb20f51637ca3954150d0405ff1a7edde0ff19e38fb99a80a66210d2a28f"}, - {file = "tqdm-4.46.0.tar.gz", hash = "sha256:4733c4a10d0f2a4d098d801464bdaf5240c7dadd2a7fde4ee93b0a0efd9fb25e"}, + {file = "tqdm-4.46.1-py2.py3-none-any.whl", hash = "sha256:07c06493f1403c1380b630ae3dcbe5ae62abcf369a93bbc052502279f189ab8c"}, + {file = "tqdm-4.46.1.tar.gz", hash = "sha256:cd140979c2bebd2311dfb14781d8f19bd5a9debb92dcab9f6ef899c987fcf71f"}, ] traitlets = [ {file = "traitlets-4.3.3-py2.py3-none-any.whl", hash = "sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44"}, @@ -2016,16 +2021,16 @@ urllib3 = [ {file = "urllib3-1.25.9.tar.gz", hash = "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527"}, ] vistir = [ - {file = "vistir-0.5.0-py2.py3-none-any.whl", hash = "sha256:e47afdec8baf35032a8d17116765f751ecd2f2146d47e5af457c5de1fe5a334e"}, - {file = "vistir-0.5.0.tar.gz", hash = "sha256:33f8e905d40a77276b3d5310c8b57c1479a4e46930042b4894fcf7ed60ad76c4"}, + {file = "vistir-0.5.2-py2.py3-none-any.whl", hash = "sha256:a37079cdbd85d31a41cdd18457fe521e15ec08b255811e81aa061fd5f48a20fb"}, + {file = "vistir-0.5.2.tar.gz", hash = "sha256:eff1d19ef50c703a329ed294e5ec0b0fbb35b96c1b3ee6dcdb266dddbe1e935a"}, ] vulture = [ - {file = "vulture-1.4-py2.py3-none-any.whl", hash = "sha256:89f977c83043c9e01336eedc1b86346a82264d6c85498b7fab4722d914a5f171"}, - {file = "vulture-1.4.tar.gz", hash = "sha256:7ed28f87e6b08e62675946c96788f30f44da6882ad07f4af50485b65a0fac77a"}, + {file = "vulture-1.5-py2.py3-none-any.whl", hash = "sha256:7a26d9648a0366b2b15ca9edadc40dae12ee9f48519d7046bd7e84a0a8dfdeaa"}, + {file = "vulture-1.5.tar.gz", hash = "sha256:07dfab84a32867ae2636bb3998ce50a4e059556dacb0cca4dbe51a1f3cc9d6d7"}, ] wcwidth = [ - {file = "wcwidth-0.1.9-py2.py3-none-any.whl", hash = "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1"}, - {file = "wcwidth-0.1.9.tar.gz", hash = "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1"}, + {file = "wcwidth-0.2.4-py2.py3-none-any.whl", hash = "sha256:79375666b9954d4a1a10739315816324c3e73110af9d0e102d906fdb0aec009f"}, + {file = "wcwidth-0.2.4.tar.gz", hash = "sha256:8c6b5b6ee1360b842645f336d9e5d68c55817c26d3050f46b235ef2bc650e48f"}, ] wheel = [ {file = "wheel-0.34.2-py2.py3-none-any.whl", hash = "sha256:df277cb51e61359aba502208d680f90c0493adec6f0e848af94948778aed386e"}, From 4cb7c66de817661728dec21aea4182dc489cb2e7 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 8 Jun 2020 21:56:33 -0700 Subject: [PATCH 0614/1439] Update config option docs --- docs/configuration/options.md | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/docs/configuration/options.md b/docs/configuration/options.md index 1c62abf85..727ee7f54 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -32,7 +32,7 @@ Force specific imports to the top of their appropriate section. Files that sort imports should skip over. If you want to skip multiple files you should specify twice: --skip file1 --skip file2. **Type:** Frozenset -**Default:** `frozenset({'.tox', 'venv', 'build', '.pants.d', 'node_modules', '.nox', 'buck-out', '_build', 'dist', '.git', '.eggs', '.mypy_cache', '.hg', '.venv'})` +**Default:** `frozenset({'dist', '.git', 'buck-out', '.nox', '.eggs', '_build', '.venv', '.tox', 'build', 'node_modules', 'venv', '.pants.d', '.hg', '.mypy_cache'})` **Python & Config File Name:** skip **CLI Flags:** @@ -144,7 +144,7 @@ Force sortImports to recognize a module as being part of the current python proj Force sortImports to recognize a module as part of the python standard library. **Type:** Frozenset -**Default:** `frozenset({'gettext', 'calendar', 'sunau', 're', 'urllib', 'test', 'resource', 'html', 'platform', 'quopri', 'uu', 'shelve', 'zipfile', 'chunk', 'tokenize', 'turtle', 'imghdr', 'keyword', 'cmd', 'plistlib', 'struct', 'string', 'tarfile', 'ipaddress', 'encodings', 'webbrowser', 'difflib', 'copyreg', 'modulefinder', 'faulthandler', 'abc', 'code', 'netrc', 'gzip', 'binhex', 'fileinput', 'contextvars', 'smtplib', 'io', 'statistics', 'secrets', 'os', 'cgitb', 'grp', 'mmap', 'pyclbr', 'pathlib', 'heapq', 'winsound', 'cgi', 'pydoc', 'hmac', 'pkgutil', 'zlib', 'smtpd', 'distutils', 'pickle', 'codecs', 'math', 'lzma', 'unittest', 'email', 'dis', 'socket', 'tty', 'ssl', 'sysconfig', 'numbers', 'pdb', 'ossaudiodev', 'http', 'multiprocessing', 'pwd', 'shlex', 'socketserver', 'ctypes', 'mailbox', 'collections', 'sched', 'optparse', 'venv', 'builtins', 'site', 'dummy_threading', 'zipapp', 'imaplib', 'compileall', 'shutil', 'errno', 'cmath', 'argparse', 'pprint', 'telnetlib', 'enum', 'fpectl', 'poplib', 'time', 'csv', 'gc', 'select', 'codeop', 'nis', 'cProfile', 'warnings', 'asyncio', 'msilib', 'spwd', 'audioop', 'doctest', 'asyncore', 'ftplib', 'symbol', 'array', 'winreg', 'pipes', 'ensurepip', 'mailcap', 'typing', 'atexit', 'pstats', 'xdrlib', 'itertools', 'mimetypes', 'zipimport', 'aifc', 'rlcompleter', 'stringprep', 'textwrap', 'formatter', 'posix', 'tkinter', 'dbm', 'syslog', 'weakref', 'functools', 'linecache', 'datetime', 'curses', 'trace', 'pty', 'ast', 'nntplib', 'xml', 'decimal', 'turtledemo', 'profile', 'base64', 'filecmp', 'locale', 'readline', 'crypt', 'tabnanny', 'msvcrt', 'stat', 'glob', 'types', 'reprlib', 'wsgiref', 'json', 'lib2to3', 'py_compile', 'sndhdr', 'queue', 'uuid', 'threading', 'selectors', 'imp', 'asynchat', 'inspect', 'timeit', 'concurrent', 'bdb', 'getopt', 'unicodedata', 'termios', 'importlib', 'macpath', 'parser', 'wave', 'signal', 'sys', 'bisect', 'dataclasses', 'hashlib', 'fnmatch', 'pickletools', 'logging', 'sqlite3', 'tempfile', 'subprocess', 'traceback', 'tracemalloc', '_thread', 'xmlrpc', 'fractions', 'token', 'runpy', 'random', 'binascii', 'getpass', 'copy', 'configparser', 'fcntl', 'marshal', 'symtable', 'colorsys', 'operator', 'bz2', '_dummy_thread', 'contextlib'})` +**Default:** `frozenset({'distutils', 'builtins', 'pickle', 'compileall', 'tty', 'test', 'winsound', 'telnetlib', 'sysconfig', 'aifc', 'socketserver', 'calendar', 'bz2', 'sndhdr', 'codecs', 'errno', 'turtledemo', 'winreg', 'sunau', 'dbm', 'configparser', 'lib2to3', 'webbrowser', 'array', 'code', 'collections', 'quopri', 'imp', 'sys', 'textwrap', 'dummy_threading', 'inspect', 'shelve', 'itertools', 'smtpd', 'heapq', 'optparse', 'cgitb', 'wsgiref', 'mailcap', 'fileinput', 'tempfile', 'copyreg', 'pkgutil', 'json', 'importlib', 'zipapp', 'marshal', 'parser', 'select', 'ssl', 'enum', 'grp', 'crypt', 'abc', 'dataclasses', '_dummy_thread', 'email', 'getopt', 'logging', 'types', 'fcntl', 'imghdr', 'poplib', 'asyncore', 'functools', 'cgi', 'bdb', 'bisect', 'typing', 'mailbox', 're', 'smtplib', 'pathlib', 'reprlib', 'gettext', 'codeop', 'tracemalloc', 'unittest', 'termios', 'tkinter', 'colorsys', 'cProfile', 'keyword', 'queue', 'unicodedata', 'profile', 'shutil', 'imaplib', 'formatter', 'difflib', 'base64', 'io', 'getpass', 'pyclbr', 'weakref', 'numbers', 'pwd', 'nntplib', 'fractions', 'gzip', 'encodings', 'glob', 'fpectl', 'msvcrt', 'hashlib', 'html', 'http', 'xml', 'tokenize', 'signal', 'subprocess', 'xdrlib', 'shlex', 'pdb', 'plistlib', 'pty', 'threading', 'struct', 'fnmatch', 'syslog', 'tarfile', 'doctest', 'tabnanny', 'resource', 'stat', 'zlib', 'spwd', 'locale', 'macpath', 'random', 'ast', 'pipes', 'uu', 'pprint', 'statistics', 'cmd', 'warnings', 'sqlite3', 'mmap', 'posix', 'token', 'decimal', 'multiprocessing', 'contextlib', 'mimetypes', 'binhex', 'site', 'asyncio', 'modulefinder', 'runpy', 'wave', 'os', 'netrc', 'pstats', 'gc', 'venv', 'asynchat', 'platform', 'argparse', 'chunk', 'string', 'readline', 'selectors', 'ctypes', 'hmac', 'turtle', 'rlcompleter', 'filecmp', 'csv', 'msilib', 'zipimport', 'audioop', 'stringprep', 'zipfile', 'cmath', 'ipaddress', 'time', 'socket', 'math', 'lzma', 'ensurepip', 'nis', 'pickletools', 'linecache', 'trace', 'uuid', 'traceback', 'pydoc', 'symbol', 'faulthandler', '_thread', 'operator', 'timeit', 'ftplib', 'ossaudiodev', 'dis', 'concurrent', 'binascii', 'symtable', 'copy', 'sched', 'curses', 'contextvars', 'atexit', 'xmlrpc', 'datetime', 'secrets', 'py_compile', 'urllib'})` **Python & Config File Name:** known_standard_library **CLI Flags:** @@ -631,6 +631,27 @@ Base profile type to use for configuration. - --profile +## Src Paths +**No Description** + +**Type:** Frozenset +**Default:** `frozenset()` +**Python & Config File Name:** src_paths +**CLI Flags:** + + - **Not Supported** + +## Source Paths +Add an explicitly defined source path (modules within src paths have their imports automatically catorgorized as first_party). + +**Type:** String +**Default:** `None` +**Python & Config File Name:** **Not Supported** +**CLI Flags:** + + - --src + - --src-path + ## Check Checks the file for unsorted / unformatted imports and prints them to the command line without modifying the file. From ec4017a31a063e62f66af2f3751fb4ce4d9b7b6b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 9 Jun 2020 20:43:49 -0700 Subject: [PATCH 0615/1439] Ensure all provided src_paths are absolute --- isort/settings.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/isort/settings.py b/isort/settings.py index b3d129801..d4f2d55dc 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -300,7 +300,11 @@ def __init__( combined_config["import_headings"] = import_headings if "src_paths" not in combined_config: - combined_config["src_paths"] = frozenset((Path.cwd(),)) + combined_config["src_paths"] = frozenset((Path.cwd().absolute(),)) + else: + combined_config["src_paths"] = frozenset( + path.absolute() for path in combined_config["src_paths"] + ) super().__init__(sources=tuple(sources), **combined_config) # type: ignore From 28279072bce131e4a12572205f8f94e18ad5cd2b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 10 Jun 2020 20:44:15 -0700 Subject: [PATCH 0616/1439] All paths should already be absolute --- isort/finders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/finders.py b/isort/finders.py index 489739cf6..13983074e 100644 --- a/isort/finders.py +++ b/isort/finders.py @@ -187,7 +187,7 @@ def find(self, module_name: str) -> Optional[str]: elif self.conda_env and self.conda_env in prefix: return sections.THIRDPARTY for src_path in self.config.src_paths: - if str(src_path.absolute()) in os.path.abspath(package_path): + if str(src_path) in os.path.abspath(package_path): return sections.FIRSTPARTY if os.path.normcase(prefix).startswith(self.stdlib_lib_prefix): From e242c820accf52d9498e48f9b48d6209d42a1559 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 13 Jun 2020 16:05:03 -0700 Subject: [PATCH 0617/1439] Add support for automatically determining source paths --- isort/main.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/isort/main.py b/isort/main.py index e54f68f9a..533fdd46f 100644 --- a/isort/main.py +++ b/isort/main.py @@ -571,7 +571,7 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: def _preconvert(item): """Preconverts objects from native types into JSONifyiable types""" - if isinstance(item, frozenset): + if isinstance(item, frozenset) or isinstance(item, set): return list(item) elif isinstance(item, WrapModes): return item.name @@ -630,6 +630,14 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = check = config_dict.pop("check", False) show_diff = config_dict.pop("show_diff", False) write_to_stdout = config_dict.pop("write_to_stdout", False) + + src_paths = config_dict.setdefault("src_paths", set()) + for file_name in file_names: + if os.path.isdir(file_name): + src_paths.add(Path(file_name).absolute()) + else: + src_paths.add(Path(file_name).parent.absolute()) + config = Config(**config_dict) if show_config: print(json.dumps(config.__dict__, indent=4, separators=(",", ": "), default=_preconvert)) From d604f61030eeb10b9d62b10b3867107ba3686c90 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 14 Jun 2020 16:04:43 -0700 Subject: [PATCH 0618/1439] Ignore ignored directories when determining first party from src_paths --- isort/finders.py | 4 +++- isort/settings.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/isort/finders.py b/isort/finders.py index 13983074e..2b87428fb 100644 --- a/isort/finders.py +++ b/isort/finders.py @@ -10,6 +10,7 @@ from fnmatch import fnmatch from functools import lru_cache from glob import glob +from pathlib import Path from typing import Dict, Iterable, Iterator, List, Optional, Pattern, Sequence, Tuple, Type from . import sections @@ -166,6 +167,7 @@ def __init__(self, config: Config, path: str = ".") -> None: def find(self, module_name: str) -> Optional[str]: for prefix in self.paths: package_path = "/".join((prefix, module_name.split(".")[0])) + path_obj = Path(package_path).resolve() is_module = ( exists_case_sensitive(package_path + ".py") or any( @@ -187,7 +189,7 @@ def find(self, module_name: str) -> Optional[str]: elif self.conda_env and self.conda_env in prefix: return sections.THIRDPARTY for src_path in self.config.src_paths: - if str(src_path) in os.path.abspath(package_path): + if src_path in path_obj.parents and not self.config.is_skipped(path_obj): return sections.FIRSTPARTY if os.path.normcase(prefix).startswith(self.stdlib_lib_prefix): diff --git a/isort/settings.py b/isort/settings.py index d4f2d55dc..05e7293c6 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -300,10 +300,10 @@ def __init__( combined_config["import_headings"] = import_headings if "src_paths" not in combined_config: - combined_config["src_paths"] = frozenset((Path.cwd().absolute(),)) + combined_config["src_paths"] = frozenset((Path.cwd().resolve(),)) else: combined_config["src_paths"] = frozenset( - path.absolute() for path in combined_config["src_paths"] + path.resolve() for path in combined_config["src_paths"] ) super().__init__(sources=tuple(sources), **combined_config) # type: ignore From eb10d8acfab8478a33618b733f834d119dc7bccb Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 14 Jun 2020 16:05:49 -0700 Subject: [PATCH 0619/1439] Replace absolute with resolve --- isort/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/main.py b/isort/main.py index 533fdd46f..16717efb0 100644 --- a/isort/main.py +++ b/isort/main.py @@ -634,9 +634,9 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = src_paths = config_dict.setdefault("src_paths", set()) for file_name in file_names: if os.path.isdir(file_name): - src_paths.add(Path(file_name).absolute()) + src_paths.add(Path(file_name).resolve()) else: - src_paths.add(Path(file_name).parent.absolute()) + src_paths.add(Path(file_name).parent.resolve()) config = Config(**config_dict) if show_config: From 20cbf89090cefd53aa6bf1896116503026717501 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 15 Jun 2020 21:21:32 -0700 Subject: [PATCH 0620/1439] Fix minor formatting issue --- isort/main.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/isort/main.py b/isort/main.py index 441610c7d..b6945a620 100644 --- a/isort/main.py +++ b/isort/main.py @@ -229,8 +229,7 @@ def _build_arg_parser() -> argparse.ArgumentParser: "--future", dest="known_future_library", action="append", - help="Force isort to recognize a module as part " - "of the future compatibility libraries.", + help="Force isort to recognize a module as part of the future compatibility libraries.", ) parser.add_argument( "--fas", From 37c22aa822d7135308b11178f89d4eeb12a03b1a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 15 Jun 2020 21:53:37 -0700 Subject: [PATCH 0621/1439] Add flag, configuration, and testing for existing finders to move into deprecated state. New finder to follow --- isort/deprecated/__init__.py | 0 isort/{ => deprecated}/finders.py | 6 +- isort/main.py | 7 + isort/parse.py | 7 +- isort/settings.py | 1 + tests/test_deprecated_finders.py | 212 ++++++++++++++++++++++++++++++ tests/test_finders.py | 117 ----------------- tests/test_importable.py | 2 +- tests/test_isort.py | 93 +------------ 9 files changed, 230 insertions(+), 215 deletions(-) create mode 100644 isort/deprecated/__init__.py rename isort/{ => deprecated}/finders.py (99%) create mode 100644 tests/test_deprecated_finders.py delete mode 100644 tests/test_finders.py diff --git a/isort/deprecated/__init__.py b/isort/deprecated/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/isort/finders.py b/isort/deprecated/finders.py similarity index 99% rename from isort/finders.py rename to isort/deprecated/finders.py index 2b87428fb..b9276d5f0 100644 --- a/isort/finders.py +++ b/isort/deprecated/finders.py @@ -13,9 +13,9 @@ from pathlib import Path from typing import Dict, Iterable, Iterator, List, Optional, Pattern, Sequence, Tuple, Type -from . import sections -from .settings import Config -from .utils import chdir, exists_case_sensitive +from isort import sections +from isort.settings import Config +from isort.utils import chdir, exists_case_sensitive try: from pipreqs import pipreqs diff --git a/isort/main.py b/isort/main.py index b6945a620..1265e7a44 100644 --- a/isort/main.py +++ b/isort/main.py @@ -545,6 +545,13 @@ def _build_arg_parser() -> argparse.ArgumentParser: action="store_true", help="Tells isort to apply changes interactively.", ) + parser.add_argument( + "--old-finders", + "--magic", + dest="old_finders", + action="store_true", + help="Use the old deprecated finder logic that relies on environment introspection magic.", + ) parser.add_argument( "--show-config", dest="show_config", diff --git a/isort/parse.py b/isort/parse.py index 0266d23f1..f7b760523 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -7,7 +7,7 @@ from isort.settings import DEFAULT_CONFIG, Config from .comments import parse as parse_comments -from .finders import FindersManager +from .deprecated.finders import FindersManager if TYPE_CHECKING: from mypy_extensions import TypedDict @@ -141,7 +141,10 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte out_lines = [] original_line_count = len(in_lines) section_comments = [f"# {heading}" for heading in config.import_headings.values()] - finder = FindersManager(config=config) + if config.old_finders: + finder = FindersManager(config=config) + else: + finder = FindersManager(config=config) # TODO: replace with alternative new finder line_count = len(in_lines) diff --git a/isort/settings.py b/isort/settings.py index 05e7293c6..5e7b5dfd4 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -162,6 +162,7 @@ class _Config: directory: str = "" profile: str = "" src_paths: FrozenSet[Path] = frozenset() + old_finders: bool = False def __post_init__(self): py_version = self.py_version diff --git a/tests/test_deprecated_finders.py b/tests/test_deprecated_finders.py new file mode 100644 index 000000000..24517bbb3 --- /dev/null +++ b/tests/test_deprecated_finders.py @@ -0,0 +1,212 @@ +import importlib.machinery +import os +import posixpath +from pathlib import Path +from unittest.mock import patch + +import pytest + +from isort import sections, settings +from isort.deprecated import finders +from isort.deprecated.finders import FindersManager +from isort.settings import Config + +PIPFILE = """ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[requires] +python_version = "3.5" + +[packages] +Django = "~=1.11" +deal = {editable = true, git = "https://github.com/orsinium/deal.git"} + +[dev-packages] +""" + + +class TestFindersManager: + def test_init(self): + assert FindersManager(settings.DEFAULT_CONFIG) + + class ExceptionOnInit(finders.BaseFinder): + def __init__(*args, **kwargs): + super().__init__(*args, **kwargs) + raise ValueError("test") + + with patch( + "isort.deprecated.finders.FindersManager._default_finders_classes", + FindersManager._default_finders_classes + (ExceptionOnInit,), + ): + assert FindersManager(settings.Config(verbose=True)) + + def test_no_finders(self): + assert FindersManager(settings.DEFAULT_CONFIG, []).find("isort") is None + + def test_find_broken_finder(self): + class ExceptionOnFind(finders.BaseFinder): + def find(*args, **kwargs): + raise ValueError("test") + + assert ( + FindersManager(settings.Config(verbose=True), [ExceptionOnFind]).find("isort") is None + ) + + +class AbstractTestFinder: + kind = finders.BaseFinder + + @classmethod + def setup_class(cls): + cls.instance = cls.kind(settings.DEFAULT_CONFIG) + + def test_create(self): + assert self.kind(settings.DEFAULT_CONFIG) + + def test_find(self): + self.instance.find("isort") + self.instance.find("") + + +class TestForcedSeparateFinder(AbstractTestFinder): + kind = finders.ForcedSeparateFinder + + +class TestDefaultFinder(AbstractTestFinder): + kind = finders.DefaultFinder + + +class TestKnownPatternFinder(AbstractTestFinder): + kind = finders.KnownPatternFinder + + +class TestLocalFinder(AbstractTestFinder): + kind = finders.LocalFinder + + +class TestPathFinder(AbstractTestFinder): + kind = finders.PathFinder + + def test_conda_and_virtual_env(self, tmpdir): + python3lib = tmpdir.mkdir("lib").mkdir("python3") + python3lib.mkdir("site-packages").mkdir("y") + python3lib.mkdir("n").mkdir("site-packages").mkdir("x") + tmpdir.mkdir("z").join("__init__.py").write("__version__ = '1.0.0'") + tmpdir.chdir() + + conda = self.kind(settings.Config(conda_env=str(tmpdir)), str(tmpdir)) + venv = self.kind(settings.Config(virtual_env=str(tmpdir)), str(tmpdir)) + assert conda.find("y") == venv.find("y") == "THIRDPARTY" + assert conda.find("x") == venv.find("x") == "THIRDPARTY" + assert conda.find("z") == "THIRDPARTY" + assert conda.find("os") == venv.find("os") == "STDLIB" + + def test_default_section(self, tmpdir): + tmpdir.join("file.py").write("import b\nimport a\n") + assert self.kind(settings.Config(default_section="CUSTOM"), tmpdir).find("file") == "CUSTOM" + + def test_src_paths(self, tmpdir): + tmpdir.join("file.py").write("import b\nimport a\n") + assert ( + self.kind(settings.Config(src_paths=[Path(str(tmpdir))]), tmpdir).find("file") + == "FIRSTPARTY" + ) + + +class TestPipfileFinder(AbstractTestFinder): + kind = finders.PipfileFinder + + +class TestRequirementsFinder(AbstractTestFinder): + kind = finders.RequirementsFinder + + def test_no_pipreqs(self): + with patch("isort.deprecated.finders.pipreqs", None): + assert not self.kind(settings.DEFAULT_CONFIG).find("isort") + + def test_not_enabled(self): + test_finder = self.kind(settings.DEFAULT_CONFIG) + test_finder.enabled = False + assert not test_finder.find("isort") + + def test_requirements_dir(self, tmpdir): + tmpdir.mkdir("requirements").join("development.txt").write("x==1.00") + test_finder = self.kind(settings.DEFAULT_CONFIG, str(tmpdir)) + assert test_finder.find("x") + + +def test_requirements_finder(tmpdir) -> None: + subdir = tmpdir.mkdir("subdir").join("lol.txt") + subdir.write("flask") + req_file = tmpdir.join("requirements.txt") + req_file.write("Django==1.11\n-e git+https://github.com/orsinium/deal.git#egg=deal\n") + for path in (str(tmpdir), str(subdir)): + + finder = finders.RequirementsFinder(config=Config(), path=path) + + files = list(finder._get_files()) + assert len(files) == 1 # file finding + assert files[0].endswith("requirements.txt") # file finding + assert set(finder._get_names(str(req_file))) == {"Django", "deal"} # file parsing + + assert finder.find("django") == sections.THIRDPARTY # package in reqs + assert finder.find("flask") is None # package not in reqs + assert finder.find("deal") == sections.THIRDPARTY # vcs + + assert len(finder.mapping) > 100 + assert finder._normalize_name("deal") == "deal" + assert finder._normalize_name("Django") == "django" # lowercase + assert finder._normalize_name("django_haystack") == "haystack" # mapping + assert finder._normalize_name("Flask-RESTful") == "flask_restful" # conver `-`to `_` + + req_file.remove() + + +def test_pipfile_finder(tmpdir) -> None: + pipfile = tmpdir.join("Pipfile") + pipfile.write(PIPFILE) + finder = finders.PipfileFinder(config=Config(), path=str(tmpdir)) + + assert set(finder._get_names(str(tmpdir))) == {"Django", "deal"} # file parsing + + assert finder.find("django") == sections.THIRDPARTY # package in reqs + assert finder.find("flask") is None # package not in reqs + assert finder.find("deal") == sections.THIRDPARTY # vcs + + assert len(finder.mapping) > 100 + assert finder._normalize_name("deal") == "deal" + assert finder._normalize_name("Django") == "django" # lowercase + assert finder._normalize_name("django_haystack") == "haystack" # mapping + assert finder._normalize_name("Flask-RESTful") == "flask_restful" # conver `-`to `_` + + pipfile.remove() + + +def test_path_finder(monkeypatch) -> None: + config = config = Config() + finder = finders.PathFinder(config=config) + third_party_prefix = next(path for path in finder.paths if "site-packages" in path) + ext_suffixes = importlib.machinery.EXTENSION_SUFFIXES + imaginary_paths = { + posixpath.join(finder.stdlib_lib_prefix, "example_1.py"), + posixpath.join(third_party_prefix, "example_2.py"), + posixpath.join(os.getcwd(), "example_3.py"), + } + imaginary_paths.update( + { + posixpath.join(third_party_prefix, "example_" + str(i) + ext_suffix) + for i, ext_suffix in enumerate(ext_suffixes, 4) + } + ) + + monkeypatch.setattr( + "isort.deprecated.finders.exists_case_sensitive", lambda p: p in imaginary_paths + ) + assert finder.find("example_1") == sections.STDLIB + assert finder.find("example_2") == sections.THIRDPARTY + assert finder.find("example_3") == sections.FIRSTPARTY + for i, _ in enumerate(ext_suffixes, 4): + assert finder.find("example_" + str(i)) == sections.THIRDPARTY diff --git a/tests/test_finders.py b/tests/test_finders.py deleted file mode 100644 index ce7ecb73e..000000000 --- a/tests/test_finders.py +++ /dev/null @@ -1,117 +0,0 @@ -from pathlib import Path -from unittest.mock import patch - -import pytest - -from isort import finders, settings -from isort.finders import FindersManager - - -class TestFindersManager: - def test_init(self): - assert FindersManager(settings.DEFAULT_CONFIG) - - class ExceptionOnInit(finders.BaseFinder): - def __init__(*args, **kwargs): - super().__init__(*args, **kwargs) - raise ValueError("test") - - with patch( - "isort.finders.FindersManager._default_finders_classes", - FindersManager._default_finders_classes + (ExceptionOnInit,), - ): - assert FindersManager(settings.Config(verbose=True)) - - def test_no_finders(self): - assert FindersManager(settings.DEFAULT_CONFIG, []).find("isort") is None - - def test_find_broken_finder(self): - class ExceptionOnFind(finders.BaseFinder): - def find(*args, **kwargs): - raise ValueError("test") - - assert ( - FindersManager(settings.Config(verbose=True), [ExceptionOnFind]).find("isort") is None - ) - - -class AbstractTestFinder: - kind = finders.BaseFinder - - @classmethod - def setup_class(cls): - cls.instance = cls.kind(settings.DEFAULT_CONFIG) - - def test_create(self): - assert self.kind(settings.DEFAULT_CONFIG) - - def test_find(self): - self.instance.find("isort") - self.instance.find("") - - -class TestForcedSeparateFinder(AbstractTestFinder): - kind = finders.ForcedSeparateFinder - - -class TestDefaultFinder(AbstractTestFinder): - kind = finders.DefaultFinder - - -class TestKnownPatternFinder(AbstractTestFinder): - kind = finders.KnownPatternFinder - - -class TestLocalFinder(AbstractTestFinder): - kind = finders.LocalFinder - - -class TestPathFinder(AbstractTestFinder): - kind = finders.PathFinder - - def test_conda_and_virtual_env(self, tmpdir): - python3lib = tmpdir.mkdir("lib").mkdir("python3") - python3lib.mkdir("site-packages").mkdir("y") - python3lib.mkdir("n").mkdir("site-packages").mkdir("x") - tmpdir.mkdir("z").join("__init__.py").write("__version__ = '1.0.0'") - tmpdir.chdir() - - conda = self.kind(settings.Config(conda_env=str(tmpdir)), str(tmpdir)) - venv = self.kind(settings.Config(virtual_env=str(tmpdir)), str(tmpdir)) - assert conda.find("y") == venv.find("y") == "THIRDPARTY" - assert conda.find("x") == venv.find("x") == "THIRDPARTY" - assert conda.find("z") == "THIRDPARTY" - assert conda.find("os") == venv.find("os") == "STDLIB" - - def test_default_section(self, tmpdir): - tmpdir.join("file.py").write("import b\nimport a\n") - assert self.kind(settings.Config(default_section="CUSTOM"), tmpdir).find("file") == "CUSTOM" - - def test_src_paths(self, tmpdir): - tmpdir.join("file.py").write("import b\nimport a\n") - assert ( - self.kind(settings.Config(src_paths=[Path(str(tmpdir))]), tmpdir).find("file") - == "FIRSTPARTY" - ) - - -class TestPipfileFinder(AbstractTestFinder): - kind = finders.PipfileFinder - - -class TestRequirementsFinder(AbstractTestFinder): - kind = finders.RequirementsFinder - - def test_no_pipreqs(self): - with patch("isort.finders.pipreqs", None): - assert not self.kind(settings.DEFAULT_CONFIG).find("isort") - - def test_not_enabled(self): - test_finder = self.kind(settings.DEFAULT_CONFIG) - test_finder.enabled = False - assert not test_finder.find("isort") - - def test_requirements_dir(self, tmpdir): - tmpdir.mkdir("requirements").join("development.txt").write("x==1.00") - test_finder = self.kind(settings.DEFAULT_CONFIG, str(tmpdir)) - assert test_finder.find("x") diff --git a/tests/test_importable.py b/tests/test_importable.py index 0fcf0300e..97116d0b1 100644 --- a/tests/test_importable.py +++ b/tests/test_importable.py @@ -10,8 +10,8 @@ def test_importable(): import isort._version import isort.api import isort.comments + import isort.deprecated.finders import isort.exceptions - import isort.finders import isort.format import isort.hooks import isort.logo diff --git a/tests/test_isort.py b/tests/test_isort.py index 9d5b1a9b9..1c88f1fd9 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -2,11 +2,9 @@ Should be ran using py.test by simply running py.test in the isort project directory """ -import importlib.machinery import os import os.path from pathlib import Path -import posixpath import subprocess import sys import sysconfig @@ -15,7 +13,7 @@ import py import pytest -from isort import finders, main, sections, api +from isort import main, api from isort.main import is_python_file from isort.settings import WrapModes, Config from isort.utils import exists_case_sensitive @@ -3069,33 +3067,6 @@ def test_new_lines_are_preserved() -> None: os.remove(n_newline.name) -def test_requirements_finder(tmpdir) -> None: - subdir = tmpdir.mkdir("subdir").join("lol.txt") - subdir.write("flask") - req_file = tmpdir.join("requirements.txt") - req_file.write("Django==1.11\n-e git+https://github.com/orsinium/deal.git#egg=deal\n") - for path in (str(tmpdir), str(subdir)): - - finder = finders.RequirementsFinder(config=Config(), path=path) - - files = list(finder._get_files()) - assert len(files) == 1 # file finding - assert files[0].endswith("requirements.txt") # file finding - assert set(finder._get_names(str(req_file))) == {"Django", "deal"} # file parsing - - assert finder.find("django") == sections.THIRDPARTY # package in reqs - assert finder.find("flask") is None # package not in reqs - assert finder.find("deal") == sections.THIRDPARTY # vcs - - assert len(finder.mapping) > 100 - assert finder._normalize_name("deal") == "deal" - assert finder._normalize_name("Django") == "django" # lowercase - assert finder._normalize_name("django_haystack") == "haystack" # mapping - assert finder._normalize_name("Flask-RESTful") == "flask_restful" # conver `-`to `_` - - req_file.remove() - - def test_forced_separate_is_deterministic_issue_774(tmpdir) -> None: config_file = tmpdir.join("setup.cfg") @@ -3123,43 +3094,6 @@ def test_forced_separate_is_deterministic_issue_774(tmpdir) -> None: assert api.sort_code_string(test_input, settings_file=config_file.strpath) == test_input -PIPFILE = """ -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[requires] -python_version = "3.5" - -[packages] -Django = "~=1.11" -deal = {editable = true, git = "https://github.com/orsinium/deal.git"} - -[dev-packages] -""" - - -def test_pipfile_finder(tmpdir) -> None: - pipfile = tmpdir.join("Pipfile") - pipfile.write(PIPFILE) - finder = finders.PipfileFinder(config=Config(), path=str(tmpdir)) - - assert set(finder._get_names(str(tmpdir))) == {"Django", "deal"} # file parsing - - assert finder.find("django") == sections.THIRDPARTY # package in reqs - assert finder.find("flask") is None # package not in reqs - assert finder.find("deal") == sections.THIRDPARTY # vcs - - assert len(finder.mapping) > 100 - assert finder._normalize_name("deal") == "deal" - assert finder._normalize_name("Django") == "django" # lowercase - assert finder._normalize_name("django_haystack") == "haystack" # mapping - assert finder._normalize_name("Flask-RESTful") == "flask_restful" # conver `-`to `_` - - pipfile.remove() - - def test_monkey_patched_urllib() -> None: with pytest.raises(ImportError): # Previous versions of isort monkey patched urllib which caused unusual @@ -3167,31 +3101,6 @@ def test_monkey_patched_urllib() -> None: from urllib import quote # type: ignore # noqa: F401 -def test_path_finder(monkeypatch) -> None: - config = config = Config() - finder = finders.PathFinder(config=config) - third_party_prefix = next(path for path in finder.paths if "site-packages" in path) - ext_suffixes = importlib.machinery.EXTENSION_SUFFIXES - imaginary_paths = { - posixpath.join(finder.stdlib_lib_prefix, "example_1.py"), - posixpath.join(third_party_prefix, "example_2.py"), - posixpath.join(os.getcwd(), "example_3.py"), - } - imaginary_paths.update( - { - posixpath.join(third_party_prefix, "example_" + str(i) + ext_suffix) - for i, ext_suffix in enumerate(ext_suffixes, 4) - } - ) - - monkeypatch.setattr("isort.finders.exists_case_sensitive", lambda p: p in imaginary_paths) - assert finder.find("example_1") == sections.STDLIB - assert finder.find("example_2") == sections.THIRDPARTY - assert finder.find("example_3") == sections.FIRSTPARTY - for i, _ in enumerate(ext_suffixes, 4): - assert finder.find("example_" + str(i)) == sections.THIRDPARTY - - def test_argument_parsing() -> None: from isort.main import parse_args From 6ffdc23d95e27f8a3637a1b77c7049ed62222272 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 15 Jun 2020 21:54:36 -0700 Subject: [PATCH 0622/1439] Update config option doc --- docs/configuration/options.md | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/docs/configuration/options.md b/docs/configuration/options.md index 727ee7f54..6d836c4ce 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -32,7 +32,7 @@ Force specific imports to the top of their appropriate section. Files that sort imports should skip over. If you want to skip multiple files you should specify twice: --skip file1 --skip file2. **Type:** Frozenset -**Default:** `frozenset({'dist', '.git', 'buck-out', '.nox', '.eggs', '_build', '.venv', '.tox', 'build', 'node_modules', 'venv', '.pants.d', '.hg', '.mypy_cache'})` +**Default:** `frozenset({'.nox', 'node_modules', 'buck-out', '.venv', '.mypy_cache', 'build', '.pants.d', '.hg', 'dist', '.git', '_build', '.eggs', 'venv', '.tox'})` **Python & Config File Name:** skip **CLI Flags:** @@ -108,7 +108,7 @@ Put all imports into the same section bucket - --no-sections ## Known Future Library -Force sortImports to recognize a module as part of the future compatibility libraries. +Force isort to recognize a module as part of the future compatibility libraries. **Type:** Frozenset **Default:** `frozenset({'__future__'})` @@ -119,7 +119,7 @@ Force sortImports to recognize a module as part of the future compatibility libr - --future ## Known Third Party -Force sortImports to recognize a module as being part of a third party library. +Force isort to recognize a module as being part of a third party library. **Type:** Frozenset **Default:** `frozenset({'google.appengine.api'})` @@ -130,7 +130,7 @@ Force sortImports to recognize a module as being part of a third party library. - --thirdparty ## Known First Party -Force sortImports to recognize a module as being part of the current python project. +Force isort to recognize a module as being part of the current python project. **Type:** Frozenset **Default:** `frozenset()` @@ -141,10 +141,10 @@ Force sortImports to recognize a module as being part of the current python proj - --project ## Known Standard Library -Force sortImports to recognize a module as part of the python standard library. +Force isort to recognize a module as part of the python standard library. **Type:** Frozenset -**Default:** `frozenset({'distutils', 'builtins', 'pickle', 'compileall', 'tty', 'test', 'winsound', 'telnetlib', 'sysconfig', 'aifc', 'socketserver', 'calendar', 'bz2', 'sndhdr', 'codecs', 'errno', 'turtledemo', 'winreg', 'sunau', 'dbm', 'configparser', 'lib2to3', 'webbrowser', 'array', 'code', 'collections', 'quopri', 'imp', 'sys', 'textwrap', 'dummy_threading', 'inspect', 'shelve', 'itertools', 'smtpd', 'heapq', 'optparse', 'cgitb', 'wsgiref', 'mailcap', 'fileinput', 'tempfile', 'copyreg', 'pkgutil', 'json', 'importlib', 'zipapp', 'marshal', 'parser', 'select', 'ssl', 'enum', 'grp', 'crypt', 'abc', 'dataclasses', '_dummy_thread', 'email', 'getopt', 'logging', 'types', 'fcntl', 'imghdr', 'poplib', 'asyncore', 'functools', 'cgi', 'bdb', 'bisect', 'typing', 'mailbox', 're', 'smtplib', 'pathlib', 'reprlib', 'gettext', 'codeop', 'tracemalloc', 'unittest', 'termios', 'tkinter', 'colorsys', 'cProfile', 'keyword', 'queue', 'unicodedata', 'profile', 'shutil', 'imaplib', 'formatter', 'difflib', 'base64', 'io', 'getpass', 'pyclbr', 'weakref', 'numbers', 'pwd', 'nntplib', 'fractions', 'gzip', 'encodings', 'glob', 'fpectl', 'msvcrt', 'hashlib', 'html', 'http', 'xml', 'tokenize', 'signal', 'subprocess', 'xdrlib', 'shlex', 'pdb', 'plistlib', 'pty', 'threading', 'struct', 'fnmatch', 'syslog', 'tarfile', 'doctest', 'tabnanny', 'resource', 'stat', 'zlib', 'spwd', 'locale', 'macpath', 'random', 'ast', 'pipes', 'uu', 'pprint', 'statistics', 'cmd', 'warnings', 'sqlite3', 'mmap', 'posix', 'token', 'decimal', 'multiprocessing', 'contextlib', 'mimetypes', 'binhex', 'site', 'asyncio', 'modulefinder', 'runpy', 'wave', 'os', 'netrc', 'pstats', 'gc', 'venv', 'asynchat', 'platform', 'argparse', 'chunk', 'string', 'readline', 'selectors', 'ctypes', 'hmac', 'turtle', 'rlcompleter', 'filecmp', 'csv', 'msilib', 'zipimport', 'audioop', 'stringprep', 'zipfile', 'cmath', 'ipaddress', 'time', 'socket', 'math', 'lzma', 'ensurepip', 'nis', 'pickletools', 'linecache', 'trace', 'uuid', 'traceback', 'pydoc', 'symbol', 'faulthandler', '_thread', 'operator', 'timeit', 'ftplib', 'ossaudiodev', 'dis', 'concurrent', 'binascii', 'symtable', 'copy', 'sched', 'curses', 'contextvars', 'atexit', 'xmlrpc', 'datetime', 'secrets', 'py_compile', 'urllib'})` +**Default:** `frozenset({'multiprocessing', 'codeop', 'pprint', 'chunk', 'msilib', 'concurrent', 'sched', 'random', 'webbrowser', 'socketserver', 'nis', 'tracemalloc', 'filecmp', 'formatter', 'codecs', 'winsound', 'tty', 'xml', 'gzip', 'binascii', 'enum', 'symtable', '_thread', 'xmlrpc', 'io', 'timeit', 'typing', 'datetime', 'sysconfig', 'ipaddress', 'traceback', 'zipfile', 'audioop', 'mimetypes', 'poplib', 'glob', 'weakref', 'pdb', 'locale', 'queue', 'collections', '_dummy_thread', 'trace', 'linecache', 'code', 'wave', 'reprlib', 'zipimport', 'posix', 'select', 'time', 'sqlite3', 'cgitb', 'ast', 'imp', 'shutil', 'resource', 'decimal', 'itertools', 'json', 'curses', 'pkgutil', 'contextvars', 'bdb', 'dis', 'pstats', 'copyreg', 'distutils', 'marshal', 'winreg', 'faulthandler', 'nntplib', 'ssl', 'struct', 'functools', 'pydoc', 'stat', 'asyncore', 'runpy', 'token', 'ossaudiodev', 'shlex', 'urllib', 'cgi', 'imghdr', 'netrc', 'operator', 'py_compile', 'unittest', 'sunau', 'tempfile', 'textwrap', 'dbm', 'calendar', 'cmath', 'selectors', 'dataclasses', 'pickletools', 'macpath', 'pipes', 'pyclbr', 'asyncio', 'copy', 'spwd', 'heapq', 'statistics', 'venv', 'optparse', 'string', 'encodings', 'fnmatch', 'ftplib', 'bisect', 'fractions', 'msvcrt', 'html', 'binhex', 'crypt', 'colorsys', 'telnetlib', 'smtplib', 'ctypes', 'stringprep', 'fpectl', 'warnings', 'atexit', 'signal', 'subprocess', 'aifc', 'getpass', 'ensurepip', 'imaplib', 'gc', 'profile', 'parser', 'math', 'errno', 'numbers', 'readline', 'argparse', 'mailbox', 'smtpd', 'zlib', 'os', 'platform', 'tarfile', 'tkinter', 'configparser', 'plistlib', 'keyword', 'pathlib', 'mailcap', 'lzma', 'cProfile', 'sndhdr', 'secrets', 'mmap', 'array', 'pickle', 'syslog', 'threading', 'xdrlib', 'http', 'socket', 'tabnanny', 'email', 'rlcompleter', 'logging', 'inspect', 'test', 'quopri', 'difflib', 'bz2', 'types', 'csv', 'gettext', 'dummy_threading', 'symbol', 'turtledemo', 'compileall', 'hmac', 'doctest', 'unicodedata', 'turtle', 'uuid', 'sys', 'termios', 'modulefinder', 'contextlib', 'tokenize', 'zipapp', 're', 'cmd', 'uu', 'base64', 'fileinput', 'wsgiref', 'getopt', 'grp', 'lib2to3', 'pwd', 'site', 'asynchat', 'shelve', 'pty', 'builtins', 'fcntl', 'importlib', 'abc', 'hashlib'})` **Python & Config File Name:** known_standard_library **CLI Flags:** @@ -641,6 +641,17 @@ Base profile type to use for configuration. - **Not Supported** +## Old Finders +Use the old deprecated finder logic that relies on environment introspection magic. + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** old_finders +**CLI Flags:** + + - --old-finders + - --magic + ## Source Paths Add an explicitly defined source path (modules within src paths have their imports automatically catorgorized as first_party). From 70281c6c3f0b3d2f932df25db594328954ccc49b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 16 Jun 2020 21:51:52 -0700 Subject: [PATCH 0623/1439] Initial outline of new module placement functionality --- isort/deprecated/finders.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/isort/deprecated/finders.py b/isort/deprecated/finders.py index b9276d5f0..e299d017c 100644 --- a/isort/deprecated/finders.py +++ b/isort/deprecated/finders.py @@ -14,6 +14,7 @@ from typing import Dict, Iterable, Iterator, List, Optional, Pattern, Sequence, Tuple, Type from isort import sections +from isort.place import KNOWN_SECTION_MAPPING from isort.settings import Config from isort.utils import chdir, exists_case_sensitive @@ -35,13 +36,6 @@ except ImportError: Pipfile = None -KNOWN_SECTION_MAPPING: Dict[str, str] = { - sections.STDLIB: "STANDARD_LIBRARY", - sections.FUTURE: "FUTURE_LIBRARY", - sections.FIRSTPARTY: "FIRST_PARTY", - sections.THIRDPARTY: "THIRD_PARTY", -} - class BaseFinder(metaclass=ABCMeta): def __init__(self, config: Config) -> None: From b2ca491cde4bcd15bc4139d640ed19e10f3649af Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 17 Jun 2020 23:59:28 -0700 Subject: [PATCH 0624/1439] Initial work toward new finder module --- isort/deprecated/finders.py | 3 +-- isort/settings.py | 49 +++++++++++++++++++++++++++++++++++-- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/isort/deprecated/finders.py b/isort/deprecated/finders.py index e299d017c..f32e9acf2 100644 --- a/isort/deprecated/finders.py +++ b/isort/deprecated/finders.py @@ -14,8 +14,7 @@ from typing import Dict, Iterable, Iterator, List, Optional, Pattern, Sequence, Tuple, Type from isort import sections -from isort.place import KNOWN_SECTION_MAPPING -from isort.settings import Config +from isort.settings import KNOWN_SECTION_MAPPING, Config from isort.utils import chdir, exists_case_sensitive try: diff --git a/isort/settings.py b/isort/settings.py index 5e7b5dfd4..7fef02b28 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -14,10 +14,10 @@ from distutils.util import strtobool as _as_bool from functools import lru_cache from pathlib import Path -from typing import Any, Callable, Dict, FrozenSet, Iterable, List, Optional, Set, Tuple +from typing import Any, Callable, Dict, FrozenSet, Iterable, List, Optional, Pattern, Set, Tuple from warnings import warn -from . import stdlibs +from . import sections, stdlibs from ._future import dataclass, field from .exceptions import ProfileDoesNotExist from .profiles import profiles @@ -76,6 +76,12 @@ IMPORT_HEADING_PREFIX = "import_heading_" KNOWN_PREFIX = "known_" +KNOWN_SECTION_MAPPING: Dict[str, str] = { + sections.STDLIB: "STANDARD_LIBRARY", + sections.FUTURE: "FUTURE_LIBRARY", + sections.FIRSTPARTY: "FIRST_PARTY", + sections.THIRDPARTY: "THIRD_PARTY", +} @dataclass(frozen=True) @@ -218,6 +224,7 @@ def __init__( config_vars = vars(config).copy() config_vars.update(config_overrides) config_vars["py_version"] = config_vars["py_version"].replace("py", "") + config_vars.pop("_known_patterns") super().__init__(**config_vars) # type: ignore return @@ -307,6 +314,7 @@ def __init__( path.resolve() for path in combined_config["src_paths"] ) + self._known_patterns = None super().__init__(sources=tuple(sources), **combined_config) # type: ignore def is_skipped(self, file_path: Path) -> bool: @@ -343,6 +351,43 @@ def is_skipped(self, file_path: Path) -> bool: return False + @property + def known_patterns(self): + if self._known_patterns: + return self._known_patterns + + self._known_patterns: List[Tuple[Pattern[str], str]] = [] + for placement in reversed(self.sections): + known_placement = KNOWN_SECTION_MAPPING.get(placement, placement).lower() + config_key = f"{KNOWN_PREFIX}{known_placement}" + known_patterns = list( + getattr(self, config_key, self.known_other.get(known_placement, [])) + ) + known_patterns = [ + pattern + for known_pattern in known_patterns + for pattern in self._parse_known_pattern(known_pattern) + ] + for known_pattern in known_patterns: + regexp = "^" + known_pattern.replace("*", ".*").replace("?", ".?") + "$" + self._known_patterns.append((re.compile(regexp), placement)) + + return self._known_patterns + + @staticmethod + def _parse_known_pattern(pattern: str) -> List[str]: + """Expand pattern if identified as a directory and return found sub packages""" + if pattern.endswith(os.path.sep): + patterns = [ + filename + for filename in os.listdir(pattern) + if os.path.isdir(os.path.join(pattern, filename)) + ] + else: + patterns = [pattern] + + return patterns + def _get_str_to_type_converter(setting_name: str) -> Callable[[str], Any]: type_converter: Callable[[str], Any] = type(_DEFAULT_SETTINGS.get(setting_name, "")) From 76b305d7b086caa357ed66183af3a4bc981e64df Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 18 Jun 2020 00:06:21 -0700 Subject: [PATCH 0625/1439] Fix typing errors on new placement logit --- isort/settings.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/isort/settings.py b/isort/settings.py index 7fef02b28..c977f8b41 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -9,6 +9,7 @@ import fnmatch import os import posixpath +import re import sys import warnings from distutils.util import strtobool as _as_bool @@ -314,7 +315,7 @@ def __init__( path.resolve() for path in combined_config["src_paths"] ) - self._known_patterns = None + self._known_patterns: Optional[List[Tuple[Pattern[str], str]]] = None super().__init__(sources=tuple(sources), **combined_config) # type: ignore def is_skipped(self, file_path: Path) -> bool: @@ -353,10 +354,10 @@ def is_skipped(self, file_path: Path) -> bool: @property def known_patterns(self): - if self._known_patterns: + if self._known_patterns is not None: return self._known_patterns - self._known_patterns: List[Tuple[Pattern[str], str]] = [] + self._known_patterns = [] for placement in reversed(self.sections): known_placement = KNOWN_SECTION_MAPPING.get(placement, placement).lower() config_key = f"{KNOWN_PREFIX}{known_placement}" From 535b4e86d91bcc09aaac9dc6e37717bbaa95499e Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 18 Jun 2020 00:11:47 -0700 Subject: [PATCH 0626/1439] Switch to new finder --- isort/parse.py | 13 +++++++------ isort/settings.py | 3 ++- tests/test_api.py | 1 - tests/test_comments.py | 1 - tests/test_deprecated_finders.py | 3 +-- tests/test_format.py | 3 +-- tests/test_io.py | 1 - tests/test_main.py | 1 - tests/test_output.py | 1 - tests/test_parse.py | 1 - tests/test_settings.py | 1 - tests/test_setuptools_command.py | 1 - tests/test_wrap_modes.py | 1 - 13 files changed, 11 insertions(+), 20 deletions(-) diff --git a/isort/parse.py b/isort/parse.py index f7b760523..40a0c75a0 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -1,13 +1,14 @@ """Defines parsing functions used by isort for parsing import definitions""" from collections import OrderedDict, defaultdict +from functools import partial from itertools import chain from typing import TYPE_CHECKING, Any, Dict, List, NamedTuple, Optional, Tuple from warnings import warn -from isort.settings import DEFAULT_CONFIG, Config - +from . import place from .comments import parse as parse_comments from .deprecated.finders import FindersManager +from .settings import DEFAULT_CONFIG, Config if TYPE_CHECKING: from mypy_extensions import TypedDict @@ -142,9 +143,9 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte original_line_count = len(in_lines) section_comments = [f"# {heading}" for heading in config.import_headings.values()] if config.old_finders: - finder = FindersManager(config=config) + finder = FindersManager(config=config).find else: - finder = FindersManager(config=config) # TODO: replace with alternative new finder + finder = partial(place.module, config=config) line_count = len(in_lines) @@ -326,7 +327,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte del just_imports[as_index : as_index + 2] if type_of_import == "from": import_from = just_imports.pop(0) - placed_module = finder.find(import_from) + placed_module = finder(import_from) if config.verbose: print(f"from-type place_module for {import_from} returned {placed_module}") if placed_module == "": @@ -403,7 +404,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte import_index -= len( categorized_comments["above"]["straight"].get(module, []) ) - placed_module = finder.find(module) + placed_module = finder(module) if config.verbose: print(f"else-type place_module for {module} returned {placed_module}") if placed_module == "": diff --git a/isort/settings.py b/isort/settings.py index c977f8b41..4f1710c11 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -8,7 +8,6 @@ import configparser import fnmatch import os -import posixpath import re import sys import warnings @@ -18,6 +17,8 @@ from typing import Any, Callable, Dict, FrozenSet, Iterable, List, Optional, Pattern, Set, Tuple from warnings import warn +import posixpath + from . import sections, stdlibs from ._future import dataclass, field from .exceptions import ProfileDoesNotExist diff --git a/tests/test_api.py b/tests/test_api.py index e4984ceb1..2d7a35799 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -2,7 +2,6 @@ from unittest.mock import MagicMock, patch import pytest - from isort import api, exceptions from isort.settings import Config diff --git a/tests/test_comments.py b/tests/test_comments.py index b1b4ed7b4..1805f137c 100644 --- a/tests/test_comments.py +++ b/tests/test_comments.py @@ -1,5 +1,4 @@ from hypothesis_auto import auto_pytest_magic - from isort import comments auto_pytest_magic(comments.parse) diff --git a/tests/test_deprecated_finders.py b/tests/test_deprecated_finders.py index 24517bbb3..0ca0467f5 100644 --- a/tests/test_deprecated_finders.py +++ b/tests/test_deprecated_finders.py @@ -1,11 +1,10 @@ import importlib.machinery import os -import posixpath from pathlib import Path from unittest.mock import patch +import posixpath import pytest - from isort import sections, settings from isort.deprecated import finders from isort.deprecated.finders import FindersManager diff --git a/tests/test_format.py b/tests/test_format.py index 79b967019..27839a237 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -1,10 +1,9 @@ from unittest.mock import MagicMock, patch +import isort.format import pytest from hypothesis_auto import auto_pytest_magic -import isort.format - auto_pytest_magic(isort.format.show_unified_diff, auto_allow_exceptions_=(UnicodeEncodeError,)) diff --git a/tests/test_io.py b/tests/test_io.py index 59a86faca..1ad967549 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -2,7 +2,6 @@ from unittest.mock import patch import pytest - from isort import io diff --git a/tests/test_main.py b/tests/test_main.py index f9ef13acc..ee808ba32 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -6,7 +6,6 @@ import pytest from hypothesis_auto import auto_pytest_magic - from isort import main from isort._version import __version__ from isort.settings import DEFAULT_CONFIG, Config diff --git a/tests/test_output.py b/tests/test_output.py index 2457f70d7..2aaaf877f 100644 --- a/tests/test_output.py +++ b/tests/test_output.py @@ -1,5 +1,4 @@ from hypothesis_auto import auto_pytest_magic - from isort import output auto_pytest_magic(output.with_comments, auto_allow_exceptions_=(ValueError,)) diff --git a/tests/test_parse.py b/tests/test_parse.py index f3a24d668..51979c9d2 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -1,5 +1,4 @@ from hypothesis_auto import auto_pytest_magic - from isort import parse from isort.settings import Config diff --git a/tests/test_settings.py b/tests/test_settings.py index 648da100d..c7326236e 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -2,7 +2,6 @@ from unittest.mock import MagicMock, patch import pytest - from isort import exceptions, settings from isort.settings import Config diff --git a/tests/test_setuptools_command.py b/tests/test_setuptools_command.py index 634dd8877..cba43e437 100644 --- a/tests/test_setuptools_command.py +++ b/tests/test_setuptools_command.py @@ -2,7 +2,6 @@ from unittest.mock import MagicMock import pytest - from isort import setuptools_commands diff --git a/tests/test_wrap_modes.py b/tests/test_wrap_modes.py index bf0d698e8..1cfc4bc5b 100644 --- a/tests/test_wrap_modes.py +++ b/tests/test_wrap_modes.py @@ -1,5 +1,4 @@ from hypothesis_auto import auto_pytest_magic - from isort import wrap_modes auto_pytest_magic(wrap_modes.grid, auto_allow_exceptions_=(ValueError,)) From 95f86bec13f7dcd924dce750ee4eba0bfce3fde9 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 18 Jun 2020 22:42:05 -0700 Subject: [PATCH 0627/1439] Add module missing from CI/CD --- isort/place.py | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 isort/place.py diff --git a/isort/place.py b/isort/place.py new file mode 100644 index 000000000..437e7b523 --- /dev/null +++ b/isort/place.py @@ -0,0 +1,78 @@ +"""Contains all logic related to placing an import within a certain section.""" +import importlib +import os +from fnmatch import fnmatch +from pathlib import Path +from typing import Dict, Optional + +from isort import sections +from isort.settings import DEFAULT_CONFIG, Config +from isort.utils import exists_case_sensitive + +LOCAL = "LOCALFOLDER" + + +def module(name: str, config: Config = DEFAULT_CONFIG) -> str: + """Returns the section placement for the given module name.""" + return ( + _forced_separate(name, config) + or _local(name, config) + or _known_pattern(name, config) + or _src_path(name, config) + or config.default_section + ) + + +def _forced_separate(name: str, config: Config) -> Optional[str]: + for forced_separate in config.forced_separate: + # Ensure all forced_separate patterns will match to end of string + path_glob = forced_separate + if not forced_separate.endswith("*"): + path_glob = "%s*" % forced_separate + + if fnmatch(name, path_glob) or fnmatch(name, "." + path_glob): + return forced_separate + + return None + + +def _local(name: str, config: Config) -> Optional[str]: + if name.startswith("."): + return LOCAL + + return None + + +def _known_pattern(name: str, config: Config) -> Optional[str]: + parts = name.split(".") + module_names_to_check = (".".join(parts[:first_k]) for first_k in range(len(parts), 0, -1)) + for module_name_to_check in module_names_to_check: + for pattern, placement in config.known_patterns: + if pattern.match(module_name_to_check): + return placement + + return None + + +def _src_path(name: str, config: Config) -> Optional[str]: + for src_path in config.src_paths: + module_path = (src_path / os.path.sep.join(name.split(".")[0])).resolve() + if _is_module(module_path) or _is_package(module_path): + return sections.FIRSTPARTY + + return None + + +def _is_module(path: Path) -> bool: + return ( + exists_case_sensitive(str(path.with_suffix(".py"))) + or any( + exists_case_sensitive(str(path.with_suffix(ext_suffix))) + for ext_suffix in importlib.machinery.EXTENSION_SUFFIXES + ) + or exists_case_sensitive(str(path / "__init__.py")) + ) + + +def _is_package(path: Path) -> bool: + return exists_case_sensitive(str(path)) and path.is_dir() From 9f9e1fbbdea3a5d1a393a370bdaa2c993c2f536b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 18 Jun 2020 23:10:41 -0700 Subject: [PATCH 0628/1439] Fix test failures that resulted from finder refactoring --- isort/settings.py | 13 ++++++----- tests/test_deprecated_finders.py | 4 ++-- tests/test_isort.py | 39 ++++++++++++++++++++------------ tests/test_main.py | 4 ++-- 4 files changed, 35 insertions(+), 25 deletions(-) diff --git a/isort/settings.py b/isort/settings.py index 4f1710c11..456e78956 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -19,11 +19,12 @@ import posixpath -from . import sections, stdlibs +from . import stdlibs from ._future import dataclass, field from .exceptions import ProfileDoesNotExist from .profiles import profiles from .sections import DEFAULT as SECTION_DEFAULTS +from .sections import FIRSTPARTY, FUTURE, STDLIB, THIRDPARTY from .wrap_modes import WrapModes from .wrap_modes import from_string as wrap_mode_from_string @@ -79,10 +80,10 @@ IMPORT_HEADING_PREFIX = "import_heading_" KNOWN_PREFIX = "known_" KNOWN_SECTION_MAPPING: Dict[str, str] = { - sections.STDLIB: "STANDARD_LIBRARY", - sections.FUTURE: "FUTURE_LIBRARY", - sections.FIRSTPARTY: "FIRST_PARTY", - sections.THIRDPARTY: "THIRD_PARTY", + STDLIB: "STANDARD_LIBRARY", + FUTURE: "FUTURE_LIBRARY", + FIRSTPARTY: "FIRST_PARTY", + THIRDPARTY: "THIRD_PARTY", } @@ -136,7 +137,7 @@ class _Config: reverse_relative: bool = False force_single_line: bool = False single_line_exclusions: Tuple[str, ...] = () - default_section: str = "FIRSTPARTY" + default_section: str = THIRDPARTY import_headings: Dict[str, str] = field(default_factory=dict) balanced_wrapping: bool = False use_parentheses: bool = False diff --git a/tests/test_deprecated_finders.py b/tests/test_deprecated_finders.py index 0ca0467f5..3318c0bfd 100644 --- a/tests/test_deprecated_finders.py +++ b/tests/test_deprecated_finders.py @@ -111,7 +111,7 @@ def test_src_paths(self, tmpdir): tmpdir.join("file.py").write("import b\nimport a\n") assert ( self.kind(settings.Config(src_paths=[Path(str(tmpdir))]), tmpdir).find("file") - == "FIRSTPARTY" + == settings.DEFAULT_CONFIG.default_section ) @@ -206,6 +206,6 @@ def test_path_finder(monkeypatch) -> None: ) assert finder.find("example_1") == sections.STDLIB assert finder.find("example_2") == sections.THIRDPARTY - assert finder.find("example_3") == sections.FIRSTPARTY + assert finder.find("example_3") == settings.DEFAULT_CONFIG.default_section for i, _ in enumerate(ext_suffixes, 4): assert finder.find("example_" + str(i)) == sections.THIRDPARTY diff --git a/tests/test_isort.py b/tests/test_isort.py index 1c88f1fd9..8ebeec306 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -13,7 +13,7 @@ import py import pytest -from isort import main, api +from isort import main, api, sections from isort.main import is_python_file from isort.settings import WrapModes, Config from isort.utils import exists_case_sensitive @@ -64,7 +64,7 @@ def default_settings_path(tmpdir_factory) -> Iterator[str]: def test_happy_path() -> None: """Test the most basic use case, straight imports no code, simply not organized by category.""" test_input = "import sys\nimport os\nimport myproject.test\nimport django.settings" - test_output = api.sort_code_string(test_input, known_third_party=["django"]) + test_output = api.sort_code_string(test_input, known_first_party=["myproject"]) assert test_output == ( "import os\n" "import sys\n" "\n" "import django.settings\n" "\n" "import myproject.test\n" ) @@ -725,7 +725,7 @@ def test_skip() -> None: "import sys # isort: skip this import needs to be placed here\n\n\n\n\n\n\n" ) - test_output = api.sort_code_string(test_input, known_third_party=["django"]) + test_output = api.sort_code_string(test_input, known_first_party=["myproject"]) assert test_output == ( "import django\n" "\n" @@ -1125,7 +1125,7 @@ def test_titled_imports() -> None: ) test_output = api.sort_code_string( code=test_input, - known_third_party=["django"], + known_first_party=["myproject"], import_heading_stdlib="Standard Library", import_heading_firstparty="My Stuff", ) @@ -1143,7 +1143,7 @@ def test_titled_imports() -> None: ) test_second_run = api.sort_code_string( code=test_output, - known_third_party=["django"], + known_first_party=["myproject"], import_heading_stdlib="Standard Library", import_heading_firstparty="My Stuff", ) @@ -1625,6 +1625,7 @@ def test_correctly_placed_imports() -> None: force_single_line=True, line_length=140, known_third_party=["django", "model_mommy"], + default_section=sections.FIRSTPARTY, ) == test_input ) @@ -1718,9 +1719,9 @@ def test_place_comments() -> None: "import os\n" "import sys\n" ) - test_output = api.sort_code_string(test_input, known_third_party=["django"]) + test_output = api.sort_code_string(test_input, known_first_party=["myproject"]) assert test_output == expected_output - test_output = api.sort_code_string(test_output, known_third_party=["django"]) + test_output = api.sort_code_string(test_output, known_first_party=["myproject"]) assert test_output == expected_output @@ -2090,7 +2091,7 @@ def test_alphabetic_sorting() -> None: "force_alphabetical_sort_within_sections": True, } # type: Dict[str, Any] - output = api.sort_code_string(test_input, known_first_party=["django"], **options) + output = api.sort_code_string(test_input, **options) assert output == test_input test_input = "# -*- coding: utf-8 -*-\nfrom django.db import models\n" @@ -2679,8 +2680,9 @@ def test_import_case_produces_inconsistent_results_issue_472() -> None: def test_inconsistent_behavior_in_python_2_and_3_issue_479() -> None: """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" + "\n" + "from future.standard_library import hooks\n" ) assert api.sort_code_string(test_input, known_first_party=["future"]) == test_input @@ -2837,22 +2839,29 @@ def test_not_splitted_sections() -> None: + statement ) - assert api.sort_code_string(test_input) == test_input - assert api.sort_code_string(test_input, no_lines_before=["LOCALFOLDER"]) == ( - stdlib_section + whiteline + firstparty_section + local_section + whiteline + statement - ) + assert api.sort_code_string(test_input, known_first_party=["app"]) == test_input + assert api.sort_code_string( + test_input, no_lines_before=["LOCALFOLDER"], known_first_party=["app"] + ) == (stdlib_section + whiteline + firstparty_section + local_section + whiteline + statement) # by default STDLIB and FIRSTPARTY sections are split by THIRDPARTY section, # so don't merge them if THIRDPARTY imports aren't exist - assert api.sort_code_string(test_input, no_lines_before=["FIRSTPARTY"]) == test_input + assert ( + api.sort_code_string(test_input, no_lines_before=["FIRSTPARTY"], known_first_party=["app"]) + == test_input + ) # in case when THIRDPARTY section is excluded from sections list, # it's ok to merge STDLIB and FIRSTPARTY assert api.sort_code_string( code=test_input, sections=["STDLIB", "FIRSTPARTY", "LOCALFOLDER"], no_lines_before=["FIRSTPARTY"], + known_first_party=["app"], ) == (stdlib_section + firstparty_section + whiteline + local_section + whiteline + statement) # it doesn't change output, because stdlib packages don't have any whitelines before them - assert api.sort_code_string(test_input, no_lines_before=["STDLIB"]) == test_input + assert ( + api.sort_code_string(test_input, no_lines_before=["STDLIB"], known_first_party=["app"]) + == test_input + ) def test_no_lines_before_empty_section() -> None: diff --git a/tests/test_main.py b/tests/test_main.py index ee808ba32..c05f4ac99 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -143,8 +143,8 @@ def test_main(capsys, tmpdir): out, error = capsys.readouterr() assert ( out - == """else-type place_module for b returned FIRSTPARTY -else-type place_module for a returned FIRSTPARTY + == f"""else-type place_module for b returned {DEFAULT_CONFIG.default_section} +else-type place_module for a returned {DEFAULT_CONFIG.default_section} import a import b """ From 6275aef0eb7465cbe0f0bb33d575cff7af03d12b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 19 Jun 2020 21:37:44 -0700 Subject: [PATCH 0629/1439] Expose and test for isort.place --- isort/__init__.py | 2 +- tests/test_importable.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/isort/__init__.py b/isort/__init__.py index 66ce7dcb4..2927c3fcf 100644 --- a/isort/__init__.py +++ b/isort/__init__.py @@ -1,5 +1,5 @@ """Defines the public isort interface""" -from . import settings # noqa: F401 +from . import place, settings # noqa: F401 from ._version import __version__ from .api import check_code_string as check_code from .api import check_file, check_stream diff --git a/tests/test_importable.py b/tests/test_importable.py index 97116d0b1..499b50297 100644 --- a/tests/test_importable.py +++ b/tests/test_importable.py @@ -20,6 +20,7 @@ def test_importable(): import isort.parse import isort.profiles import isort.pylama_isort + import isort.place import isort.sections import isort.settings import isort.setuptools_commands From 5a9624dd8105bb60322a095efb1a5df18322fe39 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 19 Jun 2020 21:41:39 -0700 Subject: [PATCH 0630/1439] Ensure old_finders is used as well as new one --- tests/test_importable.py | 2 +- tests/test_isort.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_importable.py b/tests/test_importable.py index 499b50297..660de9908 100644 --- a/tests/test_importable.py +++ b/tests/test_importable.py @@ -18,9 +18,9 @@ def test_importable(): import isort.main import isort.output import isort.parse + import isort.place import isort.profiles import isort.pylama_isort - import isort.place import isort.sections import isort.settings import isort.setuptools_commands diff --git a/tests/test_isort.py b/tests/test_isort.py index 8ebeec306..58a03ba17 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -4591,6 +4591,7 @@ def test_cimport_support(): from web_request_client_cef3 cimport * """ assert api.sort_code_string(test_input).strip() == expected_output.strip() + assert api.sort_code_string(test_input, old_finders=True).strip() == expected_output.strip() def test_cdef_support(): From 8abb2122390fc32a766aec728e0d4fff9e578039 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 20 Jun 2020 15:52:15 -0700 Subject: [PATCH 0631/1439] Provide reasons when place imports --- isort/__init__.py | 4 ++-- isort/api.py | 2 ++ isort/place.py | 27 ++++++++++++++++----------- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/isort/__init__.py b/isort/__init__.py index 2927c3fcf..c653a2cd6 100644 --- a/isort/__init__.py +++ b/isort/__init__.py @@ -1,8 +1,8 @@ """Defines the public isort interface""" -from . import place, settings # noqa: F401 +from . import settings # noqa: F401 from ._version import __version__ from .api import check_code_string as check_code -from .api import check_file, check_stream +from .api import check_file, check_stream, place_module, place_module_with_reason from .api import sort_code_string as code from .api import sort_file as file from .api import sort_stream as stream diff --git a/isort/api.py b/isort/api.py index 817799807..37eb56c55 100644 --- a/isort/api.py +++ b/isort/api.py @@ -21,6 +21,8 @@ show_unified_diff, ) from .io import Empty +from .place import module as place_module +from .place import module_with_reason as place_module_with_reason from .settings import DEFAULT_CONFIG, FILE_SKIP_COMMENTS, Config CIMPORT_IDENTIFIERS = ("cimport ", "cimport*", "from.cimport") diff --git a/isort/place.py b/isort/place.py index 437e7b523..5ab5aba3b 100644 --- a/isort/place.py +++ b/isort/place.py @@ -3,7 +3,7 @@ import os from fnmatch import fnmatch from pathlib import Path -from typing import Dict, Optional +from typing import Dict, Optional, Tuple from isort import sections from isort.settings import DEFAULT_CONFIG, Config @@ -14,16 +14,21 @@ def module(name: str, config: Config = DEFAULT_CONFIG) -> str: """Returns the section placement for the given module name.""" + return module_with_reason(name, config)[0] + + +def module_with_reason(name: str, config: Config = DEFAULT_CONFIG) -> Tuple[str, str]: + """Returns the section placement for the given module name alongside the reasoning.""" return ( _forced_separate(name, config) or _local(name, config) or _known_pattern(name, config) or _src_path(name, config) - or config.default_section + or (config.default_section, "Default option in Config or universal default.") ) -def _forced_separate(name: str, config: Config) -> Optional[str]: +def _forced_separate(name: str, config: Config) -> Optional[Tuple[str, str]]: for forced_separate in config.forced_separate: # Ensure all forced_separate patterns will match to end of string path_glob = forced_separate @@ -31,34 +36,34 @@ def _forced_separate(name: str, config: Config) -> Optional[str]: path_glob = "%s*" % forced_separate if fnmatch(name, path_glob) or fnmatch(name, "." + path_glob): - return forced_separate + return (forced_separate, f"Matched forced_separate ({forced_separate}) config value.") return None -def _local(name: str, config: Config) -> Optional[str]: +def _local(name: str, config: Config) -> Optional[Tuple[str, str]]: if name.startswith("."): - return LOCAL + return (LOCAL, "Module name started with a dot.") return None -def _known_pattern(name: str, config: Config) -> Optional[str]: +def _known_pattern(name: str, config: Config) -> Optional[Tuple[str, str]]: parts = name.split(".") module_names_to_check = (".".join(parts[:first_k]) for first_k in range(len(parts), 0, -1)) for module_name_to_check in module_names_to_check: for pattern, placement in config.known_patterns: if pattern.match(module_name_to_check): - return placement + return (placement, f"Matched configured known pattern {pattern}") return None -def _src_path(name: str, config: Config) -> Optional[str]: +def _src_path(name: str, config: Config) -> Optional[Tuple[str, str]]: for src_path in config.src_paths: - module_path = (src_path / os.path.sep.join(name.split(".")[0])).resolve() + module_path = (src_path / name.split(".")[0]).resolve() if _is_module(module_path) or _is_package(module_path): - return sections.FIRSTPARTY + return (sections.FIRSTPARTY, f"Found in one of the configured src_paths: {src_path}.") return None From c8a0279d421c2837e4f7e4ef1eaf2cc9cb94210c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 20 Jun 2020 15:54:28 -0700 Subject: [PATCH 0632/1439] Update script to include empty user agent --- scripts/mkstdlibs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/mkstdlibs.py b/scripts/mkstdlibs.py index a79836bab..d28b93d20 100755 --- a/scripts/mkstdlibs.py +++ b/scripts/mkstdlibs.py @@ -4,7 +4,7 @@ URL = "https://docs.python.org/{}/objects.inv" PATH = "isort/stdlibs/py{}.py" -VERSIONS = [("2", "7"), ("3", "5"), ("3", "6"), ("3", "7"), ("3", "8")] +VERSIONS = [("2", "7"), ("3", "5"), ("3", "6"), ("3", "7"), ("3", "8"), ("3", "9")] DOCSTRING = """ File contains the standard library of Python {}. @@ -17,6 +17,7 @@ class FakeConfig: intersphinx_timeout = None tls_verify = True + user_agent = "" class FakeApp: From c78be06276b5d95f45dd406fbf6fe6f9e948caf5 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 20 Jun 2020 15:57:59 -0700 Subject: [PATCH 0633/1439] Add python 39 support --- isort/settings.py | 2 +- isort/stdlibs/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/settings.py b/isort/settings.py index 456e78956..2e372e9d6 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -179,7 +179,7 @@ def __post_init__(self): if sys.version_info.major == 2 and sys.version_info.minor <= 6: py_version = "2" elif sys.version_info.major == 3 and ( - sys.version_info.minor <= 5 or sys.version_info.minor >= 8 + sys.version_info.minor <= 5 or sys.version_info.minor >= 9 ): py_version = "3" else: diff --git a/isort/stdlibs/__init__.py b/isort/stdlibs/__init__.py index 9967ae558..9021bc455 100644 --- a/isort/stdlibs/__init__.py +++ b/isort/stdlibs/__init__.py @@ -1 +1 @@ -from . import all, py2, py3, py27, py35, py36, py37, py38 +from . import all, py2, py3, py27, py35, py36, py37, py38, py39 From 9a7f0bc841e3677a7ebd321e8d899cacfe174adc Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 21 Jun 2020 23:03:53 -0700 Subject: [PATCH 0634/1439] Improve first part import detection to handle classic non explicit paths --- isort/place.py | 13 +++++++++++-- tests/test_api.py | 1 + tests/test_comments.py | 1 + tests/test_deprecated_finders.py | 1 + tests/test_format.py | 3 ++- tests/test_io.py | 1 + tests/test_main.py | 1 + tests/test_output.py | 1 + tests/test_parse.py | 1 + tests/test_settings.py | 1 + tests/test_setuptools_command.py | 1 + tests/test_wrap_modes.py | 1 + 12 files changed, 23 insertions(+), 3 deletions(-) diff --git a/isort/place.py b/isort/place.py index 5ab5aba3b..a4120ef09 100644 --- a/isort/place.py +++ b/isort/place.py @@ -61,8 +61,13 @@ def _known_pattern(name: str, config: Config) -> Optional[Tuple[str, str]]: def _src_path(name: str, config: Config) -> Optional[Tuple[str, str]]: for src_path in config.src_paths: - module_path = (src_path / name.split(".")[0]).resolve() - if _is_module(module_path) or _is_package(module_path): + root_module_name = name.split(".")[0] + module_path = (src_path / root_module_name).resolve() + if ( + _is_module(module_path) + or _is_package(module_path) + or _src_path_is_module(src_path, root_module_name) + ): return (sections.FIRSTPARTY, f"Found in one of the configured src_paths: {src_path}.") return None @@ -81,3 +86,7 @@ def _is_module(path: Path) -> bool: def _is_package(path: Path) -> bool: return exists_case_sensitive(str(path)) and path.is_dir() + + +def _src_path_is_module(src_path: Path, module: str) -> bool: + return module == src_path.name and src_path.is_dir() and exists_case_sensitive(str(src_path)) diff --git a/tests/test_api.py b/tests/test_api.py index 2d7a35799..e4984ceb1 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -2,6 +2,7 @@ from unittest.mock import MagicMock, patch import pytest + from isort import api, exceptions from isort.settings import Config diff --git a/tests/test_comments.py b/tests/test_comments.py index 1805f137c..b1b4ed7b4 100644 --- a/tests/test_comments.py +++ b/tests/test_comments.py @@ -1,4 +1,5 @@ from hypothesis_auto import auto_pytest_magic + from isort import comments auto_pytest_magic(comments.parse) diff --git a/tests/test_deprecated_finders.py b/tests/test_deprecated_finders.py index 3318c0bfd..558f0f483 100644 --- a/tests/test_deprecated_finders.py +++ b/tests/test_deprecated_finders.py @@ -5,6 +5,7 @@ import posixpath import pytest + from isort import sections, settings from isort.deprecated import finders from isort.deprecated.finders import FindersManager diff --git a/tests/test_format.py b/tests/test_format.py index 27839a237..79b967019 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -1,9 +1,10 @@ from unittest.mock import MagicMock, patch -import isort.format import pytest from hypothesis_auto import auto_pytest_magic +import isort.format + auto_pytest_magic(isort.format.show_unified_diff, auto_allow_exceptions_=(UnicodeEncodeError,)) diff --git a/tests/test_io.py b/tests/test_io.py index 1ad967549..59a86faca 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -2,6 +2,7 @@ from unittest.mock import patch import pytest + from isort import io diff --git a/tests/test_main.py b/tests/test_main.py index c05f4ac99..b97f8928d 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -6,6 +6,7 @@ import pytest from hypothesis_auto import auto_pytest_magic + from isort import main from isort._version import __version__ from isort.settings import DEFAULT_CONFIG, Config diff --git a/tests/test_output.py b/tests/test_output.py index 2aaaf877f..2457f70d7 100644 --- a/tests/test_output.py +++ b/tests/test_output.py @@ -1,4 +1,5 @@ from hypothesis_auto import auto_pytest_magic + from isort import output auto_pytest_magic(output.with_comments, auto_allow_exceptions_=(ValueError,)) diff --git a/tests/test_parse.py b/tests/test_parse.py index 51979c9d2..f3a24d668 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -1,4 +1,5 @@ from hypothesis_auto import auto_pytest_magic + from isort import parse from isort.settings import Config diff --git a/tests/test_settings.py b/tests/test_settings.py index c7326236e..648da100d 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -2,6 +2,7 @@ from unittest.mock import MagicMock, patch import pytest + from isort import exceptions, settings from isort.settings import Config diff --git a/tests/test_setuptools_command.py b/tests/test_setuptools_command.py index cba43e437..634dd8877 100644 --- a/tests/test_setuptools_command.py +++ b/tests/test_setuptools_command.py @@ -2,6 +2,7 @@ from unittest.mock import MagicMock import pytest + from isort import setuptools_commands diff --git a/tests/test_wrap_modes.py b/tests/test_wrap_modes.py index 1cfc4bc5b..bf0d698e8 100644 --- a/tests/test_wrap_modes.py +++ b/tests/test_wrap_modes.py @@ -1,4 +1,5 @@ from hypothesis_auto import auto_pytest_magic + from isort import wrap_modes auto_pytest_magic(wrap_modes.grid, auto_allow_exceptions_=(ValueError,)) From a174eb57037254f6277a9418db407995ea9aff9c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 21 Jun 2020 23:04:10 -0700 Subject: [PATCH 0635/1439] Add python 3.9 imports --- isort/stdlibs/py39.py | 217 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 isort/stdlibs/py39.py diff --git a/isort/stdlibs/py39.py b/isort/stdlibs/py39.py new file mode 100644 index 000000000..17efc3886 --- /dev/null +++ b/isort/stdlibs/py39.py @@ -0,0 +1,217 @@ +""" +File contains the standard library of Python 3.9. + +DO NOT EDIT. If the standard library changes, a new list should be created +using the mkstdlibs.py script. +""" + +stdlib = { + "_thread", + "abc", + "aifc", + "argparse", + "array", + "ast", + "asynchat", + "asyncio", + "asyncore", + "atexit", + "audioop", + "base64", + "bdb", + "binascii", + "binhex", + "bisect", + "builtins", + "bz2", + "cProfile", + "calendar", + "cgi", + "cgitb", + "chunk", + "cmath", + "cmd", + "code", + "codecs", + "codeop", + "collections", + "colorsys", + "compileall", + "concurrent", + "configparser", + "contextlib", + "contextvars", + "copy", + "copyreg", + "crypt", + "csv", + "ctypes", + "curses", + "dataclasses", + "datetime", + "dbm", + "decimal", + "difflib", + "dis", + "distutils", + "doctest", + "email", + "encodings", + "ensurepip", + "enum", + "errno", + "faulthandler", + "fcntl", + "filecmp", + "fileinput", + "fnmatch", + "formatter", + "fractions", + "ftplib", + "functools", + "gc", + "getopt", + "getpass", + "gettext", + "glob", + "graphlib", + "grp", + "gzip", + "hashlib", + "heapq", + "hmac", + "html", + "http", + "imaplib", + "imghdr", + "imp", + "importlib", + "inspect", + "io", + "ipaddress", + "itertools", + "json", + "keyword", + "lib2to3", + "linecache", + "locale", + "logging", + "lzma", + "mailbox", + "mailcap", + "marshal", + "math", + "mimetypes", + "mmap", + "modulefinder", + "msilib", + "msvcrt", + "multiprocessing", + "netrc", + "nis", + "nntplib", + "numbers", + "operator", + "optparse", + "os", + "ossaudiodev", + "parser", + "pathlib", + "pdb", + "pickle", + "pickletools", + "pipes", + "pkgutil", + "platform", + "plistlib", + "poplib", + "posix", + "pprint", + "profile", + "pstats", + "pty", + "pwd", + "py_compile", + "pyclbr", + "pydoc", + "queue", + "quopri", + "random", + "re", + "readline", + "reprlib", + "resource", + "rlcompleter", + "runpy", + "sched", + "secrets", + "select", + "selectors", + "shelve", + "shlex", + "shutil", + "signal", + "site", + "smtpd", + "smtplib", + "sndhdr", + "socket", + "socketserver", + "spwd", + "sqlite3", + "ssl", + "stat", + "statistics", + "string", + "stringprep", + "struct", + "subprocess", + "sunau", + "symbol", + "symtable", + "sys", + "sysconfig", + "syslog", + "tabnanny", + "tarfile", + "telnetlib", + "tempfile", + "termios", + "test", + "textwrap", + "threading", + "time", + "timeit", + "tkinter", + "token", + "tokenize", + "trace", + "traceback", + "tracemalloc", + "tty", + "turtle", + "turtledemo", + "types", + "typing", + "unicodedata", + "unittest", + "urllib", + "uu", + "uuid", + "venv", + "warnings", + "wave", + "weakref", + "webbrowser", + "winreg", + "winsound", + "wsgiref", + "xdrlib", + "xml", + "xmlrpc", + "zipapp", + "zipfile", + "zipimport", + "zlib", + "zoneinfo", +} From d5183aa830cae52b25f45079b9816375e1ded898 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 21 Jun 2020 23:23:10 -0700 Subject: [PATCH 0636/1439] Add test for module placement --- tests/conftest.py | 11 +++++++++++ tests/test_place.py | 14 ++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 tests/test_place.py diff --git a/tests/conftest.py b/tests/conftest.py index 60764f054..3b778a700 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,6 @@ """isort test wide fixtures and configuration""" import os +from pathlib import Path import pytest @@ -15,3 +16,13 @@ def test_dir(): @pytest.fixture def src_dir(): return SRC_DIR + + +@pytest.fixture +def test_path(): + return Path(TEST_DIR).resolve() + + +@pytest.fixture +def src_path(): + return Path(SRC_DIR).resolve() diff --git a/tests/test_place.py b/tests/test_place.py new file mode 100644 index 000000000..50d3aee3f --- /dev/null +++ b/tests/test_place.py @@ -0,0 +1,14 @@ +"""Tests for the isort import placement module""" +from functools import partial + +from isort import place, sections +from isort.settings import Config + + +def test_module(src_path): + place_tester = partial(place.module, config=Config(src_paths=[src_path])) + assert place_tester("isort") == sections.FIRSTPARTY + assert place_tester("os") == sections.STDLIB + assert place_tester(".deprecated") == sections.LOCALFOLDER + assert place_tester("__future__") == sections.FUTURE + assert place_tester("hug") == sections.THIRDPARTY From 597172f2ac402f81aee1034fd1244c8ac058b483 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 21 Jun 2020 23:58:47 -0700 Subject: [PATCH 0637/1439] Ensure src_paths in config are set relative to config location --- isort/settings.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/isort/settings.py b/isort/settings.py index 2e372e9d6..b52ac9794 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -86,6 +86,8 @@ THIRDPARTY: "THIRD_PARTY", } +RUNTIME_SOURCE = "runtime" + @dataclass(frozen=True) class _Config: @@ -257,7 +259,7 @@ def __init__( if config_settings: sources.append(config_settings) if config_overrides: - config_overrides["source"] = "runtime" + config_overrides["source"] = RUNTIME_SOURCE sources.append(config_overrides) combined_config = {**profile, **config_settings, **config_overrides} @@ -297,6 +299,19 @@ def __init__( config_settings.get("source", None) or os.getcwd() ) + + if "src_paths" not in combined_config: + combined_config["src_paths"] = frozenset((Path.cwd().resolve(),)) + else: + if combined_config.get("source", RUNTIME_SOURCE) == RUNTIME_SOURCE: + path_root = Path.cwd().resolve() + else: + path_root = Path(combined_config["source"]).resolve().parent + + combined_config["src_paths"] = frozenset( + path_root / path for path in combined_config["src_paths"] + ) + # Remove any config values that are used for creating config object but # aren't defined in dataclass combined_config.pop("source", None) @@ -310,13 +325,6 @@ def __init__( combined_config.pop(f"{IMPORT_HEADING_PREFIX}{import_heading_key}") combined_config["import_headings"] = import_headings - if "src_paths" not in combined_config: - combined_config["src_paths"] = frozenset((Path.cwd().resolve(),)) - else: - combined_config["src_paths"] = frozenset( - path.resolve() for path in combined_config["src_paths"] - ) - self._known_patterns: Optional[List[Tuple[Pattern[str], str]]] = None super().__init__(sources=tuple(sources), **combined_config) # type: ignore From 09ad2764c021daf543a779b6d8de69be77fbef86 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 22 Jun 2020 22:34:30 -0700 Subject: [PATCH 0638/1439] Ensure runtime_src_path can work with config src_paths --- .isort.cfg | 1 + isort/settings.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.isort.cfg b/.isort.cfg index b3b28cbc2..428419bc1 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -1,2 +1,3 @@ [settings] profile=hug +src_paths=isort,test diff --git a/isort/settings.py b/isort/settings.py index b52ac9794..f289710ab 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -260,6 +260,7 @@ def __init__( sources.append(config_settings) if config_overrides: config_overrides["source"] = RUNTIME_SOURCE + config_overrides["runtime_src_paths"] = config_overrides.pop("src_paths", ()) sources.append(config_overrides) combined_config = {**profile, **config_settings, **config_overrides} @@ -299,23 +300,22 @@ def __init__( config_settings.get("source", None) or os.getcwd() ) - - if "src_paths" not in combined_config: + if "src_paths" not in combined_config and not combined_config.get("runtime_src_paths"): combined_config["src_paths"] = frozenset((Path.cwd().resolve(),)) else: - if combined_config.get("source", RUNTIME_SOURCE) == RUNTIME_SOURCE: - path_root = Path.cwd().resolve() - else: - path_root = Path(combined_config["source"]).resolve().parent - + path_root = Path(combined_config.get("directory", Path.cwd())).resolve() combined_config["src_paths"] = frozenset( - path_root / path for path in combined_config["src_paths"] + {path_root / path for path in combined_config.get("src_paths", ())}.union( + Path.cwd().resolve() / path + for path in combined_config.get("runtime_src_paths", ()) + ) ) # Remove any config values that are used for creating config object but # aren't defined in dataclass combined_config.pop("source", None) combined_config.pop("sources", None) + combined_config.pop("runtime_src_paths", None) if known_other: for known_key in known_other: combined_config.pop(f"{KNOWN_PREFIX}{known_key}", None) From 6aab314b9033591d05ab2c2f0925931fac68a617 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 22 Jun 2020 22:40:58 -0700 Subject: [PATCH 0639/1439] Ensure path_root is a directory --- isort/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/isort/settings.py b/isort/settings.py index f289710ab..dc9552925 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -304,6 +304,7 @@ def __init__( combined_config["src_paths"] = frozenset((Path.cwd().resolve(),)) else: path_root = Path(combined_config.get("directory", Path.cwd())).resolve() + path_root = path_root if path_root.is_dir() else path_root.parent combined_config["src_paths"] = frozenset( {path_root / path for path in combined_config.get("src_paths", ())}.union( Path.cwd().resolve() / path From 78d6d275475adc25fae539c76331b3590360e130 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 23 Jun 2020 23:19:13 -0700 Subject: [PATCH 0640/1439] Test untested areas of deprecated finders, back to 100% test coverage --- tests/test_isort.py | 49 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/tests/test_isort.py b/tests/test_isort.py index 58a03ba17..9c5fc5d4c 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -856,6 +856,9 @@ def test_explicitly_local_import() -> None: assert api.sort_code_string(test_input) == ( "import lib1\nimport lib2\n\nimport .lib6\nfrom . import lib7\n" ) + assert api.sort_code_string(test_input, old_finders=True) == ( + "import lib1\nimport lib2\n\nimport .lib6\nfrom . import lib7\n" + ) def test_quotes_in_file() -> None: @@ -926,6 +929,17 @@ def test_forced_separate() -> None: ) == test_input ) + assert ( + api.sort_code_string( + code=test_input, + forced_separate=["django.contrib"], + known_third_party=["django"], + line_length=120, + order_by_type=False, + old_finders=True, + ) + == test_input + ) test_input = "from .foo import bar\n\nfrom .y import ca\n" assert ( @@ -934,6 +948,16 @@ def test_forced_separate() -> None: ) == test_input ) + assert ( + api.sort_code_string( + code=test_input, + forced_separate=[".y"], + line_length=120, + order_by_type=False, + old_finders=True, + ) + == test_input + ) def test_default_section() -> None: @@ -997,13 +1021,23 @@ def test_known_pattern_path_expansion() -> None: default_section="THIRDPARTY", known_first_party=["./", "this", "kate_plugin", "isort"], ) - assert test_output == ( - "import os\n" - "import sys\n" - "\n" - "import isort.settings\n" - "import this\n" - "from kate_plugin import isort_plugin\n" + test_output_old_finder = api.sort_code_string( + code=test_input, + default_section="FIRSTPARTY", + old_finders=True, + known_first_party=["./", "this", "kate_plugin", "isort"], + ) + assert ( + test_output_old_finder + == test_output + == ( + "import os\n" + "import sys\n" + "\n" + "import isort.settings\n" + "import this\n" + "from kate_plugin import isort_plugin\n" + ) ) @@ -2470,6 +2504,7 @@ def test_sys_path_mutation(tmpdir) -> None: expected_length = len(sys.path) api.sort_code_string(test_input, **options) assert len(sys.path) == expected_length + api.sort_code_string(test_input, old_finders=True, **options) def test_long_single_line() -> None: From a4dea5fa72907ffefb21e40ef194f274c86f847a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 24 Jun 2020 22:37:40 -0700 Subject: [PATCH 0641/1439] Ensure posixpath is included in stdlib across versions --- isort/stdlibs/py27.py | 1 + isort/stdlibs/py35.py | 1 + isort/stdlibs/py36.py | 1 + isort/stdlibs/py37.py | 1 + isort/stdlibs/py38.py | 1 + isort/stdlibs/py39.py | 1 + scripts/mkstdlibs.py | 2 +- 7 files changed, 7 insertions(+), 1 deletion(-) diff --git a/isort/stdlibs/py27.py b/isort/stdlibs/py27.py index ee0c616ae..0daca7fe0 100644 --- a/isort/stdlibs/py27.py +++ b/isort/stdlibs/py27.py @@ -202,6 +202,7 @@ "poplib", "posix", "posixfile", + "posixpath", "pprint", "profile", "pstats", diff --git a/isort/stdlibs/py35.py b/isort/stdlibs/py35.py index df4707b0a..75d5768a5 100644 --- a/isort/stdlibs/py35.py +++ b/isort/stdlibs/py35.py @@ -127,6 +127,7 @@ "plistlib", "poplib", "posix", + "posixpath", "pprint", "profile", "pstats", diff --git a/isort/stdlibs/py36.py b/isort/stdlibs/py36.py index e9cd206ba..b2318eda2 100644 --- a/isort/stdlibs/py36.py +++ b/isort/stdlibs/py36.py @@ -127,6 +127,7 @@ "plistlib", "poplib", "posix", + "posixpath", "pprint", "profile", "pstats", diff --git a/isort/stdlibs/py37.py b/isort/stdlibs/py37.py index 29f3f9527..a251bf967 100644 --- a/isort/stdlibs/py37.py +++ b/isort/stdlibs/py37.py @@ -128,6 +128,7 @@ "plistlib", "poplib", "posix", + "posixpath", "pprint", "profile", "pstats", diff --git a/isort/stdlibs/py38.py b/isort/stdlibs/py38.py index 69d9ec21c..cab8825d5 100644 --- a/isort/stdlibs/py38.py +++ b/isort/stdlibs/py38.py @@ -127,6 +127,7 @@ "plistlib", "poplib", "posix", + "posixpath", "pprint", "profile", "pstats", diff --git a/isort/stdlibs/py39.py b/isort/stdlibs/py39.py index 17efc3886..9b443df52 100644 --- a/isort/stdlibs/py39.py +++ b/isort/stdlibs/py39.py @@ -126,6 +126,7 @@ "plistlib", "poplib", "posix", + "posixpath", "pprint", "profile", "pstats", diff --git a/scripts/mkstdlibs.py b/scripts/mkstdlibs.py index d28b93d20..f20aac34e 100755 --- a/scripts/mkstdlibs.py +++ b/scripts/mkstdlibs.py @@ -30,7 +30,7 @@ class FakeApp: url = URL.format(version) invdata = fetch_inventory(FakeApp(), "", url) - modules = set() + modules = {"posixpath"} # Any modules we want to enforce across Python versions stdlibs for module in invdata["py:module"]: root, *_ = module.split(".") if root not in ["__future__", "__main__"]: From 6cdc628e92667b9b3cb6f64959bbc8d1a5c527c0 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 25 Jun 2020 23:06:18 -0700 Subject: [PATCH 0642/1439] Add missing stdlib modules --- isort/settings.py | 3 +-- isort/stdlibs/py27.py | 2 ++ isort/stdlibs/py35.py | 2 ++ isort/stdlibs/py36.py | 2 ++ isort/stdlibs/py37.py | 2 ++ isort/stdlibs/py38.py | 2 ++ isort/stdlibs/py39.py | 2 ++ scripts/mkstdlibs.py | 3 ++- tests/test_deprecated_finders.py | 2 +- 9 files changed, 16 insertions(+), 4 deletions(-) diff --git a/isort/settings.py b/isort/settings.py index dc9552925..ec5c468b2 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -8,6 +8,7 @@ import configparser import fnmatch import os +import posixpath import re import sys import warnings @@ -17,8 +18,6 @@ from typing import Any, Callable, Dict, FrozenSet, Iterable, List, Optional, Pattern, Set, Tuple from warnings import warn -import posixpath - from . import stdlibs from ._future import dataclass, field from .exceptions import ProfileDoesNotExist diff --git a/isort/stdlibs/py27.py b/isort/stdlibs/py27.py index 0daca7fe0..4bd8d9fab 100644 --- a/isort/stdlibs/py27.py +++ b/isort/stdlibs/py27.py @@ -185,6 +185,7 @@ "new", "nis", "nntplib", + "ntpath", "numbers", "operator", "optparse", @@ -237,6 +238,7 @@ "socket", "spwd", "sqlite3", + "sre_constants", "ssl", "stat", "statvfs", diff --git a/isort/stdlibs/py35.py b/isort/stdlibs/py35.py index 75d5768a5..9055b97b0 100644 --- a/isort/stdlibs/py35.py +++ b/isort/stdlibs/py35.py @@ -111,6 +111,7 @@ "netrc", "nis", "nntplib", + "ntpath", "numbers", "operator", "optparse", @@ -160,6 +161,7 @@ "socketserver", "spwd", "sqlite3", + "sre_constants", "ssl", "stat", "statistics", diff --git a/isort/stdlibs/py36.py b/isort/stdlibs/py36.py index b2318eda2..215460e0e 100644 --- a/isort/stdlibs/py36.py +++ b/isort/stdlibs/py36.py @@ -111,6 +111,7 @@ "netrc", "nis", "nntplib", + "ntpath", "numbers", "operator", "optparse", @@ -161,6 +162,7 @@ "socketserver", "spwd", "sqlite3", + "sre_constants", "ssl", "stat", "statistics", diff --git a/isort/stdlibs/py37.py b/isort/stdlibs/py37.py index a251bf967..c65a954f1 100644 --- a/isort/stdlibs/py37.py +++ b/isort/stdlibs/py37.py @@ -112,6 +112,7 @@ "netrc", "nis", "nntplib", + "ntpath", "numbers", "operator", "optparse", @@ -162,6 +163,7 @@ "socketserver", "spwd", "sqlite3", + "sre_constants", "ssl", "stat", "statistics", diff --git a/isort/stdlibs/py38.py b/isort/stdlibs/py38.py index cab8825d5..727258eff 100644 --- a/isort/stdlibs/py38.py +++ b/isort/stdlibs/py38.py @@ -111,6 +111,7 @@ "netrc", "nis", "nntplib", + "ntpath", "numbers", "operator", "optparse", @@ -161,6 +162,7 @@ "socketserver", "spwd", "sqlite3", + "sre_constants", "ssl", "stat", "statistics", diff --git a/isort/stdlibs/py39.py b/isort/stdlibs/py39.py index 9b443df52..20b1c00b6 100644 --- a/isort/stdlibs/py39.py +++ b/isort/stdlibs/py39.py @@ -110,6 +110,7 @@ "netrc", "nis", "nntplib", + "ntpath", "numbers", "operator", "optparse", @@ -160,6 +161,7 @@ "socketserver", "spwd", "sqlite3", + "sre_constants", "ssl", "stat", "statistics", diff --git a/scripts/mkstdlibs.py b/scripts/mkstdlibs.py index f20aac34e..1d6ff61ef 100755 --- a/scripts/mkstdlibs.py +++ b/scripts/mkstdlibs.py @@ -30,7 +30,8 @@ class FakeApp: url = URL.format(version) invdata = fetch_inventory(FakeApp(), "", url) - modules = {"posixpath"} # Any modules we want to enforce across Python versions stdlibs + # Any modules we want to enforce across Python versions stdlib can be included in set init + modules = {"posixpath", "ntpath", "sre_constants"} for module in invdata["py:module"]: root, *_ = module.split(".") if root not in ["__future__", "__main__"]: diff --git a/tests/test_deprecated_finders.py b/tests/test_deprecated_finders.py index 558f0f483..614e3317f 100644 --- a/tests/test_deprecated_finders.py +++ b/tests/test_deprecated_finders.py @@ -1,9 +1,9 @@ import importlib.machinery import os +import posixpath from pathlib import Path from unittest.mock import patch -import posixpath import pytest from isort import sections, settings From dbd543de4815e49a6e0267eebc0665151d914382 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 26 Jun 2020 01:08:06 -0700 Subject: [PATCH 0643/1439] Add initial isort wheel --- docs/interactive/isort-5.0.0-py3-none-any.whl | Bin 0 -> 72682 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/interactive/isort-5.0.0-py3-none-any.whl diff --git a/docs/interactive/isort-5.0.0-py3-none-any.whl b/docs/interactive/isort-5.0.0-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..7c1d2fbe7891d846ef40bafdef9e1369be560b72 GIT binary patch literal 72682 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 literal 0 HcmV?d00001 From 5dfc9682c07238bccee240f185b16b3dacd141ea Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 26 Jun 2020 01:36:16 -0700 Subject: [PATCH 0644/1439] Add interactive isort --- README.md | 26 +++++++-------- docs/interactive/interactive.css | 26 +++++++++++++++ docs/interactive/interactive.js | 56 ++++++++++++++++++++++++++++++++ docs/interactive/try.md | 48 +++++++++++++++++++++++++++ 4 files changed, 143 insertions(+), 13 deletions(-) create mode 100644 docs/interactive/interactive.css create mode 100644 docs/interactive/interactive.js create mode 100644 docs/interactive/try.md diff --git a/README.md b/README.md index 4df3c38e1..ec44e29bb 100644 --- a/README.md +++ b/README.md @@ -25,19 +25,7 @@ editors](https://github.com/timothycrosley/isort/wiki/isort-Plugins) to quickly sort all your imports. It requires Python 3.6+ to run but supports formatting Python 2 code too. ------------------------------------------------------------------------- - -[Get professionally supported isort with the Tidelift -Subscription](https://tidelift.com/subscription/pkg/pypi-isort?utm_source=pypi-isort&utm_medium=referral&utm_campaign=readme) - -Professional support for isort is available as part of the [Tidelift -Subscription](https://tidelift.com/subscription/pkg/pypi-isort?utm_source=pypi-isort&utm_medium=referral&utm_campaign=readme). -Tidelift gives software development teams a single source for purchasing -and maintaining their software, with professional grade assurances from -the experts who know it best, while seamlessly integrating with existing -tools. - ------------------------------------------------------------------------- +[Try isort now from your browser!](/docs/interactive/try) ![Example Usage](https://raw.github.com/timothycrosley/isort/develop/example.gif) @@ -676,6 +664,18 @@ I was given permission to open source sortImports and here we are :) ------------------------------------------------------------------------ +[Get professionally supported isort with the Tidelift +Subscription](https://tidelift.com/subscription/pkg/pypi-isort?utm_source=pypi-isort&utm_medium=referral&utm_campaign=readme) + +Professional support for isort is available as part of the [Tidelift +Subscription](https://tidelift.com/subscription/pkg/pypi-isort?utm_source=pypi-isort&utm_medium=referral&utm_campaign=readme). +Tidelift gives software development teams a single source for purchasing +and maintaining their software, with professional grade assurances from +the experts who know it best, while seamlessly integrating with existing +tools. + +------------------------------------------------------------------------ + Thanks and I hope you find isort useful! ~Timothy Crosley diff --git a/docs/interactive/interactive.css b/docs/interactive/interactive.css new file mode 100644 index 000000000..8e72565e2 --- /dev/null +++ b/docs/interactive/interactive.css @@ -0,0 +1,26 @@ +.editor { + height: 400px; + width: 49.7%; + display: inline-block; +} +.configurator { + height: 200px; + width: 100%; + clear: both; + float: left; +} +#liveTester { + background: black; + color: white; +} +#sideBySide { + white-space: nowrap; +} +#liveTester.fullscreen { + position: absolute; + top: 0px; + left: 0px; + right: 0px; + z-index: 99; + bottom: 0px; +} diff --git a/docs/interactive/interactive.js b/docs/interactive/interactive.js new file mode 100644 index 000000000..167b8437e --- /dev/null +++ b/docs/interactive/interactive.js @@ -0,0 +1,56 @@ +window.addEventListener('load', () => { + var input = ace.edit("inputEditor"); + var output = ace.edit("outputEditor"); + var configurator = ace.edit("configEditor"); + + [input, output, configurator].forEach((editor) => { + editor.setTheme("ace/theme/monokai"); + editor.session.setMode("ace/mode/python"); + editor.resize(); + }); + + configurator.session.setMode("ace/mode/json"); + + function updateOutput() { + output.setValue(document.sort_code(input.getValue(), configurator.getValue())); + } + + output.setReadOnly(true); + input.session.on('change', updateOutput); + configurator.session.on('change', updateOutput); + + document.updateOutput = updateOutput; +}); + + +languagePluginLoader.then(() => { + return pyodide.loadPackage(['micropip']) +}).then(() => { + console.log(pyodide.runPython('import sys\nsys.version')); + console.log(pyodide.runPython('print(1 + 2)')); + pyodide.runPython(` +import micropip + +from js import document + +def use_isort(*args): + import isort + import json + import textwrap + print(isort.code("import b; import a")) + + def sort_code(code, configuration): + try: + configuration = json.loads(configuration or "{}") + except Exception as configuration_error: + return "\\n".join(textwrap.wrap(f"Exception thrown trying to read given configuration {configuration_error}", 40)) + try: + return isort.code(code, **configuration) + except Exception as isort_error: + return "\\n".join(textwrap.wrap(f"Exception thrown trying to sort given imports {isort_error}", 40)) + + document.sort_code = sort_code + document.updateOutput() + +micropip.install('https://github.com/timothycrosley/isort/raw/develop/docs/interactive/isort-5.0.0-py3-none-any.whl').then(use_isort)`); +}); diff --git a/docs/interactive/try.md b/docs/interactive/try.md new file mode 100644 index 000000000..9ce5edd6c --- /dev/null +++ b/docs/interactive/try.md @@ -0,0 +1,48 @@ +Try isort from your browser! +======== + +Use our live isort editor to see how isort can help improve the formatting of your Python imports. + +!!! important - "Safe to use. No code is transmitted." + The below live isort tester doesn't transmit any of the code you paste to our server or anyone else's. Instead, this page runs a complete Python3 installation with isort installed entirely within your browser. To accomplish this, it utilizes the [pyodide](https://github.com/iodide-project/pyodide) project. + + + + + + + + + +

+ + +
+Like what you saw? Installing isort to use locally is as simple as `pip3 install isort`. From e3f5300b8be2f293957f0573247f14dce6139aa0 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 26 Jun 2020 01:43:50 -0700 Subject: [PATCH 0645/1439] Fix links --- README.md | 2 +- docs/interactive/interactive.js | 2 +- docs/interactive/try.md | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ec44e29bb..5fa6d6f8b 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ editors](https://github.com/timothycrosley/isort/wiki/isort-Plugins) to quickly sort all your imports. It requires Python 3.6+ to run but supports formatting Python 2 code too. -[Try isort now from your browser!](/docs/interactive/try) +[Try isort now from your browser!](https://timothycrosley.github.io/isort/docs/interactive/try/) ![Example Usage](https://raw.github.com/timothycrosley/isort/develop/example.gif) diff --git a/docs/interactive/interactive.js b/docs/interactive/interactive.js index 167b8437e..3144b968b 100644 --- a/docs/interactive/interactive.js +++ b/docs/interactive/interactive.js @@ -52,5 +52,5 @@ def use_isort(*args): document.sort_code = sort_code document.updateOutput() -micropip.install('https://github.com/timothycrosley/isort/raw/develop/docs/interactive/isort-5.0.0-py3-none-any.whl').then(use_isort)`); +micropip.install('https://timothycrosley.github.io/isort/docs/interactive/isort-5.0.0-py3-none-any.whl').then(use_isort)`); }); diff --git a/docs/interactive/try.md b/docs/interactive/try.md index 9ce5edd6c..d82f53b89 100644 --- a/docs/interactive/try.md +++ b/docs/interactive/try.md @@ -13,7 +13,7 @@ Use our live isort editor to see how isort can help improve the formatting of yo - + @@ -43,6 +43,6 @@ import b, a - +
Like what you saw? Installing isort to use locally is as simple as `pip3 install isort`. From a26a58423b183217d0defdb6b67e5fd285be7186 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 26 Jun 2020 22:35:30 -0700 Subject: [PATCH 0646/1439] Fix issue #1241: setting src paths from command line leading to exception --- isort/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/main.py b/isort/main.py index 1265e7a44..44f01b37d 100644 --- a/isort/main.py +++ b/isort/main.py @@ -137,7 +137,7 @@ def _build_arg_parser() -> argparse.ArgumentParser: parser.add_argument( "--src", "--src-path", - dest="source_paths", + dest="src_paths", action="append", help="Add an explicitly defined source path " "(modules within src paths have their imports automatically catorgorized as first_party).", @@ -637,7 +637,7 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = show_diff = config_dict.pop("show_diff", False) write_to_stdout = config_dict.pop("write_to_stdout", False) - src_paths = config_dict.setdefault("src_paths", set()) + src_paths = set(config_dict.setdefault("src_paths", ())) for file_name in file_names: if os.path.isdir(file_name): src_paths.add(Path(file_name).resolve()) From 837f6624ae4325c8015fe6a7501b795bad529cb0 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 27 Jun 2020 02:51:48 -0700 Subject: [PATCH 0647/1439] Fix how src_paths are auto determined (only by dir passed in, manually passed in, or isort cwd) add an LRU cache to module placement with a max size of 1000 placed module names --- isort/main.py | 4 +--- isort/place.py | 2 ++ isort/settings.py | 3 +++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/isort/main.py b/isort/main.py index 44f01b37d..0d565d3b0 100644 --- a/isort/main.py +++ b/isort/main.py @@ -637,12 +637,10 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = show_diff = config_dict.pop("show_diff", False) write_to_stdout = config_dict.pop("write_to_stdout", False) - src_paths = set(config_dict.setdefault("src_paths", ())) + src_paths = config_dict["src_paths"] = set(config_dict.get("src_paths", ())) for file_name in file_names: if os.path.isdir(file_name): src_paths.add(Path(file_name).resolve()) - else: - src_paths.add(Path(file_name).parent.resolve()) config = Config(**config_dict) if show_config: diff --git a/isort/place.py b/isort/place.py index a4120ef09..aeced034f 100644 --- a/isort/place.py +++ b/isort/place.py @@ -2,6 +2,7 @@ import importlib import os from fnmatch import fnmatch +from functools import lru_cache from pathlib import Path from typing import Dict, Optional, Tuple @@ -17,6 +18,7 @@ def module(name: str, config: Config = DEFAULT_CONFIG) -> str: return module_with_reason(name, config)[0] +@lru_cache(maxsize=1000) def module_with_reason(name: str, config: Config = DEFAULT_CONFIG) -> Tuple[str, str]: """Returns the section placement for the given module name alongside the reasoning.""" return ( diff --git a/isort/settings.py b/isort/settings.py index ec5c468b2..d81d5531c 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -212,6 +212,9 @@ def __post_init__(self): f"{self.wrap_length} > {self.line_length}." ) + def __hash__(self): + return id(self) + _DEFAULT_SETTINGS = {**vars(_Config()), "source": "defaults"} From 187b59889c1be0839437cc78d7c8b9be23e7c130 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 27 Jun 2020 11:38:17 -0700 Subject: [PATCH 0648/1439] Fix configuration options link --- docs/interactive/try.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/interactive/try.md b/docs/interactive/try.md index d82f53b89..af6bbe792 100644 --- a/docs/interactive/try.md +++ b/docs/interactive/try.md @@ -32,7 +32,7 @@ import b, a
Loading...
- Configuration (Note: the below must follow JSON format). Full configuration guide is here: + Configuration (Note: the below must follow JSON format). Full configuration guide is here:
{"line_length": 80, "multi_line_output": 2, From 5e60bd868bed2640ae56817a71b1b10f935101b8 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 28 Jun 2020 22:26:15 -0700 Subject: [PATCH 0649/1439] Add 3.9 to officially supported versions --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 2c91b9c1f..fa664d084 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ classifiers = [ "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", From aa8b8b037eb9560d9c8031fe3df623e8ad713c5f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 28 Jun 2020 22:39:02 -0700 Subject: [PATCH 0650/1439] Fix issue #1238: Add extra-builtin option --- docs/configuration/options.md | 34 +++++++++++++++++----------------- isort/main.py | 8 +++++++- isort/settings.py | 9 +++++---- tests/test_place.py | 7 +++++++ 4 files changed, 36 insertions(+), 22 deletions(-) diff --git a/docs/configuration/options.md b/docs/configuration/options.md index 6d836c4ce..dfd22a7f9 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -32,7 +32,7 @@ Force specific imports to the top of their appropriate section. Files that sort imports should skip over. If you want to skip multiple files you should specify twice: --skip file1 --skip file2. **Type:** Frozenset -**Default:** `frozenset({'.nox', 'node_modules', 'buck-out', '.venv', '.mypy_cache', 'build', '.pants.d', '.hg', 'dist', '.git', '_build', '.eggs', 'venv', '.tox'})` +**Default:** `frozenset({'.git', '.eggs', '.hg', '.mypy_cache', 'buck-out', 'dist', '.pants.d', '.venv', '.nox', 'build', '.tox', '_build', 'node_modules', 'venv'})` **Python & Config File Name:** skip **CLI Flags:** @@ -141,16 +141,26 @@ Force isort to recognize a module as being part of the current python project. - --project ## Known Standard Library -Force isort to recognize a module as part of the python standard library. +Force isort to recognize a module as part of Python's standard library. **Type:** Frozenset -**Default:** `frozenset({'multiprocessing', 'codeop', 'pprint', 'chunk', 'msilib', 'concurrent', 'sched', 'random', 'webbrowser', 'socketserver', 'nis', 'tracemalloc', 'filecmp', 'formatter', 'codecs', 'winsound', 'tty', 'xml', 'gzip', 'binascii', 'enum', 'symtable', '_thread', 'xmlrpc', 'io', 'timeit', 'typing', 'datetime', 'sysconfig', 'ipaddress', 'traceback', 'zipfile', 'audioop', 'mimetypes', 'poplib', 'glob', 'weakref', 'pdb', 'locale', 'queue', 'collections', '_dummy_thread', 'trace', 'linecache', 'code', 'wave', 'reprlib', 'zipimport', 'posix', 'select', 'time', 'sqlite3', 'cgitb', 'ast', 'imp', 'shutil', 'resource', 'decimal', 'itertools', 'json', 'curses', 'pkgutil', 'contextvars', 'bdb', 'dis', 'pstats', 'copyreg', 'distutils', 'marshal', 'winreg', 'faulthandler', 'nntplib', 'ssl', 'struct', 'functools', 'pydoc', 'stat', 'asyncore', 'runpy', 'token', 'ossaudiodev', 'shlex', 'urllib', 'cgi', 'imghdr', 'netrc', 'operator', 'py_compile', 'unittest', 'sunau', 'tempfile', 'textwrap', 'dbm', 'calendar', 'cmath', 'selectors', 'dataclasses', 'pickletools', 'macpath', 'pipes', 'pyclbr', 'asyncio', 'copy', 'spwd', 'heapq', 'statistics', 'venv', 'optparse', 'string', 'encodings', 'fnmatch', 'ftplib', 'bisect', 'fractions', 'msvcrt', 'html', 'binhex', 'crypt', 'colorsys', 'telnetlib', 'smtplib', 'ctypes', 'stringprep', 'fpectl', 'warnings', 'atexit', 'signal', 'subprocess', 'aifc', 'getpass', 'ensurepip', 'imaplib', 'gc', 'profile', 'parser', 'math', 'errno', 'numbers', 'readline', 'argparse', 'mailbox', 'smtpd', 'zlib', 'os', 'platform', 'tarfile', 'tkinter', 'configparser', 'plistlib', 'keyword', 'pathlib', 'mailcap', 'lzma', 'cProfile', 'sndhdr', 'secrets', 'mmap', 'array', 'pickle', 'syslog', 'threading', 'xdrlib', 'http', 'socket', 'tabnanny', 'email', 'rlcompleter', 'logging', 'inspect', 'test', 'quopri', 'difflib', 'bz2', 'types', 'csv', 'gettext', 'dummy_threading', 'symbol', 'turtledemo', 'compileall', 'hmac', 'doctest', 'unicodedata', 'turtle', 'uuid', 'sys', 'termios', 'modulefinder', 'contextlib', 'tokenize', 'zipapp', 're', 'cmd', 'uu', 'base64', 'fileinput', 'wsgiref', 'getopt', 'grp', 'lib2to3', 'pwd', 'site', 'asynchat', 'shelve', 'pty', 'builtins', 'fcntl', 'importlib', 'abc', 'hashlib'})` +**Default:** `frozenset({'distutils', 'reprlib', 'site', 'importlib', 'cmath', 'symbol', 'zipfile', 'math', 'weakref', 'configparser', 'array', 'cgi', 'io', 'stringprep', 'ensurepip', 'posixpath', 'turtledemo', 'cProfile', 'heapq', 'tty', 'doctest', 'macpath', 'spwd', 'sched', 'csv', 'atexit', 'tarfile', 'dbm', 'wave', 'http', 'sunau', 'resource', 'pdb', 'turtle', 'socket', 'html', 'runpy', 'colorsys', 'audioop', 'select', 'argparse', 'gettext', 'random', 'xmlrpc', 'json', 'types', 'ntpath', 'collections', 'poplib', 'mimetypes', 'copyreg', 'rlcompleter', 'warnings', 'lib2to3', 'tkinter', 'functools', 'signal', 'calendar', 'termios', 'wsgiref', 'difflib', 'fractions', 'logging', 'ossaudiodev', 'sndhdr', 'fnmatch', '_dummy_thread', 'numbers', 'typing', 'nntplib', 'email', 'imghdr', 'urllib', 'fcntl', 'msilib', 'multiprocessing', 'uuid', 'operator', 'pickle', 'mailbox', 'getpass', 'platform', 'getopt', 'marshal', 'sqlite3', 'ftplib', 'pwd', 'abc', 'codecs', 'optparse', 'smtplib', 'keyword', 'datetime', 'pathlib', 'curses', 'unicodedata', 'pprint', 'codeop', 'ast', 'concurrent', 'tracemalloc', 'xdrlib', 'imaplib', 'gzip', 'gc', 'shelve', 'hashlib', 'fileinput', 'statistics', 'copy', 'smtpd', 'token', 'zipimport', 'fpectl', 'builtins', 'py_compile', 'binascii', 'dataclasses', 'os', 'pickletools', 'lzma', 'netrc', 'cmd', 'contextvars', 'pydoc', 'sre_constants', 'pty', 'binhex', 'bdb', 'glob', 'aifc', 'errno', 'xml', 'decimal', 'syslog', 'code', 'inspect', 'pstats', 'venv', 'winsound', 'secrets', 'compileall', 'readline', 'telnetlib', 'timeit', 'uu', 'profile', 'enum', 'faulthandler', 'quopri', 'asyncore', 'contextlib', 'nis', 'hmac', 'textwrap', 'winreg', 'filecmp', 'modulefinder', 'cgitb', 'encodings', 'selectors', 'pipes', 'bz2', 'pkgutil', 'asyncio', 'imp', 'zipapp', 'grp', '_thread', 'crypt', 'subprocess', 'traceback', 'queue', 'tabnanny', 'locale', 'trace', 'string', 'struct', 'ipaddress', 'tokenize', 'ctypes', 'test', 'itertools', 'tempfile', 'formatter', 'ssl', 'bisect', 'shlex', 're', 'parser', 'webbrowser', 'posix', 'sysconfig', 'shutil', 'linecache', 'asynchat', 'base64', 'plistlib', 'unittest', 'msvcrt', 'chunk', 'threading', 'stat', 'mmap', 'pyclbr', 'sys', 'socketserver', 'dis', 'dummy_threading', 'symtable', 'mailcap', 'time', 'zlib'})` **Python & Config File Name:** known_standard_library **CLI Flags:** - -b - --builtin +## Extra Standard Library +Extra modules to be included in the list of ones in Python's standard library. + +**Type:** Frozenset +**Default:** `frozenset()` +**Python & Config File Name:** extra_standard_library +**CLI Flags:** + + - --extra-builtin + ## Known Other **No Description** @@ -283,7 +293,7 @@ One or more modules to exclude from the single line rule. Sets the default section for imports (by default FIRSTPARTY) options: ('FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER') **Type:** String -**Default:** `FIRSTPARTY` +**Default:** `THIRDPARTY` **Python & Config File Name:** default_section **CLI Flags:** @@ -632,14 +642,15 @@ Base profile type to use for configuration. - --profile ## Src Paths -**No Description** +Add an explicitly defined source path (modules within src paths have their imports automatically catorgorized as first_party). **Type:** Frozenset **Default:** `frozenset()` **Python & Config File Name:** src_paths **CLI Flags:** - - **Not Supported** + - --src + - --src-path ## Old Finders Use the old deprecated finder logic that relies on environment introspection magic. @@ -652,17 +663,6 @@ Use the old deprecated finder logic that relies on environment introspection mag - --old-finders - --magic -## Source Paths -Add an explicitly defined source path (modules within src paths have their imports automatically catorgorized as first_party). - -**Type:** String -**Default:** `None` -**Python & Config File Name:** **Not Supported** -**CLI Flags:** - - - --src - - --src-path - ## Check Checks the file for unsorted / unformatted imports and prints them to the command line without modifying the file. diff --git a/isort/main.py b/isort/main.py index 0d565d3b0..a114a0b9a 100644 --- a/isort/main.py +++ b/isort/main.py @@ -169,7 +169,13 @@ def _build_arg_parser() -> argparse.ArgumentParser: "--builtin", dest="known_standard_library", action="append", - help="Force isort to recognize a module as part of the python standard library.", + help="Force isort to recognize a module as part of Python's standard library.", + ) + parser.add_argument( + "--extra-builtin", + dest="extra_standard_library", + action="append", + help="Extra modules to be included in the list of ones in Python's standard library.", ) parser.add_argument( "-c", diff --git a/isort/settings.py b/isort/settings.py index d81d5531c..81e58418c 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -126,6 +126,7 @@ class _Config: known_third_party: FrozenSet[str] = frozenset(("google.appengine.api",)) known_first_party: FrozenSet[str] = frozenset() known_standard_library: FrozenSet[str] = frozenset() + extra_standard_library: FrozenSet[str] = frozenset() known_other: Dict[str, FrozenSet[str]] = field(default_factory=dict) multi_line_output: WrapModes = WrapModes.GRID # type: ignore forced_separate: Tuple[str, ...] = () @@ -374,12 +375,12 @@ def known_patterns(self): for placement in reversed(self.sections): known_placement = KNOWN_SECTION_MAPPING.get(placement, placement).lower() config_key = f"{KNOWN_PREFIX}{known_placement}" - known_patterns = list( - getattr(self, config_key, self.known_other.get(known_placement, [])) - ) + known_modules = getattr(self, config_key, self.known_other.get(known_placement, ())) + extra_modules = getattr(self, f"extra_{known_placement}", ()) + all_modules = set(known_modules).union(extra_modules) known_patterns = [ pattern - for known_pattern in known_patterns + for known_pattern in all_modules for pattern in self._parse_known_pattern(known_pattern) ] for known_pattern in known_patterns: diff --git a/tests/test_place.py b/tests/test_place.py index 50d3aee3f..f4a952641 100644 --- a/tests/test_place.py +++ b/tests/test_place.py @@ -12,3 +12,10 @@ def test_module(src_path): assert place_tester(".deprecated") == sections.LOCALFOLDER assert place_tester("__future__") == sections.FUTURE assert place_tester("hug") == sections.THIRDPARTY + + +def test_extra_standard_library(src_path): + place_tester = partial(place.module, config=Config(src_paths=[src_path], + extra_standard_library=["hug"])) + assert place_tester("os") == sections.STDLIB + assert place_tester("hug") == sections.STDLIB From 3e1bc70ab44f2ccb6b051c809ade2eacfa7e11ec Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 28 Jun 2020 22:42:00 -0700 Subject: [PATCH 0651/1439] Linting fix --- tests/test_place.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_place.py b/tests/test_place.py index f4a952641..cc231d5f5 100644 --- a/tests/test_place.py +++ b/tests/test_place.py @@ -15,7 +15,8 @@ def test_module(src_path): def test_extra_standard_library(src_path): - place_tester = partial(place.module, config=Config(src_paths=[src_path], - extra_standard_library=["hug"])) + place_tester = partial( + place.module, config=Config(src_paths=[src_path], extra_standard_library=["hug"]) + ) assert place_tester("os") == sections.STDLIB assert place_tester("hug") == sections.STDLIB From e5e8633e77336fb3af43873e60130bf5dc550779 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 28 Jun 2020 22:52:28 -0700 Subject: [PATCH 0652/1439] Add toml as a vendorized module --- .coveragerc | 1 + isort/_vendored/toml/LICENSE | 27 + isort/_vendored/toml/__init__.py | 23 + isort/_vendored/toml/decoder.py | 1053 ++++++++++++++++++++++++++++++ isort/_vendored/toml/encoder.py | 295 +++++++++ isort/_vendored/toml/ordered.py | 13 + isort/_vendored/toml/tz.py | 21 + isort/settings.py | 20 +- pyproject.toml | 2 - scripts/lint.sh | 2 +- setup.cfg | 1 + 11 files changed, 1442 insertions(+), 16 deletions(-) create mode 100644 isort/_vendored/toml/LICENSE create mode 100644 isort/_vendored/toml/__init__.py create mode 100644 isort/_vendored/toml/decoder.py create mode 100644 isort/_vendored/toml/encoder.py create mode 100644 isort/_vendored/toml/ordered.py create mode 100644 isort/_vendored/toml/tz.py diff --git a/.coveragerc b/.coveragerc index e4734ab3e..56861b744 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,6 +1,7 @@ [run] omit = isort/_future/* + isort/_vendored/* except ImportError: tests/* diff --git a/isort/_vendored/toml/LICENSE b/isort/_vendored/toml/LICENSE new file mode 100644 index 000000000..5010e3075 --- /dev/null +++ b/isort/_vendored/toml/LICENSE @@ -0,0 +1,27 @@ +The MIT License + +Copyright 2013-2019 William Pearson +Copyright 2015-2016 Julien Enselme +Copyright 2016 Google Inc. +Copyright 2017 Samuel Vasko +Copyright 2017 Nate Prewitt +Copyright 2017 Jack Evans +Copyright 2019 Filippo Broggini + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/isort/_vendored/toml/__init__.py b/isort/_vendored/toml/__init__.py new file mode 100644 index 000000000..6b7337aca --- /dev/null +++ b/isort/_vendored/toml/__init__.py @@ -0,0 +1,23 @@ +"""Python module which parses and emits TOML. + +Released under the MIT license. +""" +from toml import decoder, encoder + +__version__ = "0.10.1" +_spec_ = "0.5.0" + +load = decoder.load +loads = decoder.loads +TomlDecoder = decoder.TomlDecoder +TomlDecodeError = decoder.TomlDecodeError +TomlPreserveCommentDecoder = decoder.TomlPreserveCommentDecoder + +dump = encoder.dump +dumps = encoder.dumps +TomlEncoder = encoder.TomlEncoder +TomlArraySeparatorEncoder = encoder.TomlArraySeparatorEncoder +TomlPreserveInlineDictEncoder = encoder.TomlPreserveInlineDictEncoder +TomlNumpyEncoder = encoder.TomlNumpyEncoder +TomlPreserveCommentEncoder = encoder.TomlPreserveCommentEncoder +TomlPathlibEncoder = encoder.TomlPathlibEncoder diff --git a/isort/_vendored/toml/decoder.py b/isort/_vendored/toml/decoder.py new file mode 100644 index 000000000..b90b6933e --- /dev/null +++ b/isort/_vendored/toml/decoder.py @@ -0,0 +1,1053 @@ +import datetime +import io +import re +import sys +from os import linesep + +from .tz import TomlTz + +if sys.version_info < (3,): + _range = xrange # noqa: F821 +else: + unicode = str + _range = range + basestring = str + unichr = chr + + +def _detect_pathlib_path(p): + if (3, 4) <= sys.version_info: + import pathlib + + if isinstance(p, pathlib.PurePath): + return True + return False + + +def _ispath(p): + if isinstance(p, (bytes, basestring)): + return True + return _detect_pathlib_path(p) + + +def _getpath(p): + if (3, 6) <= sys.version_info: + import os + + return os.fspath(p) + if _detect_pathlib_path(p): + return str(p) + return p + + +try: + FNFError = FileNotFoundError +except NameError: + FNFError = IOError + + +TIME_RE = re.compile(r"([0-9]{2}):([0-9]{2}):([0-9]{2})(\.([0-9]{3,6}))?") + + +class TomlDecodeError(ValueError): + """Base toml Exception / Error.""" + + def __init__(self, msg, doc, pos): + lineno = doc.count("\n", 0, pos) + 1 + colno = pos - doc.rfind("\n", 0, pos) + emsg = "{} (line {} column {} char {})".format(msg, lineno, colno, pos) + ValueError.__init__(self, emsg) + self.msg = msg + self.doc = doc + self.pos = pos + self.lineno = lineno + self.colno = colno + + +# Matches a TOML number, which allows underscores for readability +_number_with_underscores = re.compile("([0-9])(_([0-9]))*") + + +class CommentValue(object): + def __init__(self, val, comment, beginline, _dict): + self.val = val + separator = "\n" if beginline else " " + self.comment = separator + comment + self._dict = _dict + + def __getitem__(self, key): + return self.val[key] + + def __setitem__(self, key, value): + self.val[key] = value + + def dump(self, dump_value_func): + retstr = dump_value_func(self.val) + if isinstance(self.val, self._dict): + return self.comment + "\n" + unicode(retstr) + else: + return unicode(retstr) + self.comment + + +def _strictly_valid_num(n): + n = n.strip() + if not n: + return False + if n[0] == "_": + return False + if n[-1] == "_": + return False + if "_." in n or "._" in n: + return False + if len(n) == 1: + return True + if n[0] == "0" and n[1] not in [".", "o", "b", "x"]: + return False + if n[0] == "+" or n[0] == "-": + n = n[1:] + if len(n) > 1 and n[0] == "0" and n[1] != ".": + return False + if "__" in n: + return False + return True + + +def load(f, _dict=dict, decoder=None): + """Parses named file or files as toml and returns a dictionary + + Args: + f: Path to the file to open, array of files to read into single dict + or a file descriptor + _dict: (optional) Specifies the class of the returned toml dictionary + decoder: The decoder to use + + Returns: + Parsed toml file represented as a dictionary + + Raises: + TypeError -- When f is invalid type + TomlDecodeError: Error while decoding toml + IOError / FileNotFoundError -- When an array with no valid (existing) + (Python 2 / Python 3) file paths is passed + """ + + if _ispath(f): + with io.open(_getpath(f), encoding="utf-8") as ffile: + return loads(ffile.read(), _dict, decoder) + elif isinstance(f, list): + from os import path as op + from warnings import warn + + if not [path for path in f if op.exists(path)]: + error_msg = "Load expects a list to contain filenames only." + error_msg += linesep + error_msg += "The list needs to contain the path of at least one " "existing file." + raise FNFError(error_msg) + if decoder is None: + decoder = TomlDecoder(_dict) + d = decoder.get_empty_table() + for l in f: # noqa: E741 + if op.exists(l): + d.update(load(l, _dict, decoder)) + else: + warn("Non-existent filename in list with at least one valid " "filename") + return d + else: + try: + return loads(f.read(), _dict, decoder) + except AttributeError: + raise TypeError("You can only load a file descriptor, filename or " "list") + + +_groupname_re = re.compile(r"^[A-Za-z0-9_-]+$") + + +def loads(s, _dict=dict, decoder=None): + """Parses string as toml + + Args: + s: String to be parsed + _dict: (optional) Specifies the class of the returned toml dictionary + + Returns: + Parsed toml file represented as a dictionary + + Raises: + TypeError: When a non-string is passed + TomlDecodeError: Error while decoding toml + """ + + implicitgroups = [] + if decoder is None: + decoder = TomlDecoder(_dict) + retval = decoder.get_empty_table() + currentlevel = retval + if not isinstance(s, basestring): + raise TypeError("Expecting something like a string") + + if not isinstance(s, unicode): + s = s.decode("utf8") + + original = s + sl = list(s) + openarr = 0 + openstring = False + openstrchar = "" + multilinestr = False + arrayoftables = False + beginline = True + keygroup = False + dottedkey = False + keyname = 0 + key = "" + prev_key = "" + line_no = 1 + + for i, item in enumerate(sl): + if item == "\r" and sl[i + 1] == "\n": + sl[i] = " " + continue + if keyname: + key += item + if item == "\n": + raise TomlDecodeError( + "Key name found without value." " Reached end of line.", original, i + ) + if openstring: + if item == openstrchar: + oddbackslash = False + k = 1 + while i >= k and sl[i - k] == "\\": + oddbackslash = not oddbackslash + k += 1 + if not oddbackslash: + keyname = 2 + openstring = False + openstrchar = "" + continue + elif keyname == 1: + if item.isspace(): + keyname = 2 + continue + elif item == ".": + dottedkey = True + continue + elif item.isalnum() or item == "_" or item == "-": + continue + elif dottedkey and sl[i - 1] == "." and (item == '"' or item == "'"): + openstring = True + openstrchar = item + continue + elif keyname == 2: + if item.isspace(): + if dottedkey: + nextitem = sl[i + 1] + if not nextitem.isspace() and nextitem != ".": + keyname = 1 + continue + if item == ".": + dottedkey = True + nextitem = sl[i + 1] + if not nextitem.isspace() and nextitem != ".": + keyname = 1 + continue + if item == "=": + keyname = 0 + prev_key = key[:-1].rstrip() + key = "" + dottedkey = False + else: + raise TomlDecodeError( + "Found invalid character in key name: '" + + item + + "'. Try quoting the key name.", + original, + i, + ) + if item == "'" and openstrchar != '"': + k = 1 + try: + while sl[i - k] == "'": + k += 1 + if k == 3: + break + except IndexError: + pass + if k == 3: + multilinestr = not multilinestr + openstring = multilinestr + else: + openstring = not openstring + if openstring: + openstrchar = "'" + else: + openstrchar = "" + if item == '"' and openstrchar != "'": + oddbackslash = False + k = 1 + tripquote = False + try: + while sl[i - k] == '"': + k += 1 + if k == 3: + tripquote = True + break + if k == 1 or (k == 3 and tripquote): + while sl[i - k] == "\\": + oddbackslash = not oddbackslash + k += 1 + except IndexError: + pass + if not oddbackslash: + if tripquote: + multilinestr = not multilinestr + openstring = multilinestr + else: + openstring = not openstring + if openstring: + openstrchar = '"' + else: + openstrchar = "" + if item == "#" and (not openstring and not keygroup and not arrayoftables): + j = i + comment = "" + try: + while sl[j] != "\n": + comment += s[j] + sl[j] = " " + j += 1 + except IndexError: + break + if not openarr: + decoder.preserve_comment(line_no, prev_key, comment, beginline) + if item == "[" and (not openstring and not keygroup and not arrayoftables): + if beginline: + if len(sl) > i + 1 and sl[i + 1] == "[": + arrayoftables = True + else: + keygroup = True + else: + openarr += 1 + if item == "]" and not openstring: + if keygroup: + keygroup = False + elif arrayoftables: + if sl[i - 1] == "]": + arrayoftables = False + else: + openarr -= 1 + if item == "\n": + if openstring or multilinestr: + if not multilinestr: + raise TomlDecodeError("Unbalanced quotes", original, i) + if (sl[i - 1] == "'" or sl[i - 1] == '"') and (sl[i - 2] == sl[i - 1]): + sl[i] = sl[i - 1] + if sl[i - 3] == sl[i - 1]: + sl[i - 3] = " " + elif openarr: + sl[i] = " " + else: + beginline = True + line_no += 1 + elif beginline and sl[i] != " " and sl[i] != "\t": + beginline = False + if not keygroup and not arrayoftables: + if sl[i] == "=": + raise TomlDecodeError("Found empty keyname. ", original, i) + keyname = 1 + key += item + if keyname: + raise TomlDecodeError( + "Key name found without value." " Reached end of file.", original, len(s) + ) + if openstring: # reached EOF and have an unterminated string + raise TomlDecodeError( + "Unterminated string found." " Reached end of file.", original, len(s) + ) + s = "".join(sl) + s = s.split("\n") + multikey = None + multilinestr = "" + multibackslash = False + pos = 0 + for idx, line in enumerate(s): + if idx > 0: + pos += len(s[idx - 1]) + 1 + + decoder.embed_comments(idx, currentlevel) + + if not multilinestr or multibackslash or "\n" not in multilinestr: + line = line.strip() + if line == "" and (not multikey or multibackslash): + continue + if multikey: + if multibackslash: + multilinestr += line + else: + multilinestr += line + multibackslash = False + closed = False + if multilinestr[0] == "[": + closed = line[-1] == "]" + elif len(line) > 2: + closed = ( + line[-1] == multilinestr[0] + and line[-2] == multilinestr[0] + and line[-3] == multilinestr[0] + ) + if closed: + try: + value, vtype = decoder.load_value(multilinestr) + except ValueError as err: + raise TomlDecodeError(str(err), original, pos) + currentlevel[multikey] = value + multikey = None + multilinestr = "" + else: + k = len(multilinestr) - 1 + while k > -1 and multilinestr[k] == "\\": + multibackslash = not multibackslash + k -= 1 + if multibackslash: + multilinestr = multilinestr[:-1] + else: + multilinestr += "\n" + continue + if line[0] == "[": + arrayoftables = False + if len(line) == 1: + raise TomlDecodeError( + "Opening key group bracket on line by " "itself.", original, pos + ) + if line[1] == "[": + arrayoftables = True + line = line[2:] + splitstr = "]]" + else: + line = line[1:] + splitstr = "]" + i = 1 + quotesplits = decoder._get_split_on_quotes(line) + quoted = False + for quotesplit in quotesplits: + if not quoted and splitstr in quotesplit: + break + i += quotesplit.count(splitstr) + quoted = not quoted + line = line.split(splitstr, i) + if len(line) < i + 1 or line[-1].strip() != "": + raise TomlDecodeError("Key group not on a line by itself.", original, pos) + groups = splitstr.join(line[:-1]).split(".") + i = 0 + while i < len(groups): + groups[i] = groups[i].strip() + if len(groups[i]) > 0 and (groups[i][0] == '"' or groups[i][0] == "'"): + groupstr = groups[i] + j = i + 1 + while not groupstr[0] == groupstr[-1]: + j += 1 + if j > len(groups) + 2: + raise TomlDecodeError( + "Invalid group name '" + groupstr + "' Something " + "went wrong.", + original, + pos, + ) + groupstr = ".".join(groups[i:j]).strip() + groups[i] = groupstr[1:-1] + groups[i + 1 : j] = [] + else: + if not _groupname_re.match(groups[i]): + raise TomlDecodeError( + "Invalid group name '" + groups[i] + "'. Try quoting it.", original, pos + ) + i += 1 + currentlevel = retval + for i in _range(len(groups)): + group = groups[i] + if group == "": + raise TomlDecodeError( + "Can't have a keygroup with an empty " "name", original, pos + ) + try: + currentlevel[group] + if i == len(groups) - 1: + if group in implicitgroups: + implicitgroups.remove(group) + if arrayoftables: + raise TomlDecodeError( + "An implicitly defined " "table can't be an array", + original, + pos, + ) + elif arrayoftables: + currentlevel[group].append(decoder.get_empty_table()) + else: + raise TomlDecodeError( + "What? " + group + " already exists?" + str(currentlevel), + original, + pos, + ) + except TypeError: + currentlevel = currentlevel[-1] + if group not in currentlevel: + currentlevel[group] = decoder.get_empty_table() + if i == len(groups) - 1 and arrayoftables: + currentlevel[group] = [decoder.get_empty_table()] + except KeyError: + if i != len(groups) - 1: + implicitgroups.append(group) + currentlevel[group] = decoder.get_empty_table() + if i == len(groups) - 1 and arrayoftables: + currentlevel[group] = [decoder.get_empty_table()] + currentlevel = currentlevel[group] + if arrayoftables: + try: + currentlevel = currentlevel[-1] + except KeyError: + pass + elif line[0] == "{": + if line[-1] != "}": + raise TomlDecodeError( + "Line breaks are not allowed in inline" "objects", original, pos + ) + try: + decoder.load_inline_object(line, currentlevel, multikey, multibackslash) + except ValueError as err: + raise TomlDecodeError(str(err), original, pos) + elif "=" in line: + try: + ret = decoder.load_line(line, currentlevel, multikey, multibackslash) + except ValueError as err: + raise TomlDecodeError(str(err), original, pos) + if ret is not None: + multikey, multilinestr, multibackslash = ret + return retval + + +def _load_date(val): + microsecond = 0 + tz = None + try: + if len(val) > 19: + if val[19] == ".": + if val[-1].upper() == "Z": + subsecondval = val[20:-1] + tzval = "Z" + else: + subsecondvalandtz = val[20:] + if "+" in subsecondvalandtz: + splitpoint = subsecondvalandtz.index("+") + subsecondval = subsecondvalandtz[:splitpoint] + tzval = subsecondvalandtz[splitpoint:] + elif "-" in subsecondvalandtz: + splitpoint = subsecondvalandtz.index("-") + subsecondval = subsecondvalandtz[:splitpoint] + tzval = subsecondvalandtz[splitpoint:] + else: + tzval = None + subsecondval = subsecondvalandtz + if tzval is not None: + tz = TomlTz(tzval) + microsecond = int(int(subsecondval) * (10 ** (6 - len(subsecondval)))) + else: + tz = TomlTz(val[19:]) + except ValueError: + tz = None + if "-" not in val[1:]: + return None + try: + if len(val) == 10: + d = datetime.date(int(val[:4]), int(val[5:7]), int(val[8:10])) + else: + d = datetime.datetime( + int(val[:4]), + int(val[5:7]), + int(val[8:10]), + int(val[11:13]), + int(val[14:16]), + int(val[17:19]), + microsecond, + tz, + ) + except ValueError: + return None + return d + + +def _load_unicode_escapes(v, hexbytes, prefix): + skip = False + i = len(v) - 1 + while i > -1 and v[i] == "\\": + skip = not skip + i -= 1 + for hx in hexbytes: + if skip: + skip = False + i = len(hx) - 1 + while i > -1 and hx[i] == "\\": + skip = not skip + i -= 1 + v += prefix + v += hx + continue + hxb = "" + i = 0 + hxblen = 4 + if prefix == "\\U": + hxblen = 8 + hxb = "".join(hx[i : i + hxblen]).lower() + if hxb.strip("0123456789abcdef"): + raise ValueError("Invalid escape sequence: " + hxb) + if hxb[0] == "d" and hxb[1].strip("01234567"): + raise ValueError( + "Invalid escape sequence: " + hxb + ". Only scalar unicode points are allowed." + ) + v += unichr(int(hxb, 16)) + v += unicode(hx[len(hxb) :]) + return v + + +# Unescape TOML string values. + +# content after the \ +_escapes = ["0", "b", "f", "n", "r", "t", '"'] +# What it should be replaced by +_escapedchars = ["\0", "\b", "\f", "\n", "\r", "\t", '"'] +# Used for substitution +_escape_to_escapedchars = dict(zip(_escapes, _escapedchars)) + + +def _unescape(v): + """Unescape characters in a TOML string.""" + i = 0 + backslash = False + while i < len(v): + if backslash: + backslash = False + if v[i] in _escapes: + v = v[: i - 1] + _escape_to_escapedchars[v[i]] + v[i + 1 :] + elif v[i] == "\\": + v = v[: i - 1] + v[i:] + elif v[i] == "u" or v[i] == "U": + i += 1 + else: + raise ValueError("Reserved escape sequence used") + continue + elif v[i] == "\\": + backslash = True + i += 1 + return v + + +class InlineTableDict(object): + """Sentinel subclass of dict for inline tables.""" + + +class TomlDecoder(object): + def __init__(self, _dict=dict): + self._dict = _dict + + def get_empty_table(self): + return self._dict() + + def get_empty_inline_table(self): + class DynamicInlineTableDict(self._dict, InlineTableDict): + """Concrete sentinel subclass for inline tables. + It is a subclass of _dict which is passed in dynamically at load + time + + It is also a subclass of InlineTableDict + """ + + return DynamicInlineTableDict() + + def load_inline_object(self, line, currentlevel, multikey=False, multibackslash=False): + candidate_groups = line[1:-1].split(",") + groups = [] + if len(candidate_groups) == 1 and not candidate_groups[0].strip(): + candidate_groups.pop() + while len(candidate_groups) > 0: + candidate_group = candidate_groups.pop(0) + try: + _, value = candidate_group.split("=", 1) + except ValueError: + raise ValueError("Invalid inline table encountered") + value = value.strip() + if (value[0] == value[-1] and value[0] in ('"', "'")) or ( + value[0] in "-0123456789" + or value in ("true", "false") + or (value[0] == "[" and value[-1] == "]") + or (value[0] == "{" and value[-1] == "}") + ): + groups.append(candidate_group) + elif len(candidate_groups) > 0: + candidate_groups[0] = candidate_group + "," + candidate_groups[0] + else: + raise ValueError("Invalid inline table value encountered") + for group in groups: + status = self.load_line(group, currentlevel, multikey, multibackslash) + if status is not None: + break + + def _get_split_on_quotes(self, line): + doublequotesplits = line.split('"') + quoted = False + quotesplits = [] + if len(doublequotesplits) > 1 and "'" in doublequotesplits[0]: + singlequotesplits = doublequotesplits[0].split("'") + doublequotesplits = doublequotesplits[1:] + while len(singlequotesplits) % 2 == 0 and len(doublequotesplits): + singlequotesplits[-1] += '"' + doublequotesplits[0] + doublequotesplits = doublequotesplits[1:] + if "'" in singlequotesplits[-1]: + singlequotesplits = singlequotesplits[:-1] + singlequotesplits[-1].split("'") + quotesplits += singlequotesplits + for doublequotesplit in doublequotesplits: + if quoted: + quotesplits.append(doublequotesplit) + else: + quotesplits += doublequotesplit.split("'") + quoted = not quoted + return quotesplits + + def load_line(self, line, currentlevel, multikey, multibackslash): + i = 1 + quotesplits = self._get_split_on_quotes(line) + quoted = False + for quotesplit in quotesplits: + if not quoted and "=" in quotesplit: + break + i += quotesplit.count("=") + quoted = not quoted + pair = line.split("=", i) + strictly_valid = _strictly_valid_num(pair[-1]) + if _number_with_underscores.match(pair[-1]): + pair[-1] = pair[-1].replace("_", "") + while len(pair[-1]) and ( + pair[-1][0] != " " + and pair[-1][0] != "\t" + and pair[-1][0] != "'" + and pair[-1][0] != '"' + and pair[-1][0] != "[" + and pair[-1][0] != "{" + and pair[-1].strip() != "true" + and pair[-1].strip() != "false" + ): + try: + float(pair[-1]) + break + except ValueError: + pass + if _load_date(pair[-1]) is not None: + break + if TIME_RE.match(pair[-1]): + break + i += 1 + prev_val = pair[-1] + pair = line.split("=", i) + if prev_val == pair[-1]: + raise ValueError("Invalid date or number") + if strictly_valid: + strictly_valid = _strictly_valid_num(pair[-1]) + pair = ["=".join(pair[:-1]).strip(), pair[-1].strip()] + if "." in pair[0]: + if '"' in pair[0] or "'" in pair[0]: + quotesplits = self._get_split_on_quotes(pair[0]) + quoted = False + levels = [] + for quotesplit in quotesplits: + if quoted: + levels.append(quotesplit) + else: + levels += [level.strip() for level in quotesplit.split(".")] + quoted = not quoted + else: + levels = pair[0].split(".") + while levels[-1] == "": + levels = levels[:-1] + for level in levels[:-1]: + if level == "": + continue + if level not in currentlevel: + currentlevel[level] = self.get_empty_table() + currentlevel = currentlevel[level] + pair[0] = levels[-1].strip() + elif (pair[0][0] == '"' or pair[0][0] == "'") and (pair[0][-1] == pair[0][0]): + pair[0] = _unescape(pair[0][1:-1]) + k, koffset = self._load_line_multiline_str(pair[1]) + if k > -1: + while k > -1 and pair[1][k + koffset] == "\\": + multibackslash = not multibackslash + k -= 1 + if multibackslash: + multilinestr = pair[1][:-1] + else: + multilinestr = pair[1] + "\n" + multikey = pair[0] + else: + value, vtype = self.load_value(pair[1], strictly_valid) + try: + currentlevel[pair[0]] + raise ValueError("Duplicate keys!") + except TypeError: + raise ValueError("Duplicate keys!") + except KeyError: + if multikey: + return multikey, multilinestr, multibackslash + else: + currentlevel[pair[0]] = value + + def _load_line_multiline_str(self, p): + poffset = 0 + if len(p) < 3: + return -1, poffset + if p[0] == "[" and (p.strip()[-1] != "]" and self._load_array_isstrarray(p)): + newp = p[1:].strip().split(",") + while len(newp) > 1 and newp[-1][0] != '"' and newp[-1][0] != "'": + newp = newp[:-2] + [newp[-2] + "," + newp[-1]] + newp = newp[-1] + poffset = len(p) - len(newp) + p = newp + if p[0] != '"' and p[0] != "'": + return -1, poffset + if p[1] != p[0] or p[2] != p[0]: + return -1, poffset + if len(p) > 5 and p[-1] == p[0] and p[-2] == p[0] and p[-3] == p[0]: + return -1, poffset + return len(p) - 1, poffset + + def load_value(self, v, strictly_valid=True): + if not v: + raise ValueError("Empty value is invalid") + if v == "true": + return (True, "bool") + elif v == "false": + return (False, "bool") + elif v[0] == '"' or v[0] == "'": + quotechar = v[0] + testv = v[1:].split(quotechar) + triplequote = False + triplequotecount = 0 + if len(testv) > 1 and testv[0] == "" and testv[1] == "": + testv = testv[2:] + triplequote = True + closed = False + for tv in testv: + if tv == "": + if triplequote: + triplequotecount += 1 + else: + closed = True + else: + oddbackslash = False + try: + i = -1 + j = tv[i] + while j == "\\": + oddbackslash = not oddbackslash + i -= 1 + j = tv[i] + except IndexError: + pass + if not oddbackslash: + if closed: + raise ValueError( + "Found tokens after a closed " + "string. Invalid TOML." + ) + else: + if not triplequote or triplequotecount > 1: + closed = True + else: + triplequotecount = 0 + if quotechar == '"': + escapeseqs = v.split("\\")[1:] + backslash = False + for i in escapeseqs: + if i == "": + backslash = not backslash + else: + if i[0] not in _escapes and (i[0] != "u" and i[0] != "U" and not backslash): + raise ValueError("Reserved escape sequence used") + if backslash: + backslash = False + for prefix in ["\\u", "\\U"]: + if prefix in v: + hexbytes = v.split(prefix) + v = _load_unicode_escapes(hexbytes[0], hexbytes[1:], prefix) + v = _unescape(v) + if len(v) > 1 and v[1] == quotechar and (len(v) < 3 or v[1] == v[2]): + v = v[2:-2] + return (v[1:-1], "str") + elif v[0] == "[": + return (self.load_array(v), "array") + elif v[0] == "{": + inline_object = self.get_empty_inline_table() + self.load_inline_object(v, inline_object) + return (inline_object, "inline_object") + elif TIME_RE.match(v): + h, m, s, _, ms = TIME_RE.match(v).groups() + time = datetime.time(int(h), int(m), int(s), int(ms) if ms else 0) + return (time, "time") + else: + parsed_date = _load_date(v) + if parsed_date is not None: + return (parsed_date, "date") + if not strictly_valid: + raise ValueError("Weirdness with leading zeroes or " "underscores in your number.") + itype = "int" + neg = False + if v[0] == "-": + neg = True + v = v[1:] + elif v[0] == "+": + v = v[1:] + v = v.replace("_", "") + lowerv = v.lower() + if "." in v or ("x" not in v and ("e" in v or "E" in v)): + if "." in v and v.split(".", 1)[1] == "": + raise ValueError("This float is missing digits after " "the point") + if v[0] not in "0123456789": + raise ValueError("This float doesn't have a leading " "digit") + v = float(v) + itype = "float" + elif len(lowerv) == 3 and (lowerv == "inf" or lowerv == "nan"): + v = float(v) + itype = "float" + if itype == "int": + v = int(v, 0) + if neg: + return (0 - v, itype) + return (v, itype) + + def bounded_string(self, s): + if len(s) == 0: + return True + if s[-1] != s[0]: + return False + i = -2 + backslash = False + while len(s) + i > 0: + if s[i] == "\\": + backslash = not backslash + i -= 1 + else: + break + return not backslash + + def _load_array_isstrarray(self, a): + a = a[1:-1].strip() + if a != "" and (a[0] == '"' or a[0] == "'"): + return True + return False + + def load_array(self, a): + atype = None + retval = [] + a = a.strip() + if "[" not in a[1:-1] or "" != a[1:-1].split("[")[0].strip(): + strarray = self._load_array_isstrarray(a) + if not a[1:-1].strip().startswith("{"): + a = a[1:-1].split(",") + else: + # a is an inline object, we must find the matching parenthesis + # to define groups + new_a = [] + start_group_index = 1 + end_group_index = 2 + open_bracket_count = 1 if a[start_group_index] == "{" else 0 + in_str = False + while end_group_index < len(a[1:]): + if a[end_group_index] == '"' or a[end_group_index] == "'": + if in_str: + backslash_index = end_group_index - 1 + while backslash_index > -1 and a[backslash_index] == "\\": + in_str = not in_str + backslash_index -= 1 + in_str = not in_str + if not in_str and a[end_group_index] == "{": + open_bracket_count += 1 + if in_str or a[end_group_index] != "}": + end_group_index += 1 + continue + elif a[end_group_index] == "}" and open_bracket_count > 1: + open_bracket_count -= 1 + end_group_index += 1 + continue + + # Increase end_group_index by 1 to get the closing bracket + end_group_index += 1 + + new_a.append(a[start_group_index:end_group_index]) + + # The next start index is at least after the closing + # bracket, a closing bracket can be followed by a comma + # since we are in an array. + start_group_index = end_group_index + 1 + while start_group_index < len(a[1:]) and a[start_group_index] != "{": + start_group_index += 1 + end_group_index = start_group_index + 1 + a = new_a + b = 0 + if strarray: + while b < len(a) - 1: + ab = a[b].strip() + while not self.bounded_string(ab) or ( + len(ab) > 2 + and ab[0] == ab[1] == ab[2] + and ab[-2] != ab[0] + and ab[-3] != ab[0] + ): + a[b] = a[b] + "," + a[b + 1] + ab = a[b].strip() + if b < len(a) - 2: + a = a[: b + 1] + a[b + 2 :] + else: + a = a[: b + 1] + b += 1 + else: + al = list(a[1:-1]) + a = [] + openarr = 0 + j = 0 + for i in _range(len(al)): + if al[i] == "[": + openarr += 1 + elif al[i] == "]": + openarr -= 1 + elif al[i] == "," and not openarr: + a.append("".join(al[j:i])) + j = i + 1 + a.append("".join(al[j:])) + for i in _range(len(a)): + a[i] = a[i].strip() + if a[i] != "": + nval, ntype = self.load_value(a[i]) + if atype: + if ntype != atype: + raise ValueError("Not a homogeneous array") + else: + atype = ntype + retval.append(nval) + return retval + + def preserve_comment(self, line_no, key, comment, beginline): + pass + + def embed_comments(self, idx, currentlevel): + pass + + +class TomlPreserveCommentDecoder(TomlDecoder): + def __init__(self, _dict=dict): + self.saved_comments = {} + super(TomlPreserveCommentDecoder, self).__init__(_dict) + + def preserve_comment(self, line_no, key, comment, beginline): + self.saved_comments[line_no] = (key, comment, beginline) + + def embed_comments(self, idx, currentlevel): + if idx not in self.saved_comments: + return + + key, comment, beginline = self.saved_comments[idx] + currentlevel[key] = CommentValue(currentlevel[key], comment, beginline, self._dict) diff --git a/isort/_vendored/toml/encoder.py b/isort/_vendored/toml/encoder.py new file mode 100644 index 000000000..68ec60f9f --- /dev/null +++ b/isort/_vendored/toml/encoder.py @@ -0,0 +1,295 @@ +import datetime +import re +import sys +from decimal import Decimal + +from .decoder import InlineTableDict + +if sys.version_info >= (3,): + unicode = str + + +def dump(o, f, encoder=None): + """Writes out dict as toml to a file + + Args: + o: Object to dump into toml + f: File descriptor where the toml should be stored + encoder: The ``TomlEncoder`` to use for constructing the output string + + Returns: + String containing the toml corresponding to dictionary + + Raises: + TypeError: When anything other than file descriptor is passed + """ + + if not f.write: + raise TypeError("You can only dump an object to a file descriptor") + d = dumps(o, encoder=encoder) + f.write(d) + return d + + +def dumps(o, encoder=None): + """Stringifies input dict as toml + + Args: + o: Object to dump into toml + encoder: The ``TomlEncoder`` to use for constructing the output string + + Returns: + String containing the toml corresponding to dict + + Examples: + ```python + >>> import toml + >>> output = { + ... 'a': "I'm a string", + ... 'b': ["I'm", "a", "list"], + ... 'c': 2400 + ... } + >>> toml.dumps(output) + 'a = "I\'m a string"\nb = [ "I\'m", "a", "list",]\nc = 2400\n' + ``` + """ + + retval = "" + if encoder is None: + encoder = TomlEncoder(o.__class__) + addtoretval, sections = encoder.dump_sections(o, "") + retval += addtoretval + outer_objs = [id(o)] + while sections: + section_ids = [id(section) for section in sections] + for outer_obj in outer_objs: + if outer_obj in section_ids: + raise ValueError("Circular reference detected") + outer_objs += section_ids + newsections = encoder.get_empty_table() + for section in sections: + addtoretval, addtosections = encoder.dump_sections(sections[section], section) + + if addtoretval or (not addtoretval and not addtosections): + if retval and retval[-2:] != "\n\n": + retval += "\n" + retval += "[" + section + "]\n" + if addtoretval: + retval += addtoretval + for s in addtosections: + newsections[section + "." + s] = addtosections[s] + sections = newsections + return retval + + +def _dump_str(v): + if sys.version_info < (3,) and hasattr(v, "decode") and isinstance(v, str): + v = v.decode("utf-8") + v = "%r" % v + if v[0] == "u": + v = v[1:] + singlequote = v.startswith("'") + if singlequote or v.startswith('"'): + v = v[1:-1] + if singlequote: + v = v.replace("\\'", "'") + v = v.replace('"', '\\"') + v = v.split("\\x") + while len(v) > 1: + i = -1 + if not v[0]: + v = v[1:] + v[0] = v[0].replace("\\\\", "\\") + # No, I don't know why != works and == breaks + joinx = v[0][i] != "\\" + while v[0][:i] and v[0][i] == "\\": + joinx = not joinx + i -= 1 + if joinx: + joiner = "x" + else: + joiner = "u00" + v = [v[0] + joiner + v[1]] + v[2:] + return unicode('"' + v[0] + '"') + + +def _dump_float(v): + return "{}".format(v).replace("e+0", "e+").replace("e-0", "e-") + + +def _dump_time(v): + utcoffset = v.utcoffset() + if utcoffset is None: + return v.isoformat() + # The TOML norm specifies that it's local time thus we drop the offset + return v.isoformat()[:-6] + + +class TomlEncoder(object): + def __init__(self, _dict=dict, preserve=False): + self._dict = _dict + self.preserve = preserve + self.dump_funcs = { + str: _dump_str, + unicode: _dump_str, + list: self.dump_list, + bool: lambda v: unicode(v).lower(), + int: lambda v: v, + float: _dump_float, + Decimal: _dump_float, + datetime.datetime: lambda v: v.isoformat().replace("+00:00", "Z"), + datetime.time: _dump_time, + datetime.date: lambda v: v.isoformat(), + } + + def get_empty_table(self): + return self._dict() + + def dump_list(self, v): + retval = "[" + for u in v: + retval += " " + unicode(self.dump_value(u)) + "," + retval += "]" + return retval + + def dump_inline_table(self, section): + """Preserve inline table in its compact syntax instead of expanding + into subsection. + + https://github.com/toml-lang/toml#user-content-inline-table + """ + retval = "" + if isinstance(section, dict): + val_list = [] + for k, v in section.items(): + val = self.dump_inline_table(v) + val_list.append(k + " = " + val) + retval += "{ " + ", ".join(val_list) + " }\n" + return retval + else: + return unicode(self.dump_value(section)) + + def dump_value(self, v): + # Lookup function corresponding to v's type + dump_fn = self.dump_funcs.get(type(v)) + if dump_fn is None and hasattr(v, "__iter__"): + dump_fn = self.dump_funcs[list] + # Evaluate function (if it exists) else return v + return dump_fn(v) if dump_fn is not None else self.dump_funcs[str](v) + + def dump_sections(self, o, sup): + retstr = "" + if sup != "" and sup[-1] != ".": + sup += "." + retdict = self._dict() + arraystr = "" + for section in o: + section = unicode(section) + qsection = section + if not re.match(r"^[A-Za-z0-9_-]+$", section): + qsection = _dump_str(section) + if not isinstance(o[section], dict): + arrayoftables = False + if isinstance(o[section], list): + for a in o[section]: + if isinstance(a, dict): + arrayoftables = True + if arrayoftables: + for a in o[section]: + arraytabstr = "\n" + arraystr += "[[" + sup + qsection + "]]\n" + s, d = self.dump_sections(a, sup + qsection) + if s: + if s[0] == "[": + arraytabstr += s + else: + arraystr += s + while d: + newd = self._dict() + for dsec in d: + s1, d1 = self.dump_sections(d[dsec], sup + qsection + "." + dsec) + if s1: + arraytabstr += "[" + sup + qsection + "." + dsec + "]\n" + arraytabstr += s1 + for s1 in d1: + newd[dsec + "." + s1] = d1[s1] + d = newd + arraystr += arraytabstr + else: + if o[section] is not None: + retstr += qsection + " = " + unicode(self.dump_value(o[section])) + "\n" + elif self.preserve and isinstance(o[section], InlineTableDict): + retstr += qsection + " = " + self.dump_inline_table(o[section]) + else: + retdict[qsection] = o[section] + retstr += arraystr + return (retstr, retdict) + + +class TomlPreserveInlineDictEncoder(TomlEncoder): + def __init__(self, _dict=dict): + super(TomlPreserveInlineDictEncoder, self).__init__(_dict, True) + + +class TomlArraySeparatorEncoder(TomlEncoder): + def __init__(self, _dict=dict, preserve=False, separator=","): + super(TomlArraySeparatorEncoder, self).__init__(_dict, preserve) + if separator.strip() == "": + separator = "," + separator + elif separator.strip(" \t\n\r,"): + raise ValueError("Invalid separator for arrays") + self.separator = separator + + def dump_list(self, v): + t = [] + retval = "[" + for u in v: + t.append(self.dump_value(u)) + while t != []: + s = [] + for u in t: + if isinstance(u, list): + for r in u: + s.append(r) + else: + retval += " " + unicode(u) + self.separator + t = s + retval += "]" + return retval + + +class TomlNumpyEncoder(TomlEncoder): + def __init__(self, _dict=dict, preserve=False): + import numpy as np + + super(TomlNumpyEncoder, self).__init__(_dict, preserve) + self.dump_funcs[np.float16] = _dump_float + self.dump_funcs[np.float32] = _dump_float + self.dump_funcs[np.float64] = _dump_float + self.dump_funcs[np.int16] = self._dump_int + self.dump_funcs[np.int32] = self._dump_int + self.dump_funcs[np.int64] = self._dump_int + + def _dump_int(self, v): + return "{}".format(int(v)) + + +class TomlPreserveCommentEncoder(TomlEncoder): + def __init__(self, _dict=dict, preserve=False): + from toml.decoder import CommentValue + + super(TomlPreserveCommentEncoder, self).__init__(_dict, preserve) + self.dump_funcs[CommentValue] = lambda v: v.dump(self.dump_value) + + +class TomlPathlibEncoder(TomlEncoder): + def _dump_pathlib_path(self, v): + return _dump_str(str(v)) + + def dump_value(self, v): + if (3, 4) <= sys.version_info: + import pathlib + + if isinstance(v, pathlib.PurePath): + v = str(v) + return super(TomlPathlibEncoder, self).dump_value(v) diff --git a/isort/_vendored/toml/ordered.py b/isort/_vendored/toml/ordered.py new file mode 100644 index 000000000..013b31e5c --- /dev/null +++ b/isort/_vendored/toml/ordered.py @@ -0,0 +1,13 @@ +from collections import OrderedDict + +from . import TomlDecoder, TomlEncoder + + +class TomlOrderedDecoder(TomlDecoder): + def __init__(self): + super(self.__class__, self).__init__(_dict=OrderedDict) + + +class TomlOrderedEncoder(TomlEncoder): + def __init__(self): + super(self.__class__, self).__init__(_dict=OrderedDict) diff --git a/isort/_vendored/toml/tz.py b/isort/_vendored/toml/tz.py new file mode 100644 index 000000000..46214bd4d --- /dev/null +++ b/isort/_vendored/toml/tz.py @@ -0,0 +1,21 @@ +from datetime import timedelta, tzinfo + + +class TomlTz(tzinfo): + def __init__(self, toml_offset): + if toml_offset == "Z": + self._raw_offset = "+00:00" + else: + self._raw_offset = toml_offset + self._sign = -1 if self._raw_offset[0] == "-" else 1 + self._hours = int(self._raw_offset[1:3]) + self._minutes = int(self._raw_offset[4:6]) + + def tzname(self, dt): + return "UTC" + self._raw_offset + + def utcoffset(self, dt): + return self._sign * timedelta(hours=self._hours, minutes=self._minutes) + + def dst(self, dt): + return timedelta(0) diff --git a/isort/settings.py b/isort/settings.py index 81e58418c..52ddfdf95 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -20,6 +20,7 @@ from . import stdlibs from ._future import dataclass, field +from ._vendored import toml from .exceptions import ProfileDoesNotExist from .profiles import profiles from .sections import DEFAULT as SECTION_DEFAULTS @@ -27,12 +28,6 @@ from .wrap_modes import WrapModes from .wrap_modes import from_string as wrap_mode_from_string -try: - import toml - -except ImportError: - toml = None # type: ignore - try: import appdirs @@ -467,13 +462,12 @@ def _get_config_data(file_path: str, sections: Tuple[str]) -> Dict[str, Any]: with open(file_path) as config_file: if file_path.endswith(".toml"): - if toml: - config = toml.load(config_file) - for section in sections: - config_section = config - for key in section.split("."): - config_section = config_section.get(key, {}) - settings.update(config_section) + config = toml.load(config_file) + for section in sections: + config_section = config + for key in section.split("."): + config_section = config_section.get(key, {}) + settings.update(config_section) else: # pragma: no cover if "[tool.isort]" in config_file.read(): warnings.warn( diff --git a/pyproject.toml b/pyproject.toml index fa664d084..844aba9c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,12 +35,10 @@ appdirs = {version = "^1.4.0", optional = true} pipreqs = {version = "*", optional = true} requirementslib = {version = "*", optional = true} tomlkit = {version = ">=0.5.3", optional = true} -toml = {version = "*", optional = true} pip-api = {version = "*", optional = true} [tool.poetry.extras] pipfile = ["pipreqs", "tomlkit", "requirementslib", "pip-shims<=0.3.4"] -pyproject = ["toml"] requirements = ["pipreqs", "pip-api"] xdg_home = ["appdirs"] diff --git a/scripts/lint.sh b/scripts/lint.sh index d31f426c0..e98e032d0 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -8,4 +8,4 @@ poetry run black --check -l 100 isort/ tests/ poetry run isort --profile hug --check --diff isort/ tests/ poetry run flake8 isort/ tests/ --max-line 100 --ignore F403,F401,W503,E203 poetry run safety check -poetry run bandit -r isort/ +poetry run bandit -r isort/ -x isort/_vendored diff --git a/setup.cfg b/setup.cfg index f3badbe24..2d729bf3e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,3 +18,4 @@ testpaths = tests [flake8] max-line-length = 100 ignore = F403,F401,W503,E203 +exclude = _vendored From 5281ff2004f61aec15f13d76f6ef57a0b87189a3 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 28 Jun 2020 23:05:17 -0700 Subject: [PATCH 0653/1439] Remove app dirs dependency fix issue #1234 --- isort/settings.py | 16 ---------------- pyproject.toml | 7 ++----- 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/isort/settings.py b/isort/settings.py index 52ddfdf95..c6640999b 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -28,14 +28,6 @@ from .wrap_modes import WrapModes from .wrap_modes import from_string as wrap_mode_from_string -try: - import appdirs - - if appdirs.system == "darwin": - appdirs.system = "linux2" # pragma: no cover -except ImportError: - appdirs = None - SUPPORTED_EXTENSIONS = (".py", ".pyi", ".pyx") FILE_SKIP_COMMENTS: Tuple[str, ...] = ( "isort:" + "skip_file", @@ -62,14 +54,6 @@ ".editorconfig": ("*", "*.py", "**.py", "*.{py}"), } FALLBACK_CONFIG_SECTIONS: Tuple[str, ...] = ("isort", "tool:isort", "tool.isort") -FALLBACK_CONFIGS: Tuple[str, ...] -if appdirs: - FALLBACK_CONFIGS = ( - appdirs.user_config_dir(".isort.cfg"), - appdirs.user_config_dir(".editorconfig"), - ) -else: # pragma: no cover - FALLBACK_CONFIGS = ("~/.isort.cfg", "~/.editorconfig") IMPORT_HEADING_PREFIX = "import_heading_" KNOWN_PREFIX = "known_" diff --git a/pyproject.toml b/pyproject.toml index 844aba9c6..187f121a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,16 +31,14 @@ urls = { Changelog = "https://github.com/timothycrosley/isort/blob/master/CHANGE [tool.poetry.dependencies] python = "^3.6" -appdirs = {version = "^1.4.0", optional = true} pipreqs = {version = "*", optional = true} requirementslib = {version = "*", optional = true} tomlkit = {version = ">=0.5.3", optional = true} pip-api = {version = "*", optional = true} [tool.poetry.extras] -pipfile = ["pipreqs", "tomlkit", "requirementslib", "pip-shims<=0.3.4"] -requirements = ["pipreqs", "pip-api"] -xdg_home = ["appdirs"] +pipfile_deprecated_finder = ["pipreqs", "tomlkit", "requirementslib", "pip-shims<=0.3.4"] +requirements_deprecated_finder = ["pipreqs", "pip-api"] [tool.poetry.dev-dependencies] vulture = "^1.0" @@ -58,7 +56,6 @@ hypothesis-auto = { version = "^1.0.0" } examples = { version = "^1.0.0" } cruft = { version = "^1.1" } portray = { version = "^1.3.0" } -appdirs = "^1.4" pipfile = "^0.0.2" requirementslib = "^1.5" pipreqs = "^0.4.9" From 367272577ad718510c963ecec25eccbe138773f8 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 28 Jun 2020 23:21:39 -0700 Subject: [PATCH 0654/1439] Updated changelog --- CHANGELOG.md | 4 ++++ docs/configuration/options.md | 6 +++--- isort/main.py | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64852aaa6..033763085 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,10 @@ Changelog - isort now works on contiguous sections of imports, instead of one whole file at a time. - isort now formats all nested "as" imports in the "from" form. `import x.y as a` becomes `from x import y as a`. - `keep_direct_and_as_imports` option now defaults to `True`. + - `appdirs` is no longer supported. Unless manually specified, config should be project config only. + - `toml` is now installed as a vendorized module, meaning pyproject.toml based config is always supported. + - Completely new Python API, old version is removed and no longer accessible. + - New module placement logic and module fully replaces old finders. Old approach is still available via `--old-finders`. Internal: - isort now utilizes mypy and typing to filter out typing related issues before deployment. diff --git a/docs/configuration/options.md b/docs/configuration/options.md index dfd22a7f9..82fff98f8 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -32,7 +32,7 @@ Force specific imports to the top of their appropriate section. Files that sort imports should skip over. If you want to skip multiple files you should specify twice: --skip file1 --skip file2. **Type:** Frozenset -**Default:** `frozenset({'.git', '.eggs', '.hg', '.mypy_cache', 'buck-out', 'dist', '.pants.d', '.venv', '.nox', 'build', '.tox', '_build', 'node_modules', 'venv'})` +**Default:** `frozenset({'venv', '.nox', 'node_modules', 'buck-out', '.mypy_cache', '.tox', '.pants.d', '.venv', 'build', '.git', '.eggs', '_build', 'dist', '.hg'})` **Python & Config File Name:** skip **CLI Flags:** @@ -144,7 +144,7 @@ Force isort to recognize a module as being part of the current python project. Force isort to recognize a module as part of Python's standard library. **Type:** Frozenset -**Default:** `frozenset({'distutils', 'reprlib', 'site', 'importlib', 'cmath', 'symbol', 'zipfile', 'math', 'weakref', 'configparser', 'array', 'cgi', 'io', 'stringprep', 'ensurepip', 'posixpath', 'turtledemo', 'cProfile', 'heapq', 'tty', 'doctest', 'macpath', 'spwd', 'sched', 'csv', 'atexit', 'tarfile', 'dbm', 'wave', 'http', 'sunau', 'resource', 'pdb', 'turtle', 'socket', 'html', 'runpy', 'colorsys', 'audioop', 'select', 'argparse', 'gettext', 'random', 'xmlrpc', 'json', 'types', 'ntpath', 'collections', 'poplib', 'mimetypes', 'copyreg', 'rlcompleter', 'warnings', 'lib2to3', 'tkinter', 'functools', 'signal', 'calendar', 'termios', 'wsgiref', 'difflib', 'fractions', 'logging', 'ossaudiodev', 'sndhdr', 'fnmatch', '_dummy_thread', 'numbers', 'typing', 'nntplib', 'email', 'imghdr', 'urllib', 'fcntl', 'msilib', 'multiprocessing', 'uuid', 'operator', 'pickle', 'mailbox', 'getpass', 'platform', 'getopt', 'marshal', 'sqlite3', 'ftplib', 'pwd', 'abc', 'codecs', 'optparse', 'smtplib', 'keyword', 'datetime', 'pathlib', 'curses', 'unicodedata', 'pprint', 'codeop', 'ast', 'concurrent', 'tracemalloc', 'xdrlib', 'imaplib', 'gzip', 'gc', 'shelve', 'hashlib', 'fileinput', 'statistics', 'copy', 'smtpd', 'token', 'zipimport', 'fpectl', 'builtins', 'py_compile', 'binascii', 'dataclasses', 'os', 'pickletools', 'lzma', 'netrc', 'cmd', 'contextvars', 'pydoc', 'sre_constants', 'pty', 'binhex', 'bdb', 'glob', 'aifc', 'errno', 'xml', 'decimal', 'syslog', 'code', 'inspect', 'pstats', 'venv', 'winsound', 'secrets', 'compileall', 'readline', 'telnetlib', 'timeit', 'uu', 'profile', 'enum', 'faulthandler', 'quopri', 'asyncore', 'contextlib', 'nis', 'hmac', 'textwrap', 'winreg', 'filecmp', 'modulefinder', 'cgitb', 'encodings', 'selectors', 'pipes', 'bz2', 'pkgutil', 'asyncio', 'imp', 'zipapp', 'grp', '_thread', 'crypt', 'subprocess', 'traceback', 'queue', 'tabnanny', 'locale', 'trace', 'string', 'struct', 'ipaddress', 'tokenize', 'ctypes', 'test', 'itertools', 'tempfile', 'formatter', 'ssl', 'bisect', 'shlex', 're', 'parser', 'webbrowser', 'posix', 'sysconfig', 'shutil', 'linecache', 'asynchat', 'base64', 'plistlib', 'unittest', 'msvcrt', 'chunk', 'threading', 'stat', 'mmap', 'pyclbr', 'sys', 'socketserver', 'dis', 'dummy_threading', 'symtable', 'mailcap', 'time', 'zlib'})` +**Default:** `frozenset({'_thread', 'asyncio', 'cgitb', 'hmac', 'ctypes', 'multiprocessing', 'stat', 'functools', 'posixpath', 'atexit', 'configparser', 'tarfile', 'numbers', 'errno', 'gzip', 'cgi', 'concurrent', 'pwd', 'heapq', 'sqlite3', 'select', 'spwd', 'csv', 'distutils', 'syslog', 'time', 'quopri', 'shlex', 'audioop', 'fileinput', 'doctest', 'queue', 'email', 'pdb', 'fpectl', 'winsound', 'fcntl', 'glob', 'operator', 'colorsys', 'locale', 'contextlib', 'urllib', 'uu', 'venv', 'imaplib', 'pstats', 'dis', 'plistlib', 'sysconfig', 'lzma', 'weakref', 'unittest', 'signal', 'compileall', 'html', 'secrets', 'tracemalloc', 'aifc', 'profile', 'termios', 'test', 'turtledemo', 'tty', 'mailbox', 'ftplib', 'webbrowser', 'zipfile', 'asyncore', 're', 'unicodedata', 'linecache', 'json', 'types', 'shelve', 'pty', 'os', 'threading', 'bz2', 'encodings', 'grp', 'math', 'cProfile', 'builtins', 'inspect', 'collections', 'nis', 'chunk', 'turtle', 'uuid', 'getopt', 'symtable', 'token', 'dataclasses', 'array', 'codecs', 'pipes', 'itertools', 'netrc', 'binascii', 'telnetlib', 'filecmp', 'winreg', 'zipimport', 'symbol', 'decimal', 'difflib', 'traceback', 'mmap', 'smtplib', 'argparse', 'string', 'poplib', 'rlcompleter', 'wave', 'site', 'macpath', 'zlib', 'bdb', 'socketserver', 'cmath', 'modulefinder', 'bisect', 'mimetypes', 'statistics', 'trace', 'binhex', 'fnmatch', 'pkgutil', 'pyclbr', 'xml', 'readline', 'xmlrpc', 'tokenize', 'resource', 'keyword', 'copyreg', 'getpass', 'ast', 'abc', 'ipaddress', 'sched', 'runpy', 'crypt', 'textwrap', 'contextvars', 'random', 'marshal', 'ssl', 'selectors', 'timeit', 'ntpath', 'ensurepip', 'parser', 'msvcrt', 'sndhdr', 'typing', 'formatter', 'pathlib', 'platform', 'lib2to3', 'base64', 'ossaudiodev', 'nntplib', 'dbm', 'asynchat', 'mailcap', 'copy', 'reprlib', 'tempfile', 'enum', 'posix', 'dummy_threading', 'faulthandler', 'py_compile', 'gc', 'hashlib', 'fractions', 'smtpd', 'pprint', 'zipapp', 'tabnanny', 'sre_constants', 'logging', 'sunau', 'pickletools', 'importlib', 'stringprep', 'struct', 'subprocess', 'optparse', 'tkinter', 'http', '_dummy_thread', 'curses', 'socket', 'shutil', 'wsgiref', 'gettext', 'pydoc', 'sys', 'pickle', 'io', 'imghdr', 'xdrlib', 'msilib', 'cmd', 'datetime', 'imp', 'codeop', 'calendar', 'code', 'warnings'})` **Python & Config File Name:** known_standard_library **CLI Flags:** @@ -661,7 +661,7 @@ Use the old deprecated finder logic that relies on environment introspection mag **CLI Flags:** - --old-finders - - --magic + - --magic-placement ## Check Checks the file for unsorted / unformatted imports and prints them to the command line without modifying the file. diff --git a/isort/main.py b/isort/main.py index a114a0b9a..b136b4907 100644 --- a/isort/main.py +++ b/isort/main.py @@ -553,7 +553,7 @@ def _build_arg_parser() -> argparse.ArgumentParser: ) parser.add_argument( "--old-finders", - "--magic", + "--magic-placement", dest="old_finders", action="store_true", help="Use the old deprecated finder logic that relies on environment introspection magic.", From 0963c00cd483770992033d6d18d020f4ca013404 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 28 Jun 2020 23:36:47 -0700 Subject: [PATCH 0655/1439] Add introduction stub --- docs/major_releases/introducing_isort_5.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 docs/major_releases/introducing_isort_5.md diff --git a/docs/major_releases/introducing_isort_5.md b/docs/major_releases/introducing_isort_5.md new file mode 100644 index 000000000..e3c426a12 --- /dev/null +++ b/docs/major_releases/introducing_isort_5.md @@ -0,0 +1,6 @@ +Introducing isort 5.0.0! + +isort 5.0.0 is the first major release of isort in over 5 years and the first major refactoring os isort since it was conceived more than 10 years ago. +This does mean that there may be some pain with the upgrade process, but we believe the improvements will be well worth it. + +[Click here for attempt at full changelog with list of breaking changes.](https://timothycrosley.github.io/isort/CHANGELOG/) From c6420bb49079ca124e0040ead0862fdf70419663 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 29 Jun 2020 00:18:30 -0700 Subject: [PATCH 0656/1439] Initial release guide --- docs/major_releases/introducing_isort_5.md | 59 ++++++++++++++++++++-- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/docs/major_releases/introducing_isort_5.md b/docs/major_releases/introducing_isort_5.md index e3c426a12..d75b3fbd4 100644 --- a/docs/major_releases/introducing_isort_5.md +++ b/docs/major_releases/introducing_isort_5.md @@ -1,6 +1,57 @@ -Introducing isort 5.0.0! - -isort 5.0.0 is the first major release of isort in over 5 years and the first major refactoring os isort since it was conceived more than 10 years ago. +isort 5.0.0 is the first significant release of isort in over five years and the first major refactoring of isort since it conceived more than ten years ago. This does mean that there may be some pain with the upgrade process, but we believe the improvements will be well worth it. -[Click here for attempt at full changelog with list of breaking changes.](https://timothycrosley.github.io/isort/CHANGELOG/) +[Click here for an attempt at full changelog with a list of breaking changes.](https://timothycrosley.github.io/isort/CHANGELOG/) +[Try isort 5 right now from your browser!](https://timothycrosley.github.io/isort/docs/interactive/try/) + +So why the massive change? + +# Profile Support +``` +isort --profile black +isort --profile django +isort --profile pycharm +isort --profile google +isort --profile open_stack +isort --profile plone +isort --profile attrs +isort --profile hug +``` + +isort is very configurable. That's great, but it can be overwhelming, both for users and for the isort project. isort now comes with profiles for the most common isort configurations, +so you likely will not need to configure anything at all. + +# Sort imports **anywhere** + +```python3 +import a # <- These are sorted +import b + +b.install(a) + +import os # <- And these are sorted +import sys + + +def my_function(): + import x # <- Even these are sorted! + import z +``` + +isort 5 will find and sort contiguous section of imports no matter where they are. +It also allows you to place code in-between imports without any hacks required. + +# Streaming architecture + +```python3 +import a +import b +... +∞ +``` +isort has been refactored to use a streaming architecture. This means it can sort files of *any* size (even larger than the Python interpreter supports!) without breaking a sweat. +It also means that even when sorting imports in smaller files, it is faster and more resource-efficient. + +# Consistent behavior across **all** environments. +Sorting the same file with the same configuration should give you the same output no matter what computer or OS you are running. Extensive effort has been placed around refactoring +how modules are placed and how configuration files are loaded to ensure this is the case. From dff9af3efa32db4e3a7761f93469c6d305bb37a5 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 29 Jun 2020 00:46:19 -0700 Subject: [PATCH 0657/1439] Update major release overview --- docs/major_releases/introducing_isort_5.md | 75 +++++++++++++++++++--- 1 file changed, 65 insertions(+), 10 deletions(-) diff --git a/docs/major_releases/introducing_isort_5.md b/docs/major_releases/introducing_isort_5.md index d75b3fbd4..2753db161 100644 --- a/docs/major_releases/introducing_isort_5.md +++ b/docs/major_releases/introducing_isort_5.md @@ -1,4 +1,5 @@ -isort 5.0.0 is the first significant release of isort in over five years and the first major refactoring of isort since it conceived more than ten years ago. +isort 5.0.0 is the first major release of isort in over five years and the first significant refactoring of isort since it conceived more than ten years ago. +It's also the first version to require Python3 (Python 3.6+ at that!) to run - though it can still be run on source files from any version of Python. This does mean that there may be some pain with the upgrade process, but we believe the improvements will be well worth it. [Click here for an attempt at full changelog with a list of breaking changes.](https://timothycrosley.github.io/isort/CHANGELOG/) @@ -8,18 +9,18 @@ So why the massive change? # Profile Support ``` -isort --profile black -isort --profile django -isort --profile pycharm -isort --profile google -isort --profile open_stack -isort --profile plone -isort --profile attrs -isort --profile hug +isort --profile black . +isort --profile django . +isort --profile pycharm . +isort --profile google . +isort --profile open_stack . +isort --profile plone . +isort --profile attrs . +isort --profile hug . ``` isort is very configurable. That's great, but it can be overwhelming, both for users and for the isort project. isort now comes with profiles for the most common isort configurations, -so you likely will not need to configure anything at all. +so you likely will not need to configure anything at all. This also means that as a project, isort can run extensive tests against these specific profiles to ensure nothing breaks over time. # Sort imports **anywhere** @@ -55,3 +56,57 @@ It also means that even when sorting imports in smaller files, it is faster and # Consistent behavior across **all** environments. Sorting the same file with the same configuration should give you the same output no matter what computer or OS you are running. Extensive effort has been placed around refactoring how modules are placed and how configuration files are loaded to ensure this is the case. + + +# Cython support + +```python3 +cimport ctime +from cpython cimport PyLong_FromVoidPtr +from cpython cimport bool as py_bool +from cython.operator cimport dereference as deref +from cython.operator cimport preincrement as preinc +from libc.stdint cimport uint64_t, uintptr_t +from libc.stdlib cimport atoi, calloc, free, malloc +from libc.string cimport memcpy, strlen +from libcpp cimport bool as cpp_bool +from libcpp.map cimport map as cpp_map +from libcpp.pair cimport pair as cpp_pair +from libcpp.string cimport string as cpp_string +from libcpp.vector cimport vector as cpp_vector +from multimap cimport multimap as cpp_multimap +from wstring cimport wstring as cpp_wstring +``` + +isort 5 adds seamless support for Cython (`.pyx`) files. + +# First class Python API + +```python3 +import isort + +isort.code(""" +import b +import a +""") == """ +import a +import b +""" +``` + +isort now exposes its programmatic API as a first-class citizen. This API makes it easy to extend or use isort in your own Python project. You can see the full documentation for this new API [here](https://timothycrosley.github.io/isort/reference/isort/api/). + +# Solid base for the future + +A major focus for the release was to give isort a solid foundation for the next 5-10 years of the projects life. +isort has been refactored into functional components that are easily testable. The project now has 100% code coverage. +It utilizes tools like [hypothesis](https://hypothesis.readthedocs.io/en/latest/) to reduce the number of unexpected errors. +It went from fully dynamic to fully static typing using mypy. Finally, it utilizes the latest linters both on (like [deepsource](https://deepsource.io/gh/timothycrosley/isort/)) and offline (like [flake8](https://flake8.pycqa.org/en/latest/)) to help ensure a higher bar for all code contributions into the future. + +# Give 5.0.0 a try! + +[Try isort 5 right now from your browser!](https://timothycrosley.github.io/isort/docs/interactive/try/) + +or + +Install isort locally using `pip3 install isort` From 7fbe8b1bd0fb932251fd56b1b91d90bc825e65f7 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 29 Jun 2020 11:06:06 +0300 Subject: [PATCH 0658/1439] Typos and consistency --- docs/major_releases/introducing_isort_5.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/major_releases/introducing_isort_5.md b/docs/major_releases/introducing_isort_5.md index 2753db161..8e0be01ed 100644 --- a/docs/major_releases/introducing_isort_5.md +++ b/docs/major_releases/introducing_isort_5.md @@ -1,5 +1,5 @@ -isort 5.0.0 is the first major release of isort in over five years and the first significant refactoring of isort since it conceived more than ten years ago. -It's also the first version to require Python3 (Python 3.6+ at that!) to run - though it can still be run on source files from any version of Python. +isort 5.0.0 is the first major release of isort in over five years and the first significant refactoring of isort since it was conceived more than ten years ago. +It's also the first version to require Python 3 (Python 3.6+ at that!) to run - though it can still be run on source files from any version of Python. This does mean that there may be some pain with the upgrade process, but we believe the improvements will be well worth it. [Click here for an attempt at full changelog with a list of breaking changes.](https://timothycrosley.github.io/isort/CHANGELOG/) @@ -7,7 +7,7 @@ This does mean that there may be some pain with the upgrade process, but we beli So why the massive change? -# Profile Support +# Profile support ``` isort --profile black . isort --profile django . @@ -53,7 +53,8 @@ import b isort has been refactored to use a streaming architecture. This means it can sort files of *any* size (even larger than the Python interpreter supports!) without breaking a sweat. It also means that even when sorting imports in smaller files, it is faster and more resource-efficient. -# Consistent behavior across **all** environments. +# Consistent behavior across **all** environments + Sorting the same file with the same configuration should give you the same output no matter what computer or OS you are running. Extensive effort has been placed around refactoring how modules are placed and how configuration files are loaded to ensure this is the case. @@ -98,10 +99,10 @@ isort now exposes its programmatic API as a first-class citizen. This API makes # Solid base for the future -A major focus for the release was to give isort a solid foundation for the next 5-10 years of the projects life. +A major focus for the release was to give isort a solid foundation for the next 5-10 years of the project's life. isort has been refactored into functional components that are easily testable. The project now has 100% code coverage. -It utilizes tools like [hypothesis](https://hypothesis.readthedocs.io/en/latest/) to reduce the number of unexpected errors. -It went from fully dynamic to fully static typing using mypy. Finally, it utilizes the latest linters both on (like [deepsource](https://deepsource.io/gh/timothycrosley/isort/)) and offline (like [flake8](https://flake8.pycqa.org/en/latest/)) to help ensure a higher bar for all code contributions into the future. +It utilizes tools like [Hypothesis](https://hypothesis.readthedocs.io/en/latest/) to reduce the number of unexpected errors. +It went from fully dynamic to fully static typing using mypy. Finally, it utilizes the latest linters both on (like [DeepSource](https://deepsource.io/gh/timothycrosley/isort/)) and offline (like [Flake8](https://flake8.pycqa.org/en/latest/)) to help ensure a higher bar for all code contributions into the future. # Give 5.0.0 a try! From 21718530997e5381fd31ddcf9cb1de8bc2f3de2c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 30 Jun 2020 22:50:12 -0700 Subject: [PATCH 0659/1439] Exclude vendored modules --- .deepsource.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/.deepsource.toml b/.deepsource.toml index a62405693..81eded698 100644 --- a/.deepsource.toml +++ b/.deepsource.toml @@ -4,6 +4,7 @@ test_patterns = ["tests/**"] exclude_patterns = [ "tests/**", "isort/_future/**", + "isort/_vendored/**", ] From f86c9f9a1538be47aaf44c74b5bff836c3c2fc6d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 30 Jun 2020 22:54:55 -0700 Subject: [PATCH 0660/1439] Remove unecesarry list comprehension --- scripts/build_config_option_docs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build_config_option_docs.py b/scripts/build_config_option_docs.py index 75b46d294..1a17a6e15 100755 --- a/scripts/build_config_option_docs.py +++ b/scripts/build_config_option_docs.py @@ -56,7 +56,7 @@ def human(name: str) -> str: if name in HUMAN_NAME: return HUMAN_NAME[name] - return " ".join([part.capitalize() for part in name.replace("-", "_").split("_")]) + return " ".join(part.capitalize() for part in name.replace("-", "_").split("_")) def config_options() -> Generator[ConfigOption, None, None]: From 35c1f7030760bcd2d752dffb5598cee9050efed2 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 30 Jun 2020 23:00:56 -0700 Subject: [PATCH 0661/1439] Fix redefinition of global variable in method signature --- isort/place.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/isort/place.py b/isort/place.py index aeced034f..1ac79bfb6 100644 --- a/isort/place.py +++ b/isort/place.py @@ -90,5 +90,7 @@ def _is_package(path: Path) -> bool: return exists_case_sensitive(str(path)) and path.is_dir() -def _src_path_is_module(src_path: Path, module: str) -> bool: - return module == src_path.name and src_path.is_dir() and exists_case_sensitive(str(src_path)) +def _src_path_is_module(src_path: Path, module_name: str) -> bool: + return ( + module_name == src_path.name and src_path.is_dir() and exists_case_sensitive(str(src_path)) + ) From 91b03ad71dde0c0357d56f7f3ecc8cda04ef2e27 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 30 Jun 2020 23:11:22 -0700 Subject: [PATCH 0662/1439] Fix problems found by deepsource --- isort/api.py | 4 ++-- isort/main.py | 2 +- isort/place.py | 1 - isort/settings.py | 7 ------- isort/setuptools_commands.py | 2 +- scripts/build_config_option_docs.py | 2 +- 6 files changed, 5 insertions(+), 13 deletions(-) diff --git a/isort/api.py b/isort/api.py index 37eb56c55..da9fb075f 100644 --- a/isort/api.py +++ b/isort/api.py @@ -21,8 +21,8 @@ show_unified_diff, ) from .io import Empty -from .place import module as place_module -from .place import module_with_reason as place_module_with_reason +from .place import module as place_module # skipcq: PYL-W0611 (intended export of public API) +from .place import module_with_reason as place_module_with_reason # skipcq: PYL-W0611 (^) from .settings import DEFAULT_CONFIG, FILE_SKIP_COMMENTS, Config CIMPORT_IDENTIFIERS = ("cimport ", "cimport*", "from.cimport") diff --git a/isort/main.py b/isort/main.py index b136b4907..ef02b1afa 100644 --- a/isort/main.py +++ b/isort/main.py @@ -583,7 +583,7 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: def _preconvert(item): """Preconverts objects from native types into JSONifyiable types""" - if isinstance(item, frozenset) or isinstance(item, set): + if isinstance(item, (set, frozenset)): return list(item) elif isinstance(item, WrapModes): return item.name diff --git a/isort/place.py b/isort/place.py index 1ac79bfb6..36270f0ed 100644 --- a/isort/place.py +++ b/isort/place.py @@ -1,6 +1,5 @@ """Contains all logic related to placing an import within a certain section.""" import importlib -import os from fnmatch import fnmatch from functools import lru_cache from pathlib import Path diff --git a/isort/settings.py b/isort/settings.py index c6640999b..9c9eff1ee 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -452,13 +452,6 @@ def _get_config_data(file_path: str, sections: Tuple[str]) -> Dict[str, Any]: for key in section.split("."): config_section = config_section.get(key, {}) settings.update(config_section) - else: # pragma: no cover - if "[tool.isort]" in config_file.read(): - warnings.warn( - f"Found {file_path} with [tool.isort] section, but toml package is not " - f"installed. To configure isort with {file_path}, install with " - "'isort[pyproject]'." - ) else: if file_path.endswith(".editorconfig"): line = "\n" diff --git a/isort/setuptools_commands.py b/isort/setuptools_commands.py index 62c595fbe..9e02d6289 100644 --- a/isort/setuptools_commands.py +++ b/isort/setuptools_commands.py @@ -25,7 +25,7 @@ def initialize_options(self) -> None: def finalize_options(self) -> None: "Get options from config files." - self.arguments: Dict[str, Any] = {} + self.arguments: Dict[str, Any] = {} # skipcq: PYL-W0201 self.arguments["settings_path"] = os.getcwd() def distribution_files(self) -> Iterator[str]: diff --git a/scripts/build_config_option_docs.py b/scripts/build_config_option_docs.py index 1a17a6e15..11c2b1a67 100755 --- a/scripts/build_config_option_docs.py +++ b/scripts/build_config_option_docs.py @@ -2,7 +2,7 @@ import os from typing import Any, Generator, Iterable, Type -from isort._future import dataclass, field +from isort._future import dataclass from isort.main import _build_arg_parser from isort.settings import _DEFAULT_SETTINGS as config From d358eb26b1e4289871509608452b2df80bcde4a5 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 30 Jun 2020 23:18:43 -0700 Subject: [PATCH 0663/1439] Remove unused import --- isort/settings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/isort/settings.py b/isort/settings.py index 9c9eff1ee..9e9b3bc21 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -11,7 +11,6 @@ import posixpath import re import sys -import warnings from distutils.util import strtobool as _as_bool from functools import lru_cache from pathlib import Path From 937e7b2e4f661cd9eb922f1f5036b09eb5b01c8f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 30 Jun 2020 23:59:14 -0700 Subject: [PATCH 0664/1439] Add introduction header --- docs/configuration/options.md | 6 ++++-- docs/major_releases/introducing_isort_5.md | 6 ++++-- scripts/build_config_option_docs.py | 2 ++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/configuration/options.md b/docs/configuration/options.md index 82fff98f8..6ffcaafd4 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -5,6 +5,8 @@ As a code formatter isort has opinions. However, it also allows you to have your isort will disagree but commit to your way of formatting. To enable this, isort exposes a plethora of options to specify how you want your imports sorted, organized, and formatted. +Too busy to build your perfect isort configuration? For curated common configurations, see isort's [built-in profiles](https://timothycrosley.github.io/isort/docs/configuration/profiles/). + ## Python Version Tells isort to set the known standard library based on the the specified Python version. Default is to assume any Python 3 version could be the target, and use a union off all stdlib modules across versions. If auto is specified, the version of the interpreter used to run isort (currently: 38) will be used. @@ -32,7 +34,7 @@ Force specific imports to the top of their appropriate section. Files that sort imports should skip over. If you want to skip multiple files you should specify twice: --skip file1 --skip file2. **Type:** Frozenset -**Default:** `frozenset({'venv', '.nox', 'node_modules', 'buck-out', '.mypy_cache', '.tox', '.pants.d', '.venv', 'build', '.git', '.eggs', '_build', 'dist', '.hg'})` +**Default:** `frozenset({'node_modules', '.hg', '_build', 'dist', 'build', '.eggs', '.tox', '.nox', 'buck-out', '.mypy_cache', '.pants.d', '.venv', 'venv', '.git'})` **Python & Config File Name:** skip **CLI Flags:** @@ -144,7 +146,7 @@ Force isort to recognize a module as being part of the current python project. Force isort to recognize a module as part of Python's standard library. **Type:** Frozenset -**Default:** `frozenset({'_thread', 'asyncio', 'cgitb', 'hmac', 'ctypes', 'multiprocessing', 'stat', 'functools', 'posixpath', 'atexit', 'configparser', 'tarfile', 'numbers', 'errno', 'gzip', 'cgi', 'concurrent', 'pwd', 'heapq', 'sqlite3', 'select', 'spwd', 'csv', 'distutils', 'syslog', 'time', 'quopri', 'shlex', 'audioop', 'fileinput', 'doctest', 'queue', 'email', 'pdb', 'fpectl', 'winsound', 'fcntl', 'glob', 'operator', 'colorsys', 'locale', 'contextlib', 'urllib', 'uu', 'venv', 'imaplib', 'pstats', 'dis', 'plistlib', 'sysconfig', 'lzma', 'weakref', 'unittest', 'signal', 'compileall', 'html', 'secrets', 'tracemalloc', 'aifc', 'profile', 'termios', 'test', 'turtledemo', 'tty', 'mailbox', 'ftplib', 'webbrowser', 'zipfile', 'asyncore', 're', 'unicodedata', 'linecache', 'json', 'types', 'shelve', 'pty', 'os', 'threading', 'bz2', 'encodings', 'grp', 'math', 'cProfile', 'builtins', 'inspect', 'collections', 'nis', 'chunk', 'turtle', 'uuid', 'getopt', 'symtable', 'token', 'dataclasses', 'array', 'codecs', 'pipes', 'itertools', 'netrc', 'binascii', 'telnetlib', 'filecmp', 'winreg', 'zipimport', 'symbol', 'decimal', 'difflib', 'traceback', 'mmap', 'smtplib', 'argparse', 'string', 'poplib', 'rlcompleter', 'wave', 'site', 'macpath', 'zlib', 'bdb', 'socketserver', 'cmath', 'modulefinder', 'bisect', 'mimetypes', 'statistics', 'trace', 'binhex', 'fnmatch', 'pkgutil', 'pyclbr', 'xml', 'readline', 'xmlrpc', 'tokenize', 'resource', 'keyword', 'copyreg', 'getpass', 'ast', 'abc', 'ipaddress', 'sched', 'runpy', 'crypt', 'textwrap', 'contextvars', 'random', 'marshal', 'ssl', 'selectors', 'timeit', 'ntpath', 'ensurepip', 'parser', 'msvcrt', 'sndhdr', 'typing', 'formatter', 'pathlib', 'platform', 'lib2to3', 'base64', 'ossaudiodev', 'nntplib', 'dbm', 'asynchat', 'mailcap', 'copy', 'reprlib', 'tempfile', 'enum', 'posix', 'dummy_threading', 'faulthandler', 'py_compile', 'gc', 'hashlib', 'fractions', 'smtpd', 'pprint', 'zipapp', 'tabnanny', 'sre_constants', 'logging', 'sunau', 'pickletools', 'importlib', 'stringprep', 'struct', 'subprocess', 'optparse', 'tkinter', 'http', '_dummy_thread', 'curses', 'socket', 'shutil', 'wsgiref', 'gettext', 'pydoc', 'sys', 'pickle', 'io', 'imghdr', 'xdrlib', 'msilib', 'cmd', 'datetime', 'imp', 'codeop', 'calendar', 'code', 'warnings'})` +**Default:** `frozenset({'distutils', 'webbrowser', 'codeop', 'binhex', 'tempfile', 'platform', 'struct', 'base64', 'calendar', 'ast', 'abc', 'pwd', 'spwd', 'plistlib', 'numbers', 'secrets', 'signal', 'grp', 'unittest', 'statistics', 'typing', 'imaplib', 'multiprocessing', 'uuid', 'wsgiref', 'email', 'tokenize', 'getpass', 'asynchat', 'imp', 'mmap', 'sys', 'colorsys', 'cmath', 'tabnanny', 'rlcompleter', 'json', 'sqlite3', 'pprint', 'copyreg', 'zipapp', 'contextvars', 'tarfile', 'telnetlib', 'fileinput', 'modulefinder', 'curses', 'syslog', 'imghdr', 'copy', 'msvcrt', 'cgi', 'weakref', 'datetime', 'dataclasses', 'optparse', 'locale', 'ossaudiodev', '_thread', 'fcntl', 'sched', 'zipfile', 'subprocess', 'formatter', 'operator', 'xml', 'faulthandler', 'dummy_threading', 'shlex', 'argparse', 'trace', 'ntpath', 'unicodedata', 'uu', 'symbol', 'atexit', 'ipaddress', 'runpy', 'binascii', 'doctest', 'termios', 'lzma', 'pdb', 'shutil', 'readline', 'zlib', 're', 'zipimport', 'audioop', 'quopri', 'sndhdr', 'venv', 'msilib', 'array', 'symtable', 'encodings', 'timeit', 'nis', 'http', 'fnmatch', 'codecs', 'stat', 'csv', 'site', 'mimetypes', 'inspect', 'poplib', 'getopt', 'pipes', 'select', 'configparser', 'dbm', 'time', 'html', 'os', 'logging', 'types', 'chunk', 'winreg', 'nntplib', 'compileall', 'pydoc', 'bisect', 'warnings', 'py_compile', 'random', 'importlib', 'cProfile', 'tty', 'linecache', 'shelve', 'parser', 'gc', 'mailbox', 'reprlib', 'token', 'turtledemo', 'pty', 'contextlib', 'hmac', 'socket', 'asyncore', 'keyword', 'pkgutil', 'aifc', 'bdb', 'queue', 'xdrlib', 'ctypes', 'dis', 'ftplib', 'gzip', 'smtplib', 'code', 'pickle', 'socketserver', 'fpectl', 'resource', 'sunau', 'io', 'bz2', 'decimal', 'traceback', 'hashlib', 'smtpd', 'itertools', 'concurrent', 'textwrap', 'pstats', 'tracemalloc', 'sysconfig', 'errno', 'mailcap', 'pickletools', 'test', 'pathlib', 'functools', 'tkinter', 'xmlrpc', 'ensurepip', 'string', 'enum', '_dummy_thread', 'profile', 'glob', 'math', 'posixpath', 'builtins', 'fractions', 'crypt', 'urllib', 'gettext', 'ssl', 'sre_constants', 'collections', 'asyncio', 'netrc', 'filecmp', 'turtle', 'stringprep', 'selectors', 'heapq', 'macpath', 'winsound', 'marshal', 'posix', 'pyclbr', 'cgitb', 'difflib', 'threading', 'wave', 'cmd', 'lib2to3'})` **Python & Config File Name:** known_standard_library **CLI Flags:** diff --git a/docs/major_releases/introducing_isort_5.md b/docs/major_releases/introducing_isort_5.md index 8e0be01ed..fe9198a89 100644 --- a/docs/major_releases/introducing_isort_5.md +++ b/docs/major_releases/introducing_isort_5.md @@ -1,3 +1,5 @@ +# Introducing isort 5 + isort 5.0.0 is the first major release of isort in over five years and the first significant refactoring of isort since it was conceived more than ten years ago. It's also the first version to require Python 3 (Python 3.6+ at that!) to run - though it can still be run on source files from any version of Python. This does mean that there may be some pain with the upgrade process, but we believe the improvements will be well worth it. @@ -5,7 +7,7 @@ This does mean that there may be some pain with the upgrade process, but we beli [Click here for an attempt at full changelog with a list of breaking changes.](https://timothycrosley.github.io/isort/CHANGELOG/) [Try isort 5 right now from your browser!](https://timothycrosley.github.io/isort/docs/interactive/try/) -So why the massive change? +So why the massive change? # Profile support ``` @@ -108,6 +110,6 @@ It went from fully dynamic to fully static typing using mypy. Finally, it utiliz [Try isort 5 right now from your browser!](https://timothycrosley.github.io/isort/docs/interactive/try/) -or +or Install isort locally using `pip3 install isort` diff --git a/scripts/build_config_option_docs.py b/scripts/build_config_option_docs.py index 11c2b1a67..06fbe2b53 100755 --- a/scripts/build_config_option_docs.py +++ b/scripts/build_config_option_docs.py @@ -21,6 +21,8 @@ isort will disagree but commit to your way of formatting. To enable this, isort exposes a plethora of options to specify how you want your imports sorted, organized, and formatted. +Too busy to build your perfect isort configuration? For curated common configurations, see isort's [built-in profiles](https://timothycrosley.github.io/isort/docs/configuration/profiles/). + """ parser = _build_arg_parser() From a04e7a87cdd84b5b2a69bff3376ae19dd8ee5f6f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 1 Jul 2020 00:02:30 -0700 Subject: [PATCH 0665/1439] Fix changelog formatting --- CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 033763085..7b3eb634d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ Changelog ### 5.0.0 UNRELEASED **Breaking changes:** + - isort now requires Python 3.6+ to run but continues to support formatting on ALL versions of python including Python 2 code. - isort deprecates official support for Python 3.4, removing modules only in this release from known_standard_library: @@ -27,11 +28,13 @@ Changelog - New module placement logic and module fully replaces old finders. Old approach is still available via `--old-finders`. Internal: + - isort now utilizes mypy and typing to filter out typing related issues before deployment. - isort now utilizes black internally to ensure more consistent formatting. -Planned: - - profile support for common project types (black, django, google, etc) +- profile support for common project types (black, django, google, etc) + +- Much much more. There is some difficulty in fully capturing the extent of changes in this release - just because of how all encompassing the release is. See: [Github Issues](https://github.com/timothycrosley/isort/issues?q=is%3Aissue+is%3Aclosed) for more. ### 4.3.21 - June 25, 2019 - hot fix release - Fixed issue #957 - Long aliases and use_parentheses generates invalid syntax From 2ea917e8dad7116584c0c5927b43f25ca300f819 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 1 Jul 2020 00:04:04 -0700 Subject: [PATCH 0666/1439] Name and date 5.0.0 release --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b3eb634d..40a3f9c29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ Changelog ========= -### 5.0.0 UNRELEASED +### 5.0.0 Penny - July 4, 2020 **Breaking changes:** - isort now requires Python 3.6+ to run but continues to support formatting on ALL versions of python including From e248cd4ad4bf0424f0ab9d60155fcaee981d273a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 1 Jul 2020 00:04:26 -0700 Subject: [PATCH 0667/1439] Improve profile docs --- docs/configuration/profiles.md | 75 ++++++++++++++++++++++++++++++++++ scripts/build_profile_docs.py | 44 ++++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 docs/configuration/profiles.md create mode 100755 scripts/build_profile_docs.py diff --git a/docs/configuration/profiles.md b/docs/configuration/profiles.md new file mode 100644 index 000000000..792ff988c --- /dev/null +++ b/docs/configuration/profiles.md @@ -0,0 +1,75 @@ +Built-in Profile for isort +======== + +The following profiles are built into isort to allow easy interoperability with +common projects and code styles. + +To use any of the listed profiles, use `isort --profile PROFILE_NAME` from the command line, or `profile=PROFILE_NAME` in your configuration file. + + +#black + + + - **multi_line_output**: `3` + - **include_trailing_comma**: `True` + - **force_grid_wrap**: `0` + - **use_parentheses**: `True` + - **ensure_newline_before_comments**: `True` + +#django + + + - **combine_as_imports**: `True` + - **include_trailing_comma**: `True` + - **multi_line_output**: `5` + - **line_length**: `79` + +#pycharm + + + - **multi_line_output**: `3` + - **force_grid_wrap**: `2` + +#google + + + - **force_single_line**: `True` + - **force_sort_within_sections**: `True` + - **lexicographical**: `True` + - **single_line_exclusions**: `('typing',)` + +#open_stack + + + - **force_single_line**: `True` + - **force_sort_within_sections**: `True` + - **lexicographical**: `True` + +#plone + + + - **force_alphabetical_sort**: `True` + - **force_single_line**: `True` + - **ines_after_imports**: `2` + - **line_length**: `200` + +#attrs + + + - **atomic**: `True` + - **force_grid_wrap**: `0` + - **include_trailing_comma**: `True` + - **lines_after_imports**: `2` + - **lines_between_types**: `1` + - **multi_line_output**: `3` + - **not_skip**: `'__init__.py'` + - **use_parentheses**: `True` + +#hug + + + - **multi_line_output**: `3` + - **include_trailing_comma**: `True` + - **force_grid_wrap**: `0` + - **use_parentheses**: `True` + - **line_length**: `100` diff --git a/scripts/build_profile_docs.py b/scripts/build_profile_docs.py new file mode 100755 index 000000000..e0a86f89a --- /dev/null +++ b/scripts/build_profile_docs.py @@ -0,0 +1,44 @@ +#! /bin/env python +import os +from typing import Any, Generator, Iterable, Type, Dict + +from isort._future import dataclass +from isort.main import _build_arg_parser +from isort.profiles import profiles +from isort.settings import _DEFAULT_SETTINGS as config + +OUTPUT_FILE = os.path.abspath( + os.path.join(os.path.dirname(os.path.abspath(__file__)), "../docs/configuration/profiles.md") +) + +HEADER = """Built-in Profile for isort +======== + +The following profiles are built into isort to allow easy interoperability with +common projects and code styles. + +To use any of the listed profiles, use `isort --profile PROFILE_NAME` from the command line, or `profile=PROFILE_NAME` in your configuration file. + +""" + +def format_profile(profile_name: str, profile: Dict[str, Any]) -> str: + options = "\n".join(f" - **{name}**: `{repr(value)}`" for name, value in profile.items()) + return f""" +#{profile_name} + +{profile.get('descripiton', '')} +{options} +""" + + +def document_text() -> str: + return f"{HEADER}{''.join(format_profile(profile_name, profile) for profile_name, profile in profiles.items())}" + + +def write_document(): + with open(OUTPUT_FILE, "w") as output_file: + output_file.write(document_text()) + + +if __name__ == "__main__": + write_document() From 03a2ae05d263c0610256077a20ba0db1044bc0ba Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 1 Jul 2020 00:13:05 -0700 Subject: [PATCH 0668/1439] Change edit link to point users to develop branch --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 187f121a4..f9cfba290 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,6 +77,9 @@ isort = "isort.main:ISortCommand" [tool.poetry.plugins."pylama.linter"] isort = "isort = isort.pylama_isort:Linter" +[tool.portray.mkdocs] +edit_uri="https://github.com/timothycrosley/isort/edit/develop/" + [tool.portray.mkdocs.theme] name = "material" favicon = "art/logo.png" From 0de7d94f179841ad2d6ae5b01a3e1219668b4c36 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 1 Jul 2020 00:38:27 -0700 Subject: [PATCH 0669/1439] Add section for action comments --- docs/major_releases/introducing_isort_5.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/major_releases/introducing_isort_5.md b/docs/major_releases/introducing_isort_5.md index fe9198a89..4222752af 100644 --- a/docs/major_releases/introducing_isort_5.md +++ b/docs/major_releases/introducing_isort_5.md @@ -5,6 +5,7 @@ It's also the first version to require Python 3 (Python 3.6+ at that!) to run - This does mean that there may be some pain with the upgrade process, but we believe the improvements will be well worth it. [Click here for an attempt at full changelog with a list of breaking changes.](https://timothycrosley.github.io/isort/CHANGELOG/) + [Try isort 5 right now from your browser!](https://timothycrosley.github.io/isort/docs/interactive/try/) So why the massive change? @@ -83,6 +84,26 @@ from wstring cimport wstring as cpp_wstring isort 5 adds seamless support for Cython (`.pyx`) files. +# Action Comments + +```python3 +import e +import f + +# isort: off <- Turns isort parsing off + +import b +import a + +# isort: on <- Turns isort parsing back on + +import c +import d +``` + +isort 5 adds support for [Action Comments](https://timothycrosley.github.io/isort/docs/configuration/action_comments/) which provide a quick and convient way to control the flow of parsing within single source files. + + # First class Python API ```python3 From aff8ff465c09a93760c06a77197d00c17787dcdd Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 1 Jul 2020 00:38:38 -0700 Subject: [PATCH 0670/1439] Add section for action comments --- docs/configuration/action_comments.md | 99 +++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 docs/configuration/action_comments.md diff --git a/docs/configuration/action_comments.md b/docs/configuration/action_comments.md new file mode 100644 index 000000000..39d6ff751 --- /dev/null +++ b/docs/configuration/action_comments.md @@ -0,0 +1,99 @@ +# Action Comments + +The most basic way to configure the flow of isort within a single file is action comments. These comments are picked up and interpreted by the isort parser during parsing. + + +## isort: skip-file + +Tells isort to skip the entire file. + +Example: + +```python +# !/bin/python3 +# isort: skip-file +import os +import sys + +... +``` + +!!! warning + This should be placed as high in the file as reasonably possible. + Since isort uses a streaming architecture, it may have already completed some work before it reaches the comment. Usually, this is okay - but can be confusing if --diff or any interactive options are used from the command line. + + +## isort: skip + +If placed on the same line as (or within the continuation of a) an import statement, isort will not sort this import. + +Example: + +```python +import b +import a # isort: skip <- this will now stay below b +``` +!!! note + It is recommended to where possible use `# isort: off` and `# isort: on` instead as the behaviour is more explicit and predictable. + +## isort: off + +Turns isort parsing off. Every line after an `# isort: off` statement will be passed along unchanged until an `# isort: on` comment or the end of the file. + +Example: + +```python +import e +import f + +# isort: off + +import b +import a +``` + +## isort: on + +Turns isort parsing back on. This only makes sense if an `# isort: off` comment exists higher in the file! This allows you to have blocks of unsorted imports, around otherwise sorted ones. + +Example: + +```python + +import e +import f + +# isort: off + +import b +import a + +# isort: on + +import c +import d + +``` + +## isort: split + +Tells isort the current sort section is finished, and all future imports belong to a new sort grouping. + +Example: + +```python + +import e +import f + +# isort: split + +import a +import b +import c +import d + +``` + +!!! tip + isort split is exactly the same as placing an `# isort: on` immediately below an `# isort: off` From 664aca3391cce27aa285c4d2e66e208b1483a783 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 1 Jul 2020 01:02:58 -0700 Subject: [PATCH 0671/1439] Start quick start guide --- README.md | 2 +- docs/major_releases/introducing_isort_5.md | 10 ++++---- .../try.md => quick_start/0.-try.md} | 6 +++-- docs/quick_start/1.-install.md | 22 ++++++++++++++++++ .../interactive.css | 0 .../interactive.js | 2 +- .../isort-5.0.0-py3-none-any.whl | Bin 7 files changed, 34 insertions(+), 8 deletions(-) rename docs/{interactive/try.md => quick_start/0.-try.md} (86%) create mode 100644 docs/quick_start/1.-install.md rename docs/{interactive => quick_start}/interactive.css (100%) rename docs/{interactive => quick_start}/interactive.js (93%) rename docs/{interactive => quick_start}/isort-5.0.0-py3-none-any.whl (100%) diff --git a/README.md b/README.md index 5fa6d6f8b..3d0b6cb74 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ editors](https://github.com/timothycrosley/isort/wiki/isort-Plugins) to quickly sort all your imports. It requires Python 3.6+ to run but supports formatting Python 2 code too. -[Try isort now from your browser!](https://timothycrosley.github.io/isort/docs/interactive/try/) +[Try isort now from your browser!](https://timothycrosley.github.io/isort/docs/quick_start/0.-try/) ![Example Usage](https://raw.github.com/timothycrosley/isort/develop/example.gif) diff --git a/docs/major_releases/introducing_isort_5.md b/docs/major_releases/introducing_isort_5.md index 4222752af..75ead8c49 100644 --- a/docs/major_releases/introducing_isort_5.md +++ b/docs/major_releases/introducing_isort_5.md @@ -6,7 +6,7 @@ This does mean that there may be some pain with the upgrade process, but we beli [Click here for an attempt at full changelog with a list of breaking changes.](https://timothycrosley.github.io/isort/CHANGELOG/) -[Try isort 5 right now from your browser!](https://timothycrosley.github.io/isort/docs/interactive/try/) +[Try isort 5 right now from your browser!](https://timothycrosley.github.io/isort/docs/quick_start/0.-try/) So why the massive change? @@ -129,8 +129,10 @@ It went from fully dynamic to fully static typing using mypy. Finally, it utiliz # Give 5.0.0 a try! -[Try isort 5 right now from your browser!](https://timothycrosley.github.io/isort/docs/interactive/try/) +[Try isort 5 right now from your browser!](https://timothycrosley.github.io/isort/docs/quick_start/0.-try/) -or +OR -Install isort locally using `pip3 install isort` +Install isort locally using `pip3 install isort`. + +[Click here for full installation instructions.](https://timothycrosley.github.io/isort/docs/quick_start/1.-install/) diff --git a/docs/interactive/try.md b/docs/quick_start/0.-try.md similarity index 86% rename from docs/interactive/try.md rename to docs/quick_start/0.-try.md index af6bbe792..52e207f7f 100644 --- a/docs/interactive/try.md +++ b/docs/quick_start/0.-try.md @@ -13,7 +13,7 @@ Use our live isort editor to see how isort can help improve the formatting of yo - + @@ -43,6 +43,8 @@ import b, a
- +
Like what you saw? Installing isort to use locally is as simple as `pip3 install isort`. + +[Click here for full installation instructions.](https://timothycrosley.github.io/isort/docs/quick_start/1.-install/) diff --git a/docs/quick_start/1.-install.md b/docs/quick_start/1.-install.md new file mode 100644 index 000000000..f23697186 --- /dev/null +++ b/docs/quick_start/1.-install.md @@ -0,0 +1,22 @@ +Install `isort` using your preferred Python package manager: + +`pip3 install isort` + +OR + +`poetry add isort` + +OR + +`pipenv install isort` + +OR + +For a fully isolated user installation you can use [pipx](https://github.com/pipxproject/pipx) + +`pipx install isort` + + + +!!!tip + If you want isort to act as a linter for projects, it probably makes since to add isort as an explicit development dependency for each project that uses it. If, on the other hand, you are an individual developer simply using isort as a personal tool to clean up your own commits, a global or user level installation makes sense. Both are seamlessly supported on a single machine. diff --git a/docs/interactive/interactive.css b/docs/quick_start/interactive.css similarity index 100% rename from docs/interactive/interactive.css rename to docs/quick_start/interactive.css diff --git a/docs/interactive/interactive.js b/docs/quick_start/interactive.js similarity index 93% rename from docs/interactive/interactive.js rename to docs/quick_start/interactive.js index 3144b968b..7cf058950 100644 --- a/docs/interactive/interactive.js +++ b/docs/quick_start/interactive.js @@ -52,5 +52,5 @@ def use_isort(*args): document.sort_code = sort_code document.updateOutput() -micropip.install('https://timothycrosley.github.io/isort/docs/interactive/isort-5.0.0-py3-none-any.whl').then(use_isort)`); +micropip.install('https://timothycrosley.github.io/isort/docs/quick_start/isort-5.0.0-py3-none-any.whl').then(use_isort)`); }); diff --git a/docs/interactive/isort-5.0.0-py3-none-any.whl b/docs/quick_start/isort-5.0.0-py3-none-any.whl similarity index 100% rename from docs/interactive/isort-5.0.0-py3-none-any.whl rename to docs/quick_start/isort-5.0.0-py3-none-any.whl From 1e41f7a95459a500760cebe4ae47c935887f20b9 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 1 Jul 2020 01:41:55 -0700 Subject: [PATCH 0672/1439] Add initial CLI usage documentation --- docs/quick_start/2.-cli.md | 43 +++++++++++++++++++++++++++++++++++ scripts/build_profile_docs.py | 2 +- 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 docs/quick_start/2.-cli.md diff --git a/docs/quick_start/2.-cli.md b/docs/quick_start/2.-cli.md new file mode 100644 index 000000000..b45224672 --- /dev/null +++ b/docs/quick_start/2.-cli.md @@ -0,0 +1,43 @@ +# Command Line Usage + +Once installed, `isort` exposes a command line utility for sorting, organizing, and formatting imports within Python and Cython source files. + +To verify the tool is installed correctly, run `isort --version` from the command line and you should be given the available commands and the version of isort installed. +For a list of all CLI options type `isort --help` or view [the online configuration reference](http://127.0.0.1:8000/docs/configuration/options/).: + + + +## Formatting a Project + +In general, isort is most commonly utilized across an entire projects source at once. The simplest way to do this is `isort .` or if using a `src` directory `isort src`. isort will automatically find all Python source files recursively and pick-up a configuration file placed at the root of your project if present. This can be combined with any command line configuration customizations such as specifying a profile to use (`isort . --profile black`). + + + +## Verifying a Project + +The second most common usage of isort is verifying that imports within a project are formatted correctly (often within the context of a CI/CD system). The simplest way to accomplish this is using the check command line option: `isort --check .`. To improve the usefulness of errors when they do occur, this can be combined with the diff option: `isort --check --diff .`. + + + +## Single Source Files + +Finally, isort can just as easily be ran against individual source files. Simply pass in a single or multiple source files to sort or validate (Example: `isort setup.py`). + + + +## Multiple Projects + +Running a single isort command across multiple projects, or source files spanning multiple projects, is highly discouraged. Instead it is recommended that an isort process (or command) is ran for each project independently. This is because isort creates an immutable config for each CLI instance. + +``` +# YES +isort project1 +isort project2 + +# Also YES +isort project1/src project1/test +isort project2/src project2/test + +# NO +isort project1 project2 +``` diff --git a/scripts/build_profile_docs.py b/scripts/build_profile_docs.py index e0a86f89a..30604c696 100755 --- a/scripts/build_profile_docs.py +++ b/scripts/build_profile_docs.py @@ -1,6 +1,6 @@ #! /bin/env python import os -from typing import Any, Generator, Iterable, Type, Dict +from typing import Any, Dict, Generator, Iterable, Type from isort._future import dataclass from isort.main import _build_arg_parser From daacbbe6a3bfc5f00c13b687f6789581472e7585 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 2 Jul 2020 22:18:12 -0700 Subject: [PATCH 0673/1439] Add typing info to extension and config parameter of API endpoint --- isort/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/api.py b/isort/api.py index da9fb075f..b0b46606c 100644 --- a/isort/api.py +++ b/isort/api.py @@ -32,8 +32,8 @@ def sort_code_string( code: str, - extension="py", - config=DEFAULT_CONFIG, + extension: str = "py", + config: Config = DEFAULT_CONFIG, file_path: Optional[Path] = None, disregard_skip: bool = False, **config_kwargs, From cf20c027fbed7b2249a238ae3b7d812d80153c97 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 2 Jul 2020 22:39:32 -0700 Subject: [PATCH 0674/1439] Add API configuration guide and config files guide --- art/logo_5.png | Bin 0 -> 43226 bytes docs/configuration/config_files.md | 77 +++++++++++++++++++++ docs/major_releases/introducing_isort_5.md | 2 + docs/quick_start/3.-api.md | 22 ++++++ 4 files changed, 101 insertions(+) create mode 100644 art/logo_5.png create mode 100644 docs/configuration/config_files.md create mode 100644 docs/quick_start/3.-api.md diff --git a/art/logo_5.png b/art/logo_5.png new file mode 100644 index 0000000000000000000000000000000000000000..d67f355ef014808280c83ecf879557101b89535b GIT binary patch literal 43226 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 Q72&8VYQC#`YZ?Ck08nb6&j0`b literal 0 HcmV?d00001 diff --git a/docs/configuration/config_files.md b/docs/configuration/config_files.md new file mode 100644 index 000000000..4b014ad36 --- /dev/null +++ b/docs/configuration/config_files.md @@ -0,0 +1,77 @@ +Supported Config Files +======== + +isort supports a variety of standard config formats, to allow customizations to easily be integrated into any project. +When applying configurations, isort looks for the closest supported config file, in the order files are listed below. +You can manually specify the settings file or path by setting `--settings-path` from the commandline, otherwise isort will +traverse up to 25 parent directories until it finds a suitable config file. +As soon as it finds a file, it stops looking. isort **never** merges config files together due to the confusion it can cause. + +!!! tip + You can always introspect the configuration settings isort determined, and find out which config file it picked up, by running `isort . --show-config` + + + +## .isort.cfg **[preferred format]** + +The first place isort will look for settings is in dedicated .isort.cfg files. +The advantage of using this kind of config file, is that it is explicitly for isort and follows a well understood format. +The downside, is that it means one more config file in your project when you may already have several polluting your file hierarchy. + +An example a config from the isort project itself: + +```ini +[settings] +profile=hug +src_paths=isort,test +``` + +## pyproject.toml **[preferred format]** + +The second place isort will look, and an equally excellent choice to place your configuration, is within a pyproject.toml file. +The advantage of using this config file, is that it is quickly becoming a standard place to configure all Python tools. +This means other developers will know to look here and you will keep your projects root nice and tidy. +The only disadvantage is that other tools you use might not yet support this format, negating the cleanliness. + +```toml +[tools.isort] +profile = "hug" +src_paths = ["isort", "test"] +``` + +## setup.cfg + +`setup.cfg` can be thought of as the precursor to `pyproject.toml`. While isort and newer tools are increasingly moving to pyproject.toml, if you rely on many tools that +use this standard it can be a natural fit to put your isort config there as well. + + +```ini +[isort] +profile=hug +src_paths=isort,test +``` + +## tox.ini + +[tox](https://tox.readthedocs.io/en/latest/) is a tool commonly used in the Python community to specify multiple testing environments. +Because isort verification is commonly ran as a testing step, some prefer to place the isort config inside of the tox.ini file. + +```ini +[isort] +``` + +## .editorconfig + +Finally, isort will look for a `.editorconfig` configuration with settings for Python source files. [EditorConfig](https://editorconfig.org/) is a project to enable specifying a configuration for text editing behaviour once, allowing multiple command line tools and text editors to pick it up. Since isort cares about a lot of the same settings as a text-editor (like line-length) it makes sense for it to look within these files +as well. + +``` +root = true + +[*.py] +profile = hug +indent_style = space +indent_size = 4 +skip = build,.tox,venv +src_paths=isort,test +``` diff --git a/docs/major_releases/introducing_isort_5.md b/docs/major_releases/introducing_isort_5.md index 75ead8c49..0fff587ca 100644 --- a/docs/major_releases/introducing_isort_5.md +++ b/docs/major_releases/introducing_isort_5.md @@ -1,5 +1,7 @@ # Introducing isort 5 +[![isort 5 - the best version of isort yet](https://raw.githubusercontent.com/timothycrosley/isort/develop/art/logo_5.png)](https://timothycrosley.github.io/isort/) + isort 5.0.0 is the first major release of isort in over five years and the first significant refactoring of isort since it was conceived more than ten years ago. It's also the first version to require Python 3 (Python 3.6+ at that!) to run - though it can still be run on source files from any version of Python. This does mean that there may be some pain with the upgrade process, but we believe the improvements will be well worth it. diff --git a/docs/quick_start/3.-api.md b/docs/quick_start/3.-api.md new file mode 100644 index 000000000..c1f72360e --- /dev/null +++ b/docs/quick_start/3.-api.md @@ -0,0 +1,22 @@ +# Programmatic Python API Usage + +In addition to the powerful command line interface, isort exposes a complete Python API. + +To use the Python API, `import isort` and then call the desired function call: + + + +Every function is fully type hinted and requires and returns only builtin Python objects. + +Highlights include: + +- `isort.code` - Takes a string containing code, and returns it with imports sorted. +- `isort.check_code` - Takes a string containing code, and returns `True` if all imports are sorted correctly, otherwise, `False`. +- `isort.stream` - Takes an input stream containing Python code and an output stream. Outputs code to output stream with all imports sorted. +- `isort.stream` - Takes an input stream containing Python code and returns `True` if all imports in the stream are sorted correctly, otherwise, `False`. +- `isort.file` - Takes the path of a Python source file and sorts the imports in-place. +- `isort.check_file` - Takes the path of a Python source file and returns `True` if all imports contained within are sorted correctly, otherwise, `False`. +- `isort.place_module` - Takes the name of a module as a string and returns the categorization determined for it. +- `isort.place_module_with_reason` - Takes the name of a module as a string and returns the categorization determined for it and why that categorization was given. + +For a full definition of the API see the [API reference documentation](https://timothycrosley.github.io/isort/reference/isort/api/) or try `help(isort)` from an interactive interpreter. From 662201833d109f8288506339606433983c6a7900 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 2 Jul 2020 22:55:17 -0700 Subject: [PATCH 0675/1439] Foratting --- scripts/build_profile_docs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/build_profile_docs.py b/scripts/build_profile_docs.py index 30604c696..99bc2a552 100755 --- a/scripts/build_profile_docs.py +++ b/scripts/build_profile_docs.py @@ -21,6 +21,7 @@ """ + def format_profile(profile_name: str, profile: Dict[str, Any]) -> str: options = "\n".join(f" - **{name}**: `{repr(value)}`" for name, value in profile.items()) return f""" From 64ab238666298d5ff203082a6213087fa4a39858 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 3 Jul 2020 23:29:51 -0700 Subject: [PATCH 0676/1439] Fix issue #1239 --- isort/main.py | 8 ++++---- isort/settings.py | 38 ++++++++++++++++++-------------------- tests/test_main.py | 10 +++++++++- tests/test_settings.py | 6 +++--- 4 files changed, 34 insertions(+), 28 deletions(-) diff --git a/isort/main.py b/isort/main.py index ef02b1afa..d5f6a9fba 100644 --- a/isort/main.py +++ b/isort/main.py @@ -643,10 +643,10 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = show_diff = config_dict.pop("show_diff", False) write_to_stdout = config_dict.pop("write_to_stdout", False) - src_paths = config_dict["src_paths"] = set(config_dict.get("src_paths", ())) - for file_name in file_names: - if os.path.isdir(file_name): - src_paths.add(Path(file_name).resolve()) + if "src_paths" in config_dict: + config_dict["src_paths"] = { + Path(src_path).resolve() for src_path in config_dict.get("src_paths", ()) + } config = Config(**config_dict) if show_config: diff --git a/isort/settings.py b/isort/settings.py index 9e9b3bc21..365cf92bf 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -1,9 +1,6 @@ """isort/settings.py. Defines how the default settings for isort should be loaded - -(First from the default setting dictionary at the top of the file, then overridden by any settings - in ~/.isort.cfg or $XDG_CONFIG_HOME/.isort.cfg if there are any) """ import configparser import fnmatch @@ -217,15 +214,18 @@ def __init__( sources: List[Dict[str, Any]] = [_DEFAULT_SETTINGS] config_settings: Dict[str, Any] + project_root: str if settings_file: config_settings = _get_config_data( settings_file, CONFIG_SECTIONS.get(os.path.basename(settings_file), FALLBACK_CONFIG_SECTIONS), ) + project_root = os.path.dirname(settings_file) elif settings_path: - config_settings = _find_config(settings_path) + project_root, config_settings = _find_config(settings_path) else: config_settings = {} + project_root = os.getcwd() profile_name = config_overrides.get("profile", config_settings.get("profile", "")) profile: Dict[str, Any] = {} @@ -241,7 +241,6 @@ def __init__( sources.append(config_settings) if config_overrides: config_overrides["source"] = RUNTIME_SOURCE - config_overrides["runtime_src_paths"] = config_overrides.pop("src_paths", ()) sources.append(config_overrides) combined_config = {**profile, **config_settings, **config_overrides} @@ -277,20 +276,19 @@ def __init__( combined_config[key] = type(default_value)(value) if "directory" not in combined_config: - combined_config["directory"] = os.path.basename( - config_settings.get("source", None) or os.getcwd() + combined_config["directory"] = ( + os.path.dirname(config_settings["source"]) + if config_settings.get("source", None) + else os.getcwd() ) - if "src_paths" not in combined_config and not combined_config.get("runtime_src_paths"): - combined_config["src_paths"] = frozenset((Path.cwd().resolve(),)) + path_root = Path(combined_config.get("directory", project_root)).resolve() + path_root = path_root if path_root.is_dir() else path_root.parent + if "src_paths" not in combined_config: + combined_config["src_paths"] = frozenset((path_root, path_root / "src")) else: - path_root = Path(combined_config.get("directory", Path.cwd())).resolve() - path_root = path_root if path_root.is_dir() else path_root.parent combined_config["src_paths"] = frozenset( - {path_root / path for path in combined_config.get("src_paths", ())}.union( - Path.cwd().resolve() / path - for path in combined_config.get("runtime_src_paths", ()) - ) + path_root / path for path in combined_config.get("src_paths", ()) ) # Remove any config values that are used for creating config object but @@ -407,7 +405,7 @@ def _abspaths(cwd: str, values: Iterable[str]) -> Set[str]: @lru_cache() -def _find_config(path: str) -> Dict[str, Any]: +def _find_config(path: str) -> Tuple[str, Dict[str, Any]]: current_directory = path tries = 0 while current_directory and tries < MAX_CONFIG_SEARCH_DEPTH: @@ -423,11 +421,11 @@ def _find_config(path: str) -> Dict[str, Any]: warn(f"Failed to pull configuration information from {potential_config_file}") config_data = {} if config_data: - return config_data + return (current_directory, config_data) for stop_dir in STOP_CONFIG_SEARCH_ON_DIRS: - if os.path.isdir(stop_dir): - break + if os.path.isdir(os.path.join(current_directory, stop_dir)): + return (current_directory, {}) new_directory = os.path.split(current_directory)[0] if new_directory == current_directory: @@ -436,7 +434,7 @@ def _find_config(path: str) -> Dict[str, Any]: current_directory = new_directory tries += 1 - return {} + return (path, {}) @lru_cache() diff --git a/tests/test_main.py b/tests/test_main.py index b97f8928d..e7484d8a3 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -86,7 +86,15 @@ def test_is_python_file_fifo(tmpdir): def test_main(capsys, tmpdir): - base_args = ["--settings-path", str(tmpdir), "--virtual-env", str(tmpdir)] + base_args = [ + "--settings-path", + str(tmpdir), + "--virtual-env", + str(tmpdir), + "--src-path", + str(tmpdir), + ] + tmpdir.mkdir(".git") # If no files are passed in the quick guide is returned main.main(base_args) diff --git a/tests/test_settings.py b/tests/test_settings.py index 648da100d..024f47532 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -42,13 +42,13 @@ def test_find_config(tmpdir): """, "utf8", ) - assert not settings._find_config(str(tmpdir)) + assert not settings._find_config(str(tmpdir))[1] # or if it is malformed settings._find_config.cache_clear() settings._get_config_data.cache_clear() tmp_config.write_text("""arstoyrsyan arienrsaeinrastyngpuywnlguyn354q^%$)(%_)@$""", "utf8") - assert not settings._find_config(str(tmpdir)) + assert not settings._find_config(str(tmpdir))[1] # can when it has either a file format, or generic relevant section settings._find_config.cache_clear() @@ -60,7 +60,7 @@ def test_find_config(tmpdir): """, "utf8", ) - assert settings._find_config(str(tmpdir)) + assert settings._find_config(str(tmpdir))[1] def test_get_config_data(tmpdir): From 3c655f799649de55e9f917c1140d80f4762e0bd3 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 4 Jul 2020 01:35:32 -0700 Subject: [PATCH 0677/1439] Fix badge link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3d0b6cb74..0bbb5da07 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![Lint Status](https://github.com/timothycrosley/isort/workflows/Lint/badge.svg?branch=develop)](https://github.com/timothycrosley/isort/actions?query=workflow%3ALint) [![Code coverage Status](https://codecov.io/gh/timothycrosley/isort/branch/develop/graph/badge.svg)](https://codecov.io/gh/timothycrosley/isort) [![Maintainability](https://api.codeclimate.com/v1/badges/060372d3e77573072609/maintainability)](https://codeclimate.com/github/timothycrosley/isort/maintainability) -[![License](https://img.shields.io/github/license/mashape/apistatus.svg)](https://pypi.org/project/hug/) +[![License](https://img.shields.io/github/license/mashape/apistatus.svg)](https://pypi.org/project/isort/) [![Join the chat at https://gitter.im/timothycrosley/isort](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/timothycrosley/isort?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Downloads](https://pepy.tech/badge/isort)](https://pepy.tech/project/isort) [![DeepSource](https://static.deepsource.io/deepsource-badge-light-mini.svg)](https://deepsource.io/gh/timothycrosley/isort/?ref=repository-badge) From e20047e828fbc589f2caee2a06ed1c192ce0ce63 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 4 Jul 2020 01:40:46 -0700 Subject: [PATCH 0678/1439] Update isort wheel for interactive --- docs/quick_start/isort-5.0.0-py3-none-any.whl | Bin 72682 -> 85991 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/quick_start/isort-5.0.0-py3-none-any.whl b/docs/quick_start/isort-5.0.0-py3-none-any.whl index 7c1d2fbe7891d846ef40bafdef9e1369be560b72..3bb6b89b7c6fee5628d528b001c1e95f120912f4 100644 GIT binary patch delta 44725 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 Date: Sat, 4 Jul 2020 08:50:02 +0000 Subject: [PATCH 0679/1439] Remove unused imports --- scripts/build_profile_docs.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/scripts/build_profile_docs.py b/scripts/build_profile_docs.py index 99bc2a552..d86ed1c5e 100755 --- a/scripts/build_profile_docs.py +++ b/scripts/build_profile_docs.py @@ -1,11 +1,7 @@ #! /bin/env python import os from typing import Any, Dict, Generator, Iterable, Type - -from isort._future import dataclass -from isort.main import _build_arg_parser from isort.profiles import profiles -from isort.settings import _DEFAULT_SETTINGS as config OUTPUT_FILE = os.path.abspath( os.path.join(os.path.dirname(os.path.abspath(__file__)), "../docs/configuration/profiles.md") From fbf34d12988034cdc94e7c03b48630d74a41820f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 4 Jul 2020 01:54:28 -0700 Subject: [PATCH 0680/1439] Fix urgent issue with vendored dependency failure --- docs/quick_start/isort-5.0.0-py3-none-any.whl | Bin 85991 -> 85988 bytes isort/_vendored/toml/__init__.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quick_start/isort-5.0.0-py3-none-any.whl b/docs/quick_start/isort-5.0.0-py3-none-any.whl index 3bb6b89b7c6fee5628d528b001c1e95f120912f4..f6864f6a2bd9dc9d2835c1a01188acabf9e02510 100644 GIT binary patch delta 2899 zcmX|@cR1B=AIHyqLPl1{%E(BeLHcoIW*qC-M^?r$vWgr+D2^?J{0_ddjyub2$mMt4!1TIT08AQ4s9A+L}zDi!QWnkwKUtjyFf5^SyVj2#*CbZ@~Q84kQ z>Osujsq4dqzEX@!_g-p4JQY63SfbWg0%)EgFL+rpWdjvR0U>-_MV1;1#Sh;*H10Fq z)-1QF7*u!|?38{4Gfq%8_0x;%y;U+Ar&eaj=+0{2c|nj2vMG_8d`QK zkqMmqB^xC4MSW_yHbMKm?L>{Rj=jROKH8*)(Y2@cy>8g)1hVwo$3`27!sg5a*1Klu zj>BR}Gh<(H3VVzAEx8+$MDffPUHWQrLKLs7PX{AKdP|#kSHDd5By)`z55js(HUxRv zM0W9^?5rFgQz(npy-umzpgG*Am_7$!I-u43DD2~tDipUePS5gzy*(tMn+*WC2J#?E>CeSxLo;L;*dUp8R;FHdr3sxvh~ZniJ?`IzF1zEa{1bbqxAE* zR;E?U9V99WF4o0iU1$9@Rpx#^%L<=mwhy8#z(t52&+|hmquIEs8%$SPPpt>uWIt4y zOyoG>>wTPgy;9s*;GE7T)Rm1nUod?Y}*h*u(nM1|*UhE_pXCpwahTc-f3GEBxk`CiIBVOEerVI*%72?gxsD^Ef*{SW06rpQlF^UOKO2$*rDzoPokE zW)vQs_{!{t9&}%vxLt(Qr}9@{MCc6Ksa~E4h{*Q9gnel*TF@GNR`!i8S#YTlj5>F~klR=cuRRq%^XKH(4VG)UiO@C*+359N4=Pez=bxQP z*A0Xj!e@|#;ZG_nm?~`lqX*c9w$asJJljT+O1}jQ@lkQ6E0*sM+${1`hc8pZ_;A{0l*?LXqSqvRJ4BMYS5 zJ{=g-ayE@>icw)ke%4FPZ_Rp^WTt8`sQk!WiW$+t+ay0T)agn1Ez{Y>m+Q6Iz`fqH zd$cjUBrUJ4pb!k~KNWV@SX6)gAVkfVRrfC5cVYcTB4xGXoaPA`!?}Z&JKE|7c+||3 zW9@EgF4NM2WrXsglNa8U!FIQO)gn2^CCsdGG=p!t-U&vw8_8-o81O*QDmazK%INWY z@!q#H4V$x)qfnv9gysGZ-PvDy-FB@s{)9%|@d(cRmJZHq{FP2YGs;;PD^YoUYC&E$ zgF0Kjbatu!bz7Ub%djAPUb_tef1V7ndF8bGvy@!Jo;s)9Qtt2XKH5V9?>{HjZ|WDG z4Xm9Eym=%+D;s^bn8PLWU0-#Shv;ZZK#T=MTTKa;8iPg2IX@^^M=l6$PA95XK}$ zOy6%nMZWRjwjNE70Zo`uh(CUN@7JswMwzJd+yqrJP~yUUMW>@K?#ocrsQB{Zl(~ZZ z=)Aw_t41A4i(M-p*qXZPg&!kQidd5%^k%hz{Yin;6{Z=uJf1eqmh#;hYi!olN)&U@ z;6vths%K!Ff5W~!`d;}`^0Bg&0VmF<25|C*2b~}I9kOZs@+XTv!XiADXd@+Qq$ z%}g$G@)2%(Uo!2hKn9mx@a~eMwxT}t&H@{a8OH>s88Vjctnt#q{xS-4)1x=ST5G>% z&i0{)_Dza}-g?Zqx!1nKQDcXvrLqRGV;^fT^b6^nKbX0yc&uG2qgiv*+(>yP?x{g6weA8p%AkxbtvpdAZp83T1IF|_s#ZKA_uDMc)<+Yr4%;7$ zH5)bUylAT)@6U1F*NIPp=OxR)j&`uR8UW68h6x6PQIdA8q7pCzRR51-P#y|k;f50b zBr=Fw8HmA4js6u*BmwN93}v7RzhU!ljY0*805b@w0^~pzs2fhbh^N9{nXDnK1B z>P67;;(`oS!Bt=Z1*igfKns0T1u_5zovH$95CKW6f!iPta#sUVAOJXjK6N zq^|+k;rifG9M9Fc^WvHAywaJ%Z&PD-@$ivc*Af zHA&YjLdSog3dw5$ML3ZXQqUqbeWe6sP_h=ub^v|S0@8F;^Z@4g*u~9V;Gbm{;W3Q;9eeun)A9UN7H delta 2929 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 CzeKSB diff --git a/isort/_vendored/toml/__init__.py b/isort/_vendored/toml/__init__.py index 6b7337aca..8cefeffad 100644 --- a/isort/_vendored/toml/__init__.py +++ b/isort/_vendored/toml/__init__.py @@ -2,7 +2,7 @@ Released under the MIT license. """ -from toml import decoder, encoder +from . import decoder, encoder __version__ = "0.10.1" _spec_ = "0.5.0" From 09e313e74a0564ab34feff2f64440e8be0a01699 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 4 Jul 2020 01:56:37 -0700 Subject: [PATCH 0681/1439] Prepare hotfix --- CHANGELOG.md | 3 +++ pyproject.toml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40a3f9c29..b68bde51c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ Changelog ========= +### 5.0.1 - July 4, 2020 + - Fixed a runtime error in a vendored dependency (toml). + ### 5.0.0 Penny - July 4, 2020 **Breaking changes:** diff --git a/pyproject.toml b/pyproject.toml index f9cfba290..e70d2c344 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "isort" -version = "5.0.0" +version = "5.0.1" description = "A Python utility / library to sort Python imports." authors = ["Timothy Crosley "] license = "MIT" From e48bfcbddd84c8038cc84739ff57b65646a14af0 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 4 Jul 2020 02:01:17 -0700 Subject: [PATCH 0682/1439] Update interactive to use 5.0.1 version --- docs/quick_start/interactive.js | 2 +- docs/quick_start/isort-5.0.1-py3-none-any.whl | Bin 0 -> 85990 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 docs/quick_start/isort-5.0.1-py3-none-any.whl diff --git a/docs/quick_start/interactive.js b/docs/quick_start/interactive.js index 7cf058950..328df20de 100644 --- a/docs/quick_start/interactive.js +++ b/docs/quick_start/interactive.js @@ -52,5 +52,5 @@ def use_isort(*args): document.sort_code = sort_code document.updateOutput() -micropip.install('https://timothycrosley.github.io/isort/docs/quick_start/isort-5.0.0-py3-none-any.whl').then(use_isort)`); +micropip.install('https://timothycrosley.github.io/isort/docs/quick_start/isort-5.0.1-py3-none-any.whl').then(use_isort)`); }); diff --git a/docs/quick_start/isort-5.0.1-py3-none-any.whl b/docs/quick_start/isort-5.0.1-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..c335e3a39c53e42e1019cc99edbbf04402afa130 GIT binary patch literal 85990 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�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
6Liy1uYJ8QJ6UC<;8h&);~Tw2^n& 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%0DL@}*!g?8hh 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 NfW?Se5U>CP{Xgz1qH+KL literal 0 HcmV?d00001 From 98132ec2e22f5d0670b1ae7ef0e8bcf0d8ecf1da Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 4 Jul 2020 02:12:54 -0700 Subject: [PATCH 0683/1439] Add a note about semver --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b68bde51c..ac0d20cff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ Changelog ========= +NOTE: isort follows the [semver](https://semver.org/) versioning standard. + ### 5.0.1 - July 4, 2020 - Fixed a runtime error in a vendored dependency (toml). From 49e960d9c76e8eb444abfe4bfb986b7ad2bbf7b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Mickevi=C4=8Dius?= Date: Sat, 4 Jul 2020 12:16:18 +0300 Subject: [PATCH 0684/1439] Fix a typo in poetry config sample --- docs/configuration/config_files.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration/config_files.md b/docs/configuration/config_files.md index 4b014ad36..a3d983c2b 100644 --- a/docs/configuration/config_files.md +++ b/docs/configuration/config_files.md @@ -34,7 +34,7 @@ This means other developers will know to look here and you will keep your projec The only disadvantage is that other tools you use might not yet support this format, negating the cleanliness. ```toml -[tools.isort] +[tool.isort] profile = "hug" src_paths = ["isort", "test"] ``` From 1ebb2074d334cf810da2bf63a1e1a3440c767411 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 4 Jul 2020 02:26:44 -0700 Subject: [PATCH 0685/1439] =?UTF-8?q?Add=20Martynas=20Mickevi=C4=8Dius=20(?= =?UTF-8?q?@2m)=20to=20documenters=20acknowledgment=20list?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/contributing/4.-acknowledgements.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index dc5f5108d..5f120d419 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -108,6 +108,7 @@ Documenters - Aaron Brown (@aaronvbrown) - Harutaka Kawamura (@harupy) - Brad Solomon (@bsolomon1124) +- Martynas Mickevičius (@2m) -------------------------------------------- From 7328c3de3b24a61c845c5fb6fa25db524845fbfa Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 4 Jul 2020 03:13:35 -0700 Subject: [PATCH 0686/1439] Update black profile line length to 88 --- isort/profiles.py | 1 + 1 file changed, 1 insertion(+) diff --git a/isort/profiles.py b/isort/profiles.py index 910da9357..a35c38cbf 100644 --- a/isort/profiles.py +++ b/isort/profiles.py @@ -7,6 +7,7 @@ "force_grid_wrap": 0, "use_parentheses": True, "ensure_newline_before_comments": True, + "line_length": 88, } django = { "combine_as_imports": True, From d953a69237c69646908c4acd0633f738fd38b8d0 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 4 Jul 2020 03:20:01 -0700 Subject: [PATCH 0687/1439] Fix line length for black profile --- CHANGELOG.md | 3 +++ docs/configuration/profiles.md | 1 + pyproject.toml | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac0d20cff..3948e35bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. +### 5.0.2 - July 4, 2020 + - Ensured black profile was complete, adding missing line_length definition. + ### 5.0.1 - July 4, 2020 - Fixed a runtime error in a vendored dependency (toml). diff --git a/docs/configuration/profiles.md b/docs/configuration/profiles.md index 792ff988c..8f859f232 100644 --- a/docs/configuration/profiles.md +++ b/docs/configuration/profiles.md @@ -15,6 +15,7 @@ To use any of the listed profiles, use `isort --profile PROFILE_NAME` from the c - **force_grid_wrap**: `0` - **use_parentheses**: `True` - **ensure_newline_before_comments**: `True` + - **line_length**: `88` #django diff --git a/pyproject.toml b/pyproject.toml index e70d2c344..775e8c363 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "isort" -version = "5.0.1" +version = "5.0.2" description = "A Python utility / library to sort Python imports." authors = ["Timothy Crosley "] license = "MIT" From 5325832ef78a98e4a6445a0a68a2d4cdf93d5f5b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 4 Jul 2020 03:21:22 -0700 Subject: [PATCH 0688/1439] Fix formatting of build_profile_docs --- scripts/build_profile_docs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/build_profile_docs.py b/scripts/build_profile_docs.py index d86ed1c5e..1d09ae7ab 100755 --- a/scripts/build_profile_docs.py +++ b/scripts/build_profile_docs.py @@ -1,6 +1,7 @@ #! /bin/env python import os from typing import Any, Dict, Generator, Iterable, Type + from isort.profiles import profiles OUTPUT_FILE = os.path.abspath( From 1a748704d01676bbf9ea27333c99fe7b7309af28 Mon Sep 17 00:00:00 2001 From: Taneli Hukkinen Date: Sat, 4 Jul 2020 19:09:34 +0300 Subject: [PATCH 0689/1439] Remove references to removed isort[pyproject] extra in README --- README.md | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/README.md b/README.md index 0bbb5da07..fe3254bc3 100644 --- a/README.md +++ b/README.md @@ -98,12 +98,6 @@ Install isort with both formats support: pip install isort[requirements,pipfile] ``` -Install isort with support for reading configuration from `pyproject.toml`: - -```bash -pip install isort[pyproject] -``` - Using isort =========== @@ -237,8 +231,7 @@ Or, if you prefer, you can add an `isort` or `tool:isort` section to your project's `setup.cfg` or `tox.ini` file with any desired settings. You can also add your desired settings under a `[tool.isort]` section in -your `pyproject.toml` file. For `pyproject.toml` support, use -`pip install isort[pyproject]`. +your `pyproject.toml` file. You can then override any of these settings by using command line arguments, or by passing in override values to any of the public Python API From 6e5da552e0e2ca7f23414a52932876a33af5d59c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 4 Jul 2020 11:51:02 -0700 Subject: [PATCH 0690/1439] Add Taneli Hukkinen (@hukkinj1) to documenters list --- docs/contributing/4.-acknowledgements.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index 5f120d419..750fee985 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -109,6 +109,7 @@ Documenters - Harutaka Kawamura (@harupy) - Brad Solomon (@bsolomon1124) - Martynas Mickevičius (@2m) +- Taneli Hukkinen (@hukkinj1) -------------------------------------------- From 51f050e3bf49c51881a28b374d5e36a2fd70d7a2 Mon Sep 17 00:00:00 2001 From: Taneli Hukkinen Date: Sun, 5 Jul 2020 01:30:54 +0300 Subject: [PATCH 0691/1439] Update optional feature names in README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fe3254bc3..a6c7f1389 100644 --- a/README.md +++ b/README.md @@ -83,19 +83,19 @@ pip install isort Install isort with requirements.txt support: ```bash -pip install isort[requirements] +pip install isort[requirements_deprecated_finder] ``` Install isort with Pipfile support: ```bash -pip install isort[pipfile] +pip install isort[pipfile_deprecated_finder] ``` Install isort with both formats support: ```bash -pip install isort[requirements,pipfile] +pip install isort[requirements_deprecated_finder,pipfile_deprecated_finder] ``` Using isort From 63547307f2b063ef735b0bcf6e6402b299a6d7ce Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 4 Jul 2020 22:06:08 -0700 Subject: [PATCH 0692/1439] Fix issue #1256 no documentation for Force Alphabetical Sort Within Sections --- CHANGELOG.md | 4 ++++ docs/configuration/options.md | 15 ++++++++------- isort/_version.py | 2 +- isort/main.py | 2 +- isort/setuptools_commands.py | 2 -- 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3948e35bc..c4879e41b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. +### 5.0.3 - July 4, 2020 + - Fixed setup.py command incorrectly passing check=True as a configuration parameter (see: https://github.com/timothycrosley/isort/issues/1258) + - Fixed missing patch version + ### 5.0.2 - July 4, 2020 - Ensured black profile was complete, adding missing line_length definition. diff --git a/docs/configuration/options.md b/docs/configuration/options.md index 6ffcaafd4..ae0a29a54 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -34,7 +34,7 @@ Force specific imports to the top of their appropriate section. Files that sort imports should skip over. If you want to skip multiple files you should specify twice: --skip file1 --skip file2. **Type:** Frozenset -**Default:** `frozenset({'node_modules', '.hg', '_build', 'dist', 'build', '.eggs', '.tox', '.nox', 'buck-out', '.mypy_cache', '.pants.d', '.venv', 'venv', '.git'})` +**Default:** `frozenset({'buck-out', '.nox', '.mypy_cache', '.venv', 'node_modules', 'dist', '.eggs', 'venv', '.pants.d', '.hg', '_build', 'build', '.tox', '.git'})` **Python & Config File Name:** skip **CLI Flags:** @@ -146,7 +146,7 @@ Force isort to recognize a module as being part of the current python project. Force isort to recognize a module as part of Python's standard library. **Type:** Frozenset -**Default:** `frozenset({'distutils', 'webbrowser', 'codeop', 'binhex', 'tempfile', 'platform', 'struct', 'base64', 'calendar', 'ast', 'abc', 'pwd', 'spwd', 'plistlib', 'numbers', 'secrets', 'signal', 'grp', 'unittest', 'statistics', 'typing', 'imaplib', 'multiprocessing', 'uuid', 'wsgiref', 'email', 'tokenize', 'getpass', 'asynchat', 'imp', 'mmap', 'sys', 'colorsys', 'cmath', 'tabnanny', 'rlcompleter', 'json', 'sqlite3', 'pprint', 'copyreg', 'zipapp', 'contextvars', 'tarfile', 'telnetlib', 'fileinput', 'modulefinder', 'curses', 'syslog', 'imghdr', 'copy', 'msvcrt', 'cgi', 'weakref', 'datetime', 'dataclasses', 'optparse', 'locale', 'ossaudiodev', '_thread', 'fcntl', 'sched', 'zipfile', 'subprocess', 'formatter', 'operator', 'xml', 'faulthandler', 'dummy_threading', 'shlex', 'argparse', 'trace', 'ntpath', 'unicodedata', 'uu', 'symbol', 'atexit', 'ipaddress', 'runpy', 'binascii', 'doctest', 'termios', 'lzma', 'pdb', 'shutil', 'readline', 'zlib', 're', 'zipimport', 'audioop', 'quopri', 'sndhdr', 'venv', 'msilib', 'array', 'symtable', 'encodings', 'timeit', 'nis', 'http', 'fnmatch', 'codecs', 'stat', 'csv', 'site', 'mimetypes', 'inspect', 'poplib', 'getopt', 'pipes', 'select', 'configparser', 'dbm', 'time', 'html', 'os', 'logging', 'types', 'chunk', 'winreg', 'nntplib', 'compileall', 'pydoc', 'bisect', 'warnings', 'py_compile', 'random', 'importlib', 'cProfile', 'tty', 'linecache', 'shelve', 'parser', 'gc', 'mailbox', 'reprlib', 'token', 'turtledemo', 'pty', 'contextlib', 'hmac', 'socket', 'asyncore', 'keyword', 'pkgutil', 'aifc', 'bdb', 'queue', 'xdrlib', 'ctypes', 'dis', 'ftplib', 'gzip', 'smtplib', 'code', 'pickle', 'socketserver', 'fpectl', 'resource', 'sunau', 'io', 'bz2', 'decimal', 'traceback', 'hashlib', 'smtpd', 'itertools', 'concurrent', 'textwrap', 'pstats', 'tracemalloc', 'sysconfig', 'errno', 'mailcap', 'pickletools', 'test', 'pathlib', 'functools', 'tkinter', 'xmlrpc', 'ensurepip', 'string', 'enum', '_dummy_thread', 'profile', 'glob', 'math', 'posixpath', 'builtins', 'fractions', 'crypt', 'urllib', 'gettext', 'ssl', 'sre_constants', 'collections', 'asyncio', 'netrc', 'filecmp', 'turtle', 'stringprep', 'selectors', 'heapq', 'macpath', 'winsound', 'marshal', 'posix', 'pyclbr', 'cgitb', 'difflib', 'threading', 'wave', 'cmd', 'lib2to3'})` +**Default:** `frozenset({'imaplib', 'datetime', 'collections', '_dummy_thread', 'email', 'lzma', 'mimetypes', 'statistics', 'tracemalloc', 'os', 'bdb', 'webbrowser', 'pkgutil', 'code', 'textwrap', 'html', 'time', 'shutil', 'cProfile', 'pwd', 'dis', 'modulefinder', 'builtins', 'chunk', 'mmap', 'nntplib', 'heapq', 'socket', 'threading', 'aifc', 'pydoc', 'sndhdr', 'macpath', 'io', 'unittest', 'gc', 'readline', 'ast', 'turtle', 'fractions', 'copy', 'ipaddress', 'asyncio', 'zlib', 'sre_constants', 'plistlib', 'fcntl', 'shlex', 'stringprep', 'cgitb', 'importlib', 'posixpath', 'sqlite3', 'test', 'urllib', 're', 'multiprocessing', 'py_compile', 'typing', 'pyclbr', 'select', 'traceback', 'msvcrt', 'decimal', '_thread', 'enum', 'syslog', 'linecache', 'ssl', 'base64', 'winreg', 'marshal', 'sched', 'codecs', 'errno', 'dbm', 'difflib', 'functools', 'resource', 'unicodedata', 'poplib', 'secrets', 'turtledemo', 'distutils', 'reprlib', 'mailbox', 'pipes', 'zipapp', 'array', 'winsound', 'getpass', 'tokenize', 'crypt', 'curses', 'site', 'csv', 'pickle', 'sysconfig', 'xdrlib', 'zipfile', 'audioop', 'doctest', 'http', 'imghdr', 'ctypes', 'shelve', 'ensurepip', 'math', 'signal', 'colorsys', 'pathlib', 'contextvars', 'runpy', 'profile', 'wave', 'stat', 'calendar', 'copyreg', 'random', 'cmd', 'asyncore', 'tempfile', 'selectors', 'keyword', 'inspect', 'formatter', 'smtpd', 'fileinput', 'tarfile', 'lib2to3', 'xml', 'fpectl', 'asynchat', 'parser', 'cmath', 'xmlrpc', 'json', 'symtable', 'platform', 'types', 'hmac', 'abc', 'ftplib', 'codeop', 'optparse', 'tkinter', 'argparse', 'nis', 'smtplib', 'tabnanny', 'ntpath', 'locale', 'sunau', 'subprocess', 'telnetlib', 'struct', 'binascii', 'configparser', 'compileall', 'quopri', 'rlcompleter', 'pdb', 'token', 'bisect', 'dataclasses', 'zipimport', 'warnings', 'dummy_threading', 'contextlib', 'glob', 'posix', 'tty', 'pickletools', 'trace', 'atexit', 'hashlib', 'concurrent', 'itertools', 'netrc', 'encodings', 'numbers', 'spwd', 'weakref', 'termios', 'ossaudiodev', 'symbol', 'getopt', 'gettext', 'grp', 'bz2', 'faulthandler', 'pty', 'uu', 'queue', 'timeit', 'pstats', 'logging', 'venv', 'socketserver', 'pprint', 'uuid', 'wsgiref', 'filecmp', 'string', 'binhex', 'sys', 'mailcap', 'imp', 'msilib', 'cgi', 'operator', 'fnmatch', 'gzip'})` **Python & Config File Name:** known_standard_library **CLI Flags:** @@ -477,25 +477,26 @@ Forces import adds even if the original file is empty. - --force-adds ## Force Alphabetical Sort Within Sections -**No Description** +Force all imports to be sorted alphabetically within a section **Type:** Bool **Default:** `False` **Python & Config File Name:** force_alphabetical_sort_within_sections **CLI Flags:** - - **Not Supported** + - --fass + - --force-alphabetical-sort-within-sections ## Force Alphabetical Sort -Force all imports to be sorted alphabetically within a section +Force all imports to be sorted as a single section **Type:** Bool **Default:** `False` **Python & Config File Name:** force_alphabetical_sort **CLI Flags:** - - --fass - - --force-alphabetical-sort-within-sections + - --fas + - --force-alphabetical-sort ## Force Grid Wrap Force number of from imports (defaults to 2) to be grid wrapped regardless of line length diff --git a/isort/_version.py b/isort/_version.py index ba7be38e4..4682e6132 100644 --- a/isort/_version.py +++ b/isort/_version.py @@ -1 +1 @@ -__version__ = "5.0.0" +__version__ = "5.0.3" diff --git a/isort/main.py b/isort/main.py index d5f6a9fba..744e39131 100644 --- a/isort/main.py +++ b/isort/main.py @@ -248,7 +248,7 @@ def _build_arg_parser() -> argparse.ArgumentParser: "--fass", "--force-alphabetical-sort-within-sections", action="store_true", - dest="force_alphabetical_sort", + dest="force_alphabetical_sort_within_sections", help="Force all imports to be sorted alphabetically within a section", ) parser.add_argument( diff --git a/isort/setuptools_commands.py b/isort/setuptools_commands.py index 9e02d6289..f67008877 100644 --- a/isort/setuptools_commands.py +++ b/isort/setuptools_commands.py @@ -50,10 +50,8 @@ def distribution_files(self) -> Iterator[str]: def run(self) -> None: arguments = self.arguments wrong_sorted_files = False - arguments["check"] = True for path in self.distribution_files(): for python_file in glob.iglob(os.path.join(path, "*.py")): - try: if not api.check_file(python_file, **arguments): wrong_sorted_files = True # pragma: no cover From ccfb2066b98420d415d31453ad2cd3ef7920263d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 4 Jul 2020 23:45:21 -0700 Subject: [PATCH 0693/1439] Fix issue #1253: atomic fails when passed in not readable output stream --- isort/api.py | 15 +++++++++++---- tests/test_main.py | 3 +++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/isort/api.py b/isort/api.py index b0b46606c..e1d4a7d49 100644 --- a/isort/api.py +++ b/isort/api.py @@ -119,6 +119,8 @@ def sort_stream( if file_path and config.is_skipped(file_path): raise FileSkipSetting(content_source) + _internal_output = output_stream + if config.atomic: try: file_content = input_stream.read() @@ -127,16 +129,21 @@ def sort_stream( except SyntaxError: raise ExistingSyntaxErrors(content_source) + if not output_stream.readable(): + _internal_output = StringIO() + try: - changed = _sort_imports(input_stream, output_stream, extension=extension, config=config) + changed = _sort_imports(input_stream, _internal_output, extension=extension, config=config) except FileSkipComment: raise FileSkipComment(content_source) if config.atomic: - output_stream.seek(0) + _internal_output.seek(0) try: - compile(output_stream.read(), content_source, "exec", 0, 1) - output_stream.seek(0) + compile(_internal_output.read(), content_source, "exec", 0, 1) + _internal_output.seek(0) + if _internal_output != output_stream: + output_stream.write(_internal_output.read()) except SyntaxError: # pragma: no cover raise IntroducedSyntaxErrors(content_source) diff --git a/tests/test_main.py b/tests/test_main.py index e7484d8a3..986b81987 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -221,6 +221,9 @@ def test_main(capsys, tmpdir): ) main.main([str(tmpdir), "--skip", "skip.py", "--check"]) + # without filter options passed in should successfully sort files + main.main([str(python_file), str(should_skip), "--verbose", "--atomic"]) + def test_isort_command(): """Ensure ISortCommand got registered, otherwise setuptools error must have occured""" From 352cbade3a8266451d0d90dde1cd0a672e613b36 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 4 Jul 2020 23:46:14 -0700 Subject: [PATCH 0694/1439] Update changelog to include issue #1253 fix --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4879e41b..5bc55d0d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. ### 5.0.3 - July 4, 2020 - Fixed setup.py command incorrectly passing check=True as a configuration parameter (see: https://github.com/timothycrosley/isort/issues/1258) - Fixed missing patch version + - Fixed issue #1253: Atomic fails when passed in not readable output stream ### 5.0.2 - July 4, 2020 - Ensured black profile was complete, adding missing line_length definition. From 5ab7fa6b6217991433a0ebddaa242dd18a47d20d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 4 Jul 2020 23:46:32 -0700 Subject: [PATCH 0695/1439] Bump version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 775e8c363..a47cabc94 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "isort" -version = "5.0.2" +version = "5.0.3" description = "A Python utility / library to sort Python imports." authors = ["Timothy Crosley "] license = "MIT" From bae12210fdc43c218787542f777607a938b6c4f5 Mon Sep 17 00:00:00 2001 From: Sebastian Wagner Date: Sun, 5 Jul 2020 13:31:55 +0200 Subject: [PATCH 0696/1439] Addd Changelog, docs and tests to source tarball Same as timothycrosley/isort#531 which does no longer work, as now poetry seems to be used for building the packages --- pyproject.toml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index a47cabc94..435858c0d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,14 @@ classifiers = [ "Topic :: Utilities", ] urls = { Changelog = "https://github.com/timothycrosley/isort/blob/master/CHANGELOG.md" } +include = [ + "CHANGELOG.md", + "docs/configuration/*.md", + "docs/contributing/*.md", + "docs/major_releases/*.md", + "docs/quick_start/*.md", + "tests/*.py" +] [tool.poetry.dependencies] python = "^3.6" From 6febe3b631a32d6de8edd349dc7c8e5f36934026 Mon Sep 17 00:00:00 2001 From: Marat Sharafutdinov Date: Sun, 5 Jul 2020 15:23:52 +0300 Subject: [PATCH 0697/1439] Fix Default Section documentation --- docs/configuration/options.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration/options.md b/docs/configuration/options.md index ae0a29a54..7931e2251 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -292,7 +292,7 @@ One or more modules to exclude from the single line rule. - --single-line-exclusions ## Default Section -Sets the default section for imports (by default FIRSTPARTY) options: ('FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER') +Sets the default section for imports ('FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER') **Type:** String **Default:** `THIRDPARTY` From d716117f7ff879773d5d594b0fce9bda95ad1489 Mon Sep 17 00:00:00 2001 From: r-richmond Date: Sun, 5 Jul 2020 09:38:32 -0700 Subject: [PATCH 0698/1439] First pass at adding support for examples in docs --- docs/configuration/options.md | 516 ++++++++++++++++++++-------- isort/main.py | 46 ++- scripts/build_config_option_docs.py | 113 +++++- 3 files changed, 510 insertions(+), 165 deletions(-) diff --git a/docs/configuration/options.md b/docs/configuration/options.md index ae0a29a54..1866392e8 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -1,4 +1,5 @@ -Configuration options for isort +# Configuration options for isort + ======== As a code formatter isort has opinions. However, it also allows you to have your own. If your opinions disagree with those of isort, @@ -7,19 +8,22 @@ how you want your imports sorted, organized, and formatted. Too busy to build your perfect isort configuration? For curated common configurations, see isort's [built-in profiles](https://timothycrosley.github.io/isort/docs/configuration/profiles/). - ## Python Version -Tells isort to set the known standard library based on the the specified Python version. Default is to assume any Python 3 version could be the target, and use a union off all stdlib modules across versions. If auto is specified, the version of the interpreter used to run isort (currently: 38) will be used. + +Tells isort to set the known standard library based on the the specified Python version. Default is to assume any Python 3 version could be the target, and use a union off all stdlib modules across versions. If auto is specified, the version of the interpreter used to run isort (currently: 37) will be used. **Type:** String **Default:** `py3` **Python & Config File Name:** py_version **CLI Flags:** - - --py - - --python-version +- --py +- --python-version + +**No Examples** ## Force To Top + Force specific imports to the top of their appropriate section. **Type:** Frozenset @@ -27,21 +31,27 @@ Force specific imports to the top of their appropriate section. **Python & Config File Name:** force_to_top **CLI Flags:** - - -t - - --top +- -t +- --top + +**No Examples** ## Skip + Files that sort imports should skip over. If you want to skip multiple files you should specify twice: --skip file1 --skip file2. **Type:** Frozenset -**Default:** `frozenset({'buck-out', '.nox', '.mypy_cache', '.venv', 'node_modules', 'dist', '.eggs', 'venv', '.pants.d', '.hg', '_build', 'build', '.tox', '.git'})` +**Default:** `('.eggs', '.git', '.hg', '.mypy_cache', '.nox', '.pants.d', '.tox', '.venv', '_build', 'buck-out', 'build', 'dist', 'node_modules', 'venv')` **Python & Config File Name:** skip **CLI Flags:** - - -s - - --skip +- -s +- --skip + +**No Examples** ## Skip Glob + Files that sort imports should skip over. **Type:** Frozenset @@ -49,10 +59,13 @@ Files that sort imports should skip over. **Python & Config File Name:** skip_glob **CLI Flags:** - - --sg - - --skip-glob +- --sg +- --skip-glob + +**No Examples** ## Line Length + The max length of an import line (used for wrapping long imports). **Type:** Int @@ -60,12 +73,15 @@ The max length of an import line (used for wrapping long imports). **Python & Config File Name:** line_length **CLI Flags:** - - -l - - -w - - --line-length - - --line-width +- -l +- -w +- --line-length +- --line-width + +**No Examples** ## Wrap Length + Specifies how long lines that are wrapped should be, if not set line_length is used. NOTE: wrap_length must be LOWER than or equal to line_length. @@ -74,10 +90,13 @@ NOTE: wrap_length must be LOWER than or equal to line_length. **Python & Config File Name:** wrap_length **CLI Flags:** - - --wl - - --wrap-length +- --wl +- --wrap-length + +**No Examples** ## Line Ending + Forces line endings to the specified value. If not set, values will be guessed per-file. **Type:** String @@ -85,10 +104,13 @@ Forces line endings to the specified value. If not set, values will be guessed p **Python & Config File Name:** line_ending **CLI Flags:** - - --le - - --line-ending +- --le +- --line-ending + +**No Examples** ## Sections + **No Description** **Type:** Tuple @@ -96,9 +118,12 @@ Forces line endings to the specified value. If not set, values will be guessed p **Python & Config File Name:** sections **CLI Flags:** - - **Not Supported** +- **Not Supported** + +**No Examples** ## No Sections + Put all imports into the same section bucket **Type:** Bool @@ -106,32 +131,41 @@ Put all imports into the same section bucket **Python & Config File Name:** no_sections **CLI Flags:** - - --ds - - --no-sections +- --ds +- --no-sections + +**No Examples** ## Known Future Library + Force isort to recognize a module as part of the future compatibility libraries. **Type:** Frozenset -**Default:** `frozenset({'__future__'})` +**Default:** `('__future__',)` **Python & Config File Name:** known_future_library **CLI Flags:** - - -f - - --future +- -f +- --future + +**No Examples** ## Known Third Party + Force isort to recognize a module as being part of a third party library. **Type:** Frozenset -**Default:** `frozenset({'google.appengine.api'})` +**Default:** `('google.appengine.api',)` **Python & Config File Name:** known_third_party **CLI Flags:** - - -o - - --thirdparty +- -o +- --thirdparty + +**No Examples** ## Known First Party + Force isort to recognize a module as being part of the current python project. **Type:** Frozenset @@ -139,21 +173,27 @@ Force isort to recognize a module as being part of the current python project. **Python & Config File Name:** known_first_party **CLI Flags:** - - -p - - --project +- -p +- --project + +**No Examples** ## Known Standard Library + Force isort to recognize a module as part of Python's standard library. **Type:** Frozenset -**Default:** `frozenset({'imaplib', 'datetime', 'collections', '_dummy_thread', 'email', 'lzma', 'mimetypes', 'statistics', 'tracemalloc', 'os', 'bdb', 'webbrowser', 'pkgutil', 'code', 'textwrap', 'html', 'time', 'shutil', 'cProfile', 'pwd', 'dis', 'modulefinder', 'builtins', 'chunk', 'mmap', 'nntplib', 'heapq', 'socket', 'threading', 'aifc', 'pydoc', 'sndhdr', 'macpath', 'io', 'unittest', 'gc', 'readline', 'ast', 'turtle', 'fractions', 'copy', 'ipaddress', 'asyncio', 'zlib', 'sre_constants', 'plistlib', 'fcntl', 'shlex', 'stringprep', 'cgitb', 'importlib', 'posixpath', 'sqlite3', 'test', 'urllib', 're', 'multiprocessing', 'py_compile', 'typing', 'pyclbr', 'select', 'traceback', 'msvcrt', 'decimal', '_thread', 'enum', 'syslog', 'linecache', 'ssl', 'base64', 'winreg', 'marshal', 'sched', 'codecs', 'errno', 'dbm', 'difflib', 'functools', 'resource', 'unicodedata', 'poplib', 'secrets', 'turtledemo', 'distutils', 'reprlib', 'mailbox', 'pipes', 'zipapp', 'array', 'winsound', 'getpass', 'tokenize', 'crypt', 'curses', 'site', 'csv', 'pickle', 'sysconfig', 'xdrlib', 'zipfile', 'audioop', 'doctest', 'http', 'imghdr', 'ctypes', 'shelve', 'ensurepip', 'math', 'signal', 'colorsys', 'pathlib', 'contextvars', 'runpy', 'profile', 'wave', 'stat', 'calendar', 'copyreg', 'random', 'cmd', 'asyncore', 'tempfile', 'selectors', 'keyword', 'inspect', 'formatter', 'smtpd', 'fileinput', 'tarfile', 'lib2to3', 'xml', 'fpectl', 'asynchat', 'parser', 'cmath', 'xmlrpc', 'json', 'symtable', 'platform', 'types', 'hmac', 'abc', 'ftplib', 'codeop', 'optparse', 'tkinter', 'argparse', 'nis', 'smtplib', 'tabnanny', 'ntpath', 'locale', 'sunau', 'subprocess', 'telnetlib', 'struct', 'binascii', 'configparser', 'compileall', 'quopri', 'rlcompleter', 'pdb', 'token', 'bisect', 'dataclasses', 'zipimport', 'warnings', 'dummy_threading', 'contextlib', 'glob', 'posix', 'tty', 'pickletools', 'trace', 'atexit', 'hashlib', 'concurrent', 'itertools', 'netrc', 'encodings', 'numbers', 'spwd', 'weakref', 'termios', 'ossaudiodev', 'symbol', 'getopt', 'gettext', 'grp', 'bz2', 'faulthandler', 'pty', 'uu', 'queue', 'timeit', 'pstats', 'logging', 'venv', 'socketserver', 'pprint', 'uuid', 'wsgiref', 'filecmp', 'string', 'binhex', 'sys', 'mailcap', 'imp', 'msilib', 'cgi', 'operator', 'fnmatch', 'gzip'})` +**Default:** `('_dummy_thread', '_thread', 'abc', 'aifc', 'argparse', 'array', 'ast', 'asynchat', 'asyncio', 'asyncore', 'atexit', 'audioop', 'base64', 'bdb', 'binascii', 'binhex', 'bisect', 'builtins', 'bz2', 'cProfile', 'calendar', 'cgi', 'cgitb', 'chunk', 'cmath', 'cmd', 'code', 'codecs', 'codeop', 'collections', 'colorsys', 'compileall', 'concurrent', 'configparser', 'contextlib', 'contextvars', 'copy', 'copyreg', 'crypt', 'csv', 'ctypes', 'curses', 'dataclasses', 'datetime', 'dbm', 'decimal', 'difflib', 'dis', 'distutils', 'doctest', 'dummy_threading', 'email', 'encodings', 'ensurepip', 'enum', 'errno', 'faulthandler', 'fcntl', 'filecmp', 'fileinput', 'fnmatch', 'formatter', 'fpectl', 'fractions', 'ftplib', 'functools', 'gc', 'getopt', 'getpass', 'gettext', 'glob', 'grp', 'gzip', 'hashlib', 'heapq', 'hmac', 'html', 'http', 'imaplib', 'imghdr', 'imp', 'importlib', 'inspect', 'io', 'ipaddress', 'itertools', 'json', 'keyword', 'lib2to3', 'linecache', 'locale', 'logging', 'lzma', 'macpath', 'mailbox', 'mailcap', 'marshal', 'math', 'mimetypes', 'mmap', 'modulefinder', 'msilib', 'msvcrt', 'multiprocessing', 'netrc', 'nis', 'nntplib', 'ntpath', 'numbers', 'operator', 'optparse', 'os', 'ossaudiodev', 'parser', 'pathlib', 'pdb', 'pickle', 'pickletools', 'pipes', 'pkgutil', 'platform', 'plistlib', 'poplib', 'posix', 'posixpath', 'pprint', 'profile', 'pstats', 'pty', 'pwd', 'py_compile', 'pyclbr', 'pydoc', 'queue', 'quopri', 'random', 're', 'readline', 'reprlib', 'resource', 'rlcompleter', 'runpy', 'sched', 'secrets', 'select', 'selectors', 'shelve', 'shlex', 'shutil', 'signal', 'site', 'smtpd', 'smtplib', 'sndhdr', 'socket', 'socketserver', 'spwd', 'sqlite3', 'sre_constants', 'ssl', 'stat', 'statistics', 'string', 'stringprep', 'struct', 'subprocess', 'sunau', 'symbol', 'symtable', 'sys', 'sysconfig', 'syslog', 'tabnanny', 'tarfile', 'telnetlib', 'tempfile', 'termios', 'test', 'textwrap', 'threading', 'time', 'timeit', 'tkinter', 'token', 'tokenize', 'trace', 'traceback', 'tracemalloc', 'tty', 'turtle', 'turtledemo', 'types', 'typing', 'unicodedata', 'unittest', 'urllib', 'uu', 'uuid', 'venv', 'warnings', 'wave', 'weakref', 'webbrowser', 'winreg', 'winsound', 'wsgiref', 'xdrlib', 'xml', 'xmlrpc', 'zipapp', 'zipfile', 'zipimport', 'zlib')` **Python & Config File Name:** known_standard_library **CLI Flags:** - - -b - - --builtin +- -b +- --builtin + +**No Examples** ## Extra Standard Library + Extra modules to be included in the list of ones in Python's standard library. **Type:** Frozenset @@ -161,9 +201,12 @@ Extra modules to be included in the list of ones in Python's standard library. **Python & Config File Name:** extra_standard_library **CLI Flags:** - - --extra-builtin +- --extra-builtin + +**No Examples** ## Known Other + **No Description** **Type:** Dict @@ -171,9 +214,24 @@ Extra modules to be included in the list of ones in Python's standard library. **Python & Config File Name:** known_other **CLI Flags:** - - **Not Supported** +- **Not Supported** + +**Examples:** + +No example `.isort.cfg` + +### Example `pyproject.toml` + +``` +[tool.isort] +sections = ['FUTURE', 'STDLIB', 'THIRDPARTY', 'AIRFLOW', 'FIRSTPARTY', 'LOCALFOLDER'] +known_airflow = ['airflow'] +``` + +No example cli usage ## Multi Line Output + Multi line output (0-grid, 1-vertical, 2-hanging, 3-vert-hanging, 4-vert-grid, 5-vert-grid-grouped, 6-vert-grid-grouped-no-comma). **Type:** Wrapmodes @@ -181,10 +239,13 @@ Multi line output (0-grid, 1-vertical, 2-hanging, 3-vert-hanging, 4-vert-grid, 5 **Python & Config File Name:** multi_line_output **CLI Flags:** - - -m - - --multi-line +- -m +- --multi-line + +**No Examples** ## Forced Separate + **No Description** **Type:** Tuple @@ -192,30 +253,39 @@ Multi line output (0-grid, 1-vertical, 2-hanging, 3-vert-hanging, 4-vert-grid, 5 **Python & Config File Name:** forced_separate **CLI Flags:** - - **Not Supported** +- **Not Supported** + +**No Examples** ## Indent -String to place for indents defaults to " " (4 spaces). + +String to place for indents defaults to " " (4 spaces). **Type:** String -**Default:** ` ` +**Default:** `` **Python & Config File Name:** indent **CLI Flags:** - - -i - - --indent +- -i +- --indent + +**No Examples** ## Comment Prefix + **No Description** **Type:** String -**Default:** ` #` +**Default:** `#` **Python & Config File Name:** comment_prefix **CLI Flags:** - - **Not Supported** +- **Not Supported** + +**No Examples** ## Length Sort + Sort imports by their string length. **Type:** Bool @@ -223,10 +293,13 @@ Sort imports by their string length. **Python & Config File Name:** length_sort **CLI Flags:** - - --ls - - --length-sort +- --ls +- --length-sort + +**No Examples** ## Length Sort Sections + **No Description** **Type:** Frozenset @@ -234,9 +307,12 @@ Sort imports by their string length. **Python & Config File Name:** length_sort_sections **CLI Flags:** - - **Not Supported** +- **Not Supported** + +**No Examples** ## Add Imports + Adds the specified import line to all files, automatically determining correct placement. **Type:** Frozenset @@ -244,10 +320,13 @@ Adds the specified import line to all files, automatically determining correct p **Python & Config File Name:** add_imports **CLI Flags:** - - -a - - --add-import +- -a +- --add-import + +**No Examples** ## Remove Imports + Removes the specified import from all files. **Type:** Frozenset @@ -255,10 +334,13 @@ Removes the specified import from all files. **Python & Config File Name:** remove_imports **CLI Flags:** - - --rm - - --remove-import +- --rm +- --remove-import + +**No Examples** ## Reverse Relative + Reverse order of relative imports. **Type:** Bool @@ -266,10 +348,13 @@ Reverse order of relative imports. **Python & Config File Name:** reverse_relative **CLI Flags:** - - --rr - - --reverse-relative +- --rr +- --reverse-relative + +**No Examples** ## Force Single Line + Forces all from imports to appear on their own line **Type:** Bool @@ -277,10 +362,13 @@ Forces all from imports to appear on their own line **Python & Config File Name:** force_single_line **CLI Flags:** - - --sl - - --force-single-line-imports +- --sl +- --force-single-line-imports + +**No Examples** ## Single Line Exclusions + One or more modules to exclude from the single line rule. **Type:** Tuple @@ -288,10 +376,13 @@ One or more modules to exclude from the single line rule. **Python & Config File Name:** single_line_exclusions **CLI Flags:** - - --nsl - - --single-line-exclusions +- --nsl +- --single-line-exclusions + +**No Examples** ## Default Section + Sets the default section for imports (by default FIRSTPARTY) options: ('FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER') **Type:** String @@ -299,10 +390,13 @@ Sets the default section for imports (by default FIRSTPARTY) options: ('FUTURE', **Python & Config File Name:** default_section **CLI Flags:** - - --sd - - --section-default +- --sd +- --section-default + +**No Examples** ## Import Headings + **No Description** **Type:** Dict @@ -310,9 +404,12 @@ Sets the default section for imports (by default FIRSTPARTY) options: ('FUTURE', **Python & Config File Name:** import_headings **CLI Flags:** - - **Not Supported** +- **Not Supported** + +**No Examples** ## Balanced Wrapping + Balances wrapping to produce the most consistent line length possible **Type:** Bool @@ -320,10 +417,13 @@ Balances wrapping to produce the most consistent line length possible **Python & Config File Name:** balanced_wrapping **CLI Flags:** - - -e - - --balanced +- -e +- --balanced + +**No Examples** ## Use Parentheses + Use parenthesis for line continuation on length limit instead of slashes. **Type:** Bool @@ -331,10 +431,13 @@ Use parenthesis for line continuation on length limit instead of slashes. **Python & Config File Name:** use_parentheses **CLI Flags:** - - --up - - --use-parentheses +- --up +- --use-parentheses + +**No Examples** ## Order By Type + Order imports by type in addition to alphabetically **Type:** Bool @@ -342,10 +445,13 @@ Order imports by type in addition to alphabetically **Python & Config File Name:** order_by_type **CLI Flags:** - - --ot - - --order-by-type +- --ot +- --order-by-type + +**No Examples** ## Atomic + Ensures the output doesn't save if the resulting file contains syntax errors. **Type:** Bool @@ -353,10 +459,13 @@ Ensures the output doesn't save if the resulting file contains syntax errors. **Python & Config File Name:** atomic **CLI Flags:** - - --ac - - --atomic +- --ac +- --atomic + +**No Examples** ## Lines After Imports + **No Description** **Type:** Int @@ -364,10 +473,13 @@ Ensures the output doesn't save if the resulting file contains syntax errors. **Python & Config File Name:** lines_after_imports **CLI Flags:** - - --lai - - --lines-after-imports +- --lai +- --lines-after-imports + +**No Examples** ## Lines Between Sections + **No Description** **Type:** Int @@ -375,9 +487,12 @@ Ensures the output doesn't save if the resulting file contains syntax errors. **Python & Config File Name:** lines_between_sections **CLI Flags:** - - **Not Supported** +- **Not Supported** + +**No Examples** ## Lines Between Types + **No Description** **Type:** Int @@ -385,10 +500,13 @@ Ensures the output doesn't save if the resulting file contains syntax errors. **Python & Config File Name:** lines_between_types **CLI Flags:** - - --lbt - - --lines-between-types +- --lbt +- --lines-between-types + +**No Examples** ## Combine As Imports + Combines as imports on the same line. **Type:** Bool @@ -396,10 +514,13 @@ Combines as imports on the same line. **Python & Config File Name:** combine_as_imports **CLI Flags:** - - --ca - - --combine-as +- --ca +- --combine-as + +**No Examples** ## Combine Star + Ensures that if a star import is present, nothing else is imported from that namespace. **Type:** Bool @@ -407,10 +528,13 @@ Ensures that if a star import is present, nothing else is imported from that nam **Python & Config File Name:** combine_star **CLI Flags:** - - --cs - - --combine-star +- --cs +- --combine-star + +**No Examples** ## Keep Direct And As Imports + Turns off default behavior that removes direct imports when as imports exist. **Type:** Bool @@ -418,10 +542,13 @@ Turns off default behavior that removes direct imports when as imports exist. **Python & Config File Name:** keep_direct_and_as_imports **CLI Flags:** - - -k - - --keep-direct-and-as +- -k +- --keep-direct-and-as + +**No Examples** ## Include Trailing Comma + Includes a trailing comma on multi line imports that include parentheses. **Type:** Bool @@ -429,10 +556,13 @@ Includes a trailing comma on multi line imports that include parentheses. **Python & Config File Name:** include_trailing_comma **CLI Flags:** - - --tc - - --trailing-comma +- --tc +- --trailing-comma + +**No Examples** ## From First + Switches the typical ordering preference, showing from imports first then straight ones. **Type:** Bool @@ -440,10 +570,13 @@ Switches the typical ordering preference, showing from imports first then straig **Python & Config File Name:** from_first **CLI Flags:** - - --ff - - --from-first +- --ff +- --from-first + +**No Examples** ## Verbose + Shows verbose output, such as when files are skipped or when a check is successful. **Type:** Bool @@ -451,10 +584,13 @@ Shows verbose output, such as when files are skipped or when a check is successf **Python & Config File Name:** verbose **CLI Flags:** - - -v - - --verbose +- -v +- --verbose + +**No Examples** ## Quiet + Shows extra quiet output, only errors are outputted. **Type:** Bool @@ -462,10 +598,13 @@ Shows extra quiet output, only errors are outputted. **Python & Config File Name:** quiet **CLI Flags:** - - -q - - --quiet +- -q +- --quiet + +**No Examples** ## Force Adds + Forces import adds even if the original file is empty. **Type:** Bool @@ -473,32 +612,40 @@ Forces import adds even if the original file is empty. **Python & Config File Name:** force_adds **CLI Flags:** - - --af - - --force-adds +- --af +- --force-adds + +**No Examples** ## Force Alphabetical Sort Within Sections -Force all imports to be sorted alphabetically within a section + +**No Description** **Type:** Bool **Default:** `False` **Python & Config File Name:** force_alphabetical_sort_within_sections **CLI Flags:** - - --fass - - --force-alphabetical-sort-within-sections +- **Not Supported** + +**No Examples** ## Force Alphabetical Sort -Force all imports to be sorted as a single section + +Force all imports to be sorted alphabetically within a section **Type:** Bool **Default:** `False` **Python & Config File Name:** force_alphabetical_sort **CLI Flags:** - - --fas - - --force-alphabetical-sort +- --fass +- --force-alphabetical-sort-within-sections + +**No Examples** ## Force Grid Wrap + Force number of from imports (defaults to 2) to be grid wrapped regardless of line length **Type:** Int @@ -506,10 +653,13 @@ Force number of from imports (defaults to 2) to be grid wrapped regardless of li **Python & Config File Name:** force_grid_wrap **CLI Flags:** - - --fgw - - --force-grid-wrap +- --fgw +- --force-grid-wrap + +**No Examples** ## Force Sort Within Sections + Force imports to be sorted by module, independent of import_type **Type:** Bool @@ -517,10 +667,13 @@ Force imports to be sorted by module, independent of import_type **Python & Config File Name:** force_sort_within_sections **CLI Flags:** - - --fss - - --force-sort-within-sections +- --fss +- --force-sort-within-sections + +**No Examples** ## Lexicographical + **No Description** **Type:** Bool @@ -528,9 +681,12 @@ Force imports to be sorted by module, independent of import_type **Python & Config File Name:** lexicographical **CLI Flags:** - - **Not Supported** +- **Not Supported** + +**No Examples** ## Ignore Whitespace + Tells isort to ignore whitespace differences when --check-only is being used. **Type:** Bool @@ -538,10 +694,13 @@ Tells isort to ignore whitespace differences when --check-only is being used. **Python & Config File Name:** ignore_whitespace **CLI Flags:** - - --ws - - --ignore-whitespace +- --ws +- --ignore-whitespace + +**No Examples** ## No Lines Before + Sections which should not be split with previous by empty lines **Type:** Frozenset @@ -549,10 +708,13 @@ Sections which should not be split with previous by empty lines **Python & Config File Name:** no_lines_before **CLI Flags:** - - --nlb - - --no-lines-before +- --nlb +- --no-lines-before + +**No Examples** ## No Inline Sort + Leaves `from` imports with multiple imports 'as-is' (e.g. `from foo import a, c ,b`). **Type:** Bool @@ -560,10 +722,13 @@ Leaves `from` imports with multiple imports 'as-is' (e.g. `from foo import a, c **Python & Config File Name:** no_inline_sort **CLI Flags:** - - --nis - - --no-inline-sort +- --nis +- --no-inline-sort + +**No Examples** ## Ignore Comments + **No Description** **Type:** Bool @@ -571,9 +736,12 @@ Leaves `from` imports with multiple imports 'as-is' (e.g. `from foo import a, c **Python & Config File Name:** ignore_comments **CLI Flags:** - - **Not Supported** +- **Not Supported** + +**No Examples** ## Case Sensitive + Tells isort to include casing when sorting module names **Type:** Bool @@ -581,9 +749,12 @@ Tells isort to include casing when sorting module names **Python & Config File Name:** case_sensitive **CLI Flags:** - - --case-sensitive +- --case-sensitive + +**No Examples** ## Sources + **No Description** **Type:** Tuple @@ -591,9 +762,12 @@ Tells isort to include casing when sorting module names **Python & Config File Name:** sources **CLI Flags:** - - **Not Supported** +- **Not Supported** + +**No Examples** ## Virtual Env + Virtual environment to use for determining whether a package is third-party **Type:** String @@ -601,9 +775,12 @@ Virtual environment to use for determining whether a package is third-party **Python & Config File Name:** virtual_env **CLI Flags:** - - --virtual-env +- --virtual-env + +**No Examples** ## Conda Env + Conda environment to use for determining whether a package is third-party **Type:** String @@ -611,9 +788,12 @@ Conda environment to use for determining whether a package is third-party **Python & Config File Name:** conda_env **CLI Flags:** - - --conda-env +- --conda-env + +**No Examples** ## Ensure Newline Before Comments + Inserts a blank line before a comment following an import. **Type:** Bool @@ -621,10 +801,13 @@ Inserts a blank line before a comment following an import. **Python & Config File Name:** ensure_newline_before_comments **CLI Flags:** - - -n - - --ensure-newline-before-comments +- -n +- --ensure-newline-before-comments + +**No Examples** ## Directory + **No Description** **Type:** String @@ -632,9 +815,12 @@ Inserts a blank line before a comment following an import. **Python & Config File Name:** directory **CLI Flags:** - - **Not Supported** +- **Not Supported** + +**No Examples** ## Profile + Base profile type to use for configuration. **Type:** String @@ -642,9 +828,12 @@ Base profile type to use for configuration. **Python & Config File Name:** profile **CLI Flags:** - - --profile +- --profile + +**No Examples** ## Src Paths + Add an explicitly defined source path (modules within src paths have their imports automatically catorgorized as first_party). **Type:** Frozenset @@ -652,10 +841,13 @@ Add an explicitly defined source path (modules within src paths have their impor **Python & Config File Name:** src_paths **CLI Flags:** - - --src - - --src-path +- --src +- --src-path + +**No Examples** ## Old Finders + Use the old deprecated finder logic that relies on environment introspection magic. **Type:** Bool @@ -663,10 +855,13 @@ Use the old deprecated finder logic that relies on environment introspection mag **Python & Config File Name:** old_finders **CLI Flags:** - - --old-finders - - --magic-placement +- --old-finders +- --magic-placement + +**No Examples** ## Check + Checks the file for unsorted / unformatted imports and prints them to the command line without modifying the file. **Type:** Bool @@ -674,11 +869,14 @@ Checks the file for unsorted / unformatted imports and prints them to the comman **Python & Config File Name:** **Not Supported** **CLI Flags:** - - -c - - --check-only - - --check +- -c +- --check-only +- --check + +**No Examples** ## Write To Stdout + Force resulting output to stdout, instead of in-place. **Type:** Bool @@ -686,10 +884,13 @@ Force resulting output to stdout, instead of in-place. **Python & Config File Name:** **Not Supported** **CLI Flags:** - - -d - - --stdout +- -d +- --stdout + +**No Examples** ## Show Diff + Prints a diff of all the changes isort would make to a file, instead of changing it in place **Type:** Bool @@ -697,10 +898,13 @@ Prints a diff of all the changes isort would make to a file, instead of changing **Python & Config File Name:** **Not Supported** **CLI Flags:** - - --df - - --diff +- --df +- --diff + +**No Examples** ## Jobs + Number of files to process in parallel. **Type:** Int @@ -708,10 +912,13 @@ Number of files to process in parallel. **Python & Config File Name:** **Not Supported** **CLI Flags:** - - -j - - --jobs +- -j +- --jobs + +**No Examples** ## Dont Order By Type + Don't order imports by type in addition to alphabetically **Type:** Bool @@ -719,10 +926,13 @@ Don't order imports by type in addition to alphabetically **Python & Config File Name:** **Not Supported** **CLI Flags:** - - --dt - - --dont-order-by-type +- --dt +- --dont-order-by-type + +**No Examples** ## Settings Path + Explicitly set the settings path or file instead of auto determining based on file location. **Type:** String @@ -730,12 +940,15 @@ Explicitly set the settings path or file instead of auto determining based on fi **Python & Config File Name:** **Not Supported** **CLI Flags:** - - --sp - - --settings-path - - --settings-file - - --settings +- --sp +- --settings-path +- --settings-file +- --settings + +**No Examples** ## Show Version + Displays the currently installed version of isort. **Type:** Bool @@ -743,10 +956,13 @@ Displays the currently installed version of isort. **Python & Config File Name:** **Not Supported** **CLI Flags:** - - -V - - --version +- -V +- --version + +**No Examples** ## Version Number + Returns just the current version number without the logo **Type:** String @@ -754,10 +970,13 @@ Returns just the current version number without the logo **Python & Config File Name:** **Not Supported** **CLI Flags:** - - --vn - - --version-number +- --vn +- --version-number + +**No Examples** ## Filter Files + Tells isort to filter files even when they are explicitly passed in as part of the command **Type:** Bool @@ -765,9 +984,12 @@ Tells isort to filter files even when they are explicitly passed in as part of t **Python & Config File Name:** **Not Supported** **CLI Flags:** - - --filter-files +- --filter-files + +**No Examples** ## Files + One or more Python source files that need their imports sorted. **Type:** String @@ -775,9 +997,12 @@ One or more Python source files that need their imports sorted. **Python & Config File Name:** **Not Supported** **CLI Flags:** - - +- + +**No Examples** ## Ask To Apply + Tells isort to apply changes interactively. **Type:** Bool @@ -785,9 +1010,12 @@ Tells isort to apply changes interactively. **Python & Config File Name:** **Not Supported** **CLI Flags:** - - --interactive +- --interactive + +**No Examples** ## Show Config + See isort's determined config, as well as sources of config options. **Type:** Bool @@ -795,4 +1023,6 @@ See isort's determined config, as well as sources of config options. **Python & Config File Name:** **Not Supported** **CLI Flags:** - - --show-config +- --show-config + +**No Examples** diff --git a/isort/main.py b/isort/main.py index 744e39131..2f87ba861 100644 --- a/isort/main.py +++ b/isort/main.py @@ -84,7 +84,9 @@ def sort_imports( skipped: bool = False if check: try: - incorrectly_sorted = not api.check_file(file_name, config=config, **kwargs) + incorrectly_sorted = not api.check_file( + file_name, config=config, **kwargs + ) except FileSkipped: skipped = True return SortAttempt(incorrectly_sorted, skipped) @@ -105,11 +107,15 @@ def sort_imports( return None -def iter_source_code(paths: Iterable[str], config: Config, skipped: List[str]) -> Iterator[str]: +def iter_source_code( + paths: Iterable[str], config: Config, skipped: List[str] +) -> Iterator[str]: """Iterate over all Python source files defined in paths.""" for path in paths: if os.path.isdir(path): - for dirpath, dirnames, filenames in os.walk(path, topdown=True, followlinks=True): + for dirpath, dirnames, filenames in os.walk( + path, topdown=True, followlinks=True + ): base_path = Path(dirpath) for dirname in list(dirnames): if config.is_skipped(base_path / dirname): @@ -278,12 +284,16 @@ def _build_arg_parser() -> argparse.ArgumentParser: parser.add_argument( "-i", "--indent", - help='String to place for indents defaults to " " (4 spaces).', + help='String to place for indents defaults to `" "` (4 spaces).', dest="indent", type=str, ) parser.add_argument( - "-j", "--jobs", help="Number of files to process in parallel.", dest="jobs", type=int + "-j", + "--jobs", + help="Number of files to process in parallel.", + dest="jobs", + type=int, ) parser.add_argument( "-k", @@ -292,8 +302,12 @@ def _build_arg_parser() -> argparse.ArgumentParser: action="store_true", help="Turns off default behavior that removes direct imports when as imports exist.", ) - parser.add_argument("--lai", "--lines-after-imports", dest="lines_after_imports", type=int) - parser.add_argument("--lbt", "--lines-between-types", dest="lines_between_types", type=int) + parser.add_argument( + "--lai", "--lines-after-imports", dest="lines_after_imports", type=int + ) + parser.add_argument( + "--lbt", "--lines-between-types", dest="lines_between_types", type=int + ) parser.add_argument( "--le", "--line-ending", @@ -524,7 +538,9 @@ def _build_arg_parser() -> argparse.ArgumentParser: "part of the command", ) parser.add_argument( - "files", nargs="*", help="One or more Python source files that need their imports sorted." + "files", + nargs="*", + help="One or more Python source files that need their imports sorted.", ) parser.add_argument( "--py", @@ -569,7 +585,9 @@ def _build_arg_parser() -> argparse.ArgumentParser: def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: parser = _build_arg_parser() - arguments = {key: value for key, value in vars(parser.parse_args(argv)).items() if value} + arguments = { + key: value for key, value in vars(parser.parse_args(argv)).items() if value + } if "dont_order_by_type" in arguments: arguments["order_by_type"] = False multi_line_output = arguments.get("multi_line_output", None) @@ -593,7 +611,9 @@ def _preconvert(item): raise TypeError("Unserializable object {} of type {}".format(item, type(item))) -def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = None) -> None: +def main( + argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = None +) -> None: arguments = parse_args(argv) if arguments.get("show_version"): print(ASCII_ART) @@ -650,7 +670,11 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = config = Config(**config_dict) if show_config: - print(json.dumps(config.__dict__, indent=4, separators=(",", ": "), default=_preconvert)) + print( + json.dumps( + config.__dict__, indent=4, separators=(",", ": "), default=_preconvert + ) + ) return wrong_sorted_files = False diff --git a/scripts/build_config_option_docs.py b/scripts/build_config_option_docs.py index 06fbe2b53..9f3996cb0 100755 --- a/scripts/build_config_option_docs.py +++ b/scripts/build_config_option_docs.py @@ -1,5 +1,6 @@ #! /bin/env python import os +import textwrap from typing import Any, Generator, Iterable, Type from isort._future import dataclass @@ -7,14 +8,17 @@ from isort.settings import _DEFAULT_SETTINGS as config OUTPUT_FILE = os.path.abspath( - os.path.join(os.path.dirname(os.path.abspath(__file__)), "../docs/configuration/options.md") + os.path.join( + os.path.dirname(os.path.abspath(__file__)), "../docs/configuration/options.md" + ) ) MD_NEWLINE = " " HUMAN_NAME = {"py_version": "Python Version", "vn": "Version Number", "str": "String"} DESCRIPTIONS = {} IGNORED = {"source", "help"} COLUMNS = ["Name", "Type", "Default", "Python / Config file", "CLI", "Description"] -HEADER = """Configuration options for isort +HEADER = """# Configuration options for isort + ======== As a code formatter isort has opinions. However, it also allows you to have your own. If your opinions disagree with those of isort, @@ -22,12 +26,11 @@ how you want your imports sorted, organized, and formatted. Too busy to build your perfect isort configuration? For curated common configurations, see isort's [built-in profiles](https://timothycrosley.github.io/isort/docs/configuration/profiles/). - """ parser = _build_arg_parser() -@dataclass(frozen=True) +@dataclass class ConfigOption: name: str type: Type = str @@ -35,14 +38,70 @@ class ConfigOption: config_name: str = "**Not Supported**" cli_options: Iterable[str] = ("**Not Supported**",) description: str = "**No Description**" + example_section: str = "" + example_cfg: str = "" + example_pyproject_toml: str = "" + example_cli: str = "" + + def __post_init__(self): + if ( + self.example_cfg == "" + and self.example_pyproject_toml == "" + and self.example_cli == "" + ): + self.example_section = "**No Examples**" + else: + if self.example_cfg == "": + self.example_cfg = "No example `.isort.cfg`" + else: + self.example_cfg = textwrap.dedent( + f""" + ### Example `.isort.cfg` + + ``` + {self.example_cfg} + ``` + """ + ) + + if self.example_pyproject_toml == "": + self.example_pyproject_toml = "No example pyproject.toml" + else: + self.example_pyproject_toml = textwrap.dedent( + f""" + ### Example `pyproject.toml` + + ``` + {self.example_pyproject_toml} + ``` + """ + ) + print(self.example_pyproject_toml) + + if self.example_cli == "": + self.example_cli = "No example cli usage" + else: + self.example_cli = textwrap.dedent( + f""" + ### Example cli usage + `{self.example_cli}` + """ + ) + + self.example_section = f"""**Examples:** + +{self.example_cfg} +{self.example_pyproject_toml} +{self.example_cli}""" def __str__(self): if self.name in IGNORED: return "" - cli_options = "\n - ".join(self.cli_options) + cli_options = "\n- ".join(self.cli_options) return f""" ## {human(self.name)} + {self.description} **Type:** {human(self.type.__name__)}{MD_NEWLINE} @@ -50,7 +109,9 @@ def __str__(self): **Python & Config File Name:** {self.config_name}{MD_NEWLINE} **CLI Flags:** - - {cli_options} +- {cli_options} + +{self.example_section} """ @@ -75,9 +136,34 @@ def config_options() -> Generator[ConfigOption, None, None]: if cli.help: extra_kwargs["description"] = cli.help - yield ConfigOption( - name=name, type=type(default), default=default, config_name=name, **extra_kwargs - ) + default_display = default + if isinstance(default, (set, frozenset)) and len(default) > 0: + default_display = tuple(i for i in sorted(default)) + + # todo: refactor place for example params + # needs to integrate with isort/settings/_Config + # needs to integrate with isort/main/_build_arg_parser + if name != "known_other": + yield ConfigOption( + name=name, + type=type(default), + default=default_display, + config_name=name, + **extra_kwargs, + ) + else: + yield ConfigOption( + name=name, + type=type(default), + default=default_display, + config_name=name, + example_pyproject_toml=textwrap.dedent( + """[tool.isort] + sections = ['FUTURE', 'STDLIB', 'THIRDPARTY', 'AIRFLOW', 'FIRSTPARTY', 'LOCALFOLDER'] + known_airflow = ['airflow']""" + ), + **extra_kwargs, + ) for name, cli in cli_actions.items(): extra_kwargs = {} @@ -90,12 +176,17 @@ def config_options() -> Generator[ConfigOption, None, None]: extra_kwargs["description"] = cli.help yield ConfigOption( - name=name, default=cli.default, cli_options=cli.option_strings, **extra_kwargs + name=name, + default=cli.default, + cli_options=cli.option_strings, + **extra_kwargs, ) def document_text() -> str: - return f"{HEADER}{''.join(str(config_option) for config_option in config_options())}" + return ( + f"{HEADER}{''.join(str(config_option) for config_option in config_options())}" + ) def write_document(): From c22423ecab95b3b0a95740dbcd79b88c6ed1abe8 Mon Sep 17 00:00:00 2001 From: r-richmond Date: Sun, 5 Jul 2020 09:49:03 -0700 Subject: [PATCH 0699/1439] revert changes to main.py --- isort/main.py | 46 +++++++++++----------------------------------- 1 file changed, 11 insertions(+), 35 deletions(-) diff --git a/isort/main.py b/isort/main.py index 2f87ba861..744e39131 100644 --- a/isort/main.py +++ b/isort/main.py @@ -84,9 +84,7 @@ def sort_imports( skipped: bool = False if check: try: - incorrectly_sorted = not api.check_file( - file_name, config=config, **kwargs - ) + incorrectly_sorted = not api.check_file(file_name, config=config, **kwargs) except FileSkipped: skipped = True return SortAttempt(incorrectly_sorted, skipped) @@ -107,15 +105,11 @@ def sort_imports( return None -def iter_source_code( - paths: Iterable[str], config: Config, skipped: List[str] -) -> Iterator[str]: +def iter_source_code(paths: Iterable[str], config: Config, skipped: List[str]) -> Iterator[str]: """Iterate over all Python source files defined in paths.""" for path in paths: if os.path.isdir(path): - for dirpath, dirnames, filenames in os.walk( - path, topdown=True, followlinks=True - ): + for dirpath, dirnames, filenames in os.walk(path, topdown=True, followlinks=True): base_path = Path(dirpath) for dirname in list(dirnames): if config.is_skipped(base_path / dirname): @@ -284,16 +278,12 @@ def _build_arg_parser() -> argparse.ArgumentParser: parser.add_argument( "-i", "--indent", - help='String to place for indents defaults to `" "` (4 spaces).', + help='String to place for indents defaults to " " (4 spaces).', dest="indent", type=str, ) parser.add_argument( - "-j", - "--jobs", - help="Number of files to process in parallel.", - dest="jobs", - type=int, + "-j", "--jobs", help="Number of files to process in parallel.", dest="jobs", type=int ) parser.add_argument( "-k", @@ -302,12 +292,8 @@ def _build_arg_parser() -> argparse.ArgumentParser: action="store_true", help="Turns off default behavior that removes direct imports when as imports exist.", ) - parser.add_argument( - "--lai", "--lines-after-imports", dest="lines_after_imports", type=int - ) - parser.add_argument( - "--lbt", "--lines-between-types", dest="lines_between_types", type=int - ) + parser.add_argument("--lai", "--lines-after-imports", dest="lines_after_imports", type=int) + parser.add_argument("--lbt", "--lines-between-types", dest="lines_between_types", type=int) parser.add_argument( "--le", "--line-ending", @@ -538,9 +524,7 @@ def _build_arg_parser() -> argparse.ArgumentParser: "part of the command", ) parser.add_argument( - "files", - nargs="*", - help="One or more Python source files that need their imports sorted.", + "files", nargs="*", help="One or more Python source files that need their imports sorted." ) parser.add_argument( "--py", @@ -585,9 +569,7 @@ def _build_arg_parser() -> argparse.ArgumentParser: def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: parser = _build_arg_parser() - arguments = { - key: value for key, value in vars(parser.parse_args(argv)).items() if value - } + arguments = {key: value for key, value in vars(parser.parse_args(argv)).items() if value} if "dont_order_by_type" in arguments: arguments["order_by_type"] = False multi_line_output = arguments.get("multi_line_output", None) @@ -611,9 +593,7 @@ def _preconvert(item): raise TypeError("Unserializable object {} of type {}".format(item, type(item))) -def main( - argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = None -) -> None: +def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = None) -> None: arguments = parse_args(argv) if arguments.get("show_version"): print(ASCII_ART) @@ -670,11 +650,7 @@ def main( config = Config(**config_dict) if show_config: - print( - json.dumps( - config.__dict__, indent=4, separators=(",", ": "), default=_preconvert - ) - ) + print(json.dumps(config.__dict__, indent=4, separators=(",", ": "), default=_preconvert)) return wrong_sorted_files = False From 48a2319110e52a5c05beeab2a479e3191122e893 Mon Sep 17 00:00:00 2001 From: r-richmond Date: Sun, 5 Jul 2020 09:52:43 -0700 Subject: [PATCH 0700/1439] black format to 100 line length --- scripts/build_config_option_docs.py | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/scripts/build_config_option_docs.py b/scripts/build_config_option_docs.py index 9f3996cb0..a1d43f43a 100755 --- a/scripts/build_config_option_docs.py +++ b/scripts/build_config_option_docs.py @@ -8,9 +8,7 @@ from isort.settings import _DEFAULT_SETTINGS as config OUTPUT_FILE = os.path.abspath( - os.path.join( - os.path.dirname(os.path.abspath(__file__)), "../docs/configuration/options.md" - ) + os.path.join(os.path.dirname(os.path.abspath(__file__)), "../docs/configuration/options.md") ) MD_NEWLINE = " " HUMAN_NAME = {"py_version": "Python Version", "vn": "Version Number", "str": "String"} @@ -44,11 +42,7 @@ class ConfigOption: example_cli: str = "" def __post_init__(self): - if ( - self.example_cfg == "" - and self.example_pyproject_toml == "" - and self.example_cli == "" - ): + if self.example_cfg == "" and self.example_pyproject_toml == "" and self.example_cli == "": self.example_section = "**No Examples**" else: if self.example_cfg == "": @@ -176,17 +170,12 @@ def config_options() -> Generator[ConfigOption, None, None]: extra_kwargs["description"] = cli.help yield ConfigOption( - name=name, - default=cli.default, - cli_options=cli.option_strings, - **extra_kwargs, + name=name, default=cli.default, cli_options=cli.option_strings, **extra_kwargs, ) def document_text() -> str: - return ( - f"{HEADER}{''.join(str(config_option) for config_option in config_options())}" - ) + return f"{HEADER}{''.join(str(config_option) for config_option in config_options())}" def write_document(): From 7364c6d25d67617b6596ae643d7f4dd49e7ab116 Mon Sep 17 00:00:00 2001 From: "John L. Villalovos" Date: Sun, 5 Jul 2020 15:15:26 -0700 Subject: [PATCH 0701/1439] Fix documentation with missing dot '.' Commit 11e2a74f5d379a68edaa064a0aeb70d8bcad9491 changed the line to remove the '-rc' flag but seems to have accidentally removed the dot character at the same time. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a6c7f1389..305bdf19e 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ isort mypythonfile.py mypythonfile2.py or recursively: ```bash -isort +isort . ``` *which is equivalent to:* From b13f8fdabc46c8308b94ec385474a4a1486e93e7 Mon Sep 17 00:00:00 2001 From: r-richmond Date: Sun, 5 Jul 2020 15:28:38 -0700 Subject: [PATCH 0702/1439] Standarize black config --- pyproject.toml | 3 +++ scripts/clean.sh | 2 +- scripts/lint.sh | 3 +-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a47cabc94..23332d38d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,6 @@ +[tool.black] +line-length = 100 + [tool.poetry] name = "isort" version = "5.0.3" diff --git a/scripts/clean.sh b/scripts/clean.sh index d3aa1e6b4..c29f44826 100755 --- a/scripts/clean.sh +++ b/scripts/clean.sh @@ -2,4 +2,4 @@ set -euxo pipefail poetry run isort --profile hug isort/ tests/ scripts/ -poetry run black isort/ tests/ scripts/ -l 100 +poetry run black isort/ tests/ scripts/ diff --git a/scripts/lint.sh b/scripts/lint.sh index e98e032d0..086e91126 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -1,10 +1,9 @@ #!/bin/bash set -euxo pipefail - poetry run cruft check poetry run mypy --ignore-missing-imports isort/ -poetry run black --check -l 100 isort/ tests/ +poetry run black --check isort/ tests/ poetry run isort --profile hug --check --diff isort/ tests/ poetry run flake8 isort/ tests/ --max-line 100 --ignore F403,F401,W503,E203 poetry run safety check From 0f7896221c69402526d4aa5798694d4b724dfa6d Mon Sep 17 00:00:00 2001 From: Kosei Kitahara Date: Mon, 6 Jul 2020 10:30:22 +0900 Subject: [PATCH 0703/1439] Remove not_skip. Fixes: #1271 --- docs/configuration/profiles.md | 1 - isort/profiles.py | 1 - tests/test_isort.py | 1 - 3 files changed, 3 deletions(-) diff --git a/docs/configuration/profiles.md b/docs/configuration/profiles.md index 8f859f232..36dcaf720 100644 --- a/docs/configuration/profiles.md +++ b/docs/configuration/profiles.md @@ -63,7 +63,6 @@ To use any of the listed profiles, use `isort --profile PROFILE_NAME` from the c - **lines_after_imports**: `2` - **lines_between_types**: `1` - **multi_line_output**: `3` - - **not_skip**: `'__init__.py'` - **use_parentheses**: `True` #hug diff --git a/isort/profiles.py b/isort/profiles.py index a35c38cbf..d8a00ce54 100644 --- a/isort/profiles.py +++ b/isort/profiles.py @@ -40,7 +40,6 @@ "lines_after_imports": 2, "lines_between_types": 1, "multi_line_output": 3, - "not_skip": "__init__.py", "use_parentheses": True, } hug = { diff --git a/tests/test_isort.py b/tests/test_isort.py index 9c5fc5d4c..4ef2a0a92 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -34,7 +34,6 @@ ignore_frosted_errors = E103 skip = build,.tox,venv balanced_wrapping = true -not_skip = __init__.py """ SHORT_IMPORT = "from third_party import lib1, lib2, lib3, lib4" SINGLE_FROM_IMPORT = "from third_party import lib1" From 83deb96de539537e683b844bfae9f3b692a133f8 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 5 Jul 2020 21:53:47 -0700 Subject: [PATCH 0704/1439] Add @r-richmond to acknowledgements --- docs/contributing/4.-acknowledgements.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index 750fee985..b6e0fd896 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -93,6 +93,7 @@ Code Contributors - João M.C. Teixeira (@joaomcteixeira) - Honnix (@honnix) - Anders Kaseorg (@andersk) +- @r-richmond Documenters @@ -110,6 +111,7 @@ Documenters - Brad Solomon (@bsolomon1124) - Martynas Mickevičius (@2m) - Taneli Hukkinen (@hukkinj1) +- @r-richmond -------------------------------------------- From 064410cd4c0e20a695e4a18f285934b774ffb97a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 5 Jul 2020 21:55:33 -0700 Subject: [PATCH 0705/1439] Add John Villalovos (@JohnVillalovos) to acknowledgements --- docs/contributing/4.-acknowledgements.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index b6e0fd896..c30a59c6f 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -112,6 +112,8 @@ Documenters - Martynas Mickevičius (@2m) - Taneli Hukkinen (@hukkinj1) - @r-richmond +- John Villalovos (@JohnVillalovos) + -------------------------------------------- From ac0acb39d2dd6bdb2849b13eb4b210e12a68fc73 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 5 Jul 2020 21:56:54 -0700 Subject: [PATCH 0706/1439] Add Sebastian (@sebix) to acknowledgements --- docs/contributing/4.-acknowledgements.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index c30a59c6f..2a6973814 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -94,6 +94,8 @@ Code Contributors - Honnix (@honnix) - Anders Kaseorg (@andersk) - @r-richmond +- Sebastian (@sebix) + Documenters From a3a7251323d4592b8553864d071355a11d644778 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 5 Jul 2020 22:01:43 -0700 Subject: [PATCH 0707/1439] Add - Kosei Kitahara (@Surgo) to acknowledgements --- docs/contributing/4.-acknowledgements.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index 2a6973814..9116be8a8 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -95,7 +95,7 @@ Code Contributors - Anders Kaseorg (@andersk) - @r-richmond - Sebastian (@sebix) - +- Kosei Kitahara (@Surgo) Documenters @@ -115,6 +115,7 @@ Documenters - Taneli Hukkinen (@hukkinj1) - @r-richmond - John Villalovos (@JohnVillalovos) +- Kosei Kitahara (@Surgo) -------------------------------------------- From 686d3c715183eb8ba944475512e60342cadabaf6 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 5 Jul 2020 22:36:54 -0700 Subject: [PATCH 0708/1439] Add regression test for issue #1264 demonstrating failure --- tests/test_regressions.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 tests/test_regressions.py diff --git a/tests/test_regressions.py b/tests/test_regressions.py new file mode 100644 index 000000000..5e9962d0c --- /dev/null +++ b/tests/test_regressions.py @@ -0,0 +1,14 @@ +"""A growing set of tests designed to ensure isort doesn't have regressions in new versions""" +import isort + + +def test_isort_duplicating_comments_issue_1264(): + """Ensure isort doesn't duplicate comments when force_sort_within_sections is set to `True` + as was the case in issue #1264: https://github.com/timothycrosley/isort/issues/1264 + """ + assert isort.code(""" +from homeassistant.util.logging import catch_log_exception + +# Loading the config flow file will register... +from . import config_flow +""", force_sort_within_sections=True).count("# Loading the config flow file will register...") == 1 From 78d354d9ba923fbc19c54dca03dd85d97a1e0aae Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 6 Jul 2020 00:16:36 -0700 Subject: [PATCH 0709/1439] Fix issue 1264 - removing duplicate handling of comments for section sorting --- isort/output.py | 46 ++++++++++++++++------------- scripts/build_config_option_docs.py | 2 +- tests/test_isort.py | 19 ------------ tests/test_regressions.py | 30 +++++++++++++++++-- 4 files changed, 53 insertions(+), 44 deletions(-) diff --git a/isort/output.py b/isort/output.py index ce687ff27..cf6e49684 100644 --- a/isort/output.py +++ b/isort/output.py @@ -1,7 +1,7 @@ import copy import itertools from functools import partial -from typing import Iterable, List, Tuple +from typing import Dict, Iterable, List, Tuple from isort.format import format_simplified @@ -57,9 +57,6 @@ def sorted_imports( from_modules, key=lambda key: sorting.module_key(key, config, section_name=section) ) - if config.force_sort_within_sections: - copied_comments = copy.deepcopy(parsed.categorized_comments) - section_output: List[str] = [] if config.from_first: section_output = _with_from_imports( @@ -107,11 +104,20 @@ def sorted_imports( ) if config.force_sort_within_sections: - # Remove comments - section_output = [line for line in section_output if not line.startswith("#")] + # collapse comments + comments_above = [] + new_section_output: List[str] = [] + for line in section_output: + if line.startswith("#"): + comments_above.append(line) + elif comments_above: + new_section_output.append(_LineWithComments(line, comments_above)) + comments_above = [] + else: + new_section_output.append(line) - section_output = sorting.naturally( - section_output, + new_section_output = sorting.naturally( + new_section_output, key=partial( sorting.section_key, order_by_type=config.order_by_type, @@ -121,19 +127,11 @@ def sorted_imports( ), ) - # Add comments back - all_comments = copied_comments["above"]["from"] - all_comments.update(copied_comments["above"]["straight"]) - comment_indexes = {} - for module, comment_list in all_comments.items(): - for idx, line in enumerate(section_output): - if module in line: - comment_indexes[idx] = comment_list - added = 0 - for idx, comment_list in comment_indexes.items(): - for comment in comment_list: - section_output.insert(idx + added, comment) - added += 1 + # uncollapse comments + section_output = [] + for line in new_section_output: + section_output.extend(getattr(line, "comments", ())) + section_output.append(str(line)) section_name = section no_lines_before = section_name in config.no_lines_before @@ -535,3 +533,9 @@ def _normalize_empty_lines(lines: List[str]) -> List[str]: lines.append("") return lines + + +class _LineWithComments(str): + def __new__(cls, value, comments): + cls.comments = comments + return super().__new__(cls, value) # type: ignore diff --git a/scripts/build_config_option_docs.py b/scripts/build_config_option_docs.py index a1d43f43a..927c858b4 100755 --- a/scripts/build_config_option_docs.py +++ b/scripts/build_config_option_docs.py @@ -170,7 +170,7 @@ def config_options() -> Generator[ConfigOption, None, None]: extra_kwargs["description"] = cli.help yield ConfigOption( - name=name, default=cli.default, cli_options=cli.option_strings, **extra_kwargs, + name=name, default=cli.default, cli_options=cli.option_strings, **extra_kwargs ) diff --git a/tests/test_isort.py b/tests/test_isort.py index 4ef2a0a92..94fea3d70 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -4087,25 +4087,6 @@ def test_isort_ensures_blank_line_between_import_and_comment() -> None: assert api.sort_code_string(test_input, **config) == expected_output -def test_moving_comments_issue_726(): - config = {"force_sort_within_sections": 1} # type: Dict[str, Any] - test_input = ( - "from Blue import models as BlueModels\n" - "# comment for PlaidModel\n" - "from Plaid.models import PlaidModel\n" - ) - assert api.sort_code_string(test_input, **config) == test_input - - test_input = ( - "# comment for BlueModels\n" - "from Blue import models as BlueModels\n" - "# comment for PlaidModel\n" - "# another comment for PlaidModel\n" - "from Plaid.models import PlaidModel\n" - ) - assert api.sort_code_string(test_input, **config) == test_input - - def test_pyi_formatting_issue_942(tmpdir) -> None: test_input = "import os\n\n\ndef my_method():\n" expected_py_output = test_input.splitlines() diff --git a/tests/test_regressions.py b/tests/test_regressions.py index 5e9962d0c..eb141663e 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -6,9 +6,33 @@ def test_isort_duplicating_comments_issue_1264(): """Ensure isort doesn't duplicate comments when force_sort_within_sections is set to `True` as was the case in issue #1264: https://github.com/timothycrosley/isort/issues/1264 """ - assert isort.code(""" + assert ( + isort.code( + """ from homeassistant.util.logging import catch_log_exception -# Loading the config flow file will register... +# Loading the config flow... from . import config_flow -""", force_sort_within_sections=True).count("# Loading the config flow file will register...") == 1 +""", + force_sort_within_sections=True, + ).count("# Loading the config flow...") + == 1 + ) + + +def test_moving_comments_issue_726(): + test_input = ( + "from Blue import models as BlueModels\n" + "# comment for PlaidModel\n" + "from Plaid.models import PlaidModel\n" + ) + assert isort.code(test_input, force_sort_within_sections=True) == test_input + + test_input = ( + "# comment for BlueModels\n" + "from Blue import models as BlueModels\n" + "# comment for PlaidModel\n" + "# another comment for PlaidModel\n" + "from Plaid.models import PlaidModel\n" + ) + assert isort.code(test_input, force_sort_within_sections=True) == test_input From 4c72396c8675c9cf14ddeb589014b9f7c77a7b3f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 6 Jul 2020 00:44:53 -0700 Subject: [PATCH 0710/1439] Remove some no longer accurate documentation sections --- README.md | 86 ------------------------------------------------------- 1 file changed, 86 deletions(-) diff --git a/README.md b/README.md index 305bdf19e..e58235097 100644 --- a/README.md +++ b/README.md @@ -161,92 +161,6 @@ Additionally, I will enthusiastically accept pull requests that include plugins for other text editors and add documentation for them as I am notified. -How does isort work? -==================== - -isort parses specified files for global level import lines (imports -outside of try / except blocks, functions, etc..) and puts them all at -the top of the file grouped together by the type of import: - -- Future -- Python Standard Library -- Third Party -- Current Python Project -- Explicitly Local (. before import, as in: `from . import x`) -- Custom Separate Sections (Defined by forced\_separate list in - configuration file) -- Custom Sections (Defined by sections list in configuration file) - -Inside of each section the imports are sorted alphabetically. isort -automatically removes duplicate python imports, and wraps long from -imports to the specified line length (defaults to 79). - -When will isort not work? -========================= - -If you ever have the situation where you need to have a try / except -block in the middle of top-level imports or if your import order is -directly linked to precedence. - -For example: a common practice in Django settings files is importing \* -from various settings files to form a new settings file. In this case if -any of the imports change order you are changing the settings definition -itself. - -However, you can configure isort to skip over just these files - or even -to force certain imports to the top. - -Configuring isort -================= - -If you find the default isort settings do not work well for your -project, isort provides several ways to adjust the behavior. - -To configure isort for a single user create a `~/.isort.cfg` or -`$XDG_CONFIG_HOME/.isort.cfg` file: - -```ini -[settings] -line_length=120 -force_to_top=file1.py,file2.py -skip=file3.py,file4.py -known_future_library=future,pies -known_standard_library=std,std2 -known_third_party=randomthirdparty -known_first_party=mylib1,mylib2 -indent=' ' -multi_line_output=3 -length_sort=1 -forced_separate=django.contrib,django.utils -default_section=FIRSTPARTY -no_lines_before=LOCALFOLDER -``` - -Additionally, you can specify project level configuration simply by -placing a `.isort.cfg` file at the root of your project. isort will look -up to 25 directories up, from the file it is ran against, to find a -project specific configuration. - -Or, if you prefer, you can add an `isort` or `tool:isort` section to -your project's `setup.cfg` or `tox.ini` file with any desired settings. - -You can also add your desired settings under a `[tool.isort]` section in -your `pyproject.toml` file. - -You can then override any of these settings by using command line -arguments, or by passing in override values to any of the public Python API -functions. - -Finally, as of version 3.0 isort supports editorconfig files using the -standard syntax defined here: - -Meaning you place any standard isort configuration parameters within a -.editorconfig file under the `*.py` section and they will be honored. - -For a full list of isort settings and their meanings [take a look at the -isort -wiki](https://github.com/timothycrosley/isort/wiki/isort-Settings). - Multi line output modes ======================= From a9048af93a59ecd63f065bc655eefe3c467ce34e Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 6 Jul 2020 03:16:05 -0700 Subject: [PATCH 0711/1439] Add upgrade guide --- .editorconfig | 1 - CHANGELOG.md | 6 +++- docs/major_releases/introducing_isort_5.md | 2 ++ isort/main.py | 40 +++++++++++++++++++++- 4 files changed, 46 insertions(+), 3 deletions(-) diff --git a/.editorconfig b/.editorconfig index 9fe123447..88be08210 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,7 +9,6 @@ known_third_party = kate ignore_frosted_errors = E103 skip = build,.tox,venv balanced_wrapping = true -not_skip = __init__.py [*.{rst,ini}] indent_style = space diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bc55d0d2..f791e711f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. +### 5.0.4 July 6, 2020 + - Fixed #1264: a regression with comment handling and `force_sort_within_sections` config option + - Added warning for deprecated CLI flags and linked to upgrade guide. + ### 5.0.3 - July 4, 2020 - Fixed setup.py command incorrectly passing check=True as a configuration parameter (see: https://github.com/timothycrosley/isort/issues/1258) - Fixed missing patch version @@ -22,7 +26,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. - isort deprecates official support for Python 3.4, removing modules only in this release from known_standard_library: - user - Config files are no longer composed on-top of each-other. Instead the first config file found is used. - - Since there is no longer composition negative form settings (such as --dont-skip) are no longer required and have been removed. + - Since there is no longer composition negative form settings (such as --dont-skip or it's config file variant `not_skip`) are no longer required and have been removed. - Two-letter shortened setting names (like `ac` for `atomic`) now require two dashes to avoid ambiguity: `--ac`. - For consistency with other tools `-v` now is shorthand for verbose and `-V` is shorthand for version. See Issue: #1067. - `length_sort_{section_name}` config usage has been deprecated. Instead `length_sort_sections` list can be used to specify a list of sections that need to be length sorted. diff --git a/docs/major_releases/introducing_isort_5.md b/docs/major_releases/introducing_isort_5.md index 0fff587ca..bc8c96e55 100644 --- a/docs/major_releases/introducing_isort_5.md +++ b/docs/major_releases/introducing_isort_5.md @@ -8,6 +8,8 @@ This does mean that there may be some pain with the upgrade process, but we beli [Click here for an attempt at full changelog with a list of breaking changes.](https://timothycrosley.github.io/isort/CHANGELOG/) +[Using isort 4.x.x? Click here for the isort 5.0.0 upgrade guide.](https://timothycrosley.github.io/isort/docs/upgrade_guides/5.0.0/) + [Try isort 5 right now from your browser!](https://timothycrosley.github.io/isort/docs/quick_start/0.-try/) So why the massive change? diff --git a/isort/main.py b/isort/main.py index 744e39131..7d23ec8ad 100644 --- a/isort/main.py +++ b/isort/main.py @@ -401,7 +401,7 @@ def _build_arg_parser() -> argparse.ArgumentParser: "--sd", "--section-default", dest="default_section", - help="Sets the default section for imports (by default FIRSTPARTY) options: " + help="Sets the default section for import options: " + str(sections.DEFAULT), ) parser.add_argument( @@ -564,6 +564,36 @@ def _build_arg_parser() -> argparse.ArgumentParser: action="store_true", help="See isort's determined config, as well as sources of config options.", ) + + # deprecated options + parser.add_argument( + "--recursive", + dest="deprecated_flags", + action="append_const", + const="--recursive", + help=argparse.SUPPRESS, + ) + parser.add_argument( + "-rc", dest="deprecated_flags", action="append_const", const="-rc", help=argparse.SUPPRESS + ) + parser.add_argument( + "--dont-skip", + dest="deprecated_flags", + action="append_const", + const="--dont-skip", + help=argparse.SUPPRESS, + ) + parser.add_argument( + "-ns", dest="deprecated_flags", action="append_const", const="-ns", help=argparse.SUPPRESS + ) + parser.add_argument( + "--apply", + dest="deprecated_flags", + action="append_const", + const="-y", + help=argparse.SUPPRESS, + ) + return parser @@ -642,6 +672,14 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = check = config_dict.pop("check", False) show_diff = config_dict.pop("show_diff", False) write_to_stdout = config_dict.pop("write_to_stdout", False) + deprecated_flags = config_dict.pop("deprecated_flags", False) + + if deprecated_flags: + warn( + f"\n\nThe following deprecated CLI flags where used: {', '.join(deprecated_flags)}!\n" + "Please see the 5.0.0 upgrade guide:\n" + "\thttps://timothycrosley.github.io/isort/docs/upgrade_guides/5.0.0/\n", + ) if "src_paths" in config_dict: config_dict["src_paths"] = { From fdf9d2e27ba18d21768afd4de06660d97e281110 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 6 Jul 2020 03:16:09 -0700 Subject: [PATCH 0712/1439] Add upgrade guide --- docs/upgrade_guides/5.0.0.md | 38 ++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 docs/upgrade_guides/5.0.0.md diff --git a/docs/upgrade_guides/5.0.0.md b/docs/upgrade_guides/5.0.0.md new file mode 100644 index 000000000..327f7dc40 --- /dev/null +++ b/docs/upgrade_guides/5.0.0.md @@ -0,0 +1,38 @@ +# Upgrading to 5.0.0 + +isort 5.0.0 is the first major release of isort in 5 years, and as such it does introduce some breaking changes. +This guide is meant to help migrate projects from using isort 4.x.x unto the 5.0.0 release. + +Related documentation: + +* [isort 5.0.0 changelog](https://timothycrosley.github.io/isort/CHANGELOG/#500-penny-july-4-2020) +* [isort 5 release document](https://timothycrosley.github.io/isort/docs/major_releases/introducing_isort_5/) + +## Migrating CLI options + +### `--dont-skip` or `-ns` +In an earlier version isort had a default skip of `__init__.py`. To get around that many projects wanted a way to not skip `__init__.py` or any other files that were automatically skipped in the future by isort. isort no longer has any default skips, so if the value here is `__init__.py` you can simply remove the command line option. If it is something else, just make sure you aren't specifying to skip that file somewhere else in your config. + +### `--recursive` or `-rc` +Prior to version 5.0.0, isort wouldn't automatically traverse directories. The --recursive option was necessary to tell it to do so. In 5.0.0 directories are automatically traversed for all Python files, and as such this option is no longer necessary and should simply be removed. + +### `--apply` or `-y` +Prior to version 5.0.0, depending on how isort was executed, it would ask you before making every file change. In isort 5.0.0 file changes happen by default inline with other formatters. `--interactive` is available to restore the previous behavior. If encountered this option can simply be removed. + +### `-ac`, `-wl`, `-ws`, `-tc`, `-sp`, `-sp`, `-sl`, `-sg`, `-sd`, `-rr`, `-ot`, `-nlb`, `-nis`, `-ls`, `-le`, `-lbt`, `-lai`, `-fss`, `-fgw`, `-ff`, `-fass`, `-fas`, `-dt`, `-ds`, `-df`, `-cs`, `-ca`, `-af`, `-ac` +Two-letter shortened setting names (like `ac` for `atomic`) now require two dashes to avoid ambiguity. Simply add another dash before the option, or switch to the long form option to fix (example: `--ac` or `--atomic`). + +## Migrating Config options + +The first thing to keep in mind is how isort loads config options has changed in isort 5. It will no longer merge multiple config files, instead you must have 1 isort config per a project. +If you have multiple configs, they will need to be merged into 1 single one. You can see the priority order of configuration files and the manor in which they are loaded on the +[config files documentation page](https://timothycrosley.github.io/isort/docs/configuration/config_files/). + +### `not_skip` +This is the same as the `--dont-skip` CLI option above. In an earlier version isort had a default skip of `__init__.py`. To get around that many projects wanted a way to not skip `__init__.py` or any other files that were automatically skipped in the future by isort. isort no longer has any default skips, so if the value here is `__init__.py` you can simply remove the setting. If it is something else, just make sure you aren't specifying to skip that file somewhere else in your config. + +### module placement changes: `known_third_party`, `known_first_party`, `default_section`, etc... +isort has completely rewritten its logic for placing modules in 5.0.0 to ensure the same behavior across environments. You can see the details of this change [here](https://github.com/timothycrosley/isort/issues/1147). +The TL;DR of which is that isort has now changed from `default_section=FIRSTPARTY` to `default_section=THIRDPARTY`. If you all already setting the default section to third party, your config is probably in good shape. +If not, you can either use the old finding approach with `--magic-placement` in the CLI or `old_finders=True` in your config, or preferably, you are able to remove all placement options and isort will determine it correctly. +If it doesn't, you should be able to just specify your projects modules with `known_first_party` and be done with it. From 77628b8def8a6d781d6a547ab1a7ead3afe348ca Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 6 Jul 2020 03:17:34 -0700 Subject: [PATCH 0713/1439] Update option documentation --- docs/configuration/options.md | 36 ++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/docs/configuration/options.md b/docs/configuration/options.md index 1866392e8..2c4c53912 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -10,7 +10,7 @@ Too busy to build your perfect isort configuration? For curated common configura ## Python Version -Tells isort to set the known standard library based on the the specified Python version. Default is to assume any Python 3 version could be the target, and use a union off all stdlib modules across versions. If auto is specified, the version of the interpreter used to run isort (currently: 37) will be used. +Tells isort to set the known standard library based on the the specified Python version. Default is to assume any Python 3 version could be the target, and use a union off all stdlib modules across versions. If auto is specified, the version of the interpreter used to run isort (currently: 38) will be used. **Type:** String **Default:** `py3` @@ -259,10 +259,10 @@ Multi line output (0-grid, 1-vertical, 2-hanging, 3-vert-hanging, 4-vert-grid, 5 ## Indent -String to place for indents defaults to " " (4 spaces). +String to place for indents defaults to " " (4 spaces). **Type:** String -**Default:** `` +**Default:** ` ` **Python & Config File Name:** indent **CLI Flags:** @@ -276,7 +276,7 @@ String to place for indents defaults to " " (4 spaces). **No Description** **Type:** String -**Default:** `#` +**Default:** ` #` **Python & Config File Name:** comment_prefix **CLI Flags:** @@ -383,7 +383,7 @@ One or more modules to exclude from the single line rule. ## Default Section -Sets the default section for imports (by default FIRSTPARTY) options: ('FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER') +Sets the default section for import options: ('FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER') **Type:** String **Default:** `THIRDPARTY` @@ -619,28 +619,29 @@ Forces import adds even if the original file is empty. ## Force Alphabetical Sort Within Sections -**No Description** +Force all imports to be sorted alphabetically within a section **Type:** Bool **Default:** `False` **Python & Config File Name:** force_alphabetical_sort_within_sections **CLI Flags:** -- **Not Supported** +- --fass +- --force-alphabetical-sort-within-sections **No Examples** ## Force Alphabetical Sort -Force all imports to be sorted alphabetically within a section +Force all imports to be sorted as a single section **Type:** Bool **Default:** `False` **Python & Config File Name:** force_alphabetical_sort **CLI Flags:** -- --fass -- --force-alphabetical-sort-within-sections +- --fas +- --force-alphabetical-sort **No Examples** @@ -997,7 +998,7 @@ One or more Python source files that need their imports sorted. **Python & Config File Name:** **Not Supported** **CLI Flags:** -- +- **No Examples** @@ -1026,3 +1027,16 @@ See isort's determined config, as well as sources of config options. - --show-config **No Examples** + +## Deprecated Flags + +==SUPPRESS== + +**Type:** String +**Default:** `None` +**Python & Config File Name:** **Not Supported** +**CLI Flags:** + +- --apply + +**No Examples** From f6ca70e0f9fe8514363e17d60194363302225c07 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 6 Jul 2020 03:17:59 -0700 Subject: [PATCH 0714/1439] Bump version to 5.0.4 --- isort/_version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/_version.py b/isort/_version.py index 4682e6132..af29f3407 100644 --- a/isort/_version.py +++ b/isort/_version.py @@ -1 +1 @@ -__version__ = "5.0.3" +__version__ = "5.0.4" diff --git a/pyproject.toml b/pyproject.toml index 848ac9629..a50bb6590 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ line-length = 100 [tool.poetry] name = "isort" -version = "5.0.3" +version = "5.0.4" description = "A Python utility / library to sort Python imports." authors = ["Timothy Crosley "] license = "MIT" From defefc24c6f04978c486d5e86857529b6f12abb6 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 6 Jul 2020 03:21:27 -0700 Subject: [PATCH 0715/1439] Add pragma no cover to deprecated flags check --- isort/main.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/isort/main.py b/isort/main.py index 7d23ec8ad..2e1b68bb3 100644 --- a/isort/main.py +++ b/isort/main.py @@ -401,8 +401,7 @@ def _build_arg_parser() -> argparse.ArgumentParser: "--sd", "--section-default", dest="default_section", - help="Sets the default section for import options: " - + str(sections.DEFAULT), + help="Sets the default section for import options: " + str(sections.DEFAULT), ) parser.add_argument( "--sg", @@ -674,11 +673,11 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = write_to_stdout = config_dict.pop("write_to_stdout", False) deprecated_flags = config_dict.pop("deprecated_flags", False) - if deprecated_flags: + if deprecated_flags: # pragma: no cover warn( f"\n\nThe following deprecated CLI flags where used: {', '.join(deprecated_flags)}!\n" "Please see the 5.0.0 upgrade guide:\n" - "\thttps://timothycrosley.github.io/isort/docs/upgrade_guides/5.0.0/\n", + "\thttps://timothycrosley.github.io/isort/docs/upgrade_guides/5.0.0/\n" ) if "src_paths" in config_dict: From 546ff329d4a792ddfb0576c78cf6d3e4f2321727 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 6 Jul 2020 16:39:14 +0300 Subject: [PATCH 0716/1439] Fix typo in profile doc description build --- scripts/build_profile_docs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build_profile_docs.py b/scripts/build_profile_docs.py index 1d09ae7ab..08916fb7c 100755 --- a/scripts/build_profile_docs.py +++ b/scripts/build_profile_docs.py @@ -24,7 +24,7 @@ def format_profile(profile_name: str, profile: Dict[str, Any]) -> str: return f""" #{profile_name} -{profile.get('descripiton', '')} +{profile.get('description', '')} {options} """ From 8aa62582bac49924965d059c7869dc32c3e039ac Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 6 Jul 2020 20:28:57 -0700 Subject: [PATCH 0717/1439] Remove tests from packaged file --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a50bb6590..048c1458b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,6 @@ include = [ "docs/contributing/*.md", "docs/major_releases/*.md", "docs/quick_start/*.md", - "tests/*.py" ] [tool.poetry.dependencies] From b876e7fcc95c02bb9603ba7ecbef65114317682d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 6 Jul 2020 20:56:43 -0700 Subject: [PATCH 0718/1439] Add Marat Sharafutdinov (@decaz) to acknowledgements --- docs/contributing/4.-acknowledgements.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index 9116be8a8..feb3b8217 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -116,6 +116,7 @@ Documenters - @r-richmond - John Villalovos (@JohnVillalovos) - Kosei Kitahara (@Surgo) +- Marat Sharafutdinov (@decaz) -------------------------------------------- From 0fa8d1641a898b9c3a6f8edc89f0816a15e15567 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 6 Jul 2020 20:58:48 -0700 Subject: [PATCH 0719/1439] Fix issue #1285: packaging issue with bundling tests via poetry. --- CHANGELOG.md | 3 +++ pyproject.toml | 7 ------- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f791e711f..966226ac4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ Changelog ========= NOTE: isort follows the [semver](https://semver.org/) versioning standard. +### 5.0.5 July 6, 2020 + - Fixed #1285: packaging issue with bundling tests via poetry. + ### 5.0.4 July 6, 2020 - Fixed #1264: a regression with comment handling and `force_sort_within_sections` config option diff --git a/pyproject.toml b/pyproject.toml index 048c1458b..6eec956c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,13 +31,6 @@ classifiers = [ "Topic :: Utilities", ] urls = { Changelog = "https://github.com/timothycrosley/isort/blob/master/CHANGELOG.md" } -include = [ - "CHANGELOG.md", - "docs/configuration/*.md", - "docs/contributing/*.md", - "docs/major_releases/*.md", - "docs/quick_start/*.md", -] [tool.poetry.dependencies] python = "^3.6" From b228c373ab3bc5c571ba0a616f13cac8f03ca99f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 6 Jul 2020 21:36:50 -0700 Subject: [PATCH 0720/1439] Fixed #1284: Regression when sorting files from CLI using black profile. --- CHANGELOG.md | 2 +- isort/api.py | 34 +++++++++++++++++++++------------- tests/test_io.py | 4 +++- tests/test_isort.py | 7 +++++++ 4 files changed, 32 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 966226ac4..ef948d955 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. ### 5.0.5 July 6, 2020 - Fixed #1285: packaging issue with bundling tests via poetry. - + - Fixed #1284: Regression when sorting `.pyi` files from CLI using black profile. ### 5.0.4 July 6, 2020 - Fixed #1264: a regression with comment handling and `force_sort_within_sections` config option diff --git a/isort/api.py b/isort/api.py index e1d4a7d49..5d1e572f1 100644 --- a/isort/api.py +++ b/isort/api.py @@ -32,7 +32,7 @@ def sort_code_string( code: str, - extension: str = "py", + extension: Optional[str] = None, config: Config = DEFAULT_CONFIG, file_path: Optional[Path] = None, disregard_skip: bool = False, @@ -41,7 +41,7 @@ def sort_code_string( """Sorts any imports within the provided code string, returning a new string with them sorted. - **code**: The string of code with imports that need to be sorted. - - **extension**: The file extension that contains the code. + - **extension**: The file extension that contains imports. Defaults to filename extension or py. - **config**: The config object to use when sorting imports. - **file_path**: The disk location where the code string was pulled from. - **disregard_skip**: set to `True` if you want to ignore a skip set in config for this file. @@ -65,7 +65,7 @@ def sort_code_string( def check_code_string( code: str, show_diff: bool = False, - extension: str = "py", + extension: Optional[str] = None, config: Config = DEFAULT_CONFIG, file_path: Optional[Path] = None, disregard_skip: bool = False, @@ -76,7 +76,7 @@ def check_code_string( - **code**: The string of code with imports that need to be sorted. - **show_diff**: If `True` the changes that need to be done will be printed to stdout. - - **extension**: The file extension that contains the code. + - **extension**: The file extension that contains imports. Defaults to filename extension or py. - **config**: The config object to use when sorting imports. - **file_path**: The disk location where the code string was pulled from. - **disregard_skip**: set to `True` if you want to ignore a skip set in config for this file. @@ -96,7 +96,7 @@ def check_code_string( def sort_stream( input_stream: TextIO, output_stream: TextIO, - extension: str = "py", + extension: Optional[str] = None, config: Config = DEFAULT_CONFIG, file_path: Optional[Path] = None, disregard_skip: bool = False, @@ -107,7 +107,7 @@ def sort_stream( - **input_stream**: The stream of code with imports that need to be sorted. - **output_stream**: The stream where sorted imports should be written to. - - **extension**: The file extension that contains the code. + - **extension**: The file extension that contains imports. Defaults to filename extension or py. - **config**: The config object to use when sorting imports. - **file_path**: The disk location where the code string was pulled from. - **disregard_skip**: set to `True` if you want to ignore a skip set in config for this file. @@ -133,7 +133,12 @@ def sort_stream( _internal_output = StringIO() try: - changed = _sort_imports(input_stream, _internal_output, extension=extension, config=config) + changed = _sort_imports( + input_stream, + _internal_output, + extension=extension or (file_path and file_path.suffix.lstrip(".")) or "py", + config=config, + ) except FileSkipComment: raise FileSkipComment(content_source) @@ -153,7 +158,7 @@ def sort_stream( def check_stream( input_stream: TextIO, show_diff: bool = False, - extension: str = "py", + extension: Optional[str] = None, config: Config = DEFAULT_CONFIG, file_path: Optional[Path] = None, disregard_skip: bool = False, @@ -164,7 +169,7 @@ def check_stream( - **input_stream**: The stream of code with imports that need to be sorted. - **show_diff**: If `True` the changes that need to be done will be printed to stdout. - - **extension**: The file extension that contains the code. + - **extension**: The file extension that contains imports. Defaults to filename extension or py. - **config**: The config object to use when sorting imports. - **file_path**: The disk location where the code string was pulled from. - **disregard_skip**: set to `True` if you want to ignore a skip set in config for this file. @@ -213,6 +218,7 @@ def check_file( config: Config = DEFAULT_CONFIG, file_path: Optional[Path] = None, disregard_skip: bool = True, + extension: Optional[str] = None, **config_kwargs, ) -> bool: """Checks any imports within the provided file, returning `False` if any unsorted or @@ -220,17 +226,17 @@ def check_file( - **filename**: The name or Path of the file to check. - **show_diff**: If `True` the changes that need to be done will be printed to stdout. - - **extension**: The file extension that contains the code. - **config**: The config object to use when sorting imports. - **file_path**: The disk location where the code string was pulled from. - **disregard_skip**: set to `True` if you want to ignore a skip set in config for this file. + - **extension**: The file extension that contains imports. Defaults to filename extension or py. - ****config_kwargs**: Any config modifications. """ with io.File.read(filename) as source_file: return check_stream( source_file.stream, show_diff=show_diff, - extension=source_file.extension or "py", + extension=extension, config=config, file_path=file_path or source_file.path, disregard_skip=disregard_skip, @@ -240,7 +246,7 @@ def check_file( def sort_file( filename: Union[str, Path], - extension: str = "py", + extension: Optional[str] = None, config: Config = DEFAULT_CONFIG, file_path: Optional[Path] = None, disregard_skip: bool = True, @@ -252,7 +258,7 @@ def sort_file( """Sorts and formats any groups of imports imports within the provided file or Path. - **filename**: The name or Path of the file to format. - - **extension**: The file extension that contains the code. + - **extension**: The file extension that contains imports. Defaults to filename extension or py. - **config**: The config object to use when sorting imports. - **file_path**: The disk location where the code string was pulled from. - **disregard_skip**: set to `True` if you want to ignore a skip set in config for this file. @@ -271,6 +277,7 @@ def sort_file( config=config, file_path=file_path or source_file.path, disregard_skip=disregard_skip, + extension=extension, **config_kwargs, ) else: @@ -286,6 +293,7 @@ def sort_file( config=config, file_path=file_path or source_file.path, disregard_skip=disregard_skip, + extension=extension, **config_kwargs, ) if changed: diff --git a/tests/test_io.py b/tests/test_io.py index 59a86faca..a4479c860 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -22,7 +22,9 @@ def test_read(self, tmpdir): def test_from_content(self, tmpdir): test_file = tmpdir.join("file.py") test_file.write_text("import os", "utf8") - assert io.File.from_contents("import os", filename=str(test_file)) + file_obj = io.File.from_contents("import os", filename=str(test_file)) + assert file_obj + assert file_obj.extension == "py" def test_open(self, tmpdir): with pytest.raises(Exception): diff --git a/tests/test_isort.py b/tests/test_isort.py index 94fea3d70..4616ffa3a 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -4112,6 +4112,13 @@ def test_pyi_formatting_issue_942(tmpdir) -> None: == expected_pyi_output ) + # Ensure it works for direct file API as well (see: issue #1284) + source_pyi = tmpdir.join("source.pyi") + source_pyi.write(test_input) + api.sort_file(Path(source_pyi)) + + assert source_pyi.read().splitlines() == expected_pyi_output + def test_move_class_issue_751() -> None: test_input = ( From 9bfbdf27c453e32c3a2d208a05702cdfd4d9546d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 6 Jul 2020 22:11:29 -0700 Subject: [PATCH 0721/1439] Fix #1275: Blank line after docstring removed. --- CHANGELOG.md | 1 + isort/api.py | 7 +++++++ isort/parse.py | 2 +- tests/test_isort.py | 1 + tests/test_main.py | 3 ++- tests/test_regressions.py | 24 ++++++++++++++++++++++++ 6 files changed, 36 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef948d955..6eab31241 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. ### 5.0.5 July 6, 2020 - Fixed #1285: packaging issue with bundling tests via poetry. - Fixed #1284: Regression when sorting `.pyi` files from CLI using black profile. + - Fixed #1275: Blank line after docstring removed. ### 5.0.4 July 6, 2020 - Fixed #1264: a regression with comment handling and `force_sort_within_sections` config option diff --git a/isort/api.py b/isort/api.py index 5d1e572f1..ace6a2574 100644 --- a/isort/api.py +++ b/isort/api.py @@ -450,6 +450,13 @@ def _sort_imports( isort_off = True elif stripped_line == "# isort: split": not_imports = True + elif not (stripped_line or contains_imports): + if add_imports and not indent: + import_section += line_separator.join(add_imports) + line_separator + contains_imports = True + add_imports = [] + else: + not_imports = True elif ( not stripped_line or stripped_line.startswith("#") diff --git a/isort/parse.py b/isort/parse.py index 40a0c75a0..60d5ee4e4 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -363,7 +363,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte last = out_lines[-1].rstrip() else: last = "" - if statement_index - 1 == import_index: + if statement_index - 1 == import_index: # pragma: no cover import_index -= len( categorized_comments["above"]["from"].get(import_from, []) ) diff --git a/tests/test_isort.py b/tests/test_isort.py index 4616ffa3a..103663b53 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -4682,6 +4682,7 @@ def test_noqa_issue_1065() -> None: # # USER SIGNALS # + from flask_login import user_logged_in, user_logged_out # noqa from flask_principal import identity_changed as user_identity_changed # noqa from flask_security.signals import password_changed as user_reset_password # noqa diff --git a/tests/test_main.py b/tests/test_main.py index 986b81987..ebfa81cdf 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -152,7 +152,8 @@ def test_main(capsys, tmpdir): out, error = capsys.readouterr() assert ( out - == f"""else-type place_module for b returned {DEFAULT_CONFIG.default_section} + == f""" +else-type place_module for b returned {DEFAULT_CONFIG.default_section} else-type place_module for a returned {DEFAULT_CONFIG.default_section} import a import b diff --git a/tests/test_regressions.py b/tests/test_regressions.py index eb141663e..8da338e50 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -36,3 +36,27 @@ def test_moving_comments_issue_726(): "from Plaid.models import PlaidModel\n" ) assert isort.code(test_input, force_sort_within_sections=True) == test_input + + +def test_blank_lined_removed_issue_1275(): + """Ensure isort doesn't accidentally remove blank lines after doc strings and before imports. + See: https://github.com/timothycrosley/isort/issues/1275 + """ + assert ( + isort.code( + '''""" +My docstring +""" + +from b import thing +from a import other_thing +''' + ) + == '''""" +My docstring +""" + +from a import other_thing +from b import thing +''' + ) From 1a7b1a81a0c907539e27db2ceb89522784774e5c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 6 Jul 2020 23:10:35 -0700 Subject: [PATCH 0722/1439] Fix issue #1283: Blank line after __version__ removed. --- CHANGELOG.md | 2 +- tests/test_regressions.py | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6eab31241..775935491 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. ### 5.0.5 July 6, 2020 - Fixed #1285: packaging issue with bundling tests via poetry. - Fixed #1284: Regression when sorting `.pyi` files from CLI using black profile. - - Fixed #1275: Blank line after docstring removed. + - Fixed #1275 & #1283: Blank line after docstring removed. ### 5.0.4 July 6, 2020 - Fixed #1264: a regression with comment handling and `force_sort_within_sections` config option diff --git a/tests/test_regressions.py b/tests/test_regressions.py index 8da338e50..463448d6e 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -60,3 +60,13 @@ def test_blank_lined_removed_issue_1275(): from b import thing ''' ) + +def test_blank_lined_removed_issue_1283(): + """Ensure isort doesn't accidentally remove blank lines after __version__ identifiers. + See: https://github.com/timothycrosley/isort/issues/1283 + """ + test_input = """__version__ = "0.58.1" + +from starlette import status +""" + assert isort.code(test_input) == test_input From bff46f3e8a0bedbe2792132844f1e2089f237896 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 7 Jul 2020 20:27:16 -0700 Subject: [PATCH 0723/1439] Fix issue #1298: CLI help out of date --- CHANGELOG.md | 3 ++- isort/main.py | 10 +++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 775935491..251f11e14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,11 @@ Changelog ========= NOTE: isort follows the [semver](https://semver.org/) versioning standard. -### 5.0.5 July 6, 2020 +### 5.0.5 July 7, 2020 - Fixed #1285: packaging issue with bundling tests via poetry. - Fixed #1284: Regression when sorting `.pyi` files from CLI using black profile. - Fixed #1275 & #1283: Blank line after docstring removed. + - Fixed #1298: CLI Help out of date with isort 5. ### 5.0.4 July 6, 2020 - Fixed #1264: a regression with comment handling and `force_sort_within_sections` config option diff --git a/isort/main.py b/isort/main.py index 2e1b68bb3..40fc57d42 100644 --- a/isort/main.py +++ b/isort/main.py @@ -129,9 +129,13 @@ def iter_source_code(paths: Iterable[str], config: Config, skipped: List[str]) - def _build_arg_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( description="Sort Python import definitions alphabetically " - "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." + "within logical sections. Run with no arguments to see a quick " + "start guide, otherwise, one or more files/directories/stdin must be provided. " + "Use `-` as the first argument to represent stdin. Use --interactive to use the pre 5.0.0 " + "interactive behavior." + "" + "If you've used isort 4 but are new to isort 5, see the upgrading guide:" + "https://timothycrosley.github.io/isort/docs/upgrade_guides/5.0.0/." ) inline_args_group = parser.add_mutually_exclusive_group() parser.add_argument( From dbd5e3fdae3cb8df1ccde325ed9e219aacf86294 Mon Sep 17 00:00:00 2001 From: hyeonjames Date: Mon, 6 Jul 2020 20:50:15 +0900 Subject: [PATCH 0724/1439] Issue #1276: Stop parsing in quotes --- isort/output.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/isort/output.py b/isort/output.py index cf6e49684..febf0e698 100644 --- a/isort/output.py +++ b/isort/output.py @@ -178,10 +178,9 @@ def sorted_imports( tail = formatted_output[imports_tail:] for index, line in enumerate(tail): - in_quote = _in_quote - should_skip, _in_quote, *_ = parse.skip_line( + should_skip, in_quote, *_ = parse.skip_line( line, - in_quote=_in_quote, + in_quote='', index=len(formatted_output), section_comments=parsed.section_comments, ) @@ -194,7 +193,10 @@ def sorted_imports( continue next_construct = line break - elif not in_quote: + elif in_quote: + next_construct = line + break + else: parts = line.split() if ( len(parts) >= 3 From a35e464ea451ffe5a959ae2d6a24eaecbd7d81bd Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 7 Jul 2020 20:54:00 -0700 Subject: [PATCH 0725/1439] Fixed #1290: Unecessary blank lines above nested imports when import comments turned on. --- CHANGELOG.md | 1 + isort/api.py | 49 ++++++++++++++++++++------------------- tests/test_regressions.py | 33 ++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 251f11e14..c84efd062 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. - Fixed #1284: Regression when sorting `.pyi` files from CLI using black profile. - Fixed #1275 & #1283: Blank line after docstring removed. - Fixed #1298: CLI Help out of date with isort 5. + - Fixed #1290: Unecessary blank lines above nested imports when import comments turned on. ### 5.0.4 July 6, 2020 - Fixed #1264: a regression with comment handling and `force_sort_within_sections` config option diff --git a/isort/api.py b/isort/api.py index ace6a2574..24e82658b 100644 --- a/isort/api.py +++ b/isort/api.py @@ -560,31 +560,32 @@ def _sort_imports( extension, import_type="cimport" if cimports else "import", ) - if indent: - sorted_import_section = ( - leading_whitespace - + textwrap.indent(sorted_import_section, indent).strip() - + trailing_whitespace - ) - - if not made_changes: - if config.ignore_whitespace: - compare_in = remove_whitespace( - raw_import_section, line_separator=line_separator - ).strip() - compare_out = remove_whitespace( - sorted_import_section, line_separator=line_separator - ).strip() - else: - compare_in = raw_import_section.strip() - compare_out = sorted_import_section.strip() - - if compare_out != compare_in: - made_changes = True + if not (import_section.strip() and not sorted_import_section): + if indent: + sorted_import_section = ( + leading_whitespace + + textwrap.indent(sorted_import_section, indent).strip() + + trailing_whitespace + ) - output_stream.write(sorted_import_section) - if not line and not indent and next_import_section: - output_stream.write(line_separator) + if not made_changes: + if config.ignore_whitespace: + compare_in = remove_whitespace( + raw_import_section, line_separator=line_separator + ).strip() + compare_out = remove_whitespace( + sorted_import_section, line_separator=line_separator + ).strip() + else: + compare_in = raw_import_section.strip() + compare_out = sorted_import_section.strip() + + if compare_out != compare_in: + made_changes = True + + output_stream.write(sorted_import_section) + if not line and not indent and next_import_section: + output_stream.write(line_separator) if indent: output_stream.write(line) diff --git a/tests/test_regressions.py b/tests/test_regressions.py index 463448d6e..393aa63de 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -61,6 +61,7 @@ def test_blank_lined_removed_issue_1275(): ''' ) + def test_blank_lined_removed_issue_1283(): """Ensure isort doesn't accidentally remove blank lines after __version__ identifiers. See: https://github.com/timothycrosley/isort/issues/1283 @@ -70,3 +71,35 @@ def test_blank_lined_removed_issue_1283(): from starlette import status """ assert isort.code(test_input) == test_input + + +def test_extra_blank_line_added_nested_imports_issue_1290(): + """Ensure isort doesn't added unecessary blank lines above nested imports. + See: https://github.com/timothycrosley/isort/issues/1290 + """ + test_input = '''from typing import TYPE_CHECKING + +# Special imports +from special import thing + +if TYPE_CHECKING: + # Special imports + from special import another_thing + + +def func(): + """Docstring""" + + # Special imports + from special import something_else + return +''' + assert ( + isort.code( + test_input, + import_heading_special="Special imports", + known_special=["special"], + sections=["FUTURE", "STDLIB", "THIRDPARTY", "SPECIAL", "FIRSTPARTY", "LOCALFOLDER"], + ) + == test_input + ) From 9ce5f74c4b409360923c05bd0a37322fafcec1c6 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 7 Jul 2020 21:09:02 -0700 Subject: [PATCH 0726/1439] Fixed #1297: Usage of alongside is broken. --- CHANGELOG.md | 1 + isort/api.py | 6 ++++-- tests/test_regressions.py | 11 +++++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c84efd062..86886b098 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. - Fixed #1275 & #1283: Blank line after docstring removed. - Fixed #1298: CLI Help out of date with isort 5. - Fixed #1290: Unecessary blank lines above nested imports when import comments turned on. + - Fixed #1297: Usage of `--add-imports` alongside `--check` is broken. ### 5.0.4 July 6, 2020 - Fixed #1264: a regression with comment handling and `force_sort_within_sections` config option diff --git a/isort/api.py b/isort/api.py index 24e82658b..350ce196a 100644 --- a/isort/api.py +++ b/isort/api.py @@ -509,6 +509,7 @@ def _sort_imports( not_imports = True if not_imports: + raw_import_section: str = import_section if ( add_imports and not in_top_comment @@ -521,7 +522,7 @@ def _sort_imports( add_imports = [] if next_import_section and not import_section: # pragma: no cover - import_section = next_import_section + raw_import_section = import_section = next_import_section next_import_section = "" if import_section: @@ -532,6 +533,7 @@ def _sort_imports( if not indent: import_section += line + raw_import_section += line if not contains_imports: output_stream.write(import_section) else: @@ -541,9 +543,9 @@ def _sort_imports( line_separator ).startswith(COMMENT_INDICATORS): import_section = import_section.lstrip(line_separator) + raw_import_section = raw_import_section.lstrip(line_separator) first_import_section = False - raw_import_section: str = import_section if indent: import_section = line_separator.join( line.lstrip() for line in import_section.split(line_separator) diff --git a/tests/test_regressions.py b/tests/test_regressions.py index 393aa63de..bf5cfb367 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -103,3 +103,14 @@ def func(): ) == test_input ) + + +def test_add_imports_shouldnt_make_isort_unusable_issue_1297(): + """Test to ensure add imports doesn't cause any unexpected behaviour when combined with check""" + assert isort.check_code( + """from __future__ import unicode_literals + +from os import path +""", + add_imports={"from __future__ import unicode_literals"}, + ) From c0ebbd0ff097abafd1f0060a5021b553efebc192 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 7 Jul 2020 21:13:56 -0700 Subject: [PATCH 0727/1439] Fixed #1289: Stream usage no longer auto picking up config file from current working directory. --- CHANGELOG.md | 1 + isort/main.py | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86886b098..8476aafc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. - Fixed #1298: CLI Help out of date with isort 5. - Fixed #1290: Unecessary blank lines above nested imports when import comments turned on. - Fixed #1297: Usage of `--add-imports` alongside `--check` is broken. + - Fixed #1289: Stream usage no longer auto picking up config file from current working directory. ### 5.0.4 July 6, 2020 - Fixed #1264: a regression with comment handling and `force_sort_within_sections` config option diff --git a/isort/main.py b/isort/main.py index 40fc57d42..15b9ccece 100644 --- a/isort/main.py +++ b/isort/main.py @@ -654,6 +654,7 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = print(QUICK_GUIDE) return elif file_names == ["-"] and not show_config: + arguments.setdefault("settings_path", os.getcwd()) api.sort_stream( input_stream=sys.stdin if stdin is None else stdin, output_stream=sys.stdout, From 2c9bd718cb492a673911d0c3a334516f0babdeef Mon Sep 17 00:00:00 2001 From: hyeonjames Date: Wed, 8 Jul 2020 13:16:22 +0900 Subject: [PATCH 0728/1439] Add tests for inquotes --- tests/test_isort.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_isort.py b/tests/test_isort.py index 94fea3d70..4cbef5194 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -119,6 +119,12 @@ def test_correct_space_between_imports() -> None: test_output_other = api.sort_code_string(test_input_other) assert test_output_other == "import sys\n\nprint('yo')\n" + test_input_inquotes = ( + "import sys\n" "@my_decorator('''hello\nworld''')\n" "def my_method():\n" " print('hello world')\n" + ) + test_output_inquotes = api.sort_code_string(test_input_inquotes) + assert test_output_inquotes == "import sys\n" "\n\n" "@my_decorator('''hello\nworld''')\n" "def my_method():\n" " print('hello world')\n" + def test_sort_on_number() -> None: """Ensure numbers get sorted logically (10 > 9 not the other way around)""" From 4eb73527fa94cf8c13bd80da2ed72de714c5402a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 7 Jul 2020 21:19:47 -0700 Subject: [PATCH 0729/1439] Add regression test for issue #1277 --- tests/test_regressions.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/test_regressions.py b/tests/test_regressions.py index bf5cfb367..82d81ab5a 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -106,7 +106,9 @@ def func(): def test_add_imports_shouldnt_make_isort_unusable_issue_1297(): - """Test to ensure add imports doesn't cause any unexpected behaviour when combined with check""" + """Test to ensure add imports doesn't cause any unexpected behaviour when combined with check + See: https://github.com/timothycrosley/isort/issues/1297 + """ assert isort.check_code( """from __future__ import unicode_literals @@ -114,3 +116,21 @@ def test_add_imports_shouldnt_make_isort_unusable_issue_1297(): """, add_imports={"from __future__ import unicode_literals"}, ) + + +def test_no_extra_lines_for_imports_in_functions_issue_1277(): + """Test to ensure isort doesn't introduce extra blank lines for imports within function. + See: https://github.com/timothycrosley/isort/issues/1277 + """ + test_input = """ +def main(): + import time + + import sys +""" + expected_output = """ +def main(): + import sys + import time +""" + assert isort.code(isort.code(isort.code(test_input))) == expected_output From d33e6428966e0c8c29cbbf225fa18f748e7bd9bd Mon Sep 17 00:00:00 2001 From: hyeonjames Date: Wed, 8 Jul 2020 13:20:20 +0900 Subject: [PATCH 0730/1439] Fix lint error --- isort/output.py | 2 +- tests/test_isort.py | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/isort/output.py b/isort/output.py index febf0e698..ec298d352 100644 --- a/isort/output.py +++ b/isort/output.py @@ -180,7 +180,7 @@ def sorted_imports( for index, line in enumerate(tail): should_skip, in_quote, *_ = parse.skip_line( line, - in_quote='', + in_quote="", index=len(formatted_output), section_comments=parsed.section_comments, ) diff --git a/tests/test_isort.py b/tests/test_isort.py index 4cbef5194..c5fe0b8b9 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -120,10 +120,19 @@ def test_correct_space_between_imports() -> None: assert test_output_other == "import sys\n\nprint('yo')\n" test_input_inquotes = ( - "import sys\n" "@my_decorator('''hello\nworld''')\n" "def my_method():\n" " print('hello world')\n" + "import sys\n" + "@my_decorator('''hello\nworld''')\n" + "def my_method():\n" + " print('hello world')\n" ) test_output_inquotes = api.sort_code_string(test_input_inquotes) - assert test_output_inquotes == "import sys\n" "\n\n" "@my_decorator('''hello\nworld''')\n" "def my_method():\n" " print('hello world')\n" + assert ( + test_output_inquotes == "import sys\n" + "\n\n" + "@my_decorator('''hello\nworld''')\n" + "def my_method():\n" + " print('hello world')\n" + ) def test_sort_on_number() -> None: From 1685beb09c0f6ec5fb0afe9c41dcd4b6e7c2b571 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 7 Jul 2020 21:22:57 -0700 Subject: [PATCH 0731/1439] Add test for issue #1293 --- tests/test_regressions.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/test_regressions.py b/tests/test_regressions.py index 82d81ab5a..414fa43b2 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -134,3 +134,20 @@ def main(): import time """ assert isort.code(isort.code(isort.code(test_input))) == expected_output + + +def test_no_extra_blank_lines_in_methods_issue_1293(): + """Test to ensure isort isn't introducing extra lines in methods that contain imports + See: https://github.com/timothycrosley/isort/issues/1293 + """ + test_input = """ + +class Something(object): + def on_email_deleted(self, email): + from hyperkitty.tasks import rebuild_thread_cache_new_email + + # update or cleanup thread # noqa: E303 (isort issue) + if self.emails.count() == 0: + ... +""" + assert isort.code(test_input) == test_input From 68affca268bab662e4aa4b4bf6678209239bc961 Mon Sep 17 00:00:00 2001 From: hyeonjames Date: Wed, 8 Jul 2020 13:24:37 +0900 Subject: [PATCH 0732/1439] Remove unnecessary _in_quote --- isort/output.py | 1 - 1 file changed, 1 deletion(-) diff --git a/isort/output.py b/isort/output.py index ec298d352..cd7bbff43 100644 --- a/isort/output.py +++ b/isort/output.py @@ -174,7 +174,6 @@ def sorted_imports( if len(formatted_output) > imports_tail: next_construct = "" - _in_quote: str = "" tail = formatted_output[imports_tail:] for index, line in enumerate(tail): From a009c8d8729560e56bb1b0c9486690a09e68cfc9 Mon Sep 17 00:00:00 2001 From: hyeonjames Date: Wed, 8 Jul 2020 14:31:00 +0900 Subject: [PATCH 0733/1439] Remove blocks checking variable assignment --- isort/output.py | 10 ---------- tests/test_isort.py | 3 +++ 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/isort/output.py b/isort/output.py index cd7bbff43..de4c0d200 100644 --- a/isort/output.py +++ b/isort/output.py @@ -195,16 +195,6 @@ def sorted_imports( elif in_quote: next_construct = line break - else: - parts = line.split() - if ( - len(parts) >= 3 - and parts[1] == "=" - and "'" not in parts[0] - and '"' not in parts[0] - ): - next_construct = line - break if config.lines_after_imports != -1: formatted_output[imports_tail:0] = ["" for line in range(config.lines_after_imports)] diff --git a/tests/test_isort.py b/tests/test_isort.py index c5fe0b8b9..c00d817ad 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -133,6 +133,9 @@ def test_correct_space_between_imports() -> None: "def my_method():\n" " print('hello world')\n" ) + test_input_assign = "import sys\nVAR = 1\n" + test_output_assign = api.sort_code_string(test_input_assign) + assert test_output_assign == "import sys\n\nVAR = 1\n" def test_sort_on_number() -> None: From 65e0a3f27c452dba6125062aea1e9b170a1c7366 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 7 Jul 2020 23:14:40 -0700 Subject: [PATCH 0734/1439] Fixed #1296: Force_single_line setting removes immediately following comment line. --- CHANGELOG.md | 1 + isort/output.py | 17 ++++++----------- tests/test_regressions.py | 15 +++++++++++++++ 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8476aafc1..9dcbab2ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. - Fixed #1290: Unecessary blank lines above nested imports when import comments turned on. - Fixed #1297: Usage of `--add-imports` alongside `--check` is broken. - Fixed #1289: Stream usage no longer auto picking up config file from current working directory. + - Fixed #1296: Force_single_line setting removes immediately following comment line. ### 5.0.4 July 6, 2020 - Fixed #1264: a regression with comment handling and `force_sort_within_sections` config option diff --git a/isort/output.py b/isort/output.py index cf6e49684..95aa9ddd5 100644 --- a/isort/output.py +++ b/isort/output.py @@ -281,6 +281,12 @@ def _with_from_imports( while from_imports: comments = parsed.categorized_comments["from"].pop(module, ()) + above_comments = parsed.categorized_comments["above"]["from"].pop(module, None) + if above_comments: + if new_section_output and config.ensure_newline_before_comments: + new_section_output.append("") + new_section_output.extend(above_comments) + if "*" in from_imports and config.combine_star: import_statement = wrap.line( with_comments( @@ -336,12 +342,6 @@ def _with_from_imports( ) comments = None else: - above_comments = parsed.categorized_comments["above"]["from"].pop(module, None) - if above_comments: - if new_section_output and config.ensure_newline_before_comments: - new_section_output.append("") - new_section_output.extend(above_comments) - while from_imports and from_imports[0] in as_imports: from_import = from_imports.pop(0) as_imports[from_import] = sorting.naturally(as_imports[from_import]) @@ -472,11 +472,6 @@ def _with_from_imports( import_statement = wrap.line(import_statement, parsed.line_separator, config) if import_statement: - above_comments = parsed.categorized_comments["above"]["from"].pop(module, None) - if above_comments: # pragma: no cover - if new_section_output and config.ensure_newline_before_comments: - new_section_output.append("") - new_section_output.extend(above_comments) new_section_output.append(import_statement) return new_section_output diff --git a/tests/test_regressions.py b/tests/test_regressions.py index 414fa43b2..70ef61962 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -151,3 +151,18 @@ def on_email_deleted(self, email): ... """ assert isort.code(test_input) == test_input + + +def test_force_single_line_shouldnt_remove_preceding_comment_lines_issue_1296(): + """Tests to ensure force_single_line setting doesn't result in lost comments. + See: https://github.com/timothycrosley/isort/issues/1296 + """ + test_input = """ +# A comment +# A comment + +# Oh no, I'm gone +from moo import foo +""" + # assert isort.code(test_input) == test_input + assert isort.code(test_input, force_single_line=True) == test_input From d4ce124277fbee306ef5f32289c39853e967cbe0 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 7 Jul 2020 23:27:22 -0700 Subject: [PATCH 0735/1439] Fixed #1295: doesnt work with . --- CHANGELOG.md | 1 + isort/output.py | 6 +++++- tests/test_regressions.py | 17 +++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dcbab2ba..f7051ed32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. - Fixed #1297: Usage of `--add-imports` alongside `--check` is broken. - Fixed #1289: Stream usage no longer auto picking up config file from current working directory. - Fixed #1296: Force_single_line setting removes immediately following comment line. + - Fixed #1295: `ensure_newline_before_comments` doesnt work with `force_sort_within_sections`. ### 5.0.4 July 6, 2020 - Fixed #1264: a regression with comment handling and `force_sort_within_sections` config option diff --git a/isort/output.py b/isort/output.py index 95aa9ddd5..9e41f64c5 100644 --- a/isort/output.py +++ b/isort/output.py @@ -130,7 +130,11 @@ def sorted_imports( # uncollapse comments section_output = [] for line in new_section_output: - section_output.extend(getattr(line, "comments", ())) + comments = getattr(line, "comments", ()) + if comments: + if new_section_output and config.ensure_newline_before_comments: + section_output.append("") + section_output.extend(comments) section_output.append(str(line)) section_name = section diff --git a/tests/test_regressions.py b/tests/test_regressions.py index 70ef61962..69494f307 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -166,3 +166,20 @@ def test_force_single_line_shouldnt_remove_preceding_comment_lines_issue_1296(): """ # assert isort.code(test_input) == test_input assert isort.code(test_input, force_single_line=True) == test_input + + +def test_ensure_new_line_before_comments_mixed_with_ensure_newline_before_comments_1295(): + """Tests to ensure that the black profile can be used in conjunction with + force_sort_within_sections. + + See: https://github.com/timothycrosley/isort/issues/1295 + """ + test_input = """ +from openzwave.group import ZWaveGroup +from openzwave.network import ZWaveNetwork + +# pylint: disable=import-error +from openzwave.option import ZWaveOption +""" + assert isort.code(test_input, profile="black") == test_input + assert isort.code(test_input, profile="black", force_sort_within_sections=True) == test_input From 078f3718dceec8c03a7cb80f1aff3144737cb29e Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 7 Jul 2020 23:46:06 -0700 Subject: [PATCH 0736/1439] Warn instead of failing on not_skip setting --- CHANGELOG.md | 2 + isort/main.py | 2 +- isort/settings.py | 12 + tests/test_isort.py | 752 +++++++++++++++++++++----------------------- 4 files changed, 367 insertions(+), 401 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7051ed32..ebfa905ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. - Fixed #1289: Stream usage no longer auto picking up config file from current working directory. - Fixed #1296: Force_single_line setting removes immediately following comment line. - Fixed #1295: `ensure_newline_before_comments` doesnt work with `force_sort_within_sections`. + - Setting not_skip will no longer immediately fail but instead give user a warning and direct + to upgrade docs. ### 5.0.4 July 6, 2020 - Fixed #1264: a regression with comment handling and `force_sort_within_sections` config option diff --git a/isort/main.py b/isort/main.py index 15b9ccece..116911bf1 100644 --- a/isort/main.py +++ b/isort/main.py @@ -680,7 +680,7 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = if deprecated_flags: # pragma: no cover warn( - f"\n\nThe following deprecated CLI flags where used: {', '.join(deprecated_flags)}!\n" + f"\n\nThe following deprecated CLI flags were used: {', '.join(deprecated_flags)}!\n" "Please see the 5.0.0 upgrade guide:\n" "\thttps://timothycrosley.github.io/isort/docs/upgrade_guides/5.0.0/\n" ) diff --git a/isort/settings.py b/isort/settings.py index 365cf92bf..5cefd4087 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -62,6 +62,8 @@ RUNTIME_SOURCE = "runtime" +DEPRECATED_SETTINGS = ("not_skip",) + @dataclass(frozen=True) class _Config: @@ -296,6 +298,16 @@ def __init__( combined_config.pop("source", None) combined_config.pop("sources", None) combined_config.pop("runtime_src_paths", None) + + for deprecated_option in DEPRECATED_SETTINGS: + if deprecated_option in combined_config: + warn( + f"\n\nThe following deprecated settings was used: {deprecated_option}!\n" + "Please see the 5.0.0 upgrade guide:\n" + "\thttps://timothycrosley.github.io/isort/docs/upgrade_guides/5.0.0/\n" + ) + combined_config.pop(deprecated_option) + if known_other: for known_key in known_other: combined_config.pop(f"{KNOWN_PREFIX}{known_key}", None) diff --git a/tests/test_isort.py b/tests/test_isort.py index 103663b53..a96af15ad 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -13,6 +13,7 @@ import py import pytest +import isort from isort import main, api, sections from isort.main import is_python_file from isort.settings import WrapModes, Config @@ -63,7 +64,7 @@ def default_settings_path(tmpdir_factory) -> Iterator[str]: def test_happy_path() -> None: """Test the most basic use case, straight imports no code, simply not organized by category.""" test_input = "import sys\nimport os\nimport myproject.test\nimport django.settings" - test_output = api.sort_code_string(test_input, known_first_party=["myproject"]) + test_output = isort.code(test_input, known_first_party=["myproject"]) assert test_output == ( "import os\n" "import sys\n" "\n" "import django.settings\n" "\n" "import myproject.test\n" ) @@ -82,7 +83,7 @@ def test_code_intermixed() -> None: "print('I like to put code between imports cause I want stuff to break')\n" "import myproject.test\n" ) - test_output = api.sort_code_string(test_input) + test_output = isort.code(test_input) assert test_output == ( "import sys\n" "\n" @@ -100,39 +101,39 @@ def test_correct_space_between_imports() -> None: """ test_input_method = "import sys\ndef my_method():\n print('hello world')\n" - test_output_method = api.sort_code_string(test_input_method) + test_output_method = isort.code(test_input_method) assert test_output_method == ("import sys\n\n\ndef my_method():\n print('hello world')\n") test_input_decorator = ( "import sys\n" "@my_decorator\n" "def my_method():\n" " print('hello world')\n" ) - test_output_decorator = api.sort_code_string(test_input_decorator) + test_output_decorator = isort.code(test_input_decorator) assert test_output_decorator == ( "import sys\n" "\n" "\n" "@my_decorator\n" "def my_method():\n" " print('hello world')\n" ) test_input_class = "import sys\nclass MyClass(object):\n pass\n" - test_output_class = api.sort_code_string(test_input_class) + test_output_class = isort.code(test_input_class) assert test_output_class == "import sys\n\n\nclass MyClass(object):\n pass\n" test_input_other = "import sys\nprint('yo')\n" - test_output_other = api.sort_code_string(test_input_other) + test_output_other = isort.code(test_input_other) assert test_output_other == "import sys\n\nprint('yo')\n" def test_sort_on_number() -> None: """Ensure numbers get sorted logically (10 > 9 not the other way around)""" test_input = "import lib10\nimport lib9\n" - test_output = api.sort_code_string(test_input) + test_output = isort.code(test_input) assert test_output == "import lib9\nimport lib10\n" def test_line_length() -> None: """Ensure isort enforces the set line_length.""" - assert len(api.sort_code_string(REALLY_LONG_IMPORT, line_length=80).split("\n")[0]) <= 80 - assert len(api.sort_code_string(REALLY_LONG_IMPORT, line_length=120).split("\n")[0]) <= 120 + assert len(isort.code(REALLY_LONG_IMPORT, line_length=80).split("\n")[0]) <= 80 + assert len(isort.code(REALLY_LONG_IMPORT, line_length=120).split("\n")[0]) <= 120 - test_output = api.sort_code_string(REALLY_LONG_IMPORT, line_length=42) + test_output = isort.code(REALLY_LONG_IMPORT, line_length=42) assert test_output == ( "from third_party import (lib1, lib2, lib3,\n" " lib4, lib5, lib6,\n" @@ -152,7 +153,7 @@ def test_line_length() -> None: ")\n" ) # Test case described in issue #654 assert ( - api.sort_code_string( + isort.code( code=test_input, include_trailing_comma=True, line_length=79, @@ -162,7 +163,7 @@ def test_line_length() -> None: == test_input ) - test_output = api.sort_code_string(code=REALLY_LONG_IMPORT, line_length=42, wrap_length=32) + test_output = isort.code(code=REALLY_LONG_IMPORT, line_length=42, wrap_length=32) assert test_output == ( "from third_party import (lib1,\n" " lib2,\n" @@ -191,13 +192,11 @@ def test_line_length() -> None: "from .test import a_very_long_function_name_that_exceeds_the_normal_pep8_line_length\n" ) with pytest.raises(ValueError): - test_output = api.sort_code_string(code=REALLY_LONG_IMPORT, line_length=80, wrap_length=99) - test_output = ( - api.sort_code_string(REALLY_LONG_IMPORT, line_length=100, wrap_length=99) == test_input - ) + test_output = isort.code(code=REALLY_LONG_IMPORT, line_length=80, wrap_length=99) + test_output = isort.code(REALLY_LONG_IMPORT, line_length=100, wrap_length=99) == test_input # Test Case described in issue #1015 - test_output = api.sort_code_string( + test_output = isort.code( REALLY_LONG_IMPORT, line_length=25, multi_line_output=WrapModes.HANGING_INDENT ) assert test_output == ( @@ -216,7 +215,7 @@ def test_line_length() -> None: def test_output_modes() -> None: """Test setting isort to use various output modes works as expected""" - test_output_grid = api.sort_code_string( + test_output_grid = isort.code( code=REALLY_LONG_IMPORT, multi_line_output=WrapModes.GRID, line_length=40 ) assert test_output_grid == ( @@ -233,7 +232,7 @@ def test_output_modes() -> None: " lib22)\n" ) - test_output_vertical = api.sort_code_string( + test_output_vertical = isort.code( code=REALLY_LONG_IMPORT, multi_line_output=WrapModes.VERTICAL, line_length=40 ) assert test_output_vertical == ( @@ -260,7 +259,7 @@ def test_output_modes() -> None: " lib22)\n" ) - comment_output_vertical = api.sort_code_string( + comment_output_vertical = isort.code( code=REALLY_LONG_IMPORT_WITH_COMMENT, multi_line_output=WrapModes.VERTICAL, line_length=40 ) assert comment_output_vertical == ( @@ -287,7 +286,7 @@ def test_output_modes() -> None: " lib22)\n" ) - test_output_hanging_indent = api.sort_code_string( + test_output_hanging_indent = isort.code( code=REALLY_LONG_IMPORT, multi_line_output=WrapModes.HANGING_INDENT, line_length=40, @@ -301,7 +300,7 @@ def test_output_modes() -> None: " lib18, lib20, lib21, lib22\n" ) - comment_output_hanging_indent = api.sort_code_string( + comment_output_hanging_indent = isort.code( code=REALLY_LONG_IMPORT_WITH_COMMENT, multi_line_output=WrapModes.HANGING_INDENT, line_length=40, @@ -315,7 +314,7 @@ def test_output_modes() -> None: " lib17, lib18, lib20, lib21, lib22\n" ) - test_output_vertical_indent = api.sort_code_string( + test_output_vertical_indent = isort.code( code=REALLY_LONG_IMPORT, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, line_length=40, @@ -347,7 +346,7 @@ def test_output_modes() -> None: ")\n" ) - comment_output_vertical_indent = api.sort_code_string( + comment_output_vertical_indent = isort.code( code=REALLY_LONG_IMPORT_WITH_COMMENT, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, line_length=40, @@ -379,7 +378,7 @@ def test_output_modes() -> None: ")\n" ) - test_output_vertical_grid = api.sort_code_string( + test_output_vertical_grid = isort.code( code=REALLY_LONG_IMPORT, multi_line_output=WrapModes.VERTICAL_GRID, line_length=40, @@ -393,7 +392,7 @@ def test_output_modes() -> None: " lib17, lib18, lib20, lib21, lib22)\n" ) - comment_output_vertical_grid = api.sort_code_string( + comment_output_vertical_grid = isort.code( code=REALLY_LONG_IMPORT_WITH_COMMENT, multi_line_output=WrapModes.VERTICAL_GRID, line_length=40, @@ -407,7 +406,7 @@ def test_output_modes() -> None: " lib17, lib18, lib20, lib21, lib22)\n" ) - test_output_vertical_grid_grouped = api.sort_code_string( + test_output_vertical_grid_grouped = isort.code( code=REALLY_LONG_IMPORT, multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, line_length=40, @@ -422,7 +421,7 @@ def test_output_modes() -> None: ")\n" ) - comment_output_vertical_grid_grouped = api.sort_code_string( + comment_output_vertical_grid_grouped = isort.code( code=REALLY_LONG_IMPORT_WITH_COMMENT, multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, line_length=40, @@ -437,9 +436,7 @@ def test_output_modes() -> None: ")\n" ) - output_noqa = api.sort_code_string( - code=REALLY_LONG_IMPORT_WITH_COMMENT, multi_line_output=WrapModes.NOQA - ) + output_noqa = isort.code(code=REALLY_LONG_IMPORT_WITH_COMMENT, multi_line_output=WrapModes.NOQA) assert output_noqa == ( "from third_party import lib1, lib2, lib3, lib4, lib5, lib6, lib7," " lib8, lib9, lib10, lib11," @@ -447,7 +444,7 @@ def test_output_modes() -> None: "# NOQA comment\n" ) - test_case = api.sort_code_string( + test_case = isort.code( code=SINGLE_LINE_LONG_IMPORT, multi_line_output=WrapModes.VERTICAL_GRID_GROUPED_NO_COMMA, line_length=40, @@ -458,7 +455,7 @@ def test_output_modes() -> None: "from third_party import (\n lib1, lib2, lib3, lib4, lib5, lib5ab\n)\n" ) - test_output_prefix_from_module = api.sort_code_string( + test_output_prefix_from_module = isort.code( code=REALLY_LONG_IMPORT, multi_line_output=WrapModes.VERTICAL_PREFIX_FROM_MODULE_IMPORT, line_length=40, @@ -477,7 +474,7 @@ def test_output_modes() -> None: "from third_party import lib22\n" ) - test_output_prefix_from_module_with_comment = api.sort_code_string( + test_output_prefix_from_module_with_comment = isort.code( code=REALLY_LONG_IMPORT_WITH_COMMENT, multi_line_output=WrapModes.VERTICAL_PREFIX_FROM_MODULE_IMPORT, line_length=40, @@ -502,7 +499,7 @@ def test_output_modes() -> None: " from allennlp.modules.text_field_embedders.basic_text_field_embedder" " import BasicTextFieldEmbedder" ) - test_output = api.sort_code_string(test_input, line_length=100) + test_output = isort.code(test_input, line_length=100) assert test_output == ( "def a():\n" " from allennlp.modules.text_field_embedders.basic_text_field_embedder import \\\n" @@ -520,21 +517,17 @@ def test_output_modes() -> None: " from allennlp.common.registrable import Registrable" " # import here to avoid circular imports\n" ) - test_output = api.sort_code_string(test_input, line_length=100) + test_output = isort.code(test_input, line_length=100) assert test_output == test_input def test_qa_comment_case() -> None: test_input = "from veryveryveryveryveryveryveryveryveryveryvery import X # NOQA" - test_output = api.sort_code_string( - code=test_input, line_length=40, multi_line_output=WrapModes.NOQA - ) + test_output = isort.code(code=test_input, line_length=40, multi_line_output=WrapModes.NOQA) assert test_output == "from veryveryveryveryveryveryveryveryveryveryvery import X # NOQA\n" test_input = "import veryveryveryveryveryveryveryveryveryveryvery # NOQA" - test_output = api.sort_code_string( - code=test_input, line_length=40, multi_line_output=WrapModes.NOQA - ) + test_output = isort.code(code=test_input, line_length=40, multi_line_output=WrapModes.NOQA) assert test_output == "import veryveryveryveryveryveryveryveryveryveryvery # NOQA\n" @@ -546,7 +539,7 @@ def test_length_sort() -> None: "import looooooooooooooooooooooooooooooooooooooong\n" "import medium_sizeeeeeeeeeeeeea\n" ) - test_output = api.sort_code_string(test_input, length_sort=True) + test_output = isort.code(test_input, length_sort=True) assert test_output == ( "import shortie\n" "import medium_sizeeeeeeeeeeeeea\n" @@ -565,7 +558,7 @@ def test_length_sort_section() -> None: "import looooooooooooooooooooooooooooooooooooooong\n" "import medium_sizeeeeeeeeeeeeea\n" ) - test_output = api.sort_code_string(test_input, length_sort_sections=("stdlib",)) + test_output = isort.code(test_input, length_sort_sections=("stdlib",)) assert test_output == ( "import os\n" "import sys\n" @@ -587,9 +580,7 @@ def test_convert_hanging() -> None: " lib13, lib14, lib15, lib16, lib17, \\\n" " lib18, lib20, lib21, lib22\n" ) - test_output = api.sort_code_string( - code=test_input, multi_line_output=WrapModes.GRID, line_length=40 - ) + test_output = isort.code(code=test_input, multi_line_output=WrapModes.GRID, line_length=40) assert test_output == ( "from third_party import (lib1, lib2,\n" " lib3, lib4,\n" @@ -607,7 +598,7 @@ def test_convert_hanging() -> None: def test_custom_indent() -> None: """Ensure setting a custom indent will work as expected.""" - test_output = api.sort_code_string( + test_output = isort.code( code=REALLY_LONG_IMPORT, multi_line_output=WrapModes.HANGING_INDENT, line_length=40, @@ -622,7 +613,7 @@ def test_custom_indent() -> None: " lib20, lib21, lib22\n" ) - test_output = api.sort_code_string( + test_output = isort.code( code=REALLY_LONG_IMPORT, multi_line_output=WrapModes.HANGING_INDENT, line_length=40, @@ -637,7 +628,7 @@ def test_custom_indent() -> None: " lib20, lib21, lib22\n" ) - test_output = api.sort_code_string( + test_output = isort.code( code=REALLY_LONG_IMPORT, multi_line_output=WrapModes.HANGING_INDENT, line_length=40, @@ -652,7 +643,7 @@ def test_custom_indent() -> None: "\tlib20, lib21, lib22\n" ) - test_output = api.sort_code_string( + test_output = isort.code( code=REALLY_LONG_IMPORT, multi_line_output=WrapModes.HANGING_INDENT, line_length=40, @@ -673,14 +664,14 @@ def test_use_parentheses() -> None: "from fooooooooooooooooooooooooo.baaaaaaaaaaaaaaaaaaarrrrrrr import " " my_custom_function as my_special_function" ) - test_output = api.sort_code_string(test_input, line_length=79, use_parentheses=True) + test_output = isort.code(test_input, line_length=79, use_parentheses=True) assert test_output == ( "from fooooooooooooooooooooooooo.baaaaaaaaaaaaaaaaaaarrrrrrr import (\n" " my_custom_function as my_special_function)\n" ) - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, line_length=79, use_parentheses=True, include_trailing_comma=True ) @@ -689,7 +680,7 @@ def test_use_parentheses() -> None: " my_custom_function as my_special_function,)\n" ) - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, line_length=79, use_parentheses=True, @@ -701,7 +692,7 @@ def test_use_parentheses() -> None: " my_custom_function as my_special_function\n)\n" ) - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, line_length=79, use_parentheses=True, @@ -724,7 +715,7 @@ def test_skip() -> None: "import sys # isort: skip this import needs to be placed here\n\n\n\n\n\n\n" ) - test_output = api.sort_code_string(test_input, known_first_party=["myproject"]) + test_output = isort.code(test_input, known_first_party=["myproject"]) assert test_output == ( "import django\n" "\n" @@ -739,7 +730,7 @@ def test_skip_with_file_name() -> None: """Ensure skipping a file works even when file_contents is provided.""" test_input = "import django\nimport myproject\n" with pytest.raises(FileSkipped): - api.sort_code_string( + isort.code( file_path=Path("/baz.py"), code=test_input, settings_path=os.getcwd(), skip=["baz.py"] ) @@ -748,20 +739,20 @@ def test_skip_within_file() -> None: """Ensure skipping a whole file works.""" test_input = "# isort: skip_file\nimport django\nimport myproject\n" with pytest.raises(FileSkipped): - api.sort_code_string(test_input, known_third_party=["django"]) + isort.code(test_input, known_third_party=["django"]) def test_force_to_top() -> None: """Ensure forcing a single import to the top of its category works as expected.""" test_input = "import lib6\nimport lib2\nimport lib5\nimport lib1\n" - test_output = api.sort_code_string(test_input, force_to_top=["lib5"]) + test_output = isort.code(test_input, force_to_top=["lib5"]) assert test_output == "import lib5\nimport lib1\nimport lib2\nimport lib6\n" def test_add_imports() -> None: """Ensures adding imports works as expected.""" test_input = "import lib6\nimport lib2\nimport lib5\nimport lib1\n\n" - test_output = api.sort_code_string(code=test_input, add_imports=["import lib4", "import lib7"]) + test_output = isort.code(code=test_input, add_imports=["import lib4", "import lib7"]) assert test_output == ( "import lib1\n" "import lib2\n" @@ -773,7 +764,7 @@ def test_add_imports() -> None: # Using simplified syntax test_input = "import lib6\nimport lib2\nimport lib5\nimport lib1\n\n" - test_output = api.sort_code_string(code=test_input, add_imports=["lib4", "lib7", "lib8.a"]) + test_output = isort.code(code=test_input, add_imports=["lib4", "lib7", "lib8.a"]) assert test_output == ( "import lib1\n" "import lib2\n" @@ -786,9 +777,7 @@ def test_add_imports() -> None: # On a file that has no pre-existing imports test_input = '"""Module docstring"""\n' "\nclass MyClass(object):\n pass\n" - test_output = api.sort_code_string( - code=test_input, add_imports=["from __future__ import print_function"] - ) + test_output = isort.code(code=test_input, add_imports=["from __future__ import print_function"]) assert test_output == ( '"""Module docstring"""\n' "from __future__ import print_function\n" @@ -800,62 +789,60 @@ def test_add_imports() -> None: # On a file that has no pre-existing imports, and no doc-string test_input = "class MyClass(object):\n pass\n" - test_output = api.sort_code_string( - code=test_input, add_imports=["from __future__ import print_function"] - ) + test_output = isort.code(code=test_input, add_imports=["from __future__ import print_function"]) assert test_output == ( "from __future__ import print_function\n" "\n" "\n" "class MyClass(object):\n" " pass\n" ) # On a file with no content what so ever test_input = "" - test_output = api.sort_code_string(test_input, add_imports=["lib4"]) + test_output = isort.code(test_input, add_imports=["lib4"]) assert test_output == ("") # On a file with no content what so ever, after force_adds is set to True test_input = "" - test_output = api.sort_code_string(code=test_input, add_imports=["lib4"], force_adds=True) + test_output = isort.code(code=test_input, add_imports=["lib4"], force_adds=True) assert test_output == ("import lib4\n") def test_remove_imports() -> None: """Ensures removing imports works as expected.""" test_input = "import lib6\nimport lib2\nimport lib5\nimport lib1" - test_output = api.sort_code_string(test_input, remove_imports=["lib2", "lib6"]) + test_output = isort.code(test_input, remove_imports=["lib2", "lib6"]) assert test_output == "import lib1\nimport lib5\n" # Using natural syntax test_input = ( "import lib6\n" "import lib2\n" "import lib5\n" "import lib1\n" "from lib8 import a" ) - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, remove_imports=["import lib2", "import lib6", "from lib8 import a"] ) assert test_output == "import lib1\nimport lib5\n" # From imports test_input = "from x import y" - test_output = api.sort_code_string(test_input, remove_imports=["x"]) + test_output = isort.code(test_input, remove_imports=["x"]) assert test_output == "" test_input = "from x import y" - test_output = api.sort_code_string(test_input, remove_imports=["x.y"]) + test_output = isort.code(test_input, remove_imports=["x.y"]) assert test_output == "" def test_comments_above(): """Test to ensure comments above an import will stay in place""" test_input = "import os\n\nfrom x import y\n\n# comment\nfrom z import __version__, api\n" - assert api.sort_code_string(test_input, ensure_newline_before_comments=True) == test_input + assert isort.code(test_input, ensure_newline_before_comments=True) == test_input def test_explicitly_local_import() -> None: """Ensure that explicitly local imports are separated.""" test_input = "import lib1\nimport lib2\nimport .lib6\nfrom . import lib7" - assert api.sort_code_string(test_input) == ( + assert isort.code(test_input) == ( "import lib1\nimport lib2\n\nimport .lib6\nfrom . import lib7\n" ) - assert api.sort_code_string(test_input, old_finders=True) == ( + assert isort.code(test_input, old_finders=True) == ( "import lib1\nimport lib2\n\nimport .lib6\nfrom . import lib7\n" ) @@ -863,22 +850,22 @@ def test_explicitly_local_import() -> None: def test_quotes_in_file() -> None: """Ensure imports within triple quotes don't get imported.""" test_input = "import os\n\n" '"""\n' "Let us\nimport foo\nokay?\n" '"""\n' - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input test_input = "import os\n\n" '\'"""\'\n' "import foo\n" - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input test_input = "import os\n\n" '"""Let us"""\n' "import foo\n\n" '"""okay?"""\n' - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input test_input = "import os\n\n" '#"""\n' "import foo\n" '#"""' - assert api.sort_code_string(test_input) == ('import os\n\nimport foo\n\n#"""\n#"""\n') + assert isort.code(test_input) == ('import os\n\nimport foo\n\n#"""\n#"""\n') test_input = "import os\n\n'\\\nimport foo'\n" - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input test_input = "import os\n\n'''\n\\'''\nimport junk\n'''\n" - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input def test_check_newline_in_imports(capsys) -> None: @@ -919,7 +906,7 @@ def test_forced_separate() -> None: "TO_FIELD_VAR\n" ) assert ( - api.sort_code_string( + isort.code( code=test_input, forced_separate=["django.contrib"], known_third_party=["django"], @@ -929,7 +916,7 @@ def test_forced_separate() -> None: == test_input ) assert ( - api.sort_code_string( + isort.code( code=test_input, forced_separate=["django.contrib"], known_third_party=["django"], @@ -942,13 +929,11 @@ def test_forced_separate() -> None: test_input = "from .foo import bar\n\nfrom .y import ca\n" assert ( - api.sort_code_string( - code=test_input, forced_separate=[".y"], line_length=120, order_by_type=False - ) + isort.code(code=test_input, forced_separate=[".y"], line_length=120, order_by_type=False) == test_input ) assert ( - api.sort_code_string( + isort.code( code=test_input, forced_separate=[".y"], line_length=120, @@ -962,14 +947,14 @@ def test_forced_separate() -> None: def test_default_section() -> None: """Test to ensure changing the default section works as expected.""" test_input = "import sys\nimport os\nimport myproject.test\nimport django.settings" - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, known_third_party=["django"], default_section="FIRSTPARTY" ) assert test_output == ( "import os\n" "import sys\n" "\n" "import django.settings\n" "\n" "import myproject.test\n" ) - test_output_custom = api.sort_code_string( + test_output_custom = isort.code( code=test_input, known_third_party=["django"], default_section="STDLIB" ) assert test_output_custom == ( @@ -985,9 +970,7 @@ def test_first_party_overrides_standard_section() -> None: "import os\n" "import profile.test\n" ) - test_output = api.sort_code_string( - code=test_input, known_first_party=["profile"], py_version="27" - ) + test_output = isort.code(code=test_input, known_first_party=["profile"], py_version="27") assert test_output == ( "import os\n" "import sys\n" @@ -1000,7 +983,7 @@ def test_first_party_overrides_standard_section() -> None: def test_thirdy_party_overrides_standard_section() -> None: """Test to ensure changing the default section works as expected.""" test_input = "import sys\nimport os\nimport profile.test\n" - test_output = api.sort_code_string(test_input, known_third_party=["profile"]) + test_output = isort.code(test_input, known_third_party=["profile"]) assert test_output == "import os\nimport sys\n\nimport profile.test\n" @@ -1015,12 +998,12 @@ def test_known_pattern_path_expansion() -> None: "import this\n" "import os\n" ) - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, default_section="THIRDPARTY", known_first_party=["./", "this", "kate_plugin", "isort"], ) - test_output_old_finder = api.sort_code_string( + test_output_old_finder = isort.code( code=test_input, default_section="FIRSTPARTY", old_finders=True, @@ -1049,7 +1032,7 @@ def test_force_single_line_imports() -> None: " lib13, lib14, lib15, lib16, lib17, \\\n" " lib18, lib20, lib21, lib22\n" ) - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, multi_line_output=WrapModes.GRID, line_length=40, force_single_line=True ) assert test_output == ( @@ -1079,7 +1062,7 @@ def test_force_single_line_imports() -> None: test_input = ( "from third_party import lib_a, lib_b, lib_d\n" "from third_party.lib_c import lib1\n" ) - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, multi_line_output=WrapModes.GRID, line_length=40, force_single_line=True ) assert test_output == ( @@ -1092,7 +1075,7 @@ def test_force_single_line_imports() -> None: def test_force_single_line_long_imports() -> None: test_input = "from veryveryveryveryveryvery import small, big\n" - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, multi_line_output=WrapModes.NOQA, line_length=40, force_single_line=True ) assert test_output == ( @@ -1105,7 +1088,7 @@ def test_force_single_line_imports_and_sort_within_sections() -> None: test_input = ( "from third_party import lib_a, lib_b, lib_d\n" "from third_party.lib_c import lib1\n" ) - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, multi_line_output=WrapModes.GRID, line_length=40, @@ -1118,7 +1101,7 @@ def test_force_single_line_imports_and_sort_within_sections() -> None: "from third_party import lib_d\n" "from third_party.lib_c import lib1\n" ) - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, multi_line_output=WrapModes.GRID, line_length=40, @@ -1141,8 +1124,7 @@ def test_force_single_line_imports_and_sort_within_sections() -> None: from matplotlib import pyplot as plt """ test_output = ( - api.sort_code_string(code=test_input, force_sort_within_sections=True, length_sort=True) - == test_input + isort.code(code=test_input, force_sort_within_sections=True, length_sort=True) == test_input ) @@ -1156,7 +1138,7 @@ def test_titled_imports() -> None: "import myproject.test\n" "import django.settings" ) - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, known_first_party=["myproject"], import_heading_stdlib="Standard Library", @@ -1174,7 +1156,7 @@ def test_titled_imports() -> None: "# My Stuff\n" "import myproject.test\n" ) - test_second_run = api.sort_code_string( + test_second_run = isort.code( code=test_output, known_first_party=["myproject"], import_heading_stdlib="Standard Library", @@ -1189,7 +1171,7 @@ def test_balanced_wrapping() -> None: "from __future__ import (absolute_import, division, print_function,\n" " unicode_literals)" ) - test_output = api.sort_code_string(code=test_input, line_length=70, balanced_wrapping=True) + test_output = isort.code(code=test_input, line_length=70, balanced_wrapping=True) assert test_output == ( "from __future__ import (absolute_import, division,\n" " print_function, unicode_literals)\n" @@ -1201,19 +1183,19 @@ def test_relative_import_with_space() -> None: with a space. """ test_input = "from ... fields.sproqet import SproqetCollection" - assert api.sort_code_string(test_input) == ("from ...fields.sproqet import SproqetCollection\n") + assert isort.code(test_input) == ("from ...fields.sproqet import SproqetCollection\n") test_input = "from .import foo" test_output = "from . import foo\n" - assert api.sort_code_string(test_input) == test_output + assert isort.code(test_input) == test_output test_input = "from.import foo" test_output = "from . import foo\n" - assert api.sort_code_string(test_input) == test_output + assert isort.code(test_input) == test_output def test_multiline_import() -> None: """Test the case where import spawns multiple lines with inconsistent indentation.""" test_input = "from pkg \\\n import stuff, other_suff \\\n more_stuff" - assert api.sort_code_string(test_input) == ("from pkg import more_stuff, other_suff, stuff\n") + assert isort.code(test_input) == ("from pkg import more_stuff, other_suff, stuff\n") # test again with a custom configuration custom_configuration = { @@ -1226,38 +1208,36 @@ def test_multiline_import() -> None: expected_output = ( "from pkg import more_stuff\n" "from pkg import other_suff\n" "from pkg import stuff\n" ) - assert api.sort_code_string(test_input, **custom_configuration) == expected_output + assert isort.code(test_input, **custom_configuration) == expected_output def test_single_multiline() -> None: """Test the case where a single import spawns multiple lines.""" test_input = "from os import\\\n getuid\n\nprint getuid()\n" - output = api.sort_code_string(test_input) + output = isort.code(test_input) assert output == ("from os import getuid\n\nprint getuid()\n") def test_atomic_mode() -> None: # without syntax error, everything works OK test_input = "from b import d, c\nfrom a import f, e\n" - assert api.sort_code_string(test_input, atomic=True) == ( - "from a import e, f\nfrom b import c, d\n" - ) + assert isort.code(test_input, atomic=True) == ("from a import e, f\nfrom b import c, d\n") # with syntax error content is not changed test_input += "while True print 'Hello world'" # blatant syntax error with pytest.raises(ExistingSyntaxErrors): - api.sort_code_string(test_input, atomic=True) + isort.code(test_input, atomic=True) def test_order_by_type() -> None: test_input = "from module import Class, CONSTANT, function" - assert api.sort_code_string(test_input, order_by_type=True) == ( + assert isort.code(test_input, order_by_type=True) == ( "from module import CONSTANT, Class, function\n" ) # More complex sample data test_input = "from module import Class, CONSTANT, function, BASIC, Apple" - assert api.sort_code_string(test_input, order_by_type=True) == ( + assert isort.code(test_input, order_by_type=True) == ( "from module import BASIC, CONSTANT, Apple, Class, function\n" ) @@ -1272,7 +1252,7 @@ def test_order_by_type() -> None: "from subprocess import PIPE, Popen, STDOUT\n" ) - assert api.sort_code_string(test_input, order_by_type=True, py_version="27") == ( + assert isort.code(test_input, order_by_type=True, py_version="27") == ( "import glob\n" "import os\n" "import shutil\n" @@ -1288,37 +1268,31 @@ def test_custom_lines_after_import_section() -> None: test_input = "from a import b\nfoo = 'bar'\n" # default case is one space if not method or class after imports - assert api.sort_code_string(test_input) == ("from a import b\n\nfoo = 'bar'\n") + assert isort.code(test_input) == ("from a import b\n\nfoo = 'bar'\n") # test again with a custom number of lines after the import section - assert api.sort_code_string(test_input, lines_after_imports=2) == ( - "from a import b\n\n\nfoo = 'bar'\n" - ) + assert isort.code(test_input, lines_after_imports=2) == ("from a import b\n\n\nfoo = 'bar'\n") def test_smart_lines_after_import_section() -> None: """Tests the default 'smart' behavior for dealing with lines after the import section""" # one space if not method or class after imports test_input = "from a import b\nfoo = 'bar'\n" - assert api.sort_code_string(test_input) == ("from a import b\n\nfoo = 'bar'\n") + assert isort.code(test_input) == ("from a import b\n\nfoo = 'bar'\n") # two spaces if a method or class after imports test_input = "from a import b\ndef my_function():\n pass\n" - assert api.sort_code_string(test_input) == ( - "from a import b\n\n\ndef my_function():\n pass\n" - ) + assert isort.code(test_input) == ("from a import b\n\n\ndef my_function():\n pass\n") # two spaces if an async method after imports test_input = "from a import b\nasync def my_function():\n pass\n" - assert api.sort_code_string(test_input) == ( - "from a import b\n\n\nasync def my_function():\n pass\n" - ) + assert isort.code(test_input) == ("from a import b\n\n\nasync def my_function():\n pass\n") # two spaces if a method or class after imports - even if comment before function test_input = ( "from a import b\n" "# comment should be ignored\n" "def my_function():\n" " pass\n" ) - assert api.sort_code_string(test_input) == ( + assert isort.code(test_input) == ( "from a import b\n" "\n" "\n" @@ -1336,7 +1310,7 @@ def test_smart_lines_after_import_section() -> None: "def my_function():\n" " pass\n" ) - assert api.sort_code_string(test_input) == ( + assert isort.code(test_input) == ( "from a import b\n" "\n" '"""\n' @@ -1348,7 +1322,7 @@ def test_smart_lines_after_import_section() -> None: # Ensure logic doesn't incorrectly skip over assignments to multi-line strings test_input = 'from a import b\nX = """test\n"""\ndef my_function():\n pass\n' - assert api.sort_code_string(test_input) == ( + assert isort.code(test_input) == ( "from a import b\n" "\n" 'X = """test\n' '"""\n' "def my_function():\n" " pass\n" ) @@ -1368,10 +1342,10 @@ def test_combined_from_and_as_imports() -> None: "from translate.storage import base, factory\n" "from translate.storage.placeables import general, parse as rich_parse\n" ) - assert api.sort_code_string(test_input, combine_as_imports=True) == test_input + assert isort.code(test_input, combine_as_imports=True) == test_input test_input = "import os \nimport os as _os" test_output = "import os\nimport os as _os\n" - assert api.sort_code_string(test_input, keep_direct_and_as_imports=True) == test_output + assert isort.code(test_input, keep_direct_and_as_imports=True) == test_output def test_as_imports_with_line_length() -> None: @@ -1380,7 +1354,7 @@ def test_as_imports_with_line_length() -> None: "from translate.storage import base as storage_base\n" "from translate.storage.placeables import general, parse as rich_parse\n" ) - assert api.sort_code_string(code=test_input, combine_as_imports=False, line_length=40) == ( + assert isort.code(code=test_input, combine_as_imports=False, line_length=40) == ( "from translate.storage import \\\n base as storage_base\n" "from translate.storage.placeables import \\\n general\n" "from translate.storage.placeables import \\\n parse as rich_parse\n" @@ -1391,23 +1365,23 @@ def test_keep_comments() -> None: """Test to ensure isort properly keeps comments in tact after sorting.""" # Straight Import test_input = "import foo # bar\n" - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input # Star import test_input_star = "from foo import * # bar\n" - assert api.sort_code_string(test_input_star) == test_input_star + assert isort.code(test_input_star) == test_input_star # Force Single Line From Import test_input = "from foo import bar # comment\n" - assert api.sort_code_string(test_input, force_single_line=True) == test_input + assert isort.code(test_input, force_single_line=True) == test_input # From import test_input = "from foo import bar # My Comment\n" - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input # More complicated case test_input = "from a import b # My Comment1\nfrom a import c # My Comment2\n" - assert api.sort_code_string(test_input) == ( + assert isort.code(test_input) == ( "from a import b # My Comment1\nfrom a import c # My Comment2\n" ) @@ -1415,7 +1389,7 @@ def test_keep_comments() -> None: test_input = ( "from a import b # My Comment1\n" "from a import c # My Comment2\n" "from a import d\n" ) - assert api.sort_code_string(test_input, line_length=45) == ( + assert isort.code(test_input, line_length=45) == ( "from a import b # My Comment1\n" "from a import c # My Comment2\n" "from a import d\n" ) @@ -1424,14 +1398,14 @@ def test_keep_comments() -> None: "from a import b, c # My Comment1\n" "from a import c, d # My Comment2 is really really really really long\n" ) - assert api.sort_code_string(test_input, line_length=45) == ( + assert isort.code(test_input, line_length=45) == ( "from a import ( # My Comment1; My Comment2 is really really really really long\n" " b, c, d)\n" ) # Test that comments are not stripped from 'import ... as ...' by default test_input = "from a import b as bb # b comment\nfrom a import c as cc # c comment\n" - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input # Test that 'import ... as ...' comments are not collected inappropriately test_input = ( @@ -1439,8 +1413,8 @@ def test_keep_comments() -> None: "from a import c as cc # c comment\n" "from a import d\n" ) - assert api.sort_code_string(test_input) == test_input - assert api.sort_code_string(test_input, combine_as_imports=True) == ( + assert isort.code(test_input) == test_input + assert isort.code(test_input, combine_as_imports=True) == ( "from a import b as bb, c as cc, d # b comment; c comment\n" ) @@ -1453,7 +1427,7 @@ def test_multiline_split_on_dot() -> None: "from my_lib.my_package.test.level_1.level_2.level_3.level_4.level_5.\\\n" " my_module import my_function" ) - assert api.sort_code_string(test_input, line_length=70) == ( + assert isort.code(test_input, line_length=70) == ( "from my_lib.my_package.test.level_1.level_2.level_3.level_4.level_5.my_module import \\\n" " my_function\n" ) @@ -1462,13 +1436,13 @@ def test_multiline_split_on_dot() -> None: def test_import_star() -> None: """Test to ensure isort handles star imports correctly""" test_input = "from blah import *\nfrom blah import _potato\n" - assert api.sort_code_string(test_input) == ("from blah import *\nfrom blah import _potato\n") - assert api.sort_code_string(test_input, combine_star=True) == ("from blah import *\n") + assert isort.code(test_input) == ("from blah import *\nfrom blah import _potato\n") + assert isort.code(test_input, combine_star=True) == ("from blah import *\n") def test_include_trailing_comma() -> None: """Test for the include_trailing_comma option""" - test_output_grid = api.sort_code_string( + test_output_grid = isort.code( code=SHORT_IMPORT, multi_line_output=WrapModes.GRID, line_length=40, @@ -1478,7 +1452,7 @@ def test_include_trailing_comma() -> None: "from third_party import (lib1, lib2,\n" " lib3, lib4,)\n" ) - test_output_vertical = api.sort_code_string( + test_output_vertical = isort.code( code=SHORT_IMPORT, multi_line_output=WrapModes.VERTICAL, line_length=40, @@ -1491,7 +1465,7 @@ def test_include_trailing_comma() -> None: " lib4,)\n" ) - test_output_vertical_indent = api.sort_code_string( + test_output_vertical_indent = isort.code( code=SHORT_IMPORT, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, line_length=40, @@ -1501,7 +1475,7 @@ def test_include_trailing_comma() -> None: "from third_party import (\n" " lib1,\n" " lib2,\n" " lib3,\n" " lib4,\n" ")\n" ) - test_output_vertical_grid = api.sort_code_string( + test_output_vertical_grid = isort.code( code=SHORT_IMPORT, multi_line_output=WrapModes.VERTICAL_GRID, line_length=40, @@ -1511,7 +1485,7 @@ def test_include_trailing_comma() -> None: "from third_party import (\n lib1, lib2, lib3, lib4,)\n" ) - test_output_vertical_grid_grouped = api.sort_code_string( + test_output_vertical_grid_grouped = isort.code( code=SHORT_IMPORT, multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, line_length=40, @@ -1521,14 +1495,14 @@ def test_include_trailing_comma() -> None: "from third_party import (\n lib1, lib2, lib3, lib4,\n)\n" ) - test_output_wrap_single_import_with_use_parentheses = api.sort_code_string( + test_output_wrap_single_import_with_use_parentheses = isort.code( code=SINGLE_FROM_IMPORT, line_length=25, include_trailing_comma=True, use_parentheses=True ) assert test_output_wrap_single_import_with_use_parentheses == ( "from third_party import (\n lib1,)\n" ) - test_output_wrap_single_import_vertical_indent = api.sort_code_string( + test_output_wrap_single_import_vertical_indent = isort.code( code=SINGLE_FROM_IMPORT, line_length=25, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, @@ -1548,7 +1522,7 @@ def test_include_trailing_comma() -> None: " urlencode, # pylint: disable=no-n" "ame-in-module,import-error\n)\n" ) - trailing_comma_with_comment = api.sort_code_string( + trailing_comma_with_comment = isort.code( code=trailing_comma_with_comment, line_length=80, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, @@ -1557,7 +1531,7 @@ def test_include_trailing_comma() -> None: ) assert trailing_comma_with_comment == expected_trailing_comma_with_comment # The next time around, it should be equal - trailing_comma_with_comment = api.sort_code_string( + trailing_comma_with_comment = isort.code( code=trailing_comma_with_comment, line_length=80, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, @@ -1572,16 +1546,16 @@ def test_similar_to_std_library() -> None: don't end up clobbered """ test_input = "import datetime\n\nimport requests\nimport times\n" - assert api.sort_code_string(test_input, known_third_party=["requests", "times"]) == test_input + assert isort.code(test_input, known_third_party=["requests", "times"]) == test_input def test_correctly_placed_imports() -> None: """Test to ensure comments stay on correct placement after being sorted""" test_input = "from a import b # comment for b\nfrom a import c # comment for c\n" - assert api.sort_code_string(test_input, force_single_line=True) == ( + assert isort.code(test_input, force_single_line=True) == ( "from a import b # comment for b\nfrom a import c # comment for c\n" ) - assert api.sort_code_string(test_input) == ( + assert isort.code(test_input) == ( "from a import b # comment for b\nfrom a import c # comment for c\n" ) @@ -1653,7 +1627,7 @@ def test_correctly_placed_imports() -> None: "get_right\n" ) assert ( - api.sort_code_string( + isort.code( code=test_input, force_single_line=True, line_length=140, @@ -1671,10 +1645,10 @@ def test_auto_detection() -> None: # Issue 157 test_input = "import binascii\nimport os\n\nimport cv2\nimport requests\n" - assert api.sort_code_string(test_input, known_third_party=["cv2", "requests"]) == test_input + assert isort.code(test_input, known_third_party=["cv2", "requests"]) == test_input # alternative solution - assert api.sort_code_string(test_input, default_section="THIRDPARTY") == test_input + assert isort.code(test_input, default_section="THIRDPARTY") == test_input def test_same_line_statements() -> None: @@ -1682,10 +1656,10 @@ def test_same_line_statements() -> None: contains multiple statements including an import """ test_input = "import pdb; import nose\n" - assert api.sort_code_string(test_input) == ("import pdb\n\nimport nose\n") + assert isort.code(test_input) == ("import pdb\n\nimport nose\n") test_input = "import pdb; pdb.set_trace()\nimport nose; nose.run()\n" - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input def test_long_line_comments() -> None: @@ -1698,7 +1672,7 @@ def test_long_line_comments() -> None: "sync_stage_envdir, " "update_stage_app, update_stage_cron # noqa\n" ) - assert api.sort_code_string(code=test_input, line_length=100, balanced_wrapping=True) == ( + assert isort.code(code=test_input, line_length=100, balanced_wrapping=True) == ( "from foo.utils.fabric_stuff.live import (check_clean_live, deploy_live, # noqa\n" " sync_live_envdir, update_live_app, " "update_live_cron)\n" @@ -1713,7 +1687,7 @@ def test_tab_character_in_import() -> None: test_input = ( "from __future__ import print_function\n" "from __future__ import\tprint_function\n" ) - assert api.sort_code_string(test_input) == "from __future__ import print_function\n" + assert isort.code(test_input) == "from __future__ import print_function\n" def test_split_position() -> None: @@ -1722,7 +1696,7 @@ def test_split_position() -> None: "from p24.shared.exceptions.master.host_state_flag_unchanged " "import HostStateUnchangedException\n" ) - assert api.sort_code_string(test_input, line_length=80) == ( + assert isort.code(test_input, line_length=80) == ( "from p24.shared.exceptions.master.host_state_flag_unchanged import \\\n" " HostStateUnchangedException\n" ) @@ -1752,9 +1726,9 @@ def test_place_comments() -> None: "import os\n" "import sys\n" ) - test_output = api.sort_code_string(test_input, known_first_party=["myproject"]) + test_output = isort.code(test_input, known_first_party=["myproject"]) assert test_output == expected_output - test_output = api.sort_code_string(test_output, known_first_party=["myproject"]) + test_output = isort.code(test_output, known_first_party=["myproject"]) assert test_output == expected_output @@ -1769,7 +1743,7 @@ def test_placement_control() -> None: "import p24.imports._VERSION as VERSION\n" "import p24.shared.media_wiki_syntax as syntax\n" ) - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, known_first_party=["p24", "p24.imports._VERSION"], known_standard_library=["p24.imports", "os", "sys"], @@ -1805,7 +1779,7 @@ def test_custom_sections() -> None: "import numpy as np\n" "import p24.shared.media_wiki_syntax as syntax\n" ) - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, known_first_party=["p24", "p24.imports._VERSION"], import_heading_stdlib="Standard Library", @@ -1861,7 +1835,7 @@ def test_glob_known() -> None: "from django.conf import settings\n" "from . import another\n" ) - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, import_heading_stdlib="Standard Library", import_heading_thirdparty="Third Party", @@ -1906,7 +1880,7 @@ def test_sticky_comments() -> None: "# Used for type-hinting (ref: https://github.com/davidhalter/jedi/issues/414).\n" "from selenium.webdriver.remote.webdriver import WebDriver # noqa\n" ) - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input test_input = ( "from django import forms\n" @@ -1915,26 +1889,26 @@ def test_sticky_comments() -> None: "from django.contrib.gis.geos import GEOSException, GEOSGeometry\n" "from django.utils.translation import ugettext_lazy as _\n" ) - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input def test_zipimport() -> None: """Imports ending in "import" shouldn't be clobbered""" test_input = "from zipimport import zipimport\n" - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input def test_from_ending() -> None: """Imports ending in "from" shouldn't be clobbered.""" test_input = "from foo import get_foo_from, get_foo\n" expected_output = "from foo import get_foo, get_foo_from\n" - assert api.sort_code_string(test_input) == expected_output + assert isort.code(test_input) == expected_output def test_from_first() -> None: """Tests the setting from_first works correctly""" test_input = "from os import path\nimport os\n" - assert api.sort_code_string(test_input, from_first=True) == test_input + assert isort.code(test_input, from_first=True) == test_input def test_top_comments() -> None: @@ -1945,32 +1919,32 @@ def test_top_comments() -> None: "#\n" "from __future__ import unicode_literals\n" ) - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input test_input = ( "# -*- coding: utf-8 -*-\n" "from django.db import models\n" "from django.utils.encoding import python_2_unicode_compatible\n" ) - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input test_input = "# Comment\nimport sys\n" - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input test_input = "# -*- coding\nimport sys\n" - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input def test_consistency() -> None: """Ensures consistency of handling even when dealing with non ordered-by-type imports""" test_input = "from sqlalchemy.dialects.postgresql import ARRAY, array\n" - assert api.sort_code_string(test_input, order_by_type=True) == test_input + assert isort.code(test_input, order_by_type=True) == test_input def test_force_grid_wrap() -> None: """Ensures removing imports works as expected.""" test_input = "from bar import lib2\nfrom foo import lib6, lib7\n" - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, force_grid_wrap=2, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT ) assert ( @@ -1982,7 +1956,7 @@ def test_force_grid_wrap() -> None: ) """ ) - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, force_grid_wrap=3, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT ) assert test_output == test_input @@ -1995,7 +1969,7 @@ def test_force_grid_wrap_long() -> None: "from bar import lib2\n" "from babar import something_that_is_kind_of_long" ) - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, force_grid_wrap=2, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, @@ -2018,7 +1992,7 @@ def test_uses_jinja_variables() -> None: test_input = ( "import sys\n" "import os\n" "import myproject.{ test }\n" "import django.{ settings }" ) - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, known_third_party=["django"], known_first_party=["myproject"] ) assert test_output == ( @@ -2031,13 +2005,13 @@ def test_uses_jinja_variables() -> None: ) test_input = "import {{ cookiecutter.repo_name }}\n" "from foo import {{ cookiecutter.bar }}\n" - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input def test_fcntl() -> None: """Test to ensure fcntl gets correctly recognized as stdlib import""" test_input = "import fcntl\nimport os\nimport sys\n" - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input def test_import_split_is_word_boundary_aware() -> None: @@ -2046,7 +2020,7 @@ def test_import_split_is_word_boundary_aware() -> None: "from mycompany.model.size_value_array_import_func import \\\n" " get_size_value_array_import_func_jobs" ) - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, line_length=79 ) assert test_output == ( @@ -2072,7 +2046,7 @@ def test_encoding_not_in_comment(tmpdir) -> None: file_contents = "class Foo\n coding: latin1\n\ns = u'ã'\n" tmp_fname.write_binary(file_contents.encode("utf8")) assert ( - api.sort_code_string( + isort.code( Path(tmp_fname).read_text("utf8"), file_path=Path(tmp_fname), settings_path=os.getcwd() ) == file_contents @@ -2085,7 +2059,7 @@ def test_encoding_not_in_first_two_lines(tmpdir) -> None: file_contents = "\n\n# -*- coding: latin1\n\ns = u'ã'\n" tmp_fname.write_binary(file_contents.encode("utf8")) assert ( - api.sort_code_string( + isort.code( Path(tmp_fname).read_text("utf8"), file_path=Path(tmp_fname), settings_path=os.getcwd() ) == file_contents @@ -2100,10 +2074,10 @@ def test_comment_at_top_of_file() -> None: "# Comment two\n" "from django.contrib.gis.geos import GEOSException\n" ) - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input test_input = "# -*- coding: utf-8 -*-\nfrom django.db import models\n" - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input def test_alphabetic_sorting() -> None: @@ -2124,11 +2098,11 @@ def test_alphabetic_sorting() -> None: "force_alphabetical_sort_within_sections": True, } # type: Dict[str, Any] - output = api.sort_code_string(test_input, **options) + output = isort.code(test_input, **options) assert output == test_input test_input = "# -*- coding: utf-8 -*-\nfrom django.db import models\n" - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input def test_alphabetic_sorting_multi_line() -> None: @@ -2138,7 +2112,7 @@ def test_alphabetic_sorting_multi_line() -> None: " CONSTANT_F, CONSTANT_G, CONSTANT_H, CONSTANT_I, CONSTANT_J)\n" ) options = {"force_alphabetical_sort_within_sections": True} # type: Dict[str, Any] - assert api.sort_code_string(test_input, **options) == test_input + assert isort.code(test_input, **options) == test_input def test_comments_not_duplicated() -> None: @@ -2149,7 +2123,7 @@ def test_comments_not_duplicated() -> None: "from service import demo # inline comment\n" "from service import settings\n" ) - output = api.sort_code_string(test_input) + output = isort.code(test_input) assert output.count("# Whole line comment\n") == 1 assert output.count("# inline comment\n") == 1 @@ -2166,7 +2140,7 @@ def test_top_of_line_comments() -> None: "\n" "import logging\n" ) - output = api.sort_code_string(test_input) + output = isort.code(test_input) print(output) assert output.startswith("# -*- coding: utf-8 -*-\n") @@ -2174,7 +2148,7 @@ def test_top_of_line_comments() -> None: def test_basic_comment() -> None: """Test to ensure a basic comment wont crash isort""" test_input = "import logging\n# Foo\nimport os\n" - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input def test_shouldnt_add_lines() -> None: @@ -2182,7 +2156,7 @@ def test_shouldnt_add_lines() -> None: See: issue #316 """ test_input = '"""Text"""\n' "# This is a comment\nimport pkg_resources\n" - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input def test_sections_parsed_correct(tmpdir) -> None: @@ -2207,7 +2181,7 @@ def test_sections_parsed_correct(tmpdir) -> None: "from nose import *\n" ) tmpdir.join(".isort.cfg").write(conf_file_data) - assert api.sort_code_string(test_input, settings_path=str(tmpdir)) == correct_output + assert isort.code(test_input, settings_path=str(tmpdir)) == correct_output @pytest.mark.skipif(toml is None, reason="Requires toml package to be installed.") @@ -2246,7 +2220,7 @@ def test_pyproject_conf_file(tmpdir) -> None: "from nose import *\n" ) tmpdir.join("pyproject.toml").write(conf_file_data) - assert api.sort_code_string(test_input, settings_path=str(tmpdir)) == correct_output + assert isort.code(test_input, settings_path=str(tmpdir)) == correct_output def test_alphabetic_sorting_no_newlines() -> None: @@ -2254,13 +2228,11 @@ def test_alphabetic_sorting_no_newlines() -> None: erroneously introduce new lines (issue #328) """ test_input = "import os\n" - test_output = api.sort_code_string( - code=test_input, force_alphabetical_sort_within_sections=True - ) + test_output = isort.code(code=test_input, force_alphabetical_sort_within_sections=True) assert test_input == test_output test_input = "import os\n" "import unittest\n" "\n" "from a import b\n" "\n" "\n" "print(1)\n" - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, force_alphabetical_sort_within_sections=True, lines_after_imports=2 ) assert test_input == test_output @@ -2274,7 +2246,7 @@ def test_sort_within_section() -> None: "from foo import bar\n" "from foo.bar import Quux, baz\n" ) - test_output = api.sort_code_string(test_input, force_sort_within_sections=True) + test_output = isort.code(test_input, force_sort_within_sections=True) assert test_output == test_input test_input = ( @@ -2284,7 +2256,7 @@ def test_sort_within_section() -> None: "from foo.bar import Quux\n" "from Foob import ar\n" ) - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, force_sort_within_sections=True, order_by_type=False, @@ -2296,18 +2268,14 @@ def test_sort_within_section() -> None: def test_sorting_with_two_top_comments() -> None: """Test to ensure isort will sort files that contain 2 top comments""" test_input = "#! comment1\n''' comment2\n'''\nimport b\nimport a\n" - assert api.sort_code_string(test_input) == ( - "#! comment1\n''' comment2\n'''\nimport a\nimport b\n" - ) + assert isort.code(test_input) == ("#! comment1\n''' comment2\n'''\nimport a\nimport b\n") def test_lines_between_sections() -> None: """Test to ensure lines_between_sections works""" test_input = "from bar import baz\nimport os\n" - assert api.sort_code_string(test_input, lines_between_sections=0) == ( - "import os\nfrom bar import baz\n" - ) - assert api.sort_code_string(test_input, lines_between_sections=2) == ( + assert isort.code(test_input, lines_between_sections=0) == ("import os\nfrom bar import baz\n") + assert isort.code(test_input, lines_between_sections=2) == ( "import os\n\n\nfrom bar import baz\n" ) @@ -2325,9 +2293,7 @@ def test_forced_sepatate_globs() -> None: "\n" "import sys\n" ) - test_output = api.sort_code_string( - code=test_input, forced_separate=["*.models"], line_length=120 - ) + test_output = isort.code(code=test_input, forced_separate=["*.models"], line_length=120) assert test_output == ( "import os\n" @@ -2364,18 +2330,18 @@ def test_no_additional_lines_issue_358() -> None: " unicode_literals\n" ")\n" ) - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, line_length=20 ) assert test_output == expected_output - test_output = api.sort_code_string( + test_output = isort.code( code=test_output, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, line_length=20 ) assert test_output == expected_output for _attempt in range(5): - test_output = api.sort_code_string( + test_output = isort.code( code=test_output, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, line_length=20 ) assert test_output == expected_output @@ -2402,18 +2368,18 @@ def test_no_additional_lines_issue_358() -> None: " unicode_literals\n" ")\n" ) - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, line_length=20 ) assert test_output == expected_output - test_output = api.sort_code_string( + test_output = isort.code( code=test_output, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, line_length=20 ) assert test_output == expected_output for _attempt in range(5): - test_output = api.sort_code_string( + test_output = isort.code( code=test_output, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, line_length=20 ) assert test_output == expected_output @@ -2424,7 +2390,7 @@ def test_import_by_paren_issue_375() -> None: paren is directly by the import body """ test_input = "from .models import(\n Foo,\n Bar,\n)\n" - assert api.sort_code_string(test_input) == "from .models import Bar, Foo\n" + assert isort.code(test_input) == "from .models import Bar, Foo\n" def test_import_by_paren_issue_460() -> None: @@ -2436,7 +2402,7 @@ def test_import_by_paren_issue_460() -> None: import io import os """ - assert api.sort_code_string((test_input)) == test_input + assert isort.code((test_input)) == test_input def test_function_with_docstring() -> None: @@ -2453,7 +2419,7 @@ def test_function_with_docstring() -> None: ' """ Single line triple quoted doctring """\n' " pass\n" ) - assert api.sort_code_string(test_input, add_imports=add_imports) == expected_output + assert isort.code(test_input, add_imports=add_imports) == expected_output def test_plone_style() -> None: @@ -2470,7 +2436,7 @@ def test_plone_style() -> None: "import Zope\n" ) options = {"force_single_line": True, "force_alphabetical_sort": True} # type: Dict[str, Any] - assert api.sort_code_string(test_input, **options) == test_input + assert isort.code(test_input, **options) == test_input def test_third_party_case_sensitive() -> None: @@ -2478,7 +2444,7 @@ def test_third_party_case_sensitive() -> None: test_input = "import thirdparty\nimport os\nimport ABC\n" expected_output = "import os\n\nimport ABC\nimport thirdparty\n" - assert api.sort_code_string(test_input) == expected_output + assert isort.code(test_input) == expected_output def test_exists_case_sensitive_file(tmpdir) -> None: @@ -2501,14 +2467,14 @@ def test_sys_path_mutation(tmpdir) -> None: test_input = "from myproject import test" options = {"virtual_env": str(tmpdir)} # type: Dict[str, Any] expected_length = len(sys.path) - api.sort_code_string(test_input, **options) + isort.code(test_input, **options) assert len(sys.path) == expected_length - api.sort_code_string(test_input, old_finders=True, **options) + isort.code(test_input, old_finders=True, **options) def test_long_single_line() -> None: """Test to ensure long single lines get handled correctly""" - output = api.sort_code_string( + output = isort.code( code="from ..views import (" " _a," "_xxxxxx_xxxxxxx_xxxxxxxx_xxx_xxxxxxx as xxxxxx_xxxxxxx_xxxxxxxx_xxx_xxxxxxx)", @@ -2517,7 +2483,7 @@ def test_long_single_line() -> None: for line in output.split("\n"): assert len(line) <= 79 - output = api.sort_code_string( + output = isort.code( code="from ..views import (" " _a," "_xxxxxx_xxxxxxx_xxxxxxxx_xxx_xxxxxxx as xxxxxx_xxxxxxx_xxxxxxxx_xxx_xxxxxxx)", @@ -2542,32 +2508,32 @@ def test_import_inside_class_issue_432() -> None: " def bar(self):\n" " pass\n" ) - assert api.sort_code_string(test_input, add_imports=["import baz"]) == expected_output + assert isort.code(test_input, add_imports=["import baz"]) == expected_output def test_wildcard_import_without_space_issue_496() -> None: """Test to ensure issue #496: wildcard without space, is resolved""" test_input = "from findorserver.coupon.models import*" expected_output = "from findorserver.coupon.models import *\n" - assert api.sort_code_string(test_input) == expected_output + assert isort.code(test_input) == expected_output def test_import_line_mangles_issues_491() -> None: """Test to ensure comment on import with parens doesn't cause issues""" test_input = "import os # ([\n\n" 'print("hi")\n' - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input def test_import_line_mangles_issues_505() -> None: """Test to ensure comment on import with parens doesn't cause issues""" test_input = "from sys import * # (\n\n\ndef test():\n" ' print("Test print")\n' - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input def test_import_line_mangles_issues_439() -> None: """Test to ensure comment on import with parens doesn't cause issues""" test_input = "import a # () import\nfrom b import b\n" - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input def test_alias_using_paren_issue_466() -> None: @@ -2579,7 +2545,7 @@ def test_alias_using_paren_issue_466() -> None: "from django.db.backends.mysql.base import (\n" " DatabaseWrapper as MySQLDatabaseWrapper)\n" ) - assert api.sort_code_string(test_input, line_length=50, use_parentheses=True) == expected_output + assert isort.code(test_input, line_length=50, use_parentheses=True) == expected_output test_input = ( "from django.db.backends.mysql.base import DatabaseWrapper as MySQLDatabaseWrapper\n" @@ -2590,7 +2556,7 @@ def test_alias_using_paren_issue_466() -> None: ")\n" ) assert ( - api.sort_code_string( + isort.code( code=test_input, line_length=50, multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, @@ -2610,7 +2576,7 @@ def test_long_alias_using_paren_issue_957() -> None: " module as very_very_very_very_very_very_very_very_very_very_long_alias\n" ")\n" ) - out = api.sort_code_string( + out = isort.code( code=test_input, line_length=50, use_parentheses=True, @@ -2627,7 +2593,7 @@ def test_long_alias_using_paren_issue_957() -> None: " module as very_very_very_very_very_very_very_very_very_very_long_alias\n" ")\n" ) - out = api.sort_code_string( + out = isort.code( code=test_input, line_length=50, use_parentheses=True, @@ -2646,7 +2612,7 @@ def test_long_alias_using_paren_issue_957() -> None: "_very_very_very_very_very_very_long_alias\n" ")\n" ) - out = api.sort_code_string( + out = isort.code( code=test_input, line_length=50, use_parentheses=True, @@ -2687,9 +2653,7 @@ def test_import_wraps_with_comment_issue_471() -> None: " SuperLongClassName) # @UnusedImport -- long string of comments which wrap over\n" ) assert ( - api.sort_code_string( - code=test_input, line_length=50, multi_line_output=1, use_parentheses=True - ) + isort.code(code=test_input, line_length=50, multi_line_output=1, use_parentheses=True) == expected_output ) @@ -2702,13 +2666,13 @@ def test_import_case_produces_inconsistent_results_issue_472() -> None: "from sqlalchemy.dialects.postgresql import ARRAY\n" "from sqlalchemy.dialects.postgresql import array\n" ) - assert api.sort_code_string(test_input, force_single_line=True) == test_input + assert isort.code(test_input, force_single_line=True) == test_input test_input = ( "from scrapy.core.downloader.handlers.http import " "HttpDownloadHandler, HTTPDownloadHandler\n" ) - assert api.sort_code_string(test_input, line_length=100) == test_input + assert isort.code(test_input, line_length=100) == test_input def test_inconsistent_behavior_in_python_2_and_3_issue_479() -> None: @@ -2718,7 +2682,7 @@ def test_inconsistent_behavior_in_python_2_and_3_issue_479() -> None: "\n" "from future.standard_library import hooks\n" ) - assert api.sort_code_string(test_input, known_first_party=["future"]) == test_input + assert isort.code(test_input, known_first_party=["future"]) == test_input def test_sort_within_section_comments_issue_436() -> None: @@ -2732,14 +2696,14 @@ def test_sort_within_section_comments_issue_436() -> None: "# it must not be ... comment line 3\n" "import report\n" ) - assert api.sort_code_string(test_input, force_sort_within_sections=True) == test_input + assert isort.code(test_input, force_sort_within_sections=True) == test_input def test_sort_within_sections_with_force_to_top_issue_473() -> None: """Test to ensure it's possible to sort within sections with items forced to top""" test_input = "import z\nimport foo\nfrom foo import bar\n" assert ( - api.sort_code_string(code=test_input, force_sort_within_sections=True, force_to_top=["z"]) + isort.code(code=test_input, force_sort_within_sections=True, force_to_top=["z"]) == test_input ) @@ -2749,7 +2713,7 @@ def test_correct_number_of_new_lines_with_comment_issue_435() -> None: doesn't mess up the new line spacing """ test_input = "import foo\n\n# comment\n\n\ndef baz():\n pass\n" - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input def test_future_below_encoding_issue_545() -> None: @@ -2769,7 +2733,7 @@ def test_future_below_encoding_issue_545() -> None: "\n" 'print("hello")\n' ) - assert api.sort_code_string(test_input) == expected_output + assert isort.code(test_input) == expected_output def test_no_extra_lines_issue_557() -> None: @@ -2786,7 +2750,7 @@ def test_no_extra_lines_issue_557() -> None: "HTTPDownloadHandler\n" ) assert ( - api.sort_code_string( + isort.code( code=test_input, force_alphabetical_sort=True, force_sort_within_sections=True, @@ -2803,9 +2767,7 @@ def test_long_import_wrap_support_with_mode_2() -> None: " an_even_longer_function_name_over_80_characters\n" ) assert ( - api.sort_code_string( - code=test_input, multi_line_output=WrapModes.HANGING_INDENT, line_length=80 - ) + isort.code(code=test_input, multi_line_output=WrapModes.HANGING_INDENT, line_length=80) == test_input ) @@ -2820,7 +2782,7 @@ def test_pylint_comments_incorrectly_wrapped_issue_571() -> None: "from PyQt5.QtCore import \\\n" " QRegExp # @UnresolvedImport pylint: disable=import-error,useless-suppression\n" ) - assert api.sort_code_string(test_input, line_length=60) == expected_output + assert isort.code(test_input, line_length=60) == expected_output def test_ensure_async_methods_work_issue_537() -> None: @@ -2832,29 +2794,29 @@ def test_ensure_async_methods_work_issue_537() -> None: "async def test_myfunction(test_client, app):\n" " a = await myfunction(test_client, app)\n" ) - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input def test_ensure_as_imports_sort_correctly_within_from_imports_issue_590() -> None: """Test to ensure combination from and as import statements are sorted correct""" test_input = "from os import defpath\nfrom os import pathsep as separator\n" - assert api.sort_code_string(test_input, force_sort_within_sections=True) == test_input + assert isort.code(test_input, force_sort_within_sections=True) == test_input test_input = "from os import defpath\nfrom os import pathsep as separator\n" - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input test_input = "from os import defpath\nfrom os import pathsep as separator\n" - assert api.sort_code_string(test_input, force_single_line=True) == test_input + assert isort.code(test_input, force_single_line=True) == test_input def test_ensure_line_endings_are_preserved_issue_493() -> None: """Test to ensure line endings are not converted""" test_input = "from os import defpath\r\nfrom os import pathsep as separator\r\n" - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input test_input = "from os import defpath\rfrom os import pathsep as separator\r" - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input test_input = "from os import defpath\nfrom os import pathsep as separator\n" - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input def test_not_splitted_sections() -> None: @@ -2873,19 +2835,19 @@ def test_not_splitted_sections() -> None: + statement ) - assert api.sort_code_string(test_input, known_first_party=["app"]) == test_input - assert api.sort_code_string( - test_input, no_lines_before=["LOCALFOLDER"], known_first_party=["app"] - ) == (stdlib_section + whiteline + firstparty_section + local_section + whiteline + statement) + assert isort.code(test_input, known_first_party=["app"]) == test_input + assert isort.code(test_input, no_lines_before=["LOCALFOLDER"], known_first_party=["app"]) == ( + stdlib_section + whiteline + firstparty_section + local_section + whiteline + statement + ) # by default STDLIB and FIRSTPARTY sections are split by THIRDPARTY section, # so don't merge them if THIRDPARTY imports aren't exist assert ( - api.sort_code_string(test_input, no_lines_before=["FIRSTPARTY"], known_first_party=["app"]) + isort.code(test_input, no_lines_before=["FIRSTPARTY"], known_first_party=["app"]) == test_input ) # in case when THIRDPARTY section is excluded from sections list, # it's ok to merge STDLIB and FIRSTPARTY - assert api.sort_code_string( + assert isort.code( code=test_input, sections=["STDLIB", "FIRSTPARTY", "LOCALFOLDER"], no_lines_before=["FIRSTPARTY"], @@ -2893,15 +2855,14 @@ def test_not_splitted_sections() -> None: ) == (stdlib_section + firstparty_section + whiteline + local_section + whiteline + statement) # it doesn't change output, because stdlib packages don't have any whitelines before them assert ( - api.sort_code_string(test_input, no_lines_before=["STDLIB"], known_first_party=["app"]) - == test_input + isort.code(test_input, no_lines_before=["STDLIB"], known_first_party=["app"]) == test_input ) def test_no_lines_before_empty_section() -> None: test_input = "import first\nimport custom\n" assert ( - api.sort_code_string( + isort.code( code=test_input, known_third_party=["first"], known_custom=["custom"], @@ -2918,18 +2879,14 @@ def test_no_inline_sort() -> None: If `--force-single-line-imports` flag is enabled, then `--no-inline-sort` is ignored. """ test_input = "from foo import a, c, b\n" + assert isort.code(test_input, no_inline_sort=True, force_single_line=False) == test_input assert ( - api.sort_code_string(test_input, no_inline_sort=True, force_single_line=False) == test_input - ) - assert ( - api.sort_code_string(test_input, no_inline_sort=False, force_single_line=False) + isort.code(test_input, no_inline_sort=False, force_single_line=False) == "from foo import a, b, c\n" ) expected = "from foo import a\nfrom foo import b\nfrom foo import c\n" - assert ( - api.sort_code_string(test_input, no_inline_sort=False, force_single_line=True) == expected - ) - assert api.sort_code_string(test_input, no_inline_sort=True, force_single_line=True) == expected + assert isort.code(test_input, no_inline_sort=False, force_single_line=True) == expected + assert isort.code(test_input, no_inline_sort=True, force_single_line=True) == expected def test_relative_import_of_a_module() -> None: @@ -2956,32 +2913,32 @@ def test_relative_import_of_a_module() -> None: "from six.moves import asd\n" ) - sorted_result = api.sort_code_string(test_input, force_single_line=True) + sorted_result = isort.code(test_input, force_single_line=True) assert sorted_result == expected_results def test_escaped_parens_sort() -> None: test_input = "from foo import \\ \n(a,\nb,\nc)\n" expected = "from foo import a, b, c\n" - assert api.sort_code_string(test_input) == expected + assert isort.code(test_input) == expected def test_escaped_parens_sort_with_comment() -> None: test_input = "from foo import \\ \n(a,\nb,# comment\nc)\n" expected = "from foo import b # comment\nfrom foo import a, c\n" - assert api.sort_code_string(test_input) == expected + assert isort.code(test_input) == expected def test_escaped_parens_sort_with_first_comment() -> None: test_input = "from foo import \\ \n(a,# comment\nb,\nc)\n" expected = "from foo import a # comment\nfrom foo import b, c\n" - assert api.sort_code_string(test_input) == expected + assert isort.code(test_input) == expected def test_escaped_no_parens_sort_with_first_comment() -> None: test_input = "from foo import a, \\\nb, \\\nc # comment\n" expected = "from foo import c # comment\nfrom foo import a, b\n" - assert api.sort_code_string(test_input) == expected + assert isort.code(test_input) == expected def test_is_python_file_ioerror(tmpdir) -> None: @@ -3025,7 +2982,7 @@ def test_to_ensure_imports_are_brought_to_top_issue_651() -> None: "multiline text\n" '"""\n' ) - assert api.sort_code_string(test_input) == expected_output + assert isort.code(test_input) == expected_output def test_to_ensure_importing_from_imports_module_works_issue_662() -> None: @@ -3037,7 +2994,7 @@ def test_to_ensure_importing_from_imports_module_works_issue_662() -> None: " warn(description=description or qualname(fun), deprecation=deprecation, " "removal=removal)\n" ) - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input def test_to_ensure_no_unexpected_changes_issue_666() -> None: @@ -3056,12 +3013,12 @@ def test_to_ensure_no_unexpected_changes_issue_666() -> None: "from django.utils.translation import ugettext_lazy as _\n" '"""\n' ) - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input def test_to_ensure_tabs_dont_become_space_issue_665() -> None: test_input = "import os\n\n\ndef my_method():\n\tpass\n" - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input def test_new_lines_are_preserved() -> None: @@ -3134,7 +3091,7 @@ def test_forced_separate_is_deterministic_issue_774(tmpdir) -> None: "from separate4 import quux\n" ) - assert api.sort_code_string(test_input, settings_file=config_file.strpath) == test_input + assert isort.code(test_input, settings_file=config_file.strpath) == test_input def test_monkey_patched_urllib() -> None: @@ -3269,7 +3226,7 @@ def test_comments_not_removed_issue_576() -> None: "# this comment is important and should not be removed\n" "from sys import api_version as api_version\n" ) - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input def test_reverse_relative_imports_issue_417() -> None: @@ -3287,10 +3244,7 @@ def test_reverse_relative_imports_issue_417() -> None: "from ...eu import dignissim\n" "from ...ex import metus\n" ) - assert ( - api.sort_code_string(test_input, force_single_line=True, reverse_relative=True) - == test_input - ) + assert isort.code(test_input, force_single_line=True, reverse_relative=True) == test_input def test_inconsistent_relative_imports_issue_577() -> None: @@ -3308,26 +3262,26 @@ def test_inconsistent_relative_imports_issue_577() -> None: "from .dolor import consecteur\n" "from .sit import apidiscing\n" ) - assert api.sort_code_string(test_input, force_single_line=True) == test_input + assert isort.code(test_input, force_single_line=True) == test_input def test_unwrap_issue_762() -> None: test_input = "from os.path \\\nimport (join, split)\n" - assert api.sort_code_string(test_input) == "from os.path import join, split\n" + assert isort.code(test_input) == "from os.path import join, split\n" test_input = "from os.\\\n path import (join, split)" - assert api.sort_code_string(test_input) == "from os.path import join, split\n" + assert isort.code(test_input) == "from os.path import join, split\n" def test_multiple_as_imports() -> None: test_input = "from a import b as b\nfrom a import b as bb\nfrom a import b as bb_\n" - test_output = api.sort_code_string(test_input) + test_output = isort.code(test_input) assert test_output == test_input - test_output = api.sort_code_string(test_input, combine_as_imports=True) + test_output = isort.code(test_input, combine_as_imports=True) assert test_output == "from a import b as b, b as bb, b as bb_\n" - test_output = api.sort_code_string(test_input, keep_direct_and_as_imports=True) + test_output = isort.code(test_input, keep_direct_and_as_imports=True) assert test_output == test_input - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, combine_as_imports=True, keep_direct_and_as_imports=True ) assert test_output == "from a import b as b, b as bb, b as bb_\n" @@ -3338,15 +3292,15 @@ def test_multiple_as_imports() -> None: "from a import b as bb\n" "from a import b as bb_\n" ) - test_output = api.sort_code_string(test_input, keep_direct_and_as_imports=False) + test_output = isort.code(test_input, keep_direct_and_as_imports=False) assert test_output == "from a import b as b\nfrom a import b as bb\nfrom a import b as bb_\n" - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, combine_as_imports=True, keep_direct_and_as_imports=False ) assert test_output == "from a import b as b, b as bb, b as bb_\n" - test_output = api.sort_code_string(test_input, keep_direct_and_as_imports=True) + test_output = isort.code(test_input, keep_direct_and_as_imports=True) assert test_output == test_input - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, combine_as_imports=True, keep_direct_and_as_imports=True ) assert test_output == "from a import b, b as b, b as bb, b as bb_\n" @@ -3357,40 +3311,36 @@ def test_multiple_as_imports() -> None: "from a import b\n" "from a import b as f\n" ) - test_output = api.sort_code_string(test_input, keep_direct_and_as_imports=False) + test_output = isort.code(test_input, keep_direct_and_as_imports=False) assert test_output == "from a import b as c\nfrom a import b as e\nfrom a import b as f\n" - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, combine_as_imports=True, keep_direct_and_as_imports=False ) assert test_output == "from a import b as c, b as e, b as f\n" - test_output = api.sort_code_string(test_input, keep_direct_and_as_imports=True) + test_output = isort.code(test_input, keep_direct_and_as_imports=True) assert ( test_output == "from a import b\nfrom a import b as c\nfrom a import b as e\nfrom a import b as f\n" ) - test_output = api.sort_code_string( - code=test_input, no_inline_sort=True, keep_direct_and_as_imports=False - ) + test_output = isort.code(code=test_input, no_inline_sort=True, keep_direct_and_as_imports=False) assert test_output == "from a import b as c\nfrom a import b as e\nfrom a import b as f\n" - test_output = api.sort_code_string( - code=test_input, keep_direct_and_as_imports=True, no_inline_sort=True - ) + test_output = isort.code(code=test_input, keep_direct_and_as_imports=True, no_inline_sort=True) assert ( test_output == "from a import b\nfrom a import b as c\nfrom a import b as e\nfrom a import b as f\n" ) - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, combine_as_imports=True, keep_direct_and_as_imports=True ) assert test_output == "from a import b, b as c, b as e, b as f\n" - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, combine_as_imports=True, no_inline_sort=True, keep_direct_and_as_imports=False, ) assert test_output == "from a import b as e, b as c, b as f\n" - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, combine_as_imports=True, keep_direct_and_as_imports=True, @@ -3399,17 +3349,17 @@ def test_multiple_as_imports() -> None: assert test_output == "from a import b, b as e, b as c, b as f\n" test_input = "import a as a\nimport a as aa\nimport a as aa_\n" - test_output = api.sort_code_string(test_input, keep_direct_and_as_imports=False) + test_output = isort.code(test_input, keep_direct_and_as_imports=False) assert test_output == test_input - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, combine_as_imports=True, keep_direct_and_as_imports=True ) assert test_output == test_input test_input = "import a\nimport a as a\nimport a as aa\nimport a as aa_\n" - test_output = api.sort_code_string(test_input, keep_direct_and_as_imports=False) + test_output = isort.code(test_input, keep_direct_and_as_imports=False) assert test_output == "import a as a\nimport a as aa\nimport a as aa_\n" - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, combine_as_imports=True, keep_direct_and_as_imports=True ) assert test_output == test_input @@ -3426,7 +3376,7 @@ def test_all_imports_from_single_module() -> None: "from a import b as c, g as h\n" "from a import e as f\n" ) - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, combine_star=False, combine_as_imports=False, @@ -3444,7 +3394,7 @@ def test_all_imports_from_single_module() -> None: "from a import i as j\n" "from a import w, x, y, z\n" ) - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, combine_star=True, combine_as_imports=False, @@ -3453,7 +3403,7 @@ def test_all_imports_from_single_module() -> None: no_inline_sort=False, ) assert test_output == "import a\nfrom a import *\n" - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, combine_star=False, combine_as_imports=True, @@ -3466,7 +3416,7 @@ def test_all_imports_from_single_module() -> None: "from a import *\n" "from a import b as c, b as d, e as f, g as h, i as j, w, x, y, z\n" ) - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, combine_star=False, combine_as_imports=False, @@ -3485,7 +3435,7 @@ def test_all_imports_from_single_module() -> None: "from a import i as j\n" "from a import w, x, y, z\n" ) - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, combine_star=False, combine_as_imports=False, @@ -3506,7 +3456,7 @@ def test_all_imports_from_single_module() -> None: "from a import y\n" "from a import z\n" ) - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, combine_star=False, combine_as_imports=False, @@ -3524,7 +3474,7 @@ def test_all_imports_from_single_module() -> None: "from a import g as h\n" "from a import e as f\n" ) - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, combine_star=True, combine_as_imports=True, @@ -3533,7 +3483,7 @@ def test_all_imports_from_single_module() -> None: no_inline_sort=False, ) assert test_output == "import a\nfrom a import *\n" - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, combine_star=True, combine_as_imports=False, @@ -3542,7 +3492,7 @@ def test_all_imports_from_single_module() -> None: no_inline_sort=False, ) assert test_output == "import a\nfrom a import *\n" - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, combine_star=True, combine_as_imports=False, @@ -3551,7 +3501,7 @@ def test_all_imports_from_single_module() -> None: no_inline_sort=False, ) assert test_output == "import a\nfrom a import *\n" - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, combine_star=True, combine_as_imports=False, @@ -3560,7 +3510,7 @@ def test_all_imports_from_single_module() -> None: no_inline_sort=True, ) assert test_output == "import a\nfrom a import *\n" - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, combine_star=False, combine_as_imports=True, @@ -3573,7 +3523,7 @@ def test_all_imports_from_single_module() -> None: "from a import *\n" "from a import b, b as c, b as d, e as f, g as h, i as j, w, x, y, z\n" ) - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, combine_star=False, combine_as_imports=True, @@ -3594,7 +3544,7 @@ def test_all_imports_from_single_module() -> None: "from a import y\n" "from a import z\n" ) - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, combine_star=False, combine_as_imports=True, @@ -3607,7 +3557,7 @@ def test_all_imports_from_single_module() -> None: "from a import *\n" "from a import b as d, b as c, z, x, y, w, i as j, g as h, e as f\n" ) - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, combine_star=False, combine_as_imports=False, @@ -3629,7 +3579,7 @@ def test_all_imports_from_single_module() -> None: "from a import y\n" "from a import z\n" ) - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, combine_star=False, combine_as_imports=False, @@ -3648,7 +3598,7 @@ def test_all_imports_from_single_module() -> None: "from a import g as h\n" "from a import e as f\n" ) - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, combine_star=False, combine_as_imports=False, @@ -3669,7 +3619,7 @@ def test_all_imports_from_single_module() -> None: "from a import y\n" "from a import z\n" ) - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, combine_star=True, combine_as_imports=True, @@ -3678,7 +3628,7 @@ def test_all_imports_from_single_module() -> None: no_inline_sort=False, ) assert test_output == "import a\nfrom a import *\n" - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, combine_star=True, combine_as_imports=True, @@ -3687,7 +3637,7 @@ def test_all_imports_from_single_module() -> None: no_inline_sort=False, ) assert test_output == "import a\nfrom a import *\n" - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, combine_star=True, combine_as_imports=True, @@ -3696,7 +3646,7 @@ def test_all_imports_from_single_module() -> None: no_inline_sort=True, ) assert test_output == "import a\nfrom a import *\n" - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, combine_star=True, combine_as_imports=False, @@ -3705,7 +3655,7 @@ def test_all_imports_from_single_module() -> None: no_inline_sort=False, ) assert test_output == "import a\nfrom a import *\n" - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, combine_star=True, combine_as_imports=False, @@ -3714,7 +3664,7 @@ def test_all_imports_from_single_module() -> None: no_inline_sort=True, ) assert test_output == "import a\nfrom a import *\n" - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, combine_star=True, combine_as_imports=False, @@ -3723,7 +3673,7 @@ def test_all_imports_from_single_module() -> None: no_inline_sort=True, ) assert test_output == "import a\nfrom a import *\n" - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, combine_star=False, combine_as_imports=True, @@ -3745,7 +3695,7 @@ def test_all_imports_from_single_module() -> None: "from a import y\n" "from a import z\n" ) - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, combine_star=False, combine_as_imports=True, @@ -3758,7 +3708,7 @@ def test_all_imports_from_single_module() -> None: "from a import *\n" "from a import b, b as d, b as c, z, x, y, w, i as j, g as h, e as f\n" ) - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, combine_star=False, combine_as_imports=False, @@ -3780,7 +3730,7 @@ def test_all_imports_from_single_module() -> None: "from a import y\n" "from a import z\n" ) - test_output = api.sort_code_string( + test_output = isort.code( code=test_input, combine_star=True, combine_as_imports=True, @@ -3811,7 +3761,7 @@ def test_noqa_issue_679() -> None: "import zed # NOQA\n" "import ujson # NOQA\n" ) - assert api.sort_code_string(test_input) == test_output + assert isort.code(test_input) == test_output def test_extract_multiline_output_wrap_setting_from_a_config_file(tmpdir: py.path.local) -> None: @@ -3845,7 +3795,7 @@ def test_ensure_support_for_non_typed_but_cased_alphabetic_sort_issue_890() -> N "from pkg import recorder\n" ) assert ( - api.sort_code_string( + isort.code( code=test_input, case_sensitive=True, order_by_type=False, force_single_line=True ) == expected_output @@ -3854,14 +3804,14 @@ def test_ensure_support_for_non_typed_but_cased_alphabetic_sort_issue_890() -> N def test_to_ensure_empty_line_not_added_to_file_start_issue_889() -> None: test_input = "# comment\nimport os\n# comment2\nimport sys\n" - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input def test_to_ensure_correctly_handling_of_whitespace_only_issue_811(capsys) -> None: test_input = ( "import os\n" "import sys\n" "\n" "\x0c\n" "def my_function():\n" ' print("hi")\n' ) - api.sort_code_string(test_input, ignore_whitespace=True) + isort.code(test_input, ignore_whitespace=True) out, err = capsys.readouterr() assert out == "" assert err == "" @@ -3869,7 +3819,7 @@ def test_to_ensure_correctly_handling_of_whitespace_only_issue_811(capsys) -> No def test_standard_library_deprecates_user_issue_778() -> None: test_input = "import os\n\nimport user\n" - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input def test_settings_path_skip_issue_909(tmpdir) -> None: @@ -3967,8 +3917,8 @@ def test_failing_file_check_916() -> None: "multi_line_output": 3, "lines_after_imports": 2, } # type: Dict[str, Any] - assert api.sort_code_string(test_input, **settings) == expected_output - assert api.sort_code_string(expected_output, **settings) == expected_output + assert isort.code(test_input, **settings) == expected_output + assert isort.code(expected_output, **settings) == expected_output assert api.check_code_string(expected_output, **settings) @@ -3990,7 +3940,7 @@ def test_import_heading_issue_905() -> None: "# Local imports\n" "from oklib.plot_ok import imagesc\n" ) - assert api.sort_code_string(test_input, **config) == test_input + assert isort.code(test_input, **config) == test_input def test_isort_keeps_comments_issue_691() -> None: @@ -4017,7 +3967,7 @@ def test_isort_keeps_comments_issue_691() -> None: "def path(*subdirectories):\n" " return os.path.join(PROJECT_DIR, *subdirectories)\n" ) - assert api.sort_code_string(test_input) == expected_output + assert isort.code(test_input) == expected_output def test_isort_ensures_blank_line_between_import_and_comment() -> None: @@ -4084,29 +4034,27 @@ def test_isort_ensures_blank_line_between_import_and_comment() -> None: "# noinspection PyUnresolvedReferences\n" "from four.b import b as bb\n" ) - assert api.sort_code_string(test_input, **config) == expected_output + assert isort.code(test_input, **config) == expected_output def test_pyi_formatting_issue_942(tmpdir) -> None: test_input = "import os\n\n\ndef my_method():\n" expected_py_output = test_input.splitlines() expected_pyi_output = "import os\n\ndef my_method():\n".splitlines() - assert api.sort_code_string(test_input).splitlines() == expected_py_output - assert api.sort_code_string(test_input, extension="pyi").splitlines() == expected_pyi_output + assert isort.code(test_input).splitlines() == expected_py_output + assert isort.code(test_input, extension="pyi").splitlines() == expected_pyi_output source_py = tmpdir.join("source.py") source_py.write(test_input) assert ( - api.sort_code_string( - code=Path(source_py).read_text(), file_path=Path(source_py) - ).splitlines() + isort.code(code=Path(source_py).read_text(), file_path=Path(source_py)).splitlines() == expected_py_output ) source_pyi = tmpdir.join("source.pyi") source_pyi.write(test_input) assert ( - api.sort_code_string( + isort.code( code=Path(source_pyi).read_text(), extension="pyi", file_path=Path(source_pyi) ).splitlines() == expected_pyi_output @@ -4142,7 +4090,7 @@ def test_move_class_issue_751() -> None: " return item" "\n" ) - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input def test_python_version() -> None: @@ -4156,15 +4104,15 @@ def test_python_version() -> None: assert args["py_version"] == "3" test_input = "import os\n\nimport user\n" - assert api.sort_code_string(test_input, py_version="3") == test_input + assert isort.code(test_input, py_version="3") == test_input # user is part of the standard library in python 2 output_python_2 = "import os\nimport user\n" - assert api.sort_code_string(test_input, py_version="27") == output_python_2 + assert isort.code(test_input, py_version="27") == output_python_2 test_input = "import os\nimport xml" - print(api.sort_code_string(test_input, py_version="all")) + print(isort.code(test_input, py_version="all")) def test_isort_with_single_character_import() -> None: @@ -4174,7 +4122,7 @@ def test_isort_with_single_character_import() -> None: See Issue #376: https://github.com/timothycrosley/isort/issues/376 """ test_input = "from django.db.models import CASCADE, SET_NULL, Q\n" - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input def test_isort_nested_imports() -> None: @@ -4191,7 +4139,7 @@ def import_test(): return True """ assert ( - api.sort_code_string(test_input) + isort.code(test_input) == """ def import_test(): import os @@ -4216,7 +4164,7 @@ def test_isort_off() -> None: from . import local """ - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input def test_isort_split() -> None: @@ -4229,7 +4177,7 @@ def test_isort_split() -> None: import os import sys """ - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input def test_comment_look_alike(): @@ -4245,7 +4193,7 @@ def test_comment_look_alike(): import os ''' assert ( - api.sort_code_string(test_input) + isort.code(test_input) == ''' """This is a multi-line comment @@ -4612,13 +4560,13 @@ def test_cimport_support(): from string_visitor cimport * from web_request_client_cef3 cimport * """ - assert api.sort_code_string(test_input).strip() == expected_output.strip() - assert api.sort_code_string(test_input, old_finders=True).strip() == expected_output.strip() + assert isort.code(test_input).strip() == expected_output.strip() + assert isort.code(test_input, old_finders=True).strip() == expected_output.strip() def test_cdef_support(): assert ( - api.sort_code_string( + isort.code( code=""" from cpython.version cimport PY_MAJOR_VERSION @@ -4636,7 +4584,7 @@ def test_cdef_support(): ) assert ( - api.sort_code_string( + isort.code( code=""" from cpython.version cimport PY_MAJOR_VERSION @@ -4659,7 +4607,7 @@ def test_top_level_import_order() -> None: "from rest_framework import throttling, viewsets\n" "from rest_framework.authentication import TokenAuthentication\n" ) - assert api.sort_code_string(test_input, force_sort_within_sections=True) == test_input + assert isort.code(test_input, force_sort_within_sections=True) == test_input def test_noqa_issue_1065() -> None: @@ -4689,7 +4637,7 @@ def test_noqa_issue_1065() -> None: from flask_security.signals import user_confirmed # noqa from flask_security.signals import user_registered # noqa """ - assert api.sort_code_string(test_input) == expected_output + assert isort.code(test_input) == expected_output def test_single_line_exclusions(): @@ -4705,9 +4653,7 @@ def test_single_line_exclusions(): from typing import List, TypeVar """ assert ( - api.sort_code_string( - code=test_input, force_single_line=True, single_line_exclusions=("typing",) - ) + isort.code(code=test_input, force_single_line=True, single_line_exclusions=("typing",)) == expected_output ) @@ -4719,7 +4665,7 @@ def test_nested_comment_handling(): # comment for bar """ - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input # If comments appear inside import sections at same indentation they can be re-arranged. test_input = """ @@ -4735,7 +4681,7 @@ def test_nested_comment_handling(): import os import sys """ - assert api.sort_code_string(test_input) == expected_output + assert isort.code(test_input) == expected_output # Comments shouldn't be unexpectedly rearranged. See issue #1090. test_input = """ @@ -4749,7 +4695,7 @@ def f(): from b import b """ - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input # Whitespace shouldn't be adjusted for nested imports. See issue #1090. test_input = """ @@ -4758,7 +4704,7 @@ def f(): except ImportError: import bar """ - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input def test_comments_top_of_file(): @@ -4770,7 +4716,7 @@ def test_comments_top_of_file(): # comment 4 from foo import * """ - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input test_input = """# -*- coding: utf-8 -*- @@ -4793,7 +4739,7 @@ def _process_item(self, item, spider): item['inserted_at'] = datetime.now() return item """ - assert api.sort_code_string(test_input) == test_input + assert isort.code(test_input) == test_input def test_multiple_aliases(): @@ -4803,7 +4749,7 @@ def test_multiple_aliases(): import datetime as dt import datetime as dt2 """ - assert api.sort_code_string(keep_direct_and_as_imports=True, code=test_input) == test_input + assert isort.code(keep_direct_and_as_imports=True, code=test_input) == test_input def test_parens_in_comment(): @@ -4813,7 +4759,7 @@ def test_parens_in_comment(): ) """ expected_output = "from foo import bar # (some text in brackets)\n" - assert api.sort_code_string(test_input) == expected_output + assert isort.code(test_input) == expected_output def test_as_imports_mixed(): @@ -4824,7 +4770,7 @@ def test_as_imports_mixed(): expected_output = """from datetime import datetime from datetime import datetime as dt """ - assert api.sort_code_string(test_input, keep_direct_and_as_imports=True) == expected_output + assert isort.code(test_input, keep_direct_and_as_imports=True) == expected_output def test_no_sections_with_future(): @@ -4836,7 +4782,7 @@ def test_no_sections_with_future(): import os """ - assert api.sort_code_string(test_input, no_sections=True) == expected_output + assert isort.code(test_input, no_sections=True) == expected_output def test_no_sections_with_as_import(): @@ -4844,7 +4790,7 @@ def test_no_sections_with_as_import(): test_input = """import oumpy as np import sympy """ - assert api.sort_code_string(test_input, no_sections=True) == test_input + assert isort.code(test_input, no_sections=True) == test_input def test_no_lines_too_long(): @@ -4860,7 +4806,7 @@ def test_no_lines_too_long(): from package2 import \\ first_package """ - assert api.sort_code_string(test_input, line_length=25, multi_line_output=2) == expected_output + assert isort.code(test_input, line_length=25, multi_line_output=2) == expected_output def test_python_future_category(): @@ -4909,7 +4855,7 @@ def test_python_future_category(): from .query_elastic import QueryElastic """ assert ( - api.sort_code_string( + isort.code( code=test_input, force_grid_wrap=False, include_trailing_comma=True, @@ -4947,4 +4893,10 @@ def test_combine_star_comments_above(): # my future comment from future import * """ - assert api.sort_code_string(input_text, combine_star=True) == expected_output + assert isort.code(input_text, combine_star=True) == expected_output + + +def test_deprecated_settings(): + """Test to ensure isort warns when deprecated settings are used, but doesn't fail to run""" + with pytest.warns(UserWarning): + assert isort.code("hi", not_skip=True) From 25f2b5c2058661c7241c119a83bd843fda7f0666 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 7 Jul 2020 23:46:41 -0700 Subject: [PATCH 0737/1439] Bump version to 5.0.5 --- isort/_version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/_version.py b/isort/_version.py index af29f3407..9fff231c6 100644 --- a/isort/_version.py +++ b/isort/_version.py @@ -1 +1 @@ -__version__ = "5.0.4" +__version__ = "5.0.5" diff --git a/pyproject.toml b/pyproject.toml index 6eec956c7..34137b0a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ line-length = 100 [tool.poetry] name = "isort" -version = "5.0.4" +version = "5.0.5" description = "A Python utility / library to sort Python imports." authors = ["Timothy Crosley "] license = "MIT" From 07a4d82b357afb462c7d51531439cddb22278fb0 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 7 Jul 2020 23:53:35 -0700 Subject: [PATCH 0738/1439] Print used isort versin to console for live editor, use black profile by default --- docs/quick_start/0.-try.md | 2 +- docs/quick_start/interactive.js | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/quick_start/0.-try.md b/docs/quick_start/0.-try.md index 52e207f7f..d2c32f6f6 100644 --- a/docs/quick_start/0.-try.md +++ b/docs/quick_start/0.-try.md @@ -35,7 +35,7 @@ import b, a  Configuration (Note: the below must follow JSON format). Full configuration guide is here:
{"line_length": 80, - "multi_line_output": 2, + "profile": "black", "atomic": true }
diff --git a/docs/quick_start/interactive.js b/docs/quick_start/interactive.js index 328df20de..8ecd3eea0 100644 --- a/docs/quick_start/interactive.js +++ b/docs/quick_start/interactive.js @@ -26,8 +26,6 @@ window.addEventListener('load', () => { languagePluginLoader.then(() => { return pyodide.loadPackage(['micropip']) }).then(() => { - console.log(pyodide.runPython('import sys\nsys.version')); - console.log(pyodide.runPython('print(1 + 2)')); pyodide.runPython(` import micropip @@ -37,7 +35,8 @@ def use_isort(*args): import isort import json import textwrap - print(isort.code("import b; import a")) + + print(f"Using {isort.__version__} of isort.") def sort_code(code, configuration): try: @@ -52,5 +51,5 @@ def use_isort(*args): document.sort_code = sort_code document.updateOutput() -micropip.install('https://timothycrosley.github.io/isort/docs/quick_start/isort-5.0.1-py3-none-any.whl').then(use_isort)`); +micropip.install('isort').then(use_isort)`); }); From e00418befcd170d1040daec42f82cbcfb0418ce5 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 8 Jul 2020 00:35:10 -0700 Subject: [PATCH 0739/1439] Only show examples if present in docs, at least until examples are more prelevant --- docs/configuration/options.md | 158 ++++++++++++++-------------- scripts/build_config_option_docs.py | 17 +-- 2 files changed, 83 insertions(+), 92 deletions(-) diff --git a/docs/configuration/options.md b/docs/configuration/options.md index 74e3b4ff3..bec87881e 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -1,7 +1,5 @@ # Configuration options for isort -======== - As a code formatter isort has opinions. However, it also allows you to have your own. If your opinions disagree with those of isort, isort will disagree but commit to your way of formatting. To enable this, isort exposes a plethora of options to specify how you want your imports sorted, organized, and formatted. @@ -20,7 +18,7 @@ Tells isort to set the known standard library based on the the specified Python - --py - --python-version -**No Examples** + ## Force To Top @@ -34,7 +32,7 @@ Force specific imports to the top of their appropriate section. - -t - --top -**No Examples** + ## Skip @@ -48,7 +46,7 @@ Files that sort imports should skip over. If you want to skip multiple files you - -s - --skip -**No Examples** + ## Skip Glob @@ -62,7 +60,7 @@ Files that sort imports should skip over. - --sg - --skip-glob -**No Examples** + ## Line Length @@ -78,7 +76,7 @@ The max length of an import line (used for wrapping long imports). - --line-length - --line-width -**No Examples** + ## Wrap Length @@ -93,7 +91,7 @@ NOTE: wrap_length must be LOWER than or equal to line_length. - --wl - --wrap-length -**No Examples** + ## Line Ending @@ -107,7 +105,7 @@ Forces line endings to the specified value. If not set, values will be guessed p - --le - --line-ending -**No Examples** + ## Sections @@ -120,7 +118,7 @@ Forces line endings to the specified value. If not set, values will be guessed p - **Not Supported** -**No Examples** + ## No Sections @@ -134,7 +132,7 @@ Put all imports into the same section bucket - --ds - --no-sections -**No Examples** + ## Known Future Library @@ -148,7 +146,7 @@ Force isort to recognize a module as part of the future compatibility libraries. - -f - --future -**No Examples** + ## Known Third Party @@ -162,7 +160,7 @@ Force isort to recognize a module as being part of a third party library. - -o - --thirdparty -**No Examples** + ## Known First Party @@ -176,7 +174,7 @@ Force isort to recognize a module as being part of the current python project. - -p - --project -**No Examples** + ## Known Standard Library @@ -190,7 +188,7 @@ Force isort to recognize a module as part of Python's standard library. - -b - --builtin -**No Examples** + ## Extra Standard Library @@ -203,7 +201,7 @@ Extra modules to be included in the list of ones in Python's standard library. - --extra-builtin -**No Examples** + ## Known Other @@ -218,7 +216,7 @@ Extra modules to be included in the list of ones in Python's standard library. **Examples:** -No example `.isort.cfg` + ### Example `pyproject.toml` @@ -228,7 +226,10 @@ sections = ['FUTURE', 'STDLIB', 'THIRDPARTY', 'AIRFLOW', 'FIRSTPARTY', 'LOCALFOL known_airflow = ['airflow'] ``` -No example cli usage + +### Example cli usage +`` + ## Multi Line Output @@ -242,7 +243,7 @@ Multi line output (0-grid, 1-vertical, 2-hanging, 3-vert-hanging, 4-vert-grid, 5 - -m - --multi-line -**No Examples** + ## Forced Separate @@ -255,7 +256,7 @@ Multi line output (0-grid, 1-vertical, 2-hanging, 3-vert-hanging, 4-vert-grid, 5 - **Not Supported** -**No Examples** + ## Indent @@ -269,7 +270,7 @@ String to place for indents defaults to " " (4 spaces). - -i - --indent -**No Examples** + ## Comment Prefix @@ -282,7 +283,7 @@ String to place for indents defaults to " " (4 spaces). - **Not Supported** -**No Examples** + ## Length Sort @@ -296,7 +297,7 @@ Sort imports by their string length. - --ls - --length-sort -**No Examples** + ## Length Sort Sections @@ -309,7 +310,7 @@ Sort imports by their string length. - **Not Supported** -**No Examples** + ## Add Imports @@ -323,7 +324,7 @@ Adds the specified import line to all files, automatically determining correct p - -a - --add-import -**No Examples** + ## Remove Imports @@ -337,7 +338,7 @@ Removes the specified import from all files. - --rm - --remove-import -**No Examples** + ## Reverse Relative @@ -351,7 +352,7 @@ Reverse order of relative imports. - --rr - --reverse-relative -**No Examples** + ## Force Single Line @@ -365,7 +366,7 @@ Forces all from imports to appear on their own line - --sl - --force-single-line-imports -**No Examples** + ## Single Line Exclusions @@ -379,10 +380,11 @@ One or more modules to exclude from the single line rule. - --nsl - --single-line-exclusions -**No Examples** + ## Default Section -Sets the default section for imports ('FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER') + +Sets the default section for import options: ('FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER') **Type:** String **Default:** `THIRDPARTY` @@ -392,7 +394,7 @@ Sets the default section for imports ('FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPA - --sd - --section-default -**No Examples** + ## Import Headings @@ -405,7 +407,7 @@ Sets the default section for imports ('FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPA - **Not Supported** -**No Examples** + ## Balanced Wrapping @@ -419,7 +421,7 @@ Balances wrapping to produce the most consistent line length possible - -e - --balanced -**No Examples** + ## Use Parentheses @@ -433,7 +435,7 @@ Use parenthesis for line continuation on length limit instead of slashes. - --up - --use-parentheses -**No Examples** + ## Order By Type @@ -447,7 +449,7 @@ Order imports by type in addition to alphabetically - --ot - --order-by-type -**No Examples** + ## Atomic @@ -461,7 +463,7 @@ Ensures the output doesn't save if the resulting file contains syntax errors. - --ac - --atomic -**No Examples** + ## Lines After Imports @@ -475,7 +477,7 @@ Ensures the output doesn't save if the resulting file contains syntax errors. - --lai - --lines-after-imports -**No Examples** + ## Lines Between Sections @@ -488,7 +490,7 @@ Ensures the output doesn't save if the resulting file contains syntax errors. - **Not Supported** -**No Examples** + ## Lines Between Types @@ -502,7 +504,7 @@ Ensures the output doesn't save if the resulting file contains syntax errors. - --lbt - --lines-between-types -**No Examples** + ## Combine As Imports @@ -516,7 +518,7 @@ Combines as imports on the same line. - --ca - --combine-as -**No Examples** + ## Combine Star @@ -530,7 +532,7 @@ Ensures that if a star import is present, nothing else is imported from that nam - --cs - --combine-star -**No Examples** + ## Keep Direct And As Imports @@ -544,7 +546,7 @@ Turns off default behavior that removes direct imports when as imports exist. - -k - --keep-direct-and-as -**No Examples** + ## Include Trailing Comma @@ -558,7 +560,7 @@ Includes a trailing comma on multi line imports that include parentheses. - --tc - --trailing-comma -**No Examples** + ## From First @@ -572,7 +574,7 @@ Switches the typical ordering preference, showing from imports first then straig - --ff - --from-first -**No Examples** + ## Verbose @@ -586,7 +588,7 @@ Shows verbose output, such as when files are skipped or when a check is successf - -v - --verbose -**No Examples** + ## Quiet @@ -600,7 +602,7 @@ Shows extra quiet output, only errors are outputted. - -q - --quiet -**No Examples** + ## Force Adds @@ -614,7 +616,7 @@ Forces import adds even if the original file is empty. - --af - --force-adds -**No Examples** + ## Force Alphabetical Sort Within Sections @@ -628,7 +630,7 @@ Force all imports to be sorted alphabetically within a section - --fass - --force-alphabetical-sort-within-sections -**No Examples** + ## Force Alphabetical Sort @@ -642,7 +644,7 @@ Force all imports to be sorted as a single section - --fas - --force-alphabetical-sort -**No Examples** + ## Force Grid Wrap @@ -656,7 +658,7 @@ Force number of from imports (defaults to 2) to be grid wrapped regardless of li - --fgw - --force-grid-wrap -**No Examples** + ## Force Sort Within Sections @@ -670,7 +672,7 @@ Force imports to be sorted by module, independent of import_type - --fss - --force-sort-within-sections -**No Examples** + ## Lexicographical @@ -683,7 +685,7 @@ Force imports to be sorted by module, independent of import_type - **Not Supported** -**No Examples** + ## Ignore Whitespace @@ -697,7 +699,7 @@ Tells isort to ignore whitespace differences when --check-only is being used. - --ws - --ignore-whitespace -**No Examples** + ## No Lines Before @@ -711,7 +713,7 @@ Sections which should not be split with previous by empty lines - --nlb - --no-lines-before -**No Examples** + ## No Inline Sort @@ -725,7 +727,7 @@ Leaves `from` imports with multiple imports 'as-is' (e.g. `from foo import a, c - --nis - --no-inline-sort -**No Examples** + ## Ignore Comments @@ -738,7 +740,7 @@ Leaves `from` imports with multiple imports 'as-is' (e.g. `from foo import a, c - **Not Supported** -**No Examples** + ## Case Sensitive @@ -751,7 +753,7 @@ Tells isort to include casing when sorting module names - --case-sensitive -**No Examples** + ## Sources @@ -764,7 +766,7 @@ Tells isort to include casing when sorting module names - **Not Supported** -**No Examples** + ## Virtual Env @@ -777,7 +779,7 @@ Virtual environment to use for determining whether a package is third-party - --virtual-env -**No Examples** + ## Conda Env @@ -790,7 +792,7 @@ Conda environment to use for determining whether a package is third-party - --conda-env -**No Examples** + ## Ensure Newline Before Comments @@ -804,7 +806,7 @@ Inserts a blank line before a comment following an import. - -n - --ensure-newline-before-comments -**No Examples** + ## Directory @@ -817,7 +819,7 @@ Inserts a blank line before a comment following an import. - **Not Supported** -**No Examples** + ## Profile @@ -830,7 +832,7 @@ Base profile type to use for configuration. - --profile -**No Examples** + ## Src Paths @@ -844,7 +846,7 @@ Add an explicitly defined source path (modules within src paths have their impor - --src - --src-path -**No Examples** + ## Old Finders @@ -858,7 +860,7 @@ Use the old deprecated finder logic that relies on environment introspection mag - --old-finders - --magic-placement -**No Examples** + ## Check @@ -873,7 +875,7 @@ Checks the file for unsorted / unformatted imports and prints them to the comman - --check-only - --check -**No Examples** + ## Write To Stdout @@ -887,7 +889,7 @@ Force resulting output to stdout, instead of in-place. - -d - --stdout -**No Examples** + ## Show Diff @@ -901,7 +903,7 @@ Prints a diff of all the changes isort would make to a file, instead of changing - --df - --diff -**No Examples** + ## Jobs @@ -915,7 +917,7 @@ Number of files to process in parallel. - -j - --jobs -**No Examples** + ## Dont Order By Type @@ -929,7 +931,7 @@ Don't order imports by type in addition to alphabetically - --dt - --dont-order-by-type -**No Examples** + ## Settings Path @@ -945,7 +947,7 @@ Explicitly set the settings path or file instead of auto determining based on fi - --settings-file - --settings -**No Examples** + ## Show Version @@ -959,7 +961,7 @@ Displays the currently installed version of isort. - -V - --version -**No Examples** + ## Version Number @@ -973,7 +975,7 @@ Returns just the current version number without the logo - --vn - --version-number -**No Examples** + ## Filter Files @@ -986,7 +988,7 @@ Tells isort to filter files even when they are explicitly passed in as part of t - --filter-files -**No Examples** + ## Files @@ -999,7 +1001,7 @@ One or more Python source files that need their imports sorted. - -**No Examples** + ## Ask To Apply @@ -1012,7 +1014,7 @@ Tells isort to apply changes interactively. - --interactive -**No Examples** + ## Show Config @@ -1025,7 +1027,7 @@ See isort's determined config, as well as sources of config options. - --show-config -**No Examples** + ## Deprecated Flags @@ -1038,4 +1040,4 @@ See isort's determined config, as well as sources of config options. - --apply -**No Examples** + diff --git a/scripts/build_config_option_docs.py b/scripts/build_config_option_docs.py index 927c858b4..d7b0c6434 100755 --- a/scripts/build_config_option_docs.py +++ b/scripts/build_config_option_docs.py @@ -17,8 +17,6 @@ COLUMNS = ["Name", "Type", "Default", "Python / Config file", "CLI", "Description"] HEADER = """# Configuration options for isort -======== - As a code formatter isort has opinions. However, it also allows you to have your own. If your opinions disagree with those of isort, isort will disagree but commit to your way of formatting. To enable this, isort exposes a plethora of options to specify how you want your imports sorted, organized, and formatted. @@ -42,12 +40,8 @@ class ConfigOption: example_cli: str = "" def __post_init__(self): - if self.example_cfg == "" and self.example_pyproject_toml == "" and self.example_cli == "": - self.example_section = "**No Examples**" - else: - if self.example_cfg == "": - self.example_cfg = "No example `.isort.cfg`" - else: + if self.example_cfg or self.example_pyproject_toml or self.example_cli: + if self.example_cfg: self.example_cfg = textwrap.dedent( f""" ### Example `.isort.cfg` @@ -58,9 +52,7 @@ def __post_init__(self): """ ) - if self.example_pyproject_toml == "": - self.example_pyproject_toml = "No example pyproject.toml" - else: + if self.example_pyproject_toml: self.example_pyproject_toml = textwrap.dedent( f""" ### Example `pyproject.toml` @@ -70,11 +62,8 @@ def __post_init__(self): ``` """ ) - print(self.example_pyproject_toml) if self.example_cli == "": - self.example_cli = "No example cli usage" - else: self.example_cli = textwrap.dedent( f""" ### Example cli usage From 2e21943eacb6e35a854c0caaea77ca36e5cf909f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 8 Jul 2020 21:03:46 -0700 Subject: [PATCH 0740/1439] Fixed --- CHANGELOG.md | 4 ++++ isort/wrap.py | 4 +++- tests/test_regressions.py | 19 +++++++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebfa905ac..6a89fb271 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ Changelog ========= NOTE: isort follows the [semver](https://semver.org/) versioning standard. + +### 5.0.6 July 8, 2020 + - Fixed #1302: comments and --trailing-comma can generate invalid code + ### 5.0.5 July 7, 2020 - Fixed #1285: packaging issue with bundling tests via poetry. - Fixed #1284: Regression when sorting `.pyi` files from CLI using black profile. diff --git a/isort/wrap.py b/isort/wrap.py index 2265218ad..872b096e7 100644 --- a/isort/wrap.py +++ b/isort/wrap.py @@ -76,7 +76,9 @@ def line(content: str, line_separator: str, config: Config = DEFAULT_CONFIG) -> ): line_parts = re.split(exp, line_without_comment) if comment: - _comma_maybe = "," if config.include_trailing_comma else "" + _comma_maybe = ( + "," if (config.include_trailing_comma and config.use_parentheses) else "" + ) line_parts[-1] = f"{line_parts[-1].strip()}{_comma_maybe} #{comment}" next_line = [] while (len(content) + 2) > ( diff --git a/tests/test_regressions.py b/tests/test_regressions.py index 69494f307..a34a82ebb 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -183,3 +183,22 @@ def test_ensure_new_line_before_comments_mixed_with_ensure_newline_before_commen """ assert isort.code(test_input, profile="black") == test_input assert isort.code(test_input, profile="black", force_sort_within_sections=True) == test_input + + +def test_trailing_comma_doesnt_introduce_broken_code_with_comment_and_wrap_issue_1302(): + """Tests to assert the combination of include_trailing_comma and a wrapped line doesnt break. + See: https://github.com/timothycrosley/isort/issues/1302. + """ + assert ( + isort.code( + """ +from somewhere import very_very_very_very_very_very_long_symbol # some comment +""", + line_length=50, + include_trailing_comma=True, + ) + == """ +from somewhere import \\ + very_very_very_very_very_very_long_symbol # some comment +""" + ) From 340713cbe9ad9245eb9f42108d39e7bd16a1970f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 8 Jul 2020 22:06:16 -0700 Subject: [PATCH 0741/1439] Fixed #1293: extra new line in indented imports, when immediately followed by a comment. --- CHANGELOG.md | 3 ++- isort/api.py | 4 +++- tests/test_regressions.py | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a89fb271..23e0fda38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,8 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. ### 5.0.6 July 8, 2020 - - Fixed #1302: comments and --trailing-comma can generate invalid code + - Fixed #1302: comments and --trailing-comma can generate invalid code. + - Fixed #1293: extra new line in indented imports, when immediately followed by a comment. ### 5.0.5 July 7, 2020 - Fixed #1285: packaging issue with bundling tests via poetry. diff --git a/isort/api.py b/isort/api.py index 350ce196a..856198551 100644 --- a/isort/api.py +++ b/isort/api.py @@ -551,7 +551,9 @@ def _sort_imports( line.lstrip() for line in import_section.split(line_separator) ) out_config = Config( - config=config, line_length=max(config.line_length - len(indent), 0) + config=config, + line_length=max(config.line_length - len(indent), 0), + lines_after_imports=1, ) else: out_config = config diff --git a/tests/test_regressions.py b/tests/test_regressions.py index a34a82ebb..a20f2bf9b 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -151,6 +151,7 @@ def on_email_deleted(self, email): ... """ assert isort.code(test_input) == test_input + assert isort.code(test_input, lines_after_imports=2) == test_input def test_force_single_line_shouldnt_remove_preceding_comment_lines_issue_1296(): From 9ee7a97c5a613c7ed536de5635bf26151ba47f55 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 8 Jul 2020 22:14:39 -0700 Subject: [PATCH 0742/1439] Fixed #1304: isort 5 no longer recognises as a stdlib module. --- CHANGELOG.md | 1 + isort/stdlibs/py27.py | 3 +++ isort/stdlibs/py35.py | 3 +++ isort/stdlibs/py36.py | 3 +++ isort/stdlibs/py37.py | 3 +++ isort/stdlibs/py38.py | 3 +++ isort/stdlibs/py39.py | 3 +++ scripts/mkstdlibs.py | 2 +- tests/test_regressions.py | 7 +++++++ 9 files changed, 27 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23e0fda38..9250c04fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. ### 5.0.6 July 8, 2020 - Fixed #1302: comments and --trailing-comma can generate invalid code. - Fixed #1293: extra new line in indented imports, when immediately followed by a comment. + - Fixed #1304: isort 5 no longer recognises `sre_parse` as a stdlib module. ### 5.0.5 July 7, 2020 - Fixed #1285: packaging issue with bundling tests via poetry. diff --git a/isort/stdlibs/py27.py b/isort/stdlibs/py27.py index 4bd8d9fab..87aa67f1f 100644 --- a/isort/stdlibs/py27.py +++ b/isort/stdlibs/py27.py @@ -238,7 +238,10 @@ "socket", "spwd", "sqlite3", + "sre", + "sre_compile", "sre_constants", + "sre_parse", "ssl", "stat", "statvfs", diff --git a/isort/stdlibs/py35.py b/isort/stdlibs/py35.py index 9055b97b0..274d8a7d1 100644 --- a/isort/stdlibs/py35.py +++ b/isort/stdlibs/py35.py @@ -161,7 +161,10 @@ "socketserver", "spwd", "sqlite3", + "sre", + "sre_compile", "sre_constants", + "sre_parse", "ssl", "stat", "statistics", diff --git a/isort/stdlibs/py36.py b/isort/stdlibs/py36.py index 215460e0e..8ae02a150 100644 --- a/isort/stdlibs/py36.py +++ b/isort/stdlibs/py36.py @@ -162,7 +162,10 @@ "socketserver", "spwd", "sqlite3", + "sre", + "sre_compile", "sre_constants", + "sre_parse", "ssl", "stat", "statistics", diff --git a/isort/stdlibs/py37.py b/isort/stdlibs/py37.py index c65a954f1..0eb1dd6fa 100644 --- a/isort/stdlibs/py37.py +++ b/isort/stdlibs/py37.py @@ -163,7 +163,10 @@ "socketserver", "spwd", "sqlite3", + "sre", + "sre_compile", "sre_constants", + "sre_parse", "ssl", "stat", "statistics", diff --git a/isort/stdlibs/py38.py b/isort/stdlibs/py38.py index 727258eff..9bcea9a16 100644 --- a/isort/stdlibs/py38.py +++ b/isort/stdlibs/py38.py @@ -162,7 +162,10 @@ "socketserver", "spwd", "sqlite3", + "sre", + "sre_compile", "sre_constants", + "sre_parse", "ssl", "stat", "statistics", diff --git a/isort/stdlibs/py39.py b/isort/stdlibs/py39.py index 20b1c00b6..7bcb8f2b7 100644 --- a/isort/stdlibs/py39.py +++ b/isort/stdlibs/py39.py @@ -161,7 +161,10 @@ "socketserver", "spwd", "sqlite3", + "sre", + "sre_compile", "sre_constants", + "sre_parse", "ssl", "stat", "statistics", diff --git a/scripts/mkstdlibs.py b/scripts/mkstdlibs.py index 1d6ff61ef..ee08ba1d0 100755 --- a/scripts/mkstdlibs.py +++ b/scripts/mkstdlibs.py @@ -31,7 +31,7 @@ class FakeApp: invdata = fetch_inventory(FakeApp(), "", url) # Any modules we want to enforce across Python versions stdlib can be included in set init - modules = {"posixpath", "ntpath", "sre_constants"} + modules = {"posixpath", "ntpath", "sre_constants", "sre_parse", "sre_compile", "sre"} for module in invdata["py:module"]: root, *_ = module.split(".") if root not in ["__future__", "__main__"]: diff --git a/tests/test_regressions.py b/tests/test_regressions.py index a20f2bf9b..d650e8724 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -203,3 +203,10 @@ def test_trailing_comma_doesnt_introduce_broken_code_with_comment_and_wrap_issue very_very_very_very_very_very_long_symbol # some comment """ ) + + +def test_ensure_sre_parse_is_identified_as_stdlib_1304(): + """Ensure sre_parse is idenified as STDLIB. + See: https://github.com/timothycrosley/isort/issues/1304. + """ + assert isort.place_module("sre_parse") == isort.place_module("sre") == isort.settings.STDLIB From 7daceb6a9497bb3878bb727d662d4b78621c7b4a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 8 Jul 2020 22:54:39 -0700 Subject: [PATCH 0743/1439] Fixed #1300: add_imports moves comments following import section. --- CHANGELOG.md | 1 + isort/api.py | 4 +++- tests/test_regressions.py | 16 +++++++++++++++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9250c04fb..0c998fe24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. - Fixed #1302: comments and --trailing-comma can generate invalid code. - Fixed #1293: extra new line in indented imports, when immediately followed by a comment. - Fixed #1304: isort 5 no longer recognises `sre_parse` as a stdlib module. + - Fixed #1300: add_imports moves comments following import section. ### 5.0.5 July 7, 2020 - Fixed #1285: packaging issue with bundling tests via poetry. diff --git a/isort/api.py b/isort/api.py index 856198551..e6d392ca8 100644 --- a/isort/api.py +++ b/isort/api.py @@ -527,7 +527,9 @@ def _sort_imports( if import_section: if add_imports and not indent: - import_section += line_separator.join(add_imports) + line_separator + import_section = ( + line_separator.join(add_imports) + line_separator + import_section + ) contains_imports = True add_imports = [] diff --git a/tests/test_regressions.py b/tests/test_regressions.py index d650e8724..3e288568e 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -205,8 +205,22 @@ def test_trailing_comma_doesnt_introduce_broken_code_with_comment_and_wrap_issue ) -def test_ensure_sre_parse_is_identified_as_stdlib_1304(): +def test_ensure_sre_parse_is_identified_as_stdlib_issue_1304(): """Ensure sre_parse is idenified as STDLIB. See: https://github.com/timothycrosley/isort/issues/1304. """ assert isort.place_module("sre_parse") == isort.place_module("sre") == isort.settings.STDLIB + + +def test_add_imports_shouldnt_move_lower_comments_issue_1300(): + """Ensure add_imports doesn't move comments immediately below imports. + See:: https://github.com/timothycrosley/isort/issues/1300. + """ + test_input = """from __future__ import unicode_literals + +from os import path + +# A comment for a constant +ANSWER = 42 +""" + assert isort.code(test_input, add_imports=["from os import path"]) == test_input From 2120836a14beecca88b8d97b97348d1b4ef8b21f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 8 Jul 2020 23:04:19 -0700 Subject: [PATCH 0744/1439] Add additional test cases --- tests/test_isort.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_isort.py b/tests/test_isort.py index e150c069d..4a9fc4014 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -138,6 +138,19 @@ def test_correct_space_between_imports() -> None: test_output_assign = api.sort_code_string(test_input_assign) assert test_output_assign == "import sys\n\nVAR = 1\n" + test_input_assign = "import sys\nVAR = 1\ndef y():\n" + test_output_assign = api.sort_code_string(test_input_assign) + assert test_output_assign == "import sys\n\nVAR = 1\ndef y():\n" + + test_input = """ +import os + +x = "hi" +def x(): + pass +""" + assert isort.code(test_input) == test_input + def test_sort_on_number() -> None: """Ensure numbers get sorted logically (10 > 9 not the other way around)""" From b82969d5698e10b1a40b8a7015a3868fac3fc9c4 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 8 Jul 2020 23:05:13 -0700 Subject: [PATCH 0745/1439] Add Seung Hyeon, Kim (@hyeonjames) to acknowledgements --- CHANGELOG.md | 1 + docs/contributing/4.-acknowledgements.md | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c998fe24..b1be8cefa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. - Fixed #1293: extra new line in indented imports, when immediately followed by a comment. - Fixed #1304: isort 5 no longer recognises `sre_parse` as a stdlib module. - Fixed #1300: add_imports moves comments following import section. + - Fixed #1276: Fix a bug that creates only one line after triple quotes. ### 5.0.5 July 7, 2020 - Fixed #1285: packaging issue with bundling tests via poetry. diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index feb3b8217..1f6133ad2 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -96,6 +96,7 @@ Code Contributors - @r-richmond - Sebastian (@sebix) - Kosei Kitahara (@Surgo) +- Seung Hyeon, Kim (@hyeonjames) Documenters From e0c6175638c76722b437611761a5a5e88331720e Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 8 Jul 2020 23:05:56 -0700 Subject: [PATCH 0746/1439] Bump version to 5.0.6 --- isort/_version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/_version.py b/isort/_version.py index 9fff231c6..599b2cdc3 100644 --- a/isort/_version.py +++ b/isort/_version.py @@ -1 +1 @@ -__version__ = "5.0.5" +__version__ = "5.0.6" diff --git a/pyproject.toml b/pyproject.toml index 34137b0a0..9da676125 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ line-length = 100 [tool.poetry] name = "isort" -version = "5.0.5" +version = "5.0.6" description = "A Python utility / library to sort Python imports." authors = ["Timothy Crosley "] license = "MIT" From f16032d04d58bff8d34498d3b95dbc8b409ba701 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 9 Jul 2020 20:20:32 -0700 Subject: [PATCH 0747/1439] Fixed #1306: unexpected --diff behavior. --- isort/api.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/isort/api.py b/isort/api.py index e6d392ca8..572bc5167 100644 --- a/isort/api.py +++ b/isort/api.py @@ -304,8 +304,9 @@ def sort_file( file_output=tmp_file.read_text(encoding=source_file.encoding), file_path=file_path or source_file.path, ) - if ask_to_apply and not ask_whether_to_apply_changes_to_file( - str(source_file.path) + if show_diff or ( + ask_to_apply + and not ask_whether_to_apply_changes_to_file(str(source_file.path)) ): return source_file.stream.close() From c04215f2885871ce931dab8743ac44841c47d743 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 9 Jul 2020 20:20:38 -0700 Subject: [PATCH 0748/1439] Fixed #1306: unexpected --diff behavior. --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1be8cefa..a067ba911 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. +### 5.0.7 July 9, 2020 + - Fixed #1306: unexpected --diff behavior. + ### 5.0.6 July 8, 2020 - Fixed #1302: comments and --trailing-comma can generate invalid code. - Fixed #1293: extra new line in indented imports, when immediately followed by a comment. From 66ac6c838d45c5f75efecf764e006bf6a5d02f89 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 9 Jul 2020 20:51:31 -0700 Subject: [PATCH 0749/1439] Fixed #1279: Fixed NOQA comment regression. --- CHANGELOG.md | 1 + isort/main.py | 6 ++++++ isort/parse.py | 8 +++++--- isort/settings.py | 1 + tests/test_isort.py | 50 ++++++++++++++++++++++++++++----------------- 5 files changed, 44 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a067ba911..1ddcead0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. ### 5.0.7 July 9, 2020 - Fixed #1306: unexpected --diff behavior. + - Fixed #1279: Fixed NOQA comment regression. ### 5.0.6 July 8, 2020 - Fixed #1302: comments and --trailing-comma can generate invalid code. diff --git a/isort/main.py b/isort/main.py index 116911bf1..3581fb67f 100644 --- a/isort/main.py +++ b/isort/main.py @@ -567,6 +567,12 @@ def _build_arg_parser() -> argparse.ArgumentParser: action="store_true", help="See isort's determined config, as well as sources of config options.", ) + parser.add_argument( + "--honor-noqa", + dest="honor_noqa", + action="store_true", + help="Tells isort to honor noqa comments to enforce skipping those comments.", + ) # deprecated options parser.add_argument( diff --git a/isort/parse.py b/isort/parse.py index 60d5ee4e4..ad19cab72 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -51,9 +51,11 @@ def _normalize_line(raw_line: str) -> Tuple[str, str]: return (line, raw_line) -def import_type(line: str) -> Optional[str]: +def import_type(line: str, config: Config = DEFAULT_CONFIG) -> Optional[str]: """If the current line is an import line it will return its type (from or straight)""" - if "isort:skip" in line or "isort: skip" in line or "NOQA" in line: + if config.honor_noqa and line.lower().rstrip().endswith("noqa"): + return None + elif "isort:skip" in line or "isort: skip" in line: return None elif line.startswith(("import ", "cimport ")): return "straight" @@ -195,7 +197,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte (line.strip() for line in line.split(";")) if ";" in line else (line,) # type: ignore ): line, raw_line = _normalize_line(line) - type_of_import = import_type(line) or "" + type_of_import = import_type(line, config) or "" if not type_of_import: out_lines.append(raw_line) continue diff --git a/isort/settings.py b/isort/settings.py index 5cefd4087..0c61b6bf4 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -149,6 +149,7 @@ class _Config: ensure_newline_before_comments: bool = False directory: str = "" profile: str = "" + honor_noqa: bool = False src_paths: FrozenSet[Path] = frozenset() old_finders: bool = False diff --git a/tests/test_isort.py b/tests/test_isort.py index 4a9fc4014..c13e37a65 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -3773,26 +3773,38 @@ def test_all_imports_from_single_module() -> None: def test_noqa_issue_679() -> None: - # Test to ensure that NOQA notation is being observed as expected - test_input = ( - "import os\n" - "\n" - "import requestsss\n" - "import zed # NOQA\n" - "import ujson # NOQA\n" - "\n" - "import foo" - ) - test_output = ( - "import os\n" - "\n" - "import foo\n" - "import requestsss\n" - "\n" - "import zed # NOQA\n" - "import ujson # NOQA\n" - ) + """Test to ensure that NOQA notation is being observed as expected + if honor_noqa is set to `True` + """ + test_input = """ +import os + +import requestsss +import zed # NOQA +import ujson # NOQA + +import foo""" + test_output = """ +import os + +import foo +import requestsss +import ujson # NOQA +import zed # NOQA +""" + test_output_honor_noqa = """ +import os + +import foo +import requestsss + +import zed # NOQA +import ujson # NOQA +""" assert isort.code(test_input) == test_output + assert isort.code(test_input.lower()) == test_output.lower() + assert isort.code(test_input, honor_noqa=True) == test_output_honor_noqa + assert isort.code(test_input.lower(), honor_noqa=True) == test_output_honor_noqa.lower() def test_extract_multiline_output_wrap_setting_from_a_config_file(tmpdir: py.path.local) -> None: From ceabe5320d6f117ad501f038bc0e85d5f720ee41 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 9 Jul 2020 21:01:17 -0700 Subject: [PATCH 0750/1439] Bump version to 5.0.7 --- isort/_version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/_version.py b/isort/_version.py index 599b2cdc3..988a7dec4 100644 --- a/isort/_version.py +++ b/isort/_version.py @@ -1 +1 @@ -__version__ = "5.0.6" +__version__ = "5.0.7" diff --git a/pyproject.toml b/pyproject.toml index 9da676125..cdc6d3df3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ line-length = 100 [tool.poetry] name = "isort" -version = "5.0.6" +version = "5.0.7" description = "A Python utility / library to sort Python imports." authors = ["Timothy Crosley "] license = "MIT" From f82d3da732c1e1526e7e5bd038599fd71c3b65d2 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 9 Jul 2020 21:27:07 -0700 Subject: [PATCH 0751/1439] Fix terminal usage examples --- docs/quick_start/1.-install.md | 2 +- docs/quick_start/2.-cli.md | 10 +++++----- docs/quick_start/3.-api.md | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/quick_start/1.-install.md b/docs/quick_start/1.-install.md index f23697186..37c469024 100644 --- a/docs/quick_start/1.-install.md +++ b/docs/quick_start/1.-install.md @@ -16,7 +16,7 @@ For a fully isolated user installation you can use [pipx](https://github.com/pip `pipx install isort` - + !!!tip If you want isort to act as a linter for projects, it probably makes since to add isort as an explicit development dependency for each project that uses it. If, on the other hand, you are an individual developer simply using isort as a personal tool to clean up your own commits, a global or user level installation makes sense. Both are seamlessly supported on a single machine. diff --git a/docs/quick_start/2.-cli.md b/docs/quick_start/2.-cli.md index b45224672..4dcdd4d27 100644 --- a/docs/quick_start/2.-cli.md +++ b/docs/quick_start/2.-cli.md @@ -2,28 +2,28 @@ Once installed, `isort` exposes a command line utility for sorting, organizing, and formatting imports within Python and Cython source files. -To verify the tool is installed correctly, run `isort --version` from the command line and you should be given the available commands and the version of isort installed. +To verify the tool is installed correctly, run `isort` from the command line and you should be given the available commands and the version of isort installed. For a list of all CLI options type `isort --help` or view [the online configuration reference](http://127.0.0.1:8000/docs/configuration/options/).: - + ## Formatting a Project In general, isort is most commonly utilized across an entire projects source at once. The simplest way to do this is `isort .` or if using a `src` directory `isort src`. isort will automatically find all Python source files recursively and pick-up a configuration file placed at the root of your project if present. This can be combined with any command line configuration customizations such as specifying a profile to use (`isort . --profile black`). - + ## Verifying a Project The second most common usage of isort is verifying that imports within a project are formatted correctly (often within the context of a CI/CD system). The simplest way to accomplish this is using the check command line option: `isort --check .`. To improve the usefulness of errors when they do occur, this can be combined with the diff option: `isort --check --diff .`. - + ## Single Source Files Finally, isort can just as easily be ran against individual source files. Simply pass in a single or multiple source files to sort or validate (Example: `isort setup.py`). - + ## Multiple Projects diff --git a/docs/quick_start/3.-api.md b/docs/quick_start/3.-api.md index c1f72360e..9e6d2e963 100644 --- a/docs/quick_start/3.-api.md +++ b/docs/quick_start/3.-api.md @@ -4,7 +4,7 @@ In addition to the powerful command line interface, isort exposes a complete Pyt To use the Python API, `import isort` and then call the desired function call: - + Every function is fully type hinted and requires and returns only builtin Python objects. From f99bffd230b5c7bd14fc95ec18ecf7449c0bd511 Mon Sep 17 00:00:00 2001 From: Gerard Dalmau Date: Fri, 10 Jul 2020 13:22:00 +0200 Subject: [PATCH 0752/1439] Fix typo in plone profile --- isort/profiles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/profiles.py b/isort/profiles.py index d8a00ce54..cd976cd29 100644 --- a/isort/profiles.py +++ b/isort/profiles.py @@ -30,7 +30,7 @@ plone = { "force_alphabetical_sort": True, "force_single_line": True, - "ines_after_imports": 2, + "lines_after_imports": 2, "line_length": 200, } attrs = { From a1639c76f98931bdf334228fa7afdab8757eccb0 Mon Sep 17 00:00:00 2001 From: Timothy Edmund Crosley Date: Fri, 10 Jul 2020 17:44:19 -0700 Subject: [PATCH 0753/1439] Update 4.-acknowledgements.md Add Gerard Dalmau (@gdalmau) to acknowledgements --- docs/contributing/4.-acknowledgements.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index 1f6133ad2..0b2f23699 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -97,6 +97,7 @@ Code Contributors - Sebastian (@sebix) - Kosei Kitahara (@Surgo) - Seung Hyeon, Kim (@hyeonjames) +- Gerard Dalmau (@gdalmau) Documenters From 90617aa59cd7d509a8989acbd0b865b84ff372b9 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 11 Jul 2020 00:28:58 -0700 Subject: [PATCH 0754/1439] Fixed #1277 & #1278: New line detection issues on Windows. --- CHANGELOG.md | 3 +++ isort/api.py | 6 +++--- tests/test_regressions.py | 21 +++++++++++++++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ddcead0f..27cb7533a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. +### 5.0.8 July 11, 2020 + - Fixed #1277 & #1278: New line detection issues on Windows. + ### 5.0.7 July 9, 2020 - Fixed #1306: unexpected --diff behavior. - Fixed #1279: Fixed NOQA comment regression. diff --git a/isort/api.py b/isort/api.py index 572bc5167..58367f3e0 100644 --- a/isort/api.py +++ b/isort/api.py @@ -396,14 +396,14 @@ def _sort_imports( if not line_separator: line_separator = "\n" else: - if not line_separator: - line_separator = line[-1] + stripped_line = line.strip() + if stripped_line and not line_separator: + line_separator = line[len(line.rstrip()):] for file_skip_comment in FILE_SKIP_COMMENTS: if file_skip_comment in line: raise FileSkipComment("Passed in content") - stripped_line = line.strip() if ( (index == 0 or (index in (1, 2) and not contains_imports)) and stripped_line.startswith("#") diff --git a/tests/test_regressions.py b/tests/test_regressions.py index 3e288568e..8b751e318 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -224,3 +224,24 @@ def test_add_imports_shouldnt_move_lower_comments_issue_1300(): ANSWER = 42 """ assert isort.code(test_input, add_imports=["from os import path"]) == test_input + + +def test_windows_newline_issue_1277(): + """Test to ensure windows new lines are correctly handled within indented scopes. + See: https://github.com/timothycrosley/isort/issues/1277 + """ + assert ( + isort.code("\ndef main():\r\n import time\r\n\n import sys\r\n") == + "\ndef main():\r\n import sys\r\n import time\r\n" + ) + + +def test_windows_newline_issue_1278(): + """Test to ensure windows new lines are correctly handled within indented scopes. + See: https://github.com/timothycrosley/isort/issues/1278 + """ + assert isort.check_code( + "\ntry:\r\n import datadog_agent\r\n\r\n " + "from ..log import CheckLoggingAdapter, init_logging\r\n\r\n init_logging()\r\n" + "except ImportError:\r\n pass\r\n" + ) From c7ea8ec26c5aa124ae494f2a14222038a8c79c1a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 11 Jul 2020 00:47:01 -0700 Subject: [PATCH 0755/1439] Formatting --- isort/api.py | 2 +- tests/test_regressions.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/isort/api.py b/isort/api.py index 58367f3e0..873d77bfd 100644 --- a/isort/api.py +++ b/isort/api.py @@ -398,7 +398,7 @@ def _sort_imports( else: stripped_line = line.strip() if stripped_line and not line_separator: - line_separator = line[len(line.rstrip()):] + line_separator = line[len(line.rstrip()) :] for file_skip_comment in FILE_SKIP_COMMENTS: if file_skip_comment in line: diff --git a/tests/test_regressions.py b/tests/test_regressions.py index 8b751e318..6910f9914 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -231,8 +231,8 @@ def test_windows_newline_issue_1277(): See: https://github.com/timothycrosley/isort/issues/1277 """ assert ( - isort.code("\ndef main():\r\n import time\r\n\n import sys\r\n") == - "\ndef main():\r\n import sys\r\n import time\r\n" + isort.code("\ndef main():\r\n import time\r\n\n import sys\r\n") + == "\ndef main():\r\n import sys\r\n import time\r\n" ) From c548602bc0418838f6f819b4f5ba6070d2d4a5ac Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 11 Jul 2020 00:51:35 -0700 Subject: [PATCH 0756/1439] Fixed #1294: bundled git hook broken in V5. --- CHANGELOG.md | 1 + isort/hooks.py | 12 +++++------- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27cb7533a..984159e18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. ### 5.0.8 July 11, 2020 - Fixed #1277 & #1278: New line detection issues on Windows. + - Fixed #1294: Fix bundled git hook. ### 5.0.7 July 9, 2020 - Fixed #1306: unexpected --diff behavior. diff --git a/isort/hooks.py b/isort/hooks.py index 07b92f898..8eabda560 100644 --- a/isort/hooks.py +++ b/isort/hooks.py @@ -47,21 +47,19 @@ def git_hook(strict: bool = False, modify: bool = False) -> int: """ # Get list of files modified and staged - diff_cmd = ["git", "diff-index", "--cached", "--name-only", "--diff-filter=ACMRTUXB HEAD"] + diff_cmd = ["git", "diff-index", "--cached", "--name-only", "--diff-filter=ACMRTUXB", "HEAD"] files_modified = get_lines(diff_cmd) errors = 0 for filename in files_modified: if filename.endswith(".py"): # Get the staged contents of the file - staged_cmd = ["git", "show", ":%s" % filename] + staged_cmd = ["git", "show", f":{filename}"] staged_contents = get_output(staged_cmd) - if not ( - api.sort_file(filename) - if modify - else api.check_code_string(staged_contents, file_path=Path(filename)) - ): + if not api.check_code_string(staged_contents, file_path=Path(filename)): errors += 1 + if modify: + api.sort_file(filename) return errors if strict else 0 From 37fb75c4c54ddd0ea9aaaae0fbc74a6683ece2b0 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 11 Jul 2020 01:10:04 -0700 Subject: [PATCH 0757/1439] Add upgrade guide for known_standard_library --- docs/upgrade_guides/5.0.0.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/upgrade_guides/5.0.0.md b/docs/upgrade_guides/5.0.0.md index 327f7dc40..4724695cb 100644 --- a/docs/upgrade_guides/5.0.0.md +++ b/docs/upgrade_guides/5.0.0.md @@ -31,6 +31,11 @@ If you have multiple configs, they will need to be merged into 1 single one. You ### `not_skip` This is the same as the `--dont-skip` CLI option above. In an earlier version isort had a default skip of `__init__.py`. To get around that many projects wanted a way to not skip `__init__.py` or any other files that were automatically skipped in the future by isort. isort no longer has any default skips, so if the value here is `__init__.py` you can simply remove the setting. If it is something else, just make sure you aren't specifying to skip that file somewhere else in your config. +### `known_standard_library` +isort settings no longer merge together, instead they override. The old behavior of merging together caused many hard to +track down errors, but the one place it was very convenient was for adding a few additional standard library modules. +In isort 5, you can still get this behavior by moving your extra modules from the `known_standard_library` setting to [`extra_standard_library`](https://timothycrosley.github.io/isort/docs/configuration/options/#extra-standard-library). + ### module placement changes: `known_third_party`, `known_first_party`, `default_section`, etc... isort has completely rewritten its logic for placing modules in 5.0.0 to ensure the same behavior across environments. You can see the details of this change [here](https://github.com/timothycrosley/isort/issues/1147). The TL;DR of which is that isort has now changed from `default_section=FIRSTPARTY` to `default_section=THIRDPARTY`. If you all already setting the default section to third party, your config is probably in good shape. From 046a62f799b34121c38c8b86487a46ff54bf8ee3 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 11 Jul 2020 01:18:58 -0700 Subject: [PATCH 0758/1439] Bump version to 5.0.8 --- isort/_version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/_version.py b/isort/_version.py index 988a7dec4..09e343d56 100644 --- a/isort/_version.py +++ b/isort/_version.py @@ -1 +1 @@ -__version__ = "5.0.7" +__version__ = "5.0.8" diff --git a/pyproject.toml b/pyproject.toml index cdc6d3df3..7dfe6b95d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ line-length = 100 [tool.poetry] name = "isort" -version = "5.0.7" +version = "5.0.8" description = "A Python utility / library to sort Python imports." authors = ["Timothy Crosley "] license = "MIT" From d744e7f8faab8d3d7914431af95154b0b4dcdbd1 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 11 Jul 2020 03:02:09 -0700 Subject: [PATCH 0759/1439] - Fixed #1301: Import headings in nested sections leads to check errors --- CHANGELOG.md | 3 +++ isort/api.py | 21 ++++++++------------- isort/hooks.py | 6 ++++-- isort/output.py | 2 +- isort/parse.py | 9 +++------ isort/settings.py | 16 ++++++++++++++-- tests/test_parse.py | 1 - tests/test_regressions.py | 17 +++++++++++++++++ 8 files changed, 50 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 984159e18..99180f782 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. +### 5.0.9 July 11, 2020 + - Fixed #1301: Import headings in nested sections leads to check errors + ### 5.0.8 July 11, 2020 - Fixed #1277 & #1278: New line detection issues on Windows. - Fixed #1294: Fix bundled git hook. diff --git a/isort/api.py b/isort/api.py index 873d77bfd..9cbae8089 100644 --- a/isort/api.py +++ b/isort/api.py @@ -8,18 +8,10 @@ from warnings import warn from . import io, output, parse -from .exceptions import ( - ExistingSyntaxErrors, - FileSkipComment, - FileSkipSetting, - IntroducedSyntaxErrors, -) -from .format import ( - ask_whether_to_apply_changes_to_file, - format_natural, - remove_whitespace, - show_unified_diff, -) +from .exceptions import (ExistingSyntaxErrors, FileSkipComment, + FileSkipSetting, IntroducedSyntaxErrors) +from .format import (ask_whether_to_apply_changes_to_file, format_natural, + remove_whitespace, show_unified_diff) from .io import Empty from .place import module as place_module # skipcq: PYL-W0611 (intended export of public API) from .place import module_with_reason as place_module_with_reason # skipcq: PYL-W0611 (^) @@ -398,7 +390,7 @@ def _sort_imports( else: stripped_line = line.strip() if stripped_line and not line_separator: - line_separator = line[len(line.rstrip()) :] + line_separator = line[len(line.rstrip()) :].replace(" ", "").replace("\t", "") for file_skip_comment in FILE_SKIP_COMMENTS: if file_skip_comment in line: @@ -451,6 +443,9 @@ def _sort_imports( isort_off = True elif stripped_line == "# isort: split": not_imports = True + elif stripped_line in config.section_comments and not import_section: + import_section += line + indent = line[: -len(line.lstrip())] elif not (stripped_line or contains_imports): if add_imports and not indent: import_section += line_separator.join(add_imports) + line_separator diff --git a/isort/hooks.py b/isort/hooks.py index 8eabda560..725db7d33 100644 --- a/isort/hooks.py +++ b/isort/hooks.py @@ -57,9 +57,11 @@ def git_hook(strict: bool = False, modify: bool = False) -> int: staged_cmd = ["git", "show", f":{filename}"] staged_contents = get_output(staged_cmd) - if not api.check_code_string(staged_contents, file_path=Path(filename)): + if not api.check_code_string( + staged_contents, file_path=Path(filename), settings_path=Path(filename).parent + ): errors += 1 if modify: - api.sort_file(filename) + api.sort_file(filename, settings_path=Path(filename).parent) return errors if strict else 0 diff --git a/isort/output.py b/isort/output.py index 3c5794828..65beccf80 100644 --- a/isort/output.py +++ b/isort/output.py @@ -185,7 +185,7 @@ def sorted_imports( line, in_quote="", index=len(formatted_output), - section_comments=parsed.section_comments, + section_comments=config.section_comments, ) if not should_skip and line.strip(): if ( diff --git a/isort/parse.py b/isort/parse.py index ad19cab72..800e69fc9 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -80,7 +80,7 @@ def _strip_syntax(import_string: str) -> str: def skip_line( - line: str, in_quote: str, index: int, section_comments: List[str] + line: str, in_quote: str, index: int, section_comments: Tuple[str, ...] ) -> Tuple[bool, str]: """Determine if a given line should be skipped. @@ -134,7 +134,6 @@ class ParsedContent(NamedTuple): original_line_count: int line_separator: str sections: Any - section_comments: List[str] def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedContent: @@ -143,7 +142,6 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte in_lines = contents.split(line_separator) out_lines = [] original_line_count = len(in_lines) - section_comments = [f"# {heading}" for heading in config.import_headings.values()] if config.old_finders: finder = FindersManager(config=config).find else: @@ -172,10 +170,10 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte index += 1 statement_index = index (skipping_line, in_quote) = skip_line( - line, in_quote=in_quote, index=index, section_comments=section_comments + line, in_quote=in_quote, index=index, section_comments=config.section_comments ) - if line in section_comments and not skipping_line: + if line in config.section_comments and not skipping_line: if import_index == -1: import_index = index - 1 continue @@ -435,5 +433,4 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte original_line_count=original_line_count, line_separator=line_separator, sections=config.sections, - section_comments=section_comments, ) diff --git a/isort/settings.py b/isort/settings.py index 0c61b6bf4..1def2af80 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -11,7 +11,8 @@ from distutils.util import strtobool as _as_bool from functools import lru_cache from pathlib import Path -from typing import Any, Callable, Dict, FrozenSet, Iterable, List, Optional, Pattern, Set, Tuple +from typing import (Any, Callable, Dict, FrozenSet, Iterable, List, Optional, + Pattern, Set, Tuple) from warnings import warn from . import stdlibs @@ -206,11 +207,15 @@ def __init__( config: Optional[_Config] = None, **config_overrides, ): + self._known_patterns: Optional[List[Tuple[Pattern[str], str]]] = None + self._section_comments: Optional[Tuple[str, ...]] = None + if config: config_vars = vars(config).copy() config_vars.update(config_overrides) config_vars["py_version"] = config_vars["py_version"].replace("py", "") config_vars.pop("_known_patterns") + config_vars.pop("_section_comments") super().__init__(**config_vars) # type: ignore return @@ -318,7 +323,6 @@ def __init__( combined_config.pop(f"{IMPORT_HEADING_PREFIX}{import_heading_key}") combined_config["import_headings"] = import_headings - self._known_patterns: Optional[List[Tuple[Pattern[str], str]]] = None super().__init__(sources=tuple(sources), **combined_config) # type: ignore def is_skipped(self, file_path: Path) -> bool: @@ -378,6 +382,14 @@ def known_patterns(self): return self._known_patterns + @property + def section_comments(self) -> Tuple[str, ...]: + if self._section_comments is not None: + return self._section_comments + + self._section_comments = tuple(f"# {heading}" for heading in self.import_headings.values()) + return self._section_comments + @staticmethod def _parse_known_pattern(pattern: str) -> List[str]: """Expand pattern if identified as a directory and return found sub packages""" diff --git a/tests/test_parse.py b/tests/test_parse.py index f3a24d668..685ebcf73 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -36,7 +36,6 @@ def test_file_contents(): original_line_count, _, _, - _, ) = parse.file_contents(TEST_CONTENTS, config=Config(default_section="")) assert "\n".join(in_lines) == TEST_CONTENTS assert "import" not in "\n".join(out_lines) diff --git a/tests/test_regressions.py b/tests/test_regressions.py index 6910f9914..dd1754014 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -245,3 +245,20 @@ def test_windows_newline_issue_1278(): "from ..log import CheckLoggingAdapter, init_logging\r\n\r\n init_logging()\r\n" "except ImportError:\r\n pass\r\n" ) + + +def test_check_never_passes_with_indented_headings_1301(): + """Test to ensure that test can pass even when there are indented headings. + See: https://github.com/timothycrosley/isort/issues/1301 + """ + assert isort.check_code( + """ +try: + # stdlib + import logging + from os import abc, path +except ImportError: + pass +""", + import_heading_stdlib="stdlib", + ) From 776978111fe47fb036836d155d342dc859af68f8 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 11 Jul 2020 03:12:36 -0700 Subject: [PATCH 0760/1439] - Fixed #1301: Import headings in nested sections leads to check errors --- isort/__init__.py | 1 + isort/api.py | 16 ++++++++++++---- isort/hooks.py | 10 +++++----- isort/settings.py | 3 +-- 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/isort/__init__.py b/isort/__init__.py index c653a2cd6..2d12b7a67 100644 --- a/isort/__init__.py +++ b/isort/__init__.py @@ -6,3 +6,4 @@ from .api import sort_code_string as code from .api import sort_file as file from .api import sort_stream as stream +from .settings import Config diff --git a/isort/api.py b/isort/api.py index 9cbae8089..136141a22 100644 --- a/isort/api.py +++ b/isort/api.py @@ -8,10 +8,18 @@ from warnings import warn from . import io, output, parse -from .exceptions import (ExistingSyntaxErrors, FileSkipComment, - FileSkipSetting, IntroducedSyntaxErrors) -from .format import (ask_whether_to_apply_changes_to_file, format_natural, - remove_whitespace, show_unified_diff) +from .exceptions import ( + ExistingSyntaxErrors, + FileSkipComment, + FileSkipSetting, + IntroducedSyntaxErrors, +) +from .format import ( + ask_whether_to_apply_changes_to_file, + format_natural, + remove_whitespace, + show_unified_diff, +) from .io import Empty from .place import module as place_module # skipcq: PYL-W0611 (intended export of public API) from .place import module_with_reason as place_module_with_reason # skipcq: PYL-W0611 (^) diff --git a/isort/hooks.py b/isort/hooks.py index 725db7d33..96a678d0a 100644 --- a/isort/hooks.py +++ b/isort/hooks.py @@ -3,11 +3,12 @@ usage: exit_code = git_hook(strict=True|False, modify=True|False) """ +import os import subprocess # nosec - Needed for hook from pathlib import Path from typing import List -from isort import api +from isort import Config, api def get_output(command: List[str]) -> str: @@ -51,17 +52,16 @@ def git_hook(strict: bool = False, modify: bool = False) -> int: files_modified = get_lines(diff_cmd) errors = 0 + config = Config(settings_path=os.path.dirname(os.path.abspath(files_modified[0]))) for filename in files_modified: if filename.endswith(".py"): # Get the staged contents of the file staged_cmd = ["git", "show", f":{filename}"] staged_contents = get_output(staged_cmd) - if not api.check_code_string( - staged_contents, file_path=Path(filename), settings_path=Path(filename).parent - ): + if not api.check_code_string(staged_contents, file_path=Path(filename), config=config): errors += 1 if modify: - api.sort_file(filename, settings_path=Path(filename).parent) + api.sort_file(filename, config=config) return errors if strict else 0 diff --git a/isort/settings.py b/isort/settings.py index 1def2af80..01af6df19 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -11,8 +11,7 @@ from distutils.util import strtobool as _as_bool from functools import lru_cache from pathlib import Path -from typing import (Any, Callable, Dict, FrozenSet, Iterable, List, Optional, - Pattern, Set, Tuple) +from typing import Any, Callable, Dict, FrozenSet, Iterable, List, Optional, Pattern, Set, Tuple from warnings import warn from . import stdlibs From ea0642d254c37fda55a46e0c1e4bc1dfee4ccefd Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 11 Jul 2020 03:14:03 -0700 Subject: [PATCH 0761/1439] Fix usage without modified files of git hook --- isort/hooks.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/isort/hooks.py b/isort/hooks.py index 96a678d0a..13b0b7434 100644 --- a/isort/hooks.py +++ b/isort/hooks.py @@ -50,6 +50,8 @@ def git_hook(strict: bool = False, modify: bool = False) -> int: # Get list of files modified and staged diff_cmd = ["git", "diff-index", "--cached", "--name-only", "--diff-filter=ACMRTUXB", "HEAD"] files_modified = get_lines(diff_cmd) + if not files_modified: + return 0 errors = 0 config = Config(settings_path=os.path.dirname(os.path.abspath(files_modified[0]))) From b49aba99b919473b66f05b663588cfac2472c601 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 11 Jul 2020 03:14:32 -0700 Subject: [PATCH 0762/1439] Bump version to 5.0.9 --- isort/_version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/_version.py b/isort/_version.py index 09e343d56..603e34093 100644 --- a/isort/_version.py +++ b/isort/_version.py @@ -1 +1 @@ -__version__ = "5.0.8" +__version__ = "5.0.9" diff --git a/pyproject.toml b/pyproject.toml index 7dfe6b95d..467664039 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ line-length = 100 [tool.poetry] name = "isort" -version = "5.0.8" +version = "5.0.9" description = "A Python utility / library to sort Python imports." authors = ["Timothy Crosley "] license = "MIT" From de127f5c5ea3acd6892933b0dbb4fac48f48aa73 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 11 Jul 2020 20:06:44 -0700 Subject: [PATCH 0763/1439] Add additional documentation for order_by_type --- docs/configuration/options.md | 25 ++++++++++++++++++++++--- isort/main.py | 15 +++++++++++++-- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/docs/configuration/options.md b/docs/configuration/options.md index bec87881e..231674123 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -181,7 +181,7 @@ Force isort to recognize a module as being part of the current python project. Force isort to recognize a module as part of Python's standard library. **Type:** Frozenset -**Default:** `('_dummy_thread', '_thread', 'abc', 'aifc', 'argparse', 'array', 'ast', 'asynchat', 'asyncio', 'asyncore', 'atexit', 'audioop', 'base64', 'bdb', 'binascii', 'binhex', 'bisect', 'builtins', 'bz2', 'cProfile', 'calendar', 'cgi', 'cgitb', 'chunk', 'cmath', 'cmd', 'code', 'codecs', 'codeop', 'collections', 'colorsys', 'compileall', 'concurrent', 'configparser', 'contextlib', 'contextvars', 'copy', 'copyreg', 'crypt', 'csv', 'ctypes', 'curses', 'dataclasses', 'datetime', 'dbm', 'decimal', 'difflib', 'dis', 'distutils', 'doctest', 'dummy_threading', 'email', 'encodings', 'ensurepip', 'enum', 'errno', 'faulthandler', 'fcntl', 'filecmp', 'fileinput', 'fnmatch', 'formatter', 'fpectl', 'fractions', 'ftplib', 'functools', 'gc', 'getopt', 'getpass', 'gettext', 'glob', 'grp', 'gzip', 'hashlib', 'heapq', 'hmac', 'html', 'http', 'imaplib', 'imghdr', 'imp', 'importlib', 'inspect', 'io', 'ipaddress', 'itertools', 'json', 'keyword', 'lib2to3', 'linecache', 'locale', 'logging', 'lzma', 'macpath', 'mailbox', 'mailcap', 'marshal', 'math', 'mimetypes', 'mmap', 'modulefinder', 'msilib', 'msvcrt', 'multiprocessing', 'netrc', 'nis', 'nntplib', 'ntpath', 'numbers', 'operator', 'optparse', 'os', 'ossaudiodev', 'parser', 'pathlib', 'pdb', 'pickle', 'pickletools', 'pipes', 'pkgutil', 'platform', 'plistlib', 'poplib', 'posix', 'posixpath', 'pprint', 'profile', 'pstats', 'pty', 'pwd', 'py_compile', 'pyclbr', 'pydoc', 'queue', 'quopri', 'random', 're', 'readline', 'reprlib', 'resource', 'rlcompleter', 'runpy', 'sched', 'secrets', 'select', 'selectors', 'shelve', 'shlex', 'shutil', 'signal', 'site', 'smtpd', 'smtplib', 'sndhdr', 'socket', 'socketserver', 'spwd', 'sqlite3', 'sre_constants', 'ssl', 'stat', 'statistics', 'string', 'stringprep', 'struct', 'subprocess', 'sunau', 'symbol', 'symtable', 'sys', 'sysconfig', 'syslog', 'tabnanny', 'tarfile', 'telnetlib', 'tempfile', 'termios', 'test', 'textwrap', 'threading', 'time', 'timeit', 'tkinter', 'token', 'tokenize', 'trace', 'traceback', 'tracemalloc', 'tty', 'turtle', 'turtledemo', 'types', 'typing', 'unicodedata', 'unittest', 'urllib', 'uu', 'uuid', 'venv', 'warnings', 'wave', 'weakref', 'webbrowser', 'winreg', 'winsound', 'wsgiref', 'xdrlib', 'xml', 'xmlrpc', 'zipapp', 'zipfile', 'zipimport', 'zlib')` +**Default:** `('_dummy_thread', '_thread', 'abc', 'aifc', 'argparse', 'array', 'ast', 'asynchat', 'asyncio', 'asyncore', 'atexit', 'audioop', 'base64', 'bdb', 'binascii', 'binhex', 'bisect', 'builtins', 'bz2', 'cProfile', 'calendar', 'cgi', 'cgitb', 'chunk', 'cmath', 'cmd', 'code', 'codecs', 'codeop', 'collections', 'colorsys', 'compileall', 'concurrent', 'configparser', 'contextlib', 'contextvars', 'copy', 'copyreg', 'crypt', 'csv', 'ctypes', 'curses', 'dataclasses', 'datetime', 'dbm', 'decimal', 'difflib', 'dis', 'distutils', 'doctest', 'dummy_threading', 'email', 'encodings', 'ensurepip', 'enum', 'errno', 'faulthandler', 'fcntl', 'filecmp', 'fileinput', 'fnmatch', 'formatter', 'fpectl', 'fractions', 'ftplib', 'functools', 'gc', 'getopt', 'getpass', 'gettext', 'glob', 'grp', 'gzip', 'hashlib', 'heapq', 'hmac', 'html', 'http', 'imaplib', 'imghdr', 'imp', 'importlib', 'inspect', 'io', 'ipaddress', 'itertools', 'json', 'keyword', 'lib2to3', 'linecache', 'locale', 'logging', 'lzma', 'macpath', 'mailbox', 'mailcap', 'marshal', 'math', 'mimetypes', 'mmap', 'modulefinder', 'msilib', 'msvcrt', 'multiprocessing', 'netrc', 'nis', 'nntplib', 'ntpath', 'numbers', 'operator', 'optparse', 'os', 'ossaudiodev', 'parser', 'pathlib', 'pdb', 'pickle', 'pickletools', 'pipes', 'pkgutil', 'platform', 'plistlib', 'poplib', 'posix', 'posixpath', 'pprint', 'profile', 'pstats', 'pty', 'pwd', 'py_compile', 'pyclbr', 'pydoc', 'queue', 'quopri', 'random', 're', 'readline', 'reprlib', 'resource', 'rlcompleter', 'runpy', 'sched', 'secrets', 'select', 'selectors', 'shelve', 'shlex', 'shutil', 'signal', 'site', 'smtpd', 'smtplib', 'sndhdr', 'socket', 'socketserver', 'spwd', 'sqlite3', 'sre', 'sre_compile', 'sre_constants', 'sre_parse', 'ssl', 'stat', 'statistics', 'string', 'stringprep', 'struct', 'subprocess', 'sunau', 'symbol', 'symtable', 'sys', 'sysconfig', 'syslog', 'tabnanny', 'tarfile', 'telnetlib', 'tempfile', 'termios', 'test', 'textwrap', 'threading', 'time', 'timeit', 'tkinter', 'token', 'tokenize', 'trace', 'traceback', 'tracemalloc', 'tty', 'turtle', 'turtledemo', 'types', 'typing', 'unicodedata', 'unittest', 'urllib', 'uu', 'uuid', 'venv', 'warnings', 'wave', 'weakref', 'webbrowser', 'winreg', 'winsound', 'wsgiref', 'xdrlib', 'xml', 'xmlrpc', 'zipapp', 'zipfile', 'zipimport', 'zlib')` **Python & Config File Name:** known_standard_library **CLI Flags:** @@ -439,7 +439,10 @@ Use parenthesis for line continuation on length limit instead of slashes. ## Order By Type -Order imports by type in addition to alphabetically +Order imports by type in addition to alphabetically. + +**NOTE**: type here refers to the implied type from the import name capitalization. + isort does not do type introspection for the imports. These "types" are simply: CONSTANT_VARIABLE, CamelCaseClass, variable_or_function. If your project follows PEP8 or a related coding standard and has many imports this is a good default, otherwise you likely will want to turn it off. From the CLI the `--dont-order-by-type` option will turn this off. **Type:** Bool **Default:** `True` @@ -834,6 +837,19 @@ Base profile type to use for configuration. +## Honor Noqa + +Tells isort to honor noqa comments to enforce skipping those comments. + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** honor_noqa +**CLI Flags:** + +- --honor-noqa + + + ## Src Paths Add an explicitly defined source path (modules within src paths have their imports automatically catorgorized as first_party). @@ -921,7 +937,10 @@ Number of files to process in parallel. ## Dont Order By Type -Don't order imports by type in addition to alphabetically +Don't order imports by type in addition to alphabetically. + +**NOTE**: type here refers to the implied type from the import name capitalization. + isort does not do type introspection for the imports. These "types" are simply: CONSTANT_VARIABLE, CamelCaseClass, variable_or_function. If your project follows PEP8 or a related coding standard and has many imports this is a good default. You can turn this on from the CLI using `--order-by-type`. **Type:** Bool **Default:** `False` diff --git a/isort/main.py b/isort/main.py index 3581fb67f..f8245ee47 100644 --- a/isort/main.py +++ b/isort/main.py @@ -356,14 +356,25 @@ def _build_arg_parser() -> argparse.ArgumentParser: "--order-by-type", dest="order_by_type", action="store_true", - help="Order imports by type in addition to alphabetically", + help="Order imports by type in addition to alphabetically.\n\n" + "**NOTE**: type here refers to the implied type from the import name capitalization.\n" + ' isort does not do type introspection for the imports. These "types" are simply: ' + "CONSTANT_VARIABLE, CamelCaseClass, variable_or_function. If your project follows PEP8" + " or a related coding standard and has many imports this is a good default, otherwise you " + "likely will want to turn it off. From the CLI the `--dont-order-by-type` option will turn " + "this off.", ) parser.add_argument( "--dt", "--dont-order-by-type", dest="dont_order_by_type", action="store_true", - help="Don't order imports by type in addition to alphabetically", + help="Don't order imports by type in addition to alphabetically.\n\n" + "**NOTE**: type here refers to the implied type from the import name capitalization.\n" + ' isort does not do type introspection for the imports. These "types" are simply: ' + "CONSTANT_VARIABLE, CamelCaseClass, variable_or_function. If your project follows PEP8" + " or a related coding standard and has many imports this is a good default. You can turn " + "this on from the CLI using `--order-by-type`." ) parser.add_argument( "-p", From 0b7b0191871d74485376005a6e809a057aa48847 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 11 Jul 2020 20:06:59 -0700 Subject: [PATCH 0764/1439] formatting --- isort/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/main.py b/isort/main.py index f8245ee47..e9779dcf8 100644 --- a/isort/main.py +++ b/isort/main.py @@ -374,7 +374,7 @@ def _build_arg_parser() -> argparse.ArgumentParser: ' isort does not do type introspection for the imports. These "types" are simply: ' "CONSTANT_VARIABLE, CamelCaseClass, variable_or_function. If your project follows PEP8" " or a related coding standard and has many imports this is a good default. You can turn " - "this on from the CLI using `--order-by-type`." + "this on from the CLI using `--order-by-type`.", ) parser.add_argument( "-p", From d7c20f505690dd27fec0e7d23b0f579e086d5120 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 11 Jul 2020 20:08:59 -0700 Subject: [PATCH 0765/1439] Add additional documentation for order_by_type --- isort/main.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/isort/main.py b/isort/main.py index e9779dcf8..eb7850d33 100644 --- a/isort/main.py +++ b/isort/main.py @@ -356,8 +356,8 @@ def _build_arg_parser() -> argparse.ArgumentParser: "--order-by-type", dest="order_by_type", action="store_true", - help="Order imports by type in addition to alphabetically.\n\n" - "**NOTE**: type here refers to the implied type from the import name capitalization.\n" + help="Order imports by type, which is determined by case, in addition to alphabetically.\n" + "\n**NOTE**: type here refers to the implied type from the import name capitalization.\n" ' isort does not do type introspection for the imports. These "types" are simply: ' "CONSTANT_VARIABLE, CamelCaseClass, variable_or_function. If your project follows PEP8" " or a related coding standard and has many imports this is a good default, otherwise you " @@ -369,7 +369,8 @@ def _build_arg_parser() -> argparse.ArgumentParser: "--dont-order-by-type", dest="dont_order_by_type", action="store_true", - help="Don't order imports by type in addition to alphabetically.\n\n" + help="Don't order imports by type, which is determined by case, in addition to " + "alphabetically.\n\n" "**NOTE**: type here refers to the implied type from the import name capitalization.\n" ' isort does not do type introspection for the imports. These "types" are simply: ' "CONSTANT_VARIABLE, CamelCaseClass, variable_or_function. If your project follows PEP8" From a9d934a23bcd540d99c64c993efa737b72f7dec4 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 11 Jul 2020 20:37:00 -0700 Subject: [PATCH 0766/1439] Add additional documentation for order_by_type --- docs/configuration/options.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration/options.md b/docs/configuration/options.md index 231674123..91da19ab4 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -439,7 +439,7 @@ Use parenthesis for line continuation on length limit instead of slashes. ## Order By Type -Order imports by type in addition to alphabetically. +Order imports by type, which is determined by case, in addition to alphabetically. **NOTE**: type here refers to the implied type from the import name capitalization. isort does not do type introspection for the imports. These "types" are simply: CONSTANT_VARIABLE, CamelCaseClass, variable_or_function. If your project follows PEP8 or a related coding standard and has many imports this is a good default, otherwise you likely will want to turn it off. From the CLI the `--dont-order-by-type` option will turn this off. @@ -937,7 +937,7 @@ Number of files to process in parallel. ## Dont Order By Type -Don't order imports by type in addition to alphabetically. +Don't order imports by type, which is determined by case, in addition to alphabetically. **NOTE**: type here refers to the implied type from the import name capitalization. isort does not do type introspection for the imports. These "types" are simply: CONSTANT_VARIABLE, CamelCaseClass, variable_or_function. If your project follows PEP8 or a related coding standard and has many imports this is a good default. You can turn this on from the CLI using `--order-by-type`. From 797370f9d5eae57d87eb43c7a3edd73a807c925c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 11 Jul 2020 21:12:11 -0700 Subject: [PATCH 0767/1439] Read the docs redirect --- mkdocs.yml | 2 ++ rtd/index.md | 6 ++++++ 2 files changed, 8 insertions(+) create mode 100644 mkdocs.yml create mode 100644 rtd/index.md diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 000000000..d2ebd9d87 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,2 @@ +docs_dir: rtd +site_name: "isort redirect" diff --git a/rtd/index.md b/rtd/index.md new file mode 100644 index 000000000..6a6451dd7 --- /dev/null +++ b/rtd/index.md @@ -0,0 +1,6 @@ + + +The latest isort docs are not on ReadTheDocs. This page contains a javascript redirect to the latest documentation. +If this redirect doesn't work, please [click here for the latest documentation](https://timothycrosley.github.io/isort/). From db34caa7ac9770af87bcbfd98ad0f565589e50f5 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 11 Jul 2020 23:25:08 -0700 Subject: [PATCH 0768/1439] isort now throws an exception if an invalid settings path is given (issue #1174). --- CHANGELOG.md | 3 +++ isort/exceptions.py | 12 ++++++++++++ isort/main.py | 2 -- isort/settings.py | 6 +++++- tests/test_main.py | 18 ++++++++++-------- tests/test_settings.py | 4 ++++ 6 files changed, 34 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99180f782..506f164b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. +### 5.1.0 July 12, 2020 + - isort now throws an exception if an invalid settings path is given (issue #1174). + ### 5.0.9 July 11, 2020 - Fixed #1301: Import headings in nested sections leads to check errors diff --git a/isort/exceptions.py b/isort/exceptions.py index 64545e6eb..ffdc8d9af 100644 --- a/isort/exceptions.py +++ b/isort/exceptions.py @@ -6,6 +6,18 @@ class ISortError(Exception): """Base isort exception object from which all isort sourced exceptions should inherit""" +class InvalidSettingsPath(ISortError): + """Raised when a settings path is provided that is neither a valid file or directory""" + + def __init__(self, settings_path: str): + super().__init__( + f"isort was told to use the settings_path: {settings_path} as the base directory or " + "file that represents the starting point of config file discovery, but it does not " + "exist." + ) + self.settings_path = settings_path + + class ExistingSyntaxErrors(ISortError): """Raised when isort is told to sort imports within code that has existing syntax errors""" diff --git a/isort/main.py b/isort/main.py index eb7850d33..0d0418636 100644 --- a/isort/main.py +++ b/isort/main.py @@ -656,8 +656,6 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = if os.path.isfile(arguments["settings_path"]): arguments["settings_file"] = os.path.abspath(arguments["settings_path"]) arguments["settings_path"] = os.path.dirname(arguments["settings_file"]) - elif not os.path.isdir(arguments["settings_path"]): - warn(f"settings_path dir does not exist: {arguments['settings_path']}") else: arguments["settings_path"] = os.path.abspath(arguments["settings_path"]) diff --git a/isort/settings.py b/isort/settings.py index 01af6df19..a94d58763 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -17,7 +17,7 @@ from . import stdlibs from ._future import dataclass, field from ._vendored import toml -from .exceptions import ProfileDoesNotExist +from .exceptions import InvalidSettingsPath, ProfileDoesNotExist from .profiles import profiles from .sections import DEFAULT as SECTION_DEFAULTS from .sections import FIRSTPARTY, FUTURE, STDLIB, THIRDPARTY @@ -229,6 +229,10 @@ def __init__( ) project_root = os.path.dirname(settings_file) elif settings_path: + if not os.path.exists(settings_path): + raise InvalidSettingsPath(settings_path) + + settings_path = os.path.abspath(settings_path) project_root, config_settings = _find_config(settings_path) else: config_settings = {} diff --git a/tests/test_main.py b/tests/test_main.py index ebfa81cdf..c378c416a 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -9,6 +9,7 @@ from isort import main from isort._version import __version__ +from isort.exceptions import InvalidSettingsPath from isort.settings import DEFAULT_CONFIG, Config from isort.wrap_modes import WrapModes @@ -109,17 +110,18 @@ def test_main(capsys, tmpdir): assert returned_config assert returned_config["virtual_env"] == str(tmpdir) - # This should work even if settings path is non-existent or not provided + # This should work even if settings path is not provided main.main(base_args[2:] + ["--show-config"]) out, error = capsys.readouterr() assert json.loads(out)["virtual_env"] == str(tmpdir) - main.main( - base_args[2:] - + ["--show-config"] - + ["--settings-path", "/random-root-folder-that-cant-exist-right?"] - ) - out, error = capsys.readouterr() - assert json.loads(out)["virtual_env"] == str(tmpdir) + + # This should raise an error if an invalid settings path is provided + with pytest.raises(InvalidSettingsPath): + main.main( + base_args[2:] + + ["--show-config"] + + ["--settings-path", "/random-root-folder-that-cant-exist-right?"] + ) # Should be able to set settings path to a file config_file = tmpdir.join(".isort.cfg") diff --git a/tests/test_settings.py b/tests/test_settings.py index 024f47532..5c9d184b5 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -11,6 +11,10 @@ class TestConfig: def test_init(self): assert Config() + def test_invalid_settings_path(self): + with pytest.raises(exceptions.InvalidSettingsPath): + Config(settings_path="this_couldnt_possibly_actually_exists/could_it") + def test_invalid_pyversion(self): with pytest.raises(ValueError): Config(py_version=10) From a42df9140e9c7ffc57cbd0868fe3c1a7ab74b8d0 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 11 Jul 2020 23:45:17 -0700 Subject: [PATCH 0769/1439] Fixed #1178: support for semicolons in decorators. --- CHANGELOG.md | 1 + isort/output.py | 1 + isort/parse.py | 8 ++++++-- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 506f164b0..c6642f3ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. ### 5.1.0 July 12, 2020 - isort now throws an exception if an invalid settings path is given (issue #1174). + - Fixed #1178: support for semicolons in decorators. ### 5.0.9 July 11, 2020 - Fixed #1301: Import headings in nested sections leads to check errors diff --git a/isort/output.py b/isort/output.py index 65beccf80..493ba4fb8 100644 --- a/isort/output.py +++ b/isort/output.py @@ -186,6 +186,7 @@ def sorted_imports( in_quote="", index=len(formatted_output), section_comments=config.section_comments, + needs_import=False, ) if not should_skip and line.strip(): if ( diff --git a/isort/parse.py b/isort/parse.py index 800e69fc9..3baa421ce 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -80,7 +80,11 @@ def _strip_syntax(import_string: str) -> str: def skip_line( - line: str, in_quote: str, index: int, section_comments: Tuple[str, ...] + line: str, + in_quote: str, + index: int, + section_comments: Tuple[str, ...], + needs_import: bool = True, ) -> Tuple[bool, str]: """Determine if a given line should be skipped. @@ -109,7 +113,7 @@ def skip_line( break char_index += 1 - if ";" in line: + if ";" in line and needs_import: for part in (part.strip() for part in line.split(";")): if ( part From 9c290ba924477f62a6e0567d576ad451b7658ce7 Mon Sep 17 00:00:00 2001 From: Sebastian Wagner Date: Sun, 12 Jul 2020 14:38:49 +0200 Subject: [PATCH 0770/1439] pyproject.toml: Add tests in sdist only fixes part of timothycrosley/isort#1266 fixes timothycrosley/isort#463 --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 467664039..36c9f494d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,10 @@ classifiers = [ "Topic :: Utilities", ] urls = { Changelog = "https://github.com/timothycrosley/isort/blob/master/CHANGELOG.md" } +packages = [ + { include = "isort" }, + { include = "tests", format = "sdist" }, +] [tool.poetry.dependencies] python = "^3.6" From e587e21ef5070a9e35826b9cd4b22d7aa83b2f18 Mon Sep 17 00:00:00 2001 From: r-richmond Date: Sun, 12 Jul 2020 20:07:52 -0700 Subject: [PATCH 0771/1439] first pass --- scripts/build_config_option_docs.py | 124 ++++++++++++++++------------ 1 file changed, 70 insertions(+), 54 deletions(-) diff --git a/scripts/build_config_option_docs.py b/scripts/build_config_option_docs.py index d7b0c6434..3dbc41f5e 100755 --- a/scripts/build_config_option_docs.py +++ b/scripts/build_config_option_docs.py @@ -1,7 +1,7 @@ #! /bin/env python import os -import textwrap -from typing import Any, Generator, Iterable, Type +from textwrap import dedent +from typing import Any, Dict, Generator, Iterable, Optional, Type from isort._future import dataclass from isort.main import _build_arg_parser @@ -27,61 +27,88 @@ @dataclass -class ConfigOption: - name: str - type: Type = str - default: Any = "" - config_name: str = "**Not Supported**" - cli_options: Iterable[str] = ("**Not Supported**",) - description: str = "**No Description**" - example_section: str = "" - example_cfg: str = "" - example_pyproject_toml: str = "" - example_cli: str = "" +class Example: + section_complete: str = "" + cfg: str = "" + pyproject_toml: str = "" + cli: str = "" def __post_init__(self): - if self.example_cfg or self.example_pyproject_toml or self.example_cli: - if self.example_cfg: - self.example_cfg = textwrap.dedent( + if self.cfg or self.pyproject_toml or self.cli: + if self.cfg: + self.cfg = dedent( f""" ### Example `.isort.cfg` ``` - {self.example_cfg} + {self.cfg} ``` """ ) - if self.example_pyproject_toml: - self.example_pyproject_toml = textwrap.dedent( + if self.pyproject_toml: + self.pyproject_toml = dedent( f""" ### Example `pyproject.toml` ``` - {self.example_pyproject_toml} + {self.pyproject_toml} ``` """ ) - if self.example_cli == "": - self.example_cli = textwrap.dedent( + if self.cli == "": + self.cli = dedent( f""" ### Example cli usage - `{self.example_cli}` + `{self.cli}` """ ) - self.example_section = f"""**Examples:** + self.section_complete = f"""**Examples:** + +{self.cfg} +{self.pyproject_toml} +{self.cli}""" + + else: + self.section_complete = "" + + def __str__(self): + return self.section_complete + + +example_mapping: Dict[str, Example] +example_mapping = { + "known_other": Example( + pyproject_toml="""[tool.isort] + sections = ['FUTURE', 'STDLIB', 'THIRDPARTY', 'AIRFLOW', 'FIRSTPARTY', 'LOCALFOLDER'] + known_airflow = ['airflow']""" + ) +} + -{self.example_cfg} -{self.example_pyproject_toml} -{self.example_cli}""" +@dataclass +class ConfigOption: + name: str + type: Type = str + default: Any = "" + config_name: str = "**Not Supported**" + cli_options: Iterable[str] = tuple() + description: str = "**No Description**" + example: Optional[Example] = None def __str__(self): if self.name in IGNORED: return "" - cli_options = "\n- ".join(self.cli_options) + if self.cli_options == (): + cli_options = "**Not Supported**" + else: + cli_options = "- " + "\n- ".join(self.cli_options) + + # new line if example otherwise nothing + example = f"\n{self.example}" if self.example else "" return f""" ## {human(self.name)} @@ -92,10 +119,8 @@ def __str__(self): **Python & Config File Name:** {self.config_name}{MD_NEWLINE} **CLI Flags:** -- {cli_options} - -{self.example_section} -""" +{cli_options} +{example}""" def human(name: str) -> str: @@ -126,27 +151,14 @@ def config_options() -> Generator[ConfigOption, None, None]: # todo: refactor place for example params # needs to integrate with isort/settings/_Config # needs to integrate with isort/main/_build_arg_parser - if name != "known_other": - yield ConfigOption( - name=name, - type=type(default), - default=default_display, - config_name=name, - **extra_kwargs, - ) - else: - yield ConfigOption( - name=name, - type=type(default), - default=default_display, - config_name=name, - example_pyproject_toml=textwrap.dedent( - """[tool.isort] - sections = ['FUTURE', 'STDLIB', 'THIRDPARTY', 'AIRFLOW', 'FIRSTPARTY', 'LOCALFOLDER'] - known_airflow = ['airflow']""" - ), - **extra_kwargs, - ) + yield ConfigOption( + name=name, + type=type(default), + default=default_display, + config_name=name, + example=example_mapping.get(name, None), + **extra_kwargs, + ) for name, cli in cli_actions.items(): extra_kwargs = {} @@ -159,7 +171,11 @@ def config_options() -> Generator[ConfigOption, None, None]: extra_kwargs["description"] = cli.help yield ConfigOption( - name=name, default=cli.default, cli_options=cli.option_strings, **extra_kwargs + name=name, + default=cli.default, + cli_options=cli.option_strings, + example=example_mapping.get(name, None), + **extra_kwargs, ) From 793c569f371657537ceff289351a748aa3f0374b Mon Sep 17 00:00:00 2001 From: r-richmond Date: Sun, 12 Jul 2020 20:18:38 -0700 Subject: [PATCH 0772/1439] second cleanup pass --- docs/configuration/options.md | 171 ++-------------------------- scripts/build_config_option_docs.py | 28 +++-- 2 files changed, 27 insertions(+), 172 deletions(-) diff --git a/docs/configuration/options.md b/docs/configuration/options.md index 91da19ab4..81e5e1001 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -18,8 +18,6 @@ Tells isort to set the known standard library based on the the specified Python - --py - --python-version - - ## Force To Top Force specific imports to the top of their appropriate section. @@ -32,8 +30,6 @@ Force specific imports to the top of their appropriate section. - -t - --top - - ## Skip Files that sort imports should skip over. If you want to skip multiple files you should specify twice: --skip file1 --skip file2. @@ -46,8 +42,6 @@ Files that sort imports should skip over. If you want to skip multiple files you - -s - --skip - - ## Skip Glob Files that sort imports should skip over. @@ -60,8 +54,6 @@ Files that sort imports should skip over. - --sg - --skip-glob - - ## Line Length The max length of an import line (used for wrapping long imports). @@ -76,8 +68,6 @@ The max length of an import line (used for wrapping long imports). - --line-length - --line-width - - ## Wrap Length Specifies how long lines that are wrapped should be, if not set line_length is used. @@ -91,8 +81,6 @@ NOTE: wrap_length must be LOWER than or equal to line_length. - --wl - --wrap-length - - ## Line Ending Forces line endings to the specified value. If not set, values will be guessed per-file. @@ -105,8 +93,6 @@ Forces line endings to the specified value. If not set, values will be guessed p - --le - --line-ending - - ## Sections **No Description** @@ -116,9 +102,7 @@ Forces line endings to the specified value. If not set, values will be guessed p **Python & Config File Name:** sections **CLI Flags:** -- **Not Supported** - - +**Not Supported** ## No Sections @@ -132,8 +116,6 @@ Put all imports into the same section bucket - --ds - --no-sections - - ## Known Future Library Force isort to recognize a module as part of the future compatibility libraries. @@ -146,8 +128,6 @@ Force isort to recognize a module as part of the future compatibility libraries. - -f - --future - - ## Known Third Party Force isort to recognize a module as being part of a third party library. @@ -160,8 +140,6 @@ Force isort to recognize a module as being part of a third party library. - -o - --thirdparty - - ## Known First Party Force isort to recognize a module as being part of the current python project. @@ -174,8 +152,6 @@ Force isort to recognize a module as being part of the current python project. - -p - --project - - ## Known Standard Library Force isort to recognize a module as part of Python's standard library. @@ -188,8 +164,6 @@ Force isort to recognize a module as part of Python's standard library. - -b - --builtin - - ## Extra Standard Library Extra modules to be included in the list of ones in Python's standard library. @@ -201,8 +175,6 @@ Extra modules to be included in the list of ones in Python's standard library. - --extra-builtin - - ## Known Other **No Description** @@ -212,7 +184,7 @@ Extra modules to be included in the list of ones in Python's standard library. **Python & Config File Name:** known_other **CLI Flags:** -- **Not Supported** +**Not Supported** **Examples:** @@ -230,7 +202,6 @@ known_airflow = ['airflow'] ### Example cli usage `` - ## Multi Line Output Multi line output (0-grid, 1-vertical, 2-hanging, 3-vert-hanging, 4-vert-grid, 5-vert-grid-grouped, 6-vert-grid-grouped-no-comma). @@ -243,8 +214,6 @@ Multi line output (0-grid, 1-vertical, 2-hanging, 3-vert-hanging, 4-vert-grid, 5 - -m - --multi-line - - ## Forced Separate **No Description** @@ -254,9 +223,7 @@ Multi line output (0-grid, 1-vertical, 2-hanging, 3-vert-hanging, 4-vert-grid, 5 **Python & Config File Name:** forced_separate **CLI Flags:** -- **Not Supported** - - +**Not Supported** ## Indent @@ -270,8 +237,6 @@ String to place for indents defaults to " " (4 spaces). - -i - --indent - - ## Comment Prefix **No Description** @@ -281,9 +246,7 @@ String to place for indents defaults to " " (4 spaces). **Python & Config File Name:** comment_prefix **CLI Flags:** -- **Not Supported** - - +**Not Supported** ## Length Sort @@ -297,8 +260,6 @@ Sort imports by their string length. - --ls - --length-sort - - ## Length Sort Sections **No Description** @@ -308,9 +269,7 @@ Sort imports by their string length. **Python & Config File Name:** length_sort_sections **CLI Flags:** -- **Not Supported** - - +**Not Supported** ## Add Imports @@ -324,8 +283,6 @@ Adds the specified import line to all files, automatically determining correct p - -a - --add-import - - ## Remove Imports Removes the specified import from all files. @@ -338,8 +295,6 @@ Removes the specified import from all files. - --rm - --remove-import - - ## Reverse Relative Reverse order of relative imports. @@ -352,8 +307,6 @@ Reverse order of relative imports. - --rr - --reverse-relative - - ## Force Single Line Forces all from imports to appear on their own line @@ -366,8 +319,6 @@ Forces all from imports to appear on their own line - --sl - --force-single-line-imports - - ## Single Line Exclusions One or more modules to exclude from the single line rule. @@ -380,8 +331,6 @@ One or more modules to exclude from the single line rule. - --nsl - --single-line-exclusions - - ## Default Section Sets the default section for import options: ('FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER') @@ -394,8 +343,6 @@ Sets the default section for import options: ('FUTURE', 'STDLIB', 'THIRDPARTY', - --sd - --section-default - - ## Import Headings **No Description** @@ -405,9 +352,7 @@ Sets the default section for import options: ('FUTURE', 'STDLIB', 'THIRDPARTY', **Python & Config File Name:** import_headings **CLI Flags:** -- **Not Supported** - - +**Not Supported** ## Balanced Wrapping @@ -421,8 +366,6 @@ Balances wrapping to produce the most consistent line length possible - -e - --balanced - - ## Use Parentheses Use parenthesis for line continuation on length limit instead of slashes. @@ -435,8 +378,6 @@ Use parenthesis for line continuation on length limit instead of slashes. - --up - --use-parentheses - - ## Order By Type Order imports by type, which is determined by case, in addition to alphabetically. @@ -452,8 +393,6 @@ Order imports by type, which is determined by case, in addition to alphabeticall - --ot - --order-by-type - - ## Atomic Ensures the output doesn't save if the resulting file contains syntax errors. @@ -466,8 +405,6 @@ Ensures the output doesn't save if the resulting file contains syntax errors. - --ac - --atomic - - ## Lines After Imports **No Description** @@ -480,8 +417,6 @@ Ensures the output doesn't save if the resulting file contains syntax errors. - --lai - --lines-after-imports - - ## Lines Between Sections **No Description** @@ -491,9 +426,7 @@ Ensures the output doesn't save if the resulting file contains syntax errors. **Python & Config File Name:** lines_between_sections **CLI Flags:** -- **Not Supported** - - +**Not Supported** ## Lines Between Types @@ -507,8 +440,6 @@ Ensures the output doesn't save if the resulting file contains syntax errors. - --lbt - --lines-between-types - - ## Combine As Imports Combines as imports on the same line. @@ -521,8 +452,6 @@ Combines as imports on the same line. - --ca - --combine-as - - ## Combine Star Ensures that if a star import is present, nothing else is imported from that namespace. @@ -535,8 +464,6 @@ Ensures that if a star import is present, nothing else is imported from that nam - --cs - --combine-star - - ## Keep Direct And As Imports Turns off default behavior that removes direct imports when as imports exist. @@ -549,8 +476,6 @@ Turns off default behavior that removes direct imports when as imports exist. - -k - --keep-direct-and-as - - ## Include Trailing Comma Includes a trailing comma on multi line imports that include parentheses. @@ -563,8 +488,6 @@ Includes a trailing comma on multi line imports that include parentheses. - --tc - --trailing-comma - - ## From First Switches the typical ordering preference, showing from imports first then straight ones. @@ -577,8 +500,6 @@ Switches the typical ordering preference, showing from imports first then straig - --ff - --from-first - - ## Verbose Shows verbose output, such as when files are skipped or when a check is successful. @@ -591,8 +512,6 @@ Shows verbose output, such as when files are skipped or when a check is successf - -v - --verbose - - ## Quiet Shows extra quiet output, only errors are outputted. @@ -605,8 +524,6 @@ Shows extra quiet output, only errors are outputted. - -q - --quiet - - ## Force Adds Forces import adds even if the original file is empty. @@ -619,8 +536,6 @@ Forces import adds even if the original file is empty. - --af - --force-adds - - ## Force Alphabetical Sort Within Sections Force all imports to be sorted alphabetically within a section @@ -633,8 +548,6 @@ Force all imports to be sorted alphabetically within a section - --fass - --force-alphabetical-sort-within-sections - - ## Force Alphabetical Sort Force all imports to be sorted as a single section @@ -647,8 +560,6 @@ Force all imports to be sorted as a single section - --fas - --force-alphabetical-sort - - ## Force Grid Wrap Force number of from imports (defaults to 2) to be grid wrapped regardless of line length @@ -661,8 +572,6 @@ Force number of from imports (defaults to 2) to be grid wrapped regardless of li - --fgw - --force-grid-wrap - - ## Force Sort Within Sections Force imports to be sorted by module, independent of import_type @@ -675,8 +584,6 @@ Force imports to be sorted by module, independent of import_type - --fss - --force-sort-within-sections - - ## Lexicographical **No Description** @@ -686,9 +593,7 @@ Force imports to be sorted by module, independent of import_type **Python & Config File Name:** lexicographical **CLI Flags:** -- **Not Supported** - - +**Not Supported** ## Ignore Whitespace @@ -702,8 +607,6 @@ Tells isort to ignore whitespace differences when --check-only is being used. - --ws - --ignore-whitespace - - ## No Lines Before Sections which should not be split with previous by empty lines @@ -716,8 +619,6 @@ Sections which should not be split with previous by empty lines - --nlb - --no-lines-before - - ## No Inline Sort Leaves `from` imports with multiple imports 'as-is' (e.g. `from foo import a, c ,b`). @@ -730,8 +631,6 @@ Leaves `from` imports with multiple imports 'as-is' (e.g. `from foo import a, c - --nis - --no-inline-sort - - ## Ignore Comments **No Description** @@ -741,9 +640,7 @@ Leaves `from` imports with multiple imports 'as-is' (e.g. `from foo import a, c **Python & Config File Name:** ignore_comments **CLI Flags:** -- **Not Supported** - - +**Not Supported** ## Case Sensitive @@ -756,8 +653,6 @@ Tells isort to include casing when sorting module names - --case-sensitive - - ## Sources **No Description** @@ -767,9 +662,7 @@ Tells isort to include casing when sorting module names **Python & Config File Name:** sources **CLI Flags:** -- **Not Supported** - - +**Not Supported** ## Virtual Env @@ -782,8 +675,6 @@ Virtual environment to use for determining whether a package is third-party - --virtual-env - - ## Conda Env Conda environment to use for determining whether a package is third-party @@ -795,8 +686,6 @@ Conda environment to use for determining whether a package is third-party - --conda-env - - ## Ensure Newline Before Comments Inserts a blank line before a comment following an import. @@ -809,8 +698,6 @@ Inserts a blank line before a comment following an import. - -n - --ensure-newline-before-comments - - ## Directory **No Description** @@ -820,9 +707,7 @@ Inserts a blank line before a comment following an import. **Python & Config File Name:** directory **CLI Flags:** -- **Not Supported** - - +**Not Supported** ## Profile @@ -835,8 +720,6 @@ Base profile type to use for configuration. - --profile - - ## Honor Noqa Tells isort to honor noqa comments to enforce skipping those comments. @@ -848,8 +731,6 @@ Tells isort to honor noqa comments to enforce skipping those comments. - --honor-noqa - - ## Src Paths Add an explicitly defined source path (modules within src paths have their imports automatically catorgorized as first_party). @@ -862,8 +743,6 @@ Add an explicitly defined source path (modules within src paths have their impor - --src - --src-path - - ## Old Finders Use the old deprecated finder logic that relies on environment introspection magic. @@ -876,8 +755,6 @@ Use the old deprecated finder logic that relies on environment introspection mag - --old-finders - --magic-placement - - ## Check Checks the file for unsorted / unformatted imports and prints them to the command line without modifying the file. @@ -891,8 +768,6 @@ Checks the file for unsorted / unformatted imports and prints them to the comman - --check-only - --check - - ## Write To Stdout Force resulting output to stdout, instead of in-place. @@ -905,8 +780,6 @@ Force resulting output to stdout, instead of in-place. - -d - --stdout - - ## Show Diff Prints a diff of all the changes isort would make to a file, instead of changing it in place @@ -919,8 +792,6 @@ Prints a diff of all the changes isort would make to a file, instead of changing - --df - --diff - - ## Jobs Number of files to process in parallel. @@ -933,8 +804,6 @@ Number of files to process in parallel. - -j - --jobs - - ## Dont Order By Type Don't order imports by type, which is determined by case, in addition to alphabetically. @@ -950,8 +819,6 @@ Don't order imports by type, which is determined by case, in addition to alphabe - --dt - --dont-order-by-type - - ## Settings Path Explicitly set the settings path or file instead of auto determining based on file location. @@ -966,8 +833,6 @@ Explicitly set the settings path or file instead of auto determining based on fi - --settings-file - --settings - - ## Show Version Displays the currently installed version of isort. @@ -980,8 +845,6 @@ Displays the currently installed version of isort. - -V - --version - - ## Version Number Returns just the current version number without the logo @@ -994,8 +857,6 @@ Returns just the current version number without the logo - --vn - --version-number - - ## Filter Files Tells isort to filter files even when they are explicitly passed in as part of the command @@ -1007,8 +868,6 @@ Tells isort to filter files even when they are explicitly passed in as part of t - --filter-files - - ## Files One or more Python source files that need their imports sorted. @@ -1020,8 +879,6 @@ One or more Python source files that need their imports sorted. - - - ## Ask To Apply Tells isort to apply changes interactively. @@ -1033,8 +890,6 @@ Tells isort to apply changes interactively. - --interactive - - ## Show Config See isort's determined config, as well as sources of config options. @@ -1046,8 +901,6 @@ See isort's determined config, as well as sources of config options. - --show-config - - ## Deprecated Flags ==SUPPRESS== @@ -1058,5 +911,3 @@ See isort's determined config, as well as sources of config options. **CLI Flags:** - --apply - - diff --git a/scripts/build_config_option_docs.py b/scripts/build_config_option_docs.py index 3dbc41f5e..5cf3fd995 100755 --- a/scripts/build_config_option_docs.py +++ b/scripts/build_config_option_docs.py @@ -36,34 +36,37 @@ class Example: def __post_init__(self): if self.cfg or self.pyproject_toml or self.cli: if self.cfg: + cfg = dedent(self.cfg).lstrip() self.cfg = dedent( - f""" + """ ### Example `.isort.cfg` ``` - {self.cfg} + {cfg} ``` """ - ) + ).format(cfg=cfg) if self.pyproject_toml: + pyproject_toml = dedent(self.pyproject_toml).lstrip() self.pyproject_toml = dedent( - f""" + """ ### Example `pyproject.toml` ``` - {self.pyproject_toml} + {pyproject_toml} ``` """ - ) + ).format(pyproject_toml=pyproject_toml) if self.cli == "": + cli = dedent(self.cli).lstrip() self.cli = dedent( - f""" + """ ### Example cli usage - `{self.cli}` + `{cli}` """ - ) + ).format(cli=cli) self.section_complete = f"""**Examples:** @@ -81,9 +84,10 @@ def __str__(self): example_mapping: Dict[str, Example] example_mapping = { "known_other": Example( - pyproject_toml="""[tool.isort] - sections = ['FUTURE', 'STDLIB', 'THIRDPARTY', 'AIRFLOW', 'FIRSTPARTY', 'LOCALFOLDER'] - known_airflow = ['airflow']""" + pyproject_toml=""" + [tool.isort] + sections = ['FUTURE', 'STDLIB', 'THIRDPARTY', 'AIRFLOW', 'FIRSTPARTY', 'LOCALFOLDER'] + known_airflow = ['airflow']""" ) } From 74fc0b0ce18e29c51a45534b4bc02a77571531a7 Mon Sep 17 00:00:00 2001 From: r-richmond Date: Sun, 12 Jul 2020 20:49:23 -0700 Subject: [PATCH 0773/1439] add second example and cfg example --- docs/configuration/options.md | 68 ++++++++++++++--------------- scripts/build_config_option_docs.py | 47 +++++++++++++------- 2 files changed, 65 insertions(+), 50 deletions(-) diff --git a/docs/configuration/options.md b/docs/configuration/options.md index 81e5e1001..530113fe7 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -100,9 +100,7 @@ Forces line endings to the specified value. If not set, values will be guessed p **Type:** Tuple **Default:** `('FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER')` **Python & Config File Name:** sections -**CLI Flags:** - -**Not Supported** +**CLI Flags:** **Not Supported** ## No Sections @@ -182,13 +180,17 @@ Extra modules to be included in the list of ones in Python's standard library. **Type:** Dict **Default:** `{}` **Python & Config File Name:** known_other -**CLI Flags:** - -**Not Supported** +**CLI Flags:** **Not Supported** **Examples:** +### Example `.isort.cfg` +``` +[settings] +sections=FUTURE,STDLIB,THIRDPARTY,AIRFLOW,FIRSTPARTY,LOCALFOLDER +known_airflow=airflow +``` ### Example `pyproject.toml` @@ -198,7 +200,6 @@ sections = ['FUTURE', 'STDLIB', 'THIRDPARTY', 'AIRFLOW', 'FIRSTPARTY', 'LOCALFOL known_airflow = ['airflow'] ``` - ### Example cli usage `` @@ -214,6 +215,23 @@ Multi line output (0-grid, 1-vertical, 2-hanging, 3-vert-hanging, 4-vert-grid, 5 - -m - --multi-line +**Examples:** + +### Example `.isort.cfg` + +``` +multi_line_output=3 +``` + +### Example `pyproject.toml` + +``` +multi_line_output = 3 +``` + +### Example cli usage +`` + ## Forced Separate **No Description** @@ -221,9 +239,7 @@ Multi line output (0-grid, 1-vertical, 2-hanging, 3-vert-hanging, 4-vert-grid, 5 **Type:** Tuple **Default:** `()` **Python & Config File Name:** forced_separate -**CLI Flags:** - -**Not Supported** +**CLI Flags:** **Not Supported** ## Indent @@ -244,9 +260,7 @@ String to place for indents defaults to " " (4 spaces). **Type:** String **Default:** ` #` **Python & Config File Name:** comment_prefix -**CLI Flags:** - -**Not Supported** +**CLI Flags:** **Not Supported** ## Length Sort @@ -267,9 +281,7 @@ Sort imports by their string length. **Type:** Frozenset **Default:** `frozenset()` **Python & Config File Name:** length_sort_sections -**CLI Flags:** - -**Not Supported** +**CLI Flags:** **Not Supported** ## Add Imports @@ -350,9 +362,7 @@ Sets the default section for import options: ('FUTURE', 'STDLIB', 'THIRDPARTY', **Type:** Dict **Default:** `{}` **Python & Config File Name:** import_headings -**CLI Flags:** - -**Not Supported** +**CLI Flags:** **Not Supported** ## Balanced Wrapping @@ -424,9 +434,7 @@ Ensures the output doesn't save if the resulting file contains syntax errors. **Type:** Int **Default:** `1` **Python & Config File Name:** lines_between_sections -**CLI Flags:** - -**Not Supported** +**CLI Flags:** **Not Supported** ## Lines Between Types @@ -591,9 +599,7 @@ Force imports to be sorted by module, independent of import_type **Type:** Bool **Default:** `False` **Python & Config File Name:** lexicographical -**CLI Flags:** - -**Not Supported** +**CLI Flags:** **Not Supported** ## Ignore Whitespace @@ -638,9 +644,7 @@ Leaves `from` imports with multiple imports 'as-is' (e.g. `from foo import a, c **Type:** Bool **Default:** `False` **Python & Config File Name:** ignore_comments -**CLI Flags:** - -**Not Supported** +**CLI Flags:** **Not Supported** ## Case Sensitive @@ -660,9 +664,7 @@ Tells isort to include casing when sorting module names **Type:** Tuple **Default:** `()` **Python & Config File Name:** sources -**CLI Flags:** - -**Not Supported** +**CLI Flags:** **Not Supported** ## Virtual Env @@ -705,9 +707,7 @@ Inserts a blank line before a comment following an import. **Type:** String **Default:** `` **Python & Config File Name:** directory -**CLI Flags:** - -**Not Supported** +**CLI Flags:** **Not Supported** ## Profile diff --git a/scripts/build_config_option_docs.py b/scripts/build_config_option_docs.py index 5cf3fd995..6a094b547 100755 --- a/scripts/build_config_option_docs.py +++ b/scripts/build_config_option_docs.py @@ -37,36 +37,48 @@ def __post_init__(self): if self.cfg or self.pyproject_toml or self.cli: if self.cfg: cfg = dedent(self.cfg).lstrip() - self.cfg = dedent( - """ + self.cfg = ( + dedent( + """ ### Example `.isort.cfg` ``` {cfg} ``` """ - ).format(cfg=cfg) + ) + .format(cfg=cfg) + .lstrip() + ) if self.pyproject_toml: pyproject_toml = dedent(self.pyproject_toml).lstrip() - self.pyproject_toml = dedent( - """ + self.pyproject_toml = ( + dedent( + """ ### Example `pyproject.toml` ``` {pyproject_toml} ``` """ - ).format(pyproject_toml=pyproject_toml) + ) + .format(pyproject_toml=pyproject_toml) + .lstrip() + ) if self.cli == "": cli = dedent(self.cli).lstrip() - self.cli = dedent( - """ + self.cli = ( + dedent( + """ ### Example cli usage `{cli}` """ - ).format(cli=cli) + ) + .format(cli=cli) + .lstrip() + ) self.section_complete = f"""**Examples:** @@ -84,11 +96,16 @@ def __str__(self): example_mapping: Dict[str, Example] example_mapping = { "known_other": Example( + cfg=""" + [settings] + sections=FUTURE,STDLIB,THIRDPARTY,AIRFLOW,FIRSTPARTY,LOCALFOLDER + known_airflow=airflow""", pyproject_toml=""" [tool.isort] sections = ['FUTURE', 'STDLIB', 'THIRDPARTY', 'AIRFLOW', 'FIRSTPARTY', 'LOCALFOLDER'] - known_airflow = ['airflow']""" - ) + known_airflow = ['airflow']""", + ), + "multi_line_output": Example(cfg="multi_line_output=3", pyproject_toml="multi_line_output = 3"), } @@ -107,9 +124,9 @@ def __str__(self): return "" if self.cli_options == (): - cli_options = "**Not Supported**" + cli_options = " **Not Supported**" else: - cli_options = "- " + "\n- ".join(self.cli_options) + cli_options = "\n\n- " + "\n- ".join(self.cli_options) # new line if example otherwise nothing example = f"\n{self.example}" if self.example else "" @@ -121,9 +138,7 @@ def __str__(self): **Type:** {human(self.type.__name__)}{MD_NEWLINE} **Default:** `{self.default}`{MD_NEWLINE} **Python & Config File Name:** {self.config_name}{MD_NEWLINE} -**CLI Flags:** - -{cli_options} +**CLI Flags:**{cli_options} {example}""" From e2ec04ce43e423dfc1aff88ce95b372620b9560a Mon Sep 17 00:00:00 2001 From: r-richmond Date: Sun, 12 Jul 2020 21:04:02 -0700 Subject: [PATCH 0774/1439] add example cli usage --- docs/configuration/options.md | 12 ++++++------ scripts/build_config_option_docs.py | 10 ++++++---- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/docs/configuration/options.md b/docs/configuration/options.md index 530113fe7..eee79f7b2 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -200,9 +200,6 @@ sections = ['FUTURE', 'STDLIB', 'THIRDPARTY', 'AIRFLOW', 'FIRSTPARTY', 'LOCALFOL known_airflow = ['airflow'] ``` -### Example cli usage -`` - ## Multi Line Output Multi line output (0-grid, 1-vertical, 2-hanging, 3-vert-hanging, 4-vert-grid, 5-vert-grid-grouped, 6-vert-grid-grouped-no-comma). @@ -229,9 +226,6 @@ multi_line_output=3 multi_line_output = 3 ``` -### Example cli usage -`` - ## Forced Separate **No Description** @@ -845,6 +839,12 @@ Displays the currently installed version of isort. - -V - --version +**Examples:** + +### Example cli usage + +`isort --version` + ## Version Number Returns just the current version number without the logo diff --git a/scripts/build_config_option_docs.py b/scripts/build_config_option_docs.py index 6a094b547..54db3491d 100755 --- a/scripts/build_config_option_docs.py +++ b/scripts/build_config_option_docs.py @@ -67,12 +67,13 @@ def __post_init__(self): .lstrip() ) - if self.cli == "": + if self.cli: cli = dedent(self.cli).lstrip() self.cli = ( dedent( """ ### Example cli usage + `{cli}` """ ) @@ -80,11 +81,11 @@ def __post_init__(self): .lstrip() ) + sections = [s for s in [self.cfg, self.pyproject_toml, self.cli] if s] + sections_str = "\n".join(sections) self.section_complete = f"""**Examples:** -{self.cfg} -{self.pyproject_toml} -{self.cli}""" +{sections_str}""" else: self.section_complete = "" @@ -106,6 +107,7 @@ def __str__(self): known_airflow = ['airflow']""", ), "multi_line_output": Example(cfg="multi_line_output=3", pyproject_toml="multi_line_output = 3"), + "show_version": Example(cli="isort --version"), } From 8a8e5b3b7986dc1479386b994cdbd03744fb1f51 Mon Sep 17 00:00:00 2001 From: r-richmond Date: Sun, 12 Jul 2020 21:06:56 -0700 Subject: [PATCH 0775/1439] default headers for examples --- docs/configuration/options.md | 2 ++ scripts/build_config_option_docs.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/configuration/options.md b/docs/configuration/options.md index eee79f7b2..d0fc3add3 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -217,12 +217,14 @@ Multi line output (0-grid, 1-vertical, 2-hanging, 3-vert-hanging, 4-vert-grid, 5 ### Example `.isort.cfg` ``` +[settings] multi_line_output=3 ``` ### Example `pyproject.toml` ``` +[tool.isort] multi_line_output = 3 ``` diff --git a/scripts/build_config_option_docs.py b/scripts/build_config_option_docs.py index 54db3491d..a70ca755d 100755 --- a/scripts/build_config_option_docs.py +++ b/scripts/build_config_option_docs.py @@ -43,6 +43,7 @@ def __post_init__(self): ### Example `.isort.cfg` ``` + [settings] {cfg} ``` """ @@ -59,6 +60,7 @@ def __post_init__(self): ### Example `pyproject.toml` ``` + [tool.isort] {pyproject_toml} ``` """ @@ -98,11 +100,9 @@ def __str__(self): example_mapping = { "known_other": Example( cfg=""" - [settings] sections=FUTURE,STDLIB,THIRDPARTY,AIRFLOW,FIRSTPARTY,LOCALFOLDER known_airflow=airflow""", pyproject_toml=""" - [tool.isort] sections = ['FUTURE', 'STDLIB', 'THIRDPARTY', 'AIRFLOW', 'FIRSTPARTY', 'LOCALFOLDER'] known_airflow = ['airflow']""", ), From 1408b5944f98eb99c541767e570204e3b52009f3 Mon Sep 17 00:00:00 2001 From: r-richmond Date: Sun, 12 Jul 2020 21:10:20 -0700 Subject: [PATCH 0776/1439] default cleanup --- scripts/build_config_option_docs.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/build_config_option_docs.py b/scripts/build_config_option_docs.py index a70ca755d..917732bd6 100755 --- a/scripts/build_config_option_docs.py +++ b/scripts/build_config_option_docs.py @@ -117,7 +117,7 @@ class ConfigOption: type: Type = str default: Any = "" config_name: str = "**Not Supported**" - cli_options: Iterable[str] = tuple() + cli_options: Iterable[str] = (" **Not Supported**",) description: str = "**No Description**" example: Optional[Example] = None @@ -125,8 +125,8 @@ def __str__(self): if self.name in IGNORED: return "" - if self.cli_options == (): - cli_options = " **Not Supported**" + if self.cli_options == (" **Not Supported**",): + cli_options = self.cli_options[0] else: cli_options = "\n\n- " + "\n- ".join(self.cli_options) From ab18c41fb046ea6d838253c1ce79fdb309bc1bc7 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 12 Jul 2020 21:23:38 -0700 Subject: [PATCH 0777/1439] Fixed #1190: bad wrapping with long import name for from import. --- isort/parse.py | 6 +++++- tests/test_regressions.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/isort/parse.py b/isort/parse.py index 3baa421ce..125ba7fc1 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -288,7 +288,11 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte if type_of_import == "from": cimports: bool - import_string = import_string.replace("import(", "import (") + import_string = ( + import_string.replace("import(", "import (") + .replace("\\", " ") + .replace("\n", " ") + ) if " cimport " in import_string: parts = import_string.split(" cimport ") cimports = True diff --git a/tests/test_regressions.py b/tests/test_regressions.py index dd1754014..669a101a7 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -247,7 +247,7 @@ def test_windows_newline_issue_1278(): ) -def test_check_never_passes_with_indented_headings_1301(): +def test_check_never_passes_with_indented_headings_issue_1301(): """Test to ensure that test can pass even when there are indented headings. See: https://github.com/timothycrosley/isort/issues/1301 """ @@ -262,3 +262,30 @@ def test_check_never_passes_with_indented_headings_1301(): """, import_heading_stdlib="stdlib", ) + + +def test_isort_shouldnt_fail_on_long_from_with_dot_issue_1190(): + """Test to ensure that isort will correctly handle formatting a long from import that contains + a dot. + See: https://github.com/timothycrosley/isort/issues/1190 + """ + assert ( + isort.code( + """ +from this_is_a_very_long_import_statement.that_will_occur_across_two_lines\\ + .when_the_line_length.is_only_seventynine_chars import ( + function1, + function2, +) + """, + line_length=79, + multi_line_output=3, + ) + == """ +from this_is_a_very_long_import_statement.that_will_occur_across_two_lines""" + """.when_the_line_length.is_only_seventynine_chars import ( + function1, + function2 +) +""" + ) From f8e2e26600694d5a1d9c20da4dcf2bb188269011 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 12 Jul 2020 21:24:03 -0700 Subject: [PATCH 0778/1439] Add ticketed features test file --- tests/test_ticketed_features.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 tests/test_ticketed_features.py diff --git a/tests/test_ticketed_features.py b/tests/test_ticketed_features.py new file mode 100644 index 000000000..283d7c1b8 --- /dev/null +++ b/tests/test_ticketed_features.py @@ -0,0 +1,21 @@ +"""A growing set of tests designed to ensure when isort implements a feature described in a ticket +it fully works as defined in the associated ticket. +""" +import isort + + +def test_semicolon_ignored_for_dynamic_lines_after_import_issue_1178(): + """Test to ensure even if a semicolon is in the decorator in the line following an import + the correct line spacing detrmination will be made. + See: https://github.com/timothycrosley/isort/issues/1178. + """ + assert isort.check_code( + """ +import pytest + + +@pytest.mark.skip(';') +def test_thing(): pass +""", + show_diff=True, + ) From 7cde6c87122f41d624bef0400d0bb8aa7ece2a5e Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 13 Jul 2020 22:00:53 -0700 Subject: [PATCH 0779/1439] Fix issue #1320 --- docs/quick_start/2.-cli.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quick_start/2.-cli.md b/docs/quick_start/2.-cli.md index 4dcdd4d27..69ecd08c3 100644 --- a/docs/quick_start/2.-cli.md +++ b/docs/quick_start/2.-cli.md @@ -3,7 +3,7 @@ Once installed, `isort` exposes a command line utility for sorting, organizing, and formatting imports within Python and Cython source files. To verify the tool is installed correctly, run `isort` from the command line and you should be given the available commands and the version of isort installed. -For a list of all CLI options type `isort --help` or view [the online configuration reference](http://127.0.0.1:8000/docs/configuration/options/).: +For a list of all CLI options type `isort --help` or view [the online configuration reference](https://timothycrosley.github.io/isort/docs/configuration/options/).: From eacda16775ce067fac22785f06804d9490ed4fb0 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 13 Jul 2020 22:01:26 -0700 Subject: [PATCH 0780/1439] Fix issue #1320 --- docs/quick_start/2.-cli.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quick_start/2.-cli.md b/docs/quick_start/2.-cli.md index 69ecd08c3..844533f6a 100644 --- a/docs/quick_start/2.-cli.md +++ b/docs/quick_start/2.-cli.md @@ -3,7 +3,7 @@ Once installed, `isort` exposes a command line utility for sorting, organizing, and formatting imports within Python and Cython source files. To verify the tool is installed correctly, run `isort` from the command line and you should be given the available commands and the version of isort installed. -For a list of all CLI options type `isort --help` or view [the online configuration reference](https://timothycrosley.github.io/isort/docs/configuration/options/).: +For a list of all CLI options type `isort --help` or view [the online configuration reference](https://timothycrosley.github.io/isort/docs/configuration/options/): From ef2d3933087eb115ae7dc818de919c7263a3f49d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 13 Jul 2020 22:33:07 -0700 Subject: [PATCH 0781/1439] Remove dependency from settings on distutils --- isort/settings.py | 28 ++++++++++++++++++++++++++-- tests/test_settings.py | 15 +++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/isort/settings.py b/isort/settings.py index a94d58763..a3738c17c 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -8,7 +8,6 @@ import posixpath import re import sys -from distutils.util import strtobool as _as_bool from functools import lru_cache from pathlib import Path from typing import Any, Callable, Dict, FrozenSet, Iterable, List, Optional, Pattern, Set, Tuple @@ -64,6 +63,21 @@ DEPRECATED_SETTINGS = ("not_skip",) +_STR_BOOLEAN_MAPPING = { + "y": True, + "yes": True, + "t": True, + "on": True, + "1": True, + "true": True, + "n": False, + "no": False, + "f": False, + "off": False, + "0": False, + "false": False, +} + @dataclass(frozen=True) class _Config: @@ -536,7 +550,7 @@ def _get_config_data(file_path: str, sections: Tuple[str]) -> Dict[str, Any]: elif existing_value_type == bool: # Only some configuration formats support native boolean values. if not isinstance(value, bool): - value = bool(_as_bool(value)) + value = _as_bool(value) settings[key] = value elif key.startswith(KNOWN_PREFIX): settings[key] = _abspaths(os.path.dirname(file_path), _as_list(value)) @@ -554,4 +568,14 @@ def _get_config_data(file_path: str, sections: Tuple[str]) -> Dict[str, Any]: return settings +def _as_bool(value: str) -> bool: + """Given a string value that represents True or False, returns the Boolean equivalent. + Heavily inspired from distutils strtobool. + """ + try: + return _STR_BOOLEAN_MAPPING[value.lower()] + except KeyError: + raise ValueError(f"invalid truth value {value}") + + DEFAULT_CONFIG = Config() diff --git a/tests/test_settings.py b/tests/test_settings.py index 5c9d184b5..e47165d33 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -87,3 +87,18 @@ def test_get_config_data(tmpdir): assert loaded_settings["force_grid_wrap"] == 0 assert loaded_settings["indent"] == "\t" assert str(tmpdir) in loaded_settings["source"] + + +def test_as_bool(): + assert settings._as_bool("TrUe") is True + assert settings._as_bool("true") is True + assert settings._as_bool("t") is True + assert settings._as_bool("FALSE") is False + assert settings._as_bool("faLSE") is False + assert settings._as_bool("f") is False + with pytest.raises(ValueError): + settings._as_bool("") + with pytest.raises(ValueError): + settings._as_bool("falsey") + with pytest.raises(ValueError): + settings._as_bool("truthy") From 94c3d6b5097d640ae668511c5342ed16703492f9 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 13 Jul 2020 22:51:02 -0700 Subject: [PATCH 0782/1439] Fixed #1315: Extra newline before comment with -n + --fss. --- CHANGELOG.md | 5 +++-- isort/output.py | 2 +- tests/test_regressions.py | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6642f3ca..3bd87643e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,12 +3,13 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. -### 5.1.0 July 12, 2020 +### 5.1.0 July TBD, 2020 - isort now throws an exception if an invalid settings path is given (issue #1174). - Fixed #1178: support for semicolons in decorators. + - Fixed #1315: Extra newline before comment with -n + --fss. ### 5.0.9 July 11, 2020 - - Fixed #1301: Import headings in nested sections leads to check errors +is ### 5.0.8 July 11, 2020 - Fixed #1277 & #1278: New line detection issues on Windows. diff --git a/isort/output.py b/isort/output.py index 493ba4fb8..2ca4c60d7 100644 --- a/isort/output.py +++ b/isort/output.py @@ -132,7 +132,7 @@ def sorted_imports( for line in new_section_output: comments = getattr(line, "comments", ()) if comments: - if new_section_output and config.ensure_newline_before_comments: + if section_output and config.ensure_newline_before_comments: section_output.append("") section_output.extend(comments) section_output.append(str(line)) diff --git a/tests/test_regressions.py b/tests/test_regressions.py index 669a101a7..77521b089 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -289,3 +289,37 @@ def test_isort_shouldnt_fail_on_long_from_with_dot_issue_1190(): ) """ ) + + +def test_isort_shouldnt_add_extra_new_line_when_fass_and_n_issue_1315(): + """Test to ensure isort doesnt add a second extra new line when combining --fss and -n options. + See: https://github.com/timothycrosley/isort/issues/1315 + """ + assert isort.check_code( + """import sys + +# Comment canary +from . import foo +""", + ensure_newline_before_comments=True, # -n + force_sort_within_sections=True, # -fss + show_diff=True, # for better debugging in the case the test case fails. + ) + + assert ( + isort.code( + """ +from . import foo +# Comment canary +from .. import foo +""", + ensure_newline_before_comments=True, + force_sort_within_sections=True, + ) + == """ +from . import foo + +# Comment canary +from .. import foo +""" + ) From 95dab39e346e0c1665ba9cd716f96de047a5cec6 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 13 Jul 2020 23:32:42 -0700 Subject: [PATCH 0783/1439] Fixed #1280: rewrite of as imports changes the behavior of the imports --- CHANGELOG.md | 2 ++ isort/hooks.py | 15 ++++++++++----- isort/output.py | 9 +++++---- isort/parse.py | 19 +++++++++---------- tests/test_isort.py | 25 +++++++++++++------------ tests/test_regressions.py | 17 +++++++++++++++++ 6 files changed, 56 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bd87643e..ced2475a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. - isort now throws an exception if an invalid settings path is given (issue #1174). - Fixed #1178: support for semicolons in decorators. - Fixed #1315: Extra newline before comment with -n + --fss. +**Formatting changes implied:** + - Fixed #1280: rewrite of as imports changes the behavior of the imports. ### 5.0.9 July 11, 2020 is diff --git a/isort/hooks.py b/isort/hooks.py index 13b0b7434..2f3decd4c 100644 --- a/isort/hooks.py +++ b/isort/hooks.py @@ -8,7 +8,7 @@ from pathlib import Path from typing import List -from isort import Config, api +from isort import Config, api, exceptions def get_output(command: List[str]) -> str: @@ -61,9 +61,14 @@ def git_hook(strict: bool = False, modify: bool = False) -> int: staged_cmd = ["git", "show", f":{filename}"] staged_contents = get_output(staged_cmd) - if not api.check_code_string(staged_contents, file_path=Path(filename), config=config): - errors += 1 - if modify: - api.sort_file(filename, config=config) + try: + if not api.check_code_string(staged_contents, + file_path=Path(filename), + config=config): + errors += 1 + if modify: + api.sort_file(filename, config=config) + except exceptions.FileSkipComment: + pass return errors if strict else 0 diff --git a/isort/output.py b/isort/output.py index 2ca4c60d7..b3cc785ce 100644 --- a/isort/output.py +++ b/isort/output.py @@ -255,10 +255,10 @@ def _with_from_imports( sub_modules = [f"{module}.{from_import}" for from_import in from_imports] as_imports = { from_import: [ - f"{from_import} as {as_module}" for as_module in parsed.as_map[sub_module] + f"{from_import} as {as_module}" for as_module in parsed.as_map["from"][sub_module] ] for from_import, sub_module in zip(from_imports, sub_modules) - if sub_module in parsed.as_map + if sub_module in parsed.as_map["from"] } if config.combine_as_imports and not ("*" in from_imports and config.combine_star): if not config.no_inline_sort: @@ -487,11 +487,12 @@ def _with_straight_imports( continue import_definition = [] - if module in parsed.as_map: + if module in parsed.as_map["straight"]: if config.keep_direct_and_as_imports and parsed.imports[section]["straight"][module]: import_definition.append(f"{import_type} {module}") import_definition.extend( - f"{import_type} {module} as {as_import}" for as_import in parsed.as_map[module] + f"{import_type} {module} as {as_import}" + for as_import in parsed.as_map["straight"][module] ) else: import_definition.append(f"{import_type} {module}") diff --git a/isort/parse.py b/isort/parse.py index 125ba7fc1..7f112fc14 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -131,7 +131,7 @@ class ParsedContent(NamedTuple): import_index: int place_imports: Dict[str, List[str]] import_placements: Dict[str, str] - as_map: Dict[str, List[str]] + as_map: Dict[str, Dict[str, List[str]]] imports: Dict[str, Dict[str, Any]] categorized_comments: "CommentsDict" change_count: int @@ -155,7 +155,10 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte place_imports: Dict[str, List[str]] = {} import_placements: Dict[str, str] = {} - as_map: Dict[str, List[str]] = defaultdict(list) + as_map: Dict[str, Dict[str, List[str]]] = { + "straight": defaultdict(list), + "from": defaultdict(list), + } imports: OrderedDict[str, Dict[str, Any]] = OrderedDict() for section in chain(config.sections, config.forced_separate): imports[section] = {"straight": OrderedDict(), "from": OrderedDict()} @@ -318,17 +321,13 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte if type_of_import == "from": module = just_imports[0] + "." + just_imports[as_index - 1] as_name = just_imports[as_index + 1] - if as_name not in as_map[module]: - as_map[module].append(as_name) + if as_name not in as_map["from"][module]: + as_map["from"][module].append(as_name) else: module = just_imports[as_index - 1] as_name = just_imports[as_index + 1] - if as_name not in as_map[module]: - as_map[module].append(as_name) - if "." in module: - type_of_import = "from" - just_imports[:as_index] = module.rsplit(".", 1) - as_index = just_imports.index("as") + if as_name not in as_map["straight"][module]: + as_map["straight"][module].append(as_name) if not config.combine_as_imports: categorized_comments["straight"][module] = comments comments = [] diff --git a/tests/test_isort.py b/tests/test_isort.py index c13e37a65..e39b7ed86 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -1784,14 +1784,14 @@ def test_placement_control() -> None: assert test_output == ( "import os\n" + "import p24.imports._argparse as argparse\n" + "import p24.imports._subprocess as subprocess\n" "import sys\n" - "from p24.imports import _VERSION as VERSION\n" - "from p24.imports import _argparse as argparse\n" - "from p24.imports import _subprocess as subprocess\n" "\n" "from bottle import Bottle, redirect, response, run\n" "\n" - "from p24.shared import media_wiki_syntax as syntax\n" + "import p24.imports._VERSION as VERSION\n" + "import p24.shared.media_wiki_syntax as syntax\n" ) @@ -1836,10 +1836,9 @@ def test_custom_sections() -> None: assert test_output == ( "# Standard Library\n" "import os\n" + "import p24.imports._argparse as argparse\n" + "import p24.imports._subprocess as subprocess\n" "import sys\n" - "from p24.imports import _VERSION as VERSION\n" - "from p24.imports import _argparse as argparse\n" - "from p24.imports import _subprocess as subprocess\n" "\n" "# Django\n" "from django.conf import settings\n" @@ -1853,7 +1852,8 @@ def test_custom_sections() -> None: "import pandas as pd\n" "\n" "# First Party\n" - "from p24.shared import media_wiki_syntax as syntax\n" + "import p24.imports._VERSION as VERSION\n" + "import p24.shared.media_wiki_syntax as syntax\n" ) @@ -4061,9 +4061,10 @@ def test_isort_ensures_blank_line_between_import_and_comment() -> None: "import one.b\n" "\n" "# noinspection PyUnresolvedReferences\n" + "import two.a as aa\n" + "\n" "# noinspection PyUnresolvedReferences\n" - "from two import a as aa\n" - "from two import b as bb\n" + "import two.b as bb\n" "\n" "# noinspection PyUnresolvedReferences\n" "from three.a import a\n" @@ -4810,8 +4811,8 @@ def test_as_imports_mixed(): test_input = """from datetime import datetime import datetime.datetime as dt """ - expected_output = """from datetime import datetime -from datetime import datetime as dt + expected_output = """import datetime.datetime as dt +from datetime import datetime """ assert isort.code(test_input, keep_direct_and_as_imports=True) == expected_output diff --git a/tests/test_regressions.py b/tests/test_regressions.py index 77521b089..d1f3a8567 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -323,3 +323,20 @@ def test_isort_shouldnt_add_extra_new_line_when_fass_and_n_issue_1315(): from .. import foo """ ) + + +def test_isort_doesnt_rewrite_import_with_dot_to_from_import_issue_1280(): + """Test to ensure isort doesn't rewrite imports in the from of import y.x into from y import x. + This is because they are not technically fully equivalent to eachother and can introduce broken + behaviour. + See: https://github.com/timothycrosley/isort/issues/1280 + """ + assert isort.check_code( + """ + import test.module + import test.module as m + from test import module + from test import module as m + """, + show_diff=True, + ) From 56baaee8b4f80d8d8d2bf5480deb2a2c2b78f251 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 13 Jul 2020 23:56:37 -0700 Subject: [PATCH 0784/1439] Implemented support for automatic redundant alias removal (issue #1281). --- CHANGELOG.md | 2 ++ isort/hooks.py | 8 ++++---- isort/main.py | 10 ++++++++++ isort/parse.py | 11 ++++++++--- isort/settings.py | 1 + tests/test_hooks.py | 14 +++++++++++++- tests/test_ticketed_features.py | 13 +++++++++++++ 7 files changed, 51 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ced2475a3..587259041 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,10 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. ### 5.1.0 July TBD, 2020 - isort now throws an exception if an invalid settings path is given (issue #1174). + - Implemented support for automatic redundant alias removal (issue #1281). - Fixed #1178: support for semicolons in decorators. - Fixed #1315: Extra newline before comment with -n + --fss. + **Formatting changes implied:** - Fixed #1280: rewrite of as imports changes the behavior of the imports. diff --git a/isort/hooks.py b/isort/hooks.py index 2f3decd4c..892bd312c 100644 --- a/isort/hooks.py +++ b/isort/hooks.py @@ -62,13 +62,13 @@ def git_hook(strict: bool = False, modify: bool = False) -> int: staged_contents = get_output(staged_cmd) try: - if not api.check_code_string(staged_contents, - file_path=Path(filename), - config=config): + if not api.check_code_string( + staged_contents, file_path=Path(filename), config=config + ): errors += 1 if modify: api.sort_file(filename, config=config) - except exceptions.FileSkipComment: + except exceptions.FileSkipped: # pragma: no cover pass return errors if strict else 0 diff --git a/isort/main.py b/isort/main.py index 0d0418636..320e3f898 100644 --- a/isort/main.py +++ b/isort/main.py @@ -585,6 +585,16 @@ def _build_arg_parser() -> argparse.ArgumentParser: action="store_true", help="Tells isort to honor noqa comments to enforce skipping those comments.", ) + parser.add_argument( + "--remove-redundant-aliases", + dest="remove_redundant_aliases", + action="store_true", + help=( + "Tells isort to remove redundant aliases from imports, such as import os as os." + " This defaults to false simply because some projects use these seemingly useless alias" + " to signify intent and change behaviour." + ), + ) # deprecated options parser.add_argument( diff --git a/isort/parse.py b/isort/parse.py index 7f112fc14..94b1e3161 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -319,14 +319,19 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte while "as" in just_imports: as_index = just_imports.index("as") if type_of_import == "from": - module = just_imports[0] + "." + just_imports[as_index - 1] + nested_module = just_imports[as_index - 1] + module = just_imports[0] + "." + nested_module as_name = just_imports[as_index + 1] - if as_name not in as_map["from"][module]: + if nested_module == as_name and config.remove_redundant_aliases: + pass + elif as_name not in as_map["from"][module]: as_map["from"][module].append(as_name) else: module = just_imports[as_index - 1] as_name = just_imports[as_index + 1] - if as_name not in as_map["straight"][module]: + if module == as_name and config.remove_redundant_aliases: + pass + elif as_name not in as_map["straight"][module]: as_map["straight"][module].append(as_name) if not config.combine_as_imports: categorized_comments["straight"][module] = comments diff --git a/isort/settings.py b/isort/settings.py index a3738c17c..80938dbd5 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -166,6 +166,7 @@ class _Config: honor_noqa: bool = False src_paths: FrozenSet[Path] = frozenset() old_finders: bool = False + remove_redundant_aliases: bool = False def __post_init__(self): py_version = self.py_version diff --git a/tests/test_hooks.py b/tests/test_hooks.py index 49ba8b45b..c84d6d5c9 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -2,7 +2,7 @@ from io import BytesIO from unittest.mock import MagicMock, patch -from isort import hooks +from isort import exceptions, hooks def test_git_hook(src_dir): @@ -26,3 +26,15 @@ class FakeProcessResponse(object): with patch("subprocess.run", MagicMock(return_value=FakeProcessResponse())) as run_mock: with patch("isort.api", MagicMock(return_value=False)): hooks.git_hook(modify=True) + + # Test with skipped file returned from git + with patch( + "isort.hooks.get_lines", MagicMock(return_value=[os.path.join(src_dir, "main.py")]) + ) as run_mock: + + class FakeProcessResponse(object): + stdout = b"# isort: skip-file\nimport b\nimport a\n" + + with patch("subprocess.run", MagicMock(return_value=FakeProcessResponse())) as run_mock: + with patch("isort.api", MagicMock(side_effect=exceptions.FileSkipped("", ""))): + hooks.git_hook(modify=True) diff --git a/tests/test_ticketed_features.py b/tests/test_ticketed_features.py index 283d7c1b8..2709a7690 100644 --- a/tests/test_ticketed_features.py +++ b/tests/test_ticketed_features.py @@ -19,3 +19,16 @@ def test_thing(): pass """, show_diff=True, ) + + +def test_isort_automatically_removes_duplicate_aliases_issue_1193(): + """Test to ensure isort can automatically remove duplicate aliases. + See: https://github.com/timothycrosley/isort/issues/1281 + """ + assert isort.check_code("from urllib import parse as parse\n", show_diff=True) + assert ( + isort.code("from urllib import parse as parse", remove_redundant_aliases=True) + == "from urllib import parse\n" + ) + assert isort.check_code("import os as os\n", show_diff=True) + assert isort.code("import os as os", remove_redundant_aliases=True) == "import os\n" From e605d518c0808e9fb34dedbf1dbf7cc68f6083af Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 14 Jul 2020 20:20:24 -0700 Subject: [PATCH 0785/1439] Update docs --- docs/configuration/options.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/configuration/options.md b/docs/configuration/options.md index d0fc3add3..e29ceffda 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -751,6 +751,17 @@ Use the old deprecated finder logic that relies on environment introspection mag - --old-finders - --magic-placement +## Remove Redundant Aliases + +Tells isort to remove redundant aliases from imports, such as import os as os. This defaults to false simply because some projects use these seemingly useless alias to signify intent and change behaviour. + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** remove_redundant_aliases +**CLI Flags:** + +- --remove-redundant-aliases + ## Check Checks the file for unsorted / unformatted imports and prints them to the command line without modifying the file. From fafe61d452c1ef50ebeff3b3a7b813444d4e1bfe Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 14 Jul 2020 20:24:26 -0700 Subject: [PATCH 0786/1439] Nicer formatting for redundent import docs --- docs/configuration/options.md | 2 +- isort/main.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/configuration/options.md b/docs/configuration/options.md index e29ceffda..86ad6da4b 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -753,7 +753,7 @@ Use the old deprecated finder logic that relies on environment introspection mag ## Remove Redundant Aliases -Tells isort to remove redundant aliases from imports, such as import os as os. This defaults to false simply because some projects use these seemingly useless alias to signify intent and change behaviour. +Tells isort to remove redundant aliases from imports, such as `import os as os`. This defaults to `False` simply because some projects use these seemingly useless aliases to signify intent and change behaviour. **Type:** Bool **Default:** `False` diff --git a/isort/main.py b/isort/main.py index 320e3f898..30b405728 100644 --- a/isort/main.py +++ b/isort/main.py @@ -590,9 +590,9 @@ def _build_arg_parser() -> argparse.ArgumentParser: dest="remove_redundant_aliases", action="store_true", help=( - "Tells isort to remove redundant aliases from imports, such as import os as os." - " This defaults to false simply because some projects use these seemingly useless alias" - " to signify intent and change behaviour." + "Tells isort to remove redundant aliases from imports, such as `import os as os`." + " This defaults to `False` simply because some projects use these seemingly useless " + " aliases to signify intent and change behaviour." ), ) From fd87001295467c87612a8cdbcaa4c3239f4989b6 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 14 Jul 2020 20:53:46 -0700 Subject: [PATCH 0787/1439] Fix second part of #1275, when add_imports is used --- isort/api.py | 3 +++ tests/test_regressions.py | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/isort/api.py b/isort/api.py index 136141a22..3aa25dc01 100644 --- a/isort/api.py +++ b/isort/api.py @@ -456,6 +456,9 @@ def _sort_imports( indent = line[: -len(line.lstrip())] elif not (stripped_line or contains_imports): if add_imports and not indent: + if not import_section: + output_stream.write(line) + line = "" import_section += line_separator.join(add_imports) + line_separator contains_imports = True add_imports = [] diff --git a/tests/test_regressions.py b/tests/test_regressions.py index d1f3a8567..516ce7088 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -56,6 +56,26 @@ def test_blank_lined_removed_issue_1275(): My docstring """ +from a import other_thing +from b import thing +''' + ) + + assert ( + isort.code( + '''""" +My docstring +""" + +from b import thing +from a import other_thing +''', + add_imports=["from b import thing"], + ) + == '''""" +My docstring +""" + from a import other_thing from b import thing ''' From 0d37ec2af515501cae1516ab3aa03794ebbf5215 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 14 Jul 2020 21:20:37 -0700 Subject: [PATCH 0788/1439] Fixed #1192: or option has been deprecated as it is now always on. --- CHANGELOG.md | 1 + isort/main.py | 18 +-- isort/output.py | 22 +--- isort/settings.py | 3 +- tests/test_isort.py | 268 +++----------------------------------------- 5 files changed, 30 insertions(+), 282 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 587259041..21f1b7254 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. - Implemented support for automatic redundant alias removal (issue #1281). - Fixed #1178: support for semicolons in decorators. - Fixed #1315: Extra newline before comment with -n + --fss. + - Fixed #1192: `-k` or `--keep-direct-and-as-imports` option has been deprecated as it is now always on. **Formatting changes implied:** - Fixed #1280: rewrite of as imports changes the behavior of the imports. diff --git a/isort/main.py b/isort/main.py index 30b405728..589db60fc 100644 --- a/isort/main.py +++ b/isort/main.py @@ -289,13 +289,6 @@ def _build_arg_parser() -> argparse.ArgumentParser: parser.add_argument( "-j", "--jobs", help="Number of files to process in parallel.", dest="jobs", type=int ) - parser.add_argument( - "-k", - "--keep-direct-and-as", - dest="keep_direct_and_as_imports", - action="store_true", - help="Turns off default behavior that removes direct imports when as imports exist.", - ) parser.add_argument("--lai", "--lines-after-imports", dest="lines_after_imports", type=int) parser.add_argument("--lbt", "--lines-between-types", dest="lines_between_types", type=int) parser.add_argument( @@ -621,10 +614,17 @@ def _build_arg_parser() -> argparse.ArgumentParser: "--apply", dest="deprecated_flags", action="append_const", - const="-y", + const="--apply", + help=argparse.SUPPRESS, + ) + parser.add_argument( + "-k", + "--keep-direct-and-as", + dest="deprecated_flags", + action="append_const", + const="--keep-direct-and-as", help=argparse.SUPPRESS, ) - return parser diff --git a/isort/output.py b/isort/output.py index b3cc785ce..247dbaeb6 100644 --- a/isort/output.py +++ b/isort/output.py @@ -267,10 +267,7 @@ def _with_from_imports( for from_import in copy.copy(from_imports): if from_import in as_imports: idx = from_imports.index(from_import) - if ( - config.keep_direct_and_as_imports - and parsed.imports[section]["from"][module][from_import] - ): + if parsed.imports[section]["from"][module][from_import]: from_imports[(idx + 1) : (idx + 1)] = as_imports.pop(from_import) else: from_imports[idx : (idx + 1)] = as_imports.pop(from_import) @@ -313,10 +310,7 @@ def _with_from_imports( f"{comments and ';' or config.comment_prefix} " f"{comment}" ) if from_import in as_imports: - if ( - config.keep_direct_and_as_imports - and parsed.imports[section]["from"][module][from_import] - ): + if parsed.imports[section]["from"][module][from_import]: new_section_output.append( wrap.line(single_import_line, parsed.line_separator, config) ) @@ -344,10 +338,7 @@ def _with_from_imports( from_comments = parsed.categorized_comments["straight"].get( f"{module}.{from_import}" ) - if ( - config.keep_direct_and_as_imports - and parsed.imports[section]["from"][module][from_import] - ): + if parsed.imports[section]["from"][module][from_import]: new_section_output.append( with_comments( from_comments, @@ -383,8 +374,6 @@ def _with_from_imports( comments = None for from_import in copy.copy(from_imports): - if from_import in as_imports and not config.keep_direct_and_as_imports: - continue comment = ( parsed.categorized_comments["nested"].get(module, {}).pop(from_import, None) ) @@ -408,8 +397,7 @@ def _with_from_imports( while from_imports and ( from_imports[0] not in as_imports or ( - config.keep_direct_and_as_imports - and config.combine_as_imports + config.combine_as_imports and parsed.imports[section]["from"][module][from_import] ) ): @@ -488,7 +476,7 @@ def _with_straight_imports( import_definition = [] if module in parsed.as_map["straight"]: - if config.keep_direct_and_as_imports and parsed.imports[section]["straight"][module]: + if parsed.imports[section]["straight"][module]: import_definition.append(f"{import_type} {module}") import_definition.extend( f"{import_type} {module} as {as_import}" diff --git a/isort/settings.py b/isort/settings.py index 80938dbd5..437cca597 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -61,7 +61,7 @@ RUNTIME_SOURCE = "runtime" -DEPRECATED_SETTINGS = ("not_skip",) +DEPRECATED_SETTINGS = ("not_skip", "keep_direct_and_as_imports") _STR_BOOLEAN_MAPPING = { "y": True, @@ -141,7 +141,6 @@ class _Config: lines_between_types: int = 0 combine_as_imports: bool = False combine_star: bool = False - keep_direct_and_as_imports: bool = True include_trailing_comma: bool = False from_first: bool = False verbose: bool = False diff --git a/tests/test_isort.py b/tests/test_isort.py index e39b7ed86..639d35be4 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -807,7 +807,7 @@ def test_add_imports() -> None: ) # On a file that has no pre-existing imports - test_input = '"""Module docstring"""\n' "\nclass MyClass(object):\n pass\n" + test_input = '"""Module docstring"""\n' "class MyClass(object):\n pass\n" test_output = isort.code(code=test_input, add_imports=["from __future__ import print_function"]) assert test_output == ( '"""Module docstring"""\n' @@ -1376,7 +1376,7 @@ def test_combined_from_and_as_imports() -> None: assert isort.code(test_input, combine_as_imports=True) == test_input test_input = "import os \nimport os as _os" test_output = "import os\nimport os as _os\n" - assert isort.code(test_input, keep_direct_and_as_imports=True) == test_output + assert isort.code(test_input) == test_output def test_as_imports_with_line_length() -> None: @@ -3310,11 +3310,9 @@ def test_multiple_as_imports() -> None: assert test_output == test_input test_output = isort.code(test_input, combine_as_imports=True) assert test_output == "from a import b as b, b as bb, b as bb_\n" - test_output = isort.code(test_input, keep_direct_and_as_imports=True) + test_output = isort.code(test_input) assert test_output == test_input - test_output = isort.code( - code=test_input, combine_as_imports=True, keep_direct_and_as_imports=True - ) + test_output = isort.code(code=test_input, combine_as_imports=True) assert test_output == "from a import b as b, b as bb, b as bb_\n" test_input = ( @@ -3323,17 +3321,9 @@ def test_multiple_as_imports() -> None: "from a import b as bb\n" "from a import b as bb_\n" ) - test_output = isort.code(test_input, keep_direct_and_as_imports=False) - assert test_output == "from a import b as b\nfrom a import b as bb\nfrom a import b as bb_\n" - test_output = isort.code( - code=test_input, combine_as_imports=True, keep_direct_and_as_imports=False - ) - assert test_output == "from a import b as b, b as bb, b as bb_\n" - test_output = isort.code(test_input, keep_direct_and_as_imports=True) + test_output = isort.code(test_input) assert test_output == test_input - test_output = isort.code( - code=test_input, combine_as_imports=True, keep_direct_and_as_imports=True - ) + test_output = isort.code(code=test_input, combine_as_imports=True) assert test_output == "from a import b, b as b, b as bb, b as bb_\n" test_input = ( @@ -3342,57 +3332,27 @@ def test_multiple_as_imports() -> None: "from a import b\n" "from a import b as f\n" ) - test_output = isort.code(test_input, keep_direct_and_as_imports=False) - assert test_output == "from a import b as c\nfrom a import b as e\nfrom a import b as f\n" - test_output = isort.code( - code=test_input, combine_as_imports=True, keep_direct_and_as_imports=False - ) - assert test_output == "from a import b as c, b as e, b as f\n" - test_output = isort.code(test_input, keep_direct_and_as_imports=True) + test_output = isort.code(test_input) assert ( test_output == "from a import b\nfrom a import b as c\nfrom a import b as e\nfrom a import b as f\n" ) - test_output = isort.code(code=test_input, no_inline_sort=True, keep_direct_and_as_imports=False) - assert test_output == "from a import b as c\nfrom a import b as e\nfrom a import b as f\n" - test_output = isort.code(code=test_input, keep_direct_and_as_imports=True, no_inline_sort=True) + test_output = isort.code(code=test_input, no_inline_sort=True) assert ( test_output == "from a import b\nfrom a import b as c\nfrom a import b as e\nfrom a import b as f\n" ) - test_output = isort.code( - code=test_input, combine_as_imports=True, keep_direct_and_as_imports=True - ) + test_output = isort.code(code=test_input, combine_as_imports=True) assert test_output == "from a import b, b as c, b as e, b as f\n" - test_output = isort.code( - code=test_input, - combine_as_imports=True, - no_inline_sort=True, - keep_direct_and_as_imports=False, - ) - assert test_output == "from a import b as e, b as c, b as f\n" - test_output = isort.code( - code=test_input, - combine_as_imports=True, - keep_direct_and_as_imports=True, - no_inline_sort=True, - ) + test_output = isort.code(code=test_input, combine_as_imports=True, no_inline_sort=True) assert test_output == "from a import b, b as e, b as c, b as f\n" test_input = "import a as a\nimport a as aa\nimport a as aa_\n" - test_output = isort.code(test_input, keep_direct_and_as_imports=False) - assert test_output == test_input - test_output = isort.code( - code=test_input, combine_as_imports=True, keep_direct_and_as_imports=True - ) + test_output = isort.code(code=test_input, combine_as_imports=True) assert test_output == test_input - test_input = "import a\nimport a as a\nimport a as aa\nimport a as aa_\n" - test_output = isort.code(test_input, keep_direct_and_as_imports=False) assert test_output == "import a as a\nimport a as aa\nimport a as aa_\n" - test_output = isort.code( - code=test_input, combine_as_imports=True, keep_direct_and_as_imports=True - ) + test_output = isort.code(code=test_input, combine_as_imports=True) assert test_output == test_input @@ -3411,47 +3371,6 @@ def test_all_imports_from_single_module() -> None: code=test_input, combine_star=False, combine_as_imports=False, - keep_direct_and_as_imports=False, - force_single_line=False, - no_inline_sort=False, - ) - assert test_output == ( - "import a\n" - "from a import *\n" - "from a import b as c\n" - "from a import b as d\n" - "from a import e as f\n" - "from a import g as h\n" - "from a import i as j\n" - "from a import w, x, y, z\n" - ) - test_output = isort.code( - code=test_input, - combine_star=True, - combine_as_imports=False, - keep_direct_and_as_imports=False, - force_single_line=False, - no_inline_sort=False, - ) - assert test_output == "import a\nfrom a import *\n" - test_output = isort.code( - code=test_input, - combine_star=False, - combine_as_imports=True, - keep_direct_and_as_imports=False, - force_single_line=False, - no_inline_sort=False, - ) - assert test_output == ( - "import a\n" - "from a import *\n" - "from a import b as c, b as d, e as f, g as h, i as j, w, x, y, z\n" - ) - test_output = isort.code( - code=test_input, - combine_star=False, - combine_as_imports=False, - keep_direct_and_as_imports=True, force_single_line=False, no_inline_sort=False, ) @@ -3466,86 +3385,18 @@ def test_all_imports_from_single_module() -> None: "from a import i as j\n" "from a import w, x, y, z\n" ) - test_output = isort.code( - code=test_input, - combine_star=False, - combine_as_imports=False, - keep_direct_and_as_imports=False, - force_single_line=True, - no_inline_sort=False, - ) - assert test_output == ( - "import a\n" - "from a import *\n" - "from a import b as c\n" - "from a import b as d\n" - "from a import e as f\n" - "from a import g as h\n" - "from a import i as j\n" - "from a import w\n" - "from a import x\n" - "from a import y\n" - "from a import z\n" - ) - test_output = isort.code( - code=test_input, - combine_star=False, - combine_as_imports=False, - keep_direct_and_as_imports=False, - force_single_line=False, - no_inline_sort=True, - ) - assert test_output == ( - "import a\n" - "from a import *\n" - "from a import b as c\n" - "from a import b as d\n" - "from a import z, x, y, w\n" - "from a import i as j\n" - "from a import g as h\n" - "from a import e as f\n" - ) - test_output = isort.code( - code=test_input, - combine_star=True, - combine_as_imports=True, - keep_direct_and_as_imports=False, - force_single_line=False, - no_inline_sort=False, - ) - assert test_output == "import a\nfrom a import *\n" test_output = isort.code( code=test_input, combine_star=True, combine_as_imports=False, - keep_direct_and_as_imports=True, force_single_line=False, no_inline_sort=False, ) assert test_output == "import a\nfrom a import *\n" - test_output = isort.code( - code=test_input, - combine_star=True, - combine_as_imports=False, - keep_direct_and_as_imports=False, - force_single_line=True, - no_inline_sort=False, - ) - assert test_output == "import a\nfrom a import *\n" - test_output = isort.code( - code=test_input, - combine_star=True, - combine_as_imports=False, - keep_direct_and_as_imports=False, - force_single_line=False, - no_inline_sort=True, - ) - assert test_output == "import a\nfrom a import *\n" test_output = isort.code( code=test_input, combine_star=False, combine_as_imports=True, - keep_direct_and_as_imports=True, force_single_line=False, no_inline_sort=False, ) @@ -3554,45 +3405,10 @@ def test_all_imports_from_single_module() -> None: "from a import *\n" "from a import b, b as c, b as d, e as f, g as h, i as j, w, x, y, z\n" ) - test_output = isort.code( - code=test_input, - combine_star=False, - combine_as_imports=True, - keep_direct_and_as_imports=False, - force_single_line=True, - no_inline_sort=False, - ) - assert test_output == ( - "import a\n" - "from a import *\n" - "from a import b as c\n" - "from a import b as d\n" - "from a import e as f\n" - "from a import g as h\n" - "from a import i as j\n" - "from a import w\n" - "from a import x\n" - "from a import y\n" - "from a import z\n" - ) - test_output = isort.code( - code=test_input, - combine_star=False, - combine_as_imports=True, - keep_direct_and_as_imports=False, - force_single_line=False, - no_inline_sort=True, - ) - assert test_output == ( - "import a\n" - "from a import *\n" - "from a import b as d, b as c, z, x, y, w, i as j, g as h, e as f\n" - ) test_output = isort.code( code=test_input, combine_star=False, combine_as_imports=False, - keep_direct_and_as_imports=True, force_single_line=True, no_inline_sort=False, ) @@ -3614,7 +3430,6 @@ def test_all_imports_from_single_module() -> None: code=test_input, combine_star=False, combine_as_imports=False, - keep_direct_and_as_imports=True, force_single_line=False, no_inline_sort=True, ) @@ -3629,59 +3444,18 @@ def test_all_imports_from_single_module() -> None: "from a import g as h\n" "from a import e as f\n" ) - test_output = isort.code( - code=test_input, - combine_star=False, - combine_as_imports=False, - keep_direct_and_as_imports=False, - force_single_line=True, - no_inline_sort=True, - ) - assert test_output == ( - "import a\n" - "from a import *\n" - "from a import b as c\n" - "from a import b as d\n" - "from a import e as f\n" - "from a import g as h\n" - "from a import i as j\n" - "from a import w\n" - "from a import x\n" - "from a import y\n" - "from a import z\n" - ) test_output = isort.code( code=test_input, combine_star=True, combine_as_imports=True, - keep_direct_and_as_imports=True, force_single_line=False, no_inline_sort=False, ) assert test_output == "import a\nfrom a import *\n" - test_output = isort.code( - code=test_input, - combine_star=True, - combine_as_imports=True, - keep_direct_and_as_imports=False, - force_single_line=True, - no_inline_sort=False, - ) - assert test_output == "import a\nfrom a import *\n" - test_output = isort.code( - code=test_input, - combine_star=True, - combine_as_imports=True, - keep_direct_and_as_imports=False, - force_single_line=False, - no_inline_sort=True, - ) - assert test_output == "import a\nfrom a import *\n" test_output = isort.code( code=test_input, combine_star=True, combine_as_imports=False, - keep_direct_and_as_imports=True, force_single_line=True, no_inline_sort=False, ) @@ -3690,25 +3464,14 @@ def test_all_imports_from_single_module() -> None: code=test_input, combine_star=True, combine_as_imports=False, - keep_direct_and_as_imports=True, force_single_line=False, no_inline_sort=True, ) assert test_output == "import a\nfrom a import *\n" - test_output = isort.code( - code=test_input, - combine_star=True, - combine_as_imports=False, - keep_direct_and_as_imports=False, - force_single_line=True, - no_inline_sort=True, - ) - assert test_output == "import a\nfrom a import *\n" test_output = isort.code( code=test_input, combine_star=False, combine_as_imports=True, - keep_direct_and_as_imports=True, force_single_line=True, no_inline_sort=False, ) @@ -3730,7 +3493,6 @@ def test_all_imports_from_single_module() -> None: code=test_input, combine_star=False, combine_as_imports=True, - keep_direct_and_as_imports=True, force_single_line=False, no_inline_sort=True, ) @@ -3743,7 +3505,6 @@ def test_all_imports_from_single_module() -> None: code=test_input, combine_star=False, combine_as_imports=False, - keep_direct_and_as_imports=True, force_single_line=True, no_inline_sort=True, ) @@ -3765,7 +3526,6 @@ def test_all_imports_from_single_module() -> None: code=test_input, combine_star=True, combine_as_imports=True, - keep_direct_and_as_imports=True, force_single_line=True, no_inline_sort=False, ) @@ -4793,7 +4553,7 @@ def test_multiple_aliases(): import datetime as dt import datetime as dt2 """ - assert isort.code(keep_direct_and_as_imports=True, code=test_input) == test_input + assert isort.code(code=test_input) == test_input def test_parens_in_comment(): @@ -4814,7 +4574,7 @@ def test_as_imports_mixed(): expected_output = """import datetime.datetime as dt from datetime import datetime """ - assert isort.code(test_input, keep_direct_and_as_imports=True) == expected_output + assert isort.code(test_input) == expected_output def test_no_sections_with_future(): From 413f8a0f8c1cba07b2a174fdd03c8bf279198d3b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 14 Jul 2020 21:27:26 -0700 Subject: [PATCH 0789/1439] Add -k to upgrading guide --- docs/upgrade_guides/5.0.0.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/upgrade_guides/5.0.0.md b/docs/upgrade_guides/5.0.0.md index 4724695cb..b3d788ef5 100644 --- a/docs/upgrade_guides/5.0.0.md +++ b/docs/upgrade_guides/5.0.0.md @@ -19,6 +19,10 @@ Prior to version 5.0.0, isort wouldn't automatically traverse directories. The - ### `--apply` or `-y` Prior to version 5.0.0, depending on how isort was executed, it would ask you before making every file change. In isort 5.0.0 file changes happen by default inline with other formatters. `--interactive` is available to restore the previous behavior. If encountered this option can simply be removed. +### `--keep-direct-and-as` or `-k` +Many versions ago, by default isort would remove imports such as `from datetime import datetime` if an alias for the same import also existed such as `from datetime import datetime as dt` - never allowing both to exist. +The option was originally added to allow working around this, and was then turned on as the default. Now the option for the old behaviour has been removed. Simply remove the option from your config file. + ### `-ac`, `-wl`, `-ws`, `-tc`, `-sp`, `-sp`, `-sl`, `-sg`, `-sd`, `-rr`, `-ot`, `-nlb`, `-nis`, `-ls`, `-le`, `-lbt`, `-lai`, `-fss`, `-fgw`, `-ff`, `-fass`, `-fas`, `-dt`, `-ds`, `-df`, `-cs`, `-ca`, `-af`, `-ac` Two-letter shortened setting names (like `ac` for `atomic`) now require two dashes to avoid ambiguity. Simply add another dash before the option, or switch to the long form option to fix (example: `--ac` or `--atomic`). @@ -31,6 +35,10 @@ If you have multiple configs, they will need to be merged into 1 single one. You ### `not_skip` This is the same as the `--dont-skip` CLI option above. In an earlier version isort had a default skip of `__init__.py`. To get around that many projects wanted a way to not skip `__init__.py` or any other files that were automatically skipped in the future by isort. isort no longer has any default skips, so if the value here is `__init__.py` you can simply remove the setting. If it is something else, just make sure you aren't specifying to skip that file somewhere else in your config. +### `keep_direct_and_as_imports` +This is the same as `keep-direct-and-as` from CLI. Many versions ago, by default isort would remove imports such as `from datetime import datetime` if an alias for the same import also existed such as `from datetime import datetime as dt` - never allowing both to exist. +The option was originally added to allow working around this, and was then turned on as the default. Now the option for the old behaviour has been removed. Simply remove the option from your config file. + ### `known_standard_library` isort settings no longer merge together, instead they override. The old behavior of merging together caused many hard to track down errors, but the one place it was very convenient was for adding a few additional standard library modules. From 36d4cc056570bc83b0c457491dbca5e4c4f45ea3 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 14 Jul 2020 22:16:40 -0700 Subject: [PATCH 0790/1439] - Implemented experimental support for floating all imports to the top of a file (issue #1228) --- CHANGELOG.md | 1 + docs/configuration/options.md | 26 +++---- isort/api.py | 30 ++++++++ isort/main.py | 11 ++- isort/settings.py | 1 + tests/test_ticketed_features.py | 117 ++++++++++++++++++++++++++++++++ 6 files changed, 172 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21f1b7254..6ce6172d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. ### 5.1.0 July TBD, 2020 - isort now throws an exception if an invalid settings path is given (issue #1174). - Implemented support for automatic redundant alias removal (issue #1281). + - Implemented experimental support for floating all imports to the top of a file (issue #1228) - Fixed #1178: support for semicolons in decorators. - Fixed #1315: Extra newline before comment with -n + --fss. - Fixed #1192: `-k` or `--keep-direct-and-as-imports` option has been deprecated as it is now always on. diff --git a/docs/configuration/options.md b/docs/configuration/options.md index 86ad6da4b..fecc83470 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -468,18 +468,6 @@ Ensures that if a star import is present, nothing else is imported from that nam - --cs - --combine-star -## Keep Direct And As Imports - -Turns off default behavior that removes direct imports when as imports exist. - -**Type:** Bool -**Default:** `True` -**Python & Config File Name:** keep_direct_and_as_imports -**CLI Flags:** - -- -k -- --keep-direct-and-as - ## Include Trailing Comma Includes a trailing comma on multi line imports that include parentheses. @@ -762,6 +750,17 @@ Tells isort to remove redundant aliases from imports, such as `import os as os`. - --remove-redundant-aliases +## Float To Top + +Causes all non indented imports to float to the top of the file having its imports sorted. *NOTE*: This is a **beta** feature. It currently doesn't work with cimports and is gauranteed to run much slower and use much more memory than the default. Still it can be a great shortcut for collecting imports every once in a while when you put them in the middle of a file. + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** float_to_top +**CLI Flags:** + +- --float-to-top + ## Check Checks the file for unsorted / unformatted imports and prints them to the command line without modifying the file. @@ -923,4 +922,5 @@ See isort's determined config, as well as sources of config options. **Python & Config File Name:** **Not Supported** **CLI Flags:** -- --apply +- -k +- --keep-direct-and-as diff --git a/isort/api.py b/isort/api.py index 3aa25dc01..6f7230ed5 100644 --- a/isort/api.py +++ b/isort/api.py @@ -386,6 +386,36 @@ def _sort_imports( cimports: bool = False made_changes: bool = False + if config.float_to_top: + new_input = "" + current = "" + isort_off = False + for line in chain(input_stream, (None,)): + if isort_off and line is not None: + if line == "# isort: on\n": + isort_off = False + new_input += line + elif line in ("# isort: split\n", "# isort: off\n", None): + if line == "# isort: off\n": + isort_off = True + if current: + parsed = parse.file_contents(current, config=config) + extra_space = "" + while current[-1] == "\n": + extra_space += "\n" + current = current[:-1] + extra_space = extra_space.replace("\n", "", 1) + new_input += output.sorted_imports( + parsed, config, extension, import_type="import" + ) + new_input += extra_space + current = "" + new_input += line or "" + else: + current += line or "" + + input_stream = StringIO(new_input) + for index, line in enumerate(chain(input_stream, (None,))): if line is None: if index == 0 and not config.force_adds: diff --git a/isort/main.py b/isort/main.py index 589db60fc..48da508a2 100644 --- a/isort/main.py +++ b/isort/main.py @@ -588,7 +588,16 @@ def _build_arg_parser() -> argparse.ArgumentParser: " aliases to signify intent and change behaviour." ), ) - + parser.add_argument( + "--float-to-top", + dest="float_to_top", + action="store_true", + help="Causes all non indented imports to float to the top of the file having its imports " + "sorted. *NOTE*: This is a **beta** feature. It currently doesn't work with cimports and " + "is gauranteed to run much slower and use much more memory than the default. Still it " + "can be a great shortcut for collecting imports every once in a while when you put them " + "in the middle of a file.", + ) # deprecated options parser.add_argument( "--recursive", diff --git a/isort/settings.py b/isort/settings.py index 437cca597..68f32a662 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -166,6 +166,7 @@ class _Config: src_paths: FrozenSet[Path] = frozenset() old_finders: bool = False remove_redundant_aliases: bool = False + float_to_top: bool = False def __post_init__(self): py_version = self.py_version diff --git a/tests/test_ticketed_features.py b/tests/test_ticketed_features.py index 2709a7690..4e9587816 100644 --- a/tests/test_ticketed_features.py +++ b/tests/test_ticketed_features.py @@ -32,3 +32,120 @@ def test_isort_automatically_removes_duplicate_aliases_issue_1193(): ) assert isort.check_code("import os as os\n", show_diff=True) assert isort.code("import os as os", remove_redundant_aliases=True) == "import os\n" + + +def test_isort_enables_floating_imports_to_top_of_module_issue_1228(): + """Test to ensure isort will allow floating all non-indented imports to the top of a file. + See: https://github.com/timothycrosley/isort/issues/1228. + """ + assert ( + isort.code( + """ +import os + + +def my_function_1(): + pass + +import sys + +def my_function_2(): + pass +""", + float_to_top=True, + ) + == """ +import os +import sys + + +def my_function_1(): + pass + + +def my_function_2(): + pass +""" + ) + + assert ( + isort.code( + """ +import os + + +def my_function_1(): + pass + +# isort: split +import sys + +def my_function_2(): + pass +""", + float_to_top=True, + ) + == """ +import os + + +def my_function_1(): + pass + +# isort: split +import sys + + +def my_function_2(): + pass +""" + ) + + +assert ( + isort.code( + """ +import os + + +def my_function_1(): + pass + +# isort: off +import b +import a +def y(): + pass + +# isort: on +import b + +def my_function_2(): + pass + +import a +""", + float_to_top=True, + ) + == """ +import os + + +def my_function_1(): + pass + +# isort: off +import b +import a +def y(): + pass + +# isort: on +import a +import b + + +def my_function_2(): + pass +""" +) From 70c58375ea46b11d2c7bdcdc5447590d44ee2cec Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 14 Jul 2020 22:17:46 -0700 Subject: [PATCH 0791/1439] Bump version to 5.1.0 --- CHANGELOG.md | 2 +- isort/_version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ce6172d2..45e1572d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. -### 5.1.0 July TBD, 2020 +### 5.1.0 July 14, 2020 - isort now throws an exception if an invalid settings path is given (issue #1174). - Implemented support for automatic redundant alias removal (issue #1281). - Implemented experimental support for floating all imports to the top of a file (issue #1228) diff --git a/isort/_version.py b/isort/_version.py index 603e34093..0d72820f3 100644 --- a/isort/_version.py +++ b/isort/_version.py @@ -1 +1 @@ -__version__ = "5.0.9" +__version__ = "5.1.0" diff --git a/pyproject.toml b/pyproject.toml index 36c9f494d..dcbe17b60 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ line-length = 100 [tool.poetry] name = "isort" -version = "5.0.9" +version = "5.1.0" description = "A Python utility / library to sort Python imports." authors = ["Timothy Crosley "] license = "MIT" From 4611e1c48d146ab429f35293b2aa548ac90d9240 Mon Sep 17 00:00:00 2001 From: Dylan Richardson Date: Wed, 15 Jul 2020 17:12:37 -0500 Subject: [PATCH 0792/1439] Use correct spelling for use_parentheses The spelling of this configuration option was updated, but the documentation still uses the old spelling. --- docs/configuration/options.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration/options.md b/docs/configuration/options.md index fecc83470..a5e65a305 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -374,7 +374,7 @@ Balances wrapping to produce the most consistent line length possible ## Use Parentheses -Use parenthesis for line continuation on length limit instead of slashes. +Use parentheses for line continuation on length limit instead of slashes. **Type:** Bool **Default:** `False` From 5811f3edf74cc7e93c97dd0fc8a4f73d7c79ef33 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 15 Jul 2020 20:17:15 -0700 Subject: [PATCH 0793/1439] Fix spelling in command line argument as well --- isort/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/main.py b/isort/main.py index 48da508a2..8613f8110 100644 --- a/isort/main.py +++ b/isort/main.py @@ -461,7 +461,7 @@ def _build_arg_parser() -> argparse.ArgumentParser: "--use-parentheses", dest="use_parentheses", action="store_true", - help="Use parenthesis for line continuation on length limit instead of slashes.", + help="Use parentheses for line continuation on length limit instead of slashes.", ) parser.add_argument( "-V", From 17a70624431b4fa4cf5f3925ac44d2340decf68d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 15 Jul 2020 20:49:32 -0700 Subject: [PATCH 0794/1439] Fix issue #1322: Occasionally two extra newlines before comment with & . --- CHANGELOG.md | 3 +++ isort/output.py | 8 +++++++- tests/test_regressions.py | 26 ++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45e1572d4..37e812aef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. +### 5.1.1 July 15, 2020 + - Fix issue #1322: Occasionally two extra newlines before comment with `-n` & `--fss`. + ### 5.1.0 July 14, 2020 - isort now throws an exception if an invalid settings path is given (issue #1174). - Implemented support for automatic redundant alias removal (issue #1281). diff --git a/isort/output.py b/isort/output.py index 247dbaeb6..71ec57522 100644 --- a/isort/output.py +++ b/isort/output.py @@ -108,6 +108,8 @@ def sorted_imports( comments_above = [] new_section_output: List[str] = [] for line in section_output: + if not line: + continue if line.startswith("#"): comments_above.append(line) elif comments_above: @@ -132,7 +134,11 @@ def sorted_imports( for line in new_section_output: comments = getattr(line, "comments", ()) if comments: - if section_output and config.ensure_newline_before_comments: + if ( + config.ensure_newline_before_comments + and section_output + and section_output[-1] + ): section_output.append("") section_output.extend(comments) section_output.append(str(line)) diff --git a/tests/test_regressions.py b/tests/test_regressions.py index 516ce7088..86bf0a0ee 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -360,3 +360,29 @@ def test_isort_doesnt_rewrite_import_with_dot_to_from_import_issue_1280(): """, show_diff=True, ) + + +def test_isort_shouldnt_introduce_extra_lines_with_fass_issue_1322(): + """Tests to ensure isort doesn't introduce extra lines when used with fass option. + See: https://github.com/timothycrosley/isort/issues/1322 + """ + assert ( + isort.code( + """ + import logging + +# Comment canary +from foo import bar +import quux +""", + force_sort_within_sections=True, + ensure_newline_before_comments=True, + ) + == """ + import logging + +# Comment canary +from foo import bar +import quux +""" + ) From 189fd97209fb1ca2e89483a4f99564a38bb766b8 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 15 Jul 2020 20:51:32 -0700 Subject: [PATCH 0795/1439] Fix spelling of guaranteed --- isort/main.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/isort/main.py b/isort/main.py index 8613f8110..bf6ff038b 100644 --- a/isort/main.py +++ b/isort/main.py @@ -593,10 +593,10 @@ def _build_arg_parser() -> argparse.ArgumentParser: dest="float_to_top", action="store_true", help="Causes all non indented imports to float to the top of the file having its imports " - "sorted. *NOTE*: This is a **beta** feature. It currently doesn't work with cimports and " - "is gauranteed to run much slower and use much more memory than the default. Still it " - "can be a great shortcut for collecting imports every once in a while when you put them " - "in the middle of a file.", + "sorted. *NOTE*: This is an **experimental** feature. It currently doesn't work with " + "cimports and is guaranteed to run much slower and use much more memory than the default. " + "Still it can be a great shortcut for collecting imports every once in a while when you put" + " them in the middle of a file.", ) # deprecated options parser.add_argument( From b7be8c865d83d49af773bedc61fae637f728bd7f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 15 Jul 2020 20:52:30 -0700 Subject: [PATCH 0796/1439] Fix spelling of guaranteed --- docs/configuration/options.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration/options.md b/docs/configuration/options.md index a5e65a305..303932487 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -752,7 +752,7 @@ Tells isort to remove redundant aliases from imports, such as `import os as os`. ## Float To Top -Causes all non indented imports to float to the top of the file having its imports sorted. *NOTE*: This is a **beta** feature. It currently doesn't work with cimports and is gauranteed to run much slower and use much more memory than the default. Still it can be a great shortcut for collecting imports every once in a while when you put them in the middle of a file. +Causes all non indented imports to float to the top of the file having its imports sorted. *NOTE*: This is an **experimental** feature. It currently doesn't work with cimports and is guaranteed to run much slower and use much more memory than the default. Still it can be a great shortcut for collecting imports every once in a while when you put them in the middle of a file. **Type:** Bool **Default:** `False` From cde7ec88266f4633450c12bee896955e73e48b53 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 15 Jul 2020 21:52:06 -0700 Subject: [PATCH 0797/1439] Fixed issue #1189: '--diff' broken when reading from standard input. --- CHANGELOG.md | 3 ++- isort/api.py | 23 +++++++++++++++++++++++ isort/format.py | 6 ++++-- tests/test_api.py | 8 ++++++++ 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37e812aef..5c0288449 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,8 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. ### 5.1.1 July 15, 2020 - - Fix issue #1322: Occasionally two extra newlines before comment with `-n` & `--fss`. + - Fixed issue #1322: Occasionally two extra newlines before comment with `-n` & `--fss`. + - Fixed issue #1189: `--diff` broken when reading from standard input. ### 5.1.0 July 14, 2020 - isort now throws an exception if an invalid settings path is given (issue #1174). diff --git a/isort/api.py b/isort/api.py index 6f7230ed5..6e6969ddb 100644 --- a/isort/api.py +++ b/isort/api.py @@ -100,6 +100,7 @@ def sort_stream( config: Config = DEFAULT_CONFIG, file_path: Optional[Path] = None, disregard_skip: bool = False, + show_diff: bool = False, **config_kwargs, ): """Sorts any imports within the provided code stream, outputs to the provided output stream. @@ -113,6 +114,28 @@ def sort_stream( - **disregard_skip**: set to `True` if you want to ignore a skip set in config for this file. - ****config_kwargs**: Any config modifications. """ + if show_diff: + _output_stream = StringIO() + _input_stream = StringIO(input_stream.read()) + changed = sort_stream( + input_stream=_input_stream, + output_stream=_output_stream, + extension=extension, + config=config, + file_path=file_path, + disregard_skip=disregard_skip, + **config_kwargs, + ) + _output_stream.seek(0) + _input_stream.seek(0) + show_unified_diff( + file_input=_input_stream.read(), + file_output=_output_stream.read(), + file_path=file_path, + output=output_stream, + ) + return changed + config = _config(path=file_path, config=config, **config_kwargs) content_source = str(file_path or "Passed in content") if not disregard_skip: diff --git a/isort/format.py b/isort/format.py index 27ba9e594..361808fb1 100644 --- a/isort/format.py +++ b/isort/format.py @@ -28,7 +28,9 @@ def format_natural(import_line: str) -> str: return import_line -def show_unified_diff(*, file_input: str, file_output: str, file_path: Optional[Path]): +def show_unified_diff( + *, file_input: str, file_output: str, file_path: Optional[Path], output=sys.stdout +): file_name = "" if file_path is None else str(file_path) file_mtime = str( datetime.now() if file_path is None else datetime.fromtimestamp(file_path.stat().st_mtime) @@ -43,7 +45,7 @@ def show_unified_diff(*, file_input: str, file_output: str, file_path: Optional[ tofiledate=str(datetime.now()), ) for line in unified_diff_lines: - sys.stdout.write(line) + output.write(line) def ask_whether_to_apply_changes_to_file(file_path: str) -> bool: diff --git a/tests/test_api.py b/tests/test_api.py index e4984ceb1..76d473a2e 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,4 +1,5 @@ """Tests the isort API module""" +from io import StringIO from unittest.mock import MagicMock, patch import pytest @@ -41,3 +42,10 @@ def test_check_file(tmpdir) -> None: def test_sorted_imports_multiple_configs() -> None: with pytest.raises(ValueError): api.sort_code_string("import os", config=Config(line_length=80), line_length=80) + + +def test_diff_stream() -> None: + output = StringIO() + assert api.sort_stream(StringIO("import b\nimport a\n"), output, show_diff=True) + output.seek(0) + assert "import a\n import b\n" in output.read() From bade597864476b2e70e4727d411b005dee1282e2 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 15 Jul 2020 22:18:03 -0700 Subject: [PATCH 0798/1439] Improve documentation --- docs/configuration/action_comments.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/configuration/action_comments.md b/docs/configuration/action_comments.md index 39d6ff751..4c82a9174 100644 --- a/docs/configuration/action_comments.md +++ b/docs/configuration/action_comments.md @@ -26,6 +26,7 @@ import sys ## isort: skip If placed on the same line as (or within the continuation of a) an import statement, isort will not sort this import. +More specifically, it prevents the import statement to be recognized by isort as an import. In consequence, this line will be treated as code and be pushed down to below the import section of the file. Example: From 8bcb2dbfe9fb3a2a88e324430fc2629414506d2d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 15 Jul 2020 22:19:50 -0700 Subject: [PATCH 0799/1439] Improve skip comment documentation --- docs/configuration/action_comments.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration/action_comments.md b/docs/configuration/action_comments.md index 4c82a9174..c9be7d43b 100644 --- a/docs/configuration/action_comments.md +++ b/docs/configuration/action_comments.md @@ -26,7 +26,7 @@ import sys ## isort: skip If placed on the same line as (or within the continuation of a) an import statement, isort will not sort this import. -More specifically, it prevents the import statement to be recognized by isort as an import. In consequence, this line will be treated as code and be pushed down to below the import section of the file. +More specifically, it prevents the import statement from being recognized by isort as an import. In consequence, this line will be treated as code and be pushed down to below the import section of the file. Example: From 48b0f953e8bc4e1f7b328f11ccae2bd8c11be257 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 15 Jul 2020 22:41:03 -0700 Subject: [PATCH 0800/1439] Bump version to 5.1.1 --- isort/_version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/_version.py b/isort/_version.py index 0d72820f3..a9c316e20 100644 --- a/isort/_version.py +++ b/isort/_version.py @@ -1 +1 @@ -__version__ = "5.1.0" +__version__ = "5.1.1" diff --git a/pyproject.toml b/pyproject.toml index dcbe17b60..085140a41 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ line-length = 100 [tool.poetry] name = "isort" -version = "5.1.0" +version = "5.1.1" description = "A Python utility / library to sort Python imports." authors = ["Timothy Crosley "] license = "MIT" From 4af5a6f73db4af34e9bdc1f752f0800c65c516dc Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 16 Jul 2020 23:55:30 -0700 Subject: [PATCH 0801/1439] Fixed issue #1219 / #1326: Comments not wrapped for long lines --- CHANGELOG.md | 3 +++ isort/output.py | 28 +++++++++++++++++----------- tests/test_isort.py | 2 +- tests/test_regressions.py | 16 ++++++++++++++++ 4 files changed, 37 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c0288449..2665354f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. +### 5.1.2 July 17, 2020 + - Fixed issue #1219 / #1326: Comments not wrapped for long lines + ### 5.1.1 July 15, 2020 - Fixed issue #1322: Occasionally two extra newlines before comment with `-n` & `--fss`. - Fixed issue #1189: `--diff` broken when reading from standard input. diff --git a/isort/output.py b/isort/output.py index 71ec57522..620a41599 100644 --- a/isort/output.py +++ b/isort/output.py @@ -346,21 +346,27 @@ def _with_from_imports( ) if parsed.imports[section]["from"][module][from_import]: new_section_output.append( - with_comments( - from_comments, - wrap.line( - import_start + from_import, parsed.line_separator, config + wrap.line( + with_comments( + from_comments, + import_start + from_import, + removed=config.ignore_comments, + comment_prefix=config.comment_prefix, ), - removed=config.ignore_comments, - comment_prefix=config.comment_prefix, + parsed.line_separator, + config, ) ) new_section_output.extend( - with_comments( - from_comments, - wrap.line(import_start + as_import, parsed.line_separator, config), - removed=config.ignore_comments, - comment_prefix=config.comment_prefix, + wrap.line( + with_comments( + from_comments, + import_start + as_import, + removed=config.ignore_comments, + comment_prefix=config.comment_prefix, + ), + parsed.line_separator, + config, ) for as_import in as_imports[from_import] ) diff --git a/tests/test_isort.py b/tests/test_isort.py index 639d35be4..99d0f2d5b 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -4441,7 +4441,7 @@ def test_noqa_issue_1065() -> None: from flask_security.signals import user_confirmed # noqa from flask_security.signals import user_registered # noqa """ - assert isort.code(test_input) == expected_output + assert isort.code(test_input, line_length=100) == expected_output def test_single_line_exclusions(): diff --git a/tests/test_regressions.py b/tests/test_regressions.py index 86bf0a0ee..123bec3a1 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -386,3 +386,19 @@ def test_isort_shouldnt_introduce_extra_lines_with_fass_issue_1322(): import quux """ ) + + +def test_comments_should_cause_wrapping_on_long_lines_black_mode_issue_1219(): + """Tests to ensure if isort encounters a single import line which is made too long with a comment + it is wrapped when using black profile. + See: https://github.com/timothycrosley/isort/issues/1219 + """ + assert isort.check_code( + """ +from many_stop_words import ( + get_stop_words as get_base_stopwords, # extended list of stop words, also for en +) +""", + show_diff=True, + profile="black", + ) From 9d12eb5567a1a36afe03da594ea0f94c36efa729 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 17 Jul 2020 00:30:02 -0700 Subject: [PATCH 0802/1439] Fixed issue #1156: Bug related to isort:skip usage followed by a multiline comment block --- CHANGELOG.md | 1 + isort/parse.py | 2 +- tests/test_regressions.py | 25 +++++++++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2665354f7..006a72957 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. ### 5.1.2 July 17, 2020 - Fixed issue #1219 / #1326: Comments not wrapped for long lines + - Fixed issue #1156: Bug related to isort:skip usage followed by a multiline comment block ### 5.1.1 July 15, 2020 - Fixed issue #1322: Occasionally two extra newlines before comment with `-n` & `--fss`. diff --git a/isort/parse.py b/isort/parse.py index 94b1e3161..a67ae1e94 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -371,7 +371,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte categorized_comments["above"]["from"].setdefault(import_from, []).insert( 0, out_lines.pop(-1) ) - if len(out_lines) > max(import_index - 1, 1) - 1: + if len(out_lines): last = out_lines[-1].rstrip() else: last = "" diff --git a/tests/test_regressions.py b/tests/test_regressions.py index 123bec3a1..275c8b202 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -402,3 +402,28 @@ def test_comments_should_cause_wrapping_on_long_lines_black_mode_issue_1219(): show_diff=True, profile="black", ) + + +def test_comment_blocks_should_stay_associated_without_extra_lines_issue_1156(): + """Tests to ensure isort doesn't add an extra line when there are large import blocks + or otherwise warp the intent. + See: https://github.com/timothycrosley/isort/issues/1156 + """ + assert ( + isort.code( + """from top_level_ignored import config # isort:skip +#################################### +# COMMENT BLOCK SEPARATING THESE # +#################################### +from ast import excepthandler +import logging +""" + ) + == """from top_level_ignored import config # isort:skip +import logging +#################################### +# COMMENT BLOCK SEPARATING THESE # +#################################### +from ast import excepthandler +""" + ) From 7a12cab28144e0b743a121878f40f263867d3670 Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2020 07:35:22 +0000 Subject: [PATCH 0803/1439] Remove length check in favour of truthiness of the object --- isort/parse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/parse.py b/isort/parse.py index a67ae1e94..283d09a71 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -371,7 +371,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte categorized_comments["above"]["from"].setdefault(import_from, []).insert( 0, out_lines.pop(-1) ) - if len(out_lines): + if out_lines: last = out_lines[-1].rstrip() else: last = "" From 293c3fb2d6e9dcf47d1c3f710d0ef0f17ae32774 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 17 Jul 2020 20:38:34 -0700 Subject: [PATCH 0804/1439] Bump version to 5.1.2 --- isort/_version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/_version.py b/isort/_version.py index a9c316e20..b2e51c322 100644 --- a/isort/_version.py +++ b/isort/_version.py @@ -1 +1 @@ -__version__ = "5.1.1" +__version__ = "5.1.2" diff --git a/pyproject.toml b/pyproject.toml index 085140a41..028d522b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ line-length = 100 [tool.poetry] name = "isort" -version = "5.1.1" +version = "5.1.2" description = "A Python utility / library to sort Python imports." authors = ["Timothy Crosley "] license = "MIT" From 63724dbb36c425a574ae069ec0a57172a0364b36 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 18 Jul 2020 15:06:54 -0700 Subject: [PATCH 0805/1439] Fixed changelog error for 5.0.9 release --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 006a72957..bb8d6b2ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. - Fixed #1280: rewrite of as imports changes the behavior of the imports. ### 5.0.9 July 11, 2020 -is +Fixed #1301: Import headings in nested sections leads to check errors ### 5.0.8 July 11, 2020 - Fixed #1277 & #1278: New line detection issues on Windows. From b7261b84466b399763928aa3b21a1b1f8d45028a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 18 Jul 2020 15:11:48 -0700 Subject: [PATCH 0806/1439] Fix changelog formatting --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb8d6b2ff..1d5c96bc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. - Fixed #1280: rewrite of as imports changes the behavior of the imports. ### 5.0.9 July 11, 2020 -Fixed #1301: Import headings in nested sections leads to check errors + - Fixed #1301: Import headings in nested sections leads to check errors ### 5.0.8 July 11, 2020 - Fixed #1277 & #1278: New line detection issues on Windows. From bf47b311f854d25a9161e9ae6cda709002bf3f77 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 18 Jul 2020 20:40:10 -0700 Subject: [PATCH 0807/1439] Fixed issue #1329: Fix comments duplicated when --fass option is set. --- CHANGELOG.md | 3 +++ isort/output.py | 5 +++-- tests/test_regressions.py | 19 +++++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d5c96bc2..6173eed0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. +### 5.1.3 July 18, 2020 + - Fixed issue #1329: Fix comments duplicated when --fass option is set. + ### 5.1.2 July 17, 2020 - Fixed issue #1219 / #1326: Comments not wrapped for long lines - Fixed issue #1156: Bug related to isort:skip usage followed by a multiline comment block diff --git a/isort/output.py b/isort/output.py index 620a41599..306744da2 100644 --- a/isort/output.py +++ b/isort/output.py @@ -529,5 +529,6 @@ def _normalize_empty_lines(lines: List[str]) -> List[str]: class _LineWithComments(str): def __new__(cls, value, comments): - cls.comments = comments - return super().__new__(cls, value) # type: ignore + instance = super().__new__(cls, value) # type: ignore + instance.comments = comments + return instance diff --git a/tests/test_regressions.py b/tests/test_regressions.py index 275c8b202..105972ee8 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -427,3 +427,22 @@ def test_comment_blocks_should_stay_associated_without_extra_lines_issue_1156(): from ast import excepthandler """ ) + + +def test_comment_shouldnt_be_duplicated_with_fass_enabled_issue_1329(): + """Tests to ensure isort doesn't duplicate comments when imports occur with comment on top, + immediately after large comment blocks. + See: https://github.com/timothycrosley/isort/pull/1329/files. + """ + assert isort.check_code( + """''' +Multi-line docstring +''' +# Comment for A. +import a +# Comment for B - not A! +import b +""", + force_sort_within_sections=True, + show_diff=True, + ) From 447580671b2e37a21a3cf1a4d2e80c387f9af98a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 18 Jul 2020 20:40:43 -0700 Subject: [PATCH 0808/1439] Bump to version 5.1.3 --- isort/_version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/_version.py b/isort/_version.py index b2e51c322..368870818 100644 --- a/isort/_version.py +++ b/isort/_version.py @@ -1 +1 @@ -__version__ = "5.1.2" +__version__ = "5.1.3" diff --git a/pyproject.toml b/pyproject.toml index 028d522b0..f901140d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ line-length = 100 [tool.poetry] name = "isort" -version = "5.1.2" +version = "5.1.3" description = "A Python utility / library to sort Python imports." authors = ["Timothy Crosley "] license = "MIT" From e08c86c2e3d2f14a590d6a663919bf8e4d6c9a5f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 18 Jul 2020 21:38:53 -0700 Subject: [PATCH 0809/1439] Add release policy --- docs/major_releases/release_policy.md | 46 +++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 docs/major_releases/release_policy.md diff --git a/docs/major_releases/release_policy.md b/docs/major_releases/release_policy.md new file mode 100644 index 000000000..3397b30c9 --- /dev/null +++ b/docs/major_releases/release_policy.md @@ -0,0 +1,46 @@ +# isort Project Official Release Policy + +isort has moved from being a simple hobby project for individuals to sort imports in their Python files +to an essential part of the CI/CD pipeline for large companies and significant Open Source projects. +Due to this evolution, it is now of increased importance that isort maintains a level of quality, predictability, and consistency +that gives projects big and small confidence to depend on it. + +## Formatting guarantees + +With isort 5.1.0, the isort Project guarantees that formatting will stay the same for the options given in accordance to its test suite for the duration of all major releases. This means projects can safely use isort > 5.1.0 < 6.0.0 +without worrying about major formatting changes disrupting their Project. + +## Packaging guarantees + +Starting with the 5.0.0 release isort includes the following project guarantees to help guide development: + +- isort will never have dependencies, optional, required, or otherwise. +- isort will always act the same independent to the Python environment it is installed in. + +## Versioning + +isort follows the [Semantic Versioning 2.0.0 specification](https://semver.org/spec/v2.0.0.html) meaning it has three numerical version parts with distinct rules +`MAJOR.MINOR.PATCH`. + +### Patch Releases x.x.1 + +Within the isort Project, patch releases are really meant solely to fix bugs and minor oversights. +Patch releases should *never* drastically change formatting, even if it's for the better. + +### Minor Releases x.1.x + +Minor changes can contain new backward-incompatible features, and of particular note can include bug fixes +that result in intentional formatting changes - but they should still never be too large in scope. +API backward compatibility should strictly be maintained. + +### Major Releases 1.x.x + +Major releases are the only place where backward-incompatible changes or substantial formatting changes can occur. +Because these kind of changes are likely to break projects that utilize isort, either as a formatter or library, +isort must do the following: + +- Release a release candidate with at least 2 weeks for bugs to be reported and fixed. +- Keep releasing follow up release candidates until there are no or few bugs reported. +- Provide an upgrade guide that helps users work around any backward-incompatible changes. +- Provide a detailed changelog of all changes. +- Where possible, warn and point to the upgrade guide instead of breaking when options are removed. From 678e11dd4a6e1c5d592ce9a08bd0e8f269c07f08 Mon Sep 17 00:00:00 2001 From: Robert Tasarz Date: Mon, 20 Jul 2020 01:24:55 +0200 Subject: [PATCH 0810/1439] wrap_length corrected for indentation in unison with line_length --- isort/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/isort/api.py b/isort/api.py index 6e6969ddb..f6453d383 100644 --- a/isort/api.py +++ b/isort/api.py @@ -615,6 +615,7 @@ def _sort_imports( out_config = Config( config=config, line_length=max(config.line_length - len(indent), 0), + wrap_length=max(config.wrap_length - len(indent), 0), lines_after_imports=1, ) else: From ced720f70ecffff6d5eabf39b665b6e556a35ab5 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 19 Jul 2020 20:41:57 -0700 Subject: [PATCH 0811/1439] Add Robert Tasarz (@rtasarz) to contributors list --- docs/contributing/4.-acknowledgements.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index 0b2f23699..62e5bf742 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -98,6 +98,7 @@ Code Contributors - Kosei Kitahara (@Surgo) - Seung Hyeon, Kim (@hyeonjames) - Gerard Dalmau (@gdalmau) +- Robert Tasarz (@rtasarz) Documenters From d0701d16816f29d990b516634b641b2cb3b3920d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 19 Jul 2020 20:49:17 -0700 Subject: [PATCH 0812/1439] Fix issue #1333: Added regression test and changelog entry --- CHANGELOG.md | 3 +++ tests/test_regressions.py | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6173eed0a..ce3479ce1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. +### 5.1.4 July 19, 2020 + - Fixed issue #1333: Use of wrap_length raises an exception about it not being lower or equal to line_length. + ### 5.1.3 July 18, 2020 - Fixed issue #1329: Fix comments duplicated when --fass option is set. diff --git a/tests/test_regressions.py b/tests/test_regressions.py index 105972ee8..b69e35aea 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -446,3 +446,21 @@ def test_comment_shouldnt_be_duplicated_with_fass_enabled_issue_1329(): force_sort_within_sections=True, show_diff=True, ) + + +def test_wrap_mode_equal_to_line_length_with_indendet_imports_issue_1333(): + assert isort.check_code( + """ +import a +import b + + +def function(): + import a as b + import c as d +""", + line_length=17, + wrap_length=17, + show_diff=True + ) + From 15a50acc32ba7c3a94ec3395ff0705371b71302d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 19 Jul 2020 21:01:53 -0700 Subject: [PATCH 0813/1439] Fixed #1330: Ensure stdout can be stubbed dynamically for function. --- CHANGELOG.md | 1 + isort/format.py | 12 ++++++++++-- tests/test_regressions.py | 3 +-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce3479ce1..3cce6af80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. ### 5.1.4 July 19, 2020 - Fixed issue #1333: Use of wrap_length raises an exception about it not being lower or equal to line_length. + - Fixed issue #1330: Ensure stdout can be stubbed dynamically for `show_unified_diff` function. ### 5.1.3 July 18, 2020 - Fixed issue #1329: Fix comments duplicated when --fass option is set. diff --git a/isort/format.py b/isort/format.py index 361808fb1..15ad6233a 100644 --- a/isort/format.py +++ b/isort/format.py @@ -2,7 +2,7 @@ from datetime import datetime from difflib import unified_diff from pathlib import Path -from typing import Optional +from typing import Optional, TextIO def format_simplified(import_line: str) -> str: @@ -29,8 +29,16 @@ def format_natural(import_line: str) -> str: def show_unified_diff( - *, file_input: str, file_output: str, file_path: Optional[Path], output=sys.stdout + *, file_input: str, file_output: str, file_path: Optional[Path], output: Optional[TextIO] = None ): + """Shows a unified_diff for the provided input and output against the provided file path. + + - **file_input**: A string that represents the contents of a file before changes. + - **file_output**: A string that represents the contents of a file after changes. + - **file_path**: A Path object that represents the file path of the file being changed. + - **output**: A stream to output the diff to. If non is provided uses sys.stdout. + """ + output = sys.stdout if output is None else output file_name = "" if file_path is None else str(file_path) file_mtime = str( datetime.now() if file_path is None else datetime.fromtimestamp(file_path.stat().st_mtime) diff --git a/tests/test_regressions.py b/tests/test_regressions.py index b69e35aea..e4b6d3045 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -461,6 +461,5 @@ def function(): """, line_length=17, wrap_length=17, - show_diff=True + show_diff=True, ) - From 04dda885af509c520faeb4a691a22727c5ae2178 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 19 Jul 2020 21:03:47 -0700 Subject: [PATCH 0814/1439] Bump version to 5.1.4 --- isort/_version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/_version.py b/isort/_version.py index 368870818..1070cc03c 100644 --- a/isort/_version.py +++ b/isort/_version.py @@ -1 +1 @@ -__version__ = "5.1.3" +__version__ = "5.1.4" diff --git a/pyproject.toml b/pyproject.toml index f901140d6..67087d85b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ line-length = 100 [tool.poetry] name = "isort" -version = "5.1.3" +version = "5.1.4" description = "A Python utility / library to sort Python imports." authors = ["Timothy Crosley "] license = "MIT" From e2c48a1c4574a7cb19c7eddb81363c77fc00e104 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 19 Jul 2020 21:30:01 -0700 Subject: [PATCH 0815/1439] Fixed #1335: Official API for diff capturing --- CHANGELOG.md | 3 +++ isort/api.py | 38 ++++++++++++++++++++++----------- tests/test_ticketed_features.py | 12 +++++++++++ 3 files changed, 41 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cce6af80..0b6232d93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. +### 5.2.0 TBD + - Implemented #1335: Official API for diff capturing. + ### 5.1.4 July 19, 2020 - Fixed issue #1333: Use of wrap_length raises an exception about it not being lower or equal to line_length. - Fixed issue #1330: Ensure stdout can be stubbed dynamically for `show_unified_diff` function. diff --git a/isort/api.py b/isort/api.py index f6453d383..4e5e4479d 100644 --- a/isort/api.py +++ b/isort/api.py @@ -4,7 +4,7 @@ from io import StringIO from itertools import chain from pathlib import Path -from typing import List, Optional, TextIO, Union +from typing import List, Optional, TextIO, Union, cast from warnings import warn from . import io, output, parse @@ -36,6 +36,7 @@ def sort_code_string( config: Config = DEFAULT_CONFIG, file_path: Optional[Path] = None, disregard_skip: bool = False, + show_diff: Union[bool, TextIO] = False, **config_kwargs, ): """Sorts any imports within the provided code string, returning a new string with them sorted. @@ -45,6 +46,8 @@ def sort_code_string( - **config**: The config object to use when sorting imports. - **file_path**: The disk location where the code string was pulled from. - **disregard_skip**: set to `True` if you want to ignore a skip set in config for this file. + - **show_diff**: If `True` the changes that need to be done will be printed to stdout, if a + TextIO stream is provided results will be written to it, otherwise no diff will be computed. - ****config_kwargs**: Any config modifications. """ input_stream = StringIO(code) @@ -57,6 +60,7 @@ def sort_code_string( config=config, file_path=file_path, disregard_skip=disregard_skip, + show_diff=show_diff, ) output_stream.seek(0) return output_stream.read() @@ -64,7 +68,7 @@ def sort_code_string( def check_code_string( code: str, - show_diff: bool = False, + show_diff: Union[bool, TextIO] = False, extension: Optional[str] = None, config: Config = DEFAULT_CONFIG, file_path: Optional[Path] = None, @@ -75,7 +79,8 @@ def check_code_string( Returns `True` if everything is correct, otherwise `False`. - **code**: The string of code with imports that need to be sorted. - - **show_diff**: If `True` the changes that need to be done will be printed to stdout. + - **show_diff**: If `True` the changes that need to be done will be printed to stdout, if a + TextIO stream is provided results will be written to it, otherwise no diff will be computed. - **extension**: The file extension that contains imports. Defaults to filename extension or py. - **config**: The config object to use when sorting imports. - **file_path**: The disk location where the code string was pulled from. @@ -100,7 +105,7 @@ def sort_stream( config: Config = DEFAULT_CONFIG, file_path: Optional[Path] = None, disregard_skip: bool = False, - show_diff: bool = False, + show_diff: Union[bool, TextIO] = False, **config_kwargs, ): """Sorts any imports within the provided code stream, outputs to the provided output stream. @@ -112,6 +117,8 @@ def sort_stream( - **config**: The config object to use when sorting imports. - **file_path**: The disk location where the code string was pulled from. - **disregard_skip**: set to `True` if you want to ignore a skip set in config for this file. + - **show_diff**: If `True` the changes that need to be done will be printed to stdout, if a + TextIO stream is provided results will be written to it, otherwise no diff will be computed. - ****config_kwargs**: Any config modifications. """ if show_diff: @@ -132,7 +139,7 @@ def sort_stream( file_input=_input_stream.read(), file_output=_output_stream.read(), file_path=file_path, - output=output_stream, + output=output_stream if show_diff is True else cast(TextIO, show_diff), ) return changed @@ -180,7 +187,7 @@ def sort_stream( def check_stream( input_stream: TextIO, - show_diff: bool = False, + show_diff: Union[bool, TextIO] = False, extension: Optional[str] = None, config: Config = DEFAULT_CONFIG, file_path: Optional[Path] = None, @@ -191,7 +198,8 @@ def check_stream( incorrectly imports are found or `True` if no problems are identified. - **input_stream**: The stream of code with imports that need to be sorted. - - **show_diff**: If `True` the changes that need to be done will be printed to stdout. + - **show_diff**: If `True` the changes that need to be done will be printed to stdout, if a + TextIO stream is provided results will be written to it, otherwise no diff will be computed. - **extension**: The file extension that contains imports. Defaults to filename extension or py. - **config**: The config object to use when sorting imports. - **file_path**: The disk location where the code string was pulled from. @@ -230,14 +238,17 @@ def check_stream( output_stream.seek(0) show_unified_diff( - file_input=file_contents, file_output=output_stream.read(), file_path=file_path + file_input=file_contents, + file_output=output_stream.read(), + file_path=file_path, + output=None if show_diff is True else cast(TextIO, show_diff), ) return False def check_file( filename: Union[str, Path], - show_diff: bool = False, + show_diff: Union[bool, TextIO] = False, config: Config = DEFAULT_CONFIG, file_path: Optional[Path] = None, disregard_skip: bool = True, @@ -248,7 +259,8 @@ def check_file( incorrectly imports are found or `True` if no problems are identified. - **filename**: The name or Path of the file to check. - - **show_diff**: If `True` the changes that need to be done will be printed to stdout. + - **show_diff**: If `True` the changes that need to be done will be printed to stdout, if a + TextIO stream is provided results will be written to it, otherwise no diff will be computed. - **config**: The config object to use when sorting imports. - **file_path**: The disk location where the code string was pulled from. - **disregard_skip**: set to `True` if you want to ignore a skip set in config for this file. @@ -274,7 +286,7 @@ def sort_file( file_path: Optional[Path] = None, disregard_skip: bool = True, ask_to_apply: bool = False, - show_diff: bool = False, + show_diff: Union[bool, TextIO] = False, write_to_stdout: bool = False, **config_kwargs, ): @@ -286,7 +298,8 @@ def sort_file( - **file_path**: The disk location where the code string was pulled from. - **disregard_skip**: set to `True` if you want to ignore a skip set in config for this file. - **ask_to_apply**: If `True`, prompt before applying any changes. - - **show_diff**: If `True` the changes that need to be done will be printed to stdout. + - **show_diff**: If `True` the changes that need to be done will be printed to stdout, if a + TextIO stream is provided results will be written to it, otherwise no diff will be computed. - **write_to_stdout**: If `True`, write to stdout instead of the input file. - ****config_kwargs**: Any config modifications. """ @@ -326,6 +339,7 @@ def sort_file( file_input=source_file.stream.read(), file_output=tmp_file.read_text(encoding=source_file.encoding), file_path=file_path or source_file.path, + output=None if show_diff is True else cast(TextIO, show_diff), ) if show_diff or ( ask_to_apply diff --git a/tests/test_ticketed_features.py b/tests/test_ticketed_features.py index 4e9587816..917fe46d6 100644 --- a/tests/test_ticketed_features.py +++ b/tests/test_ticketed_features.py @@ -1,6 +1,8 @@ """A growing set of tests designed to ensure when isort implements a feature described in a ticket it fully works as defined in the associated ticket. """ +from io import StringIO + import isort @@ -149,3 +151,13 @@ def my_function_2(): pass """ ) + + +def test_isort_provides_official_api_for_diff_output_issue_1335(): + """Test to ensure isort API for diff capturing allows capturing diff without sys.stdout. + See: https://github.com/timothycrosley/isort/issues/1335. + """ + diff_output = StringIO() + isort.code("import b\nimport a\n", show_diff=diff_output) + diff_output.seek(0) + assert "+import a" in diff_output.read() From e3f924a13ab0db4ac15f45c6b94d9069ee3638aa Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 20 Jul 2020 21:29:07 -0700 Subject: [PATCH 0816/1439] resolves #1331: Warns when sections don't match up. --- CHANGELOG.md | 1 + isort/settings.py | 20 +++++++++++++++++- tests/test_ticketed_features.py | 37 +++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b6232d93..7b6eba858 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. ### 5.2.0 TBD - Implemented #1335: Official API for diff capturing. + - Implemented #1331: Warn when sections don't match up. ### 5.1.4 July 19, 2020 - Fixed issue #1333: Use of wrap_length raises an exception about it not being lower or equal to line_length. diff --git a/isort/settings.py b/isort/settings.py index 68f32a662..46c6002a1 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -290,7 +290,14 @@ def __init__( "known_third_party", "known_first_party", ): - known_other[key[len(KNOWN_PREFIX) :].lower()] = frozenset(value) + import_heading = key[len(KNOWN_PREFIX) :].lower() + known_other[import_heading] = frozenset(value) + if not import_heading.upper() in combined_config.get("sections", ()): + warn( + f"`{key}` setting is defined, but not {import_heading.upper} is not" + " included in `sections` config option:" + f" {combined_config.get('sections', SECTION_DEFAULTS)}." + ) if key.startswith(IMPORT_HEADING_PREFIX): import_headings[key[len(IMPORT_HEADING_PREFIX) :].lower()] = str(value) @@ -301,6 +308,17 @@ def __init__( combined_config[key] = type(default_value)(value) + for section in combined_config.get("sections", ()): + if section in SECTION_DEFAULTS: + continue + elif not section.lower() in known_other: + config_keys = ", ".join(known_other.keys()) + warn( + f"`sections` setting includes {section}, but no known_{section.lower()} " + "is defined. " + f"The following known_SECTION config options are defined: {config_keys}." + ) + if "directory" not in combined_config: combined_config["directory"] = ( os.path.dirname(config_settings["source"]) diff --git a/tests/test_ticketed_features.py b/tests/test_ticketed_features.py index 917fe46d6..ae9fec59d 100644 --- a/tests/test_ticketed_features.py +++ b/tests/test_ticketed_features.py @@ -3,7 +3,10 @@ """ from io import StringIO +import pytest + import isort +from isort import Config def test_semicolon_ignored_for_dynamic_lines_after_import_issue_1178(): @@ -161,3 +164,37 @@ def test_isort_provides_official_api_for_diff_output_issue_1335(): isort.code("import b\nimport a\n", show_diff=diff_output) diff_output.seek(0) assert "+import a" in diff_output.read() + + +def test_isort_warns_when_known_sections_dont_match_issue_1331(): + """Test to ensure that isort warns if there is a mismatch between sections and known_sections. + See: https://github.com/timothycrosley/isort/issues/1331. + """ + assert ( + isort.place_module( + "bot_core", + config=Config( + known_robotlocomotion_upstream=["bot_core"], + sections=["ROBOTLOCOMOTION_UPSTREAM", "THIRDPARTY"], + ), + ) + == "ROBOTLOCOMOTION_UPSTREAM" + ) + with pytest.warns(UserWarning): + assert ( + isort.place_module( + "bot_core", + config=Config( + known_robotlocomotion_upstream=["bot_core"], + sections=["ROBOTLOOMOTION_UPSTREAM", "THIRDPARTY"], + ), + ) + == "THIRDPARTY" + ) + with pytest.warns(UserWarning): + assert ( + isort.place_module( + "bot_core", config=Config(known_robotlocomotion_upstream=["bot_core"]) + ) + == "THIRDPARTY" + ) From 4ab8148950a37d2380ad608ed78e8a15c2b9558c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 20 Jul 2020 21:41:23 -0700 Subject: [PATCH 0817/1439] Resolves #1332: Additional documentation on discrepency between known_x and sections=X --- README.md | 13 +++++++++++++ isort/settings.py | 3 ++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e58235097..0d48c5511 100644 --- a/README.md +++ b/README.md @@ -339,6 +339,19 @@ no_lines_before=LOCALFOLDER would produce a section with both FIRSTPARTY and LOCALFOLDER modules combined. +**IMPORTANT NOTE**: It is very important to know when setting `known` sections that the naming +does not directly map for historical reasons. For custom settings, the only difference is +capitalization (`known_custom=custom` VS `sections=CUSTOM,...`) for all others reference the +following mapping: + + - `known_standard_library` : `STANDARD_LIBRARY` + - `known_future_library` : `FUTURE` + - `known_first_party`: `FIRSTPARTY` + - `known_third_party`: `THIRDPARTY` + - `known_local_folder`: `LOCALFOLDER` + +This will likely be changed in isort 6.0.0+ in a backwards compatible way. + Auto-comment import sections ============================ diff --git a/isort/settings.py b/isort/settings.py index 46c6002a1..74ec6cd73 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -19,7 +19,7 @@ from .exceptions import InvalidSettingsPath, ProfileDoesNotExist from .profiles import profiles from .sections import DEFAULT as SECTION_DEFAULTS -from .sections import FIRSTPARTY, FUTURE, STDLIB, THIRDPARTY +from .sections import FIRSTPARTY, FUTURE, LOCALFOLDER, STDLIB, THIRDPARTY from .wrap_modes import WrapModes from .wrap_modes import from_string as wrap_mode_from_string @@ -57,6 +57,7 @@ FUTURE: "FUTURE_LIBRARY", FIRSTPARTY: "FIRST_PARTY", THIRDPARTY: "THIRD_PARTY", + LOCALFOLDER: "LOCAL_FOLDER", } RUNTIME_SOURCE = "runtime" From 67b2a54fdb314650f477d7a8aa9aa46e0245d6e5 Mon Sep 17 00:00:00 2001 From: Ryo Miyajima Date: Wed, 22 Jul 2020 12:02:26 +0900 Subject: [PATCH 0818/1439] remove minimum indents --- isort/api.py | 2 +- tests/test_isort.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/isort/api.py b/isort/api.py index f6453d383..b2c00109f 100644 --- a/isort/api.py +++ b/isort/api.py @@ -610,7 +610,7 @@ def _sort_imports( if indent: import_section = line_separator.join( - line.lstrip() for line in import_section.split(line_separator) + line[len(indent):] for line in import_section.split(line_separator) ) out_config = Config( config=config, diff --git a/tests/test_isort.py b/tests/test_isort.py index 99d0f2d5b..13b81ae99 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -3957,6 +3957,17 @@ def import_test(): ) +def test_isort_skipped_nested_imports() -> None: + """Ensure `isort:skip`s are honored in nested imports""" + test_input = """ + def import_test(): + from os ( # isort:skip + import path + ) + """ + assert isort.code(test_input) == test_input + + def test_isort_off() -> None: """Test that isort can be turned on and off at will using comments""" test_input = """import os From ef03ccce541467210aa0ceda20c10f67096d4e56 Mon Sep 17 00:00:00 2001 From: Ryo Miyajima Date: Wed, 22 Jul 2020 12:10:39 +0900 Subject: [PATCH 0819/1439] lint --- isort/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/api.py b/isort/api.py index b2c00109f..fe540e3c8 100644 --- a/isort/api.py +++ b/isort/api.py @@ -610,7 +610,7 @@ def _sort_imports( if indent: import_section = line_separator.join( - line[len(indent):] for line in import_section.split(line_separator) + line[len(indent) :] for line in import_section.split(line_separator) ) out_config = Config( config=config, From 9205b6981ffac4920bc42e0ddeba28e445e42c9d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 21 Jul 2020 23:05:09 -0700 Subject: [PATCH 0820/1439] Fixed #1339: Extra indent is not preserved when isort:skip is used in nested imports. --- CHANGELOG.md | 1 + isort/api.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b6eba858..5363f9488 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. ### 5.2.0 TBD - Implemented #1335: Official API for diff capturing. - Implemented #1331: Warn when sections don't match up. + - Fixed #1339: Extra indent is not preserved when isort:skip is used in nested imports. ### 5.1.4 July 19, 2020 - Fixed issue #1333: Use of wrap_length raises an exception about it not being lower or equal to line_length. diff --git a/isort/api.py b/isort/api.py index 4e5e4479d..2ef7a0f58 100644 --- a/isort/api.py +++ b/isort/api.py @@ -623,8 +623,8 @@ def _sort_imports( first_import_section = False if indent: - import_section = line_separator.join( - line.lstrip() for line in import_section.split(line_separator) + import_section = "".join( + line[len(indent):] for line in import_section.splitlines(keepends=True) ) out_config = Config( config=config, From 22d45d191cf0cd8f2eb2fd2c3a10826d05086c4b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 21 Jul 2020 23:12:58 -0700 Subject: [PATCH 0821/1439] Fix formatting (black) --- isort/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/api.py b/isort/api.py index 2ef7a0f58..8fb92bc2a 100644 --- a/isort/api.py +++ b/isort/api.py @@ -624,7 +624,7 @@ def _sort_imports( if indent: import_section = "".join( - line[len(indent):] for line in import_section.splitlines(keepends=True) + line[len(indent) :] for line in import_section.splitlines(keepends=True) ) out_config = Config( config=config, From d6d164c0a41eb25e319472944ed6b1e4aee709a9 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 21 Jul 2020 23:23:37 -0700 Subject: [PATCH 0822/1439] Move new test over to regression test suite (issue #1339) --- tests/test_isort.py | 11 ----------- tests/test_regressions.py | 15 +++++++++++++++ 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/tests/test_isort.py b/tests/test_isort.py index 13b81ae99..99d0f2d5b 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -3957,17 +3957,6 @@ def import_test(): ) -def test_isort_skipped_nested_imports() -> None: - """Ensure `isort:skip`s are honored in nested imports""" - test_input = """ - def import_test(): - from os ( # isort:skip - import path - ) - """ - assert isort.code(test_input) == test_input - - def test_isort_off() -> None: """Test that isort can be turned on and off at will using comments""" test_input = """import os diff --git a/tests/test_regressions.py b/tests/test_regressions.py index e4b6d3045..b62173883 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -463,3 +463,18 @@ def function(): wrap_length=17, show_diff=True, ) + + +def test_isort_skipped_nested_imports_issue_1339(): + """Ensure `isort:skip are honored in nested imports. + See: https://github.com/timothycrosley/isort/issues/1339. + """ + assert isort.check_code( + """ + def import_test(): + from os ( # isort:skip + import path + ) + """, + show_diff=True, + ) From 4bc459e10706b6f278a0dce8c3e200515af85b92 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 21 Jul 2020 23:28:03 -0700 Subject: [PATCH 0823/1439] Add Ryo Miyajima (@sergeant-wizard) to acknowledgements --- docs/contributing/4.-acknowledgements.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index 62e5bf742..0bf5f938e 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -99,6 +99,7 @@ Code Contributors - Seung Hyeon, Kim (@hyeonjames) - Gerard Dalmau (@gdalmau) - Robert Tasarz (@rtasarz) +- Ryo Miyajima (@sergeant-wizard) Documenters From 064b26e8d2cc9488fd1c118a567f64ccc535eb15 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 21 Jul 2020 23:44:19 -0700 Subject: [PATCH 0824/1439] Resolves #1261: Allow filter_files to be configured in config files --- CHANGELOG.md | 1 + isort/main.py | 6 +++--- isort/settings.py | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5363f9488..fb1f6ed76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. ### 5.2.0 TBD - Implemented #1335: Official API for diff capturing. - Implemented #1331: Warn when sections don't match up. + - Implemented #1261: By popular demand, `filter_files` can now be set in the config option. - Fixed #1339: Extra indent is not preserved when isort:skip is used in nested imports. ### 5.1.4 July 19, 2020 diff --git a/isort/main.py b/isort/main.py index bf6ff038b..0d45df358 100644 --- a/isort/main.py +++ b/isort/main.py @@ -529,7 +529,8 @@ def _build_arg_parser() -> argparse.ArgumentParser: dest="filter_files", action="store_true", help="Tells isort to filter files even when they are explicitly passed in as " - "part of the command", + "part of the CLI command. Note that while this can be set as part of the config file it " + "only affects CLI operation.", ) parser.add_argument( "files", nargs="*", help="One or more Python source files that need their imports sorted." @@ -707,7 +708,6 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = config_dict = arguments.copy() ask_to_apply = config_dict.pop("ask_to_apply", False) jobs = config_dict.pop("jobs", ()) - filter_files = config_dict.pop("filter_files", False) check = config_dict.pop("check", False) show_diff = config_dict.pop("show_diff", False) write_to_stdout = config_dict.pop("write_to_stdout", False) @@ -733,7 +733,7 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = wrong_sorted_files = False skipped: List[str] = [] - if filter_files: + if config.filter_files: filtered_files = [] for file_name in file_names: if config.is_skipped(Path(file_name)): diff --git a/isort/settings.py b/isort/settings.py index 74ec6cd73..73940fb3f 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -168,6 +168,7 @@ class _Config: old_finders: bool = False remove_redundant_aliases: bool = False float_to_top: bool = False + filter_files: bool = False def __post_init__(self): py_version = self.py_version From 42135a3dc25a4f3d3d37875fc0117dfffd84d69e Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 21 Jul 2020 23:45:25 -0700 Subject: [PATCH 0825/1439] Update filter files config option docs --- docs/configuration/options.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/configuration/options.md b/docs/configuration/options.md index 303932487..affacd117 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -761,6 +761,17 @@ Causes all non indented imports to float to the top of the file having its impor - --float-to-top +## Filter Files + +Tells isort to filter files even when they are explicitly passed in as part of the CLI command. Note that while this can be set as part of the config file it only affects CLI operation. + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** filter_files +**CLI Flags:** + +- --filter-files + ## Check Checks the file for unsorted / unformatted imports and prints them to the command line without modifying the file. @@ -869,17 +880,6 @@ Returns just the current version number without the logo - --vn - --version-number -## Filter Files - -Tells isort to filter files even when they are explicitly passed in as part of the command - -**Type:** Bool -**Default:** `False` -**Python & Config File Name:** **Not Supported** -**CLI Flags:** - -- --filter-files - ## Files One or more Python source files that need their imports sorted. From b37951acd33d05277c9db5437f86c4cb4a83f494 Mon Sep 17 00:00:00 2001 From: Timothy Edmund Crosley Date: Wed, 22 Jul 2020 00:01:41 -0700 Subject: [PATCH 0826/1439] Update settings.py --- isort/settings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/isort/settings.py b/isort/settings.py index 5a9530ec1..a1db641a9 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -7,6 +7,7 @@ import os import posixpath import re +import subprocess import sys from functools import lru_cache from pathlib import Path @@ -374,7 +375,7 @@ def is_skipped(self, file_path: Path) -> bool: os_path = str(file_path) if self.skip_gitignore: - result = subprocess.run(['git', 'check-ignore', '--quiet', filename]) + result = subprocess.run(['git', 'check-ignore', '--quiet', os_path]) if result.returncode == 0: return True From 060571c229a9770d114aad6d5e046e5d8c2ac5ad Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 22 Jul 2020 00:03:33 -0700 Subject: [PATCH 0827/1439] Improve documentation around usage of isort: split --- docs/configuration/action_comments.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/configuration/action_comments.md b/docs/configuration/action_comments.md index c9be7d43b..ca1edd25e 100644 --- a/docs/configuration/action_comments.md +++ b/docs/configuration/action_comments.md @@ -35,7 +35,7 @@ import b import a # isort: skip <- this will now stay below b ``` !!! note - It is recommended to where possible use `# isort: off` and `# isort: on` instead as the behaviour is more explicit and predictable. + It is recommended to where possible use `# isort: off` and `# isort: on` or `# isort: split` instead as the behavior is more explicit and predictable. ## isort: off @@ -96,5 +96,13 @@ import d ``` +You can also use it inline to keep an import from having imports above or below it swap position: + +```python +import c +import b # isort: split +import a +``` + !!! tip isort split is exactly the same as placing an `# isort: on` immediately below an `# isort: off` From 65e9143c94f35904c31a930840128c942d14e48c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 22 Jul 2020 00:06:10 -0700 Subject: [PATCH 0828/1439] Add @mdagaro to code contributors list --- docs/contributing/4.-acknowledgements.md | 1 + isort/main.py | 2 +- isort/settings.py | 10 +++++----- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index 0bf5f938e..1a892420b 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -100,6 +100,7 @@ Code Contributors - Gerard Dalmau (@gdalmau) - Robert Tasarz (@rtasarz) - Ryo Miyajima (@sergeant-wizard) +- @mdagaro Documenters diff --git a/isort/main.py b/isort/main.py index 5c5a7ad4b..a3b20a241 100644 --- a/isort/main.py +++ b/isort/main.py @@ -424,7 +424,7 @@ def _build_arg_parser() -> argparse.ArgumentParser: "--skip-gitignore", action="store_true", dest="skip_gitignore", - help="Treat project as a git respository and ignore files listed in .gitignore" + help="Treat project as a git respository and ignore files listed in .gitignore", ) inline_args_group.add_argument( "--sl", diff --git a/isort/settings.py b/isort/settings.py index a1db641a9..4fec3159d 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -373,11 +373,11 @@ def is_skipped(self, file_path: Path) -> bool: file_name = str(file_path) os_path = str(file_path) - + if self.skip_gitignore: - result = subprocess.run(['git', 'check-ignore', '--quiet', os_path]) - if result.returncode == 0: - return True + result = subprocess.run(["git", "check-ignore", "--quiet", os_path]) + if result.returncode == 0: + return True normalized_path = os_path.replace("\\", "/") if normalized_path[1:2] == ":": @@ -595,7 +595,7 @@ def _get_config_data(file_path: str, sections: Tuple[str]) -> Dict[str, Any]: return settings - + def _as_bool(value: str) -> bool: """Given a string value that represents True or False, returns the Boolean equivalent. Heavily inspired from distutils strtobool. From 943262adc08d05e312170791bfd2afd1e19dda69 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 22 Jul 2020 00:09:08 -0700 Subject: [PATCH 0829/1439] isort + black --- isort/settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/settings.py b/isort/settings.py index 4fec3159d..7deeb75b4 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -7,7 +7,7 @@ import os import posixpath import re -import subprocess +import subprocess # nosec: Needed for gitignore support. import sys from functools import lru_cache from pathlib import Path @@ -375,7 +375,7 @@ def is_skipped(self, file_path: Path) -> bool: os_path = str(file_path) if self.skip_gitignore: - result = subprocess.run(["git", "check-ignore", "--quiet", os_path]) + result = subprocess.run(["git", "check-ignore", "--quiet", os_path]) # nosec if result.returncode == 0: return True From 09cf66c0d4ad54273e43b3691ea7c916374e40b6 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 22 Jul 2020 00:42:04 -0700 Subject: [PATCH 0830/1439] Add test cases for #960 --- isort/settings.py | 4 +++- tests/test_main.py | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/isort/settings.py b/isort/settings.py index 7deeb75b4..b36b6143b 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -375,7 +375,9 @@ def is_skipped(self, file_path: Path) -> bool: os_path = str(file_path) if self.skip_gitignore: - result = subprocess.run(["git", "check-ignore", "--quiet", os_path]) # nosec + result = subprocess.run( # nosec + ["git", "-C", file_path.resolve().parent, "check-ignore", "--quiet", os_path] + ) if result.returncode == 0: return True diff --git a/tests/test_main.py b/tests/test_main.py index c378c416a..3d8f20117 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,5 +1,6 @@ import json import os +import subprocess import sys from datetime import datetime from io import BytesIO, TextIOWrapper @@ -227,6 +228,20 @@ def test_main(capsys, tmpdir): # without filter options passed in should successfully sort files main.main([str(python_file), str(should_skip), "--verbose", "--atomic"]) + # should respect gitignore if requested. + out, error = capsys.readouterr() # clear sysoutput before tests + subprocess.run(["git", "init", str(tmpdir)]) + main.main([str(python_file), "--skip-gitignore", "--filter-files"]) + out, error = capsys.readouterr() + assert "Skipped" not in out + tmpdir.join(".gitignore").write("has_imports.py") + main.main([str(python_file)]) + out, error = capsys.readouterr() + assert "Skipped" not in out + main.main([str(python_file), "--skip-gitignore", "--filter-files"]) + out, error = capsys.readouterr() + assert "Skipped" in out + def test_isort_command(): """Ensure ISortCommand got registered, otherwise setuptools error must have occured""" From cba1fca3dc38e3331b3297b06e714e97d3126d6a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 22 Jul 2020 00:43:49 -0700 Subject: [PATCH 0831/1439] Add documentation for issue #960 resolution --- CHANGELOG.md | 1 + docs/configuration/options.md | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb1f6ed76..aaabf766f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. - Implemented #1335: Official API for diff capturing. - Implemented #1331: Warn when sections don't match up. - Implemented #1261: By popular demand, `filter_files` can now be set in the config option. + - Implemented #960: Support for respecting git ignore via "--gitignore" or "skip_gitignore=True". - Fixed #1339: Extra indent is not preserved when isort:skip is used in nested imports. ### 5.1.4 July 19, 2020 diff --git a/docs/configuration/options.md b/docs/configuration/options.md index affacd117..10479a7cd 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -54,6 +54,18 @@ Files that sort imports should skip over. - --sg - --skip-glob +## Skip Gitignore + +Treat project as a git respository and ignore files listed in .gitignore + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** skip_gitignore +**CLI Flags:** + +- --gitignore +- --skip-gitignore + ## Line Length The max length of an import line (used for wrapping long imports). From 716966af6e0d866f45cbfc7f99e4dd081c7ec9c8 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 22 Jul 2020 00:53:25 -0700 Subject: [PATCH 0832/1439] Fix windows OS test failure --- isort/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/settings.py b/isort/settings.py index b36b6143b..9370904b3 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -376,7 +376,7 @@ def is_skipped(self, file_path: Path) -> bool: if self.skip_gitignore: result = subprocess.run( # nosec - ["git", "-C", file_path.resolve().parent, "check-ignore", "--quiet", os_path] + ["git", "-C", str(file_path.resolve().parent), "check-ignore", "--quiet", os_path] ) if result.returncode == 0: return True From 259beb44d6b9ca35e11ee86c31b2bfe8c3833f38 Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Wed, 22 Jul 2020 21:18:58 +0300 Subject: [PATCH 0833/1439] Adds script that displays unacknowledged authors. --- scripts/check_acknowledgments.py | 85 ++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100755 scripts/check_acknowledgments.py diff --git a/scripts/check_acknowledgments.py b/scripts/check_acknowledgments.py new file mode 100755 index 000000000..1466cd978 --- /dev/null +++ b/scripts/check_acknowledgments.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +import os +import re +import sys +from concurrent.futures import ThreadPoolExecutor +from functools import partial +from pathlib import Path +from typing import Any, Dict, Iterable, List, Set, Union + +import requests + +IGNORED_AUTHOR_LOGINS = {"deepsource-autofix[bot]"} + +GITHUB_USER_PATTERN = re.compile(r"@(?P([a-z\d]+-)*[a-z\d]+)") +REQUESTS_TIMEOUT = (60, 60) +REPO = "timothycrosley/isort" +GITHUB_API_CONTRIBUTORS = f"https://api.github.com/repos/{REPO}/contributors" +GITHUB_USER_CONTRIBUTIONS = f"https://github.com/{REPO}/commits?author=" +USER_DELIMITER = "-" * 80 + + +def _acknowledged_logins() -> Set[str]: + project_root = Path(__file__).parent.parent + acknowledgments_file = project_root / "docs" / "contributing" / "4.-acknowledgements.md" + markdown_text = acknowledgments_file.read_text() + logins = {match.group("login") for match in re.finditer(GITHUB_USER_PATTERN, markdown_text)} + return logins + + +def _request_json(url: str) -> Any: + r = requests.get(url, timeout=REQUESTS_TIMEOUT) + r.raise_for_status() + return r.json() + + +_fetch_author_logins = partial(_request_json, GITHUB_API_CONTRIBUTORS) +_fetch_user = _request_json + + +def _fetch_users(urls: Iterable[str]) -> Iterable[Dict[str, Any]]: + with ThreadPoolExecutor() as executor: + return executor.map(_fetch_user, urls) + + +def _user_info(user: Dict[str, str], verbose=False) -> str: + login = "@" + user["login"] + name = user.get("name") + display_name = f"{name} ({login})" if name else login + user_info = f"- {display_name}" + if verbose: + contributions = f" {GITHUB_USER_CONTRIBUTIONS}{user['login']}" + user_info += "\n" + contributions + return user_info + + +def main(): + acknowledged_logins = _acknowledged_logins() + authors = (a for a in _fetch_author_logins() if a["login"] not in IGNORED_AUTHOR_LOGINS) + + unacknowledged_author_urls = ( + author["url"] for author in authors if author["login"] not in acknowledged_logins + ) + unacknowledged_users = list(_fetch_users(unacknowledged_author_urls)) + + if not unacknowledged_users: + sys.exit() + + print("Found unacknowledged authors:") + print() + + for user in unacknowledged_users: + print(_user_info(user, verbose=True)) + print(USER_DELIMITER) + + print() + print("Printing again for easy inclusion in Markdown file:") + print() + for user in unacknowledged_users: + print(_user_info(user)) + + sys.exit(1) + + +if __name__ == "__main__": + main() From 7e5df1bfc0fee409962cc1b7818a473c8ac58621 Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Wed, 22 Jul 2020 21:46:23 +0300 Subject: [PATCH 0834/1439] Remove unused import (os) --- scripts/check_acknowledgments.py | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/check_acknowledgments.py b/scripts/check_acknowledgments.py index 1466cd978..c7d4b396d 100755 --- a/scripts/check_acknowledgments.py +++ b/scripts/check_acknowledgments.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -import os import re import sys from concurrent.futures import ThreadPoolExecutor From c4a266e82d4322a6551ded66dfc77f4d336ffea6 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 22 Jul 2020 21:42:27 -0700 Subject: [PATCH 0835/1439] isort: split can now be used at the end of an import line. --- CHANGELOG.md | 1 + docs/contributing/4.-acknowledgements.md | 16 ++ isort/api.py | 2 +- isort/parse.py | 2 +- poetry.lock | 180 ++++++++++++++++++++++- pyproject.toml | 1 + scripts/check_acknowledgments.py | 108 +++++++------- tests/test_isort.py | 9 ++ 8 files changed, 256 insertions(+), 63 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aaabf766f..5e27ce571 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. - Implemented #1331: Warn when sections don't match up. - Implemented #1261: By popular demand, `filter_files` can now be set in the config option. - Implemented #960: Support for respecting git ignore via "--gitignore" or "skip_gitignore=True". + - `# isort: split` can now be used at the end of an import line. - Fixed #1339: Extra indent is not preserved when isort:skip is used in nested imports. ### 5.1.4 July 19, 2020 diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index 1a892420b..d602d9c3c 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -101,6 +101,22 @@ Code Contributors - Robert Tasarz (@rtasarz) - Ryo Miyajima (@sergeant-wizard) - @mdagaro +- Maksim Kurnikov (@mkurnikov) +- Daniel Hahler (@blueyed) +- Dario Navin (@Zarathustra2) +- @ucodery +- Aarni Koskela (@akx) +- Alex Chan (@alexwlchan) +- Rick Thomas (@richardlthomas) +- Adam Hitchcock (@NorthIsUp) +- Danny Weinberg (@FuegoFro) +- Jeppe Fihl-Pearson (@Tenzer) +- Jonas Lundberg (@lundberg) +- Neil (@NeilGirdhar) +- @dmanikowski-reef +- Stephen Brown II (@StephenBrown2) +- Ankur Dedania (@AnkurDedania) +- Tamas Szabo (@sztamas) Documenters diff --git a/isort/api.py b/isort/api.py index 8fb92bc2a..d135fd98c 100644 --- a/isort/api.py +++ b/isort/api.py @@ -516,7 +516,7 @@ def _sort_imports( elif stripped_line == "# isort: off": not_imports = True isort_off = True - elif stripped_line == "# isort: split": + elif stripped_line.endswith("# isort: split"): not_imports = True elif stripped_line in config.section_comments and not import_section: import_section += line diff --git a/isort/parse.py b/isort/parse.py index 283d09a71..b86802c36 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -55,7 +55,7 @@ def import_type(line: str, config: Config = DEFAULT_CONFIG) -> Optional[str]: """If the current line is an import line it will return its type (from or straight)""" if config.honor_noqa and line.lower().rstrip().endswith("noqa"): return None - elif "isort:skip" in line or "isort: skip" in line: + elif "isort:skip" in line or "isort: skip" in line or "isort: split" in line: return None elif line.startswith(("import ", "cimport ")): return "straight" diff --git a/poetry.lock b/poetry.lock index b64bd119c..b1bdaa498 100644 --- a/poetry.lock +++ b/poetry.lock @@ -151,6 +151,18 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" version = "0.4.3" +[[package]] +category = "dev" +description = "PEP 567 Backport" +marker = "python_version < \"3.7\"" +name = "contextvars" +optional = false +python-versions = "*" +version = "2.4" + +[package.dependencies] +immutables = ">=0.9" + [[package]] category = "dev" description = "A command-line utility that creates projects from project templates, e.g. creating a Python package project from a Python package project template." @@ -347,6 +359,72 @@ version = "3.1.3" [package.dependencies] gitdb = ">=4.0.1,<5" +[[package]] +category = "dev" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +name = "h11" +optional = false +python-versions = "*" +version = "0.9.0" + +[[package]] +category = "dev" +description = "HTTP/2 State-Machine based protocol implementation" +name = "h2" +optional = false +python-versions = "*" +version = "3.2.0" + +[package.dependencies] +hpack = ">=3.0,<4" +hyperframe = ">=5.2.0,<6" + +[[package]] +category = "dev" +description = "Pure-Python HPACK header compression" +name = "hpack" +optional = false +python-versions = "*" +version = "3.0.0" + +[[package]] +category = "dev" +description = "Chromium HSTS Preload list as a Python package and updated daily" +name = "hstspreload" +optional = false +python-versions = ">=3.6" +version = "2020.7.22" + +[[package]] +category = "dev" +description = "A minimal low-level HTTP client." +name = "httpcore" +optional = false +python-versions = ">=3.6" +version = "0.9.1" + +[package.dependencies] +h11 = ">=0.8,<0.10" +h2 = ">=3.0.0,<4.0.0" +sniffio = ">=1.0.0,<2.0.0" + +[[package]] +category = "dev" +description = "The next generation HTTP client." +name = "httpx" +optional = false +python-versions = ">=3.6" +version = "0.13.3" + +[package.dependencies] +certifi = "*" +chardet = ">=3.0.0,<4.0.0" +hstspreload = "*" +httpcore = ">=0.9.0,<0.10.0" +idna = ">=2.0.0,<3.0.0" +rfc3986 = ">=1.3,<2" +sniffio = "*" + [[package]] category = "dev" description = "A Python framework that makes developing APIs as simple as possible, but no simpler." @@ -359,6 +437,14 @@ version = "2.6.1" falcon = "2.0.0" requests = "*" +[[package]] +category = "dev" +description = "HTTP/2 framing layer for Python" +name = "hyperframe" +optional = false +python-versions = "*" +version = "5.2.0" + [[package]] category = "dev" description = "A library for property-based testing" @@ -405,6 +491,15 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "2.9" +[[package]] +category = "dev" +description = "Immutable Collections" +marker = "python_version < \"3.7\"" +name = "immutables" +optional = false +python-versions = ">=3.5" +version = "0.14" + [[package]] category = "main" description = "Read metadata from Python packages" @@ -1172,6 +1267,17 @@ dev = ["vulture", "flake8", "rope", "isort", "invoke", "twine", "pre-commit", "l tests = ["mock", "pytest", "twine", "readme-renderer", "pytest-xdist", "pytest-cov", "pytest-timeout", "coverage", "hypothesis"] typing = ["typing", "mypy", "mypy-extensions", "mypytools", "pytype", "typed-ast", "monkeytype"] +[[package]] +category = "dev" +description = "Validating URI References per RFC 3986" +name = "rfc3986" +optional = false +python-versions = "*" +version = "1.4.0" + +[package.extras] +idna2008 = ["idna"] + [[package]] category = "dev" description = "Checks installed dependencies for known vulnerabilities." @@ -1214,6 +1320,19 @@ version = "3.0.1" [package.dependencies] smmap = ">=3.0.1" +[[package]] +category = "dev" +description = "Sniff out which async library your code is running under" +name = "sniffio" +optional = false +python-versions = ">=3.5" +version = "1.1.0" + +[package.dependencies] +[package.dependencies.contextvars] +python = "<3.7" +version = ">=2.1" + [[package]] category = "dev" description = "This package provides 26 stemmers for 25 languages generated from Snowball algorithms." @@ -1409,13 +1528,11 @@ docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] testing = ["jaraco.itertools", "func-timeout"] [extras] -pipfile = ["pipreqs", "tomlkit", "requirementslib"] -pyproject = ["toml"] -requirements = ["pipreqs", "pip-api"] -xdg_home = ["appdirs"] +pipfile_deprecated_finder = ["pipreqs", "tomlkit", "requirementslib"] +requirements_deprecated_finder = ["pipreqs", "pip-api"] [metadata] -content-hash = "983dd12d7389a2267be62bd2f32082e7a7f66b2ef89c3298f5a688df6e70a715" +content-hash = "ace34d56082ee3f0ccfa77f6c8b6a6e8a6449a1e51a0e31a69368c697af7384b" python-versions = "^3.6" [metadata.files] @@ -1478,6 +1595,9 @@ colorama = [ {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, ] +contextvars = [ + {file = "contextvars-2.4.tar.gz", hash = "sha256:f38c908aaa59c14335eeea12abea5f443646216c4e29380d7bf34d2018e2c39e"}, +] cookiecutter = [ {file = "cookiecutter-1.7.2-py2.py3-none-any.whl", hash = "sha256:430eb882d028afb6102c084bab6cf41f6559a77ce9b18dc6802e3bc0cc5f4a30"}, {file = "cookiecutter-1.7.2.tar.gz", hash = "sha256:efb6b2d4780feda8908a873e38f0e61778c23f6a2ea58215723bcceb5b515dac"}, @@ -1584,10 +1704,38 @@ gitpython = [ {file = "GitPython-3.1.3-py3-none-any.whl", hash = "sha256:ef1d60b01b5ce0040ad3ec20bc64f783362d41fa0822a2742d3586e1f49bb8ac"}, {file = "GitPython-3.1.3.tar.gz", hash = "sha256:e107af4d873daed64648b4f4beb89f89f0cfbe3ef558fc7821ed2331c2f8da1a"}, ] +h11 = [ + {file = "h11-0.9.0-py2.py3-none-any.whl", hash = "sha256:4bc6d6a1238b7615b266ada57e0618568066f57dd6fa967d1290ec9309b2f2f1"}, + {file = "h11-0.9.0.tar.gz", hash = "sha256:33d4bca7be0fa039f4e84d50ab00531047e53d6ee8ffbc83501ea602c169cae1"}, +] +h2 = [ + {file = "h2-3.2.0-py2.py3-none-any.whl", hash = "sha256:61e0f6601fa709f35cdb730863b4e5ec7ad449792add80d1410d4174ed139af5"}, + {file = "h2-3.2.0.tar.gz", hash = "sha256:875f41ebd6f2c44781259005b157faed1a5031df3ae5aa7bcb4628a6c0782f14"}, +] +hpack = [ + {file = "hpack-3.0.0-py2.py3-none-any.whl", hash = "sha256:0edd79eda27a53ba5be2dfabf3b15780928a0dff6eb0c60a3d6767720e970c89"}, + {file = "hpack-3.0.0.tar.gz", hash = "sha256:8eec9c1f4bfae3408a3f30500261f7e6a65912dc138526ea054f9ad98892e9d2"}, +] +hstspreload = [ + {file = "hstspreload-2020.7.22-py3-none-any.whl", hash = "sha256:79edbdbd09346b4c5cf729384498818943114b0a4f939a5f80abacbc47aa2197"}, + {file = "hstspreload-2020.7.22.tar.gz", hash = "sha256:7bc3d59d3f8c8dd03f0266f7bb309070e6a968edc19d29a812ebd49b280c5965"}, +] +httpcore = [ + {file = "httpcore-0.9.1-py3-none-any.whl", hash = "sha256:9850fe97a166a794d7e920590d5ec49a05488884c9fc8b5dba8561effab0c2a0"}, + {file = "httpcore-0.9.1.tar.gz", hash = "sha256:ecc5949310d9dae4de64648a4ce529f86df1f232ce23dcfefe737c24d21dfbe9"}, +] +httpx = [ + {file = "httpx-0.13.3-py3-none-any.whl", hash = "sha256:32d930858eab677bc29a742aaa4f096de259f1c78c68a90ad11f5c3c04f08335"}, + {file = "httpx-0.13.3.tar.gz", hash = "sha256:3642bd13e90b80ba8a243a730275eb10a4c26ec96f5fc16b87e458d4ab21efae"}, +] hug = [ {file = "hug-2.6.1-py2.py3-none-any.whl", hash = "sha256:31c8fc284f81377278629a4b94cbb619ae9ce829cdc2da9564ccc66a121046b4"}, {file = "hug-2.6.1.tar.gz", hash = "sha256:b0edace2acb618873779c9ce6ecf9165db54fef95c22262f5700fcdd9febaec9"}, ] +hyperframe = [ + {file = "hyperframe-5.2.0-py2.py3-none-any.whl", hash = "sha256:5187962cb16dcc078f23cb5a4b110098d546c3f41ff2d4038a9896893bbd0b40"}, + {file = "hyperframe-5.2.0.tar.gz", hash = "sha256:a9f5c17f2cc3c719b917c4f33ed1c61bd1f8dfac4b1bd23b7c80b3400971b41f"}, +] hypothesis = [ {file = "hypothesis-5.16.0-py3-none-any.whl", hash = "sha256:21bb5fbe456f775233fe20bcbeb26f648d68025bce554c94c0698fb4c33e7008"}, {file = "hypothesis-5.16.0.tar.gz", hash = "sha256:7c819501a26ff82ccb4bee8a96b1ff4fff187e041d79ed176964ca550b77098b"}, @@ -1600,6 +1748,20 @@ idna = [ {file = "idna-2.9-py2.py3-none-any.whl", hash = "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"}, {file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"}, ] +immutables = [ + {file = "immutables-0.14-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:860666fab142401a5535bf65cbd607b46bc5ed25b9d1eb053ca8ed9a1a1a80d6"}, + {file = "immutables-0.14-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:ce01788878827c3f0331c254a4ad8d9721489a5e65cc43e19c80040b46e0d297"}, + {file = "immutables-0.14-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:8797eed4042f4626b0bc04d9cf134208918eb0c937a8193a2c66df5041e62d2e"}, + {file = "immutables-0.14-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:33ce2f977da7b5e0dddd93744862404bdb316ffe5853ec853e53141508fa2e6a"}, + {file = "immutables-0.14-cp36-cp36m-win_amd64.whl", hash = "sha256:6c8eace4d98988c72bcb37c05e79aae756832738305ae9497670482a82db08bc"}, + {file = "immutables-0.14-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:ab6c18b7b2b2abc83e0edc57b0a38bf0915b271582a1eb8c7bed1c20398f8040"}, + {file = "immutables-0.14-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c099212fd6504513a50e7369fe281007c820cf9d7bb22a336486c63d77d6f0b2"}, + {file = "immutables-0.14-cp37-cp37m-win_amd64.whl", hash = "sha256:714aedbdeba4439d91cb5e5735cb10631fc47a7a69ea9cc8ecbac90322d50a4a"}, + {file = "immutables-0.14-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:1c11050c49e193a1ec9dda1747285333f6ba6a30bbeb2929000b9b1192097ec0"}, + {file = "immutables-0.14-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:c453e12b95e1d6bb4909e8743f88b7f5c0c97b86a8bc0d73507091cb644e3c1e"}, + {file = "immutables-0.14-cp38-cp38-win_amd64.whl", hash = "sha256:ef9da20ec0f1c5853b5c8f8e3d9e1e15b8d98c259de4b7515d789a606af8745e"}, + {file = "immutables-0.14.tar.gz", hash = "sha256:a0a1cc238b678455145bae291d8426f732f5255537ed6a5b7645949704c70a78"}, +] importlib-metadata = [ {file = "importlib_metadata-1.6.1-py2.py3-none-any.whl", hash = "sha256:15ec6c0fd909e893e3a08b3a7c76ecb149122fb14b7efe1199ddd4c7c57ea958"}, {file = "importlib_metadata-1.6.1.tar.gz", hash = "sha256:0505dd08068cfec00f53a74a0ad927676d7757da81b7436a6eefe4c7cf75c545"}, @@ -1929,6 +2091,10 @@ requirementslib = [ {file = "requirementslib-1.5.11-py2.py3-none-any.whl", hash = "sha256:3dea8f577be1f7f5aafb66007cb49ef659f9249649aa11312a903fa4205a7350"}, {file = "requirementslib-1.5.11.tar.gz", hash = "sha256:6acae5ba27c9a1a45d120d74e0b922b202e0c614591880ae060d06c1e77eff66"}, ] +rfc3986 = [ + {file = "rfc3986-1.4.0-py2.py3-none-any.whl", hash = "sha256:af9147e9aceda37c91a05f4deb128d4b4b49d6b199775fd2d2927768abdc8f50"}, + {file = "rfc3986-1.4.0.tar.gz", hash = "sha256:112398da31a3344dc25dbf477d8df6cb34f9278a94fee2625d89e4514be8bb9d"}, +] safety = [ {file = "safety-1.9.0-py2.py3-none-any.whl", hash = "sha256:86c1c4a031fe35bd624fce143fbe642a0234d29f7cbf7a9aa269f244a955b087"}, {file = "safety-1.9.0.tar.gz", hash = "sha256:23bf20690d4400edc795836b0c983c2b4cbbb922233108ff925b7dd7750f00c9"}, @@ -1945,6 +2111,10 @@ smmap2 = [ {file = "smmap2-3.0.1-py3-none-any.whl", hash = "sha256:0cb6ea470b1ad9a65a02ca7f4c7ae601861f7dd24a43812ca51cfca2892bb524"}, {file = "smmap2-3.0.1.tar.gz", hash = "sha256:44cc8bdaf96442dbb9a8e2e14377d074b3d0eea292eee3c95c8c449b6c92c557"}, ] +sniffio = [ + {file = "sniffio-1.1.0-py3-none-any.whl", hash = "sha256:20ed6d5b46f8ae136d00b9dcb807615d83ed82ceea6b2058cecb696765246da5"}, + {file = "sniffio-1.1.0.tar.gz", hash = "sha256:8e3810100f69fe0edd463d02ad407112542a11ffdc29f67db2bf3771afb87a21"}, +] snowballstemmer = [ {file = "snowballstemmer-2.0.0-py2.py3-none-any.whl", hash = "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0"}, {file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"}, diff --git a/pyproject.toml b/pyproject.toml index 67087d85b..464e8f059 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,6 +74,7 @@ pip = "^20.0.2" pip-shims = "^0.5.2" smmap2 = "^3.0.1" gitdb2 = "^4.0.2" +httpx = "^0.13.3" [tool.poetry.scripts] isort = "isort.main:main" diff --git a/scripts/check_acknowledgments.py b/scripts/check_acknowledgments.py index c7d4b396d..fdc61606e 100755 --- a/scripts/check_acknowledgments.py +++ b/scripts/check_acknowledgments.py @@ -1,44 +1,23 @@ #!/usr/bin/env python3 -import re +import asyncio import sys -from concurrent.futures import ThreadPoolExecutor -from functools import partial from pathlib import Path -from typing import Any, Dict, Iterable, List, Set, Union +from typing import Dict -import requests +import httpx +import hug IGNORED_AUTHOR_LOGINS = {"deepsource-autofix[bot]"} -GITHUB_USER_PATTERN = re.compile(r"@(?P([a-z\d]+-)*[a-z\d]+)") -REQUESTS_TIMEOUT = (60, 60) REPO = "timothycrosley/isort" GITHUB_API_CONTRIBUTORS = f"https://api.github.com/repos/{REPO}/contributors" GITHUB_USER_CONTRIBUTIONS = f"https://github.com/{REPO}/commits?author=" +GITHUB_USER_TYPE = "User" USER_DELIMITER = "-" * 80 +PER_PAGE = 100 - -def _acknowledged_logins() -> Set[str]: - project_root = Path(__file__).parent.parent - acknowledgments_file = project_root / "docs" / "contributing" / "4.-acknowledgements.md" - markdown_text = acknowledgments_file.read_text() - logins = {match.group("login") for match in re.finditer(GITHUB_USER_PATTERN, markdown_text)} - return logins - - -def _request_json(url: str) -> Any: - r = requests.get(url, timeout=REQUESTS_TIMEOUT) - r.raise_for_status() - return r.json() - - -_fetch_author_logins = partial(_request_json, GITHUB_API_CONTRIBUTORS) -_fetch_user = _request_json - - -def _fetch_users(urls: Iterable[str]) -> Iterable[Dict[str, Any]]: - with ThreadPoolExecutor() as executor: - return executor.map(_fetch_user, urls) +_ACK_FILE = Path(__file__).parent.parent / "docs" / "contributing" / "4.-acknowledgements.md" +ACKNOWLEDGEMENTS = _ACK_FILE.read_text().lower() def _user_info(user: Dict[str, str], verbose=False) -> str: @@ -52,33 +31,50 @@ def _user_info(user: Dict[str, str], verbose=False) -> str: return user_info -def main(): - acknowledged_logins = _acknowledged_logins() - authors = (a for a in _fetch_author_logins() if a["login"] not in IGNORED_AUTHOR_LOGINS) - - unacknowledged_author_urls = ( - author["url"] for author in authors if author["login"] not in acknowledged_logins - ) - unacknowledged_users = list(_fetch_users(unacknowledged_author_urls)) - - if not unacknowledged_users: - sys.exit() - - print("Found unacknowledged authors:") - print() - - for user in unacknowledged_users: - print(_user_info(user, verbose=True)) - print(USER_DELIMITER) - - print() - print("Printing again for easy inclusion in Markdown file:") - print() - for user in unacknowledged_users: - print(_user_info(user)) - - sys.exit(1) +@hug.cli() +async def main(): + async with httpx.AsyncClient() as client: + page = 0 + results = [] + contributors = set() + while not page or len(results) == PER_PAGE: + page += 1 + response = await client.get(f"{GITHUB_API_CONTRIBUTORS}?per_page={PER_PAGE}&page{page}") + results = response.json() + print(results) + contributors.update( + ( + contributor + for contributor in results + if contributor["type"] == GITHUB_USER_TYPE + and contributer["login"] not in IGNORED_AUTHOR_LOGINS + and f"@{contributor['login']}" not in ACKNOWLEDGEMENTS + ) + ) + + breakpoint() + unacknowledged_users = await asyncio.gather( + (client.get(contributor["url"]).json() for contributor in contributors) + ) + + if not unacknowledged_users: + sys.exit() + + print("Found unacknowledged authors:") + print() + + for user in unacknowledged_users: + print(_user_info(user, verbose=True)) + print(USER_DELIMITER) + + print() + print("Printing again for easy inclusion in Markdown file:") + print() + for user in unacknowledged_users: + print(_user_info(user)) + + sys.exit(1) if __name__ == "__main__": - main() + main.interface.cli() diff --git a/tests/test_isort.py b/tests/test_isort.py index 99d0f2d5b..9d195583e 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -3983,6 +3983,15 @@ def test_isort_split() -> None: """ assert isort.code(test_input) == test_input + test_input = """import c + +import b # isort: split + +import a +import c +""" + assert isort.code(test_input) == test_input + def test_comment_look_alike(): """Test to ensure isort will handle what looks like a single line comment From fd0d9897e31a1bbc9c305da642b5974e79cf3c53 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 22 Jul 2020 22:41:23 -0700 Subject: [PATCH 0836/1439] Update to work via basic authentication --- scripts/check_acknowledgments.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/scripts/check_acknowledgments.py b/scripts/check_acknowledgments.py index fdc61606e..20f560c18 100755 --- a/scripts/check_acknowledgments.py +++ b/scripts/check_acknowledgments.py @@ -33,29 +33,33 @@ def _user_info(user: Dict[str, str], verbose=False) -> str: @hug.cli() async def main(): + auth = (input("Github Username: "), + getpass()) async with httpx.AsyncClient() as client: page = 0 results = [] - contributors = set() + contributors = [] while not page or len(results) == PER_PAGE: page += 1 - response = await client.get(f"{GITHUB_API_CONTRIBUTORS}?per_page={PER_PAGE}&page{page}") + response = await client.get( + f"{GITHUB_API_CONTRIBUTORS}?per_page={PER_PAGE}&page={page}", + auth=auth + ) results = response.json() - print(results) - contributors.update( + contributors.extend( ( contributor for contributor in results if contributor["type"] == GITHUB_USER_TYPE - and contributer["login"] not in IGNORED_AUTHOR_LOGINS - and f"@{contributor['login']}" not in ACKNOWLEDGEMENTS + and contributor["login"] not in IGNORED_AUTHOR_LOGINS + and f"@{contributor['login'].lower()}" not in ACKNOWLEDGEMENTS ) ) - breakpoint() unacknowledged_users = await asyncio.gather( - (client.get(contributor["url"]).json() for contributor in contributors) + *(client.get(contributor["url"], auth=auth) for contributor in contributors) ) + unacknowledged_users = [request.json() for request in unacknowledged_users] if not unacknowledged_users: sys.exit() From 13da2c6730a6a6a16f9762d393c2ea2f9ed42132 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 22 Jul 2020 22:45:27 -0700 Subject: [PATCH 0837/1439] Add missing contributors --- docs/contributing/4.-acknowledgements.md | 72 +++++++++++++++++++++++- scripts/check_acknowledgments.py | 7 +-- 2 files changed, 72 insertions(+), 7 deletions(-) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index d602d9c3c..210d17e29 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -103,20 +103,86 @@ Code Contributors - @mdagaro - Maksim Kurnikov (@mkurnikov) - Daniel Hahler (@blueyed) -- Dario Navin (@Zarathustra2) - @ucodery - Aarni Koskela (@akx) - Alex Chan (@alexwlchan) - Rick Thomas (@richardlthomas) -- Adam Hitchcock (@NorthIsUp) -- Danny Weinberg (@FuegoFro) - Jeppe Fihl-Pearson (@Tenzer) - Jonas Lundberg (@lundberg) - Neil (@NeilGirdhar) - @dmanikowski-reef - Stephen Brown II (@StephenBrown2) - Ankur Dedania (@AnkurDedania) +- Anthony Sottile (@asottile) +- Bendik Samseth (@bsamseth) +- Dan W Anderson (@anderson-dan-w) +- DeepSource Bot (@deepsourcebot) +- Mitar (@mitar) +- Omer Katz (@thedrow) +- Santiago Castro (@bryant1410) +- Sergey Fursov (@GeyseR) +- Thomas Robitaille (@astrofrog) +- Ville Skyttä (@scop) +- Hakan Çelik (@hakancelik96) +- Dylan Katz (@Plazmaz) +- Linus Lewandowski (@LEW21) +- Bastien Gérard (@bagerard) +- Brian Dombrowski (@bdombro) +- Ed Morley (@edmorley) +- Graeme Coupar (@obmarg) +- Jerome Leclanche (@jleclanche) +- Joshu Coats (@rhwlo) +- Mansour Behabadi (@oxplot) +- Sam Lai (@slai) - Tamas Szabo (@sztamas) +- Yedidyah Bar David (@didib) +- Hidetoshi Hirokawa (@h-hirokawa) +- Aaron Chong (@acjh) +- Harai Akihiro (@harai) +- Andy Freeland (@rouge8) +- @ethifus +- Joachim Brandon LeBlanc (@demosdemon) +- Brian May (@brianmay) +- Bruno Oliveira (@nicoddemus) +- Bruno Renié (@brutasse) +- Bryce Guinta (@brycepg) +- David Chan (@dchanm) +- David Smith (@smithdc1) +- Irv Lustig (@Dr-Irv) +- Dylan Richardson (@dylrich) +- Emil Melnikov (@emilmelnikov) +- Eric Johnson (@metrizable) +- @ryabtsev +- Felix Yan (@felixonmars) +- Gil Forcada Codinachs (@gforcada) +- Ilya Konstantinov (@ikonst) +- Jace Browning (@jacebrowning) +- Jin Suk Park (@jinmel) +- Jürgen Gmach (@jugmac00) +- Maciej Gawinecki (@dzieciou) +- Minn Soe (@MinnSoe) +- Nikolaus Wittenstein (@adzenith) +- Norman J. Harman Jr. (@njharman) +- P R Gurunath (@gurunath-p) +- Patrick Hayes (@pfhayes) +- Pete Grayson (@jpgrayson) +- Philip Jenvey (@pjenvey) +- Rajiv Bakulesh Shah (@brainix) +- Reid D McKenzie (@arrdem) +- Robert DeRose (@RobertDeRose) +- Roey Darwish Dror (@r-darwish) +- Rudinei Goi Roecker (@rudineirk) +- Wagner (@wagner-certat) +- Nikita Sobolev (@sobolevn) +- Terence Honles (@terencehonles) +- The Gitter Badger (@gitter-badger) +- Tim Gates (@timgates42) +- Tim Staley (@timstaley) +- Vincent Hatakeyama (@vincent-hatakeyama) +- Yaron de Leeuw (@jarondl) +- @jwg4 +- @nicolelodeon +- Łukasz Langa (@ambv) Documenters diff --git a/scripts/check_acknowledgments.py b/scripts/check_acknowledgments.py index 20f560c18..eab364e57 100755 --- a/scripts/check_acknowledgments.py +++ b/scripts/check_acknowledgments.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import asyncio import sys +from getpass import getpass from pathlib import Path from typing import Dict @@ -33,8 +34,7 @@ def _user_info(user: Dict[str, str], verbose=False) -> str: @hug.cli() async def main(): - auth = (input("Github Username: "), - getpass()) + auth = (input("Github Username: "), getpass()) async with httpx.AsyncClient() as client: page = 0 results = [] @@ -42,8 +42,7 @@ async def main(): while not page or len(results) == PER_PAGE: page += 1 response = await client.get( - f"{GITHUB_API_CONTRIBUTORS}?per_page={PER_PAGE}&page={page}", - auth=auth + f"{GITHUB_API_CONTRIBUTORS}?per_page={PER_PAGE}&page={page}", auth=auth ) results = response.json() contributors.extend( From cb3c3f4373505687756854065f40f77e645daa11 Mon Sep 17 00:00:00 2001 From: Greg Pstrucha Date: Thu, 23 Jul 2020 10:31:35 -0700 Subject: [PATCH 0838/1439] Make sure that warnings are formatting properly --- isort/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/api.py b/isort/api.py index d135fd98c..74982b1df 100644 --- a/isort/api.py +++ b/isort/api.py @@ -356,9 +356,9 @@ def sort_file( except FileNotFoundError: pass except ExistingSyntaxErrors: - warn("{file_path} unable to sort due to existing syntax errors") + warn(f"{file_path} unable to sort due to existing syntax errors") except IntroducedSyntaxErrors: # pragma: no cover - warn("{file_path} unable to sort as isort introduces new syntax errors") + warn(f"{file_path} unable to sort as isort introduces new syntax errors") def _config( From fbe83ebbbc69d99040288526c114893d3e991b7f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 23 Jul 2020 17:23:30 -0700 Subject: [PATCH 0839/1439] Add Grzegorz Pstrucha (@Gricha) to acknowledgements --- docs/contributing/4.-acknowledgements.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index 210d17e29..fbdfe144d 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -183,6 +183,7 @@ Code Contributors - @jwg4 - @nicolelodeon - Łukasz Langa (@ambv) +- Grzegorz Pstrucha (@Gricha) Documenters From 95d17ad67be0de3bd173370162e076c5140d8c11 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 23 Jul 2020 20:39:20 -0700 Subject: [PATCH 0840/1439] Add code style badges --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 0d48c5511..331dc0afb 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ [![License](https://img.shields.io/github/license/mashape/apistatus.svg)](https://pypi.org/project/isort/) [![Join the chat at https://gitter.im/timothycrosley/isort](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/timothycrosley/isort?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Downloads](https://pepy.tech/badge/isort)](https://pepy.tech/project/isort) +[![Imports: isort](https://img.shields.io/badge/%20%E2%9F%B3imports-isort-%231674b1?style=flat-square&labelColor=ef8336)](https://timothycrosley.github.io/isort/) +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![DeepSource](https://static.deepsource.io/deepsource-badge-light-mini.svg)](https://deepsource.io/gh/timothycrosley/isort/?ref=repository-badge) _________________ From 00d57d75378b44076c31e8f0119855a743c46b0a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 23 Jul 2020 20:40:32 -0700 Subject: [PATCH 0841/1439] Move isort badge to far right side --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 331dc0afb..a38d382dc 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,9 @@ [![License](https://img.shields.io/github/license/mashape/apistatus.svg)](https://pypi.org/project/isort/) [![Join the chat at https://gitter.im/timothycrosley/isort](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/timothycrosley/isort?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Downloads](https://pepy.tech/badge/isort)](https://pepy.tech/project/isort) -[![Imports: isort](https://img.shields.io/badge/%20%E2%9F%B3imports-isort-%231674b1?style=flat-square&labelColor=ef8336)](https://timothycrosley.github.io/isort/) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![DeepSource](https://static.deepsource.io/deepsource-badge-light-mini.svg)](https://deepsource.io/gh/timothycrosley/isort/?ref=repository-badge) +[![Imports: isort](https://img.shields.io/badge/%20%E2%9F%B3imports-isort-%231674b1?style=flat-square&labelColor=ef8336)](https://timothycrosley.github.io/isort/) _________________ [Read Latest Documentation](https://timothycrosley.github.io/isort/) - [Browse GitHub Code Repository](https://github.com/timothycrosley/isort/) From e640ccdfcd22faee7bc3cf6fedabd179565d058d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 23 Jul 2020 21:05:59 -0700 Subject: [PATCH 0842/1439] Improved badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a38d382dc..fd77a4cab 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ [![Downloads](https://pepy.tech/badge/isort)](https://pepy.tech/project/isort) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![DeepSource](https://static.deepsource.io/deepsource-badge-light-mini.svg)](https://deepsource.io/gh/timothycrosley/isort/?ref=repository-badge) -[![Imports: isort](https://img.shields.io/badge/%20%E2%9F%B3imports-isort-%231674b1?style=flat-square&labelColor=ef8336)](https://timothycrosley.github.io/isort/) +[![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://timothycrosley.github.io/isort/) _________________ [Read Latest Documentation](https://timothycrosley.github.io/isort/) - [Browse GitHub Code Repository](https://github.com/timothycrosley/isort/) From 09d68a89978f1b678ea3a5c7f0c3d13367330615 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 23 Jul 2020 21:06:39 -0700 Subject: [PATCH 0843/1439] Put isort label next to black label --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fd77a4cab..8d875a606 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ [![Join the chat at https://gitter.im/timothycrosley/isort](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/timothycrosley/isort?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Downloads](https://pepy.tech/badge/isort)](https://pepy.tech/project/isort) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) -[![DeepSource](https://static.deepsource.io/deepsource-badge-light-mini.svg)](https://deepsource.io/gh/timothycrosley/isort/?ref=repository-badge) [![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://timothycrosley.github.io/isort/) +[![DeepSource](https://static.deepsource.io/deepsource-badge-light-mini.svg)](https://deepsource.io/gh/timothycrosley/isort/?ref=repository-badge) _________________ [Read Latest Documentation](https://timothycrosley.github.io/isort/) - [Browse GitHub Code Repository](https://github.com/timothycrosley/isort/) From ca585bf28f4b5acbb5d20ff6bfbf47cdfbde4b29 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 23 Jul 2020 21:29:58 -0700 Subject: [PATCH 0844/1439] Add badge reference --- README.md | 20 ++++++++++++++++++++ docs/configuration/options.md | 2 +- isort/main.py | 4 +++- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8d875a606..8c73c7140 100644 --- a/README.md +++ b/README.md @@ -563,6 +563,26 @@ setup( ) ``` +Spread the word +========== + +[![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://timothycrosley.github.io/isort/) + +Place this badge at the top your repository to let others know your project uses isort. + +For README.md: + +```markdown +[![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://timothycrosley.github.io/isort/) +``` + +Or README.rst: + +```rst +.. image:: https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336 + :target: https://timothycrosley.github.io/isort/ +``` + Security contact information ========== diff --git a/docs/configuration/options.md b/docs/configuration/options.md index 10479a7cd..4c7afc0c5 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -386,7 +386,7 @@ Balances wrapping to produce the most consistent line length possible ## Use Parentheses -Use parentheses for line continuation on length limit instead of slashes. +Use parentheses for line continuation on length limit instead of slashes. **NOTE**: This is separate from wrap modes, and only affects individual line to long wrapping, not sections of multiple imports. **Type:** Bool **Default:** `False` diff --git a/isort/main.py b/isort/main.py index a3b20a241..a9f40114a 100644 --- a/isort/main.py +++ b/isort/main.py @@ -468,7 +468,9 @@ def _build_arg_parser() -> argparse.ArgumentParser: "--use-parentheses", dest="use_parentheses", action="store_true", - help="Use parentheses for line continuation on length limit instead of slashes.", + help="Use parentheses for line continuation on length limit instead of slashes." + " **NOTE**: This is separate from wrap modes, and only affects how individual lines that " + " are too long get continued, not sections of multiple imports.", ) parser.add_argument( "-V", From 47fd55611a7be85e55aedff05e2b7354fbbb2697 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 23 Jul 2020 21:30:40 -0700 Subject: [PATCH 0845/1439] Fix small grammer error --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8c73c7140..4b7e55825 100644 --- a/README.md +++ b/README.md @@ -568,7 +568,7 @@ Spread the word [![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://timothycrosley.github.io/isort/) -Place this badge at the top your repository to let others know your project uses isort. +Place this badge at the top of your repository to let others know your project uses isort. For README.md: From 8be3090f920e523fbc234a15fdd4953bbd13aa99 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 23 Jul 2020 22:02:43 -0700 Subject: [PATCH 0846/1439] Implemented #727: Ability to only add imports if existing imports exist. --- CHANGELOG.md | 1 + docs/configuration/options.md | 14 +++++++++++++- isort/api.py | 3 ++- isort/main.py | 8 ++++++++ isort/settings.py | 1 + tests/test_ticketed_features.py | 14 ++++++++++++++ 6 files changed, 39 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e27ce571..67161f207 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. - Implemented #1331: Warn when sections don't match up. - Implemented #1261: By popular demand, `filter_files` can now be set in the config option. - Implemented #960: Support for respecting git ignore via "--gitignore" or "skip_gitignore=True". + - Implemented #727: Ability to only add imports if existing imports exist. - `# isort: split` can now be used at the end of an import line. - Fixed #1339: Extra indent is not preserved when isort:skip is used in nested imports. diff --git a/docs/configuration/options.md b/docs/configuration/options.md index 4c7afc0c5..46c5c01c6 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -315,6 +315,18 @@ Removes the specified import from all files. - --rm - --remove-import +## Append Only + +Only adds the imports specified in --add-imports if the file contains existing imports. + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** append_only +**CLI Flags:** + +- --append +- --append-only + ## Reverse Relative Reverse order of relative imports. @@ -386,7 +398,7 @@ Balances wrapping to produce the most consistent line length possible ## Use Parentheses -Use parentheses for line continuation on length limit instead of slashes. **NOTE**: This is separate from wrap modes, and only affects individual line to long wrapping, not sections of multiple imports. +Use parentheses for line continuation on length limit instead of slashes. **NOTE**: This is separate from wrap modes, and only affects how individual lines that are too long get continued, not sections of multiple imports. **Type:** Bool **Default:** `False` diff --git a/isort/api.py b/isort/api.py index 74982b1df..edebf529d 100644 --- a/isort/api.py +++ b/isort/api.py @@ -522,7 +522,7 @@ def _sort_imports( import_section += line indent = line[: -len(line.lstrip())] elif not (stripped_line or contains_imports): - if add_imports and not indent: + if add_imports and not indent and not config.append_only: if not import_section: output_stream.write(line) line = "" @@ -586,6 +586,7 @@ def _sort_imports( raw_import_section: str = import_section if ( add_imports + and not config.append_only and not in_top_comment and not in_quote and not import_section diff --git a/isort/main.py b/isort/main.py index a9f40114a..362b62bb3 100644 --- a/isort/main.py +++ b/isort/main.py @@ -154,6 +154,14 @@ def _build_arg_parser() -> argparse.ArgumentParser: help="Adds the specified import line to all files, " "automatically determining correct placement.", ) + parser.add_argument( + "--append", + "--append-only", + dest="append_only", + action="store_true", + help="Only adds the imports specified in --add-imports if the file" + " contains existing imports.", + ) parser.add_argument( "--ac", "--atomic", diff --git a/isort/settings.py b/isort/settings.py index 9370904b3..cb69d1ec6 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -130,6 +130,7 @@ class _Config: length_sort_sections: FrozenSet[str] = frozenset() add_imports: FrozenSet[str] = frozenset() remove_imports: FrozenSet[str] = frozenset() + append_only: bool = False reverse_relative: bool = False force_single_line: bool = False single_line_exclusions: Tuple[str, ...] = () diff --git a/tests/test_ticketed_features.py b/tests/test_ticketed_features.py index ae9fec59d..e57b32d91 100644 --- a/tests/test_ticketed_features.py +++ b/tests/test_ticketed_features.py @@ -198,3 +198,17 @@ def test_isort_warns_when_known_sections_dont_match_issue_1331(): ) == "THIRDPARTY" ) + + +def test_isort_supports_append_only_imports_727(): + """Test to ensure isort provides a way to only add imports as an append. + See: https://github.com/timothycrosley/isort/issues/727. + """ + assert isort.code("", add_imports=["from __future__ import absolute_imports"]) == "" + assert ( + isort.code("import os", add_imports=["from __future__ import absolute_imports"]) + == """from __future__ import absolute_imports + +import os +""" + ) From 5f3c16683b7c4c7d8104eeedd7ca15f2e3e1fdc7 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 23 Jul 2020 22:21:57 -0700 Subject: [PATCH 0847/1439] Update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67161f207..292a8f2d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -103,7 +103,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. - isort now does nothing, beyond giving instructions and exiting status code 0, when ran with no arguments. - a new `--interactive` flag has been added to enable the old style behaviour. - isort now works on contiguous sections of imports, instead of one whole file at a time. - - isort now formats all nested "as" imports in the "from" form. `import x.y as a` becomes `from x import y as a`. + - ~~isort now formats all nested "as" imports in the "from" form. `import x.y as a` becomes `from x import y as a`.~~ NOTE: This was undone in version 5.1.0 due to feedback it caused issues with some project conventions. - `keep_direct_and_as_imports` option now defaults to `True`. - `appdirs` is no longer supported. Unless manually specified, config should be project config only. - `toml` is now installed as a vendorized module, meaning pyproject.toml based config is always supported. From c64e4d3d39c13c5eefb29643bffe310075967a81 Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Sat, 25 Jul 2020 07:06:34 +0300 Subject: [PATCH 0848/1439] Adds new wrap mode 10. Adds a new wrap mode Hanging Indent, With Parentheses (10). Fixes #941. --- isort/wrap_modes.py | 87 ++++++++++++++++------------------------ tests/test_isort.py | 30 +++++++++++++- tests/test_wrap_modes.py | 1 + 3 files changed, 64 insertions(+), 54 deletions(-) diff --git a/isort/wrap_modes.py b/isort/wrap_modes.py index 17e8fc743..92a63c3f7 100644 --- a/isort/wrap_modes.py +++ b/isort/wrap_modes.py @@ -106,18 +106,28 @@ def vertical(**interface): return f"{interface['statement']}({first_import}{_imports}{_comma_maybe})" -@_wrap_mode -def hanging_indent(**interface): +def _hanging_indent_common(use_parentheses=False, **interface): if not interface["imports"]: return "" + line_length_limit = interface["line_length"] - (1 if use_parentheses else 3) + + def end_line(line): + if use_parentheses: + return line + if not line.endswith(" "): + line += " " + return line + "\\" + + if use_parentheses: + interface["statement"] += "(" next_import = interface["imports"].pop(0) next_statement = interface["statement"] + next_import # Check for first import - if len(next_statement) + 3 > interface["line_length"]: + if len(next_statement) > line_length_limit: next_statement = ( isort.comments.add_to_line( interface["comments"], - f"{interface['statement']}\\", + end_line(interface["statement"]), removed=interface["remove_comments"], comment_prefix=interface["comment_prefix"], ) @@ -133,14 +143,12 @@ def hanging_indent(**interface): removed=interface["remove_comments"], comment_prefix=interface["comment_prefix"], ) - if ( - len(next_statement.split(interface["line_separator"])[-1]) + 3 - > interface["line_length"] - ): + current_line = next_statement.split(interface["line_separator"])[-1] + if len(current_line) > line_length_limit: next_statement = ( isort.comments.add_to_line( interface["comments"], - f"{interface['statement']}, \\", + end_line(interface["statement"] + ","), removed=interface["remove_comments"], comment_prefix=interface["comment_prefix"], ) @@ -148,7 +156,14 @@ def hanging_indent(**interface): ) interface["comments"] = [] interface["statement"] = next_statement - return interface["statement"] + _comma_maybe = "," if interface["include_trailing_comma"] else "" + _close_parentheses_maybe = ")" if use_parentheses else "" + return interface["statement"] + _comma_maybe + _close_parentheses_maybe + + +@_wrap_mode +def hanging_indent(**interface): + return _hanging_indent_common(use_parentheses=False, **interface) @_wrap_mode @@ -167,7 +182,7 @@ def vertical_hanging_indent(**interface): ) -def vertical_grid_common(need_trailing_char: bool, **interface): +def _vertical_grid_common(need_trailing_char: bool, **interface): if not interface["imports"]: return "" @@ -203,40 +218,13 @@ def vertical_grid_common(need_trailing_char: bool, **interface): @_wrap_mode def vertical_grid(**interface) -> str: - return ( - vertical_grid_common( - statement=interface["statement"], - imports=interface["imports"], - white_space=interface["white_space"], - indent=interface["indent"], - line_length=interface["line_length"], - comments=interface["comments"], - line_separator=interface["line_separator"], - comment_prefix=interface["comment_prefix"], - include_trailing_comma=interface["include_trailing_comma"], - remove_comments=interface["remove_comments"], - need_trailing_char=True, - ) - + ")" - ) + return _vertical_grid_common(need_trailing_char=True, **interface) + ")" @_wrap_mode def vertical_grid_grouped(**interface): return ( - vertical_grid_common( - statement=interface["statement"], - imports=interface["imports"], - white_space=interface["white_space"], - indent=interface["indent"], - line_length=interface["line_length"], - comments=interface["comments"], - line_separator=interface["line_separator"], - comment_prefix=interface["comment_prefix"], - include_trailing_comma=interface["include_trailing_comma"], - remove_comments=interface["remove_comments"], - need_trailing_char=True, - ) + _vertical_grid_common(need_trailing_char=True, **interface) + interface["line_separator"] + ")" ) @@ -245,19 +233,7 @@ def vertical_grid_grouped(**interface): @_wrap_mode def vertical_grid_grouped_no_comma(**interface): return ( - vertical_grid_common( - statement=interface["statement"], - imports=interface["imports"], - white_space=interface["white_space"], - indent=interface["indent"], - line_length=interface["line_length"], - comments=interface["comments"], - line_separator=interface["line_separator"], - comment_prefix=interface["comment_prefix"], - include_trailing_comma=interface["include_trailing_comma"], - remove_comments=interface["remove_comments"], - need_trailing_char=False, - ) + _vertical_grid_common(need_trailing_char=False, **interface) + interface["line_separator"] + ")" ) @@ -325,6 +301,11 @@ def vertical_prefix_from_module_import(**interface): return interface["statement"] +@_wrap_mode +def hanging_indent_with_parentheses(**interface): + return _hanging_indent_common(use_parentheses=True, **interface) + + WrapModes = enum.Enum( # type: ignore "WrapModes", {wrap_mode: index for index, wrap_mode in enumerate(_wrap_modes.keys())} ) diff --git a/tests/test_isort.py b/tests/test_isort.py index 9d195583e..12a9fc902 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -525,6 +525,34 @@ def test_output_modes() -> None: "from third_party import lib21, lib22\n" ) + test_output_hanging_indent_with_parentheses = isort.code( + code=REALLY_LONG_IMPORT, + multi_line_output=WrapModes.HANGING_INDENT_WITH_PARENTHESES, + line_length=40, + indent=" ", + ) + assert test_output_hanging_indent_with_parentheses == ( + "from third_party import (lib1, lib2,\n" + " lib3, lib4, lib5, lib6, lib7, lib8,\n" + " lib9, lib10, lib11, lib12, lib13,\n" + " lib14, lib15, lib16, lib17, lib18,\n" + " lib20, lib21, lib22)\n" + ) + + comment_output_hanging_indent_with_parentheses = isort.code( + code=REALLY_LONG_IMPORT_WITH_COMMENT, + multi_line_output=WrapModes.HANGING_INDENT_WITH_PARENTHESES, + line_length=40, + indent=" ", + ) + assert comment_output_hanging_indent_with_parentheses == ( + "from third_party import (lib1, # comment\n" + " lib2, lib3, lib4, lib5, lib6, lib7,\n" + " lib8, lib9, lib10, lib11, lib12,\n" + " lib13, lib14, lib15, lib16, lib17,\n" + " lib18, lib20, lib21, lib22)\n" + ) + test_input = ( "def a():\n" " from allennlp.modules.text_field_embedders.basic_text_field_embedder" @@ -2518,7 +2546,7 @@ def test_long_single_line() -> None: code="from ..views import (" " _a," "_xxxxxx_xxxxxxx_xxxxxxxx_xxx_xxxxxxx as xxxxxx_xxxxxxx_xxxxxxxx_xxx_xxxxxxx)", - line_length=76, + line_length=79, combine_as_imports=True, ) for line in output.split("\n"): diff --git a/tests/test_wrap_modes.py b/tests/test_wrap_modes.py index bf0d698e8..d27f92e77 100644 --- a/tests/test_wrap_modes.py +++ b/tests/test_wrap_modes.py @@ -20,6 +20,7 @@ auto_allow_exceptions_=(ValueError,), imports=["one", "two"], ) +auto_pytest_magic(wrap_modes.hanging_indent_with_parentheses, auto_allow_exceptions_=(ValueError,)) def test_wrap_mode_interface(): From 2d76984f1345763f31aff8c1fc940000700cd46e Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 24 Jul 2020 21:08:00 -0700 Subject: [PATCH 0849/1439] Implemented #970: Support for custom sharable isort profiles. --- CHANGELOG.md | 1 + isort/settings.py | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 292a8f2d8..38a3bdf36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. - Implemented #1261: By popular demand, `filter_files` can now be set in the config option. - Implemented #960: Support for respecting git ignore via "--gitignore" or "skip_gitignore=True". - Implemented #727: Ability to only add imports if existing imports exist. + - Implemented #970: Support for custom sharable isort profiles. - `# isort: split` can now be used at the end of an import line. - Fixed #1339: Extra indent is not preserved when isort:skip is used in nested imports. diff --git a/isort/settings.py b/isort/settings.py index cb69d1ec6..09255c819 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -261,6 +261,12 @@ def __init__( profile_name = config_overrides.get("profile", config_settings.get("profile", "")) profile: Dict[str, Any] = {} if profile_name: + if profile_name not in profiles: + import pkg_resources + + for plugin in pkg_resources.iter_entry_points("isort.profiles"): + profiles.setdefault(plugin.name, plugin.load().PROFILE) # pragma: no cover + if profile_name not in profiles: raise ProfileDoesNotExist(profile_name) From 045c0842901d7a1ad49ad18169f075aeffbbe74b Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Sat, 25 Jul 2020 15:47:04 +0300 Subject: [PATCH 0850/1439] Adds lazy option to git hook. Fixes #1214 --- README.md | 7 ++++++- isort/hooks.py | 8 +++++++- tests/test_hooks.py | 22 +++++++++++++++++++--- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 4b7e55825..0ce08a04d 100644 --- a/README.md +++ b/README.md @@ -526,13 +526,18 @@ include the following in `.git/hooks/pre-commit`: import sys from isort.hooks import git_hook -sys.exit(git_hook(strict=True, modify=True)) +sys.exit(git_hook(strict=True, modify=True, lazy=True)) ``` If you just want to display warnings, but allow the commit to happen anyway, call `git_hook` without the strict parameter. If you want to display warnings, but not also fix the code, call `git_hook` without the modify parameter. +The `lazy` argument is to support users who are "lazy" to add files +individually to the index and tend to use `git commit -a` instead. +Set it to `True` to ensure all tracked files are properly isorted, +leave it out or set it to `False` to check only files added to your +index. Setuptools integration ---------------------- diff --git a/isort/hooks.py b/isort/hooks.py index 892bd312c..3198a1d09 100644 --- a/isort/hooks.py +++ b/isort/hooks.py @@ -33,7 +33,7 @@ def get_lines(command: List[str]) -> List[str]: return [line.strip() for line in stdout.splitlines()] -def git_hook(strict: bool = False, modify: bool = False) -> int: +def git_hook(strict: bool = False, modify: bool = False, lazy: bool = False) -> int: """ Git pre-commit hook to check staged files for isort errors @@ -43,12 +43,18 @@ def git_hook(strict: bool = False, modify: bool = False) -> int: :param bool modify - if True, fix the sources if they are not sorted properly. If False, only report result without modifying anything. + :param bool lazy - if True, also check/fix unstaged files. + This is useful if you frequently use ``git commit -a`` for example. + If False, ony check/fix the staged files for isort errors. :return number of errors if in strict mode, 0 otherwise. """ # Get list of files modified and staged diff_cmd = ["git", "diff-index", "--cached", "--name-only", "--diff-filter=ACMRTUXB", "HEAD"] + if lazy: + diff_cmd.remove("--cached") + files_modified = get_lines(diff_cmd) if not files_modified: return 0 diff --git a/tests/test_hooks.py b/tests/test_hooks.py index c84d6d5c9..677b0d209 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -11,9 +11,25 @@ def test_git_hook(src_dir): # Ensure correct subprocess command is called with patch("subprocess.run", MagicMock()) as run_mock: hooks.git_hook() - assert run_mock.called_with( - ["git", "diff-index", "--cached", "--name-only", "--diff-filter=ACMRTUXB HEAD"] - ) + assert run_mock.called_once() + assert run_mock.call_args[0][0] == [ + "git", + "diff-index", + "--cached", + "--name-only", + "--diff-filter=ACMRTUXB", + "HEAD", + ] + + hooks.git_hook(lazy=True) + assert run_mock.called_once() + assert run_mock.call_args[0][0] == [ + "git", + "diff-index", + "--name-only", + "--diff-filter=ACMRTUXB", + "HEAD", + ] # Test with incorrectly sorted file returned from git with patch( From 71565fe9bc5c2e8ad2285a42d443035d3794cd14 Mon Sep 17 00:00:00 2001 From: Abtin Date: Sat, 25 Jul 2020 22:32:57 +0430 Subject: [PATCH 0851/1439] Update profiles.md fix typo --- docs/configuration/profiles.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration/profiles.md b/docs/configuration/profiles.md index 36dcaf720..3b0e4cc47 100644 --- a/docs/configuration/profiles.md +++ b/docs/configuration/profiles.md @@ -51,7 +51,7 @@ To use any of the listed profiles, use `isort --profile PROFILE_NAME` from the c - **force_alphabetical_sort**: `True` - **force_single_line**: `True` - - **ines_after_imports**: `2` + - **lines_after_imports**: `2` - **line_length**: `200` #attrs From 0fa03642eb0e2fe40ce65ecd850c689d85eb66ad Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 25 Jul 2020 13:14:38 -0700 Subject: [PATCH 0852/1439] Update changelog with fix for #1214 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38a3bdf36..1a0a1afdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. - Implemented #960: Support for respecting git ignore via "--gitignore" or "skip_gitignore=True". - Implemented #727: Ability to only add imports if existing imports exist. - Implemented #970: Support for custom sharable isort profiles. + - Implemented #1214: Added support for git_hook lazy option (Thanks @sztamas!) - `# isort: split` can now be used at the end of an import line. - Fixed #1339: Extra indent is not preserved when isort:skip is used in nested imports. From aca099127376c5b7b20c56d5b5cbfd3cba53eee0 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 25 Jul 2020 13:47:56 -0700 Subject: [PATCH 0853/1439] Resolve #1347: Improve description and documentation for force_sort_within_sections --- README.md | 52 ++++++++++++++++++++--------------- docs/configuration/options.md | 2 +- isort/main.py | 4 ++- 3 files changed, 34 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 0ce08a04d..1484f0dd5 100644 --- a/README.md +++ b/README.md @@ -407,6 +407,31 @@ as a configuration item, e.g.: length_sort_stdlib=1 +Controlling how isort sections `from` imports +========================= + +By default isort places straight (`import y`) imports above from imports (`from x import y`): + +```python +import b +from a import a # This will always appear below because it is a from import. +``` + +However, if you prefer to keep strict alphabetical sorting you can set [force sort within sections](https://timothycrosley.github.io/isort/docs/configuration/options/#force-sort-within-sections) to true. Resulting in: + + +```python +from a import a # This will now appear at top because a appears in the alphabet before b +import b +``` + +You can even tell isort to always place from imports on top, instead of the default of placing them on bottom, using [from first](https://timothycrosley.github.io/isort/docs/configuration/options/#from-first). + +```python +from b import b # If from first is set to True, all from imports will be placed before non-from imports. +import a +``` + Skip processing of imports (outside of configuration) ===================================================== @@ -445,23 +470,18 @@ Adding an import to multiple files isort makes it easy to add an import statement across multiple files, while being assured it's correctly placed. -From the command line: +To add an import to all files: ```bash isort -a "from __future__ import print_function" *.py ``` -from within Kate: +To add an import only to files that already have imports: -``` -ctrl+] +```bash +isort -a "from __future__ import print_function" --append-only *.py ``` -or: - -``` -menu > Python > Add Import -``` Removing an import from multiple files ====================================== @@ -472,19 +492,7 @@ without having to be concerned with how it was originally formatted. From the command line: ```bash -isort -rm "os.system" *.py -``` - -from within Kate: - -``` -ctrl+shift+] -``` - -or: - -``` -menu > Python > Remove Import +isort --rm "os.system" *.py ``` Using isort to verify code diff --git a/docs/configuration/options.md b/docs/configuration/options.md index 46c5c01c6..d70602bb3 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -590,7 +590,7 @@ Force number of from imports (defaults to 2) to be grid wrapped regardless of li ## Force Sort Within Sections -Force imports to be sorted by module, independent of import_type +Don't sort straight-style imports (like import sys) before from-style imports (like from itertools import groupby). Instead, sort the imports by module, independent of import style. **Type:** Bool **Default:** `False` diff --git a/isort/main.py b/isort/main.py index 362b62bb3..3ccf0d14d 100644 --- a/isort/main.py +++ b/isort/main.py @@ -285,7 +285,9 @@ def _build_arg_parser() -> argparse.ArgumentParser: "--force-sort-within-sections", action="store_true", dest="force_sort_within_sections", - help="Force imports to be sorted by module, independent of import_type", + help="Don't sort straight-style imports (like import sys) before from-style imports " + "(like from itertools import groupby). Instead, sort the imports by module, " + "independent of import style.", ) parser.add_argument( "-i", From 33917280a357630e4caf09bc0aaca9c0330ad51f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 25 Jul 2020 13:51:11 -0700 Subject: [PATCH 0854/1439] Fix readme to show side sections as clickable links --- README.md | 51 +++++++++++++++++---------------------------------- 1 file changed, 17 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 1484f0dd5..a5ba62c60 100644 --- a/README.md +++ b/README.md @@ -73,8 +73,7 @@ print("Hey") print("yo") ``` -Installing isort -================ +## Installing isort Installing isort is as simple as: @@ -100,8 +99,7 @@ Install isort with both formats support: pip install isort[requirements_deprecated_finder,pipfile_deprecated_finder] ``` -Using isort -=========== +## Using isort **From the command line**: @@ -153,8 +151,7 @@ import isort sorted_code = isort.code("import b\nimport a\n") ``` -Installing isort's for your preferred text editor -================================================== +## Installing isort's for your preferred text editor Several plugins have been written that enable to use isort from within a variety of text-editors. You can find a full list of them [on the isort @@ -163,8 +160,7 @@ Additionally, I will enthusiastically accept pull requests that include plugins for other text editors and add documentation for them as I am notified. -Multi line output modes -======================= +## Multi line output modes You will notice above the \"multi\_line\_output\" setting. This setting defines how from imports wrap when they extend past the line\_length @@ -275,8 +271,7 @@ For the import styles that use parentheses, you can control whether or not to include a trailing comma after the last import with the `include_trailing_comma` option (defaults to `False`). -Intelligently Balanced Multi-line Imports -========================================= +## Intelligently Balanced Multi-line Imports As of isort 3.1.0 support for balanced multi-line imports has been added. With this enabled isort will dynamically change the import length @@ -300,8 +295,7 @@ from __future__ import (absolute_import, division, print_function, To enable this set `balanced_wrapping` to `True` in your config or pass the `-e` option into the command line utility. -Custom Sections and Ordering -============================ +## Custom Sections and Ordering You can change the section order with `sections` option from the default of: @@ -354,8 +348,7 @@ following mapping: This will likely be changed in isort 6.0.0+ in a backwards compatible way. -Auto-comment import sections -============================ +## Auto-comment import sections Some projects prefer to have import sections uniquely titled to aid in identifying the sections quickly when visually scanning. isort can @@ -383,8 +376,7 @@ import django.settings import myproject.test ``` -Ordering by import length -========================= +## Ordering by import length isort also makes it easy to sort your imports by length, simply by setting the `length_sort` option to `True`. This will result in the @@ -407,8 +399,7 @@ as a configuration item, e.g.: length_sort_stdlib=1 -Controlling how isort sections `from` imports -========================= +## Controlling how isort sections `from` imports By default isort places straight (`import y`) imports above from imports (`from x import y`): @@ -432,8 +423,7 @@ from b import b # If from first is set to True, all from imports will be placed import a ``` -Skip processing of imports (outside of configuration) -===================================================== +## Skip processing of imports (outside of configuration) To make isort ignore a single import simply add a comment at the end of the import line containing the text `isort:skip`: @@ -464,8 +454,7 @@ import b import a ``` -Adding an import to multiple files -================================== +## Adding an import to multiple files isort makes it easy to add an import statement across multiple files, while being assured it's correctly placed. @@ -483,8 +472,7 @@ isort -a "from __future__ import print_function" --append-only *.py ``` -Removing an import from multiple files -====================================== +## Removing an import from multiple files isort also makes it easy to remove an import from multiple files, without having to be concerned with how it was originally formatted. @@ -495,8 +483,7 @@ From the command line: isort --rm "os.system" *.py ``` -Using isort to verify code -========================== +## Using isort to verify code The `--check-only` option ------------------------- @@ -547,8 +534,7 @@ Set it to `True` to ensure all tracked files are properly isorted, leave it out or set it to `False` to check only files added to your index. -Setuptools integration ----------------------- +## Setuptools integration Upon installation, isort enables a `setuptools` command that checks Python files declared by your project. @@ -576,8 +562,7 @@ setup( ) ``` -Spread the word -========== +## Spread the word [![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://timothycrosley.github.io/isort/) @@ -596,15 +581,13 @@ Or README.rst: :target: https://timothycrosley.github.io/isort/ ``` -Security contact information -========== +## Security contact information To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. -Why isort? -========== +## Why isort? isort simply stands for import sort. It was originally called "sortImports" however I got tired of typing the extra characters and From 3972ecc8536677b3499163b3ba1ab7bdc2de9e45 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 25 Jul 2020 13:57:55 -0700 Subject: [PATCH 0855/1439] Update changelog with fix for 941 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a0a1afdd..0e1096e81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. - Implemented #727: Ability to only add imports if existing imports exist. - Implemented #970: Support for custom sharable isort profiles. - Implemented #1214: Added support for git_hook lazy option (Thanks @sztamas!) + - Implemented #941: Added an additional `multi_line_output` mode for more compact formatting (Thanks @sztamas!) - `# isort: split` can now be used at the end of an import line. - Fixed #1339: Extra indent is not preserved when isort:skip is used in nested imports. From 97e96f469e339889ea935e13688b4a12b7a4ace8 Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Sun, 26 Jul 2020 08:59:51 +0300 Subject: [PATCH 0856/1439] Adds color to ERROR and SUCCESS in check mode. Fixes #1177. --- isort/api.py | 11 +++++++++-- isort/format.py | 16 +++++++++++++++- poetry.lock | 3 ++- pyproject.toml | 1 + tests/test_format.py | 15 +++++++++++++++ tests/test_isort.py | 3 ++- 6 files changed, 44 insertions(+), 5 deletions(-) diff --git a/isort/api.py b/isort/api.py index edebf529d..55f881bde 100644 --- a/isort/api.py +++ b/isort/api.py @@ -7,6 +7,8 @@ from typing import List, Optional, TextIO, Union, cast from warnings import warn +import colorama + from . import io, output, parse from .exceptions import ( ExistingSyntaxErrors, @@ -19,6 +21,8 @@ format_natural, remove_whitespace, show_unified_diff, + style_error, + style_success, ) from .io import Empty from .place import module as place_module # skipcq: PYL-W0611 (intended export of public API) @@ -219,10 +223,13 @@ def check_stream( if not changed: if config.verbose: - print(f"SUCCESS: {file_path or ''} Everything Looks Good!") + print(f"{style_success('SUCCESS')}: {file_path or ''} Everything Looks Good!") return True else: - print(f"ERROR: {file_path or ''} Imports are incorrectly sorted and/or formatted.") + print( + f"{style_error('ERROR')}: " + f"{file_path or ''} Imports are incorrectly sorted and/or formatted." + ) if show_diff: output_stream = StringIO() input_stream.seek(0) diff --git a/isort/format.py b/isort/format.py index 15ad6233a..896dab7b8 100644 --- a/isort/format.py +++ b/isort/format.py @@ -1,8 +1,13 @@ import sys from datetime import datetime from difflib import unified_diff +from functools import partial from pathlib import Path -from typing import Optional, TextIO +from typing import Iterable, Optional, TextIO + +import colorama + +colorama.init() def format_simplified(import_line: str) -> str: @@ -71,3 +76,12 @@ def ask_whether_to_apply_changes_to_file(file_path: str) -> bool: def remove_whitespace(content: str, line_separator: str = "\n") -> str: content = content.replace(line_separator, "").replace(" ", "").replace("\x0c", "") return content + + +def style_text(text: str, styles: Iterable[str]) -> str: + escape_sequences = "".join(styles) + return escape_sequences + text + colorama.Style.RESET_ALL + + +style_error = partial(style_text, styles=(colorama.Fore.RED,)) +style_success = partial(style_text, styles=(colorama.Fore.GREEN,)) diff --git a/poetry.lock b/poetry.lock index b1bdaa498..bb75100cf 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1532,7 +1532,7 @@ pipfile_deprecated_finder = ["pipreqs", "tomlkit", "requirementslib"] requirements_deprecated_finder = ["pipreqs", "pip-api"] [metadata] -content-hash = "ace34d56082ee3f0ccfa77f6c8b6a6e8a6449a1e51a0e31a69368c697af7384b" +content-hash = "8c1bc470e3767a45aeb8bb88ef814f2d7ed3bde07a0d9347cd74d51aa9d7cc4b" python-versions = "^3.6" [metadata.files] @@ -2084,6 +2084,7 @@ regex = [ {file = "regex-2020.6.8.tar.gz", hash = "sha256:e9b64e609d37438f7d6e68c2546d2cb8062f3adb27e6336bc129b51be20773ac"}, ] requests = [ + {file = "requests-2.23.0-py2.7.egg", hash = "sha256:5d2d0ffbb515f39417009a46c14256291061ac01ba8f875b90cad137de83beb4"}, {file = "requests-2.23.0-py2.py3-none-any.whl", hash = "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee"}, {file = "requests-2.23.0.tar.gz", hash = "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"}, ] diff --git a/pyproject.toml b/pyproject.toml index 464e8f059..7d15bee1d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,7 @@ pipreqs = {version = "*", optional = true} requirementslib = {version = "*", optional = true} tomlkit = {version = ">=0.5.3", optional = true} pip-api = {version = "*", optional = true} +colorama = "^0.4.3" [tool.poetry.extras] pipfile_deprecated_finder = ["pipreqs", "tomlkit", "requirementslib", "pip-shims<=0.3.4"] diff --git a/tests/test_format.py b/tests/test_format.py index 79b967019..819f72f2f 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -1,5 +1,6 @@ from unittest.mock import MagicMock, patch +import colorama import pytest from hypothesis_auto import auto_pytest_magic @@ -16,3 +17,17 @@ def test_ask_whether_to_apply_changes_to_file(): with patch("isort.format.input", MagicMock(return_value="q")): with pytest.raises(SystemExit): assert isort.format.ask_whether_to_apply_changes_to_file("") + + +def test_style_error(): + message = "ERROR" + styled = isort.format.style_error(message) + assert message in styled + assert styled == colorama.Fore.RED + message + colorama.Style.RESET_ALL + + +def test_style_success(): + message = "OK" + styled = isort.format.style_success(message) + assert message in styled + assert styled == colorama.Fore.GREEN + message + colorama.Style.RESET_ALL diff --git a/tests/test_isort.py b/tests/test_isort.py index 9d195583e..ff5887fe0 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -2656,7 +2656,8 @@ def test_strict_whitespace_by_default(capsys) -> None: test_input = "import os\nfrom django.conf import settings\n" assert not api.check_code_string(test_input) out, _ = capsys.readouterr() - assert out == "ERROR: Imports are incorrectly sorted and/or formatted.\n" + assert "ERROR" in out + assert out.endswith("Imports are incorrectly sorted and/or formatted.\n") def test_strict_whitespace_no_closing_newline_issue_676(capsys) -> None: From 83bf671fe613c655642d199381f1a66cfe985603 Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Sun, 26 Jul 2020 09:22:02 +0300 Subject: [PATCH 0857/1439] Remove unused import. --- isort/api.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/isort/api.py b/isort/api.py index 55f881bde..6f45ca3f8 100644 --- a/isort/api.py +++ b/isort/api.py @@ -7,8 +7,6 @@ from typing import List, Optional, TextIO, Union, cast from warnings import warn -import colorama - from . import io, output, parse from .exceptions import ( ExistingSyntaxErrors, From 57eb70a4c70311f6b1748062853ded38f7fec366 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 26 Jul 2020 01:26:11 -0700 Subject: [PATCH 0858/1439] Add test for an example for shared profiles support (issue #970) --- .../example_shared_isort_profile.py | 7 +++++++ example_shared_isort_profile/poetry.lock | 7 +++++++ example_shared_isort_profile/pyproject.toml | 18 ++++++++++++++++++ isort/main.py | 4 ++-- isort/settings.py | 2 +- poetry.lock | 14 +++++++++++++- pyproject.toml | 1 + tests/test_ticketed_features.py | 14 ++++++++++++-- 8 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 example_shared_isort_profile/example_shared_isort_profile.py create mode 100644 example_shared_isort_profile/poetry.lock create mode 100644 example_shared_isort_profile/pyproject.toml diff --git a/example_shared_isort_profile/example_shared_isort_profile.py b/example_shared_isort_profile/example_shared_isort_profile.py new file mode 100644 index 000000000..986ddd5de --- /dev/null +++ b/example_shared_isort_profile/example_shared_isort_profile.py @@ -0,0 +1,7 @@ +PROFILE = { + "multi_line_output": 3, + "include_trailing_comma": True, + "force_grid_wrap": 0, + "use_parentheses": True, + "line_length": 100, +} diff --git a/example_shared_isort_profile/poetry.lock b/example_shared_isort_profile/poetry.lock new file mode 100644 index 000000000..12fbad913 --- /dev/null +++ b/example_shared_isort_profile/poetry.lock @@ -0,0 +1,7 @@ +package = [] + +[metadata] +content-hash = "8165d934e932435bf4742b9198674202413b43524911713d5c7c55cb8d314618" +python-versions = "^3.5" + +[metadata.files] diff --git a/example_shared_isort_profile/pyproject.toml b/example_shared_isort_profile/pyproject.toml new file mode 100644 index 000000000..7a290ddc0 --- /dev/null +++ b/example_shared_isort_profile/pyproject.toml @@ -0,0 +1,18 @@ +[tool.poetry] +name = "example_shared_isort_profile" +version = "0.0.1" +description = "An example shared isort profile" +authors = ["Timothy Crosley "] +license = "MIT" + +[tool.poetry.plugins."isort.profiles"] +example = "example_shared_isort_profile:PROFILE" + +[tool.poetry.dependencies] +python = "^3.6" + +[tool.poetry.dev-dependencies] + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" diff --git a/isort/main.py b/isort/main.py index 3ccf0d14d..c90b824ad 100644 --- a/isort/main.py +++ b/isort/main.py @@ -569,9 +569,9 @@ def _build_arg_parser() -> argparse.ArgumentParser: parser.add_argument( "--profile", dest="profile", - choices=list(profiles.keys()), type=str, - help="Base profile type to use for configuration.", + help="Base profile type to use for configuration. " + f"Profiles include: {', '.join(profiles.keys())}. As well as any shared profiles.", ) parser.add_argument( "--interactive", diff --git a/isort/settings.py b/isort/settings.py index 09255c819..b6eebb237 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -265,7 +265,7 @@ def __init__( import pkg_resources for plugin in pkg_resources.iter_entry_points("isort.profiles"): - profiles.setdefault(plugin.name, plugin.load().PROFILE) # pragma: no cover + profiles.setdefault(plugin.name, plugin.load()) if profile_name not in profiles: raise ProfileDoesNotExist(profile_name) diff --git a/poetry.lock b/poetry.lock index b1bdaa498..1e81bd69d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -259,6 +259,14 @@ toml = "*" [package.extras] pipenv = ["pipenv"] +[[package]] +category = "dev" +description = "An example shared isort profile" +name = "example-shared-isort-profile" +optional = false +python-versions = ">=3.6,<4.0" +version = "0.0.1" + [[package]] category = "dev" description = "Tests and Documentation Done by Example." @@ -1532,7 +1540,7 @@ pipfile_deprecated_finder = ["pipreqs", "tomlkit", "requirementslib"] requirements_deprecated_finder = ["pipreqs", "pip-api"] [metadata] -content-hash = "ace34d56082ee3f0ccfa77f6c8b6a6e8a6449a1e51a0e31a69368c697af7384b" +content-hash = "4db91d831e91eff8a42ff63d3ba2683d3575886c5d23c09cd27dc4196d41b28b" python-versions = "^3.6" [metadata.files] @@ -1657,6 +1665,10 @@ dparse = [ {file = "dparse-0.5.1-py3-none-any.whl", hash = "sha256:e953a25e44ebb60a5c6efc2add4420c177f1d8404509da88da9729202f306994"}, {file = "dparse-0.5.1.tar.gz", hash = "sha256:a1b5f169102e1c894f9a7d5ccf6f9402a836a5d24be80a986c7ce9eaed78f367"}, ] +example-shared-isort-profile = [ + {file = "example_shared_isort_profile-0.0.1-py3-none-any.whl", hash = "sha256:3fa3e2d093e68285fc7893704b727791ed3e0969d07bdd2733e366303d1a2582"}, + {file = "example_shared_isort_profile-0.0.1.tar.gz", hash = "sha256:fc2a0fa892611d6c1c2060dc0ca8e511e3f65fad8e4e449e8a375ef49549e710"}, +] examples = [ {file = "examples-1.0.2-py3-none-any.whl", hash = "sha256:372fefd15d5a17bda3b003cf26edbc2d29632bc63f29c816b55ed33dcccb3e65"}, {file = "examples-1.0.2.tar.gz", hash = "sha256:f29ba443f158bb47913ac21f098306a9749ed459a2290540ff1f86baac074597"}, diff --git a/pyproject.toml b/pyproject.toml index 464e8f059..43b71a910 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,6 +75,7 @@ pip-shims = "^0.5.2" smmap2 = "^3.0.1" gitdb2 = "^4.0.2" httpx = "^0.13.3" +example_shared_isort_profile = "^0.0.1" [tool.poetry.scripts] isort = "isort.main:main" diff --git a/tests/test_ticketed_features.py b/tests/test_ticketed_features.py index e57b32d91..cfb586e47 100644 --- a/tests/test_ticketed_features.py +++ b/tests/test_ticketed_features.py @@ -6,7 +6,7 @@ import pytest import isort -from isort import Config +from isort import Config, exceptions def test_semicolon_ignored_for_dynamic_lines_after_import_issue_1178(): @@ -200,7 +200,7 @@ def test_isort_warns_when_known_sections_dont_match_issue_1331(): ) -def test_isort_supports_append_only_imports_727(): +def test_isort_supports_append_only_imports_issue_727(): """Test to ensure isort provides a way to only add imports as an append. See: https://github.com/timothycrosley/isort/issues/727. """ @@ -212,3 +212,13 @@ def test_isort_supports_append_only_imports_727(): import os """ ) + + +def test_isort_supports_shared_profiles_issue_970(): + """Test to ensure isort provides a way to use shared profiles. + See: https://github.com/timothycrosley/isort/issues/970. + """ + assert isort.code("import a", profile="example") == "import a\n" # shared profile + assert isort.code("import a", profile="black") == "import a\n" # bundled profile + with pytest.raises(exceptions.ProfileDoesNotExist): + assert isort.code("import a", profile="madeupfake") == "import a\n" # non-existent profile From def4fe17e93398180fcb6cfee69c67610173b260 Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Mon, 27 Jul 2020 07:41:11 +0300 Subject: [PATCH 0859/1439] Flake8 config cleanup. --- isort/__init__.py | 2 +- isort/api.py | 4 ++-- isort/io.py | 2 +- isort/main.py | 2 +- isort/output.py | 2 +- isort/place.py | 2 +- scripts/lint.sh | 2 +- setup.cfg | 8 +++++++- tests/test_api.py | 2 +- tests/test_deprecated_finders.py | 2 -- tests/test_hooks.py | 1 - tests/test_importable.py | 2 +- tests/test_isort.py | 1 - tests/test_settings.py | 1 - tests/test_setuptools_command.py | 5 ----- 15 files changed, 17 insertions(+), 21 deletions(-) diff --git a/isort/__init__.py b/isort/__init__.py index 2d12b7a67..236255dd8 100644 --- a/isort/__init__.py +++ b/isort/__init__.py @@ -1,5 +1,5 @@ """Defines the public isort interface""" -from . import settings # noqa: F401 +from . import settings from ._version import __version__ from .api import check_code_string as check_code from .api import check_file, check_stream, place_module, place_module_with_reason diff --git a/isort/api.py b/isort/api.py index edebf529d..e576506d3 100644 --- a/isort/api.py +++ b/isort/api.py @@ -21,8 +21,8 @@ show_unified_diff, ) from .io import Empty -from .place import module as place_module # skipcq: PYL-W0611 (intended export of public API) -from .place import module_with_reason as place_module_with_reason # skipcq: PYL-W0611 (^) +from .place import module as place_module # noqa: F401 +from .place import module_with_reason as place_module_with_reason # noqa: F401 from .settings import DEFAULT_CONFIG, FILE_SKIP_COMMENTS, Config CIMPORT_IDENTIFIERS = ("cimport ", "cimport*", "from.cimport") diff --git a/isort/io.py b/isort/io.py index 753a6a6b5..a0357347b 100644 --- a/isort/io.py +++ b/isort/io.py @@ -4,7 +4,7 @@ from contextlib import contextmanager from io import BytesIO, StringIO, TextIOWrapper from pathlib import Path -from typing import Iterator, List, NamedTuple, Optional, TextIO, Tuple, Union +from typing import Iterator, NamedTuple, TextIO, Union _ENCODING_PATTERN = re.compile(br"^[ \t\f]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)") diff --git a/isort/main.py b/isort/main.py index c90b824ad..190d96b94 100644 --- a/isort/main.py +++ b/isort/main.py @@ -18,7 +18,7 @@ from .settings import SUPPORTED_EXTENSIONS, VALID_PY_TARGETS, Config, WrapModes try: - from .setuptools_commands import ISortCommand # skipcq: PYL-W0611 + from .setuptools_commands import ISortCommand # noqa: F401 except ImportError: pass diff --git a/isort/output.py b/isort/output.py index 306744da2..53e7791b0 100644 --- a/isort/output.py +++ b/isort/output.py @@ -1,7 +1,7 @@ import copy import itertools from functools import partial -from typing import Dict, Iterable, List, Tuple +from typing import Iterable, List, Tuple from isort.format import format_simplified diff --git a/isort/place.py b/isort/place.py index 36270f0ed..34b2eeb86 100644 --- a/isort/place.py +++ b/isort/place.py @@ -3,7 +3,7 @@ from fnmatch import fnmatch from functools import lru_cache from pathlib import Path -from typing import Dict, Optional, Tuple +from typing import Optional, Tuple from isort import sections from isort.settings import DEFAULT_CONFIG, Config diff --git a/scripts/lint.sh b/scripts/lint.sh index 086e91126..6d754e03b 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -5,6 +5,6 @@ poetry run cruft check poetry run mypy --ignore-missing-imports isort/ poetry run black --check isort/ tests/ poetry run isort --profile hug --check --diff isort/ tests/ -poetry run flake8 isort/ tests/ --max-line 100 --ignore F403,F401,W503,E203 +poetry run flake8 isort/ tests/ poetry run safety check poetry run bandit -r isort/ -x isort/_vendored diff --git a/setup.cfg b/setup.cfg index 2d729bf3e..b2bf769a2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,5 +17,11 @@ testpaths = tests [flake8] max-line-length = 100 -ignore = F403,F401,W503,E203 +# Ignore non PEP 8 compliant rules as suggested by black +ignore = + W503 # https://github.com/psf/black/blob/master/docs/the_black_code_style.md#line-breaks--binary-operators + E203 # https://github.com/psf/black/blob/master/docs/the_black_code_style.md#slices exclude = _vendored +per-file-ignores = + isort/__init__.py:F401 + isort/stdlibs/__init__.py:F401 diff --git a/tests/test_api.py b/tests/test_api.py index 76d473a2e..4cbe37281 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -4,7 +4,7 @@ import pytest -from isort import api, exceptions +from isort import api from isort.settings import Config diff --git a/tests/test_deprecated_finders.py b/tests/test_deprecated_finders.py index 614e3317f..bbd43596b 100644 --- a/tests/test_deprecated_finders.py +++ b/tests/test_deprecated_finders.py @@ -4,8 +4,6 @@ from pathlib import Path from unittest.mock import patch -import pytest - from isort import sections, settings from isort.deprecated import finders from isort.deprecated.finders import FindersManager diff --git a/tests/test_hooks.py b/tests/test_hooks.py index 677b0d209..ac3ea6436 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -1,5 +1,4 @@ import os -from io import BytesIO from unittest.mock import MagicMock, patch from isort import exceptions, hooks diff --git a/tests/test_importable.py b/tests/test_importable.py index 660de9908..c48ce333a 100644 --- a/tests/test_importable.py +++ b/tests/test_importable.py @@ -38,4 +38,4 @@ def test_importable(): import isort.wrap_modes with pytest.raises(SystemExit): - import isort.__main__ + import isort.__main__ # noqa: F401 diff --git a/tests/test_isort.py b/tests/test_isort.py index 12a9fc902..3947c261a 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -7,7 +7,6 @@ from pathlib import Path import subprocess import sys -import sysconfig from tempfile import NamedTemporaryFile from typing import Any, Dict, Iterator, List, Set, Tuple diff --git a/tests/test_settings.py b/tests/test_settings.py index e47165d33..b7cb6be76 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -1,5 +1,4 @@ from pathlib import Path -from unittest.mock import MagicMock, patch import pytest diff --git a/tests/test_setuptools_command.py b/tests/test_setuptools_command.py index 634dd8877..c8da0d23b 100644 --- a/tests/test_setuptools_command.py +++ b/tests/test_setuptools_command.py @@ -1,8 +1,3 @@ -import os -from unittest.mock import MagicMock - -import pytest - from isort import setuptools_commands From 04d00ebc217295a8f7b3c6b55e19f7c578dccfcf Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 26 Jul 2020 22:06:59 -0700 Subject: [PATCH 0860/1439] Fixed #1348: --diff works incorrectly with files that have CRLF line endings. --- .isort.cfg | 1 + CHANGELOG.md | 1 + isort/api.py | 27 ++++++++++++++++----------- isort/format.py | 1 - tests/example_crlf_file.py | 10 ++++++++++ tests/test_regressions.py | 14 ++++++++++++++ 6 files changed, 42 insertions(+), 12 deletions(-) create mode 100644 tests/example_crlf_file.py diff --git a/.isort.cfg b/.isort.cfg index 428419bc1..3f3b28ad5 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -1,3 +1,4 @@ [settings] profile=hug src_paths=isort,test +skip=tests/example_crlf_file.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e1096e81..1e23349f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. - Implemented #941: Added an additional `multi_line_output` mode for more compact formatting (Thanks @sztamas!) - `# isort: split` can now be used at the end of an import line. - Fixed #1339: Extra indent is not preserved when isort:skip is used in nested imports. + - Fixed #1348: `--diff` works incorrectly with files that have CRLF line endings. ### 5.1.4 July 19, 2020 - Fixed issue #1333: Use of wrap_length raises an exception about it not being lower or equal to line_length. diff --git a/isort/api.py b/isort/api.py index edebf529d..3e484a519 100644 --- a/isort/api.py +++ b/isort/api.py @@ -335,17 +335,22 @@ def sort_file( if changed: if show_diff or ask_to_apply: source_file.stream.seek(0) - show_unified_diff( - file_input=source_file.stream.read(), - file_output=tmp_file.read_text(encoding=source_file.encoding), - file_path=file_path or source_file.path, - output=None if show_diff is True else cast(TextIO, show_diff), - ) - if show_diff or ( - ask_to_apply - and not ask_whether_to_apply_changes_to_file(str(source_file.path)) - ): - return + with tmp_file.open( + encoding=source_file.encoding, newline="" + ) as tmp_out: + show_unified_diff( + file_input=source_file.stream.read(), + file_output=tmp_out.read(), + file_path=file_path or source_file.path, + output=None if show_diff is True else cast(TextIO, show_diff), + ) + if show_diff or ( + ask_to_apply + and not ask_whether_to_apply_changes_to_file( + str(source_file.path) + ) + ): + return source_file.stream.close() tmp_file.replace(source_file.path) if not config.quiet: diff --git a/isort/format.py b/isort/format.py index 15ad6233a..cdcc5a00b 100644 --- a/isort/format.py +++ b/isort/format.py @@ -43,7 +43,6 @@ def show_unified_diff( file_mtime = str( datetime.now() if file_path is None else datetime.fromtimestamp(file_path.stat().st_mtime) ) - unified_diff_lines = unified_diff( file_input.splitlines(keepends=True), file_output.splitlines(keepends=True), diff --git a/tests/example_crlf_file.py b/tests/example_crlf_file.py new file mode 100644 index 000000000..78db95188 --- /dev/null +++ b/tests/example_crlf_file.py @@ -0,0 +1,10 @@ +import b +import a + + +def func(): + x = 1 + y = 2 + z = 3 + c = 4 + return x + y + z + c diff --git a/tests/test_regressions.py b/tests/test_regressions.py index b62173883..5d1d0413a 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -1,4 +1,6 @@ """A growing set of tests designed to ensure isort doesn't have regressions in new versions""" +from io import StringIO + import isort @@ -478,3 +480,15 @@ def import_test(): """, show_diff=True, ) + + +def test_windows_diff_too_large_misrepresentative_issue_1348(test_path): + """Ensure isort handles windows files correctly when it come to producing a diff with --diff. + See: https://github.com/timothycrosley/isort/issues/1348 + """ + diff_output = StringIO() + isort.file(test_path / "example_crlf_file.py", show_diff=diff_output) + diff_output.seek(0) + assert diff_output.read().endswith( + "-1,5 +1,5 @@\n+import a\r\n import b\r\n" "-import a\r\n \r\n \r\n def func():\r\n" + ) From 9f5838207bb051ecd7d38901830b523c6dfa8aca Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 26 Jul 2020 23:32:25 -0700 Subject: [PATCH 0861/1439] Resolve #1020: Option for LOCALFOLDER. --- CHANGELOG.md | 1 + isort/main.py | 7 +++++++ isort/settings.py | 4 +++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e23349f5..ace3950e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. - Implemented #970: Support for custom sharable isort profiles. - Implemented #1214: Added support for git_hook lazy option (Thanks @sztamas!) - Implemented #941: Added an additional `multi_line_output` mode for more compact formatting (Thanks @sztamas!) + - Implemented #1020: Option for LOCALFOLDER. - `# isort: split` can now be used at the end of an import line. - Fixed #1339: Extra indent is not preserved when isort:skip is used in nested imports. - Fixed #1348: `--diff` works incorrectly with files that have CRLF line endings. diff --git a/isort/main.py b/isort/main.py index c90b824ad..c489ebe5c 100644 --- a/isort/main.py +++ b/isort/main.py @@ -387,6 +387,13 @@ def _build_arg_parser() -> argparse.ArgumentParser: action="append", help="Force isort to recognize a module as being part of the current python project.", ) + parser.add_argument( + "--known-local-folder", + dest="known_local_folder", + action="append", + help="Force isort to recognize a module as being a local folder. " + "Generally, this is reserved for relative imports (from . import module).", + ) parser.add_argument( "-q", "--quiet", diff --git a/isort/settings.py b/isort/settings.py index b6eebb237..2916d0517 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -119,6 +119,7 @@ class _Config: known_future_library: FrozenSet[str] = frozenset(("__future__",)) known_third_party: FrozenSet[str] = frozenset(("google.appengine.api",)) known_first_party: FrozenSet[str] = frozenset() + known_local_folder: FrozenSet[str] = frozenset() known_standard_library: FrozenSet[str] = frozenset() extra_standard_library: FrozenSet[str] = frozenset() known_other: Dict[str, FrozenSet[str]] = field(default_factory=dict) @@ -300,12 +301,13 @@ def __init__( "known_future_library", "known_third_party", "known_first_party", + "known_local_folder", ): import_heading = key[len(KNOWN_PREFIX) :].lower() known_other[import_heading] = frozenset(value) if not import_heading.upper() in combined_config.get("sections", ()): warn( - f"`{key}` setting is defined, but not {import_heading.upper} is not" + f"`{key}` setting is defined, but not {import_heading.upper()} is not" " included in `sections` config option:" f" {combined_config.get('sections', SECTION_DEFAULTS)}." ) From 6c8fc62fea9bf42bcb48fcdd210123eb7db9ba96 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 26 Jul 2020 23:51:36 -0700 Subject: [PATCH 0862/1439] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ace3950e1..aca46e2ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. - `# isort: split` can now be used at the end of an import line. - Fixed #1339: Extra indent is not preserved when isort:skip is used in nested imports. - Fixed #1348: `--diff` works incorrectly with files that have CRLF line endings. + - Improved code repositories usage of pylint tags (#1350). ### 5.1.4 July 19, 2020 - Fixed issue #1333: Use of wrap_length raises an exception about it not being lower or equal to line_length. From ca19112f21a4862d855485f1dfaf12f8ca6a52ea Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 27 Jul 2020 00:02:17 -0700 Subject: [PATCH 0863/1439] Add Abtin (@abtinmo) to acknowledgements --- docs/contributing/4.-acknowledgements.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index fbdfe144d..875baa933 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -205,7 +205,7 @@ Documenters - John Villalovos (@JohnVillalovos) - Kosei Kitahara (@Surgo) - Marat Sharafutdinov (@decaz) - +- Abtin (@abtinmo) -------------------------------------------- From b40e584d0742a52fc061f4dd55a4e8469d19c34f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 27 Jul 2020 01:53:15 -0700 Subject: [PATCH 0864/1439] Resolve #1353: Added support for output format plugin --- CHANGELOG.md | 1 + .../example_isort_formatting_plugin.py | 22 + example_isort_formatting_plugin/poetry.lock | 173 +++++++ .../pyproject.toml | 20 + isort/exceptions.py | 8 + isort/main.py | 9 + isort/output.py | 5 + isort/settings.py | 14 +- poetry.lock | 423 ++++++++++-------- pyproject.toml | 3 +- scripts/clean.sh | 2 + scripts/lint.sh | 2 + setup.cfg | 1 + tests/test_main.py | 1 + tests/test_ticketed_features.py | 9 + 15 files changed, 509 insertions(+), 184 deletions(-) create mode 100644 example_isort_formatting_plugin/example_isort_formatting_plugin.py create mode 100644 example_isort_formatting_plugin/poetry.lock create mode 100644 example_isort_formatting_plugin/pyproject.toml diff --git a/CHANGELOG.md b/CHANGELOG.md index aca46e2ed..f21f51f8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. - Implemented #1214: Added support for git_hook lazy option (Thanks @sztamas!) - Implemented #941: Added an additional `multi_line_output` mode for more compact formatting (Thanks @sztamas!) - Implemented #1020: Option for LOCALFOLDER. + - Implemented #1353: Added support for output formatting plugins. - `# isort: split` can now be used at the end of an import line. - Fixed #1339: Extra indent is not preserved when isort:skip is used in nested imports. - Fixed #1348: `--diff` works incorrectly with files that have CRLF line endings. diff --git a/example_isort_formatting_plugin/example_isort_formatting_plugin.py b/example_isort_formatting_plugin/example_isort_formatting_plugin.py new file mode 100644 index 000000000..ff65412f2 --- /dev/null +++ b/example_isort_formatting_plugin/example_isort_formatting_plugin.py @@ -0,0 +1,22 @@ +import black + +import isort + + +def black_format_import_section( + contents: str, extension: str, config: isort.settings.Config +) -> str: + """Formats the given import section using black.""" + if extension.lower() not in ("pyi", "py"): + return contents + + try: + return black.format_file_contents( + contents, + fast=True, + mode=black.FileMode( + is_pyi=extension.lower() == "pyi", line_length=config.line_length, + ), + ) + except black.NothingChanged: + return contents diff --git a/example_isort_formatting_plugin/poetry.lock b/example_isort_formatting_plugin/poetry.lock new file mode 100644 index 000000000..9c9a80305 --- /dev/null +++ b/example_isort_formatting_plugin/poetry.lock @@ -0,0 +1,173 @@ +[[package]] +category = "main" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +name = "appdirs" +optional = false +python-versions = "*" +version = "1.4.4" + +[[package]] +category = "main" +description = "Classes Without Boilerplate" +name = "attrs" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "19.3.0" + +[package.extras] +azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"] +dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"] +docs = ["sphinx", "zope.interface"] +tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] + +[[package]] +category = "main" +description = "The uncompromising code formatter." +name = "black" +optional = false +python-versions = ">=3.6" +version = "19.10b0" + +[package.dependencies] +appdirs = "*" +attrs = ">=18.1.0" +click = ">=6.5" +pathspec = ">=0.6,<1" +regex = "*" +toml = ">=0.9.4" +typed-ast = ">=1.4.0" + +[package.extras] +d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] + +[[package]] +category = "main" +description = "Composable command line interface toolkit" +name = "click" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "7.1.2" + +[[package]] +category = "main" +description = "A Python utility / library to sort Python imports." +name = "isort" +optional = false +python-versions = ">=3.6,<4.0" +version = "5.1.4" + +[package.extras] +pipfile_deprecated_finder = ["pipreqs", "requirementslib", "tomlkit (>=0.5.3)"] +requirements_deprecated_finder = ["pipreqs", "pip-api"] + +[[package]] +category = "main" +description = "Utility library for gitignore style pattern matching of file paths." +name = "pathspec" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.8.0" + +[[package]] +category = "main" +description = "Alternative regular expression module, to replace re." +name = "regex" +optional = false +python-versions = "*" +version = "2020.7.14" + +[[package]] +category = "main" +description = "Python Library for Tom's Obvious, Minimal Language" +name = "toml" +optional = false +python-versions = "*" +version = "0.10.1" + +[[package]] +category = "main" +description = "a fork of Python 2 and 3 ast modules with type comment support" +name = "typed-ast" +optional = false +python-versions = "*" +version = "1.4.1" + +[metadata] +content-hash = "48357b0d05225fbb76f9f02a8c7b5b39ee03707c4ae37d1357f306e51d8c8aa2" +python-versions = "^3.6" + +[metadata.files] +appdirs = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] +attrs = [ + {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, + {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, +] +black = [ + {file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"}, + {file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"}, +] +click = [ + {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, + {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, +] +isort = [ + {file = "isort-5.1.4-py3-none-any.whl", hash = "sha256:ae3007f72a2e9da36febd3454d8be4b175d6ca17eb765841d5fe3d038aede79d"}, + {file = "isort-5.1.4.tar.gz", hash = "sha256:145072eedc4927cc9c1f9478f2d83b2fc1e6469df4129c02ef4e8c742207a46c"}, +] +pathspec = [ + {file = "pathspec-0.8.0-py2.py3-none-any.whl", hash = "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0"}, + {file = "pathspec-0.8.0.tar.gz", hash = "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"}, +] +regex = [ + {file = "regex-2020.7.14-cp27-cp27m-win32.whl", hash = "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7"}, + {file = "regex-2020.7.14-cp27-cp27m-win_amd64.whl", hash = "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644"}, + {file = "regex-2020.7.14-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc"}, + {file = "regex-2020.7.14-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067"}, + {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd"}, + {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88"}, + {file = "regex-2020.7.14-cp36-cp36m-win32.whl", hash = "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4"}, + {file = "regex-2020.7.14-cp36-cp36m-win_amd64.whl", hash = "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f"}, + {file = "regex-2020.7.14-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162"}, + {file = "regex-2020.7.14-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf"}, + {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7"}, + {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89"}, + {file = "regex-2020.7.14-cp37-cp37m-win32.whl", hash = "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6"}, + {file = "regex-2020.7.14-cp37-cp37m-win_amd64.whl", hash = "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204"}, + {file = "regex-2020.7.14-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99"}, + {file = "regex-2020.7.14-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e"}, + {file = "regex-2020.7.14-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e"}, + {file = "regex-2020.7.14-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a"}, + {file = "regex-2020.7.14-cp38-cp38-win32.whl", hash = "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341"}, + {file = "regex-2020.7.14-cp38-cp38-win_amd64.whl", hash = "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840"}, + {file = "regex-2020.7.14.tar.gz", hash = "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb"}, +] +toml = [ + {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"}, + {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"}, +] +typed-ast = [ + {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, + {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, + {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, + {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, + {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, + {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, + {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, + {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, + {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, + {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, + {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, + {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, + {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, + {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, + {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, + {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, + {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, + {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, + {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, + {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, + {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, +] diff --git a/example_isort_formatting_plugin/pyproject.toml b/example_isort_formatting_plugin/pyproject.toml new file mode 100644 index 000000000..9e060ec1f --- /dev/null +++ b/example_isort_formatting_plugin/pyproject.toml @@ -0,0 +1,20 @@ +[tool.poetry] +name = "example_isort_formatting_plugin" +version = "0.0.1" +description = "An example plugin that modifies isort formatting using black." +authors = ["Timothy Crosley "] +license = "MIT" + +[tool.poetry.plugins."isort.formatters"] +example = "example_isort_formatting_plugin:black_format_import_section" + +[tool.poetry.dependencies] +python = "^3.6" +isort = "^5.1.4" +black = "^19.10b0" + +[tool.poetry.dev-dependencies] + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" diff --git a/isort/exceptions.py b/isort/exceptions.py index ffdc8d9af..b7be81daf 100644 --- a/isort/exceptions.py +++ b/isort/exceptions.py @@ -77,3 +77,11 @@ def __init__(self, profile: str): f"Available profiles: {','.join(profiles)}." ) self.profile = profile + + +class FormattingPluginDoesNotExist(ISortError): + """Raised when a formatting plugin is set by the user that doesn't exist""" + + def __init__(self, formatter: str): + super().__init__(f"Specified formatting plugin of {formatter} does not exist. ") + self.formatter = formatter diff --git a/isort/main.py b/isort/main.py index ba13fcc40..51a4d5563 100644 --- a/isort/main.py +++ b/isort/main.py @@ -625,6 +625,13 @@ def _build_arg_parser() -> argparse.ArgumentParser: "Still it can be a great shortcut for collecting imports every once in a while when you put" " them in the middle of a file.", ) + parser.add_argument( + "--formatter", + dest="formatter", + type=str, + help="Specifies the name of a formatting plugin to use when producing output.", + ) + # deprecated options parser.add_argument( "--recursive", @@ -686,6 +693,8 @@ def _preconvert(item): return item.name elif isinstance(item, Path): return str(item) + elif callable(item) and hasattr(item, "__name__"): + return item.__name__ else: raise TypeError("Unserializable object {} of type {}".format(item, type(item))) diff --git a/isort/output.py b/isort/output.py index 53e7791b0..b89f0e30a 100644 --- a/isort/output.py +++ b/isort/output.py @@ -171,6 +171,11 @@ def sorted_imports( while output and output[0].strip() == "": output.pop(0) + if config.formatting_function: + output = config.formatting_function( + parsed.line_separator.join(output), extension, config + ).splitlines() + output_at = 0 if parsed.import_index < parsed.original_line_count: output_at = parsed.import_index diff --git a/isort/settings.py b/isort/settings.py index 2916d0517..e2281acc4 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -17,7 +17,7 @@ from . import stdlibs from ._future import dataclass, field from ._vendored import toml -from .exceptions import InvalidSettingsPath, ProfileDoesNotExist +from .exceptions import FormattingPluginDoesNotExist, InvalidSettingsPath, ProfileDoesNotExist from .profiles import profiles from .sections import DEFAULT as SECTION_DEFAULTS from .sections import FIRSTPARTY, FUTURE, LOCALFOLDER, STDLIB, THIRDPARTY @@ -173,6 +173,8 @@ class _Config: remove_redundant_aliases: bool = False float_to_top: bool = False filter_files: bool = False + formatter: str = "" + formatting_function: Optional[Callable[[str, str, object], str]] = None def __post_init__(self): py_version = self.py_version @@ -348,6 +350,16 @@ def __init__( path_root / path for path in combined_config.get("src_paths", ()) ) + if "formatter" in combined_config: + import pkg_resources + + for plugin in pkg_resources.iter_entry_points("isort.formatters"): + if plugin.name == combined_config["formatter"]: + combined_config["formatting_function"] = plugin.load() + break + else: + raise FormattingPluginDoesNotExist(combined_config["formatter"]) + # Remove any config values that are used for creating config object but # aren't defined in dataclass combined_config.pop("source", None) diff --git a/poetry.lock b/poetry.lock index 1e81bd69d..1aa86e4a0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -21,7 +21,7 @@ description = "Better dates & times for Python" name = "arrow" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "0.15.6" +version = "0.15.8" [package.dependencies] python-dateutil = "*" @@ -55,7 +55,7 @@ description = "Specifications for callback functions passed in to an API" name = "backcall" optional = false python-versions = "*" -version = "0.1.0" +version = "0.2.0" [[package]] category = "dev" @@ -89,16 +89,19 @@ description = "The uncompromising code formatter." name = "black" optional = false python-versions = ">=3.6" -version = "18.9b0" +version = "19.10b0" [package.dependencies] appdirs = "*" -attrs = ">=17.4.0" +attrs = ">=18.1.0" click = ">=6.5" +pathspec = ">=0.6,<1" +regex = "*" toml = ">=0.9.4" +typed-ast = ">=1.4.0" [package.extras] -d = ["aiohttp (>=3.3.2)"] +d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] [[package]] category = "main" @@ -125,7 +128,7 @@ description = "Python package for providing Mozilla's CA Bundle." name = "certifi" optional = false python-versions = "*" -version = "2020.4.5.2" +version = "2020.6.20" [[package]] category = "main" @@ -188,7 +191,7 @@ description = "Code coverage measurement for Python" name = "coverage" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" -version = "5.1" +version = "5.2.1" [package.extras] toml = ["toml"] @@ -233,7 +236,7 @@ description = "Distribution utilities" name = "distlib" optional = false python-versions = "*" -version = "0.3.0" +version = "0.3.1" [[package]] category = "main" @@ -259,6 +262,18 @@ toml = "*" [package.extras] pipenv = ["pipenv"] +[[package]] +category = "dev" +description = "An example plugin that modifies isort formatting using black." +name = "example-isort-formatting-plugin" +optional = false +python-versions = ">=3.6,<4.0" +version = "0.0.1" + +[package.dependencies] +black = ">=19.10b0,<20.0" +isort = ">=5.1.4,<6.0.0" + [[package]] category = "dev" description = "An example shared isort profile" @@ -362,7 +377,7 @@ description = "Python Git Library" name = "gitpython" optional = false python-versions = ">=3.4" -version = "3.1.3" +version = "3.1.7" [package.dependencies] gitdb = ">=4.0.1,<5" @@ -459,7 +474,7 @@ description = "A library for property-based testing" name = "hypothesis" optional = false python-versions = ">=3.5.2" -version = "5.16.0" +version = "5.23.2" [package.dependencies] attrs = ">=19.2.0" @@ -497,7 +512,7 @@ description = "Internationalized Domain Names in Applications (IDNA)" name = "idna" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.9" +version = "2.10" [[package]] category = "dev" @@ -515,7 +530,7 @@ marker = "python_version < \"3.8\"" name = "importlib-metadata" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -version = "1.6.1" +version = "1.7.0" [package.dependencies] zipp = ">=0.5" @@ -530,7 +545,7 @@ description = "IPython: Productive Interactive Computing" name = "ipython" optional = false python-versions = ">=3.6" -version = "7.15.0" +version = "7.16.1" [package.dependencies] appnope = "*" @@ -570,14 +585,14 @@ description = "An autocompletion tool for Python that can be used for text edito name = "jedi" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "0.17.0" +version = "0.17.2" [package.dependencies] -parso = ">=0.7.0" +parso = ">=0.7.0,<0.8.0" [package.extras] qa = ["flake8 (3.7.9)"] -testing = ["colorama", "docopt", "pytest (>=3.9.0,<5.0.0)"] +testing = ["Django (<3.1)", "colorama", "docopt", "pytest (>=3.9.0,<5.0.0)"] [[package]] category = "dev" @@ -612,7 +627,7 @@ marker = "python_version > \"2.7\"" name = "joblib" optional = false python-versions = ">=3.6" -version = "0.15.1" +version = "0.16.0" [[package]] category = "dev" @@ -722,13 +737,25 @@ description = "A Material Design theme for MkDocs" name = "mkdocs-material" optional = false python-versions = "*" -version = "4.6.3" +version = "5.5.0" [package.dependencies] Pygments = ">=2.4" markdown = ">=3.2" -mkdocs = ">=1.0" -pymdown-extensions = ">=6.3" +mkdocs = ">=1.1" +mkdocs-material-extensions = ">=1.0" +pymdown-extensions = ">=7.0" + +[[package]] +category = "dev" +description = "Extension pack for Python Markdown." +name = "mkdocs-material-extensions" +optional = false +python-versions = ">=3.5" +version = "1.0" + +[package.dependencies] +mkdocs-material = ">=5.0.0" [[package]] category = "dev" @@ -736,7 +763,7 @@ description = "More routines for operating on iterables, beyond itertools" name = "more-itertools" optional = false python-versions = ">=3.5" -version = "8.3.0" +version = "8.4.0" [[package]] category = "dev" @@ -790,8 +817,8 @@ category = "dev" description = "NumPy is the fundamental package for array computing with Python." name = "numpy" optional = false -python-versions = ">=3.5" -version = "1.18.5" +python-versions = ">=3.6" +version = "1.19.1" [[package]] category = "main" @@ -822,11 +849,19 @@ description = "A Python Parser" name = "parso" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.7.0" +version = "0.7.1" [package.extras] testing = ["docopt", "pytest (>=3.0.7)"] +[[package]] +category = "dev" +description = "Utility library for gitignore style pattern matching of file paths." +name = "pathspec" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.8.0" + [[package]] category = "dev" description = "Python Build Reasonableness" @@ -993,15 +1028,15 @@ description = "Your Project with Great Documentation" name = "portray" optional = false python-versions = ">=3.6,<4.0" -version = "1.3.2" +version = "1.4.0" [package.dependencies] GitPython = ">=3.0,<4.0" hug = ">=2.6,<3.0" mkdocs = ">=1.0,<2.0" -mkdocs-material = ">=4.4,<5.0" +mkdocs-material = ">=5.0,<6.0" pdocs = ">=1.0.2,<2.0.0" -pymdown-extensions = ">=6.0,<7.0" +pymdown-extensions = ">=7.0,<8.0" toml = ">=0.10.0,<0.11.0" yaspin = ">=0.15.0,<0.16.0" @@ -1039,7 +1074,7 @@ description = "library with cross-python path, ini-parsing, io, code, log facili name = "py" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.8.1" +version = "1.9.0" [[package]] category = "dev" @@ -1055,7 +1090,7 @@ description = "Data validation and settings management using python 3.6 type hin name = "pydantic" optional = false python-versions = ">=3.6" -version = "1.5.1" +version = "1.6.1" [package.dependencies] [package.dependencies.dataclasses] @@ -1114,7 +1149,7 @@ description = "Extension pack for Python Markdown." name = "pymdown-extensions" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" -version = "6.3" +version = "7.1" [package.dependencies] Markdown = ">=3.2" @@ -1159,11 +1194,11 @@ description = "Pytest plugin for measuring coverage." name = "pytest-cov" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "2.9.0" +version = "2.10.0" [package.dependencies] coverage = ">=4.4" -pytest = ">=3.6" +pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests (2.0.2)", "six", "pytest-xdist", "virtualenv"] @@ -1199,7 +1234,7 @@ description = "A Python Slugify application that handles Unicode" name = "python-slugify" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "4.0.0" +version = "4.0.1" [package.dependencies] text-unidecode = ">=1.3" @@ -1218,11 +1253,10 @@ version = "5.3.1" [[package]] category = "dev" description = "Alternative regular expression module, to replace re." -marker = "python_version > \"2.7\"" name = "regex" optional = false python-versions = "*" -version = "2020.6.8" +version = "2020.7.14" [[package]] category = "main" @@ -1230,7 +1264,7 @@ description = "Python HTTP for Humans." name = "requests" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "2.23.0" +version = "2.24.0" [package.dependencies] certifi = ">=2017.4.17" @@ -1248,7 +1282,7 @@ description = "A tool for converting between pip-style and pipfile requirements. name = "requirementslib" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -version = "1.5.11" +version = "1.5.12" [package.dependencies] appdirs = "*" @@ -1363,11 +1397,15 @@ description = "Manage dynamic plugins for Python applications" name = "stevedore" optional = false python-versions = ">=3.6" -version = "2.0.0" +version = "3.2.0" [package.dependencies] pbr = ">=2.0.0,<2.1.0 || >2.1.0" +[package.dependencies.importlib-metadata] +python = "<3.8" +version = ">=1.7.0" + [[package]] category = "dev" description = "The most basic Text::Unidecode port" @@ -1407,7 +1445,7 @@ marker = "python_version > \"2.7\"" name = "tqdm" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*" -version = "4.46.1" +version = "4.48.0" [package.extras] dev = ["py-make (>=0.1.0)", "twine", "argopt", "pydoc-markdown"] @@ -1450,7 +1488,7 @@ description = "HTTP library with thread-safe connection pooling, file post, and name = "urllib3" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" -version = "1.25.9" +version = "1.25.10" [package.extras] brotli = ["brotlipy (>=0.6.0)"] @@ -1490,7 +1528,7 @@ description = "Measures the displayed width of unicode strings in a terminal" name = "wcwidth" optional = false python-versions = "*" -version = "0.2.4" +version = "0.2.5" [[package]] category = "main" @@ -1540,7 +1578,7 @@ pipfile_deprecated_finder = ["pipreqs", "tomlkit", "requirementslib"] requirements_deprecated_finder = ["pipreqs", "pip-api"] [metadata] -content-hash = "4db91d831e91eff8a42ff63d3ba2683d3575886c5d23c09cd27dc4196d41b28b" +content-hash = "8f4faa0c61a376603d64c87851e5a0e464698d4be31d7a29d33ed31df439b877" python-versions = "^3.6" [metadata.files] @@ -1553,8 +1591,8 @@ appnope = [ {file = "appnope-0.1.0.tar.gz", hash = "sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71"}, ] arrow = [ - {file = "arrow-0.15.6-py2.py3-none-any.whl", hash = "sha256:a24c1de90850f6fb2033fd6bf8a11f281e84cb54825e5eabdda219e673b52aac"}, - {file = "arrow-0.15.6.tar.gz", hash = "sha256:eb5d339f00072cc297d7de252a2e75f272085d1231a3723f1026d1fa91367118"}, + {file = "arrow-0.15.8-py2.py3-none-any.whl", hash = "sha256:271b8e05174d48e50324ed0dc5d74796c839c7e579a4f21cf1a7394665f9e94f"}, + {file = "arrow-0.15.8.tar.gz", hash = "sha256:edc31dc051db12c95da9bac0271cd1027b8e36912daf6d4580af53b23e62721a"}, ] atomicwrites = [ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, @@ -1565,8 +1603,8 @@ attrs = [ {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, ] backcall = [ - {file = "backcall-0.1.0.tar.gz", hash = "sha256:38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4"}, - {file = "backcall-0.1.0.zip", hash = "sha256:bbbf4b1e5cd2bdb08f915895b51081c041bac22394fdfcfdfbe9f14b77c08bf2"}, + {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, + {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, ] bandit = [ {file = "bandit-1.6.2-py2.py3-none-any.whl", hash = "sha256:336620e220cf2d3115877685e264477ff9d9abaeb0afe3dc7264f55fa17a3952"}, @@ -1577,8 +1615,8 @@ binaryornot = [ {file = "binaryornot-0.4.4.tar.gz", hash = "sha256:359501dfc9d40632edc9fac890e19542db1a287bbcfa58175b66658392018061"}, ] black = [ - {file = "black-18.9b0-py36-none-any.whl", hash = "sha256:817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739"}, - {file = "black-18.9b0.tar.gz", hash = "sha256:e030a9a28f542debc08acceb273f228ac422798e5215ba2a791a6ddeaaca22a5"}, + {file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"}, + {file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"}, ] cached-property = [ {file = "cached-property-1.5.1.tar.gz", hash = "sha256:9217a59f14a5682da7c4b8829deadbfc194ac22e9908ccf7c8820234e80a1504"}, @@ -1588,8 +1626,8 @@ cerberus = [ {file = "Cerberus-1.3.2.tar.gz", hash = "sha256:302e6694f206dd85cb63f13fd5025b31ab6d38c99c50c6d769f8fa0b0f299589"}, ] certifi = [ - {file = "certifi-2020.4.5.2-py2.py3-none-any.whl", hash = "sha256:9cd41137dc19af6a5e03b630eefe7d1f458d964d406342dd3edf625839b944cc"}, - {file = "certifi-2020.4.5.2.tar.gz", hash = "sha256:5ad7e9a056d25ffa5082862e36f119f7f7cec6457fa07ee2f8c339814b80c9b1"}, + {file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"}, + {file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"}, ] chardet = [ {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, @@ -1611,37 +1649,40 @@ cookiecutter = [ {file = "cookiecutter-1.7.2.tar.gz", hash = "sha256:efb6b2d4780feda8908a873e38f0e61778c23f6a2ea58215723bcceb5b515dac"}, ] coverage = [ - {file = "coverage-5.1-cp27-cp27m-macosx_10_12_x86_64.whl", hash = "sha256:0cb4be7e784dcdc050fc58ef05b71aa8e89b7e6636b99967fadbdba694cf2b65"}, - {file = "coverage-5.1-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:c317eaf5ff46a34305b202e73404f55f7389ef834b8dbf4da09b9b9b37f76dd2"}, - {file = "coverage-5.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b83835506dfc185a319031cf853fa4bb1b3974b1f913f5bb1a0f3d98bdcded04"}, - {file = "coverage-5.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5f2294dbf7875b991c381e3d5af2bcc3494d836affa52b809c91697449d0eda6"}, - {file = "coverage-5.1-cp27-cp27m-win32.whl", hash = "sha256:de807ae933cfb7f0c7d9d981a053772452217df2bf38e7e6267c9cbf9545a796"}, - {file = "coverage-5.1-cp27-cp27m-win_amd64.whl", hash = "sha256:bf9cb9a9fd8891e7efd2d44deb24b86d647394b9705b744ff6f8261e6f29a730"}, - {file = "coverage-5.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:acf3763ed01af8410fc36afea23707d4ea58ba7e86a8ee915dfb9ceff9ef69d0"}, - {file = "coverage-5.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:dec5202bfe6f672d4511086e125db035a52b00f1648d6407cc8e526912c0353a"}, - {file = "coverage-5.1-cp35-cp35m-macosx_10_12_x86_64.whl", hash = "sha256:7a5bdad4edec57b5fb8dae7d3ee58622d626fd3a0be0dfceda162a7035885ecf"}, - {file = "coverage-5.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:1601e480b9b99697a570cea7ef749e88123c04b92d84cedaa01e117436b4a0a9"}, - {file = "coverage-5.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:dbe8c6ae7534b5b024296464f387d57c13caa942f6d8e6e0346f27e509f0f768"}, - {file = "coverage-5.1-cp35-cp35m-win32.whl", hash = "sha256:a027ef0492ede1e03a8054e3c37b8def89a1e3c471482e9f046906ba4f2aafd2"}, - {file = "coverage-5.1-cp35-cp35m-win_amd64.whl", hash = "sha256:0e61d9803d5851849c24f78227939c701ced6704f337cad0a91e0972c51c1ee7"}, - {file = "coverage-5.1-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:2d27a3f742c98e5c6b461ee6ef7287400a1956c11421eb574d843d9ec1f772f0"}, - {file = "coverage-5.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:66460ab1599d3cf894bb6baee8c684788819b71a5dc1e8fa2ecc152e5d752019"}, - {file = "coverage-5.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5c542d1e62eece33c306d66fe0a5c4f7f7b3c08fecc46ead86d7916684b36d6c"}, - {file = "coverage-5.1-cp36-cp36m-win32.whl", hash = "sha256:2742c7515b9eb368718cd091bad1a1b44135cc72468c731302b3d641895b83d1"}, - {file = "coverage-5.1-cp36-cp36m-win_amd64.whl", hash = "sha256:dead2ddede4c7ba6cb3a721870f5141c97dc7d85a079edb4bd8d88c3ad5b20c7"}, - {file = "coverage-5.1-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:01333e1bd22c59713ba8a79f088b3955946e293114479bbfc2e37d522be03355"}, - {file = "coverage-5.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:e1ea316102ea1e1770724db01998d1603ed921c54a86a2efcb03428d5417e489"}, - {file = "coverage-5.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:adeb4c5b608574a3d647011af36f7586811a2c1197c861aedb548dd2453b41cd"}, - {file = "coverage-5.1-cp37-cp37m-win32.whl", hash = "sha256:782caea581a6e9ff75eccda79287daefd1d2631cc09d642b6ee2d6da21fc0a4e"}, - {file = "coverage-5.1-cp37-cp37m-win_amd64.whl", hash = "sha256:00f1d23f4336efc3b311ed0d807feb45098fc86dee1ca13b3d6768cdab187c8a"}, - {file = "coverage-5.1-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:402e1744733df483b93abbf209283898e9f0d67470707e3c7516d84f48524f55"}, - {file = "coverage-5.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:a3f3654d5734a3ece152636aad89f58afc9213c6520062db3978239db122f03c"}, - {file = "coverage-5.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6402bd2fdedabbdb63a316308142597534ea8e1895f4e7d8bf7476c5e8751fef"}, - {file = "coverage-5.1-cp38-cp38-win32.whl", hash = "sha256:8fa0cbc7ecad630e5b0f4f35b0f6ad419246b02bc750de7ac66db92667996d24"}, - {file = "coverage-5.1-cp38-cp38-win_amd64.whl", hash = "sha256:79a3cfd6346ce6c13145731d39db47b7a7b859c0272f02cdb89a3bdcbae233a0"}, - {file = "coverage-5.1-cp39-cp39-win32.whl", hash = "sha256:a82b92b04a23d3c8a581fc049228bafde988abacba397d57ce95fe95e0338ab4"}, - {file = "coverage-5.1-cp39-cp39-win_amd64.whl", hash = "sha256:bb28a7245de68bf29f6fb199545d072d1036a1917dca17a1e75bbb919e14ee8e"}, - {file = "coverage-5.1.tar.gz", hash = "sha256:f90bfc4ad18450c80b024036eaf91e4a246ae287701aaa88eaebebf150868052"}, + {file = "coverage-5.2.1-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:40f70f81be4d34f8d491e55936904db5c527b0711b2a46513641a5729783c2e4"}, + {file = "coverage-5.2.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:675192fca634f0df69af3493a48224f211f8db4e84452b08d5fcebb9167adb01"}, + {file = "coverage-5.2.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:2fcc8b58953d74d199a1a4d633df8146f0ac36c4e720b4a1997e9b6327af43a8"}, + {file = "coverage-5.2.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:64c4f340338c68c463f1b56e3f2f0423f7b17ba6c3febae80b81f0e093077f59"}, + {file = "coverage-5.2.1-cp27-cp27m-win32.whl", hash = "sha256:52f185ffd3291196dc1aae506b42e178a592b0b60a8610b108e6ad892cfc1bb3"}, + {file = "coverage-5.2.1-cp27-cp27m-win_amd64.whl", hash = "sha256:30bc103587e0d3df9e52cd9da1dd915265a22fad0b72afe54daf840c984b564f"}, + {file = "coverage-5.2.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:9ea749fd447ce7fb1ac71f7616371f04054d969d412d37611716721931e36efd"}, + {file = "coverage-5.2.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ce7866f29d3025b5b34c2e944e66ebef0d92e4a4f2463f7266daa03a1332a651"}, + {file = "coverage-5.2.1-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:4869ab1c1ed33953bb2433ce7b894a28d724b7aa76c19b11e2878034a4e4680b"}, + {file = "coverage-5.2.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:a3ee9c793ffefe2944d3a2bd928a0e436cd0ac2d9e3723152d6fd5398838ce7d"}, + {file = "coverage-5.2.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:28f42dc5172ebdc32622a2c3f7ead1b836cdbf253569ae5673f499e35db0bac3"}, + {file = "coverage-5.2.1-cp35-cp35m-win32.whl", hash = "sha256:e26c993bd4b220429d4ec8c1468eca445a4064a61c74ca08da7429af9bc53bb0"}, + {file = "coverage-5.2.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4186fc95c9febeab5681bc3248553d5ec8c2999b8424d4fc3a39c9cba5796962"}, + {file = "coverage-5.2.1-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:b360d8fd88d2bad01cb953d81fd2edd4be539df7bfec41e8753fe9f4456a5082"}, + {file = "coverage-5.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:1adb6be0dcef0cf9434619d3b892772fdb48e793300f9d762e480e043bd8e716"}, + {file = "coverage-5.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:098a703d913be6fbd146a8c50cc76513d726b022d170e5e98dc56d958fd592fb"}, + {file = "coverage-5.2.1-cp36-cp36m-win32.whl", hash = "sha256:962c44070c281d86398aeb8f64e1bf37816a4dfc6f4c0f114756b14fc575621d"}, + {file = "coverage-5.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1ed2bdb27b4c9fc87058a1cb751c4df8752002143ed393899edb82b131e0546"}, + {file = "coverage-5.2.1-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:c890728a93fffd0407d7d37c1e6083ff3f9f211c83b4316fae3778417eab9811"}, + {file = "coverage-5.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:538f2fd5eb64366f37c97fdb3077d665fa946d2b6d95447622292f38407f9258"}, + {file = "coverage-5.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:27ca5a2bc04d68f0776f2cdcb8bbd508bbe430a7bf9c02315cd05fb1d86d0034"}, + {file = "coverage-5.2.1-cp37-cp37m-win32.whl", hash = "sha256:aab75d99f3f2874733946a7648ce87a50019eb90baef931698f96b76b6769a46"}, + {file = "coverage-5.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:c2ff24df02a125b7b346c4c9078c8936da06964cc2d276292c357d64378158f8"}, + {file = "coverage-5.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:304fbe451698373dc6653772c72c5d5e883a4aadaf20343592a7abb2e643dae0"}, + {file = "coverage-5.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c96472b8ca5dc135fb0aa62f79b033f02aa434fb03a8b190600a5ae4102df1fd"}, + {file = "coverage-5.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8505e614c983834239f865da2dd336dcf9d72776b951d5dfa5ac36b987726e1b"}, + {file = "coverage-5.2.1-cp38-cp38-win32.whl", hash = "sha256:700997b77cfab016533b3e7dbc03b71d33ee4df1d79f2463a318ca0263fc29dd"}, + {file = "coverage-5.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:46794c815e56f1431c66d81943fa90721bb858375fb36e5903697d5eef88627d"}, + {file = "coverage-5.2.1-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:16042dc7f8e632e0dcd5206a5095ebd18cb1d005f4c89694f7f8aafd96dd43a3"}, + {file = "coverage-5.2.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:c1bbb628ed5192124889b51204de27c575b3ffc05a5a91307e7640eff1d48da4"}, + {file = "coverage-5.2.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4f6428b55d2916a69f8d6453e48a505c07b2245653b0aa9f0dee38785939f5e4"}, + {file = "coverage-5.2.1-cp39-cp39-win32.whl", hash = "sha256:9e536783a5acee79a9b308be97d3952b662748c4037b6a24cbb339dc7ed8eb89"}, + {file = "coverage-5.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:b8f58c7db64d8f27078cbf2a4391af6aa4e4767cc08b37555c4ae064b8558d9b"}, + {file = "coverage-5.2.1.tar.gz", hash = "sha256:a34cb28e0747ea15e82d13e14de606747e9e484fb28d63c999483f5d5188e89b"}, ] cruft = [ {file = "cruft-1.1.2-py3-none-any.whl", hash = "sha256:3864a6775381a69b36720401a9db1714c20af51190c8378fcd5de4560aeb9282"}, @@ -1656,7 +1697,8 @@ decorator = [ {file = "decorator-4.4.2.tar.gz", hash = "sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7"}, ] distlib = [ - {file = "distlib-0.3.0.zip", hash = "sha256:2e166e231a26b36d6dfe35a48c4464346620f8645ed0ace01ee31822b288de21"}, + {file = "distlib-0.3.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"}, + {file = "distlib-0.3.1.zip", hash = "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1"}, ] docopt = [ {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, @@ -1665,6 +1707,10 @@ dparse = [ {file = "dparse-0.5.1-py3-none-any.whl", hash = "sha256:e953a25e44ebb60a5c6efc2add4420c177f1d8404509da88da9729202f306994"}, {file = "dparse-0.5.1.tar.gz", hash = "sha256:a1b5f169102e1c894f9a7d5ccf6f9402a836a5d24be80a986c7ce9eaed78f367"}, ] +example-isort-formatting-plugin = [ + {file = "example_isort_formatting_plugin-0.0.1-py3-none-any.whl", hash = "sha256:3c4bd66eb457480daa1320e2e1ef4160e639f2629315cfc54830b2613aa823bc"}, + {file = "example_isort_formatting_plugin-0.0.1.tar.gz", hash = "sha256:2666045920accaa0c3f7e60e85369c408658faef50c6c8971519562a14b7e7d8"}, +] example-shared-isort-profile = [ {file = "example_shared_isort_profile-0.0.1-py3-none-any.whl", hash = "sha256:3fa3e2d093e68285fc7893704b727791ed3e0969d07bdd2733e366303d1a2582"}, {file = "example_shared_isort_profile-0.0.1.tar.gz", hash = "sha256:fc2a0fa892611d6c1c2060dc0ca8e511e3f65fad8e4e449e8a375ef49549e710"}, @@ -1713,8 +1759,8 @@ gitdb2 = [ {file = "gitdb2-4.0.2.tar.gz", hash = "sha256:0986cb4003de743f2b3aba4c828edd1ab58ce98e1c4a8acf72ef02760d4beb4e"}, ] gitpython = [ - {file = "GitPython-3.1.3-py3-none-any.whl", hash = "sha256:ef1d60b01b5ce0040ad3ec20bc64f783362d41fa0822a2742d3586e1f49bb8ac"}, - {file = "GitPython-3.1.3.tar.gz", hash = "sha256:e107af4d873daed64648b4f4beb89f89f0cfbe3ef558fc7821ed2331c2f8da1a"}, + {file = "GitPython-3.1.7-py3-none-any.whl", hash = "sha256:fa3b92da728a457dd75d62bb5f3eb2816d99a7fe6c67398e260637a40e3fafb5"}, + {file = "GitPython-3.1.7.tar.gz", hash = "sha256:2db287d71a284e22e5c2846042d0602465c7434d910406990d5b74df4afb0858"}, ] h11 = [ {file = "h11-0.9.0-py2.py3-none-any.whl", hash = "sha256:4bc6d6a1238b7615b266ada57e0618568066f57dd6fa967d1290ec9309b2f2f1"}, @@ -1749,16 +1795,16 @@ hyperframe = [ {file = "hyperframe-5.2.0.tar.gz", hash = "sha256:a9f5c17f2cc3c719b917c4f33ed1c61bd1f8dfac4b1bd23b7c80b3400971b41f"}, ] hypothesis = [ - {file = "hypothesis-5.16.0-py3-none-any.whl", hash = "sha256:21bb5fbe456f775233fe20bcbeb26f648d68025bce554c94c0698fb4c33e7008"}, - {file = "hypothesis-5.16.0.tar.gz", hash = "sha256:7c819501a26ff82ccb4bee8a96b1ff4fff187e041d79ed176964ca550b77098b"}, + {file = "hypothesis-5.23.2-py3-none-any.whl", hash = "sha256:30f78de476f4db986570c43ef3a5c6ba4666bdf3a925e2bf7835215a8b08fe0f"}, + {file = "hypothesis-5.23.2.tar.gz", hash = "sha256:d4048800a2f7b76f81baa9a31c1f102f18e7e37a9c038995e59d5f977e25003b"}, ] hypothesis-auto = [ {file = "hypothesis-auto-1.1.4.tar.gz", hash = "sha256:5e2c2fb09dc09842512d80630bb792359a1d33d2c0473ad47ee23da0be9e32b1"}, {file = "hypothesis_auto-1.1.4-py3-none-any.whl", hash = "sha256:fea8560c4522c0fd490ed8cc17e420b95dabebb11b9b334c59bf2d768839015f"}, ] idna = [ - {file = "idna-2.9-py2.py3-none-any.whl", hash = "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"}, - {file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"}, + {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, + {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, ] immutables = [ {file = "immutables-0.14-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:860666fab142401a5535bf65cbd607b46bc5ed25b9d1eb053ca8ed9a1a1a80d6"}, @@ -1775,20 +1821,20 @@ immutables = [ {file = "immutables-0.14.tar.gz", hash = "sha256:a0a1cc238b678455145bae291d8426f732f5255537ed6a5b7645949704c70a78"}, ] importlib-metadata = [ - {file = "importlib_metadata-1.6.1-py2.py3-none-any.whl", hash = "sha256:15ec6c0fd909e893e3a08b3a7c76ecb149122fb14b7efe1199ddd4c7c57ea958"}, - {file = "importlib_metadata-1.6.1.tar.gz", hash = "sha256:0505dd08068cfec00f53a74a0ad927676d7757da81b7436a6eefe4c7cf75c545"}, + {file = "importlib_metadata-1.7.0-py2.py3-none-any.whl", hash = "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"}, + {file = "importlib_metadata-1.7.0.tar.gz", hash = "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83"}, ] ipython = [ - {file = "ipython-7.15.0-py3-none-any.whl", hash = "sha256:1b85d65632211bf5d3e6f1406f3393c8c429a47d7b947b9a87812aa5bce6595c"}, - {file = "ipython-7.15.0.tar.gz", hash = "sha256:0ef1433879816a960cd3ae1ae1dc82c64732ca75cec8dab5a4e29783fb571d0e"}, + {file = "ipython-7.16.1-py3-none-any.whl", hash = "sha256:2dbcc8c27ca7d3cfe4fcdff7f45b27f9a8d3edfa70ff8024a71c7a8eb5f09d64"}, + {file = "ipython-7.16.1.tar.gz", hash = "sha256:9f4fcb31d3b2c533333893b9172264e4821c1ac91839500f31bd43f2c59b3ccf"}, ] ipython-genutils = [ {file = "ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8"}, {file = "ipython_genutils-0.2.0.tar.gz", hash = "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"}, ] jedi = [ - {file = "jedi-0.17.0-py2.py3-none-any.whl", hash = "sha256:cd60c93b71944d628ccac47df9a60fec53150de53d42dc10a7fc4b5ba6aae798"}, - {file = "jedi-0.17.0.tar.gz", hash = "sha256:df40c97641cb943661d2db4c33c2e1ff75d491189423249e989bcea4464f3030"}, + {file = "jedi-0.17.2-py2.py3-none-any.whl", hash = "sha256:98cc583fa0f2f8304968199b01b6b4b94f469a1f4a74c1560506ca2a211378b5"}, + {file = "jedi-0.17.2.tar.gz", hash = "sha256:86ed7d9b750603e4ba582ea8edc678657fb4007894a12bcf6f4bb97892f31d20"}, ] jinja2 = [ {file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"}, @@ -1799,8 +1845,8 @@ jinja2-time = [ {file = "jinja2_time-0.2.0-py2.py3-none-any.whl", hash = "sha256:d3eab6605e3ec8b7a0863df09cc1d23714908fa61aa6986a845c20ba488b4efa"}, ] joblib = [ - {file = "joblib-0.15.1-py3-none-any.whl", hash = "sha256:6825784ffda353cc8a1be573118085789e5b5d29401856b35b756645ab5aecb5"}, - {file = "joblib-0.15.1.tar.gz", hash = "sha256:61e49189c84b3c5d99a969d314853f4d1d263316cc694bec17548ebaa9c47b6e"}, + {file = "joblib-0.16.0-py3-none-any.whl", hash = "sha256:d348c5d4ae31496b2aa060d6d9b787864dd204f9480baaa52d18850cb43e9f49"}, + {file = "joblib-0.16.0.tar.gz", hash = "sha256:8f52bf24c64b608bf0b2563e0e47d6fcf516abc8cfafe10cfd98ad66d94f92d6"}, ] livereload = [ {file = "livereload-2.6.2.tar.gz", hash = "sha256:d1eddcb5c5eb8d2ca1fa1f750e580da624c0f7fcb734aa5780dc81b7dcbd89be"}, @@ -1861,12 +1907,16 @@ mkdocs = [ {file = "mkdocs-1.1.2.tar.gz", hash = "sha256:f0b61e5402b99d7789efa032c7a74c90a20220a9c81749da06dbfbcbd52ffb39"}, ] mkdocs-material = [ - {file = "mkdocs-material-4.6.3.tar.gz", hash = "sha256:1d486635b03f5a2ec87325842f7b10c7ae7daa0eef76b185572eece6a6ea212c"}, - {file = "mkdocs_material-4.6.3-py2.py3-none-any.whl", hash = "sha256:7f3afa0a09c07d0b89a6a9755fdb00513aee8f0cec3538bb903325c80f66f444"}, + {file = "mkdocs-material-5.5.0.tar.gz", hash = "sha256:6497ce2aa1ee3473a4e11897ffaade84419297388ba9459caeb6255c848e682c"}, + {file = "mkdocs_material-5.5.0-py2.py3-none-any.whl", hash = "sha256:b2c113340a9843d955383e8138dc0c5b29eb47dbfa279fbfd6e5f44545398cbe"}, +] +mkdocs-material-extensions = [ + {file = "mkdocs-material-extensions-1.0.tar.gz", hash = "sha256:17d7491e189af75700310b7ec33c6c48a22060b8b445001deca040cb60471cde"}, + {file = "mkdocs_material_extensions-1.0-py3-none-any.whl", hash = "sha256:09569c3694b5acc1e8334c9730e52b4bcde65fc9d613cc20e49af131ef1a9ca0"}, ] more-itertools = [ - {file = "more-itertools-8.3.0.tar.gz", hash = "sha256:558bb897a2232f5e4f8e2399089e35aecb746e1f9191b6584a151647e89267be"}, - {file = "more_itertools-8.3.0-py3-none-any.whl", hash = "sha256:7818f596b1e87be009031c7653d01acc46ed422e6656b394b0f765ce66ed4982"}, + {file = "more-itertools-8.4.0.tar.gz", hash = "sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5"}, + {file = "more_itertools-8.4.0-py3-none-any.whl", hash = "sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2"}, ] mypy = [ {file = "mypy-0.761-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:7f672d02fffcbace4db2b05369142e0506cdcde20cea0e07c7c2171c4fd11dd6"}, @@ -1892,27 +1942,32 @@ nltk = [ {file = "nltk-3.5.zip", hash = "sha256:845365449cd8c5f9731f7cb9f8bd6fd0767553b9d53af9eb1b3abf7700936b35"}, ] numpy = [ - {file = "numpy-1.18.5-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:e91d31b34fc7c2c8f756b4e902f901f856ae53a93399368d9a0dc7be17ed2ca0"}, - {file = "numpy-1.18.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7d42ab8cedd175b5ebcb39b5208b25ba104842489ed59fbb29356f671ac93583"}, - {file = "numpy-1.18.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:a78e438db8ec26d5d9d0e584b27ef25c7afa5a182d1bf4d05e313d2d6d515271"}, - {file = "numpy-1.18.5-cp35-cp35m-win32.whl", hash = "sha256:a87f59508c2b7ceb8631c20630118cc546f1f815e034193dc72390db038a5cb3"}, - {file = "numpy-1.18.5-cp35-cp35m-win_amd64.whl", hash = "sha256:965df25449305092b23d5145b9bdaeb0149b6e41a77a7d728b1644b3c99277c1"}, - {file = "numpy-1.18.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ac792b385d81151bae2a5a8adb2b88261ceb4976dbfaaad9ce3a200e036753dc"}, - {file = "numpy-1.18.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:ef627986941b5edd1ed74ba89ca43196ed197f1a206a3f18cc9faf2fb84fd675"}, - {file = "numpy-1.18.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:f718a7949d1c4f622ff548c572e0c03440b49b9531ff00e4ed5738b459f011e8"}, - {file = "numpy-1.18.5-cp36-cp36m-win32.whl", hash = "sha256:4064f53d4cce69e9ac613256dc2162e56f20a4e2d2086b1956dd2fcf77b7fac5"}, - {file = "numpy-1.18.5-cp36-cp36m-win_amd64.whl", hash = "sha256:b03b2c0badeb606d1232e5f78852c102c0a7989d3a534b3129e7856a52f3d161"}, - {file = "numpy-1.18.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a7acefddf994af1aeba05bbbafe4ba983a187079f125146dc5859e6d817df824"}, - {file = "numpy-1.18.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:cd49930af1d1e49a812d987c2620ee63965b619257bd76eaaa95870ca08837cf"}, - {file = "numpy-1.18.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:b39321f1a74d1f9183bf1638a745b4fd6fe80efbb1f6b32b932a588b4bc7695f"}, - {file = "numpy-1.18.5-cp37-cp37m-win32.whl", hash = "sha256:cae14a01a159b1ed91a324722d746523ec757357260c6804d11d6147a9e53e3f"}, - {file = "numpy-1.18.5-cp37-cp37m-win_amd64.whl", hash = "sha256:0172304e7d8d40e9e49553901903dc5f5a49a703363ed756796f5808a06fc233"}, - {file = "numpy-1.18.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e15b382603c58f24265c9c931c9a45eebf44fe2e6b4eaedbb0d025ab3255228b"}, - {file = "numpy-1.18.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:3676abe3d621fc467c4c1469ee11e395c82b2d6b5463a9454e37fe9da07cd0d7"}, - {file = "numpy-1.18.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:4674f7d27a6c1c52a4d1aa5f0881f1eff840d2206989bae6acb1c7668c02ebfb"}, - {file = "numpy-1.18.5-cp38-cp38-win32.whl", hash = "sha256:9c9d6531bc1886454f44aa8f809268bc481295cf9740827254f53c30104f074a"}, - {file = "numpy-1.18.5-cp38-cp38-win_amd64.whl", hash = "sha256:3dd6823d3e04b5f223e3e265b4a1eae15f104f4366edd409e5a5e413a98f911f"}, - {file = "numpy-1.18.5.zip", hash = "sha256:34e96e9dae65c4839bd80012023aadd6ee2ccb73ce7fdf3074c62f301e63120b"}, + {file = "numpy-1.19.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b1cca51512299841bf69add3b75361779962f9cee7d9ee3bb446d5982e925b69"}, + {file = "numpy-1.19.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c9591886fc9cbe5532d5df85cb8e0cc3b44ba8ce4367bd4cf1b93dc19713da72"}, + {file = "numpy-1.19.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:cf1347450c0b7644ea142712619533553f02ef23f92f781312f6a3553d031fc7"}, + {file = "numpy-1.19.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:ed8a311493cf5480a2ebc597d1e177231984c818a86875126cfd004241a73c3e"}, + {file = "numpy-1.19.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:3673c8b2b29077f1b7b3a848794f8e11f401ba0b71c49fbd26fb40b71788b132"}, + {file = "numpy-1.19.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:56ef7f56470c24bb67fb43dae442e946a6ce172f97c69f8d067ff8550cf782ff"}, + {file = "numpy-1.19.1-cp36-cp36m-win32.whl", hash = "sha256:aaf42a04b472d12515debc621c31cf16c215e332242e7a9f56403d814c744624"}, + {file = "numpy-1.19.1-cp36-cp36m-win_amd64.whl", hash = "sha256:082f8d4dd69b6b688f64f509b91d482362124986d98dc7dc5f5e9f9b9c3bb983"}, + {file = "numpy-1.19.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e4f6d3c53911a9d103d8ec9518190e52a8b945bab021745af4939cfc7c0d4a9e"}, + {file = "numpy-1.19.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:5b6885c12784a27e957294b60f97e8b5b4174c7504665333c5e94fbf41ae5d6a"}, + {file = "numpy-1.19.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:1bc0145999e8cb8aed9d4e65dd8b139adf1919e521177f198529687dbf613065"}, + {file = "numpy-1.19.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:5a936fd51049541d86ccdeef2833cc89a18e4d3808fe58a8abeb802665c5af93"}, + {file = "numpy-1.19.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:ef71a1d4fd4858596ae80ad1ec76404ad29701f8ca7cdcebc50300178db14dfc"}, + {file = "numpy-1.19.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b9792b0ac0130b277536ab8944e7b754c69560dac0415dd4b2dbd16b902c8954"}, + {file = "numpy-1.19.1-cp37-cp37m-win32.whl", hash = "sha256:b12e639378c741add21fbffd16ba5ad25c0a1a17cf2b6fe4288feeb65144f35b"}, + {file = "numpy-1.19.1-cp37-cp37m-win_amd64.whl", hash = "sha256:8343bf67c72e09cfabfab55ad4a43ce3f6bf6e6ced7acf70f45ded9ebb425055"}, + {file = "numpy-1.19.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e45f8e981a0ab47103181773cc0a54e650b2aef8c7b6cd07405d0fa8d869444a"}, + {file = "numpy-1.19.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:667c07063940e934287993366ad5f56766bc009017b4a0fe91dbd07960d0aba7"}, + {file = "numpy-1.19.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:480fdd4dbda4dd6b638d3863da3be82873bba6d32d1fc12ea1b8486ac7b8d129"}, + {file = "numpy-1.19.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:935c27ae2760c21cd7354402546f6be21d3d0c806fffe967f745d5f2de5005a7"}, + {file = "numpy-1.19.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:309cbcfaa103fc9a33ec16d2d62569d541b79f828c382556ff072442226d1968"}, + {file = "numpy-1.19.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:7ed448ff4eaffeb01094959b19cbaf998ecdee9ef9932381420d514e446601cd"}, + {file = "numpy-1.19.1-cp38-cp38-win32.whl", hash = "sha256:de8b4a9b56255797cbddb93281ed92acbc510fb7b15df3f01bd28f46ebc4edae"}, + {file = "numpy-1.19.1-cp38-cp38-win_amd64.whl", hash = "sha256:92feb989b47f83ebef246adabc7ff3b9a59ac30601c3f6819f8913458610bdcc"}, + {file = "numpy-1.19.1-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:e1b1dc0372f530f26a03578ac75d5e51b3868b9b76cd2facba4c9ee0eb252ab1"}, + {file = "numpy-1.19.1.zip", hash = "sha256:b8456987b637232602ceb4d663cb34106f7eb780e247d51a260b84760fd8f491"}, ] orderedmultidict = [ {file = "orderedmultidict-1.0.1-py2.py3-none-any.whl", hash = "sha256:43c839a17ee3cdd62234c47deca1a8508a3f2ca1d0678a3bf791c87cf84adbf3"}, @@ -1923,8 +1978,12 @@ packaging = [ {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, ] parso = [ - {file = "parso-0.7.0-py2.py3-none-any.whl", hash = "sha256:158c140fc04112dc45bca311633ae5033c2c2a7b732fa33d0955bad8152a8dd0"}, - {file = "parso-0.7.0.tar.gz", hash = "sha256:908e9fae2144a076d72ae4e25539143d40b8e3eafbaeae03c1bfe226f4cdf12c"}, + {file = "parso-0.7.1-py2.py3-none-any.whl", hash = "sha256:97218d9159b2520ff45eb78028ba8b50d2bc61dcc062a9682666f2dc4bd331ea"}, + {file = "parso-0.7.1.tar.gz", hash = "sha256:caba44724b994a8a5e086460bb212abc5a8bc46951bf4a9a1210745953622eb9"}, +] +pathspec = [ + {file = "pathspec-0.8.0-py2.py3-none-any.whl", hash = "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0"}, + {file = "pathspec-0.8.0.tar.gz", hash = "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"}, ] pbr = [ {file = "pbr-5.4.5-py2.py3-none-any.whl", hash = "sha256:579170e23f8e0c2f24b0de612f71f648eccb79fb1322c814ae6b3c07b5ba23e8"}, @@ -1974,8 +2033,8 @@ pluggy = [ {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, ] portray = [ - {file = "portray-1.3.2-py3-none-any.whl", hash = "sha256:5f108eb4c6efc74901a138a1fe44384e18e5fdc4c8de6deda64b416ba015e52c"}, - {file = "portray-1.3.2.tar.gz", hash = "sha256:4c2f3ef64278877cbcfc4756d9a70edb0e4fc7875e0000d0ad9f1412d213eb49"}, + {file = "portray-1.4.0-py3-none-any.whl", hash = "sha256:a6a06042a6b7fcb876b1e6cdcaee5adaeb6751388cb292fc05b2f31b1a4c3fb2"}, + {file = "portray-1.4.0.tar.gz", hash = "sha256:ea2271c5e3fbe956070a6f8b1aee6dc3d6a66c18c11907e878db8faa6fd2c449"}, ] poyo = [ {file = "poyo-0.5.0-py2.py3-none-any.whl", hash = "sha256:3e2ca8e33fdc3c411cd101ca395668395dd5dc7ac775b8e809e3def9f9fe041a"}, @@ -1990,31 +2049,31 @@ ptyprocess = [ {file = "ptyprocess-0.6.0.tar.gz", hash = "sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0"}, ] py = [ - {file = "py-1.8.1-py2.py3-none-any.whl", hash = "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"}, - {file = "py-1.8.1.tar.gz", hash = "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa"}, + {file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"}, + {file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"}, ] pycodestyle = [ {file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"}, {file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"}, ] pydantic = [ - {file = "pydantic-1.5.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2a6904e9f18dea58f76f16b95cba6a2f20b72d787abd84ecd67ebc526e61dce6"}, - {file = "pydantic-1.5.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:da8099fca5ee339d5572cfa8af12cf0856ae993406f0b1eb9bb38c8a660e7416"}, - {file = "pydantic-1.5.1-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:68dece67bff2b3a5cc188258e46b49f676a722304f1c6148ae08e9291e284d98"}, - {file = "pydantic-1.5.1-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:ab863853cb502480b118187d670f753be65ec144e1654924bec33d63bc8b3ce2"}, - {file = "pydantic-1.5.1-cp36-cp36m-win_amd64.whl", hash = "sha256:2007eb062ed0e57875ce8ead12760a6e44bf5836e6a1a7ea81d71eeecf3ede0f"}, - {file = "pydantic-1.5.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:20a15a303ce1e4d831b4e79c17a4a29cb6740b12524f5bba3ea363bff65732bc"}, - {file = "pydantic-1.5.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:473101121b1bd454c8effc9fe66d54812fdc128184d9015c5aaa0d4e58a6d338"}, - {file = "pydantic-1.5.1-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:9be755919258d5d168aeffbe913ed6e8bd562e018df7724b68cabdee3371e331"}, - {file = "pydantic-1.5.1-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:b96ce81c4b5ca62ab81181212edfd057beaa41411cd9700fbcb48a6ba6564b4e"}, - {file = "pydantic-1.5.1-cp37-cp37m-win_amd64.whl", hash = "sha256:93b9f265329d9827f39f0fca68f5d72cc8321881cdc519a1304fa73b9f8a75bd"}, - {file = "pydantic-1.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e2c753d355126ddd1eefeb167fa61c7037ecd30b98e7ebecdc0d1da463b4ea09"}, - {file = "pydantic-1.5.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:8433dbb87246c0f562af75d00fa80155b74e4f6924b0db6a2078a3cd2f11c6c4"}, - {file = "pydantic-1.5.1-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:0a1cdf24e567d42dc762d3fed399bd211a13db2e8462af9dfa93b34c41648efb"}, - {file = "pydantic-1.5.1-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:8be325fc9da897029ee48d1b5e40df817d97fe969f3ac3fd2434ba7e198c55d5"}, - {file = "pydantic-1.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:3714a4056f5bdbecf3a41e0706ec9b228c9513eee2ad884dc2c568c4dfa540e9"}, - {file = "pydantic-1.5.1-py36.py37.py38-none-any.whl", hash = "sha256:70f27d2f0268f490fe3de0a9b6fca7b7492b8fd6623f9fecd25b221ebee385e3"}, - {file = "pydantic-1.5.1.tar.gz", hash = "sha256:f0018613c7a0d19df3240c2a913849786f21b6539b9f23d85ce4067489dfacfa"}, + {file = "pydantic-1.6.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:418b84654b60e44c0cdd5384294b0e4bc1ebf42d6e873819424f3b78b8690614"}, + {file = "pydantic-1.6.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:4900b8820b687c9a3ed753684337979574df20e6ebe4227381d04b3c3c628f99"}, + {file = "pydantic-1.6.1-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:b49c86aecde15cde33835d5d6360e55f5e0067bb7143a8303bf03b872935c75b"}, + {file = "pydantic-1.6.1-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:2de562a456c4ecdc80cf1a8c3e70c666625f7d02d89a6174ecf63754c734592e"}, + {file = "pydantic-1.6.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f769141ab0abfadf3305d4fcf36660e5cf568a666dd3efab7c3d4782f70946b1"}, + {file = "pydantic-1.6.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2dc946b07cf24bee4737ced0ae77e2ea6bc97489ba5a035b603bd1b40ad81f7e"}, + {file = "pydantic-1.6.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:36dbf6f1be212ab37b5fda07667461a9219c956181aa5570a00edfb0acdfe4a1"}, + {file = "pydantic-1.6.1-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:1783c1d927f9e1366e0e0609ae324039b2479a1a282a98ed6a6836c9ed02002c"}, + {file = "pydantic-1.6.1-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:cf3933c98cb5e808b62fae509f74f209730b180b1e3c3954ee3f7949e083a7df"}, + {file = "pydantic-1.6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f8af9b840a9074e08c0e6dc93101de84ba95df89b267bf7151d74c553d66833b"}, + {file = "pydantic-1.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:40d765fa2d31d5be8e29c1794657ad46f5ee583a565c83cea56630d3ae5878b9"}, + {file = "pydantic-1.6.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:3fa799f3cfff3e5f536cbd389368fc96a44bb30308f258c94ee76b73bd60531d"}, + {file = "pydantic-1.6.1-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:6c3f162ba175678218629f446a947e3356415b6b09122dcb364e58c442c645a7"}, + {file = "pydantic-1.6.1-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:eb75dc1809875d5738df14b6566ccf9fd9c0bcde4f36b72870f318f16b9f5c20"}, + {file = "pydantic-1.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:530d7222a2786a97bc59ee0e0ebbe23728f82974b1f1ad9a11cd966143410633"}, + {file = "pydantic-1.6.1-py36.py37.py38-none-any.whl", hash = "sha256:b5b3489cb303d0f41ad4a7390cf606a5f2c7a94dcba20c051cd1c653694cb14d"}, + {file = "pydantic-1.6.1.tar.gz", hash = "sha256:54122a8ed6b75fe1dd80797f8251ad2063ea348a03b77218d73ea9fe19bd4e73"}, ] pydocstyle = [ {file = "pydocstyle-5.0.2-py3-none-any.whl", hash = "sha256:da7831660b7355307b32778c4a0dbfb137d89254ef31a2b2978f50fc0b4d7586"}, @@ -2033,8 +2092,8 @@ pylama = [ {file = "pylama-7.7.1.tar.gz", hash = "sha256:9bae53ef9c1a431371d6a8dca406816a60d547147b60a4934721898f553b7d8f"}, ] pymdown-extensions = [ - {file = "pymdown-extensions-6.3.tar.gz", hash = "sha256:cb879686a586b22292899771f5e5bc3382808e92aa938f71b550ecdea709419f"}, - {file = "pymdown_extensions-6.3-py2.py3-none-any.whl", hash = "sha256:66fae2683c7a1dac53184f7de57f51f8dad73f9ead2f453e94e85096cb811335"}, + {file = "pymdown-extensions-7.1.tar.gz", hash = "sha256:5bf93d1ccd8281948cd7c559eb363e59b179b5373478e8a7195cf4b78e3c11b6"}, + {file = "pymdown_extensions-7.1-py2.py3-none-any.whl", hash = "sha256:8f415b21ee86d80bb2c3676f4478b274d0a8ccb13af672a4c86b9ffd22bd005c"}, ] pyparsing = [ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, @@ -2045,8 +2104,8 @@ pytest = [ {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, ] pytest-cov = [ - {file = "pytest-cov-2.9.0.tar.gz", hash = "sha256:b6a814b8ed6247bd81ff47f038511b57fe1ce7f4cc25b9106f1a4b106f1d9322"}, - {file = "pytest_cov-2.9.0-py2.py3-none-any.whl", hash = "sha256:c87dfd8465d865655a8213859f1b4749b43448b5fae465cb981e16d52a811424"}, + {file = "pytest-cov-2.10.0.tar.gz", hash = "sha256:1a629dc9f48e53512fcbfda6b07de490c374b0c83c55ff7a1720b3fccff0ac87"}, + {file = "pytest_cov-2.10.0-py2.py3-none-any.whl", hash = "sha256:6e6d18092dce6fad667cd7020deed816f858ad3b49d5b5e2b1cc1c97a4dba65c"}, ] pytest-mock = [ {file = "pytest-mock-1.13.0.tar.gz", hash = "sha256:e24a911ec96773022ebcc7030059b57cd3480b56d4f5d19b7c370ec635e6aed5"}, @@ -2057,7 +2116,7 @@ python-dateutil = [ {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, ] python-slugify = [ - {file = "python-slugify-4.0.0.tar.gz", hash = "sha256:a8fc3433821140e8f409a9831d13ae5deccd0b033d4744d94b31fea141bdd84c"}, + {file = "python-slugify-4.0.1.tar.gz", hash = "sha256:69a517766e00c1268e5bbfc0d010a0a8508de0b18d30ad5a1ff357f8ae724270"}, ] pyyaml = [ {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, @@ -2073,35 +2132,35 @@ pyyaml = [ {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, ] regex = [ - {file = "regex-2020.6.8-cp27-cp27m-win32.whl", hash = "sha256:fbff901c54c22425a5b809b914a3bfaf4b9570eee0e5ce8186ac71eb2025191c"}, - {file = "regex-2020.6.8-cp27-cp27m-win_amd64.whl", hash = "sha256:112e34adf95e45158c597feea65d06a8124898bdeac975c9087fe71b572bd938"}, - {file = "regex-2020.6.8-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:92d8a043a4241a710c1cf7593f5577fbb832cf6c3a00ff3fc1ff2052aff5dd89"}, - {file = "regex-2020.6.8-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:bae83f2a56ab30d5353b47f9b2a33e4aac4de9401fb582b55c42b132a8ac3868"}, - {file = "regex-2020.6.8-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:b2ba0f78b3ef375114856cbdaa30559914d081c416b431f2437f83ce4f8b7f2f"}, - {file = "regex-2020.6.8-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:95fa7726d073c87141f7bbfb04c284901f8328e2d430eeb71b8ffdd5742a5ded"}, - {file = "regex-2020.6.8-cp36-cp36m-win32.whl", hash = "sha256:e3cdc9423808f7e1bb9c2e0bdb1c9dc37b0607b30d646ff6faf0d4e41ee8fee3"}, - {file = "regex-2020.6.8-cp36-cp36m-win_amd64.whl", hash = "sha256:c78e66a922de1c95a208e4ec02e2e5cf0bb83a36ceececc10a72841e53fbf2bd"}, - {file = "regex-2020.6.8-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:08997a37b221a3e27d68ffb601e45abfb0093d39ee770e4257bd2f5115e8cb0a"}, - {file = "regex-2020.6.8-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2f6f211633ee8d3f7706953e9d3edc7ce63a1d6aad0be5dcee1ece127eea13ae"}, - {file = "regex-2020.6.8-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:55b4c25cbb3b29f8d5e63aeed27b49fa0f8476b0d4e1b3171d85db891938cc3a"}, - {file = "regex-2020.6.8-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:89cda1a5d3e33ec9e231ece7307afc101b5217523d55ef4dc7fb2abd6de71ba3"}, - {file = "regex-2020.6.8-cp37-cp37m-win32.whl", hash = "sha256:690f858d9a94d903cf5cada62ce069b5d93b313d7d05456dbcd99420856562d9"}, - {file = "regex-2020.6.8-cp37-cp37m-win_amd64.whl", hash = "sha256:1700419d8a18c26ff396b3b06ace315b5f2a6e780dad387e4c48717a12a22c29"}, - {file = "regex-2020.6.8-cp38-cp38-manylinux1_i686.whl", hash = "sha256:654cb773b2792e50151f0e22be0f2b6e1c3a04c5328ff1d9d59c0398d37ef610"}, - {file = "regex-2020.6.8-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:52e1b4bef02f4040b2fd547357a170fc1146e60ab310cdbdd098db86e929b387"}, - {file = "regex-2020.6.8-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:cf59bbf282b627130f5ba68b7fa3abdb96372b24b66bdf72a4920e8153fc7910"}, - {file = "regex-2020.6.8-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:5aaa5928b039ae440d775acea11d01e42ff26e1561c0ffcd3d805750973c6baf"}, - {file = "regex-2020.6.8-cp38-cp38-win32.whl", hash = "sha256:97712e0d0af05febd8ab63d2ef0ab2d0cd9deddf4476f7aa153f76feef4b2754"}, - {file = "regex-2020.6.8-cp38-cp38-win_amd64.whl", hash = "sha256:6ad8663c17db4c5ef438141f99e291c4d4edfeaacc0ce28b5bba2b0bf273d9b5"}, - {file = "regex-2020.6.8.tar.gz", hash = "sha256:e9b64e609d37438f7d6e68c2546d2cb8062f3adb27e6336bc129b51be20773ac"}, + {file = "regex-2020.7.14-cp27-cp27m-win32.whl", hash = "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7"}, + {file = "regex-2020.7.14-cp27-cp27m-win_amd64.whl", hash = "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644"}, + {file = "regex-2020.7.14-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc"}, + {file = "regex-2020.7.14-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067"}, + {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd"}, + {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88"}, + {file = "regex-2020.7.14-cp36-cp36m-win32.whl", hash = "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4"}, + {file = "regex-2020.7.14-cp36-cp36m-win_amd64.whl", hash = "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f"}, + {file = "regex-2020.7.14-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162"}, + {file = "regex-2020.7.14-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf"}, + {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7"}, + {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89"}, + {file = "regex-2020.7.14-cp37-cp37m-win32.whl", hash = "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6"}, + {file = "regex-2020.7.14-cp37-cp37m-win_amd64.whl", hash = "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204"}, + {file = "regex-2020.7.14-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99"}, + {file = "regex-2020.7.14-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e"}, + {file = "regex-2020.7.14-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e"}, + {file = "regex-2020.7.14-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a"}, + {file = "regex-2020.7.14-cp38-cp38-win32.whl", hash = "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341"}, + {file = "regex-2020.7.14-cp38-cp38-win_amd64.whl", hash = "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840"}, + {file = "regex-2020.7.14.tar.gz", hash = "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb"}, ] requests = [ - {file = "requests-2.23.0-py2.py3-none-any.whl", hash = "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee"}, - {file = "requests-2.23.0.tar.gz", hash = "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"}, + {file = "requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"}, + {file = "requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"}, ] requirementslib = [ - {file = "requirementslib-1.5.11-py2.py3-none-any.whl", hash = "sha256:3dea8f577be1f7f5aafb66007cb49ef659f9249649aa11312a903fa4205a7350"}, - {file = "requirementslib-1.5.11.tar.gz", hash = "sha256:6acae5ba27c9a1a45d120d74e0b922b202e0c614591880ae060d06c1e77eff66"}, + {file = "requirementslib-1.5.12-py2.py3-none-any.whl", hash = "sha256:5e283a74df80c27ca3ebedda5a38301d23b2d563f9d758cbfdc385fdb0f58a87"}, + {file = "requirementslib-1.5.12.tar.gz", hash = "sha256:c80ec03f70ee0b435c9e74686e61b1ddb7afd1b228ed3f6ebc64a2e5fb530b5e"}, ] rfc3986 = [ {file = "rfc3986-1.4.0-py2.py3-none-any.whl", hash = "sha256:af9147e9aceda37c91a05f4deb128d4b4b49d6b199775fd2d2927768abdc8f50"}, @@ -2136,8 +2195,8 @@ sortedcontainers = [ {file = "sortedcontainers-2.2.2.tar.gz", hash = "sha256:4e73a757831fc3ca4de2859c422564239a31d8213d09a2a666e375807034d2ba"}, ] stevedore = [ - {file = "stevedore-2.0.0-py3-none-any.whl", hash = "sha256:471c920412265cc809540ae6fb01f3f02aba89c79bbc7091372f4745a50f9691"}, - {file = "stevedore-2.0.0.tar.gz", hash = "sha256:001e90cd704be6470d46cc9076434e2d0d566c1379187e7013eb296d3a6032d9"}, + {file = "stevedore-3.2.0-py3-none-any.whl", hash = "sha256:c8f4f0ebbc394e52ddf49de8bcc3cf8ad2b4425ebac494106bbc5e3661ac7633"}, + {file = "stevedore-3.2.0.tar.gz", hash = "sha256:38791aa5bed922b0a844513c5f9ed37774b68edc609e5ab8ab8d8fe0ce4315e5"}, ] text-unidecode = [ {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, @@ -2163,8 +2222,8 @@ tornado = [ {file = "tornado-6.0.4.tar.gz", hash = "sha256:0fe2d45ba43b00a41cd73f8be321a44936dc1aba233dee979f17a042b83eb6dc"}, ] tqdm = [ - {file = "tqdm-4.46.1-py2.py3-none-any.whl", hash = "sha256:07c06493f1403c1380b630ae3dcbe5ae62abcf369a93bbc052502279f189ab8c"}, - {file = "tqdm-4.46.1.tar.gz", hash = "sha256:cd140979c2bebd2311dfb14781d8f19bd5a9debb92dcab9f6ef899c987fcf71f"}, + {file = "tqdm-4.48.0-py2.py3-none-any.whl", hash = "sha256:fcb7cb5b729b60a27f300b15c1ffd4744f080fb483b88f31dc8654b082cc8ea5"}, + {file = "tqdm-4.48.0.tar.gz", hash = "sha256:6baa75a88582b1db6d34ce4690da5501d2a1cb65c34664840a456b2c9f794d29"}, ] traitlets = [ {file = "traitlets-4.3.3-py2.py3-none-any.whl", hash = "sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44"}, @@ -2199,8 +2258,8 @@ typing-extensions = [ {file = "typing_extensions-3.7.4.2.tar.gz", hash = "sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae"}, ] urllib3 = [ - {file = "urllib3-1.25.9-py2.py3-none-any.whl", hash = "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115"}, - {file = "urllib3-1.25.9.tar.gz", hash = "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527"}, + {file = "urllib3-1.25.10-py2.py3-none-any.whl", hash = "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"}, + {file = "urllib3-1.25.10.tar.gz", hash = "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a"}, ] vistir = [ {file = "vistir-0.5.2-py2.py3-none-any.whl", hash = "sha256:a37079cdbd85d31a41cdd18457fe521e15ec08b255811e81aa061fd5f48a20fb"}, @@ -2211,8 +2270,8 @@ vulture = [ {file = "vulture-1.5.tar.gz", hash = "sha256:07dfab84a32867ae2636bb3998ce50a4e059556dacb0cca4dbe51a1f3cc9d6d7"}, ] wcwidth = [ - {file = "wcwidth-0.2.4-py2.py3-none-any.whl", hash = "sha256:79375666b9954d4a1a10739315816324c3e73110af9d0e102d906fdb0aec009f"}, - {file = "wcwidth-0.2.4.tar.gz", hash = "sha256:8c6b5b6ee1360b842645f336d9e5d68c55817c26d3050f46b235ef2bc650e48f"}, + {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, + {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, ] wheel = [ {file = "wheel-0.34.2-py2.py3-none-any.whl", hash = "sha256:df277cb51e61359aba502208d680f90c0493adec6f0e848af94948778aed386e"}, diff --git a/pyproject.toml b/pyproject.toml index 43b71a910..c02489022 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,7 @@ vulture = "^1.0" bandit = "^1.6" safety = "^1.8" flake8-bugbear = "^19.8" -black = {version = "^18.3-alpha.0", allow-prereleases = true} +black = {version = "^19.10b0", allow-prereleases = true} mypy = "^0.761.0" ipython = "^7.7" pytest = "^5.0" @@ -76,6 +76,7 @@ smmap2 = "^3.0.1" gitdb2 = "^4.0.2" httpx = "^0.13.3" example_shared_isort_profile = "^0.0.1" +example_isort_formatting_plugin = "^0.0.1" [tool.poetry.scripts] isort = "isort.main:main" diff --git a/scripts/clean.sh b/scripts/clean.sh index c29f44826..7d446611e 100755 --- a/scripts/clean.sh +++ b/scripts/clean.sh @@ -2,4 +2,6 @@ set -euxo pipefail poetry run isort --profile hug isort/ tests/ scripts/ +poetry run isort --profile hug example_isort_formatting_plugin/ poetry run black isort/ tests/ scripts/ +poetry run black example_isort_formatting_plugin/ diff --git a/scripts/lint.sh b/scripts/lint.sh index 6d754e03b..9761425c3 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -4,7 +4,9 @@ set -euxo pipefail poetry run cruft check poetry run mypy --ignore-missing-imports isort/ poetry run black --check isort/ tests/ +poetry run black --check example_isort_formatting_plugin/ poetry run isort --profile hug --check --diff isort/ tests/ +poetry run isort --profile hug --check --diff example_isort_formatting_plugin/ poetry run flake8 isort/ tests/ poetry run safety check poetry run bandit -r isort/ -x isort/_vendored diff --git a/setup.cfg b/setup.cfg index b2bf769a2..770db0e11 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,3 +25,4 @@ exclude = _vendored per-file-ignores = isort/__init__.py:F401 isort/stdlibs/__init__.py:F401 + tests/example_crlf_file.py:F401 diff --git a/tests/test_main.py b/tests/test_main.py index 3d8f20117..bc4df4e91 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -76,6 +76,7 @@ def test_ascii_art(capsys): def test_preconvert(): assert main._preconvert(frozenset([1, 1, 2])) == [1, 2] assert main._preconvert(WrapModes.GRID) == "GRID" + assert main._preconvert(main._preconvert) == "_preconvert" with pytest.raises(TypeError): main._preconvert(datetime.now()) diff --git a/tests/test_ticketed_features.py b/tests/test_ticketed_features.py index cfb586e47..bfa1be5c4 100644 --- a/tests/test_ticketed_features.py +++ b/tests/test_ticketed_features.py @@ -222,3 +222,12 @@ def test_isort_supports_shared_profiles_issue_970(): assert isort.code("import a", profile="black") == "import a\n" # bundled profile with pytest.raises(exceptions.ProfileDoesNotExist): assert isort.code("import a", profile="madeupfake") == "import a\n" # non-existent profile + + +def test_isort_supports_formatting_plugins_issue_1353(): + """Test to ensure isort provides a way to create and share formatting plugins. + See: https://github.com/timothycrosley/isort/issues/1353. + """ + assert isort.code("import a", formatter="example") == "import a\n" # formatting plugin + with pytest.raises(exceptions.FormattingPluginDoesNotExist): + assert isort.code("import a", formatter="madeupfake") == "import a\n" # non-existent plugin From ae8f9c39cd75d837a4cb5a4cea4d3d11fd1cabed Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 27 Jul 2020 02:02:33 -0700 Subject: [PATCH 0865/1439] Add additional test case for comments --- tests/test_comments.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_comments.py b/tests/test_comments.py index b1b4ed7b4..4098f8ec6 100644 --- a/tests/test_comments.py +++ b/tests/test_comments.py @@ -4,3 +4,7 @@ auto_pytest_magic(comments.parse) auto_pytest_magic(comments.add_to_line) + + +def test_add_to_line(): + assert comments.add_to_line([], "import os # comment", removed=True).strip() == "import os" From 05baa33ff0fbaaa6a4c3d19523975a9d1b7c91f0 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 27 Jul 2020 02:07:50 -0700 Subject: [PATCH 0866/1439] Bump version to 5.2.0 --- CHANGELOG.md | 2 +- isort/_version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f21f51f8c..fdca75f27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. -### 5.2.0 TBD +### 5.2.0 July 27, 2020 - Implemented #1335: Official API for diff capturing. - Implemented #1331: Warn when sections don't match up. - Implemented #1261: By popular demand, `filter_files` can now be set in the config option. diff --git a/isort/_version.py b/isort/_version.py index 1070cc03c..6c235c596 100644 --- a/isort/_version.py +++ b/isort/_version.py @@ -1 +1 @@ -__version__ = "5.1.4" +__version__ = "5.2.0" diff --git a/pyproject.toml b/pyproject.toml index c02489022..a20c80a7e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ line-length = 100 [tool.poetry] name = "isort" -version = "5.1.4" +version = "5.2.0" description = "A Python utility / library to sort Python imports." authors = ["Timothy Crosley "] license = "MIT" From a996689f239a5d434d99d3a731cf378c41899020 Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Mon, 27 Jul 2020 20:20:21 +0300 Subject: [PATCH 0867/1439] Make --color optional, fail if colorama isn't installed. colorama changed to an extra dependency. --- docs/configuration/options.md | 11 +++++++ isort/api.py | 12 +++----- isort/format.py | 54 ++++++++++++++++++++++++++++------- isort/main.py | 6 ++++ isort/settings.py | 1 + poetry.lock | 3 +- pyproject.toml | 3 +- setup.cfg | 1 + tests/test_format.py | 48 +++++++++++++++++++++++-------- 9 files changed, 107 insertions(+), 32 deletions(-) diff --git a/docs/configuration/options.md b/docs/configuration/options.md index d70602bb3..f63debb12 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -937,6 +937,17 @@ See isort's determined config, as well as sources of config options. - --show-config +## Color Terminal Output + +Tells isort to use color in terminal output. + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** color_output +**CLI Flags:** + +- --color + ## Deprecated Flags ==SUPPRESS== diff --git a/isort/api.py b/isort/api.py index 7cb2640c3..9f849d646 100644 --- a/isort/api.py +++ b/isort/api.py @@ -16,11 +16,10 @@ ) from .format import ( ask_whether_to_apply_changes_to_file, + create_terminal_printer, format_natural, remove_whitespace, show_unified_diff, - style_error, - style_success, ) from .io import Empty from .place import module as place_module # noqa: F401 @@ -218,16 +217,13 @@ def check_stream( file_path=file_path, disregard_skip=disregard_skip, ) - + printer = create_terminal_printer(color=config.color_output) if not changed: if config.verbose: - print(f"{style_success('SUCCESS')}: {file_path or ''} Everything Looks Good!") + printer.success(f"{file_path or ''} Everything Looks Good!") return True else: - print( - f"{style_error('ERROR')}: " - f"{file_path or ''} Imports are incorrectly sorted and/or formatted." - ) + printer.error(f"{file_path or ''} Imports are incorrectly sorted and/or formatted.") if show_diff: output_stream = StringIO() input_stream.seek(0) diff --git a/isort/format.py b/isort/format.py index 10e9b2cc9..8cee40706 100644 --- a/isort/format.py +++ b/isort/format.py @@ -1,13 +1,16 @@ import sys from datetime import datetime from difflib import unified_diff -from functools import partial from pathlib import Path -from typing import Iterable, Optional, TextIO +from typing import Optional, TextIO -import colorama - -colorama.init() +try: + import colorama +except ImportError: + colorama_unavailable = True +else: + colorama_unavailable = False + colorama.init() def format_simplified(import_line: str) -> str: @@ -77,10 +80,41 @@ def remove_whitespace(content: str, line_separator: str = "\n") -> str: return content -def style_text(text: str, styles: Iterable[str]) -> str: - escape_sequences = "".join(styles) - return escape_sequences + text + colorama.Style.RESET_ALL +class BasicPrinter: + ERROR = "ERROR" + SUCCESS = "SUCCESS" + + def success(self, message: str) -> None: + print(f"{self.SUCCESS}: {message}") + + def error(self, message: str) -> None: + print( + f"{self.ERROR}: {message}", + # TODO this should print to stderr, but don't want to make it backward incompatible now + # file=sys.stderr + ) + + +class ColoramaPrinter(BasicPrinter): + def __init__(self): + self.ERROR = self.style_text("ERROR", colorama.Fore.RED) + self.SUCCESS = self.style_text("SUCCESS", colorama.Fore.GREEN) + + def style_text(self, text: str, style: str) -> str: + return style + text + colorama.Style.RESET_ALL + +def create_terminal_printer(color: bool): + if color and colorama_unavailable: + no_colorama_message = ( + "\n" + "Sorry, but to use --color (color_output) the colorama python package is required.\n\n" + "Reference: https://pypi.org/project/colorama/\n\n" + "You can either install it separately on your system or as the colors extra " + "for isort. Ex: \n\n" + "$ pip install isort[colors]\n" + ) + print(no_colorama_message, file=sys.stderr) + sys.exit(1) -style_error = partial(style_text, styles=(colorama.Fore.RED,)) -style_success = partial(style_text, styles=(colorama.Fore.GREEN,)) + return ColoramaPrinter() if color else BasicPrinter() diff --git a/isort/main.py b/isort/main.py index 190d96b94..a98b99606 100644 --- a/isort/main.py +++ b/isort/main.py @@ -608,6 +608,12 @@ def _build_arg_parser() -> argparse.ArgumentParser: " aliases to signify intent and change behaviour." ), ) + parser.add_argument( + "--color", + dest="color_output", + action="store_true", + help="Tells isort to use color in terminal output.", + ) parser.add_argument( "--float-to-top", dest="float_to_top", diff --git a/isort/settings.py b/isort/settings.py index b6eebb237..b15c608b9 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -172,6 +172,7 @@ class _Config: remove_redundant_aliases: bool = False float_to_top: bool = False filter_files: bool = False + color_output: bool = False def __post_init__(self): py_version = self.py_version diff --git a/poetry.lock b/poetry.lock index 40df05a8c..ea446ed9c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1536,11 +1536,12 @@ docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] testing = ["jaraco.itertools", "func-timeout"] [extras] +colors = ["colorama"] pipfile_deprecated_finder = ["pipreqs", "tomlkit", "requirementslib"] requirements_deprecated_finder = ["pipreqs", "pip-api"] [metadata] -content-hash = "8165d934e932435bf4742b9198674202413b43524911713d5c7c55cb8d314618" +content-hash = "d3e80ae0d5bb87916c6ab758738e841a77d010f3c225d0ff54f30c1a25ae9892" python-versions = "^3.6" [metadata.files] diff --git a/pyproject.toml b/pyproject.toml index cd3af26e6..2747587c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,11 +42,12 @@ pipreqs = {version = "*", optional = true} requirementslib = {version = "*", optional = true} tomlkit = {version = ">=0.5.3", optional = true} pip-api = {version = "*", optional = true} -colorama = "^0.4.3" +colorama = {version = "^0.4.3", optional = true} [tool.poetry.extras] pipfile_deprecated_finder = ["pipreqs", "tomlkit", "requirementslib", "pip-shims<=0.3.4"] requirements_deprecated_finder = ["pipreqs", "pip-api"] +colors = ["colorama"] [tool.poetry.dev-dependencies] vulture = "^1.0" diff --git a/setup.cfg b/setup.cfg index b2bf769a2..770db0e11 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,3 +25,4 @@ exclude = _vendored per-file-ignores = isort/__init__.py:F401 isort/stdlibs/__init__.py:F401 + tests/example_crlf_file.py:F401 diff --git a/tests/test_format.py b/tests/test_format.py index 819f72f2f..3a8f7236b 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -19,15 +19,39 @@ def test_ask_whether_to_apply_changes_to_file(): assert isort.format.ask_whether_to_apply_changes_to_file("") -def test_style_error(): - message = "ERROR" - styled = isort.format.style_error(message) - assert message in styled - assert styled == colorama.Fore.RED + message + colorama.Style.RESET_ALL - - -def test_style_success(): - message = "OK" - styled = isort.format.style_success(message) - assert message in styled - assert styled == colorama.Fore.GREEN + message + colorama.Style.RESET_ALL +def test_basic_printer(capsys): + printer = isort.format.create_terminal_printer(color=False) + printer.success("All good!") + out, _ = capsys.readouterr() + assert out == "SUCCESS: All good!\n" + printer.error("Some error") + out, _ = capsys.readouterr() + assert out == "ERROR: Some error\n" + + +def test_colored_printer_success(capsys): + printer = isort.format.create_terminal_printer(color=True) + printer.success("All good!") + out, _ = capsys.readouterr() + assert "SUCCESS" in out + assert "All good!" in out + assert colorama.Fore.GREEN in out + + +def test_colored_printer_error(capsys): + printer = isort.format.create_terminal_printer(color=True) + printer.error("Some error") + out, _ = capsys.readouterr() + assert "ERROR" in out + assert "Some error" in out + assert colorama.Fore.RED in out + + +@patch("isort.format.colorama_unavailable", True) +def test_colorama_not_available_handled_gracefully(capsys): + with pytest.raises(SystemExit) as system_exit: + _ = isort.format.create_terminal_printer(color=True) + assert system_exit.value.code > 0 + _, err = capsys.readouterr() + assert "colorama" in err + assert "colors extra" in err From dc44cc8ccbca40d0ae2e3a93daf1e641ff838c9d Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Mon, 27 Jul 2020 20:42:18 +0300 Subject: [PATCH 0868/1439] Instance method -> static method --- isort/format.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/isort/format.py b/isort/format.py index 8cee40706..3dbb19570 100644 --- a/isort/format.py +++ b/isort/format.py @@ -100,7 +100,8 @@ def __init__(self): self.ERROR = self.style_text("ERROR", colorama.Fore.RED) self.SUCCESS = self.style_text("SUCCESS", colorama.Fore.GREEN) - def style_text(self, text: str, style: str) -> str: + @staticmethod + def style_text(text: str, style: str) -> str: return style + text + colorama.Style.RESET_ALL From e934ad3741cea336f41ca9a7d0cd478395d904db Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 27 Jul 2020 21:01:47 -0700 Subject: [PATCH 0869/1439] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fdca75f27..e3c820eea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. +### 5.3.0 TBD + - Implemented #1177: Support for color output using `--color`. + ### 5.2.0 July 27, 2020 - Implemented #1335: Official API for diff capturing. - Implemented #1331: Warn when sections don't match up. From 0b26361d5d8fb824db7e805f3f7c78d02fcd2613 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 28 Jul 2020 20:37:23 -0700 Subject: [PATCH 0870/1439] Fix CLI documentation for filter files. --- .isort.cfg | 2 +- docs/configuration/options.md | 57 +++++++++++++++++++++++++++-------- isort/main.py | 3 +- 3 files changed, 46 insertions(+), 16 deletions(-) diff --git a/.isort.cfg b/.isort.cfg index 3f3b28ad5..1e3f216fb 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -1,4 +1,4 @@ [settings] profile=hug src_paths=isort,test -skip=tests/example_crlf_file.py +skip=tests/example_crlf_file.py,migrations diff --git a/docs/configuration/options.md b/docs/configuration/options.md index f63debb12..60db0ace2 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -162,6 +162,17 @@ Force isort to recognize a module as being part of the current python project. - -p - --project +## Known Local Folder + +Force isort to recognize a module as being a local folder. Generally, this is reserved for relative imports (from . import module). + +**Type:** Frozenset +**Default:** `frozenset()` +**Python & Config File Name:** known_local_folder +**CLI Flags:** + +- --known-local-folder + ## Known Standard Library Force isort to recognize a module as part of Python's standard library. @@ -719,7 +730,7 @@ Inserts a blank line before a comment following an import. ## Profile -Base profile type to use for configuration. +Base profile type to use for configuration. Profiles include: black, django, pycharm, google, open_stack, plone, attrs, hug. As well as any shared profiles. **Type:** String **Default:** `` @@ -787,7 +798,7 @@ Causes all non indented imports to float to the top of the file having its impor ## Filter Files -Tells isort to filter files even when they are explicitly passed in as part of the CLI command. Note that while this can be set as part of the config file it only affects CLI operation. +Tells isort to filter files even when they are explicitly passed in as part of the CLI command. **Type:** Bool **Default:** `False` @@ -796,6 +807,37 @@ Tells isort to filter files even when they are explicitly passed in as part of t - --filter-files +## Formatter + +Specifies the name of a formatting plugin to use when producing output. + +**Type:** String +**Default:** `` +**Python & Config File Name:** formatter +**CLI Flags:** + +- --formatter + +## Formatting Function + +**No Description** + +**Type:** Nonetype +**Default:** `None` +**Python & Config File Name:** formatting_function +**CLI Flags:** **Not Supported** + +## Color Output + +Tells isort to use color in terminal output. + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** color_output +**CLI Flags:** + +- --color + ## Check Checks the file for unsorted / unformatted imports and prints them to the command line without modifying the file. @@ -937,17 +979,6 @@ See isort's determined config, as well as sources of config options. - --show-config -## Color Terminal Output - -Tells isort to use color in terminal output. - -**Type:** Bool -**Default:** `False` -**Python & Config File Name:** color_output -**CLI Flags:** - -- --color - ## Deprecated Flags ==SUPPRESS== diff --git a/isort/main.py b/isort/main.py index 3724bbc1b..b6583974a 100644 --- a/isort/main.py +++ b/isort/main.py @@ -555,8 +555,7 @@ def _build_arg_parser() -> argparse.ArgumentParser: dest="filter_files", action="store_true", help="Tells isort to filter files even when they are explicitly passed in as " - "part of the CLI command. Note that while this can be set as part of the config file it " - "only affects CLI operation.", + "part of the CLI command." ) parser.add_argument( "files", nargs="*", help="One or more Python source files that need their imports sorted." From 1bf93c7082f78c6c4a5242849808e7fff62f6b95 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 28 Jul 2020 21:36:07 -0700 Subject: [PATCH 0871/1439] Fix small formatting issue --- isort/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/main.py b/isort/main.py index b6583974a..413388721 100644 --- a/isort/main.py +++ b/isort/main.py @@ -555,7 +555,7 @@ def _build_arg_parser() -> argparse.ArgumentParser: dest="filter_files", action="store_true", help="Tells isort to filter files even when they are explicitly passed in as " - "part of the CLI command." + "part of the CLI command.", ) parser.add_argument( "files", nargs="*", help="One or more Python source files that need their imports sorted." From 92b84bdc0e4a65f7cfc82e65e80c019fb551338f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 28 Jul 2020 21:39:47 -0700 Subject: [PATCH 0872/1439] Improve handling of relative path detection --- isort/settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/settings.py b/isort/settings.py index 0e8ae56be..7376004d8 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -389,8 +389,8 @@ def __init__( def is_skipped(self, file_path: Path) -> bool: """Returns True if the file and/or folder should be skipped based on current settings.""" - if self.directory and Path(self.directory) in file_path.parents: - file_name = os.path.relpath(file_path, self.directory) + if self.directory and Path(self.directory) in file_path.resolve().parents: + file_name = os.path.relpath(file_path.resolve(), self.directory) else: file_name = str(file_path) From 368556e44490bc9c137c3f6b4a181aeddf8ab92e Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 28 Jul 2020 21:44:13 -0700 Subject: [PATCH 0873/1439] Default filter-files to true in precommit hook --- .pre-commit-hooks.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index d0c1a44e3..0027e49df 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -4,3 +4,4 @@ require_serial: true language: python types: [python] + args: ['--filter-files'] From d16a72f750b8d523357b5535c62e95b603dfa794 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 28 Jul 2020 22:48:42 -0700 Subject: [PATCH 0874/1439] Add recursive symlink protection --- CHANGELOG.md | 6 +++++- isort/main.py | 15 +++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3c820eea..d91f51254 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,12 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. -### 5.3.0 TBD +### 5.2.1 July 28, 2020 + - Update precommit to default to filtering files that are defined in skip. + - Improved relative path detection for `skip` config usage. + - Added recursive symbolic link protection. - Implemented #1177: Support for color output using `--color`. + - Implemented recursive symlink detection support. ### 5.2.0 July 27, 2020 - Implemented #1335: Official API for diff capturing. diff --git a/isort/main.py b/isort/main.py index 413388721..99a37b07e 100644 --- a/isort/main.py +++ b/isort/main.py @@ -8,7 +8,7 @@ import sys from io import TextIOWrapper from pathlib import Path -from typing import Any, Dict, Iterable, Iterator, List, Optional, Sequence +from typing import Any, Dict, Iterable, Iterator, List, Optional, Sequence, Set from warnings import warn from . import __version__, api, sections @@ -107,14 +107,25 @@ def sort_imports( def iter_source_code(paths: Iterable[str], config: Config, skipped: List[str]) -> Iterator[str]: """Iterate over all Python source files defined in paths.""" + visited_dirs: Set[Path] = set() + for path in paths: if os.path.isdir(path): for dirpath, dirnames, filenames in os.walk(path, topdown=True, followlinks=True): base_path = Path(dirpath) for dirname in list(dirnames): - if config.is_skipped(base_path / dirname): + full_path = base_path / dirname + if config.is_skipped(full_path): skipped.append(dirname) dirnames.remove(dirname) + + resolved_path = full_path.resolve() + if resolved_path in visited_dirs: # pragma: no cover + warn(f"Likely recursive symlink detected to {resolved_path}") + dirnames.remove(dirname) + else: + visited_dirs.add(resolved_path) + for filename in filenames: filepath = os.path.join(dirpath, filename) if is_python_file(filepath): From 444c4ee0f6be426ab6ce51c8385a9dbc7ef6374b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 28 Jul 2020 22:49:06 -0700 Subject: [PATCH 0875/1439] Prepare 5.2.1 release --- isort/_version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/_version.py b/isort/_version.py index 6c235c596..98886d260 100644 --- a/isort/_version.py +++ b/isort/_version.py @@ -1 +1 @@ -__version__ = "5.2.0" +__version__ = "5.2.1" diff --git a/pyproject.toml b/pyproject.toml index 267750271..d832c48c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ line-length = 100 [tool.poetry] name = "isort" -version = "5.2.0" +version = "5.2.1" description = "A Python utility / library to sort Python imports." authors = ["Timothy Crosley "] license = "MIT" From 107bf48c884a15ae5de8818c8e0ad71621291339 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 28 Jul 2020 23:13:00 -0700 Subject: [PATCH 0876/1439] Add custom isort documentation theme --- art/stylesheets/extra.css | 5 +++++ pyproject.toml | 5 +++-- 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 art/stylesheets/extra.css diff --git a/art/stylesheets/extra.css b/art/stylesheets/extra.css new file mode 100644 index 000000000..499655d23 --- /dev/null +++ b/art/stylesheets/extra.css @@ -0,0 +1,5 @@ +[data-md-color-scheme="isort"] { + --md-primary-fg-color: #EF8336; + --md-primary-fg-color--light: #1674B1; + --md-primary-fg-color--dark: #1674b1; +} diff --git a/pyproject.toml b/pyproject.toml index d832c48c2..4cebbacc1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -90,13 +90,14 @@ isort = "isort.main:ISortCommand" isort = "isort = isort.pylama_isort:Linter" [tool.portray.mkdocs] -edit_uri="https://github.com/timothycrosley/isort/edit/develop/" +edit_uri = "https://github.com/timothycrosley/isort/edit/develop/" +extra_css = ["art/stylesheets/extra.css"] [tool.portray.mkdocs.theme] name = "material" favicon = "art/logo.png" logo = "art/logo.png" -palette = {primary = "orange", accent = "indigo"} +palette = {scheme = "isort"} [build-system] requires = ["poetry>=0.12"] From 212e1609eaeb5a86496ded5c355843e0979936fc Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 29 Jul 2020 21:43:04 -0700 Subject: [PATCH 0877/1439] Update isort config --- .isort.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.isort.cfg b/.isort.cfg index 1e3f216fb..3f3b28ad5 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -1,4 +1,4 @@ [settings] profile=hug src_paths=isort,test -skip=tests/example_crlf_file.py,migrations +skip=tests/example_crlf_file.py From b8c8c00dbccca7d2c3d4939d75afe4469e29213f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 29 Jul 2020 21:51:07 -0700 Subject: [PATCH 0878/1439] Add precommit section to upgrade guide --- docs/upgrade_guides/5.0.0.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/upgrade_guides/5.0.0.md b/docs/upgrade_guides/5.0.0.md index b3d788ef5..d20efa1d8 100644 --- a/docs/upgrade_guides/5.0.0.md +++ b/docs/upgrade_guides/5.0.0.md @@ -8,6 +8,9 @@ Related documentation: * [isort 5.0.0 changelog](https://timothycrosley.github.io/isort/CHANGELOG/#500-penny-july-4-2020) * [isort 5 release document](https://timothycrosley.github.io/isort/docs/major_releases/introducing_isort_5/) +!!! important - "If you use pre-commit remove seed-isort-config." + If you currently use pre-commit, make sure to see the pre-commit section of this document. In particular, make sure to remove any `seed-isort-config` pre-step. + ## Migrating CLI options ### `--dont-skip` or `-ns` @@ -49,3 +52,22 @@ isort has completely rewritten its logic for placing modules in 5.0.0 to ensure The TL;DR of which is that isort has now changed from `default_section=FIRSTPARTY` to `default_section=THIRDPARTY`. If you all already setting the default section to third party, your config is probably in good shape. If not, you can either use the old finding approach with `--magic-placement` in the CLI or `old_finders=True` in your config, or preferably, you are able to remove all placement options and isort will determine it correctly. If it doesn't, you should be able to just specify your projects modules with `known_first_party` and be done with it. + +## Migrating pre-commit + +### seed-isort-config + +If you have a step in your precommit called `seed-isort-config` or similar, it is highly recommend that you remove this. It is unnecessary in 5.x.x, is guaranteed to slow things down, and worse can conflict with isort's own module placement logic. + +### isort pre-commit step + +isort now includes an optimized precommit configuration in the repo itself. To use it you can replace any existing isort precommit step with: + +``` + - repo: https://github.com/timothycrosley/isort + rev: master + hooks: + - id: isort +``` + +under the `repos` section of your projects `.pre-commit-config.yaml` config. From 39adceea2090acc5af5bb146bafbc951c8689304 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 30 Jul 2020 01:29:31 -0700 Subject: [PATCH 0879/1439] Fixed #1356: return status when arguments are passed in without files or a content stream. --- CHANGELOG.md | 3 +++ isort/main.py | 5 ++++- tests/test_main.py | 10 ++++++++-- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d91f51254..2379e78d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. +### 5.2.2 July 30, 2020 + - Fixed #1356: return status when arguments are passed in without files or a content stream. + ### 5.2.1 July 28, 2020 - Update precommit to default to filtering files that are defined in skip. - Improved relative path detection for `skip` config usage. diff --git a/isort/main.py b/isort/main.py index 99a37b07e..30cf8f4ce 100644 --- a/isort/main.py +++ b/isort/main.py @@ -739,7 +739,10 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = file_names = arguments.pop("files", []) if not file_names and not show_config: print(QUICK_GUIDE) - return + if arguments: + sys.exit(f"Error: arguments passed in without any paths or content.") + else: + return elif file_names == ["-"] and not show_config: arguments.setdefault("settings_path", os.getcwd()) api.sort_stream( diff --git a/tests/test_main.py b/tests/test_main.py index bc4df4e91..1c980c1f0 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -99,12 +99,18 @@ def test_main(capsys, tmpdir): ] tmpdir.mkdir(".git") - # If no files are passed in the quick guide is returned - main.main(base_args) + # If nothing is passed in the quick guide is returned without erroring + main.main([]) out, error = capsys.readouterr() assert main.QUICK_GUIDE in out assert not error + # If no files are passed in but arguments are the quick guide is returned, alongside an error. + with pytest.raises(SystemExit): + main.main(base_args) + out, error = capsys.readouterr() + assert main.QUICK_GUIDE in out + # Unless the config is requested, in which case it will be returned alone as JSON main.main(base_args + ["--show-config"]) out, error = capsys.readouterr() From 253b9476c6c6fd8973c9fd9f8928d6c802481342 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 30 Jul 2020 01:30:59 -0700 Subject: [PATCH 0880/1439] Bump version number --- isort/_version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/_version.py b/isort/_version.py index 98886d260..15cf13501 100644 --- a/isort/_version.py +++ b/isort/_version.py @@ -1 +1 @@ -__version__ = "5.2.1" +__version__ = "5.2.2" diff --git a/pyproject.toml b/pyproject.toml index 4cebbacc1..8ceb3d959 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ line-length = 100 [tool.poetry] name = "isort" -version = "5.2.1" +version = "5.2.2" description = "A Python utility / library to sort Python imports." authors = ["Timothy Crosley "] license = "MIT" From efd43522b7c553e6ff33f8e04604af5eeae7b1ad Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 30 Jul 2020 01:35:36 -0700 Subject: [PATCH 0881/1439] Fix linting error, needless f string --- isort/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/main.py b/isort/main.py index 30cf8f4ce..3f25b1e37 100644 --- a/isort/main.py +++ b/isort/main.py @@ -740,7 +740,7 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = if not file_names and not show_config: print(QUICK_GUIDE) if arguments: - sys.exit(f"Error: arguments passed in without any paths or content.") + sys.exit("Error: arguments passed in without any paths or content.") else: return elif file_names == ["-"] and not show_config: From 6ba0b3de69e3dadbd3de43cd864599fe3cdc336c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 30 Jul 2020 21:48:55 -0700 Subject: [PATCH 0882/1439] Resolve #1357: Add ability to treat all or select comments as code --- CHANGELOG.md | 3 ++ isort/api.py | 11 +++- isort/main.py | 12 +++++ isort/parse.py | 4 ++ isort/settings.py | 2 + tests/test_ticketed_features.py | 95 +++++++++++++++++++++++++++++++++ 6 files changed, 125 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2379e78d5..aa41fcf80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. +### 5.3.0 TBD + - Implemented ability to treat all or select comments as code (issue #1357) + ### 5.2.2 July 30, 2020 - Fixed #1356: return status when arguments are passed in without files or a content stream. diff --git a/isort/api.py b/isort/api.py index 9f849d646..91ff2866e 100644 --- a/isort/api.py +++ b/isort/api.py @@ -438,7 +438,9 @@ def _sort_imports( if line == "# isort: on\n": isort_off = False new_input += line - elif line in ("# isort: split\n", "# isort: off\n", None): + elif line in ("# isort: split\n", "# isort: off\n", None) or str(line).endswith( + "# isort: split\n" + ): if line == "# isort: off\n": isort_off = True if current: @@ -448,9 +450,12 @@ def _sort_imports( extra_space += "\n" current = current[:-1] extra_space = extra_space.replace("\n", "", 1) - new_input += output.sorted_imports( + sorted_output = output.sorted_imports( parsed, config, extension, import_type="import" ) + if sorted_output.strip() != output.strip(): + made_changes = True + new_input += sorted_output new_input += extra_space current = "" new_input += line or "" @@ -541,6 +546,8 @@ def _sort_imports( not stripped_line or stripped_line.startswith("#") and (not indent or indent + line.lstrip() == line) + and not config.treat_all_comments_as_code + and stripped_line not in config.treat_comments_as_code ): import_section += line elif stripped_line.startswith(IMPORT_START_IDENTIFIERS): diff --git a/isort/main.py b/isort/main.py index 3f25b1e37..8ba5dba06 100644 --- a/isort/main.py +++ b/isort/main.py @@ -641,6 +641,18 @@ def _build_arg_parser() -> argparse.ArgumentParser: "Still it can be a great shortcut for collecting imports every once in a while when you put" " them in the middle of a file.", ) + parser.add_argument( + "--treat-comment-as-code", + dest="treat_comments_as_code", + action="append", + help="Tells isort to treat the specified single line comment(s) as if they are code.", + ) + parser.add_argument( + "--treat-all-comment-as-code", + dest="treat_all_comments_as_code", + action="store_true", + help="Tells isort to treat all single line comments as if they are code.", + ) parser.add_argument( "--formatter", dest="formatter", diff --git a/isort/parse.py b/isort/parse.py index b86802c36..27c785c76 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -367,6 +367,8 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte and not last.endswith("'''") and "isort:imports-" not in last and "isort: imports-" not in last + and not config.treat_all_comments_as_code + and not last.strip() in config.treat_comments_as_code ): categorized_comments["above"]["from"].setdefault(import_from, []).insert( 0, out_lines.pop(-1) @@ -404,6 +406,8 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte and not last.endswith("'''") and "isort:imports-" not in last and "isort: imports-" not in last + and not config.treat_all_comments_as_code + and not last.strip() in config.treat_comments_as_code ): categorized_comments["above"]["straight"].setdefault(module, []).insert( 0, out_lines.pop(-1) diff --git a/isort/settings.py b/isort/settings.py index 7376004d8..0f3310077 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -176,6 +176,8 @@ class _Config: formatter: str = "" formatting_function: Optional[Callable[[str, str, object], str]] = None color_output: bool = False + treat_comments_as_code: FrozenSet[str] = frozenset() + treat_all_comments_as_code: bool = False def __post_init__(self): py_version = self.py_version diff --git a/tests/test_ticketed_features.py b/tests/test_ticketed_features.py index bfa1be5c4..72109063d 100644 --- a/tests/test_ticketed_features.py +++ b/tests/test_ticketed_features.py @@ -231,3 +231,98 @@ def test_isort_supports_formatting_plugins_issue_1353(): assert isort.code("import a", formatter="example") == "import a\n" # formatting plugin with pytest.raises(exceptions.FormattingPluginDoesNotExist): assert isort.code("import a", formatter="madeupfake") == "import a\n" # non-existent plugin + + +def test_treating_comments_as_code_issue_1357(): + """Test to ensure isort provides a way to treat comments as code. + See: https://github.com/timothycrosley/isort/issues/1357 + """ + assert isort.code("""# %% +import numpy as np +np.array([1,2,3]) + +# %% +import pandas as pd +pd.Series([1,2,3]) + +# %% +# This is a comment on the second import +import pandas as pd +pd.Series([4,5,6])""", treat_comments_as_code=["# comment1", "# %%"]) == """# %% +import numpy as np + +np.array([1,2,3]) + +# %% +import pandas as pd + +pd.Series([1,2,3]) + +# %% +# This is a comment on the second import +import pandas as pd + +pd.Series([4,5,6]) +""" + assert isort.code("""# %% +import numpy as np +np.array([1,2,3]) + +# %% +import pandas as pd +pd.Series([1,2,3]) + +# %% +# This is a comment on the second import +import pandas as pd +pd.Series([4,5,6])""", treat_comments_as_code=["# comment1", "# %%"], float_to_top=True) == """# %% +import numpy as np +# This is a comment on the second import +import pandas as pd + +np.array([1,2,3]) + +# %% +pd.Series([1,2,3]) + +# %% +pd.Series([4,5,6]) +""" + assert isort.code("""# %% +import numpy as np +np.array([1,2,3]) + +# %% +import pandas as pd +pd.Series([1,2,3]) + +# %% +# This is a comment on the second import +import pandas as pd +pd.Series([4,5,6])""", treat_all_comments_as_code=True) == """# %% +import numpy as np + +np.array([1,2,3]) + +# %% +import pandas as pd + +pd.Series([1,2,3]) + +# %% +# This is a comment on the second import +import pandas as pd + +pd.Series([4,5,6]) +""" + assert isort.code("""import b + +# these are special imports that have to do with installing X plugin +import c +import a +""", treat_all_comments_as_code=True) == """import b + +# these are special imports that have to do with installing X plugin +import a +import c +""" From a177a981bd94cff40d5d6ad3f2e1b0214107c74b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 30 Jul 2020 21:53:59 -0700 Subject: [PATCH 0883/1439] black formatting --- tests/test_ticketed_features.py | 41 ++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/tests/test_ticketed_features.py b/tests/test_ticketed_features.py index 72109063d..9aab3b1d3 100644 --- a/tests/test_ticketed_features.py +++ b/tests/test_ticketed_features.py @@ -237,7 +237,9 @@ def test_treating_comments_as_code_issue_1357(): """Test to ensure isort provides a way to treat comments as code. See: https://github.com/timothycrosley/isort/issues/1357 """ - assert isort.code("""# %% + assert ( + isort.code( + """# %% import numpy as np np.array([1,2,3]) @@ -248,7 +250,10 @@ def test_treating_comments_as_code_issue_1357(): # %% # This is a comment on the second import import pandas as pd -pd.Series([4,5,6])""", treat_comments_as_code=["# comment1", "# %%"]) == """# %% +pd.Series([4,5,6])""", + treat_comments_as_code=["# comment1", "# %%"], + ) + == """# %% import numpy as np np.array([1,2,3]) @@ -264,7 +269,10 @@ def test_treating_comments_as_code_issue_1357(): pd.Series([4,5,6]) """ - assert isort.code("""# %% + ) + assert ( + isort.code( + """# %% import numpy as np np.array([1,2,3]) @@ -275,7 +283,11 @@ def test_treating_comments_as_code_issue_1357(): # %% # This is a comment on the second import import pandas as pd -pd.Series([4,5,6])""", treat_comments_as_code=["# comment1", "# %%"], float_to_top=True) == """# %% +pd.Series([4,5,6])""", + treat_comments_as_code=["# comment1", "# %%"], + float_to_top=True, + ) + == """# %% import numpy as np # This is a comment on the second import import pandas as pd @@ -288,7 +300,10 @@ def test_treating_comments_as_code_issue_1357(): # %% pd.Series([4,5,6]) """ - assert isort.code("""# %% + ) + assert ( + isort.code( + """# %% import numpy as np np.array([1,2,3]) @@ -299,7 +314,10 @@ def test_treating_comments_as_code_issue_1357(): # %% # This is a comment on the second import import pandas as pd -pd.Series([4,5,6])""", treat_all_comments_as_code=True) == """# %% +pd.Series([4,5,6])""", + treat_all_comments_as_code=True, + ) + == """# %% import numpy as np np.array([1,2,3]) @@ -315,14 +333,21 @@ def test_treating_comments_as_code_issue_1357(): pd.Series([4,5,6]) """ - assert isort.code("""import b + ) + assert ( + isort.code( + """import b # these are special imports that have to do with installing X plugin import c import a -""", treat_all_comments_as_code=True) == """import b +""", + treat_all_comments_as_code=True, + ) + == """import b # these are special imports that have to do with installing X plugin import a import c """ + ) From b668b554fbd8e1eb93c31d27eaba5ba78d9a0519 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 30 Jul 2020 22:04:53 -0700 Subject: [PATCH 0884/1439] Fix error detected by linting in diff logic --- isort/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/api.py b/isort/api.py index 91ff2866e..2c630adb7 100644 --- a/isort/api.py +++ b/isort/api.py @@ -453,7 +453,7 @@ def _sort_imports( sorted_output = output.sorted_imports( parsed, config, extension, import_type="import" ) - if sorted_output.strip() != output.strip(): + if sorted_output.strip() != current.strip(): made_changes = True new_input += sorted_output new_input += extra_space From 3827f46f73d610b018f7a35ffe6db212aa4c5cde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20=C5=A0korpil?= Date: Fri, 31 Jul 2020 13:31:42 +0200 Subject: [PATCH 0885/1439] Fix max_line_length parsing --- isort/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/settings.py b/isort/settings.py index 0f3310077..d6bc5fd55 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -587,7 +587,7 @@ def _get_config_data(file_path: str, sections: Tuple[str]) -> Dict[str, Any]: settings["indent"] = "\t" * (indent_size and int(indent_size) or 1) max_line_length = settings.pop("max_line_length", "").strip() - if max_line_length: + if (max_line_length and (max_line_length == "off" or isinstance(max_line_length, int) or max_line_length.isdigit())): settings["line_length"] = ( float("inf") if max_line_length == "off" else int(max_line_length) ) From 314c6e70a93c37b065b7c0f4f4903097fbce4b36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20=C5=A0korpil?= Date: Fri, 31 Jul 2020 13:42:52 +0200 Subject: [PATCH 0886/1439] Simplify condition --- isort/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/settings.py b/isort/settings.py index d6bc5fd55..f3a68ab2d 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -587,7 +587,7 @@ def _get_config_data(file_path: str, sections: Tuple[str]) -> Dict[str, Any]: settings["indent"] = "\t" * (indent_size and int(indent_size) or 1) max_line_length = settings.pop("max_line_length", "").strip() - if (max_line_length and (max_line_length == "off" or isinstance(max_line_length, int) or max_line_length.isdigit())): + if (max_line_length and (max_line_length == "off" or max_line_length.isdigit())): settings["line_length"] = ( float("inf") if max_line_length == "off" else int(max_line_length) ) From b882956600e0d119bf9a2c4d6e40b0a94712eca0 Mon Sep 17 00:00:00 2001 From: Zac-HD Date: Fri, 31 Jul 2020 23:13:11 +1000 Subject: [PATCH 0887/1439] Add dev dep. on Hypothesmith --- poetry.lock | 78 ++++++++++++++++++++++++++++++++++++++++++++++++-- pyproject.toml | 1 + 2 files changed, 76 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index 56f2cc577..8baff3a61 100644 --- a/poetry.lock +++ b/poetry.lock @@ -474,7 +474,7 @@ description = "A library for property-based testing" name = "hypothesis" optional = false python-versions = ">=3.5.2" -version = "5.23.2" +version = "5.23.8" [package.dependencies] attrs = ">=19.2.0" @@ -506,6 +506,19 @@ pydantic = ">=0.32.2" [package.extras] pytest = ["pytest (>=4.0.0,<5.0.0)"] +[[package]] +category = "dev" +description = "Hypothesis strategies for generating Python programs, something like CSmith" +name = "hypothesmith" +optional = false +python-versions = ">=3.6" +version = "0.1.3" + +[package.dependencies] +hypothesis = ">=5.23.7" +lark-parser = ">=0.7.2" +libcst = ">=0.3.8" + [[package]] category = "main" description = "Internationalized Domain Names in Applications (IDNA)" @@ -629,6 +642,37 @@ optional = false python-versions = ">=3.6" version = "0.16.0" +[[package]] +category = "dev" +description = "a modern parsing library" +name = "lark-parser" +optional = false +python-versions = "*" +version = "0.9.0" + +[package.extras] +regex = ["regex"] + +[[package]] +category = "dev" +description = "A concrete syntax tree with AST-like properties for Python 3.5, 3.6, 3.7 and 3.8 programs." +name = "libcst" +optional = false +python-versions = ">=3.6" +version = "0.3.8" + +[package.dependencies] +pyyaml = ">=5.2" +typing-extensions = ">=3.7.4.2" +typing-inspect = ">=0.4.0" + +[package.dependencies.dataclasses] +python = "<3.7" +version = "*" + +[package.extras] +dev = ["black", "codecov", "coverage", "hypothesis (>=4.36.0)", "hypothesmith (>=0.0.4)", "isort", "flake8", "jupyter", "nbsphinx", "pyre-check", "sphinx", "sphinx-rtd-theme"] + [[package]] category = "dev" description = "Python LiveReload is an awesome tool for web developers" @@ -1482,6 +1526,18 @@ optional = false python-versions = "*" version = "3.7.4.2" +[[package]] +category = "dev" +description = "Runtime inspection utilities for typing module." +name = "typing-inspect" +optional = false +python-versions = "*" +version = "0.6.0" + +[package.dependencies] +mypy-extensions = ">=0.3.0" +typing-extensions = ">=3.7.4" + [[package]] category = "main" description = "HTTP library with thread-safe connection pooling, file post, and more." @@ -1796,13 +1852,17 @@ hyperframe = [ {file = "hyperframe-5.2.0.tar.gz", hash = "sha256:a9f5c17f2cc3c719b917c4f33ed1c61bd1f8dfac4b1bd23b7c80b3400971b41f"}, ] hypothesis = [ - {file = "hypothesis-5.23.2-py3-none-any.whl", hash = "sha256:30f78de476f4db986570c43ef3a5c6ba4666bdf3a925e2bf7835215a8b08fe0f"}, - {file = "hypothesis-5.23.2.tar.gz", hash = "sha256:d4048800a2f7b76f81baa9a31c1f102f18e7e37a9c038995e59d5f977e25003b"}, + {file = "hypothesis-5.23.8-py3-none-any.whl", hash = "sha256:3d40ba66042b15d6653fb0360728007144f7e540efdfe0c81cdd38587d34583b"}, + {file = "hypothesis-5.23.8.tar.gz", hash = "sha256:772e85dd18c9670891e0da1b585c6fb5cffb498a681a5feca94086a70bcef07a"}, ] hypothesis-auto = [ {file = "hypothesis-auto-1.1.4.tar.gz", hash = "sha256:5e2c2fb09dc09842512d80630bb792359a1d33d2c0473ad47ee23da0be9e32b1"}, {file = "hypothesis_auto-1.1.4-py3-none-any.whl", hash = "sha256:fea8560c4522c0fd490ed8cc17e420b95dabebb11b9b334c59bf2d768839015f"}, ] +hypothesmith = [ + {file = "hypothesmith-0.1.3-py3-none-any.whl", hash = "sha256:aceb0feae6029eeaa4502cd763debec313b1aec43db8805958e5a81036c3e483"}, + {file = "hypothesmith-0.1.3.tar.gz", hash = "sha256:4cf1e2ce43407ad1c9c2ab5a940760db3ea8c3c29134435bc0600f33a4a32de4"}, +] idna = [ {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, @@ -1849,6 +1909,13 @@ joblib = [ {file = "joblib-0.16.0-py3-none-any.whl", hash = "sha256:d348c5d4ae31496b2aa060d6d9b787864dd204f9480baaa52d18850cb43e9f49"}, {file = "joblib-0.16.0.tar.gz", hash = "sha256:8f52bf24c64b608bf0b2563e0e47d6fcf516abc8cfafe10cfd98ad66d94f92d6"}, ] +lark-parser = [ + {file = "lark-parser-0.9.0.tar.gz", hash = "sha256:9e7589365d6b6de1cca40b0eaec31104a3fb96a37a11a9dfd5098e95b50aa6cd"}, +] +libcst = [ + {file = "libcst-0.3.8-py3-none-any.whl", hash = "sha256:d514cd36e8da5e1444ec693b65a4b6751781af02496f9a2442c8590eb0d321fc"}, + {file = "libcst-0.3.8.tar.gz", hash = "sha256:484fc3bf0b9b15773349548a466a36b137fbd94705fac8cdf25734fd8261fa17"}, +] livereload = [ {file = "livereload-2.6.2.tar.gz", hash = "sha256:d1eddcb5c5eb8d2ca1fa1f750e580da624c0f7fcb734aa5780dc81b7dcbd89be"}, ] @@ -2258,6 +2325,11 @@ typing-extensions = [ {file = "typing_extensions-3.7.4.2-py3-none-any.whl", hash = "sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5"}, {file = "typing_extensions-3.7.4.2.tar.gz", hash = "sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae"}, ] +typing-inspect = [ + {file = "typing_inspect-0.6.0-py2-none-any.whl", hash = "sha256:de08f50a22955ddec353876df7b2545994d6df08a2f45d54ac8c05e530372ca0"}, + {file = "typing_inspect-0.6.0-py3-none-any.whl", hash = "sha256:3b98390df4d999a28cf5b35d8b333425af5da2ece8a4ea9e98f71e7591347b4f"}, + {file = "typing_inspect-0.6.0.tar.gz", hash = "sha256:8f1b1dd25908dbfd81d3bebc218011531e7ab614ba6e5bf7826d887c834afab7"}, +] urllib3 = [ {file = "urllib3-1.25.10-py2.py3-none-any.whl", hash = "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"}, {file = "urllib3-1.25.10.tar.gz", hash = "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a"}, diff --git a/pyproject.toml b/pyproject.toml index 8ceb3d959..29eb061f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,6 +62,7 @@ pytest-cov = "^2.7" pytest-mock = "^1.10" pep8-naming = "^0.8.2" hypothesis-auto = { version = "^1.0.0" } +hypothesmith = "^0.1.3" examples = { version = "^1.0.0" } cruft = { version = "^1.1" } portray = { version = "^1.3.0" } From 30b46778c824da53428d889981454e36ddee4a92 Mon Sep 17 00:00:00 2001 From: Zac-HD Date: Fri, 31 Jul 2020 23:13:27 +1000 Subject: [PATCH 0888/1439] Hypothesmith property test --- docs/contributing/4.-acknowledgements.md | 1 + tests/test_hypothesmith.py | 93 ++++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 tests/test_hypothesmith.py diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index 875baa933..3aff4bdbb 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -184,6 +184,7 @@ Code Contributors - @nicolelodeon - Łukasz Langa (@ambv) - Grzegorz Pstrucha (@Gricha) +- Zac Hatfield-Dodds (@Zac-HD) Documenters diff --git a/tests/test_hypothesmith.py b/tests/test_hypothesmith.py new file mode 100644 index 000000000..017994ad5 --- /dev/null +++ b/tests/test_hypothesmith.py @@ -0,0 +1,93 @@ +import ast +from typing import get_type_hints + +import hypothesis +import libcst +from hypothesis import strategies as st +from hypothesmith import from_grammar, from_node + +import isort + + +def _as_config(kw) -> isort.Config: + if "wrap_length" in kw and "line_length" in kw: + kw["wrap_length"], kw["line_length"] = sorted([kw["wrap_length"], kw["line_length"]]) + try: + return isort.Config(**kw) + except ValueError: + kw["wrap_length"] = 0 + return isort.Config(**kw) + + +def _record_targets(code: str, prefix: str = "") -> str: + # target larger inputs - the Hypothesis engine will do a multi-objective + # hill-climbing search using these scores to generate 'better' examples. + nodes = list(ast.walk(ast.parse(code))) + import_nodes = [n for n in nodes if isinstance(n, (ast.Import, ast.ImportFrom))] + uniq_nodes = {type(n) for n in nodes} + for value, label in [ + (len(import_nodes), "total number of import nodes"), + (len(uniq_nodes), "number of unique ast node types"), + ]: + hypothesis.target(float(value), label=prefix + label) + return code + + +def configs(**force_strategies: st.SearchStrategy) -> st.SearchStrategy[isort.Config]: + """Generate arbitrary Config objects.""" + skip = { + "line_ending", + "sections", + "known_future_library", + "forced_separate", + "lines_after_imports", + "lines_between_sections", + "lines_between_types", + "sources", + "virtual_env", + "conda_env", + "directory", + "formatter", + "formatting_function", + } + inferred_kwargs = { + k: st.from_type(v) + for k, v in get_type_hints(isort.settings._Config).items() + if k not in skip + } + specific = { + "line_length": st.integers(0, 200), + "wrap_length": st.integers(0, 200), + "indent": st.integers(0, 20).map(lambda n: n * " "), + "default_section": st.sampled_from(sorted(isort.settings.KNOWN_SECTION_MAPPING)), + "force_grid_wrap": st.integers(0, 20), + "profile": st.sampled_from(sorted(isort.settings.profiles)), + "py_version": st.sampled_from(("auto",) + isort.settings.VALID_PY_TARGETS), + } + kwargs = {**inferred_kwargs, **specific, **force_strategies} + return st.fixed_dictionaries({}, optional=kwargs).map(_as_config) + + +st.register_type_strategy(isort.Config, configs()) + + +@hypothesis.example("import A\nimportA\r\n\n", isort.Config(), False) +@hypothesis.given( + source_code=st.lists( + from_grammar(auto_target=False) + | from_node(auto_target=False) + | from_node(libcst.Import, auto_target=False) + | from_node(libcst.ImportFrom, auto_target=False), + min_size=1, + max_size=10, + ).map("\n".join), + config=st.builds(isort.Config), + disregard_skip=st.booleans(), +) +@hypothesis.settings(suppress_health_check=[hypothesis.HealthCheck.too_slow]) +def test_isort_is_idempotent(source_code: str, config: isort.Config, disregard_skip: bool) -> None: + # NOTE: if this test finds a bug, please notify @Zac-HD so that it can be added to the + # Hypothesmith trophy case. This really helps with research impact evaluations! + _record_targets(source_code) + result = isort.code(source_code, config=config, disregard_skip=disregard_skip) + assert result == isort.code(result, config=config, disregard_skip=disregard_skip) From 51db36d29597b736169211b6f55e5a6ad477affb Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 31 Jul 2020 22:14:54 -0700 Subject: [PATCH 0889/1439] Improved handling of deprecated single line variables for usage with Visual Studio Code (issue #1363) --- CHANGELOG.md | 1 + isort/main.py | 43 +++++++++++++++++++++++++++++++++++++++++++ tests/test_main.py | 2 +- 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa41fcf80..18ac1a36b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. ### 5.3.0 TBD - Implemented ability to treat all or select comments as code (issue #1357) + - Improved handling of deprecated single line variables for usage with Visual Studio Code (issue #1363) ### 5.2.2 July 30, 2020 - Fixed #1356: return status when arguments are passed in without files or a content stream. diff --git a/isort/main.py b/isort/main.py index 8ba5dba06..e26a56262 100644 --- a/isort/main.py +++ b/isort/main.py @@ -23,6 +23,35 @@ pass shebang_re = re.compile(br"^#!.*\bpython[23w]?\b") +DEPRECATED_SINGLE_DASH_ARGS = { + "-ac", + "-af", + "-ca", + "-cs", + "-df", + "-ds", + "-dt", + "-fas", + "-fass", + "-ff", + "-fgw", + "-fss", + "-lai", + "-lbt", + "-le", + "-ls", + "-nis", + "-nlb", + "-ot", + "-rr", + "-sd", + "-sg", + "-sl", + "-sp", + "-tc", + "-wl", + "-ws", +} QUICK_GUIDE = f""" {ASCII_ART} @@ -696,10 +725,24 @@ def _build_arg_parser() -> argparse.ArgumentParser: const="--keep-direct-and-as", help=argparse.SUPPRESS, ) + return parser def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: + argv = sys.argv[1:] if argv is None else list(argv) + for index, arg in enumerate(argv): + if arg in DEPRECATED_SINGLE_DASH_ARGS: + warn( + f"\n\nThe following deprecated single dash CLI flags was used: {arg}!\n" + f"It is being auto translated to -{arg} to maintain backward compatibility.\n" + f"This behavior will be REMOVED in the 6.x.x release and is not guaranteed across" + f" 5.x.x releases.\n\n" + "Please see the 5.0.0 upgrade guide:\n" + "\thttps://timothycrosley.github.io/isort/docs/upgrade_guides/5.0.0/\n" + ) + argv[index] = f"-{arg}" + parser = _build_arg_parser() arguments = {key: value for key, value in vars(parser.parse_args(argv)).items() if value} if "dont_order_by_type" in arguments: diff --git a/tests/test_main.py b/tests/test_main.py index 1c980c1f0..f88f44deb 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -90,7 +90,7 @@ def test_is_python_file_fifo(tmpdir): def test_main(capsys, tmpdir): base_args = [ - "--settings-path", + "-sp", str(tmpdir), "--virtual-env", str(tmpdir), From f616f41c958dac408ec2ad5db26b3acd49021f60 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 31 Jul 2020 23:26:45 -0700 Subject: [PATCH 0890/1439] Improved handling of mixed newline forms within same source file --- CHANGELOG.md | 1 + isort/parse.py | 2 +- tests/test_api.py | 4 ++++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18ac1a36b..b3cc0569c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. ### 5.3.0 TBD - Implemented ability to treat all or select comments as code (issue #1357) - Improved handling of deprecated single line variables for usage with Visual Studio Code (issue #1363) + - Improved handling of mixed newline forms within same source file. ### 5.2.2 July 30, 2020 - Fixed #1356: return status when arguments are passed in without files or a content stream. diff --git a/isort/parse.py b/isort/parse.py index 27c785c76..906b9d790 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -143,7 +143,7 @@ class ParsedContent(NamedTuple): def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedContent: """Parses a python file taking out and categorizing imports.""" line_separator: str = config.line_ending or _infer_line_separator(contents) - in_lines = contents.split(line_separator) + in_lines = contents.splitlines() out_lines = [] original_line_count = len(in_lines) if config.old_finders: diff --git a/tests/test_api.py b/tests/test_api.py index 4cbe37281..1b3ed3701 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -49,3 +49,7 @@ def test_diff_stream() -> None: assert api.sort_stream(StringIO("import b\nimport a\n"), output, show_diff=True) output.seek(0) assert "import a\n import b\n" in output.read() + + +def test_sort_code_string_mixed_newlines(): + assert api.sort_code_string("import A\n\r\nimportA\n\n") == "import A\r\n\r\nimportA\r\n\n" From a0cd9a031aced65e56433011dc9acbca6188c628 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 31 Jul 2020 23:36:22 -0700 Subject: [PATCH 0891/1439] Minor improvement to handling of new lines --- isort/output.py | 5 ++++- isort/parse.py | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/isort/output.py b/isort/output.py index b89f0e30a..a008ee831 100644 --- a/isort/output.py +++ b/isort/output.py @@ -225,7 +225,10 @@ def sorted_imports( new_out_lines.append(line) if line in parsed.import_placements: new_out_lines.extend(parsed.place_imports[parsed.import_placements[line]]) - if len(formatted_output) <= index or formatted_output[index + 1].strip() != "": + if ( + len(formatted_output) <= (index + 1) + or formatted_output[index + 1].strip() != "" + ): new_out_lines.append("") formatted_output = new_out_lines diff --git a/isort/parse.py b/isort/parse.py index 906b9d790..350188a7e 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -144,6 +144,9 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte """Parses a python file taking out and categorizing imports.""" line_separator: str = config.line_ending or _infer_line_separator(contents) in_lines = contents.splitlines() + if contents and contents[-1] in ("\n", "\r"): + in_lines.append("") + out_lines = [] original_line_count = len(in_lines) if config.old_finders: From ca69cf9c9d2a668fc9122ec1ed131d29854d0937 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 31 Jul 2020 23:42:47 -0700 Subject: [PATCH 0892/1439] Update changelog to mention inclusion of hypothesmith --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3cc0569c..7ec24a873 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. - Improved handling of deprecated single line variables for usage with Visual Studio Code (issue #1363) - Improved handling of mixed newline forms within same source file. +Internal Development: + - Initial hypothesmith powered test to help catch unexpected syntax parsing and output errors (thanks @Zac-HD!) + ### 5.2.2 July 30, 2020 - Fixed #1356: return status when arguments are passed in without files or a content stream. From 28ae89244912f006247cae16795852f8748fb212 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 31 Jul 2020 23:51:35 -0700 Subject: [PATCH 0893/1439] Black formatting --- isort/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/settings.py b/isort/settings.py index f3a68ab2d..2ea9f6bca 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -587,7 +587,7 @@ def _get_config_data(file_path: str, sections: Tuple[str]) -> Dict[str, Any]: settings["indent"] = "\t" * (indent_size and int(indent_size) or 1) max_line_length = settings.pop("max_line_length", "").strip() - if (max_line_length and (max_line_length == "off" or max_line_length.isdigit())): + if max_line_length and (max_line_length == "off" or max_line_length.isdigit()): settings["line_length"] = ( float("inf") if max_line_length == "off" else int(max_line_length) ) From b195c83c680b28d7a6c2e4a2bd6ae7b28c00369e Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 31 Jul 2020 23:51:51 -0700 Subject: [PATCH 0894/1439] Supress filter_too_much errors for now --- tests/test_hypothesmith.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_hypothesmith.py b/tests/test_hypothesmith.py index 017994ad5..0db88c36b 100644 --- a/tests/test_hypothesmith.py +++ b/tests/test_hypothesmith.py @@ -84,7 +84,9 @@ def configs(**force_strategies: st.SearchStrategy) -> st.SearchStrategy[isort.Co config=st.builds(isort.Config), disregard_skip=st.booleans(), ) -@hypothesis.settings(suppress_health_check=[hypothesis.HealthCheck.too_slow]) +@hypothesis.settings( + suppress_health_check=[hypothesis.HealthCheck.too_slow, hypothesis.HealthCheck.filter_too_much] +) def test_isort_is_idempotent(source_code: str, config: isort.Config, disregard_skip: bool) -> None: # NOTE: if this test finds a bug, please notify @Zac-HD so that it can be added to the # Hypothesmith trophy case. This really helps with research impact evaluations! From 3a5f5aa95564e87fddc96431749b322e82e61c81 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 31 Jul 2020 23:52:50 -0700 Subject: [PATCH 0895/1439] =?UTF-8?q?Add=20-=20Ji=C5=99=C3=AD=20=C5=A0korp?= =?UTF-8?q?il=20(@JiriSko)=20to=20acknowledgements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/contributing/4.-acknowledgements.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index 3aff4bdbb..2ae2d8172 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -185,6 +185,7 @@ Code Contributors - Łukasz Langa (@ambv) - Grzegorz Pstrucha (@Gricha) - Zac Hatfield-Dodds (@Zac-HD) +- Jiří Škorpil (@JiriSko) Documenters From a0df0a05ad1f250ffffad0a66522a55962a274cb Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 1 Aug 2020 00:30:43 -0700 Subject: [PATCH 0896/1439] Improve documentation on float to top usage --- docs/configuration/options.md | 26 +++++++++++++++++++++++++- docs/upgrade_guides/5.0.0.md | 13 +++++++++++++ isort/main.py | 10 +++++----- 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/docs/configuration/options.md b/docs/configuration/options.md index 60db0ace2..65b70b8cb 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -787,7 +787,9 @@ Tells isort to remove redundant aliases from imports, such as `import os as os`. ## Float To Top -Causes all non indented imports to float to the top of the file having its imports sorted. *NOTE*: This is an **experimental** feature. It currently doesn't work with cimports and is guaranteed to run much slower and use much more memory than the default. Still it can be a great shortcut for collecting imports every once in a while when you put them in the middle of a file. +Causes all non-indented imports to float to the top of the file having its imports sorted. It can be an excellent shortcut for collecting imports every once in a while when you place them in the middle of a file to avoid context switching. + +*NOTE*: It currently doesn't work with cimports and introduces some extra over-head and a performance penalty. **Type:** Bool **Default:** `False` @@ -838,6 +840,28 @@ Tells isort to use color in terminal output. - --color +## Treat Comments As Code + +Tells isort to treat the specified single line comment(s) as if they are code. + +**Type:** Frozenset +**Default:** `frozenset()` +**Python & Config File Name:** treat_comments_as_code +**CLI Flags:** + +- --treat-comment-as-code + +## Treat All Comments As Code + +Tells isort to treat all single line comments as if they are code. + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** treat_all_comments_as_code +**CLI Flags:** + +- --treat-all-comment-as-code + ## Check Checks the file for unsorted / unformatted imports and prints them to the command line without modifying the file. diff --git a/docs/upgrade_guides/5.0.0.md b/docs/upgrade_guides/5.0.0.md index d20efa1d8..d5d518e87 100644 --- a/docs/upgrade_guides/5.0.0.md +++ b/docs/upgrade_guides/5.0.0.md @@ -11,6 +11,16 @@ Related documentation: !!! important - "If you use pre-commit remove seed-isort-config." If you currently use pre-commit, make sure to see the pre-commit section of this document. In particular, make sure to remove any `seed-isort-config` pre-step. +## Imports no Longer Moved to Top + +One of the most immediately evident changes when upgrading to isort 5, is it now avoids moving imports around code by default. +The great thing about this is that it means that isort can safely run against complex code bases that need to place side effects between import sections without needing any comments, flags, or configs. It's also part of the rearchitecting that allows it to sort within type checking conditionals and functions. However, it can be a jarring change +for those of us who have gotten used to placing imports right above their usage in code to avoid context switching. No need to worry! isort still supports this work mode. + +If you want to move all imports to the top, you can use the new`--float-to-top` flag in the CLI or `float_to_top=true` option in your config file. + +See: [https://timothycrosley.github.io/isort/docs/configuration/options/#float-to-top](https://timothycrosley.github.io/isort/docs/configuration/options/#float-to-top) + ## Migrating CLI options ### `--dont-skip` or `-ns` @@ -29,6 +39,9 @@ The option was originally added to allow working around this, and was then turne ### `-ac`, `-wl`, `-ws`, `-tc`, `-sp`, `-sp`, `-sl`, `-sg`, `-sd`, `-rr`, `-ot`, `-nlb`, `-nis`, `-ls`, `-le`, `-lbt`, `-lai`, `-fss`, `-fgw`, `-ff`, `-fass`, `-fas`, `-dt`, `-ds`, `-df`, `-cs`, `-ca`, `-af`, `-ac` Two-letter shortened setting names (like `ac` for `atomic`) now require two dashes to avoid ambiguity. Simply add another dash before the option, or switch to the long form option to fix (example: `--ac` or `--atomic`). +### `-v` and `-V` +The `-v` (previously for version now for verbose) and `-V` (previously for verbose and now for version) options have been swapped to be more consistent with tools across the CLI and in particular Python ecosystem. + ## Migrating Config options The first thing to keep in mind is how isort loads config options has changed in isort 5. It will no longer merge multiple config files, instead you must have 1 isort config per a project. diff --git a/isort/main.py b/isort/main.py index e26a56262..bf93036cc 100644 --- a/isort/main.py +++ b/isort/main.py @@ -664,11 +664,11 @@ def _build_arg_parser() -> argparse.ArgumentParser: "--float-to-top", dest="float_to_top", action="store_true", - help="Causes all non indented imports to float to the top of the file having its imports " - "sorted. *NOTE*: This is an **experimental** feature. It currently doesn't work with " - "cimports and is guaranteed to run much slower and use much more memory than the default. " - "Still it can be a great shortcut for collecting imports every once in a while when you put" - " them in the middle of a file.", + help="Causes all non-indented imports to float to the top of the file having its imports " + "sorted. It can be an excellent shortcut for collecting imports every once in a while " + "when you place them in the middle of a file to avoid context switching.\n\n" + "*NOTE*: It currently doesn't work with cimports and introduces some extra over-head " + "and a performance penalty." ) parser.add_argument( "--treat-comment-as-code", From 146c3debeb9fa78eeba86efc4d9e143e41be6710 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 1 Aug 2020 00:47:09 -0700 Subject: [PATCH 0897/1439] Formatting --- isort/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/main.py b/isort/main.py index bf93036cc..781690ce1 100644 --- a/isort/main.py +++ b/isort/main.py @@ -668,7 +668,7 @@ def _build_arg_parser() -> argparse.ArgumentParser: "sorted. It can be an excellent shortcut for collecting imports every once in a while " "when you place them in the middle of a file to avoid context switching.\n\n" "*NOTE*: It currently doesn't work with cimports and introduces some extra over-head " - "and a performance penalty." + "and a performance penalty.", ) parser.add_argument( "--treat-comment-as-code", From d15c2c3a9599e4ba9f3ee3c01cf6cc7cf852a626 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 1 Aug 2020 22:40:17 -0700 Subject: [PATCH 0898/1439] Implemented ability to treat all or select comments as code (issue #1357) --- CHANGELOG.md | 1 + docs/configuration/options.md | 26 ++++++++++++++++++- isort/main.py | 47 ++++++++++++----------------------- isort/settings.py | 34 +++++++++++++++++++++++-- tests/test_hypothesmith.py | 1 + tests/test_isort.py | 23 ----------------- tests/test_main.py | 18 -------------- tests/test_settings.py | 44 ++++++++++++++++++++++++++++++++ 8 files changed, 119 insertions(+), 75 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ec24a873..3f24a3b23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. ### 5.3.0 TBD - Implemented ability to treat all or select comments as code (issue #1357) + - Implemented ability to use different configs for different file extensions (issue #1162) - Improved handling of deprecated single line variables for usage with Visual Studio Code (issue #1363) - Improved handling of mixed newline forms within same source file. diff --git a/docs/configuration/options.md b/docs/configuration/options.md index 65b70b8cb..01ac61280 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -143,7 +143,7 @@ Force isort to recognize a module as part of the future compatibility libraries. Force isort to recognize a module as being part of a third party library. **Type:** Frozenset -**Default:** `('google.appengine.api',)` +**Default:** `frozenset()` **Python & Config File Name:** known_third_party **CLI Flags:** @@ -862,6 +862,30 @@ Tells isort to treat all single line comments as if they are code. - --treat-all-comment-as-code +## Supported Extensions + +Specifies what extensions isort can be ran against. + +**Type:** Frozenset +**Default:** `('.py', '.pyi', '.pyx')` +**Python & Config File Name:** supported_extensions +**CLI Flags:** + +- --ext +- --extension +- --supported-extension + +## Blocked Extensions + +Specifies what extensions isort can never be ran against. + +**Type:** Frozenset +**Default:** `('.pex',)` +**Python & Config File Name:** blocked_extensions +**CLI Flags:** + +- --blocked-extension + ## Check Checks the file for unsorted / unformatted imports and prints them to the command line without modifying the file. diff --git a/isort/main.py b/isort/main.py index 781690ce1..e9155fe0b 100644 --- a/isort/main.py +++ b/isort/main.py @@ -3,8 +3,6 @@ import functools import json import os -import re -import stat import sys from io import TextIOWrapper from pathlib import Path @@ -15,14 +13,13 @@ from .exceptions import FileSkipped from .logo import ASCII_ART from .profiles import profiles -from .settings import SUPPORTED_EXTENSIONS, VALID_PY_TARGETS, Config, WrapModes +from .settings import VALID_PY_TARGETS, Config, WrapModes try: from .setuptools_commands import ISortCommand # noqa: F401 except ImportError: pass -shebang_re = re.compile(br"^#!.*\bpython[23w]?\b") DEPRECATED_SINGLE_DASH_ARGS = { "-ac", "-af", @@ -68,32 +65,6 @@ """ -def is_python_file(path: str) -> bool: - _root, ext = os.path.splitext(path) - if ext in SUPPORTED_EXTENSIONS: - return True - if ext in (".pex",): - return False - - # Skip editor backup files. - if path.endswith("~"): - return False - - try: - if stat.S_ISFIFO(os.stat(path).st_mode): - return False - except OSError: - pass - - try: - with open(path, "rb") as fp: - line = fp.readline(100) - except OSError: - return False - else: - return bool(shebang_re.match(line)) - - class SortAttempt: def __init__(self, incorrectly_sorted: bool, skipped: bool) -> None: self.incorrectly_sorted = incorrectly_sorted @@ -157,7 +128,7 @@ def iter_source_code(paths: Iterable[str], config: Config, skipped: List[str]) - for filename in filenames: filepath = os.path.join(dirpath, filename) - if is_python_file(filepath): + if config.is_supported_filetype(filepath): if config.is_skipped(Path(filepath)): skipped.append(filename) else: @@ -688,6 +659,20 @@ def _build_arg_parser() -> argparse.ArgumentParser: type=str, help="Specifies the name of a formatting plugin to use when producing output.", ) + parser.add_argument( + "--ext", + "--extension", + "--supported-extension", + dest="supported_extensions", + action="append", + help="Specifies what extensions isort can be ran against.", + ) + parser.add_argument( + "--blocked-extension", + dest="blocked_extensions", + action="append", + help="Specifies what extensions isort can never be ran against.", + ) # deprecated options parser.add_argument( diff --git a/isort/settings.py b/isort/settings.py index 2ea9f6bca..37a7e0cfe 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -7,6 +7,7 @@ import os import posixpath import re +import stat import subprocess # nosec: Needed for gitignore support. import sys from functools import lru_cache @@ -24,7 +25,9 @@ from .wrap_modes import WrapModes from .wrap_modes import from_string as wrap_mode_from_string -SUPPORTED_EXTENSIONS = (".py", ".pyi", ".pyx") +_SHEBANG_RE = re.compile(br"^#!.*\bpython[23w]?\b") +SUPPORTED_EXTENSIONS = frozenset({".py", ".pyi", ".pyx"}) +BLOCKED_EXTENSIONS = frozenset({".pex"}) FILE_SKIP_COMMENTS: Tuple[str, ...] = ( "isort:" + "skip_file", "isort: " + "skip_file", @@ -117,7 +120,7 @@ class _Config: sections: Tuple[str, ...] = SECTION_DEFAULTS no_sections: bool = False known_future_library: FrozenSet[str] = frozenset(("__future__",)) - known_third_party: FrozenSet[str] = frozenset(("google.appengine.api",)) + known_third_party: FrozenSet[str] = frozenset() known_first_party: FrozenSet[str] = frozenset() known_local_folder: FrozenSet[str] = frozenset() known_standard_library: FrozenSet[str] = frozenset() @@ -178,6 +181,8 @@ class _Config: color_output: bool = False treat_comments_as_code: FrozenSet[str] = frozenset() treat_all_comments_as_code: bool = False + supported_extensions: FrozenSet[str] = SUPPORTED_EXTENSIONS + blocked_extensions: FrozenSet[str] = BLOCKED_EXTENSIONS def __post_init__(self): py_version = self.py_version @@ -389,6 +394,31 @@ def __init__( super().__init__(sources=tuple(sources), **combined_config) # type: ignore + def is_supported_filetype(self, file_name: str): + _root, ext = os.path.splitext(file_name) + if ext in self.supported_extensions: + return True + elif ext in self.blocked_extensions: + return False + + # Skip editor backup files. + if file_name.endswith("~"): + return False + + try: + if stat.S_ISFIFO(os.stat(file_name).st_mode): + return False + except OSError: + pass + + try: + with open(file_name, "rb") as fp: + line = fp.readline(100) + except OSError: + return False + else: + return bool(_SHEBANG_RE.match(line)) + def is_skipped(self, file_path: Path) -> bool: """Returns True if the file and/or folder should be skipped based on current settings.""" if self.directory and Path(self.directory) in file_path.resolve().parents: diff --git a/tests/test_hypothesmith.py b/tests/test_hypothesmith.py index 0db88c36b..94964cee0 100644 --- a/tests/test_hypothesmith.py +++ b/tests/test_hypothesmith.py @@ -84,6 +84,7 @@ def configs(**force_strategies: st.SearchStrategy) -> st.SearchStrategy[isort.Co config=st.builds(isort.Config), disregard_skip=st.booleans(), ) +@hypothesis.seed(235738473415671197623909623354096762459) @hypothesis.settings( suppress_health_check=[hypothesis.HealthCheck.too_slow, hypothesis.HealthCheck.filter_too_much] ) diff --git a/tests/test_isort.py b/tests/test_isort.py index 68ea177d3..0f7d71365 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -14,7 +14,6 @@ import pytest import isort from isort import main, api, sections -from isort.main import is_python_file from isort.settings import WrapModes, Config from isort.utils import exists_case_sensitive from isort.exceptions import FileSkipped, ExistingSyntaxErrors @@ -3000,28 +2999,6 @@ def test_escaped_no_parens_sort_with_first_comment() -> None: assert isort.code(test_input) == expected -def test_is_python_file_ioerror(tmpdir) -> None: - does_not_exist = tmpdir.join("fake.txt") - assert is_python_file(str(does_not_exist)) is False - - -def test_is_python_file_shebang(tmpdir) -> None: - path = tmpdir.join("myscript") - path.write("#!/usr/bin/env python\n") - assert is_python_file(str(path)) is True - - -def test_is_python_file_editor_backup(tmpdir) -> None: - path = tmpdir.join("myscript~") - path.write("#!/usr/bin/env python\n") - assert is_python_file(str(path)) is False - - -def test_is_python_typing_stub(tmpdir) -> None: - stub = tmpdir.join("stub.pyi") - assert is_python_file(str(stub)) is True - - @pytest.mark.skip(reason="TODO: Duplicates currently not handled.") def test_to_ensure_imports_are_brought_to_top_issue_651() -> None: test_input = ( diff --git a/tests/test_main.py b/tests/test_main.py index f88f44deb..e4572eb43 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,7 +1,5 @@ import json -import os import subprocess -import sys from datetime import datetime from io import BytesIO, TextIOWrapper @@ -37,15 +35,6 @@ def test_sort_imports(tmpdir): assert main.sort_imports(str(tmp_file), config=skip_config, disregard_skip=False).skipped -def test_is_python_file(): - assert main.is_python_file("file.py") - assert main.is_python_file("file.pyi") - assert main.is_python_file("file.pyx") - assert not main.is_python_file("file.pyc") - assert not main.is_python_file("file.txt") - assert not main.is_python_file("file.pex") - - def test_parse_args(): assert main.parse_args([]) == {} assert main.parse_args(["--multi-line", "1"]) == {"multi_line_output": WrapModes.VERTICAL} @@ -81,13 +70,6 @@ def test_preconvert(): main._preconvert(datetime.now()) -@pytest.mark.skipif(sys.platform == "win32", reason="cannot create fifo file on Windows platform") -def test_is_python_file_fifo(tmpdir): - fifo_file = os.path.join(tmpdir, "fifo_file") - os.mkfifo(fifo_file) - assert not main.is_python_file(fifo_file) - - def test_main(capsys, tmpdir): base_args = [ "-sp", diff --git a/tests/test_settings.py b/tests/test_settings.py index b7cb6be76..8da098e22 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -1,3 +1,5 @@ +import os +import sys from pathlib import Path import pytest @@ -7,6 +9,8 @@ class TestConfig: + instance = Config() + def test_init(self): assert Config() @@ -26,6 +30,46 @@ def test_is_skipped(self): assert Config().is_skipped(Path("C:\\path\\isort.py")) assert Config(skip=["/path/isort.py"]).is_skipped(Path("C:\\path\\isort.py")) + def test_is_supported_filetype(self): + assert self.instance.is_supported_filetype("file.py") + assert self.instance.is_supported_filetype("file.pyi") + assert self.instance.is_supported_filetype("file.pyx") + assert not self.instance.is_supported_filetype("file.pyc") + assert not self.instance.is_supported_filetype("file.txt") + assert not self.instance.is_supported_filetype("file.pex") + + def test_is_supported_filetype_ioerror(self, tmpdir): + does_not_exist = tmpdir.join("fake.txt") + assert not self.instance.is_supported_filetype(str(does_not_exist)) + + def test_is_supported_filetype_shebang(self, tmpdir): + path = tmpdir.join("myscript") + path.write("#!/usr/bin/env python\n") + assert self.instance.is_supported_filetype(str(path)) + + def test_is_supported_filetype_editor_backup(self, tmpdir): + path = tmpdir.join("myscript~") + path.write("#!/usr/bin/env python\n") + assert not self.instance.is_supported_filetype(str(path)) + + def test_is_supported_filetype_defaults(self, tmpdir): + assert self.instance.is_supported_filetype(str(tmpdir.join("stub.pyi"))) + assert self.instance.is_supported_filetype(str(tmpdir.join("source.py"))) + assert self.instance.is_supported_filetype(str(tmpdir.join("source.pyx"))) + + def test_is_supported_filetype_configuration(self, tmpdir): + config = Config(supported_extensions=(".pyx",), blocked_extensions=(".py",)) + assert config.is_supported_filetype(str(tmpdir.join("stub.pyx"))) + assert not config.is_supported_filetype(str(tmpdir.join("stub.py"))) + + @pytest.mark.skipif( + sys.platform == "win32", reason="cannot create fifo file on Windows platform" + ) + def test_is_supported_filetype_fifo(self, tmpdir): + fifo_file = os.path.join(tmpdir, "fifo_file") + os.mkfifo(fifo_file) + assert not self.instance.is_supported_filetype(fifo_file) + def test_as_list(): assert settings._as_list([" one "]) == ["one"] From d35ba18fc0bd0c54c76f97f75c354eca758ea1cf Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 2 Aug 2020 03:41:45 -0700 Subject: [PATCH 0899/1439] Initial work toward adding experimental support for sorting literals #1358 --- CHANGELOG.md | 1 + isort/api.py | 335 +------------------------------- isort/exceptions.py | 29 +++ tests/test_exceptions.py | 18 ++ tests/test_ticketed_features.py | 50 +++++ 5 files changed, 105 insertions(+), 328 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f24a3b23..aed68580f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. ### 5.3.0 TBD - Implemented ability to treat all or select comments as code (issue #1357) - Implemented ability to use different configs for different file extensions (issue #1162) + - Added experimental support for sorting literals (issue #1358) - Improved handling of deprecated single line variables for usage with Visual Studio Code (issue #1363) - Improved handling of mixed newline forms within same source file. diff --git a/isort/api.py b/isort/api.py index 2c630adb7..7c50ea2fe 100644 --- a/isort/api.py +++ b/isort/api.py @@ -1,34 +1,24 @@ import shutil import sys -import textwrap from io import StringIO -from itertools import chain from pathlib import Path -from typing import List, Optional, TextIO, Union, cast +from typing import Optional, TextIO, Union, cast from warnings import warn -from . import io, output, parse +from isort import core + +from . import io from .exceptions import ( ExistingSyntaxErrors, FileSkipComment, FileSkipSetting, IntroducedSyntaxErrors, ) -from .format import ( - ask_whether_to_apply_changes_to_file, - create_terminal_printer, - format_natural, - remove_whitespace, - show_unified_diff, -) +from .format import ask_whether_to_apply_changes_to_file, create_terminal_printer, show_unified_diff from .io import Empty from .place import module as place_module # noqa: F401 from .place import module_with_reason as place_module_with_reason # noqa: F401 -from .settings import DEFAULT_CONFIG, FILE_SKIP_COMMENTS, Config - -CIMPORT_IDENTIFIERS = ("cimport ", "cimport*", "from.cimport") -IMPORT_START_IDENTIFIERS = ("from ", "from.import", "import ", "import*") + CIMPORT_IDENTIFIERS -COMMENT_INDICATORS = ('"""', "'''", "'", '"', "#") +from .settings import DEFAULT_CONFIG, Config def sort_code_string( @@ -164,7 +154,7 @@ def sort_stream( _internal_output = StringIO() try: - changed = _sort_imports( + changed = core.process( input_stream, _internal_output, extension=extension or (file_path and file_path.suffix.lstrip(".")) or "py", @@ -388,314 +378,3 @@ def _config( config = Config(**config_kwargs) return config - - -def _sort_imports( - input_stream: TextIO, - output_stream: TextIO, - extension: str = "py", - config: Config = DEFAULT_CONFIG, -) -> bool: - """Parses stream identifying sections of contiguous imports and sorting them - - Code with unsorted imports is read from the provided `input_stream`, sorted and then - outputted to the specified `output_stream`. - - - `input_stream`: Text stream with unsorted import sections. - - `output_stream`: Text stream to output sorted inputs into. - - `config`: Config settings to use when sorting imports. Defaults settings. - - *Default*: `isort.settings.DEFAULT_CONFIG`. - - `extension`: The file extension or file extension rules that should be used. - - *Default*: `"py"`. - - *Choices*: `["py", "pyi", "pyx"]`. - - Returns `True` if there were changes that needed to be made (errors present) from what - was provided in the input_stream, otherwise `False`. - """ - line_separator: str = config.line_ending - add_imports: List[str] = [format_natural(addition) for addition in config.add_imports] - import_section: str = "" - next_import_section: str = "" - next_cimports: bool = False - in_quote: str = "" - first_comment_index_start: int = -1 - first_comment_index_end: int = -1 - contains_imports: bool = False - in_top_comment: bool = False - first_import_section: bool = True - section_comments = [f"# {heading}" for heading in config.import_headings.values()] - indent: str = "" - isort_off: bool = False - cimports: bool = False - made_changes: bool = False - - if config.float_to_top: - new_input = "" - current = "" - isort_off = False - for line in chain(input_stream, (None,)): - if isort_off and line is not None: - if line == "# isort: on\n": - isort_off = False - new_input += line - elif line in ("# isort: split\n", "# isort: off\n", None) or str(line).endswith( - "# isort: split\n" - ): - if line == "# isort: off\n": - isort_off = True - if current: - parsed = parse.file_contents(current, config=config) - extra_space = "" - while current[-1] == "\n": - extra_space += "\n" - current = current[:-1] - extra_space = extra_space.replace("\n", "", 1) - sorted_output = output.sorted_imports( - parsed, config, extension, import_type="import" - ) - if sorted_output.strip() != current.strip(): - made_changes = True - new_input += sorted_output - new_input += extra_space - current = "" - new_input += line or "" - else: - current += line or "" - - input_stream = StringIO(new_input) - - for index, line in enumerate(chain(input_stream, (None,))): - if line is None: - if index == 0 and not config.force_adds: - return False - - not_imports = True - line = "" - if not line_separator: - line_separator = "\n" - else: - stripped_line = line.strip() - if stripped_line and not line_separator: - line_separator = line[len(line.rstrip()) :].replace(" ", "").replace("\t", "") - - for file_skip_comment in FILE_SKIP_COMMENTS: - if file_skip_comment in line: - raise FileSkipComment("Passed in content") - - if ( - (index == 0 or (index in (1, 2) and not contains_imports)) - and stripped_line.startswith("#") - and stripped_line not in section_comments - ): - in_top_comment = True - elif in_top_comment: - if not line.startswith("#") or stripped_line in section_comments: - in_top_comment = False - first_comment_index_end = index - 1 - - if (not stripped_line.startswith("#") or in_quote) and '"' in line or "'" in line: - char_index = 0 - if first_comment_index_start == -1 and ( - line.startswith('"') or line.startswith("'") - ): - first_comment_index_start = index - while char_index < len(line): - if line[char_index] == "\\": - char_index += 1 - elif in_quote: - if line[char_index : char_index + len(in_quote)] == in_quote: - in_quote = "" - if first_comment_index_end < first_comment_index_start: - first_comment_index_end = index - elif line[char_index] in ("'", '"'): - long_quote = line[char_index : char_index + 3] - if long_quote in ('"""', "'''"): - in_quote = long_quote - char_index += 2 - else: - in_quote = line[char_index] - elif line[char_index] == "#": - break - char_index += 1 - - not_imports = bool(in_quote) or in_top_comment or isort_off - if not (in_quote or in_top_comment): - stripped_line = line.strip() - if isort_off: - if stripped_line == "# isort: on": - isort_off = False - elif stripped_line == "# isort: off": - not_imports = True - isort_off = True - elif stripped_line.endswith("# isort: split"): - not_imports = True - elif stripped_line in config.section_comments and not import_section: - import_section += line - indent = line[: -len(line.lstrip())] - elif not (stripped_line or contains_imports): - if add_imports and not indent and not config.append_only: - if not import_section: - output_stream.write(line) - line = "" - import_section += line_separator.join(add_imports) + line_separator - contains_imports = True - add_imports = [] - else: - not_imports = True - elif ( - not stripped_line - or stripped_line.startswith("#") - and (not indent or indent + line.lstrip() == line) - and not config.treat_all_comments_as_code - and stripped_line not in config.treat_comments_as_code - ): - import_section += line - elif stripped_line.startswith(IMPORT_START_IDENTIFIERS): - contains_imports = True - - new_indent = line[: -len(line.lstrip())] - import_statement = line - stripped_line = line.strip().split("#")[0] - while stripped_line.endswith("\\") or ( - "(" in stripped_line and ")" not in stripped_line - ): - if stripped_line.endswith("\\"): - while stripped_line and stripped_line.endswith("\\"): - line = input_stream.readline() - stripped_line = line.strip().split("#")[0] - import_statement += line - else: - while ")" not in stripped_line: - line = input_stream.readline() - stripped_line = line.strip().split("#")[0] - import_statement += line - - cimport_statement: bool = False - if ( - import_statement.lstrip().startswith(CIMPORT_IDENTIFIERS) - or " cimport " in import_statement - or " cimport*" in import_statement - or " cimport(" in import_statement - or ".cimport" in import_statement - ): - cimport_statement = True - - if cimport_statement != cimports or (new_indent != indent and import_section): - if import_section: - next_cimports = cimport_statement - next_import_section = import_statement - import_statement = "" - not_imports = True - line = "" - else: - cimports = cimport_statement - - indent = new_indent - import_section += import_statement - else: - not_imports = True - - if not_imports: - raw_import_section: str = import_section - if ( - add_imports - and not config.append_only - and not in_top_comment - and not in_quote - and not import_section - and not line.lstrip().startswith(COMMENT_INDICATORS) - ): - import_section = line_separator.join(add_imports) + line_separator - contains_imports = True - add_imports = [] - - if next_import_section and not import_section: # pragma: no cover - raw_import_section = import_section = next_import_section - next_import_section = "" - - if import_section: - if add_imports and not indent: - import_section = ( - line_separator.join(add_imports) + line_separator + import_section - ) - contains_imports = True - add_imports = [] - - if not indent: - import_section += line - raw_import_section += line - if not contains_imports: - output_stream.write(import_section) - else: - leading_whitespace = import_section[: -len(import_section.lstrip())] - trailing_whitespace = import_section[len(import_section.rstrip()) :] - if first_import_section and not import_section.lstrip( - line_separator - ).startswith(COMMENT_INDICATORS): - import_section = import_section.lstrip(line_separator) - raw_import_section = raw_import_section.lstrip(line_separator) - first_import_section = False - - if indent: - import_section = "".join( - line[len(indent) :] for line in import_section.splitlines(keepends=True) - ) - out_config = Config( - config=config, - line_length=max(config.line_length - len(indent), 0), - wrap_length=max(config.wrap_length - len(indent), 0), - lines_after_imports=1, - ) - else: - out_config = config - - sorted_import_section = output.sorted_imports( - parse.file_contents(import_section, config=config), - out_config, - extension, - import_type="cimport" if cimports else "import", - ) - if not (import_section.strip() and not sorted_import_section): - if indent: - sorted_import_section = ( - leading_whitespace - + textwrap.indent(sorted_import_section, indent).strip() - + trailing_whitespace - ) - - if not made_changes: - if config.ignore_whitespace: - compare_in = remove_whitespace( - raw_import_section, line_separator=line_separator - ).strip() - compare_out = remove_whitespace( - sorted_import_section, line_separator=line_separator - ).strip() - else: - compare_in = raw_import_section.strip() - compare_out = sorted_import_section.strip() - - if compare_out != compare_in: - made_changes = True - - output_stream.write(sorted_import_section) - if not line and not indent and next_import_section: - output_stream.write(line_separator) - - if indent: - output_stream.write(line) - if not next_import_section: - indent = "" - - if next_import_section: - cimports = next_cimports - contains_imports = True - else: - contains_imports = False - import_section = next_import_section - next_import_section = "" - else: - output_stream.write(line) - not_imports = False - - return made_changes diff --git a/isort/exceptions.py b/isort/exceptions.py index b7be81daf..c8a757003 100644 --- a/isort/exceptions.py +++ b/isort/exceptions.py @@ -85,3 +85,32 @@ class FormattingPluginDoesNotExist(ISortError): def __init__(self, formatter: str): super().__init__(f"Specified formatting plugin of {formatter} does not exist. ") self.formatter = formatter + + +class LiteralParsingFailure(ISortError): + """Raised when one of isorts literal sorting comments is used but isort can't parse the + the given data structure. + """ + + def __init__(self, code: str, original_error: Exception): + super().__init__( + f"isort failed to parse the given literal {code}. It's important to note " + "that isort literal sorting only supports simple literals parsable by " + f"ast.literal_eval which gave the exception of {original_error}." + ) + self.code = code + self.original_error = original_error + + +class LiteralSortTypeMismatch(ISortError): + """Raised when an isort literal sorting comment is used, with a type that doesn't match the + supplied data structure's type. + """ + + def __init__(self, kind: type, expected_kind: type): + super().__init__( + f"isort was told to sort a literal of type {expected_kind} but was given " + f"a literal of type {kind}." + ) + self.kind = kind + self.expected_kind = expected_kind diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 93e4e226e..f4a0d5e69 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -56,3 +56,21 @@ def setup_class(self): def test_variables(self): assert self.instance.profile == "profile" + + +class TestLiteralParsingFailure(TestISortError): + def setup_class(self): + self.instance = exceptions.LiteralParsingFailure("x = [", SyntaxError) + + def test_variables(self): + assert self.instance.code == "x = [" + assert self.instance.original_error == SyntaxError + + +class TestLiteralSortTypeMismatch(TestISortError): + def setup_class(self): + self.instance = exceptions.LiteralSortTypeMismatch(tuple, list) + + def test_variables(self): + assert self.instance.kind == tuple + assert self.instance.expected_kind == list diff --git a/tests/test_ticketed_features.py b/tests/test_ticketed_features.py index 9aab3b1d3..c8b414311 100644 --- a/tests/test_ticketed_features.py +++ b/tests/test_ticketed_features.py @@ -351,3 +351,53 @@ def test_treating_comments_as_code_issue_1357(): import c """ ) + + +def test_isort_literals_issue_1358(): + assert ( + isort.code( + """ +import x +import a + + +# isort: list +__all__ = ["b", "a", "b"] + +# isort: unique-list +__all__ = ["b", "a", "b"] + +# isort: tuple +__all__ = ("b", "a", "b") + +# isort: unique-tuple +__all__ = ("b", "a", "b") + +# isort: set +__all__ = {"b", "a", "b"} + +# isort: dict +y = {"z": "z", "b": "b", "b": "c"}""" + ) + == """ +import a +import x + +# isort: list +__all__ = ['a', 'b', 'b'] + +# isort: unique-list +__all__ = ['a', 'b'] + +# isort: tuple +__all__ = ('a', 'b', 'b') + +# isort: unique-tuple +__all__ = ('a', 'b') + +# isort: set +__all__ = {'a', 'b'} + +# isort: dict +y = {'b': 'c', 'z': 'z'}""" + ) From eed66a9eb67da63e4d27aa7588ef5717f9dd3eff Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 2 Aug 2020 03:41:52 -0700 Subject: [PATCH 0900/1439] Initial work toward adding experimental support for sorting literals #1358 --- isort/core.py | 358 ++++++++++++++++++++++++++++++++++++++++++ isort/literal.py | 87 ++++++++++ tests/test_literal.py | 33 ++++ 3 files changed, 478 insertions(+) create mode 100644 isort/core.py create mode 100644 isort/literal.py create mode 100644 tests/test_literal.py diff --git a/isort/core.py b/isort/core.py new file mode 100644 index 000000000..4dc8018a5 --- /dev/null +++ b/isort/core.py @@ -0,0 +1,358 @@ +import textwrap +from io import StringIO +from itertools import chain +from typing import List, TextIO, Union + +import isort.literal +from isort.settings import DEFAULT_CONFIG, Config + +from . import output, parse +from .exceptions import FileSkipComment +from .format import format_natural, remove_whitespace +from .settings import FILE_SKIP_COMMENTS + +CIMPORT_IDENTIFIERS = ("cimport ", "cimport*", "from.cimport") +IMPORT_START_IDENTIFIERS = ("from ", "from.import", "import ", "import*") + CIMPORT_IDENTIFIERS +COMMENT_INDICATORS = ('"""', "'''", "'", '"', "#") +CODE_SORT_COMMENTS = ( + "# isort: list", + "# isort: dict", + "# isort: set", + "# isort: unique-list", + "# isort: tuple", + "# isort: unique-tuple", +) + + +def process( + input_stream: TextIO, + output_stream: TextIO, + extension: str = "py", + config: Config = DEFAULT_CONFIG, +) -> bool: + """Parses stream identifying sections of contiguous imports and sorting them + + Code with unsorted imports is read from the provided `input_stream`, sorted and then + outputted to the specified `output_stream`. + + - `input_stream`: Text stream with unsorted import sections. + - `output_stream`: Text stream to output sorted inputs into. + - `config`: Config settings to use when sorting imports. Defaults settings. + - *Default*: `isort.settings.DEFAULT_CONFIG`. + - `extension`: The file extension or file extension rules that should be used. + - *Default*: `"py"`. + - *Choices*: `["py", "pyi", "pyx"]`. + + Returns `True` if there were changes that needed to be made (errors present) from what + was provided in the input_stream, otherwise `False`. + """ + line_separator: str = config.line_ending + add_imports: List[str] = [format_natural(addition) for addition in config.add_imports] + import_section: str = "" + next_import_section: str = "" + next_cimports: bool = False + in_quote: str = "" + first_comment_index_start: int = -1 + first_comment_index_end: int = -1 + contains_imports: bool = False + in_top_comment: bool = False + first_import_section: bool = True + section_comments = [f"# {heading}" for heading in config.import_headings.values()] + indent: str = "" + isort_off: bool = False + code_sorting: Union[bool, str] = False + code_sorting_section: str = "" + cimports: bool = False + made_changes: bool = False + + if config.float_to_top: + new_input = "" + current = "" + isort_off = False + for line in chain(input_stream, (None,)): + if isort_off and line is not None: + if line == "# isort: on\n": + isort_off = False + new_input += line + elif line in ("# isort: split\n", "# isort: off\n", None) or str(line).endswith( + "# isort: split\n" + ): + if line == "# isort: off\n": + isort_off = True + if current: + parsed = parse.file_contents(current, config=config) + extra_space = "" + while current[-1] == "\n": + extra_space += "\n" + current = current[:-1] + extra_space = extra_space.replace("\n", "", 1) + sorted_output = output.sorted_imports( + parsed, config, extension, import_type="import" + ) + if sorted_output.strip() != current.strip(): + made_changes = True + new_input += sorted_output + new_input += extra_space + current = "" + new_input += line or "" + else: + current += line or "" + + input_stream = StringIO(new_input) + + for index, line in enumerate(chain(input_stream, (None,))): + if line is None: + if index == 0 and not config.force_adds: + return False + + not_imports = True + line = "" + if not line_separator: + line_separator = "\n" + + if code_sorting and code_sorting_section: + output_stream.write( + isort.literal.assignment(code_sorting_section, str(code_sorting), extension) + ) + else: + stripped_line = line.strip() + if stripped_line and not line_separator: + line_separator = line[len(line.rstrip()) :].replace(" ", "").replace("\t", "") + + for file_skip_comment in FILE_SKIP_COMMENTS: + if file_skip_comment in line: + raise FileSkipComment("Passed in content") + + if ( + (index == 0 or (index in (1, 2) and not contains_imports)) + and stripped_line.startswith("#") + and stripped_line not in section_comments + ): + in_top_comment = True + elif in_top_comment: + if not line.startswith("#") or stripped_line in section_comments: + in_top_comment = False + first_comment_index_end = index - 1 + + if (not stripped_line.startswith("#") or in_quote) and '"' in line or "'" in line: + char_index = 0 + if first_comment_index_start == -1 and ( + line.startswith('"') or line.startswith("'") + ): + first_comment_index_start = index + while char_index < len(line): + if line[char_index] == "\\": + char_index += 1 + elif in_quote: + if line[char_index : char_index + len(in_quote)] == in_quote: + in_quote = "" + if first_comment_index_end < first_comment_index_start: + first_comment_index_end = index + elif line[char_index] in ("'", '"'): + long_quote = line[char_index : char_index + 3] + if long_quote in ('"""', "'''"): + in_quote = long_quote + char_index += 2 + else: + in_quote = line[char_index] + elif line[char_index] == "#": + break + char_index += 1 + + not_imports = bool(in_quote) or in_top_comment or isort_off + if not (in_quote or in_top_comment): + stripped_line = line.strip() + if isort_off: + if stripped_line == "# isort: on": + isort_off = False + elif stripped_line == "# isort: off": + not_imports = True + isort_off = True + elif stripped_line.endswith("# isort: split"): + not_imports = True + elif stripped_line in CODE_SORT_COMMENTS: + code_sorting = stripped_line.split("isort: ")[1].strip() + not_imports = True + elif code_sorting: + if not stripped_line: + output_stream.write( + isort.literal.assignment( + code_sorting_section, str(code_sorting), extension + ) + ) + not_imports = True + code_sorting = False + code_sorting_section = "" + else: + code_sorting_section += line + line = "" + elif stripped_line in config.section_comments and not import_section: + import_section += line + indent = line[: -len(line.lstrip())] + elif not (stripped_line or contains_imports): + if add_imports and not indent and not config.append_only: + if not import_section: + output_stream.write(line) + line = "" + import_section += line_separator.join(add_imports) + line_separator + contains_imports = True + add_imports = [] + else: + not_imports = True + elif ( + not stripped_line + or stripped_line.startswith("#") + and (not indent or indent + line.lstrip() == line) + and not config.treat_all_comments_as_code + and stripped_line not in config.treat_comments_as_code + ): + import_section += line + elif stripped_line.startswith(IMPORT_START_IDENTIFIERS): + contains_imports = True + + new_indent = line[: -len(line.lstrip())] + import_statement = line + stripped_line = line.strip().split("#")[0] + while stripped_line.endswith("\\") or ( + "(" in stripped_line and ")" not in stripped_line + ): + if stripped_line.endswith("\\"): + while stripped_line and stripped_line.endswith("\\"): + line = input_stream.readline() + stripped_line = line.strip().split("#")[0] + import_statement += line + else: + while ")" not in stripped_line: + line = input_stream.readline() + stripped_line = line.strip().split("#")[0] + import_statement += line + + cimport_statement: bool = False + if ( + import_statement.lstrip().startswith(CIMPORT_IDENTIFIERS) + or " cimport " in import_statement + or " cimport*" in import_statement + or " cimport(" in import_statement + or ".cimport" in import_statement + ): + cimport_statement = True + + if cimport_statement != cimports or (new_indent != indent and import_section): + if import_section: + next_cimports = cimport_statement + next_import_section = import_statement + import_statement = "" + not_imports = True + line = "" + else: + cimports = cimport_statement + + indent = new_indent + import_section += import_statement + else: + not_imports = True + + if not_imports: + raw_import_section: str = import_section + if ( + add_imports + and not config.append_only + and not in_top_comment + and not in_quote + and not import_section + and not line.lstrip().startswith(COMMENT_INDICATORS) + ): + import_section = line_separator.join(add_imports) + line_separator + contains_imports = True + add_imports = [] + + if next_import_section and not import_section: # pragma: no cover + raw_import_section = import_section = next_import_section + next_import_section = "" + + if import_section: + if add_imports and not indent: + import_section = ( + line_separator.join(add_imports) + line_separator + import_section + ) + contains_imports = True + add_imports = [] + + if not indent: + import_section += line + raw_import_section += line + if not contains_imports: + output_stream.write(import_section) + else: + leading_whitespace = import_section[: -len(import_section.lstrip())] + trailing_whitespace = import_section[len(import_section.rstrip()) :] + if first_import_section and not import_section.lstrip( + line_separator + ).startswith(COMMENT_INDICATORS): + import_section = import_section.lstrip(line_separator) + raw_import_section = raw_import_section.lstrip(line_separator) + first_import_section = False + + if indent: + import_section = "".join( + line[len(indent) :] for line in import_section.splitlines(keepends=True) + ) + out_config = Config( + config=config, + line_length=max(config.line_length - len(indent), 0), + wrap_length=max(config.wrap_length - len(indent), 0), + lines_after_imports=1, + ) + else: + out_config = config + + sorted_import_section = output.sorted_imports( + parse.file_contents(import_section, config=config), + out_config, + extension, + import_type="cimport" if cimports else "import", + ) + if not (import_section.strip() and not sorted_import_section): + if indent: + sorted_import_section = ( + leading_whitespace + + textwrap.indent(sorted_import_section, indent).strip() + + trailing_whitespace + ) + + if not made_changes: + if config.ignore_whitespace: + compare_in = remove_whitespace( + raw_import_section, line_separator=line_separator + ).strip() + compare_out = remove_whitespace( + sorted_import_section, line_separator=line_separator + ).strip() + else: + compare_in = raw_import_section.strip() + compare_out = sorted_import_section.strip() + + if compare_out != compare_in: + made_changes = True + + output_stream.write(sorted_import_section) + if not line and not indent and next_import_section: + output_stream.write(line_separator) + + if indent: + output_stream.write(line) + if not next_import_section: + indent = "" + + if next_import_section: + cimports = next_cimports + contains_imports = True + else: + contains_imports = False + import_section = next_import_section + next_import_section = "" + else: + output_stream.write(line) + not_imports = False + + return made_changes diff --git a/isort/literal.py b/isort/literal.py new file mode 100644 index 000000000..c4b101903 --- /dev/null +++ b/isort/literal.py @@ -0,0 +1,87 @@ +import ast +from pprint import PrettyPrinter +from typing import Any, Callable, Dict, List, Set, Tuple + +from isort.exceptions import LiteralParsingFailure, LiteralSortTypeMismatch +from isort.settings import DEFAULT_CONFIG, Config + + +class ISortPrettyPrinter(PrettyPrinter): + """an isort customized pretty printer for sorted literals""" + + def __init__(self, config: Config): + super().__init__(width=config.line_length, compact=True) + + +type_mapping: Dict[str, Tuple[type, Callable[[Any, ISortPrettyPrinter], str]]] = {} + + +def assignment(code: str, sort_type: str, extension: str, config: Config = DEFAULT_CONFIG) -> str: + """Sorts the literal present within the provided code against the provided sort type, + returning the sorted representation of the source code. + """ + if sort_type not in type_mapping: + raise ValueError( + "Trying to sort using an undefined sort_type. " + f"Defined sort types are {', '.join(type_mapping.keys())}." + ) + + variable_name, literal = code.split(" = ") + try: + value = ast.literal_eval(literal) + except Exception as error: + raise LiteralParsingFailure(code, error) + + expected_type, sort_function = type_mapping[sort_type] + if type(value) != expected_type: + raise LiteralSortTypeMismatch(type(value), expected_type) + + printer = ISortPrettyPrinter(config) + sorted_value_code = sort_function(value, printer) + if config.formatting_function: + sorted_value_code = config.formatting_function( + sorted_value_code, extension, config + ).rstrip() + + sorted_value_code += code[len(code.rstrip()) :] + return f"{variable_name} = {sorted_value_code}" + + +def register_type(name: str, kind: type): + """Registers a new literal sort type.""" + + def wrap(function): + type_mapping[name] = (kind, function) + return function + + return wrap + + +@register_type("dict", dict) +def _dict(value: Dict[Any, Any], printer: ISortPrettyPrinter) -> str: + return printer.pformat(dict(sorted(value.items(), key=lambda item: item[1]))) + + +@register_type("list", list) +def _list(value: List[Any], printer: ISortPrettyPrinter) -> str: + return printer.pformat(sorted(value)) + + +@register_type("unique-list", list) +def _unique_list(value: List[Any], printer: ISortPrettyPrinter) -> str: + return printer.pformat(list(sorted(set(value)))) + + +@register_type("set", set) +def _set(value: Set[Any], printer: ISortPrettyPrinter) -> str: + return "{" + printer.pformat(tuple(sorted(value)))[1:-1] + "}" + + +@register_type("tuple", tuple) +def _tuple(value: Tuple[Any, ...], printer: ISortPrettyPrinter) -> str: + return printer.pformat(tuple(sorted(value))) + + +@register_type("unique-tuple", tuple) +def _unique_tuple(value: Tuple[Any, ...], printer: ISortPrettyPrinter) -> str: + return printer.pformat(tuple(sorted(set(value)))) diff --git a/tests/test_literal.py b/tests/test_literal.py new file mode 100644 index 000000000..d94dd0812 --- /dev/null +++ b/tests/test_literal.py @@ -0,0 +1,33 @@ +import pytest + +import isort.literal +from isort import exceptions +from isort.settings import Config + + +def test_value_mismatch(): + with pytest.raises(exceptions.LiteralSortTypeMismatch): + isort.literal.assignment("x = [1, 2, 3]", "set", "py") + + +def test_invalid_syntax(): + with pytest.raises(exceptions.LiteralParsingFailure): + isort.literal.assignment("x = [1, 2, 3", "list", "py") + + +def test_invalid_sort_type(): + with pytest.raises(ValueError): + isort.literal.assignment("x = [1, 2, 3", "tuple-list-not-exist", "py") + + +def test_invalid_sort_type(): + with pytest.raises(ValueError): + isort.literal.assignment("x = [1, 2, 3", "tuple-list-not-exist", "py") + + +def test_value_assignment(): + assert isort.literal.assignment("x = ['b', 'a']", "list", "py") == "x = ['a', 'b']" + assert ( + isort.literal.assignment("x = ['b', 'a']", "list", "py", Config(formatter="example")) + == 'x = ["a", "b"]' + ) From 572f4ffde1de3ec8a7cb9550b58b6240663df225 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 2 Aug 2020 04:05:53 -0700 Subject: [PATCH 0901/1439] Simplify extension setting (not require preceding dot) --- isort/settings.py | 5 +++-- tests/test_literal.py | 5 ----- tests/test_settings.py | 2 +- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/isort/settings.py b/isort/settings.py index 37a7e0cfe..25ca14dd1 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -26,8 +26,8 @@ from .wrap_modes import from_string as wrap_mode_from_string _SHEBANG_RE = re.compile(br"^#!.*\bpython[23w]?\b") -SUPPORTED_EXTENSIONS = frozenset({".py", ".pyi", ".pyx"}) -BLOCKED_EXTENSIONS = frozenset({".pex"}) +SUPPORTED_EXTENSIONS = frozenset({"py", "pyi", "pyx"}) +BLOCKED_EXTENSIONS = frozenset({"pex"}) FILE_SKIP_COMMENTS: Tuple[str, ...] = ( "isort:" + "skip_file", "isort: " + "skip_file", @@ -396,6 +396,7 @@ def __init__( def is_supported_filetype(self, file_name: str): _root, ext = os.path.splitext(file_name) + ext = ext.lstrip(".") if ext in self.supported_extensions: return True elif ext in self.blocked_extensions: diff --git a/tests/test_literal.py b/tests/test_literal.py index d94dd0812..b5b856dd4 100644 --- a/tests/test_literal.py +++ b/tests/test_literal.py @@ -20,11 +20,6 @@ def test_invalid_sort_type(): isort.literal.assignment("x = [1, 2, 3", "tuple-list-not-exist", "py") -def test_invalid_sort_type(): - with pytest.raises(ValueError): - isort.literal.assignment("x = [1, 2, 3", "tuple-list-not-exist", "py") - - def test_value_assignment(): assert isort.literal.assignment("x = ['b', 'a']", "list", "py") == "x = ['a', 'b']" assert ( diff --git a/tests/test_settings.py b/tests/test_settings.py index 8da098e22..38d146390 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -58,7 +58,7 @@ def test_is_supported_filetype_defaults(self, tmpdir): assert self.instance.is_supported_filetype(str(tmpdir.join("source.pyx"))) def test_is_supported_filetype_configuration(self, tmpdir): - config = Config(supported_extensions=(".pyx",), blocked_extensions=(".py",)) + config = Config(supported_extensions=("pyx",), blocked_extensions=("py",)) assert config.is_supported_filetype(str(tmpdir.join("stub.pyx"))) assert not config.is_supported_filetype(str(tmpdir.join("stub.py"))) From 6eba70991479958345ac1fea62c3baec658b377f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 3 Aug 2020 21:23:27 -0700 Subject: [PATCH 0902/1439] Resolve #1367: Improve error handling for known import sections --- CHANGELOG.md | 1 + isort/settings.py | 35 +++++++++++++++++++++++++---------- tests/test_settings.py | 7 +++++++ 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aed68580f..efedf9e5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. - Added experimental support for sorting literals (issue #1358) - Improved handling of deprecated single line variables for usage with Visual Studio Code (issue #1363) - Improved handling of mixed newline forms within same source file. + - Improved error handling for known sections. Internal Development: - Initial hypothesmith powered test to help catch unexpected syntax parsing and output errors (thanks @Zac-HD!) diff --git a/isort/settings.py b/isort/settings.py index 25ca14dd1..c30ea15c0 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -304,7 +304,7 @@ def __init__( known_other = {} import_headings = {} - for key, value in combined_config.items(): + for key, value in tuple(combined_config.items()): # Collect all known sections beyond those that have direct entries if key.startswith(KNOWN_PREFIX) and key not in ( "known_standard_library", @@ -314,13 +314,30 @@ def __init__( "known_local_folder", ): import_heading = key[len(KNOWN_PREFIX) :].lower() - known_other[import_heading] = frozenset(value) - if not import_heading.upper() in combined_config.get("sections", ()): - warn( - f"`{key}` setting is defined, but not {import_heading.upper()} is not" - " included in `sections` config option:" - f" {combined_config.get('sections', SECTION_DEFAULTS)}." - ) + maps_to_section = import_heading.upper() + combined_config.pop(key) + if maps_to_section in KNOWN_SECTION_MAPPING: + section_name = f"known_{KNOWN_SECTION_MAPPING[maps_to_section].lower()}" + if section_name in combined_config: + warn( + f"Can't set both {key} and {section_name} in the same config file.\n" + f"Default to {section_name} if unsure." + "\n\n" + "See: https://timothycrosley.github.io/isort/" + "#custom-sections-and-ordering." + ) + else: + combined_config[section_name] = frozenset(value) + else: + known_other[import_heading] = frozenset(value) + if maps_to_section not in combined_config.get("sections", ()): + warn( + f"`{key}` setting is defined, but {maps_to_section} is not" + " included in `sections` config option:" + f" {combined_config.get('sections', SECTION_DEFAULTS)}.\n\n" + "See: https://timothycrosley.github.io/isort/" + "#custom-sections-and-ordering." + ) if key.startswith(IMPORT_HEADING_PREFIX): import_headings[key[len(IMPORT_HEADING_PREFIX) :].lower()] = str(value) @@ -384,8 +401,6 @@ def __init__( combined_config.pop(deprecated_option) if known_other: - for known_key in known_other: - combined_config.pop(f"{KNOWN_PREFIX}{known_key}", None) combined_config["known_other"] = known_other if import_headings: for import_heading_key in import_headings: diff --git a/tests/test_settings.py b/tests/test_settings.py index 38d146390..fb2316181 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -14,6 +14,13 @@ class TestConfig: def test_init(self): assert Config() + def test_known_settings(self): + assert Config(known_third_party=["one"]).known_third_party == frozenset({"one"}) + assert Config(known_thirdparty=["two"]).known_third_party == frozenset({"two"}) + assert Config( + known_third_party=["one"], known_thirdparty=["two"] + ).known_third_party == frozenset({"one"}) + def test_invalid_settings_path(self): with pytest.raises(exceptions.InvalidSettingsPath): Config(settings_path="this_couldnt_possibly_actually_exists/could_it") From 4a2256278396f5ef7590bad02091c08fcd05b630 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 3 Aug 2020 21:28:31 -0700 Subject: [PATCH 0903/1439] Resolve #1368: Add extra_standard_library to the README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a5ba62c60..ed5eff44d 100644 --- a/README.md +++ b/README.md @@ -341,6 +341,7 @@ capitalization (`known_custom=custom` VS `sections=CUSTOM,...`) for all others r following mapping: - `known_standard_library` : `STANDARD_LIBRARY` + - `extra_standard_library` : `STANDARD_LIBRARY` # Like known standard library but appends instead of replacing - `known_future_library` : `FUTURE` - `known_first_party`: `FIRSTPARTY` - `known_third_party`: `THIRDPARTY` From e675a244edd295344ddd08038a0d4043d10ff727 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 3 Aug 2020 21:41:19 -0700 Subject: [PATCH 0904/1439] Fixed #1366: spurious errors when combining skip with --gitignore. --- CHANGELOG.md | 1 + isort/settings.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index efedf9e5c..bd5014f9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. - Improved handling of deprecated single line variables for usage with Visual Studio Code (issue #1363) - Improved handling of mixed newline forms within same source file. - Improved error handling for known sections. + - Fixed #1366: spurious errors when combining skip with --gitignore. Internal Development: - Initial hypothesmith powered test to help catch unexpected syntax parsing and output errors (thanks @Zac-HD!) diff --git a/isort/settings.py b/isort/settings.py index c30ea15c0..2d2858944 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -445,6 +445,9 @@ def is_skipped(self, file_path: Path) -> bool: os_path = str(file_path) if self.skip_gitignore: + if file_path.name == ".git": # pragma: no cover + return True + result = subprocess.run( # nosec ["git", "-C", str(file_path.resolve().parent), "check-ignore", "--quiet", os_path] ) From e2656b7bfbdb5c1c9019a9c6cda32bd001593dab Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 3 Aug 2020 22:05:08 -0700 Subject: [PATCH 0905/1439] Fixed #1359: --skip-gitignore does not honor ignored symlink --- CHANGELOG.md | 1 + isort/settings.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd5014f9d..62c7986ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. - Improved handling of mixed newline forms within same source file. - Improved error handling for known sections. - Fixed #1366: spurious errors when combining skip with --gitignore. + - Fixed #1359: --skip-gitignore does not honor ignored symlink Internal Development: - Initial hypothesmith powered test to help catch unexpected syntax parsing and output errors (thanks @Zac-HD!) diff --git a/isort/settings.py b/isort/settings.py index 2d2858944..7f8bbe23d 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -449,7 +449,7 @@ def is_skipped(self, file_path: Path) -> bool: return True result = subprocess.run( # nosec - ["git", "-C", str(file_path.resolve().parent), "check-ignore", "--quiet", os_path] + ["git", "-C", str(file_path.parent), "check-ignore", "--quiet", os_path] ) if result.returncode == 0: return True From 2d56366cb6eda6c0bcb715ac9f635094d83b2203 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 3 Aug 2020 22:20:24 -0700 Subject: [PATCH 0906/1439] Reuse has changed logic for both float to top and normal operation --- isort/core.py | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/isort/core.py b/isort/core.py index 4dc8018a5..3f99a8dfc 100644 --- a/isort/core.py +++ b/isort/core.py @@ -89,8 +89,12 @@ def process( sorted_output = output.sorted_imports( parsed, config, extension, import_type="import" ) - if sorted_output.strip() != current.strip(): - made_changes = True + made_changes = made_changes or _has_changed( + before=current, + after=sorted_output, + line_separator=parsed.line_separator, + ignore_whitespace=config.ignore_whitespace, + ) new_input += sorted_output new_input += extra_space current = "" @@ -320,20 +324,12 @@ def process( + trailing_whitespace ) - if not made_changes: - if config.ignore_whitespace: - compare_in = remove_whitespace( - raw_import_section, line_separator=line_separator - ).strip() - compare_out = remove_whitespace( - sorted_import_section, line_separator=line_separator - ).strip() - else: - compare_in = raw_import_section.strip() - compare_out = sorted_import_section.strip() - - if compare_out != compare_in: - made_changes = True + made_changes = made_changes or _has_changed( + before=raw_import_section, + after=sorted_import_section, + line_separator=line_separator, + ignore_whitespace=config.ignore_whitespace, + ) output_stream.write(sorted_import_section) if not line and not indent and next_import_section: @@ -356,3 +352,13 @@ def process( not_imports = False return made_changes + + +def _has_changed(before: str, after: str, line_separator: str, ignore_whitespace: bool) -> bool: + if ignore_whitespace: + return ( + remove_whitespace(before, line_separator=line_separator).strip() + != remove_whitespace(after, line_separator=line_separator).strip() + ) + else: + return before.strip() != after.strip() From 0bab6255db14da28dd8733fb2ddef4dae9a1c182 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 3 Aug 2020 23:01:41 -0700 Subject: [PATCH 0907/1439] Ensure isort literal sorting works with indents --- isort/core.py | 45 +++++++++++++++------ isort/literal.py | 5 ++- tests/test_ticketed_features.py | 72 +++++++++++++++++++++++++++++++++ 3 files changed, 108 insertions(+), 14 deletions(-) diff --git a/isort/core.py b/isort/core.py index 3f99a8dfc..892fffc8b 100644 --- a/isort/core.py +++ b/isort/core.py @@ -62,6 +62,7 @@ def process( isort_off: bool = False code_sorting: Union[bool, str] = False code_sorting_section: str = "" + code_sorting_indent: str = "" cimports: bool = False made_changes: bool = False @@ -116,7 +117,15 @@ def process( if code_sorting and code_sorting_section: output_stream.write( - isort.literal.assignment(code_sorting_section, str(code_sorting), extension) + textwrap.indent( + isort.literal.assignment( + code_sorting_section, + str(code_sorting), + extension, + config=_indented_config(config, indent), + ), + code_sorting_indent, + ) ) else: stripped_line = line.strip() @@ -176,17 +185,25 @@ def process( not_imports = True elif stripped_line in CODE_SORT_COMMENTS: code_sorting = stripped_line.split("isort: ")[1].strip() + code_sorting_indent = line[: -len(line.lstrip())] not_imports = True elif code_sorting: if not stripped_line: output_stream.write( - isort.literal.assignment( - code_sorting_section, str(code_sorting), extension + textwrap.indent( + isort.literal.assignment( + code_sorting_section, + str(code_sorting), + extension, + config=_indented_config(config, indent), + ), + code_sorting_indent, ) ) not_imports = True code_sorting = False code_sorting_section = "" + code_sorting_indent = "" else: code_sorting_section += line line = "" @@ -301,18 +318,10 @@ def process( import_section = "".join( line[len(indent) :] for line in import_section.splitlines(keepends=True) ) - out_config = Config( - config=config, - line_length=max(config.line_length - len(indent), 0), - wrap_length=max(config.wrap_length - len(indent), 0), - lines_after_imports=1, - ) - else: - out_config = config sorted_import_section = output.sorted_imports( parse.file_contents(import_section, config=config), - out_config, + _indented_config(config, indent), extension, import_type="cimport" if cimports else "import", ) @@ -354,6 +363,18 @@ def process( return made_changes +def _indented_config(config: Config, indent: str): + if not indent: + return config + + return Config( + config=config, + line_length=max(config.line_length - len(indent), 0), + wrap_length=max(config.wrap_length - len(indent), 0), + lines_after_imports=1, + ) + + def _has_changed(before: str, after: str, line_separator: str, ignore_whitespace: bool) -> bool: if ignore_whitespace: return ( diff --git a/isort/literal.py b/isort/literal.py index c4b101903..7bb5410d6 100644 --- a/isort/literal.py +++ b/isort/literal.py @@ -27,6 +27,7 @@ def assignment(code: str, sort_type: str, extension: str, config: Config = DEFAU ) variable_name, literal = code.split(" = ") + variable_name = variable_name.lstrip() try: value = ast.literal_eval(literal) except Exception as error: @@ -37,14 +38,14 @@ def assignment(code: str, sort_type: str, extension: str, config: Config = DEFAU raise LiteralSortTypeMismatch(type(value), expected_type) printer = ISortPrettyPrinter(config) - sorted_value_code = sort_function(value, printer) + sorted_value_code = f"{variable_name} = {sort_function(value, printer)}" if config.formatting_function: sorted_value_code = config.formatting_function( sorted_value_code, extension, config ).rstrip() sorted_value_code += code[len(code.rstrip()) :] - return f"{variable_name} = {sorted_value_code}" + return sorted_value_code def register_type(name: str, kind: type): diff --git a/tests/test_ticketed_features.py b/tests/test_ticketed_features.py index c8b414311..153fe4e3c 100644 --- a/tests/test_ticketed_features.py +++ b/tests/test_ticketed_features.py @@ -376,6 +376,12 @@ def test_isort_literals_issue_1358(): # isort: set __all__ = {"b", "a", "b"} + +def method(): + # isort: list + x = ["b", "a"] + + # isort: dict y = {"z": "z", "b": "b", "b": "c"}""" ) @@ -398,6 +404,72 @@ def test_isort_literals_issue_1358(): # isort: set __all__ = {'a', 'b'} + +def method(): + # isort: list + x = ['a', 'b'] + + # isort: dict y = {'b': 'c', 'z': 'z'}""" ) + assert ( + isort.code( + """ +import x +import a + + +# isort: list +__all__ = ["b", "a", "b"] + +# isort: unique-list +__all__ = ["b", "a", "b"] + +# isort: tuple +__all__ = ("b", "a", "b") + +# isort: unique-tuple +__all__ = ("b", "a", "b") + +# isort: set +__all__ = {"b", "a", "b"} + + +def method(): + # isort: list + x = ["b", "a"] + + +# isort: dict +y = {"z": "z", "b": "b", "b": "c"}""", + formatter="example", + ) + == """ +import a +import x + +# isort: list +__all__ = ["a", "b", "b"] + +# isort: unique-list +__all__ = ["a", "b"] + +# isort: tuple +__all__ = ("a", "b", "b") + +# isort: unique-tuple +__all__ = ("a", "b") + +# isort: set +__all__ = {"a", "b"} + + +def method(): + # isort: list + x = ["a", "b"] + + +# isort: dict +y = {"b": "c", "z": "z"}""" + ) From 86e33e7952111492faf19a13f36535f3e7c029ec Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 4 Aug 2020 20:54:26 -0700 Subject: [PATCH 0908/1439] Added experimental support for sorting and deduping groupings of assignments. --- CHANGELOG.md | 1 + isort/core.py | 1 + isort/exceptions.py | 18 ++++++++++++++++++ isort/literal.py | 24 ++++++++++++++++++++++-- tests/test_exceptions.py | 8 ++++++++ tests/test_literal.py | 5 +++++ tests/test_ticketed_features.py | 10 ++++++++++ 7 files changed, 65 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62c7986ae..400f53545 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. - Implemented ability to treat all or select comments as code (issue #1357) - Implemented ability to use different configs for different file extensions (issue #1162) - Added experimental support for sorting literals (issue #1358) + - Added experimental support for sorting and deduping groupings of assignments. - Improved handling of deprecated single line variables for usage with Visual Studio Code (issue #1363) - Improved handling of mixed newline forms within same source file. - Improved error handling for known sections. diff --git a/isort/core.py b/isort/core.py index 892fffc8b..010aa7f6b 100644 --- a/isort/core.py +++ b/isort/core.py @@ -21,6 +21,7 @@ "# isort: unique-list", "# isort: tuple", "# isort: unique-tuple", + "# isort: assignments", ) diff --git a/isort/exceptions.py b/isort/exceptions.py index c8a757003..9f45744c7 100644 --- a/isort/exceptions.py +++ b/isort/exceptions.py @@ -114,3 +114,21 @@ def __init__(self, kind: type, expected_kind: type): ) self.kind = kind self.expected_kind = expected_kind + + +class AssignmentsFormatMismatch(ISortError): + """Raised when isort is told to sort assignments but the format of the assignment section + doesn't match isort's expectation. + """ + + def __init__(self, code: str): + super().__init__( + "isort was told to sort a section of assignments, however the given code:\n\n" + f"{code}\n\n" + "Does not match isort's strict single line formatting requirement for assignment " + "sorting:\n\n" + "{variable_name} = {value}\n" + "{variable_name2} = {value2}\n" + "...\n\n" + ) + self.code = code diff --git a/isort/literal.py b/isort/literal.py index 7bb5410d6..28e0855c3 100644 --- a/isort/literal.py +++ b/isort/literal.py @@ -2,7 +2,11 @@ from pprint import PrettyPrinter from typing import Any, Callable, Dict, List, Set, Tuple -from isort.exceptions import LiteralParsingFailure, LiteralSortTypeMismatch +from isort.exceptions import ( + AssignmentsFormatMismatch, + LiteralParsingFailure, + LiteralSortTypeMismatch, +) from isort.settings import DEFAULT_CONFIG, Config @@ -16,11 +20,27 @@ def __init__(self, config: Config): type_mapping: Dict[str, Tuple[type, Callable[[Any, ISortPrettyPrinter], str]]] = {} +def assignments(code: str) -> str: + sort_assignments = {} + for line in code.splitlines(keepends=True): + if line: + if " = " not in line: + raise AssignmentsFormatMismatch(code) + else: + variable_name, value = line.split(" = ", 1) + sort_assignments[variable_name] = value + + sorted_assignments = dict(sorted(sort_assignments.items(), key=lambda item: item[1])) + return "".join(f"{key} = {value}" for key, value in sorted_assignments.items()) + + def assignment(code: str, sort_type: str, extension: str, config: Config = DEFAULT_CONFIG) -> str: """Sorts the literal present within the provided code against the provided sort type, returning the sorted representation of the source code. """ - if sort_type not in type_mapping: + if sort_type == "assignments": + return assignments(code) + elif sort_type not in type_mapping: raise ValueError( "Trying to sort using an undefined sort_type. " f"Defined sort types are {', '.join(type_mapping.keys())}." diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index f4a0d5e69..8a6f60fcc 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -74,3 +74,11 @@ def setup_class(self): def test_variables(self): assert self.instance.kind == tuple assert self.instance.expected_kind == list + + +class TestAssignmentsFormatMismatch(TestISortError): + def setup_class(self): + self.instance = exceptions.AssignmentsFormatMismatch("print x") + + def test_variables(self): + assert self.instance.code == "print x" diff --git a/tests/test_literal.py b/tests/test_literal.py index b5b856dd4..0dd7458c7 100644 --- a/tests/test_literal.py +++ b/tests/test_literal.py @@ -26,3 +26,8 @@ def test_value_assignment(): isort.literal.assignment("x = ['b', 'a']", "list", "py", Config(formatter="example")) == 'x = ["a", "b"]' ) + + +def test_assignments_invalid_section(): + with pytest.raises(exceptions.AssignmentsFormatMismatch): + isort.literal.assignment("x++", "assignments", "py") diff --git a/tests/test_ticketed_features.py b/tests/test_ticketed_features.py index 153fe4e3c..7061a9e1f 100644 --- a/tests/test_ticketed_features.py +++ b/tests/test_ticketed_features.py @@ -441,6 +441,11 @@ def method(): x = ["b", "a"] +# isort: assignments +d = x +b = 2 +a = 1 + # isort: dict y = {"z": "z", "b": "b", "b": "c"}""", formatter="example", @@ -470,6 +475,11 @@ def method(): x = ["a", "b"] +# isort: assignments +a = 1 +b = 2 +d = x + # isort: dict y = {"b": "c", "z": "z"}""" ) From 79bad69f55d50ab35712ca7f01feefddccfe1add Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 4 Aug 2020 21:58:57 -0700 Subject: [PATCH 0909/1439] Improved API consistency, returning a boolean value for all modification API calls to indicate if changes were made. --- CHANGELOG.md | 1 + isort/api.py | 11 +++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 400f53545..354f0b482 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. - Improved handling of deprecated single line variables for usage with Visual Studio Code (issue #1363) - Improved handling of mixed newline forms within same source file. - Improved error handling for known sections. + - Improved API consistency, returning a boolean value for all modification API calls to indicate if changes were made. - Fixed #1366: spurious errors when combining skip with --gitignore. - Fixed #1359: --skip-gitignore does not honor ignored symlink diff --git a/isort/api.py b/isort/api.py index 7c50ea2fe..059bbf9e5 100644 --- a/isort/api.py +++ b/isort/api.py @@ -98,9 +98,9 @@ def sort_stream( disregard_skip: bool = False, show_diff: Union[bool, TextIO] = False, **config_kwargs, -): +) -> bool: """Sorts any imports within the provided code stream, outputs to the provided output stream. - Directly returns nothing. + Returns `True` if anything is modified from the original input stream, otherwise `False`. - **input_stream**: The stream of code with imports that need to be sorted. - **output_stream**: The stream where sorted imports should be written to. @@ -280,8 +280,9 @@ def sort_file( show_diff: Union[bool, TextIO] = False, write_to_stdout: bool = False, **config_kwargs, -): +) -> bool: """Sorts and formats any groups of imports imports within the provided file or Path. + Returns `True` if the file has been changed, otherwise `False`. - **filename**: The name or Path of the file to format. - **extension**: The file extension that contains imports. Defaults to filename extension or py. @@ -341,7 +342,7 @@ def sort_file( str(source_file.path) ) ): - return + return False source_file.stream.close() tmp_file.replace(source_file.path) if not config.quiet: @@ -356,6 +357,8 @@ def sort_file( except IntroducedSyntaxErrors: # pragma: no cover warn(f"{file_path} unable to sort as isort introduces new syntax errors") + return changed + def _config( path: Optional[Path] = None, config: Config = DEFAULT_CONFIG, **config_kwargs From 496e1c8e090ae5e2494e36d104d0f2b4ea8b206e Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 4 Aug 2020 22:00:25 -0700 Subject: [PATCH 0910/1439] Resolved #1181: Implemented ability to specify the types of imports. --- CHANGELOG.md | 1 + isort/settings.py | 3 +++ isort/sorting.py | 10 ++++++++-- tests/test_ticketed_features.py | 24 ++++++++++++++++++++++++ 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 354f0b482..2f548b3d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. ### 5.3.0 TBD - Implemented ability to treat all or select comments as code (issue #1357) - Implemented ability to use different configs for different file extensions (issue #1162) + - Implemented ability to specify the types of imports (issue #1181) - Added experimental support for sorting literals (issue #1358) - Added experimental support for sorting and deduping groupings of assignments. - Improved handling of deprecated single line variables for usage with Visual Studio Code (issue #1363) diff --git a/isort/settings.py b/isort/settings.py index 7f8bbe23d..00a67d4da 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -183,6 +183,9 @@ class _Config: treat_all_comments_as_code: bool = False supported_extensions: FrozenSet[str] = SUPPORTED_EXTENSIONS blocked_extensions: FrozenSet[str] = BLOCKED_EXTENSIONS + constants: FrozenSet[str] = frozenset() + classes: FrozenSet[str] = frozenset() + variables: FrozenSet[str] = frozenset() def __post_init__(self): py_version = self.py_version diff --git a/isort/sorting.py b/isort/sorting.py index ea5cafc8b..4d010a11a 100644 --- a/isort/sorting.py +++ b/isort/sorting.py @@ -26,9 +26,15 @@ def module_key( module_name = str(module_name) if sub_imports and config.order_by_type: - if module_name.isupper() and len(module_name) > 1: # see issue #376 + if module_name in config.constants: prefix = "A" - elif module_name[0:1].isupper(): + elif module_name in config.classes: + prefix = "B" + elif module_name in config.variables: + prefix = "C" + elif module_name.isupper() and len(module_name) > 1: # see issue #376 + prefix = "A" + elif module_name in config.classes or module_name[0:1].isupper(): prefix = "B" else: prefix = "C" diff --git a/tests/test_ticketed_features.py b/tests/test_ticketed_features.py index 7061a9e1f..2253cecf3 100644 --- a/tests/test_ticketed_features.py +++ b/tests/test_ticketed_features.py @@ -483,3 +483,27 @@ def method(): # isort: dict y = {"b": "c", "z": "z"}""" ) + + +def test_isort_allows_setting_import_types_issue_1181(): + """Test to ensure isort provides a way to set the type of imports. + See: https://github.com/timothycrosley/isort/issues/1181 + """ + assert isort.code("from x import AA, Big, variable") == "from x import AA, Big, variable\n" + assert ( + isort.code("from x import AA, Big, variable", constants=["variable"]) + == "from x import AA, variable, Big\n" + ) + assert ( + isort.code("from x import AA, Big, variable", variables=["AA"]) + == "from x import Big, AA, variable\n" + ) + assert ( + isort.code( + "from x import AA, Big, variable", + constants=["Big"], + variables=["AA"], + classes=["variable"], + ) + == "from x import Big, variable, AA\n" + ) From 9ea344ca75e2ca8e640331f29dd69d7a803c10f9 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 4 Aug 2020 22:34:01 -0700 Subject: [PATCH 0911/1439] Fixed #953: Added support for deduping headings. --- CHANGELOG.md | 3 ++- isort/main.py | 7 +++++++ isort/output.py | 7 +++++-- isort/settings.py | 1 + tests/test_ticketed_features.py | 36 +++++++++++++++++++++++++++++++++ 5 files changed, 51 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f548b3d5..707cb2ddc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,11 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. -### 5.3.0 TBD +### 5.3.0 Aug 4, 2020 - Implemented ability to treat all or select comments as code (issue #1357) - Implemented ability to use different configs for different file extensions (issue #1162) - Implemented ability to specify the types of imports (issue #1181) + - Implemented ability to dedup import headings (issue #953) - Added experimental support for sorting literals (issue #1358) - Added experimental support for sorting and deduping groupings of assignments. - Improved handling of deprecated single line variables for usage with Visual Studio Code (issue #1363) diff --git a/isort/main.py b/isort/main.py index e9155fe0b..c9c28c527 100644 --- a/isort/main.py +++ b/isort/main.py @@ -673,6 +673,13 @@ def _build_arg_parser() -> argparse.ArgumentParser: action="append", help="Specifies what extensions isort can never be ran against.", ) + parser.add_argument( + "--dedup-headings", + dest="dedup_headings", + action="store_true", + help="Tells isort to only show an identical custom import heading comment once, even if" + " there are multiple sections with the comment set.", + ) # deprecated options parser.add_argument( diff --git a/isort/output.py b/isort/output.py index a008ee831..d745f9a44 100644 --- a/isort/output.py +++ b/isort/output.py @@ -1,7 +1,7 @@ import copy import itertools from functools import partial -from typing import Iterable, List, Tuple +from typing import Iterable, List, Set, Tuple from isort.format import format_simplified @@ -46,6 +46,7 @@ def sorted_imports( sections = base_sections + ("no_sections",) output: List[str] = [] + seen_headings: Set[str] = set() pending_lines_before = False for section in sections: straight_modules = parsed.imports[section]["straight"] @@ -152,7 +153,9 @@ def sorted_imports( continue section_title = config.import_headings.get(section_name.lower(), "") - if section_title: + if section_title and section_title not in seen_headings: + if config.dedup_headings: + seen_headings.add(section_title) section_comment = f"# {section_title}" if section_comment not in parsed.lines_without_imports[0:1]: section_output.insert(0, section_comment) diff --git a/isort/settings.py b/isort/settings.py index 00a67d4da..66aef4c92 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -186,6 +186,7 @@ class _Config: constants: FrozenSet[str] = frozenset() classes: FrozenSet[str] = frozenset() variables: FrozenSet[str] = frozenset() + dedup_headings: bool = False def __post_init__(self): py_version = self.py_version diff --git a/tests/test_ticketed_features.py b/tests/test_ticketed_features.py index 2253cecf3..5828ec9d8 100644 --- a/tests/test_ticketed_features.py +++ b/tests/test_ticketed_features.py @@ -1,6 +1,7 @@ """A growing set of tests designed to ensure when isort implements a feature described in a ticket it fully works as defined in the associated ticket. """ +from functools import partial from io import StringIO import pytest @@ -507,3 +508,38 @@ def test_isort_allows_setting_import_types_issue_1181(): ) == "from x import Big, variable, AA\n" ) + + +def test_isort_enables_deduping_section_headers_issue_953(): + """isort should provide a way to only have identical import headings show up once. + See: https://github.com/timothycrosley/isort/issues/953 + """ + isort_code = partial( + isort.code, + config=Config( + import_heading_firstparty="Local imports.", + import_heading_localfolder="Local imports.", + dedup_headings=True, + known_first_party=["isort"], + ), + ) + + assert ( + isort_code("from . import something") + == """# Local imports. +from . import something +""" + ) + assert ( + isort_code( + """from isort import y + +from . import something""" + ) + == """# Local imports. +from isort import y + +from . import something +""" + ) + assert isort_code("import os") == "import os\n" From da0058f8dde043256b7b482a97ddecf453bb7777 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 4 Aug 2020 22:38:46 -0700 Subject: [PATCH 0912/1439] Bump version to 5.3.0 --- isort/_version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/_version.py b/isort/_version.py index 15cf13501..f5752882d 100644 --- a/isort/_version.py +++ b/isort/_version.py @@ -1 +1 @@ -__version__ = "5.2.2" +__version__ = "5.3.0" diff --git a/pyproject.toml b/pyproject.toml index 29eb061f5..12f621f71 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ line-length = 100 [tool.poetry] name = "isort" -version = "5.2.2" +version = "5.3.0" description = "A Python utility / library to sort Python imports." authors = ["Timothy Crosley "] license = "MIT" From 6ed0cd3edbc42b935de74554d995f9288391ac4c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 5 Aug 2020 22:37:57 -0700 Subject: [PATCH 0913/1439] #1363: Improve warning handling around 5.0.0 upgrade, include error codes --- CHANGELOG.md | 3 + isort/main.py | 177 ++++++++++++++++++++++++---------------------- isort/settings.py | 60 +++++++++------- 3 files changed, 129 insertions(+), 111 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 707cb2ddc..4332fa48d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. +### 5.3.1 TBD + - Improve upgrade warnings to be less noisy and point to error codes for easy interoperability with Visual Studio Code (see: #1363). + ### 5.3.0 Aug 4, 2020 - Implemented ability to treat all or select comments as code (issue #1357) - Implemented ability to use different configs for different file extensions (issue #1162) diff --git a/isort/main.py b/isort/main.py index c9c28c527..b180e3eb3 100644 --- a/isort/main.py +++ b/isort/main.py @@ -121,7 +121,8 @@ def iter_source_code(paths: Iterable[str], config: Config, skipped: List[str]) - resolved_path = full_path.resolve() if resolved_path in visited_dirs: # pragma: no cover - warn(f"Likely recursive symlink detected to {resolved_path}") + if not config.quiet: + warn(f"Likely recursive symlink detected to {resolved_path}") dirnames.remove(dirname) else: visited_dirs.add(resolved_path) @@ -723,20 +724,16 @@ def _build_arg_parser() -> argparse.ArgumentParser: def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: argv = sys.argv[1:] if argv is None else list(argv) + remapped_deprecated_args = [] for index, arg in enumerate(argv): if arg in DEPRECATED_SINGLE_DASH_ARGS: - warn( - f"\n\nThe following deprecated single dash CLI flags was used: {arg}!\n" - f"It is being auto translated to -{arg} to maintain backward compatibility.\n" - f"This behavior will be REMOVED in the 6.x.x release and is not guaranteed across" - f" 5.x.x releases.\n\n" - "Please see the 5.0.0 upgrade guide:\n" - "\thttps://timothycrosley.github.io/isort/docs/upgrade_guides/5.0.0/\n" - ) + remapped_deprecated_args.append(arg) argv[index] = f"-{arg}" parser = _build_arg_parser() arguments = {key: value for key, value in vars(parser.parse_args(argv)).items() if value} + if remapped_deprecated_args: + arguments["remapped_deprecated_args"] = remapped_deprecated_args if "dont_order_by_type" in arguments: arguments["order_by_type"] = False multi_line_output = arguments.get("multi_line_output", None) @@ -790,15 +787,6 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = sys.exit("Error: arguments passed in without any paths or content.") else: return - elif file_names == ["-"] and not show_config: - arguments.setdefault("settings_path", os.getcwd()) - api.sort_stream( - input_stream=sys.stdin if stdin is None else stdin, - output_stream=sys.stdout, - **arguments, - ) - return - if "settings_path" not in arguments: arguments["settings_path"] = ( os.path.abspath(file_names[0] if file_names else ".") or os.getcwd() @@ -813,13 +801,8 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = show_diff = config_dict.pop("show_diff", False) write_to_stdout = config_dict.pop("write_to_stdout", False) deprecated_flags = config_dict.pop("deprecated_flags", False) - - if deprecated_flags: # pragma: no cover - warn( - f"\n\nThe following deprecated CLI flags were used: {', '.join(deprecated_flags)}!\n" - "Please see the 5.0.0 upgrade guide:\n" - "\thttps://timothycrosley.github.io/isort/docs/upgrade_guides/5.0.0/\n" - ) + remapped_deprecated_args = config_dict.pop("remapped_deprecated_args", False) + wrong_sorted_files = False if "src_paths" in config_dict: config_dict["src_paths"] = { @@ -830,74 +813,98 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = if show_config: print(json.dumps(config.__dict__, indent=4, separators=(",", ": "), default=_preconvert)) return - - wrong_sorted_files = False - skipped: List[str] = [] - - if config.filter_files: - filtered_files = [] - for file_name in file_names: - if config.is_skipped(Path(file_name)): - skipped.append(file_name) - else: - filtered_files.append(file_name) - file_names = filtered_files - - file_names = iter_source_code(file_names, config, skipped) - num_skipped = 0 - if config.verbose: - print(ASCII_ART) - - if jobs: - import multiprocessing - - executor = multiprocessing.Pool(jobs) - attempt_iterator = executor.imap( - functools.partial( - sort_imports, - config=config, - check=check, - ask_to_apply=ask_to_apply, - write_to_stdout=write_to_stdout, - ), - file_names, + elif file_names == ["-"]: + arguments.setdefault("settings_path", os.getcwd()) + api.sort_stream( + input_stream=sys.stdin if stdin is None else stdin, + output_stream=sys.stdout, + **arguments, ) else: - # https://github.com/python/typeshed/pull/2814 - attempt_iterator = ( - sort_imports( # type: ignore - file_name, - config=config, - check=check, - ask_to_apply=ask_to_apply, - show_diff=show_diff, - write_to_stdout=write_to_stdout, + skipped: List[str] = [] + + if config.filter_files: + filtered_files = [] + for file_name in file_names: + if config.is_skipped(Path(file_name)): + skipped.append(file_name) + else: + filtered_files.append(file_name) + file_names = filtered_files + + file_names = iter_source_code(file_names, config, skipped) + num_skipped = 0 + if config.verbose: + print(ASCII_ART) + + if jobs: + import multiprocessing + + executor = multiprocessing.Pool(jobs) + attempt_iterator = executor.imap( + functools.partial( + sort_imports, + config=config, + check=check, + ask_to_apply=ask_to_apply, + write_to_stdout=write_to_stdout, + ), + file_names, ) - for file_name in file_names - ) + else: + # https://github.com/python/typeshed/pull/2814 + attempt_iterator = ( + sort_imports( # type: ignore + file_name, + config=config, + check=check, + ask_to_apply=ask_to_apply, + show_diff=show_diff, + write_to_stdout=write_to_stdout, + ) + for file_name in file_names + ) + + for sort_attempt in attempt_iterator: + if not sort_attempt: + continue # pragma: no cover - shouldn't happen, satisfies type constraint + incorrectly_sorted = sort_attempt.incorrectly_sorted + if arguments.get("check", False) and incorrectly_sorted: + wrong_sorted_files = True + if sort_attempt.skipped: + num_skipped += ( + 1 # pragma: no cover - shouldn't happen, due to skip in iter_source_code + ) - for sort_attempt in attempt_iterator: - if not sort_attempt: - continue # pragma: no cover - shouldn't happen, satisfies type constraint - incorrectly_sorted = sort_attempt.incorrectly_sorted - if arguments.get("check", False) and incorrectly_sorted: - wrong_sorted_files = True - if sort_attempt.skipped: - num_skipped += 1 # pragma: no cover - shouldn't happen, due to skip in iter_source_code + num_skipped += len(skipped) + if num_skipped and not arguments.get("quiet", False): + if config.verbose: + for was_skipped in skipped: + warn( + f"{was_skipped} was skipped as it's listed in 'skip' setting" + " or matches a glob in 'skip_glob' setting" + ) + print(f"Skipped {num_skipped} files") + + if not config.quiet and (remapped_deprecated_args or deprecated_flags): # pragma: no cover + if remapped_deprecated_args: + warn( + "W0502: The following deprecated single dash CLI flags were used and translated: " + f"{', '.join(remapped_deprecated_args)}!" + ) + if deprecated_flags: + warn( + "W0501: The following deprecated CLI flags were used and ignored: " + f"{', '.join(deprecated_flags)}!" + ) + warn( + "W0500: Please see the 5.0.0 Upgrade guide: " + "https://timothycrosley.github.io/isort/docs/upgrade_guides/5.0.0/" + ) if wrong_sorted_files: sys.exit(1) - num_skipped += len(skipped) - if num_skipped and not arguments.get("quiet", False): - if config.verbose: - for was_skipped in skipped: - warn( - f"{was_skipped} was skipped as it's listed in 'skip' setting" - " or matches a glob in 'skip_glob' setting" - ) - print(f"Skipped {num_skipped} files") - if __name__ == "__main__": main() diff --git a/isort/settings.py b/isort/settings.py index 66aef4c92..a319ff308 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -44,6 +44,24 @@ "tox.ini", ".editorconfig", ) +DEFAULT_SKIP: FrozenSet[str] = frozenset( + { + ".venv", + "venv", + ".tox", + ".eggs", + ".git", + ".hg", + ".mypy_cache", + ".nox", + "_build", + "buck-out", + "build", + "dist", + ".pants.d", + "node_modules", + } +) CONFIG_SECTIONS: Dict[str, Tuple[str, ...]] = { ".isort.cfg": ("settings", "isort"), @@ -94,24 +112,7 @@ class _Config: py_version: str = "3" force_to_top: FrozenSet[str] = frozenset() - skip: FrozenSet[str] = frozenset( - { - ".venv", - "venv", - ".tox", - ".eggs", - ".git", - ".hg", - ".mypy_cache", - ".nox", - "_build", - "buck-out", - "build", - "dist", - ".pants.d", - "node_modules", - } - ) + skip: FrozenSet[str] = DEFAULT_SKIP skip_glob: FrozenSet[str] = frozenset() skip_gitignore: bool = False line_length: int = 79 @@ -322,7 +323,7 @@ def __init__( combined_config.pop(key) if maps_to_section in KNOWN_SECTION_MAPPING: section_name = f"known_{KNOWN_SECTION_MAPPING[maps_to_section].lower()}" - if section_name in combined_config: + if section_name in combined_config and not self.quiet: warn( f"Can't set both {key} and {section_name} in the same config file.\n" f"Default to {section_name} if unsure." @@ -334,7 +335,10 @@ def __init__( combined_config[section_name] = frozenset(value) else: known_other[import_heading] = frozenset(value) - if maps_to_section not in combined_config.get("sections", ()): + if ( + maps_to_section not in combined_config.get("sections", ()) + and not self.quiet + ): warn( f"`{key}` setting is defined, but {maps_to_section} is not" " included in `sections` config option:" @@ -395,14 +399,18 @@ def __init__( combined_config.pop("sources", None) combined_config.pop("runtime_src_paths", None) - for deprecated_option in DEPRECATED_SETTINGS: - if deprecated_option in combined_config: + deprecated_options_used = [ + option for option in combined_config if option in DEPRECATED_SETTINGS + ] + if deprecated_options_used: + for deprecated_option in deprecated_options_used: + combined_config.pop(deprecated_option) + if not self.quiet: warn( - f"\n\nThe following deprecated settings was used: {deprecated_option}!\n" - "Please see the 5.0.0 upgrade guide:\n" - "\thttps://timothycrosley.github.io/isort/docs/upgrade_guides/5.0.0/\n" + "W503: Deprecated config options were used: " + f"{', '.join(deprecated_options_used)}." + "Please see the 5.0.0 upgrade guide: bit.ly/isortv5." ) - combined_config.pop(deprecated_option) if known_other: combined_config["known_other"] = known_other From 8cce4ddb589cd1a69f7aff994dfd07dfa41ca6dc Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 7 Aug 2020 00:18:58 -0700 Subject: [PATCH 0914/1439] Update development requirements and template version --- .cruft.json | 5 +++-- poetry.lock | 46 +++++++++++++++++++++++----------------------- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/.cruft.json b/.cruft.json index 267b8fcbb..d11673a19 100644 --- a/.cruft.json +++ b/.cruft.json @@ -1,6 +1,6 @@ { "template": "https://github.com/timothycrosley/cookiecutter-python/", - "commit": "5f2c774c99969c8483d7f329e38db5b1a7fc5050", + "commit": "8bff7dfd51b1447da409bc4323e6128dadce0310", "context": { "cookiecutter": { "full_name": "Timothy Crosley", @@ -11,5 +11,6 @@ "version": "4.3.21", "_template": "https://github.com/timothycrosley/cookiecutter-python/" } - } + }, + "directory": "" } \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 8baff3a61..9a482813c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -202,7 +202,7 @@ description = "Allows you to maintain all the necessary cruft for packaging and name = "cruft" optional = false python-versions = ">=3.6,<4.0" -version = "1.1.2" +version = "1.2.0" [package.dependencies] cookiecutter = ">=1.6,<2.0" @@ -416,7 +416,7 @@ description = "Chromium HSTS Preload list as a Python package and updated daily" name = "hstspreload" optional = false python-versions = ">=3.6" -version = "2020.7.22" +version = "2020.8.5" [[package]] category = "dev" @@ -474,7 +474,7 @@ description = "A library for property-based testing" name = "hypothesis" optional = false python-versions = ">=3.5.2" -version = "5.23.8" +version = "5.23.11" [package.dependencies] attrs = ">=19.2.0" @@ -781,7 +781,7 @@ description = "A Material Design theme for MkDocs" name = "mkdocs-material" optional = false python-versions = "*" -version = "5.5.0" +version = "5.5.3" [package.dependencies] Pygments = ">=2.4" @@ -1471,8 +1471,8 @@ category = "main" description = "Style preserving TOML library" name = "tomlkit" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.6.0" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.7.0" [[package]] category = "dev" @@ -1489,7 +1489,7 @@ marker = "python_version > \"2.7\"" name = "tqdm" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*" -version = "4.48.0" +version = "4.48.2" [package.extras] dev = ["py-make (>=0.1.0)", "twine", "argopt", "pydoc-markdown"] @@ -1576,7 +1576,7 @@ description = "Find dead code" name = "vulture" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "1.5" +version = "1.6" [[package]] category = "dev" @@ -1635,7 +1635,7 @@ pipfile_deprecated_finder = ["pipreqs", "tomlkit", "requirementslib"] requirements_deprecated_finder = ["pipreqs", "pip-api"] [metadata] -content-hash = "8f4faa0c61a376603d64c87851e5a0e464698d4be31d7a29d33ed31df439b877" +content-hash = "e108e052a0d86d747d9070739af1bd843af0855b0f3951ab9ed0d5205ca6e679" python-versions = "^3.6" [metadata.files] @@ -1742,8 +1742,8 @@ coverage = [ {file = "coverage-5.2.1.tar.gz", hash = "sha256:a34cb28e0747ea15e82d13e14de606747e9e484fb28d63c999483f5d5188e89b"}, ] cruft = [ - {file = "cruft-1.1.2-py3-none-any.whl", hash = "sha256:3864a6775381a69b36720401a9db1714c20af51190c8378fcd5de4560aeb9282"}, - {file = "cruft-1.1.2.tar.gz", hash = "sha256:05a224047a5c705a1f8e51ea6cab5cc42570a1ee3cdad36eef5a1611c9f9876e"}, + {file = "cruft-1.2.0-py3-none-any.whl", hash = "sha256:d85a0ea917eef8456570a6d589224b3115769d26202bfcbf3947d3327f089b2a"}, + {file = "cruft-1.2.0.tar.gz", hash = "sha256:76ec1f9dc6d854b0be5035dd112c610a7e3c9a77efc9282492aad995deaf3cd6"}, ] dataclasses = [ {file = "dataclasses-0.6-py3-none-any.whl", hash = "sha256:454a69d788c7fda44efd71e259be79577822f5e3f53f029a22d08004e951dc9f"}, @@ -1832,8 +1832,8 @@ hpack = [ {file = "hpack-3.0.0.tar.gz", hash = "sha256:8eec9c1f4bfae3408a3f30500261f7e6a65912dc138526ea054f9ad98892e9d2"}, ] hstspreload = [ - {file = "hstspreload-2020.7.22-py3-none-any.whl", hash = "sha256:79edbdbd09346b4c5cf729384498818943114b0a4f939a5f80abacbc47aa2197"}, - {file = "hstspreload-2020.7.22.tar.gz", hash = "sha256:7bc3d59d3f8c8dd03f0266f7bb309070e6a968edc19d29a812ebd49b280c5965"}, + {file = "hstspreload-2020.8.5-py3-none-any.whl", hash = "sha256:7b6b4b26a6ac4d9522dd547eab7049cedd7f448d9009167ba37d3e6102e562d7"}, + {file = "hstspreload-2020.8.5.tar.gz", hash = "sha256:eaf0cb2e8a837c18ddce9e02574e41cec76aed7223b7805e4d89c8f604c26eed"}, ] httpcore = [ {file = "httpcore-0.9.1-py3-none-any.whl", hash = "sha256:9850fe97a166a794d7e920590d5ec49a05488884c9fc8b5dba8561effab0c2a0"}, @@ -1852,8 +1852,8 @@ hyperframe = [ {file = "hyperframe-5.2.0.tar.gz", hash = "sha256:a9f5c17f2cc3c719b917c4f33ed1c61bd1f8dfac4b1bd23b7c80b3400971b41f"}, ] hypothesis = [ - {file = "hypothesis-5.23.8-py3-none-any.whl", hash = "sha256:3d40ba66042b15d6653fb0360728007144f7e540efdfe0c81cdd38587d34583b"}, - {file = "hypothesis-5.23.8.tar.gz", hash = "sha256:772e85dd18c9670891e0da1b585c6fb5cffb498a681a5feca94086a70bcef07a"}, + {file = "hypothesis-5.23.11-py3-none-any.whl", hash = "sha256:c0ed9cbbba273fb4ff4148ff539b73b52d5b6422b1bb86a7901f8cbb43f49db3"}, + {file = "hypothesis-5.23.11.tar.gz", hash = "sha256:69882d263175bd4731200c35c1bdfdc0a6c1af08a85672cdc51af26b18e3cac7"}, ] hypothesis-auto = [ {file = "hypothesis-auto-1.1.4.tar.gz", hash = "sha256:5e2c2fb09dc09842512d80630bb792359a1d33d2c0473ad47ee23da0be9e32b1"}, @@ -1975,8 +1975,8 @@ mkdocs = [ {file = "mkdocs-1.1.2.tar.gz", hash = "sha256:f0b61e5402b99d7789efa032c7a74c90a20220a9c81749da06dbfbcbd52ffb39"}, ] mkdocs-material = [ - {file = "mkdocs-material-5.5.0.tar.gz", hash = "sha256:6497ce2aa1ee3473a4e11897ffaade84419297388ba9459caeb6255c848e682c"}, - {file = "mkdocs_material-5.5.0-py2.py3-none-any.whl", hash = "sha256:b2c113340a9843d955383e8138dc0c5b29eb47dbfa279fbfd6e5f44545398cbe"}, + {file = "mkdocs-material-5.5.3.tar.gz", hash = "sha256:c604e1600b8f59827c53ce29505070fa24766a5ea32c18c2a99fe5769ee4b31c"}, + {file = "mkdocs_material-5.5.3-py2.py3-none-any.whl", hash = "sha256:a1d92cc8e42a83235e4be1ea6906cf839f2f9d6fb4709c9ea0b20591fa5bfc14"}, ] mkdocs-material-extensions = [ {file = "mkdocs-material-extensions-1.0.tar.gz", hash = "sha256:17d7491e189af75700310b7ec33c6c48a22060b8b445001deca040cb60471cde"}, @@ -2275,8 +2275,8 @@ toml = [ {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"}, ] tomlkit = [ - {file = "tomlkit-0.6.0-py2.py3-none-any.whl", hash = "sha256:e5d5f20809c2b09276a6c5d98fb0202325aee441a651db84ac12e0812ab7e569"}, - {file = "tomlkit-0.6.0.tar.gz", hash = "sha256:74f976908030ff164c0aa1edabe3bf83ea004b3daa5b0940b9c86a060c004e9a"}, + {file = "tomlkit-0.7.0-py2.py3-none-any.whl", hash = "sha256:6babbd33b17d5c9691896b0e68159215a9387ebfa938aa3ac42f4a4beeb2b831"}, + {file = "tomlkit-0.7.0.tar.gz", hash = "sha256:ac57f29693fab3e309ea789252fcce3061e19110085aa31af5446ca749325618"}, ] tornado = [ {file = "tornado-6.0.4-cp35-cp35m-win32.whl", hash = "sha256:5217e601700f24e966ddab689f90b7ea4bd91ff3357c3600fa1045e26d68e55d"}, @@ -2290,8 +2290,8 @@ tornado = [ {file = "tornado-6.0.4.tar.gz", hash = "sha256:0fe2d45ba43b00a41cd73f8be321a44936dc1aba233dee979f17a042b83eb6dc"}, ] tqdm = [ - {file = "tqdm-4.48.0-py2.py3-none-any.whl", hash = "sha256:fcb7cb5b729b60a27f300b15c1ffd4744f080fb483b88f31dc8654b082cc8ea5"}, - {file = "tqdm-4.48.0.tar.gz", hash = "sha256:6baa75a88582b1db6d34ce4690da5501d2a1cb65c34664840a456b2c9f794d29"}, + {file = "tqdm-4.48.2-py2.py3-none-any.whl", hash = "sha256:1a336d2b829be50e46b84668691e0a2719f26c97c62846298dd5ae2937e4d5cf"}, + {file = "tqdm-4.48.2.tar.gz", hash = "sha256:564d632ea2b9cb52979f7956e093e831c28d441c11751682f84c86fc46e4fd21"}, ] traitlets = [ {file = "traitlets-4.3.3-py2.py3-none-any.whl", hash = "sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44"}, @@ -2339,8 +2339,8 @@ vistir = [ {file = "vistir-0.5.2.tar.gz", hash = "sha256:eff1d19ef50c703a329ed294e5ec0b0fbb35b96c1b3ee6dcdb266dddbe1e935a"}, ] vulture = [ - {file = "vulture-1.5-py2.py3-none-any.whl", hash = "sha256:7a26d9648a0366b2b15ca9edadc40dae12ee9f48519d7046bd7e84a0a8dfdeaa"}, - {file = "vulture-1.5.tar.gz", hash = "sha256:07dfab84a32867ae2636bb3998ce50a4e059556dacb0cca4dbe51a1f3cc9d6d7"}, + {file = "vulture-1.6-py2.py3-none-any.whl", hash = "sha256:79ecb77e5d61a2375e2b94dea2fd859cd77a067e55e99b8238b64196adfcdc0f"}, + {file = "vulture-1.6.tar.gz", hash = "sha256:7b94784ededbf8e2913b5142dc875d8f26de7c903b37cd2d8f478f79275f7ce9"}, ] wcwidth = [ {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, From 94762ce7c9bc6c1bb3b8b39dee155a94434e27da Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 7 Aug 2020 00:49:19 -0700 Subject: [PATCH 0915/1439] Add test case for new error based approach --- isort/main.py | 2 +- tests/test_main.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/isort/main.py b/isort/main.py index b180e3eb3..39c96b9ff 100644 --- a/isort/main.py +++ b/isort/main.py @@ -886,7 +886,7 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = ) print(f"Skipped {num_skipped} files") - if not config.quiet and (remapped_deprecated_args or deprecated_flags): # pragma: no cover + if not config.quiet and (remapped_deprecated_args or deprecated_flags): if remapped_deprecated_args: warn( "W0502: The following deprecated single dash CLI flags were used and translated: " diff --git a/tests/test_main.py b/tests/test_main.py index e4572eb43..3a85bf9c3 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -231,6 +231,10 @@ def test_main(capsys, tmpdir): out, error = capsys.readouterr() assert "Skipped" in out + # warnings should be displayed if old flags are used + with pytest.warns(UserWarning): + main.main([str(python_file), "--recursive", "-fss"]) + def test_isort_command(): """Ensure ISortCommand got registered, otherwise setuptools error must have occured""" From d83489e550dde0be7e86b59b7c38ce9e423eea76 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 7 Aug 2020 00:51:08 -0700 Subject: [PATCH 0916/1439] Bump version --- CHANGELOG.md | 2 +- isort/_version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4332fa48d..88ac20d75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. -### 5.3.1 TBD +### 5.3.1 Aug 7, 2020 - Improve upgrade warnings to be less noisy and point to error codes for easy interoperability with Visual Studio Code (see: #1363). ### 5.3.0 Aug 4, 2020 diff --git a/isort/_version.py b/isort/_version.py index f5752882d..0419a93d2 100644 --- a/isort/_version.py +++ b/isort/_version.py @@ -1 +1 @@ -__version__ = "5.3.0" +__version__ = "5.3.1" diff --git a/pyproject.toml b/pyproject.toml index 12f621f71..ac075b38e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ line-length = 100 [tool.poetry] name = "isort" -version = "5.3.0" +version = "5.3.1" description = "A Python utility / library to sort Python imports." authors = ["Timothy Crosley "] license = "MIT" From 43aa1c8f91bd460df707e745434aad326afbee2f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 7 Aug 2020 00:54:37 -0700 Subject: [PATCH 0917/1439] Formatting --- tests/test_main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_main.py b/tests/test_main.py index 3a85bf9c3..d0bfad5b1 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -234,7 +234,7 @@ def test_main(capsys, tmpdir): # warnings should be displayed if old flags are used with pytest.warns(UserWarning): main.main([str(python_file), "--recursive", "-fss"]) - + def test_isort_command(): """Ensure ISortCommand got registered, otherwise setuptools error must have occured""" From 4832796a195030b3de69887e89be7bb6a1c7432f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 7 Aug 2020 00:54:54 -0700 Subject: [PATCH 0918/1439] Formatting --- tests/test_main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_main.py b/tests/test_main.py index 3a85bf9c3..d0bfad5b1 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -234,7 +234,7 @@ def test_main(capsys, tmpdir): # warnings should be displayed if old flags are used with pytest.warns(UserWarning): main.main([str(python_file), "--recursive", "-fss"]) - + def test_isort_command(): """Ensure ISortCommand got registered, otherwise setuptools error must have occured""" From 5c0ea8fba3ec4cf7b6fc2756fdf8e3d8c5db56e9 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 7 Aug 2020 01:21:34 -0700 Subject: [PATCH 0919/1439] Hotfix release to fix warning code --- CHANGELOG.md | 3 +++ isort/_version.py | 2 +- isort/settings.py | 2 +- pyproject.toml | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88ac20d75..d45e7db49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. +### 5.3.2 [Hotfix] Aug 7, 2020 + - Fixed incorrect warning code (W503->W0503). + ### 5.3.1 Aug 7, 2020 - Improve upgrade warnings to be less noisy and point to error codes for easy interoperability with Visual Studio Code (see: #1363). diff --git a/isort/_version.py b/isort/_version.py index 0419a93d2..07f0e9e28 100644 --- a/isort/_version.py +++ b/isort/_version.py @@ -1 +1 @@ -__version__ = "5.3.1" +__version__ = "5.3.2" diff --git a/isort/settings.py b/isort/settings.py index a319ff308..68f09bb6b 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -407,7 +407,7 @@ def __init__( combined_config.pop(deprecated_option) if not self.quiet: warn( - "W503: Deprecated config options were used: " + "W0503: Deprecated config options were used: " f"{', '.join(deprecated_options_used)}." "Please see the 5.0.0 upgrade guide: bit.ly/isortv5." ) diff --git a/pyproject.toml b/pyproject.toml index ac075b38e..f19a5c9cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ line-length = 100 [tool.poetry] name = "isort" -version = "5.3.1" +version = "5.3.2" description = "A Python utility / library to sort Python imports." authors = ["Timothy Crosley "] license = "MIT" From 83ac8cf22fa6f8e3da331d22f9971b05242307fc Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 7 Aug 2020 17:30:48 -0700 Subject: [PATCH 0920/1439] Update link --- docs/warning_and_error_codes/W0500.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 docs/warning_and_error_codes/W0500.md diff --git a/docs/warning_and_error_codes/W0500.md b/docs/warning_and_error_codes/W0500.md new file mode 100644 index 000000000..2a2148088 --- /dev/null +++ b/docs/warning_and_error_codes/W0500.md @@ -0,0 +1,22 @@ +# W0500 Warning Codes + +The W0500 error codes are reserved for warnings related to a major release of the isort project. +Generally, the existence of any of these will trigger one additional warning listing the upgrade guide. + +For the most recent upgrade guide, see: [The 5.0.0 Upgrade Guide.](https://timothycrosley.github.io/isort/docs/upgrade_guides/5.0.0/). + +## W0501: Deprecated CLI flags were included that will be ignored. + +This warning will be shown if a CLI flag is passed into the isort command that is no longer supported but can safely be ignored. +Often, this happens because an argument used to be required to turn on a feature that then became the default. An example of this +is `--recursive` which became the default behavior for all folders passed-in starting with 5.0.0. + +## W0502: Deprecated CLI flags were included that will safely be remapped. + +This warning will be shown if a CLI flag is passed into the isort command that is no longer supported but can safely be remapped to the new version of the flag. If you encounter this warning, you must update the argument to match the new flag +before the next major release. + +## W0503: Deprecated config options were ignored. + +This warning will be shown if a deprecated config option is defined in the Project's isort config file, but can safely be ignored. +This is similar to `W0500` but dealing with config files rather than CLI flags. From 85e07d44cc49ebbe605c24eefc6779968db3d684 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 7 Aug 2020 22:53:47 -0700 Subject: [PATCH 0921/1439] Update pre-commit instructions --- .pre-commit-config.yaml | 5 +++++ docs/upgrade_guides/5.0.0.md | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..796fbb110 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,5 @@ +repos: + - repo: https://github.com/timothycrosley/isort + rev: 5.3.0 + hooks: + - id: isort diff --git a/docs/upgrade_guides/5.0.0.md b/docs/upgrade_guides/5.0.0.md index d5d518e87..0b17595dd 100644 --- a/docs/upgrade_guides/5.0.0.md +++ b/docs/upgrade_guides/5.0.0.md @@ -78,7 +78,7 @@ isort now includes an optimized precommit configuration in the repo itself. To u ``` - repo: https://github.com/timothycrosley/isort - rev: master + rev: 5.3.2 hooks: - id: isort ``` From df8fdc5c1b3c07e01c70e42ecd8a49e3552dada1 Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Tue, 11 Aug 2020 09:39:31 +0300 Subject: [PATCH 0922/1439] Adds length_sort_straight option --- docs/configuration/options.md | 13 +++++++++++++ isort/main.py | 7 +++++++ isort/output.py | 5 ++++- isort/settings.py | 1 + isort/sorting.py | 7 ++++++- tests/test_isort.py | 23 +++++++++++++++++++++++ 6 files changed, 54 insertions(+), 2 deletions(-) diff --git a/docs/configuration/options.md b/docs/configuration/options.md index 01ac61280..dee1a8600 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -293,6 +293,19 @@ Sort imports by their string length. - --ls - --length-sort +## Length Sort Straight Imports + +Sort straight imports by their string length. Similar to `length_sort` but applies only to +straight imports and doesn't affect from imports. + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** length_sort_straight +**CLI Flags:** + +- --lss +- --length-sort-straight + ## Length Sort Sections **No Description** diff --git a/isort/main.py b/isort/main.py index 39c96b9ff..b591edb03 100644 --- a/isort/main.py +++ b/isort/main.py @@ -327,6 +327,13 @@ def _build_arg_parser() -> argparse.ArgumentParser: dest="length_sort", action="store_true", ) + parser.add_argument( + "--lss", + "--length-sort-straight", + help="Sort straight imports by their string length.", + dest="length_sort_straight", + action="store_true", + ) parser.add_argument( "-m", "--multi-line", diff --git a/isort/output.py b/isort/output.py index d745f9a44..33321b743 100644 --- a/isort/output.py +++ b/isort/output.py @@ -51,7 +51,10 @@ def sorted_imports( for section in sections: straight_modules = parsed.imports[section]["straight"] straight_modules = sorting.naturally( - straight_modules, key=lambda key: sorting.module_key(key, config, section_name=section) + straight_modules, + key=lambda key: sorting.module_key( + key, config, section_name=section, straight_import=True + ), ) from_modules = parsed.imports[section]["from"] from_modules = sorting.naturally( diff --git a/isort/settings.py b/isort/settings.py index 68f09bb6b..304e39caf 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -132,6 +132,7 @@ class _Config: indent: str = " " * 4 comment_prefix: str = " #" length_sort: bool = False + length_sort_straight: bool = False length_sort_sections: FrozenSet[str] = frozenset() add_imports: FrozenSet[str] = frozenset() remove_imports: FrozenSet[str] = frozenset() diff --git a/isort/sorting.py b/isort/sorting.py index 4d010a11a..1664a2f28 100644 --- a/isort/sorting.py +++ b/isort/sorting.py @@ -13,6 +13,7 @@ def module_key( sub_imports: bool = False, ignore_case: bool = False, section_name: Optional[Any] = None, + straight_import: Optional[bool] = False, ) -> str: match = re.match(r"^(\.+)\s*(.*)", module_name) if match: @@ -41,7 +42,11 @@ def module_key( if not config.case_sensitive: module_name = module_name.lower() - length_sort = config.length_sort or str(section_name).lower() in config.length_sort_sections + length_sort = ( + config.length_sort + or (config.length_sort_straight and straight_import) + or str(section_name).lower() in config.length_sort_sections + ) _length_sort_maybe = length_sort and (str(len(module_name)) + ":" + module_name) or module_name return f"{module_name in config.force_to_top and 'A' or 'B'}{prefix}{_length_sort_maybe}" diff --git a/tests/test_isort.py b/tests/test_isort.py index 0f7d71365..2ad132fd3 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -605,11 +605,33 @@ def test_length_sort() -> None: ) +def test_length_sort_straight() -> None: + """Test setting isort to sort straight imports on length instead of alphabetically.""" + test_input = ( + "import medium_sizeeeeeeeeeeeeee\n" + "import shortie\n" + "import looooooooooooooooooooooooooooooooooooooong\n" + "from medium_sizeeeeeeeeeeeeee import b\n" + "from shortie import c\n" + "from looooooooooooooooooooooooooooooooooooooong import a\n" + ) + test_output = isort.code(test_input, length_sort_straight=True) + assert test_output == ( + "import shortie\n" + "import medium_sizeeeeeeeeeeeeee\n" + "import looooooooooooooooooooooooooooooooooooooong\n" + "from looooooooooooooooooooooooooooooooooooooong import a\n" + "from medium_sizeeeeeeeeeeeeee import b\n" + "from shortie import c\n" + ) + + def test_length_sort_section() -> None: """Test setting isort to sort on length instead of alphabetically for a specific section.""" test_input = ( "import medium_sizeeeeeeeeeeeeee\n" "import shortie\n" + "import datetime\n" "import sys\n" "import os\n" "import looooooooooooooooooooooooooooooooooooooong\n" @@ -619,6 +641,7 @@ def test_length_sort_section() -> None: assert test_output == ( "import os\n" "import sys\n" + "import datetime\n" "\n" "import looooooooooooooooooooooooooooooooooooooong\n" "import medium_sizeeeeeeeeeeeeea\n" From 4c3b8b8986b20677f702812d63b94604f618ce8a Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Tue, 11 Aug 2020 15:18:32 +0300 Subject: [PATCH 0923/1439] Complete documentation for multi line modes. --- README.md | 34 ++++++++++++++++++++++++++++++++++ docs/configuration/options.md | 2 +- isort/main.py | 4 +++- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ed5eff44d..99e911472 100644 --- a/README.md +++ b/README.md @@ -251,6 +251,40 @@ from third_party import lib3 ... ``` +**8 - Vertical Hanging Indent Bracket** + +Same as Mode 3 - _Vertical Hanging Indent_ but the closing parentheses +on the last line is indented. + +```python +from third_party import ( + lib1, + lib2, + lib3, + lib4, + ) +``` + +**9 - Vertical Prefix From Module Import** + +Starts a new line with the same `from MODULE import ` prefix when lines are longer than the line length limit. + +```python +from third_party import lib1, lib2, lib3 +from third_party import lib4, lib5, lib6 +``` + +**10 - Hanging Indent With Parentheses** + +Same as Mode 2 - _Hanging Indent_ but uses parentheses instead of backslash +for wrapping long lines. + +```python +from third_party import ( + lib1, lib2, lib3, + lib4, lib5, lib6) +``` + Note: to change the how constant indents appear - simply change the indent property with the following accepted formats: diff --git a/docs/configuration/options.md b/docs/configuration/options.md index 01ac61280..86f891fc4 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -225,7 +225,7 @@ known_airflow = ['airflow'] ## Multi Line Output -Multi line output (0-grid, 1-vertical, 2-hanging, 3-vert-hanging, 4-vert-grid, 5-vert-grid-grouped, 6-vert-grid-grouped-no-comma). +Multi line output (0-grid, 1-vertical, 2-hanging, 3-vert-hanging, 4-vert-grid, 5-vert-grid-grouped, 6-vert-grid-grouped-no-comma, 7-noqa, 8-vertical-hanging-indent-bracket, 9-vertical-prefix-from-module-import, 10-hanging-indent-with-parentheses). **Type:** Wrapmodes **Default:** `WrapModes.GRID` diff --git a/isort/main.py b/isort/main.py index 39c96b9ff..fc7a51744 100644 --- a/isort/main.py +++ b/isort/main.py @@ -335,7 +335,9 @@ def _build_arg_parser() -> argparse.ArgumentParser: + [str(mode.value) for mode in WrapModes.__members__.values()], type=str, help="Multi line output (0-grid, 1-vertical, 2-hanging, 3-vert-hanging, 4-vert-grid, " - "5-vert-grid-grouped, 6-vert-grid-grouped-no-comma).", + "5-vert-grid-grouped, 6-vert-grid-grouped-no-comma, 7-noqa, " + "8-vertical-hanging-indent-bracket, 9-vertical-prefix-from-module-import, " + "10-hanging-indent-with-parentheses).", ) parser.add_argument( "-n", From 4892db7d02a60affafe2f609c17c67354cbce691 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 11 Aug 2020 21:45:55 -0700 Subject: [PATCH 0924/1439] Add length sort feature to changelog for next relaese --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d45e7db49..41b36710a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. +### 5.4.0 TBD + - Implemented #1373: support for length sort only of direct (AKA straight) imports. + ### 5.3.2 [Hotfix] Aug 7, 2020 - Fixed incorrect warning code (W503->W0503). From c9c176082cc8df37b8187c444ad85942704fad9c Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Wed, 12 Aug 2020 18:11:26 +0300 Subject: [PATCH 0925/1439] Bugfix #1321: --combine-as loses comment --- isort/output.py | 24 ++++++++++++++---------- isort/parse.py | 12 ++++++++++-- tests/test_regressions.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 12 deletions(-) diff --git a/isort/output.py b/isort/output.py index 33321b743..086ece72f 100644 --- a/isort/output.py +++ b/isort/output.py @@ -301,6 +301,11 @@ def _with_from_imports( new_section_output.extend(above_comments) if "*" in from_imports and config.combine_star: + if config.combine_as_imports: + comments = list(comments or ()) + comments += parsed.categorized_comments["from"].pop( + f"{module}.__combined_as__", [] + ) import_statement = wrap.line( with_comments( comments, @@ -385,7 +390,6 @@ def _with_from_imports( for as_import in as_imports[from_import] ) - star_import = False if "*" in from_imports: new_section_output.append( with_comments( @@ -396,7 +400,6 @@ def _with_from_imports( ) ) from_imports.remove("*") - star_import = True comments = None for from_import in copy.copy(from_imports): @@ -428,15 +431,16 @@ def _with_from_imports( ) ): from_import_section.append(from_imports.pop(0)) - if star_import: - import_statement = import_start + (", ").join(from_import_section) - else: - import_statement = with_comments( - comments, - import_start + (", ").join(from_import_section), - removed=config.ignore_comments, - comment_prefix=config.comment_prefix, + if config.combine_as_imports: + comments = parsed.categorized_comments["from"].pop( + f"{module}.__combined_as__", () ) + import_statement = with_comments( + comments, + import_start + (", ").join(from_import_section), + removed=config.ignore_comments, + comment_prefix=config.comment_prefix, + ) if not from_import_section: import_statement = "" diff --git a/isort/parse.py b/isort/parse.py index 350188a7e..2ab43acfc 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -320,10 +320,12 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte if "as" in just_imports and (just_imports.index("as") + 1) < len(just_imports): straight_import = False while "as" in just_imports: + nested_module = None as_index = just_imports.index("as") if type_of_import == "from": nested_module = just_imports[as_index - 1] - module = just_imports[0] + "." + nested_module + top_level_module = just_imports[0] + module = top_level_module + "." + nested_module as_name = just_imports[as_index + 1] if nested_module == as_name and config.remove_redundant_aliases: pass @@ -336,7 +338,13 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte pass elif as_name not in as_map["straight"][module]: as_map["straight"][module].append(as_name) - if not config.combine_as_imports: + + if config.combine_as_imports and nested_module: + categorized_comments["from"].setdefault( + f"{top_level_module}.__combined_as__", [] + ).extend(comments) + comments = [] + else: categorized_comments["straight"][module] = comments comments = [] del just_imports[as_index : as_index + 2] diff --git a/tests/test_regressions.py b/tests/test_regressions.py index 5d1d0413a..8236329ba 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -492,3 +492,31 @@ def test_windows_diff_too_large_misrepresentative_issue_1348(test_path): assert diff_output.read().endswith( "-1,5 +1,5 @@\n+import a\r\n import b\r\n" "-import a\r\n \r\n \r\n def func():\r\n" ) + + +def test_combine_as_does_not_lose_comments_issue_1321(): + """Test to ensure isort doesn't lose comments when --combine-as is used. + See: https://github.com/timothycrosley/isort/issues/1321 + """ + test_input = """ +from foo import * # noqa +from foo import bar as quux # other +from foo import x as a # noqa + +import operator as op # op comment +import datetime as dtime # dtime comment + +from datetime import date as d # dcomm +from datetime import datetime as dt # dtcomm +""" + + expected_output = """ +import datetime as dtime # dtime comment +import operator as op # op comment +from datetime import date as d, datetime as dt # dcomm; dtcomm + +from foo import * # noqa +from foo import bar as quux, x as a # other; noqa +""" + + assert isort.code(test_input, combine_as_imports=True) == expected_output From 680c92b752809eb8bb4fcff884f47a848a3b100a Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Wed, 12 Aug 2020 18:30:38 +0300 Subject: [PATCH 0926/1439] Bugfix #1375: --dont-order-by-type broken --- isort/main.py | 1 + tests/test_main.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/isort/main.py b/isort/main.py index 5a62b4c12..1c68f66d9 100644 --- a/isort/main.py +++ b/isort/main.py @@ -745,6 +745,7 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: arguments["remapped_deprecated_args"] = remapped_deprecated_args if "dont_order_by_type" in arguments: arguments["order_by_type"] = False + del arguments["dont_order_by_type"] multi_line_output = arguments.get("multi_line_output", None) if multi_line_output: if multi_line_output.isdigit(): diff --git a/tests/test_main.py b/tests/test_main.py index d0bfad5b1..0d2738710 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -39,6 +39,8 @@ def test_parse_args(): assert main.parse_args([]) == {} assert main.parse_args(["--multi-line", "1"]) == {"multi_line_output": WrapModes.VERTICAL} assert main.parse_args(["--multi-line", "GRID"]) == {"multi_line_output": WrapModes.GRID} + assert main.parse_args(["--dont-order-by-type"]) == {"order_by_type": False} + assert main.parse_args(["--dt"]) == {"order_by_type": False} def test_ascii_art(capsys): From eaee9e6a040aae6cc75d446eae7b6a2f403534a7 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 12 Aug 2020 20:47:09 -0700 Subject: [PATCH 0927/1439] Bump version to 5.4.0 --- CHANGELOG.md | 4 +++- isort/_version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41b36710a..1f2c86b85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,10 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. -### 5.4.0 TBD +### 5.4.0 Aug 12, 2020 - Implemented #1373: support for length sort only of direct (AKA straight) imports. + - Fixed #1380: --combine-as loses # noqa. + - Fixed #1375: --dont-order-by-type CLI broken. ### 5.3.2 [Hotfix] Aug 7, 2020 - Fixed incorrect warning code (W503->W0503). diff --git a/isort/_version.py b/isort/_version.py index 07f0e9e28..fc30498fa 100644 --- a/isort/_version.py +++ b/isort/_version.py @@ -1 +1 @@ -__version__ = "5.3.2" +__version__ = "5.4.0" diff --git a/pyproject.toml b/pyproject.toml index f19a5c9cc..2359eed8e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ line-length = 100 [tool.poetry] name = "isort" -version = "5.3.2" +version = "5.4.0" description = "A Python utility / library to sort Python imports." authors = ["Timothy Crosley "] license = "MIT" From 2952db41650211984fe64ac0b856881627e55aeb Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 13 Aug 2020 01:23:10 -0700 Subject: [PATCH 0928/1439] Fix #1381: combine_as losing non as imports, in some cases. --- CHANGELOG.md | 3 +++ isort/_version.py | 2 +- isort/output.py | 4 ++-- pyproject.toml | 2 +- tests/test_regressions.py | 15 +++++++++++++++ 5 files changed, 22 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f2c86b85..7fd5957fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. +### 5.4.1 [Hotfix] Aug 13, 2020 + - Fixed #1381: --combine-as loses # noqa in different circumstances. + ### 5.4.0 Aug 12, 2020 - Implemented #1373: support for length sort only of direct (AKA straight) imports. - Fixed #1380: --combine-as loses # noqa. diff --git a/isort/_version.py b/isort/_version.py index fc30498fa..1e41bf8f7 100644 --- a/isort/_version.py +++ b/isort/_version.py @@ -1 +1 @@ -__version__ = "5.4.0" +__version__ = "5.4.1" diff --git a/isort/output.py b/isort/output.py index 086ece72f..6a21c1315 100644 --- a/isort/output.py +++ b/isort/output.py @@ -432,8 +432,8 @@ def _with_from_imports( ): from_import_section.append(from_imports.pop(0)) if config.combine_as_imports: - comments = parsed.categorized_comments["from"].pop( - f"{module}.__combined_as__", () + comments = (comments or []) + list( + parsed.categorized_comments["from"].pop(f"{module}.__combined_as__", ()) ) import_statement = with_comments( comments, diff --git a/pyproject.toml b/pyproject.toml index 2359eed8e..bc6f2a7c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ line-length = 100 [tool.poetry] name = "isort" -version = "5.4.0" +version = "5.4.1" description = "A Python utility / library to sort Python imports." authors = ["Timothy Crosley "] license = "MIT" diff --git a/tests/test_regressions.py b/tests/test_regressions.py index 8236329ba..6fae92d2b 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -520,3 +520,18 @@ def test_combine_as_does_not_lose_comments_issue_1321(): """ assert isort.code(test_input, combine_as_imports=True) == expected_output + + +def test_combine_as_does_not_lose_comments_issue_1381(): + """Test to ensure isort doesn't lose comments when --combine-as is used. + See: https://github.com/timothycrosley/isort/issues/1381 + """ + test_input = """ +from smtplib import SMTPConnectError, SMTPNotSupportedError # important comment +""" + assert "# important comment" in isort.code(test_input, combine_as_imports=True) + + test_input = """ +from appsettings import AppSettings, ObjectSetting, StringSetting # type: ignore +""" + assert "# type: ignore" in isort.code(test_input, combine_as_imports=True) From ed22d44f040782e34554a4ddf6acfa44584af0fa Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 13 Aug 2020 19:08:31 -0700 Subject: [PATCH 0929/1439] #1383: Known other does not work anymore with .editorconfig --- CHANGELOG.md | 3 +++ isort/settings.py | 4 +++- tests/test_isort.py | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fd5957fe..58ba50eb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. +### 5.4.2 Aug 13, 2020 + - Fixed #1383: Known other does not work anymore with .editorconfig. + ### 5.4.1 [Hotfix] Aug 13, 2020 - Fixed #1381: --combine-as loses # noqa in different circumstances. diff --git a/isort/settings.py b/isort/settings.py index 304e39caf..266f55c4c 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -654,7 +654,9 @@ def _get_config_data(file_path: str, sections: Tuple[str]) -> Dict[str, Any]: float("inf") if max_line_length == "off" else int(max_line_length) ) settings = { - key: value for key, value in settings.items() if key in _DEFAULT_SETTINGS.keys() + key: value + for key, value in settings.items() + if key in _DEFAULT_SETTINGS.keys() or key.startswith(KNOWN_PREFIX) } for key, value in settings.items(): diff --git a/tests/test_isort.py b/tests/test_isort.py index 2ad132fd3..d46862f09 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -30,6 +30,8 @@ indent_size = 4 known_first_party = isort known_third_party = kate +known_something_else = something_entirely_different +sections = FUTURE, STDLIB, THIRDPARTY, FIRSTPARTY, LOCALFOLDER, SOMETHING_ELSE ignore_frosted_errors = E103 skip = build,.tox,venv balanced_wrapping = true From f7c7bdcb1d93b5739ea57859a00962ee2b024bc0 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 13 Aug 2020 19:14:13 -0700 Subject: [PATCH 0930/1439] Ensure lack of setting population for editor config gets detected --- tests/test_isort.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_isort.py b/tests/test_isort.py index d46862f09..44dc93e6b 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -54,9 +54,12 @@ def default_settings_path(tmpdir_factory) -> Iterator[str]: config_dir = tmpdir_factory.mktemp("config") config_file = config_dir.join(".editorconfig").strpath + with open(config_file, "w") as editorconfig: editorconfig.write(TEST_DEFAULT_CONFIG) + assert Config(config_file).known_other + with config_dir.as_cwd(): yield config_dir.strpath From 72961b0bd14c76d66f215f65ee95098db871da2b Mon Sep 17 00:00:00 2001 From: Abdullah Dursun Date: Fri, 14 Aug 2020 15:23:28 +0300 Subject: [PATCH 0931/1439] Fix typo --- docs/configuration/options.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration/options.md b/docs/configuration/options.md index 8daf7a838..307169d57 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -56,7 +56,7 @@ Files that sort imports should skip over. ## Skip Gitignore -Treat project as a git respository and ignore files listed in .gitignore +Treat project as a git repository and ignore files listed in .gitignore **Type:** Bool **Default:** `False` From 2a3afd11fbbf1296900411b19a87ea91e0d7fe02 Mon Sep 17 00:00:00 2001 From: Krzysztof Jagiello Date: Fri, 14 Aug 2020 15:45:53 +0200 Subject: [PATCH 0932/1439] Fix the first known party path expansion --- isort/deprecated/finders.py | 7 +++---- isort/settings.py | 7 +++---- tests/test_isort.py | 17 ++++++++++++----- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/isort/deprecated/finders.py b/isort/deprecated/finders.py index f32e9acf2..77eb23fa4 100644 --- a/isort/deprecated/finders.py +++ b/isort/deprecated/finders.py @@ -85,14 +85,13 @@ def __init__(self, config: Config) -> None: regexp = "^" + known_pattern.replace("*", ".*").replace("?", ".?") + "$" self.known_patterns.append((re.compile(regexp), placement)) - @staticmethod - def _parse_known_pattern(pattern: str) -> List[str]: + def _parse_known_pattern(self, pattern: str) -> List[str]: """Expand pattern if identified as a directory and return found sub packages""" if pattern.endswith(os.path.sep): patterns = [ filename - for filename in os.listdir(pattern) - if os.path.isdir(os.path.join(pattern, filename)) + for filename in os.listdir(os.path.join(self.config.directory, pattern)) + if os.path.isdir(os.path.join(self.config.directory, pattern, filename)) ] else: patterns = [pattern] diff --git a/isort/settings.py b/isort/settings.py index 266f55c4c..500790bef 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -523,14 +523,13 @@ def section_comments(self) -> Tuple[str, ...]: self._section_comments = tuple(f"# {heading}" for heading in self.import_headings.values()) return self._section_comments - @staticmethod - def _parse_known_pattern(pattern: str) -> List[str]: + def _parse_known_pattern(self, pattern: str) -> List[str]: """Expand pattern if identified as a directory and return found sub packages""" if pattern.endswith(os.path.sep): patterns = [ filename - for filename in os.listdir(pattern) - if os.path.isdir(os.path.join(pattern, filename)) + for filename in os.listdir(os.path.join(self.directory, pattern)) + if os.path.isdir(os.path.join(self.directory, pattern, filename)) ] else: patterns = [pattern] diff --git a/tests/test_isort.py b/tests/test_isort.py index 44dc93e6b..36260d075 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -1072,27 +1072,33 @@ def test_thirdy_party_overrides_standard_section() -> None: assert test_output == "import os\nimport sys\n\nimport profile.test\n" -def test_known_pattern_path_expansion() -> None: +def test_known_pattern_path_expansion(tmpdir) -> None: """Test to ensure patterns ending with path sep gets expanded and nested packages treated as known patterns. """ + src_dir = tmpdir.mkdir("src") + src_dir.mkdir("foo") + src_dir.mkdir("bar") test_input = ( "from kate_plugin import isort_plugin\n" "import sys\n" - "import isort.settings\n" + "from foo import settings\n" + "import bar\n" "import this\n" "import os\n" ) test_output = isort.code( code=test_input, default_section="THIRDPARTY", - known_first_party=["./", "this", "kate_plugin", "isort"], + known_first_party=["src/", "this", "kate_plugin"], + directory=str(tmpdir), ) test_output_old_finder = isort.code( code=test_input, default_section="FIRSTPARTY", old_finders=True, - known_first_party=["./", "this", "kate_plugin", "isort"], + known_first_party=["src/", "this", "kate_plugin"], + directory=str(tmpdir), ) assert ( test_output_old_finder @@ -1101,8 +1107,9 @@ def test_known_pattern_path_expansion() -> None: "import os\n" "import sys\n" "\n" - "import isort.settings\n" + "import bar\n" "import this\n" + "from foo import settings\n" "from kate_plugin import isort_plugin\n" ) ) From e6c117cc62386a562c8f742d2ecdc69e5ab20f56 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 14 Aug 2020 17:30:37 -0700 Subject: [PATCH 0933/1439] Fix repository mispelling in CLI command --- isort/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/main.py b/isort/main.py index 1c68f66d9..58c4e1015 100644 --- a/isort/main.py +++ b/isort/main.py @@ -462,7 +462,7 @@ def _build_arg_parser() -> argparse.ArgumentParser: "--skip-gitignore", action="store_true", dest="skip_gitignore", - help="Treat project as a git respository and ignore files listed in .gitignore", + help="Treat project as a git repository and ignore files listed in .gitignore", ) inline_args_group.add_argument( "--sl", From e18c94f5e82ca7b576b8bb6cdacc574778bfa67e Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 14 Aug 2020 20:10:46 -0700 Subject: [PATCH 0934/1439] Fix issue linked in changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58ba50eb5..7740a51e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,13 +5,14 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. ### 5.4.2 Aug 13, 2020 - Fixed #1383: Known other does not work anymore with .editorconfig. + - Fixed: Regression in first known party path expansion. ### 5.4.1 [Hotfix] Aug 13, 2020 - Fixed #1381: --combine-as loses # noqa in different circumstances. ### 5.4.0 Aug 12, 2020 - Implemented #1373: support for length sort only of direct (AKA straight) imports. - - Fixed #1380: --combine-as loses # noqa. + - Fixed #1321: --combine-as loses # noqa. - Fixed #1375: --dont-order-by-type CLI broken. ### 5.3.2 [Hotfix] Aug 7, 2020 From 377d260ffa6f746693f97b46d95025afc4bd8275 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 14 Aug 2020 20:12:38 -0700 Subject: [PATCH 0935/1439] Bump version to 5.4.2 --- CHANGELOG.md | 2 +- isort/_version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7740a51e2..a7a31d901 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. -### 5.4.2 Aug 13, 2020 +### 5.4.2 Aug 14, 2020 - Fixed #1383: Known other does not work anymore with .editorconfig. - Fixed: Regression in first known party path expansion. diff --git a/isort/_version.py b/isort/_version.py index 1e41bf8f7..cfda0f8e3 100644 --- a/isort/_version.py +++ b/isort/_version.py @@ -1 +1 @@ -__version__ = "5.4.1" +__version__ = "5.4.2" diff --git a/pyproject.toml b/pyproject.toml index bc6f2a7c4..1902c9f4e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ line-length = 100 [tool.poetry] name = "isort" -version = "5.4.1" +version = "5.4.2" description = "A Python utility / library to sort Python imports." authors = ["Timothy Crosley "] license = "MIT" From 1ca2417958a9f45c8eab6678b366a2aa27e98226 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 15 Aug 2020 21:43:50 -0700 Subject: [PATCH 0936/1439] Update cruft to latest template version --- .cruft.json | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.cruft.json b/.cruft.json index d11673a19..2879d2fc6 100644 --- a/.cruft.json +++ b/.cruft.json @@ -1,6 +1,6 @@ { "template": "https://github.com/timothycrosley/cookiecutter-python/", - "commit": "8bff7dfd51b1447da409bc4323e6128dadce0310", + "commit": "4fe165a760a98a06d3fbef89aae3149767e489f3", "context": { "cookiecutter": { "full_name": "Timothy Crosley", diff --git a/pyproject.toml b/pyproject.toml index 1902c9f4e..72ce030af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,7 @@ pep8-naming = "^0.8.2" hypothesis-auto = { version = "^1.0.0" } hypothesmith = "^0.1.3" examples = { version = "^1.0.0" } -cruft = { version = "^1.1" } +cruft = { version = "^2.2" } portray = { version = "^1.3.0" } pipfile = "^0.0.2" requirementslib = "^1.5" From 66bc89ff5290d28571b1d8af620644bcba5084f9 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 15 Aug 2020 21:55:50 -0700 Subject: [PATCH 0937/1439] Update lock file to include latest dependencies --- poetry.lock | 84 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 53 insertions(+), 31 deletions(-) diff --git a/poetry.lock b/poetry.lock index 9a482813c..e7472fbd6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -202,15 +202,16 @@ description = "Allows you to maintain all the necessary cruft for packaging and name = "cruft" optional = false python-versions = ">=3.6,<4.0" -version = "1.2.0" +version = "2.2.0" [package.dependencies] +click = ">=7.1.2,<8.0.0" cookiecutter = ">=1.6,<2.0" -examples = ">=1.0,<2.0" gitpython = ">=3.0,<4.0" -hug = ">=2.6,<3.0" +typer = ">=0.3.1,<0.4.0" [package.extras] +examples = ["examples (>=1.0.2,<2.0.0)"] pyproject = ["toml (>=0.10,<0.11)"] [[package]] @@ -416,7 +417,7 @@ description = "Chromium HSTS Preload list as a Python package and updated daily" name = "hstspreload" optional = false python-versions = ">=3.6" -version = "2020.8.5" +version = "2020.8.12" [[package]] category = "dev" @@ -474,7 +475,7 @@ description = "A library for property-based testing" name = "hypothesis" optional = false python-versions = ">=3.5.2" -version = "5.23.11" +version = "5.24.4" [package.dependencies] attrs = ">=19.2.0" @@ -659,7 +660,7 @@ description = "A concrete syntax tree with AST-like properties for Python 3.5, 3 name = "libcst" optional = false python-versions = ">=3.6" -version = "0.3.8" +version = "0.3.9" [package.dependencies] pyyaml = ">=5.2" @@ -781,7 +782,7 @@ description = "A Material Design theme for MkDocs" name = "mkdocs-material" optional = false python-versions = "*" -version = "5.5.3" +version = "5.5.6" [package.dependencies] Pygments = ">=2.4" @@ -994,7 +995,7 @@ description = "Compatibility shims for pip versions 8 thru current." name = "pip-shims" optional = false python-versions = "!=3.0,!=3.1,!=3.2,!=3.3,!=3.4,>=2.7" -version = "0.5.2" +version = "0.5.3" [package.dependencies] packaging = "*" @@ -1238,7 +1239,7 @@ description = "Pytest plugin for measuring coverage." name = "pytest-cov" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "2.10.0" +version = "2.10.1" [package.dependencies] coverage = ">=4.4" @@ -1326,7 +1327,7 @@ description = "A tool for converting between pip-style and pipfile requirements. name = "requirementslib" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -version = "1.5.12" +version = "1.5.13" [package.dependencies] appdirs = "*" @@ -1518,6 +1519,23 @@ optional = false python-versions = "*" version = "1.4.1" +[[package]] +category = "dev" +description = "Typer, build great CLIs. Easy to code. Based on Python type hints." +name = "typer" +optional = false +python-versions = ">=3.6" +version = "0.3.1" + +[package.dependencies] +click = ">=7.1.1,<7.2.0" + +[package.extras] +all = ["colorama (>=0.4.3,<0.5.0)", "shellingham (>=1.3.0,<2.0.0)"] +dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)"] +doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=5.4.0,<6.0.0)", "markdown-include (>=0.5.1,<0.6.0)"] +test = ["shellingham (>=1.3.0,<2.0.0)", "pytest (>=4.4.0,<5.4.0)", "pytest-cov (>=2.10.0,<3.0.0)", "coverage (>=5.2,<6.0)", "pytest-xdist (>=1.32.0,<2.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "mypy (0.782)", "black (>=19.10b0,<20.0b0)", "isort (>=5.0.6,<6.0.0)"] + [[package]] category = "dev" description = "Backported and Experimental Type Hints for Python 3.5+" @@ -1591,8 +1609,8 @@ category = "main" description = "A built-package format for Python" name = "wheel" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "0.34.2" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +version = "0.35.1" [package.extras] test = ["pytest (>=3.0.0)", "pytest-cov"] @@ -1635,7 +1653,7 @@ pipfile_deprecated_finder = ["pipreqs", "tomlkit", "requirementslib"] requirements_deprecated_finder = ["pipreqs", "pip-api"] [metadata] -content-hash = "e108e052a0d86d747d9070739af1bd843af0855b0f3951ab9ed0d5205ca6e679" +content-hash = "5463d8238a63216a861fd6d75b1a86842f2a1ab3574a5ecb8dbe8d9183b6a874" python-versions = "^3.6" [metadata.files] @@ -1742,8 +1760,8 @@ coverage = [ {file = "coverage-5.2.1.tar.gz", hash = "sha256:a34cb28e0747ea15e82d13e14de606747e9e484fb28d63c999483f5d5188e89b"}, ] cruft = [ - {file = "cruft-1.2.0-py3-none-any.whl", hash = "sha256:d85a0ea917eef8456570a6d589224b3115769d26202bfcbf3947d3327f089b2a"}, - {file = "cruft-1.2.0.tar.gz", hash = "sha256:76ec1f9dc6d854b0be5035dd112c610a7e3c9a77efc9282492aad995deaf3cd6"}, + {file = "cruft-2.2.0-py3-none-any.whl", hash = "sha256:da3dc9ee84dea1a4ea161b5d0fa86b11ecb77480eb22b4d776572f95a96ae9fc"}, + {file = "cruft-2.2.0.tar.gz", hash = "sha256:9365ae8547bf2297c9e88dec3b28a16bd4491c4931f417b8c3c1fd8140b1e0fd"}, ] dataclasses = [ {file = "dataclasses-0.6-py3-none-any.whl", hash = "sha256:454a69d788c7fda44efd71e259be79577822f5e3f53f029a22d08004e951dc9f"}, @@ -1832,8 +1850,8 @@ hpack = [ {file = "hpack-3.0.0.tar.gz", hash = "sha256:8eec9c1f4bfae3408a3f30500261f7e6a65912dc138526ea054f9ad98892e9d2"}, ] hstspreload = [ - {file = "hstspreload-2020.8.5-py3-none-any.whl", hash = "sha256:7b6b4b26a6ac4d9522dd547eab7049cedd7f448d9009167ba37d3e6102e562d7"}, - {file = "hstspreload-2020.8.5.tar.gz", hash = "sha256:eaf0cb2e8a837c18ddce9e02574e41cec76aed7223b7805e4d89c8f604c26eed"}, + {file = "hstspreload-2020.8.12-py3-none-any.whl", hash = "sha256:64f4441066d5544873faccf2e0b5757c6670217d34dc31d362ca2977f44604ff"}, + {file = "hstspreload-2020.8.12.tar.gz", hash = "sha256:3f5c324b1eb9d924e32ffeb5fe265b879806b6e346b765f57566410344f4b41e"}, ] httpcore = [ {file = "httpcore-0.9.1-py3-none-any.whl", hash = "sha256:9850fe97a166a794d7e920590d5ec49a05488884c9fc8b5dba8561effab0c2a0"}, @@ -1852,8 +1870,8 @@ hyperframe = [ {file = "hyperframe-5.2.0.tar.gz", hash = "sha256:a9f5c17f2cc3c719b917c4f33ed1c61bd1f8dfac4b1bd23b7c80b3400971b41f"}, ] hypothesis = [ - {file = "hypothesis-5.23.11-py3-none-any.whl", hash = "sha256:c0ed9cbbba273fb4ff4148ff539b73b52d5b6422b1bb86a7901f8cbb43f49db3"}, - {file = "hypothesis-5.23.11.tar.gz", hash = "sha256:69882d263175bd4731200c35c1bdfdc0a6c1af08a85672cdc51af26b18e3cac7"}, + {file = "hypothesis-5.24.4-py3-none-any.whl", hash = "sha256:4d86b1d7bbec9caffc49dbd0037fa549c456d08aa99e468dbce5871fdbf2167b"}, + {file = "hypothesis-5.24.4.tar.gz", hash = "sha256:c3ac78ae0cebe7098bc00d8b3e16b65640c97593cceb64c9eb2331ac282fa607"}, ] hypothesis-auto = [ {file = "hypothesis-auto-1.1.4.tar.gz", hash = "sha256:5e2c2fb09dc09842512d80630bb792359a1d33d2c0473ad47ee23da0be9e32b1"}, @@ -1913,8 +1931,8 @@ lark-parser = [ {file = "lark-parser-0.9.0.tar.gz", hash = "sha256:9e7589365d6b6de1cca40b0eaec31104a3fb96a37a11a9dfd5098e95b50aa6cd"}, ] libcst = [ - {file = "libcst-0.3.8-py3-none-any.whl", hash = "sha256:d514cd36e8da5e1444ec693b65a4b6751781af02496f9a2442c8590eb0d321fc"}, - {file = "libcst-0.3.8.tar.gz", hash = "sha256:484fc3bf0b9b15773349548a466a36b137fbd94705fac8cdf25734fd8261fa17"}, + {file = "libcst-0.3.9-py3-none-any.whl", hash = "sha256:ca1744d9344f51c2c9226d0472a5a3096f8b39e4fe38441ebc2ba26babd00688"}, + {file = "libcst-0.3.9.tar.gz", hash = "sha256:b5185c84f0e4a38409aac59f53a71741bec8c1b1159c874996b3266daafe63e5"}, ] livereload = [ {file = "livereload-2.6.2.tar.gz", hash = "sha256:d1eddcb5c5eb8d2ca1fa1f750e580da624c0f7fcb734aa5780dc81b7dcbd89be"}, @@ -1975,8 +1993,8 @@ mkdocs = [ {file = "mkdocs-1.1.2.tar.gz", hash = "sha256:f0b61e5402b99d7789efa032c7a74c90a20220a9c81749da06dbfbcbd52ffb39"}, ] mkdocs-material = [ - {file = "mkdocs-material-5.5.3.tar.gz", hash = "sha256:c604e1600b8f59827c53ce29505070fa24766a5ea32c18c2a99fe5769ee4b31c"}, - {file = "mkdocs_material-5.5.3-py2.py3-none-any.whl", hash = "sha256:a1d92cc8e42a83235e4be1ea6906cf839f2f9d6fb4709c9ea0b20591fa5bfc14"}, + {file = "mkdocs-material-5.5.6.tar.gz", hash = "sha256:08af704cdfaf2a07fd5f135831df9106c589bfd422f9ef026929981433e80b9d"}, + {file = "mkdocs_material-5.5.6-py2.py3-none-any.whl", hash = "sha256:29f3637d5fb758d076344b026a67b8e316743d0c2da84b9303383f6cbeabfd5f"}, ] mkdocs-material-extensions = [ {file = "mkdocs-material-extensions-1.0.tar.gz", hash = "sha256:17d7491e189af75700310b7ec33c6c48a22060b8b445001deca040cb60471cde"}, @@ -2082,8 +2100,8 @@ pip-api = [ {file = "pip_api-0.0.12-py3-none-any.whl", hash = "sha256:5266d9c8c9585e6fdeaf5d78f17b4e68dd2d657103fb24b80b629f70fac26712"}, ] pip-shims = [ - {file = "pip_shims-0.5.2-py2.py3-none-any.whl", hash = "sha256:39193b8c4aa5e4cb82e250be58df4d5eaebe931a33b0df43b369f4ae92ee5753"}, - {file = "pip_shims-0.5.2.tar.gz", hash = "sha256:423978c27d0e24e8ecb3e82b4a6c1f607e2e364153e73d0803c671d48b23195e"}, + {file = "pip_shims-0.5.3-py2.py3-none-any.whl", hash = "sha256:16ca9f87485667b16b978b68a1aae4f9cc082c0fa018aed28567f9f34a590569"}, + {file = "pip_shims-0.5.3.tar.gz", hash = "sha256:05b00ade9d1e686a98bb656dd9b0608a933897283dc21913fad6ea5409ff7e91"}, ] pipfile = [ {file = "pipfile-0.0.2.tar.gz", hash = "sha256:f7d9f15de8b660986557eb3cc5391aa1a16207ac41bc378d03f414762d36c984"}, @@ -2172,8 +2190,8 @@ pytest = [ {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, ] pytest-cov = [ - {file = "pytest-cov-2.10.0.tar.gz", hash = "sha256:1a629dc9f48e53512fcbfda6b07de490c374b0c83c55ff7a1720b3fccff0ac87"}, - {file = "pytest_cov-2.10.0-py2.py3-none-any.whl", hash = "sha256:6e6d18092dce6fad667cd7020deed816f858ad3b49d5b5e2b1cc1c97a4dba65c"}, + {file = "pytest-cov-2.10.1.tar.gz", hash = "sha256:47bd0ce14056fdd79f93e1713f88fad7bdcc583dcd7783da86ef2f085a0bb88e"}, + {file = "pytest_cov-2.10.1-py2.py3-none-any.whl", hash = "sha256:45ec2d5182f89a81fc3eb29e3d1ed3113b9e9a873bcddb2a71faaab066110191"}, ] pytest-mock = [ {file = "pytest-mock-1.13.0.tar.gz", hash = "sha256:e24a911ec96773022ebcc7030059b57cd3480b56d4f5d19b7c370ec635e6aed5"}, @@ -2227,8 +2245,8 @@ requests = [ {file = "requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"}, ] requirementslib = [ - {file = "requirementslib-1.5.12-py2.py3-none-any.whl", hash = "sha256:5e283a74df80c27ca3ebedda5a38301d23b2d563f9d758cbfdc385fdb0f58a87"}, - {file = "requirementslib-1.5.12.tar.gz", hash = "sha256:c80ec03f70ee0b435c9e74686e61b1ddb7afd1b228ed3f6ebc64a2e5fb530b5e"}, + {file = "requirementslib-1.5.13-py2.py3-none-any.whl", hash = "sha256:cdf8aa652ac52216d156cee2b89c3c9ee53373dded0035184d0b9af569a0f10c"}, + {file = "requirementslib-1.5.13.tar.gz", hash = "sha256:fd98ea873effaede6b3394725a232bcbd3fe3985987e226109a841c85a69e2e3"}, ] rfc3986 = [ {file = "rfc3986-1.4.0-py2.py3-none-any.whl", hash = "sha256:af9147e9aceda37c91a05f4deb128d4b4b49d6b199775fd2d2927768abdc8f50"}, @@ -2320,6 +2338,10 @@ typed-ast = [ {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, ] +typer = [ + {file = "typer-0.3.1-py3-none-any.whl", hash = "sha256:778a9695e68eb26a0a0321ca9d3f1a8809783f6f083549b84c67bc2385bf014e"}, + {file = "typer-0.3.1.tar.gz", hash = "sha256:85b1e5f6369750b4220ad548ea30b881a2c502504e5a0d849db9bdf6b487bdbf"}, +] typing-extensions = [ {file = "typing_extensions-3.7.4.2-py2-none-any.whl", hash = "sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392"}, {file = "typing_extensions-3.7.4.2-py3-none-any.whl", hash = "sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5"}, @@ -2347,8 +2369,8 @@ wcwidth = [ {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, ] wheel = [ - {file = "wheel-0.34.2-py2.py3-none-any.whl", hash = "sha256:df277cb51e61359aba502208d680f90c0493adec6f0e848af94948778aed386e"}, - {file = "wheel-0.34.2.tar.gz", hash = "sha256:8788e9155fe14f54164c1b9eb0a319d98ef02c160725587ad60f14ddc57b6f96"}, + {file = "wheel-0.35.1-py2.py3-none-any.whl", hash = "sha256:497add53525d16c173c2c1c733b8f655510e909ea78cc0e29d374243544b77a2"}, + {file = "wheel-0.35.1.tar.gz", hash = "sha256:99a22d87add3f634ff917310a3d87e499f19e663413a52eb9232c447aa646c9f"}, ] yarg = [ {file = "yarg-0.1.9-py2.py3-none-any.whl", hash = "sha256:4f9cebdc00fac946c9bf2783d634e538a71c7d280a4d806d45fd4dc0ef441492"}, From 6bc66fb5558e2582fa6fe3130f337730c7af20c3 Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Tue, 18 Aug 2020 13:52:23 +0300 Subject: [PATCH 0938/1439] Demonstrates problem with #1389. Changes existing test's configuration so it has no lines between sections. The default of 1 masks the error. --- tests/test_isort.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_isort.py b/tests/test_isort.py index 36260d075..bcbc0f3a5 100644 --- a/tests/test_isort.py +++ b/tests/test_isort.py @@ -3816,6 +3816,7 @@ def test_isort_keeps_comments_issue_691() -> None: def test_isort_ensures_blank_line_between_import_and_comment() -> None: config = { "ensure_newline_before_comments": True, + "lines_between_sections": 0, "known_one": ["one"], "known_two": ["two"], "known_three": ["three"], From 681fdd37c3fb8aa11481f77652c9aaee248dbbdb Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Tue, 18 Aug 2020 13:56:59 +0300 Subject: [PATCH 0939/1439] Fixes #1389 --- isort/output.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/isort/output.py b/isort/output.py index 6a21c1315..8a72b8d02 100644 --- a/isort/output.py +++ b/isort/output.py @@ -138,12 +138,6 @@ def sorted_imports( for line in new_section_output: comments = getattr(line, "comments", ()) if comments: - if ( - config.ensure_newline_before_comments - and section_output - and section_output[-1] - ): - section_output.append("") section_output.extend(comments) section_output.append(str(line)) @@ -172,6 +166,9 @@ def sorted_imports( else: pending_lines_before = pending_lines_before or not no_lines_before + if config.ensure_newline_before_comments: + output = _ensure_newline_before_comment(output) + while output and output[-1].strip() == "": output.pop() # pragma: no cover while output and output[0].strip() == "": @@ -296,8 +293,6 @@ def _with_from_imports( comments = parsed.categorized_comments["from"].pop(module, ()) above_comments = parsed.categorized_comments["above"]["from"].pop(module, None) if above_comments: - if new_section_output and config.ensure_newline_before_comments: - new_section_output.append("") new_section_output.extend(above_comments) if "*" in from_imports and config.combine_star: @@ -550,3 +545,16 @@ def __new__(cls, value, comments): instance = super().__new__(cls, value) # type: ignore instance.comments = comments return instance + + +def _ensure_newline_before_comment(output): + new_output: List[str] = [] + + def is_comment(line): + return line and line.startswith("#") + + for line, prev_line in zip(output, [None] + output): + if is_comment(line) and prev_line != "" and not is_comment(prev_line): + new_output.append("") + new_output.append(line) + return new_output From 9a6530c23f4fb244284114bae0ee79ee278589ff Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 21 Aug 2020 21:21:39 -0700 Subject: [PATCH 0940/1439] Fixed #1398: isort: off comment doesn't work, if it's the top comment in the file. --- isort/core.py | 7 +++--- tests/test_action_comments.py | 47 +++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 tests/test_action_comments.py diff --git a/isort/core.py b/isort/core.py index 010aa7f6b..35738abd9 100644 --- a/isort/core.py +++ b/isort/core.py @@ -137,6 +137,9 @@ def process( if file_skip_comment in line: raise FileSkipComment("Passed in content") + if not in_quote and stripped_line == "# isort: off": + isort_off = True + if ( (index == 0 or (index in (1, 2) and not contains_imports)) and stripped_line.startswith("#") @@ -175,13 +178,9 @@ def process( not_imports = bool(in_quote) or in_top_comment or isort_off if not (in_quote or in_top_comment): - stripped_line = line.strip() if isort_off: if stripped_line == "# isort: on": isort_off = False - elif stripped_line == "# isort: off": - not_imports = True - isort_off = True elif stripped_line.endswith("# isort: split"): not_imports = True elif stripped_line in CODE_SORT_COMMENTS: diff --git a/tests/test_action_comments.py b/tests/test_action_comments.py new file mode 100644 index 000000000..508db0d2e --- /dev/null +++ b/tests/test_action_comments.py @@ -0,0 +1,47 @@ +"""Tests for isort action comments, such as isort: skip""" +import isort + + +def test_isort_off_and_on(): + """Test so ensure isort: off action comment and associated on action comment work together""" + + # as top of file comment + assert ( + isort.code( + """# isort: off +import a +import a + +# isort: on +import a +import a +""" + ) + == """# isort: off +import a +import a + +# isort: on +import a +""" + ) + # as middle comment + assert ( + isort.code( + """ +import a +import a + +# isort: off +import a +import a +""" + ) + == """ +import a + +# isort: off +import a +import a +""" + ) From ff1ccda3a6559d32c54b9b85f52bea5ad108d31c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 21 Aug 2020 22:08:02 -0700 Subject: [PATCH 0941/1439] Add test case for issue #1396 --- tests/test_regressions.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/test_regressions.py b/tests/test_regressions.py index 6fae92d2b..3c302e180 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -535,3 +535,32 @@ def test_combine_as_does_not_lose_comments_issue_1381(): from appsettings import AppSettings, ObjectSetting, StringSetting # type: ignore """ assert "# type: ignore" in isort.code(test_input, combine_as_imports=True) + + +def test_incorrect_grouping_when_comments_issue_1396(): + """Test to ensure isort groups import correct independent of the comments present. + See: https://github.com/timothycrosley/isort/issues/1396 + """ + assert isort.code( + """from django.shortcuts import render +from apps.profiler.models import Project +from django.contrib.auth.decorators import login_required +from django.views.generic import ( + # ListView, + # DetailView, + TemplateView, + # CreateView, + # View +) +""", + line_length=88, + known_first_party=["apps"], + known_django=["django"], + sections=["FUTURE", "STDLIB", "DJANGO", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"] + ) == """from django.contrib.auth.decorators import login_required +from django.shortcuts import render +from django.views.generic import \\ + TemplateView # ListView,; DetailView,; CreateView,; View + +from apps.profiler.models import Project +""" From ec63f343666ca18fd8efa9fad07438f996da2cf6 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 21 Aug 2020 22:48:56 -0700 Subject: [PATCH 0942/1439] Fixed #1395: reverse_relative setting doesn't have any effect when combined with force_sort_within_sections. --- CHANGELOG.md | 4 ++++ isort/output.py | 1 + isort/sorting.py | 6 ++++++ tests/test_regressions.py | 39 +++++++++++++++++++++++++++++++-------- 4 files changed, 42 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7a31d901..abcc9b37c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. +### 5.5.0 TBD + - Fixed #1398: isort: off comment doesn't work, if it's the top comment in the file. + - Fixed #1395: reverse_relative setting doesn't have any effect when combined with force_sort_within_sections. + ### 5.4.2 Aug 14, 2020 - Fixed #1383: Known other does not work anymore with .editorconfig. - Fixed: Regression in first known party path expansion. diff --git a/isort/output.py b/isort/output.py index 6a21c1315..2a918323d 100644 --- a/isort/output.py +++ b/isort/output.py @@ -130,6 +130,7 @@ def sorted_imports( force_to_top=config.force_to_top, lexicographical=config.lexicographical, length_sort=config.length_sort, + reverse_relative=config.reverse_relative, ), ) diff --git a/isort/sorting.py b/isort/sorting.py index 1664a2f28..780747a3d 100644 --- a/isort/sorting.py +++ b/isort/sorting.py @@ -57,9 +57,15 @@ def section_key( force_to_top: List[str], lexicographical: bool = False, length_sort: bool = False, + reverse_relative: bool = False, ) -> str: section = "B" + if reverse_relative and line.startswith("from ."): + match = re.match(r"^from (\.+)\s*(.*)", line) + if match: + line = f"from {' '.join(match.groups())}" + if lexicographical: line = _import_line_intro_re.sub("", _import_line_midline_import_re.sub(".", line)) else: diff --git a/tests/test_regressions.py b/tests/test_regressions.py index 3c302e180..4424f9e81 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -541,26 +541,49 @@ def test_incorrect_grouping_when_comments_issue_1396(): """Test to ensure isort groups import correct independent of the comments present. See: https://github.com/timothycrosley/isort/issues/1396 """ - assert isort.code( - """from django.shortcuts import render + assert ( + isort.code( + """from django.shortcuts import render from apps.profiler.models import Project from django.contrib.auth.decorators import login_required from django.views.generic import ( - # ListView, + # ListView, # DetailView, TemplateView, # CreateView, # View ) """, - line_length=88, - known_first_party=["apps"], - known_django=["django"], - sections=["FUTURE", "STDLIB", "DJANGO", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"] - ) == """from django.contrib.auth.decorators import login_required + line_length=88, + known_first_party=["apps"], + known_django=["django"], + sections=["FUTURE", "STDLIB", "DJANGO", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"], + ) + == """from django.contrib.auth.decorators import login_required from django.shortcuts import render from django.views.generic import \\ TemplateView # ListView,; DetailView,; CreateView,; View from apps.profiler.models import Project """ + ) + + +def test_reverse_relative_combined_with_force_sort_within_sections_issue_1395(): + """Test to ensure reverse relative combines well with other common isort settings. + See: https://github.com/timothycrosley/isort/issues/1395. + """ + assert isort.check_code( + """from .fileA import a_var +from ..fileB import b_var +""", + show_diff=True, + reverse_relative=True, + force_sort_within_sections=True, + order_by_type=False, + case_sensitive=False, + multi_line_output=5, + sections=["FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "APPLICATION", "LOCALFOLDER"], + lines_after_imports=2, + no_lines_before="LOCALFOLDER", + ) From 8bd4d3f9ae73f3a79fb63cc4baa63123fa739ff5 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 22 Aug 2020 01:00:19 -0700 Subject: [PATCH 0943/1439] Fixed #1399: --skip can error in the case of projects that contain recursive symlinks. --- CHANGELOG.md | 3 ++- isort/main.py | 13 ++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index abcc9b37c..4d3ab83ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,8 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. ### 5.5.0 TBD - Fixed #1398: isort: off comment doesn't work, if it's the top comment in the file. - Fixed #1395: reverse_relative setting doesn't have any effect when combined with force_sort_within_sections. - + - Fixed #1399: --skip can error in the case of projects that contain recursive for-loops. + ### 5.4.2 Aug 14, 2020 - Fixed #1383: Known other does not work anymore with .editorconfig. - Fixed: Regression in first known party path expansion. diff --git a/isort/main.py b/isort/main.py index 58c4e1015..da912b4c4 100644 --- a/isort/main.py +++ b/isort/main.py @@ -115,17 +115,16 @@ def iter_source_code(paths: Iterable[str], config: Config, skipped: List[str]) - base_path = Path(dirpath) for dirname in list(dirnames): full_path = base_path / dirname + resolved_path = full_path.resolve() if config.is_skipped(full_path): skipped.append(dirname) dirnames.remove(dirname) - - resolved_path = full_path.resolve() - if resolved_path in visited_dirs: # pragma: no cover - if not config.quiet: - warn(f"Likely recursive symlink detected to {resolved_path}") - dirnames.remove(dirname) else: - visited_dirs.add(resolved_path) + if resolved_path in visited_dirs: # pragma: no cover + if not config.quiet: + warn(f"Likely recursive symlink detected to {resolved_path}") + dirnames.remove(dirname) + visited_dirs.add(resolved_path) for filename in filenames: filepath = os.path.join(dirpath, filename) From ce52dca988b528525e9cde550a3c2be22f2db90e Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 22 Aug 2020 01:14:38 -0700 Subject: [PATCH 0944/1439] Update changelog to mention #1389 fix --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d3ab83ac..649b52446 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,8 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. ### 5.5.0 TBD - Fixed #1398: isort: off comment doesn't work, if it's the top comment in the file. - Fixed #1395: reverse_relative setting doesn't have any effect when combined with force_sort_within_sections. - - Fixed #1399: --skip can error in the case of projects that contain recursive for-loops. + - Fixed #1399: --skip can error in the case of projects that contain recursive symlinks. + - Fixed #1389: ensure_newline_before_comments doesn't work if comment is at top of section and sections don't have lines between them. ### 5.4.2 Aug 14, 2020 - Fixed #1383: Known other does not work anymore with .editorconfig. From 3c62f89437af6d696173e6a05d5ec828bbf6ebc0 Mon Sep 17 00:00:00 2001 From: Luca Di Sera Date: Sat, 22 Aug 2020 13:04:56 +0200 Subject: [PATCH 0945/1439] Extends git_hook to allow for a user-specified configuration file. The behavior of the `git_hook` function was to search for a configuration file by starting from the directory containing the first staged file and moving upwards until a configuration file was found or a certain number of directories were checked. If the project configuration file is not stored in a directory that is a parent to each python file in the codebase, the `git_hook` function was unable to retrieve the file. For example, this might happen with a configuration such as the following: ``` git-root/ config/ .isort.cfg src/ ... ``` To allow for the `git_hook` function to support those project structures without breaking backwards compatibility, an optional argument, `settings_file`, was added to it. When `settings_file` is the empty string the function behaves as before. Otherwise, `settings_file` is considered a path to a valid configuration file that will be used for the hook's run. A test in `tests/test_hooks.py` was added to cover for this case. `README.md#Git hook` was modified to mention the new interface. --- README.md | 11 ++++++++++- isort/hooks.py | 14 ++++++++++++-- tests/test_hooks.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 99e911472..a77e20171 100644 --- a/README.md +++ b/README.md @@ -556,7 +556,7 @@ include the following in `.git/hooks/pre-commit`: import sys from isort.hooks import git_hook -sys.exit(git_hook(strict=True, modify=True, lazy=True)) +sys.exit(git_hook(strict=True, modify=True, lazy=True, settings_file="")) ``` If you just want to display warnings, but allow the commit to happen @@ -569,6 +569,15 @@ Set it to `True` to ensure all tracked files are properly isorted, leave it out or set it to `False` to check only files added to your index. +If you want to use a specific configuration file for the hook, you can pass its +path to settings_file. If no path is specifically requested, `git_hook` will +search for the configuration file starting at the directory containing the first +staged file, as per `git diff-index` ordering, and going upward in the directory +structure until a valid configuration file is found or +[`MAX_CONFIG_SEARCH_DEPTH`](src/config.py:35) directories are checked. +The settings_file parameter is used to support users who keep their configuration +file in a directory that might not be a parent of all the other files. + ## Setuptools integration Upon installation, isort enables a `setuptools` command that checks diff --git a/isort/hooks.py b/isort/hooks.py index 3198a1d09..acccede59 100644 --- a/isort/hooks.py +++ b/isort/hooks.py @@ -33,7 +33,9 @@ def get_lines(command: List[str]) -> List[str]: return [line.strip() for line in stdout.splitlines()] -def git_hook(strict: bool = False, modify: bool = False, lazy: bool = False) -> int: +def git_hook( + strict: bool = False, modify: bool = False, lazy: bool = False, settings_file: str = "" +) -> int: """ Git pre-commit hook to check staged files for isort errors @@ -46,6 +48,11 @@ def git_hook(strict: bool = False, modify: bool = False, lazy: bool = False) -> :param bool lazy - if True, also check/fix unstaged files. This is useful if you frequently use ``git commit -a`` for example. If False, ony check/fix the staged files for isort errors. + :param str settings_file - A path to a file to be used as + the configuration file for this run. + When settings_file is the empty string, the configuration file + will be searched starting at the directory containing the first + staged file, if any, and going upward in the directory structure. :return number of errors if in strict mode, 0 otherwise. """ @@ -60,7 +67,10 @@ def git_hook(strict: bool = False, modify: bool = False, lazy: bool = False) -> return 0 errors = 0 - config = Config(settings_path=os.path.dirname(os.path.abspath(files_modified[0]))) + config = Config( + settings_file=settings_file, + settings_path=os.path.dirname(os.path.abspath(files_modified[0])), + ) for filename in files_modified: if filename.endswith(".py"): # Get the staged contents of the file diff --git a/tests/test_hooks.py b/tests/test_hooks.py index ac3ea6436..3e8bc69ff 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -1,4 +1,5 @@ import os +from pathlib import Path from unittest.mock import MagicMock, patch from isort import exceptions, hooks @@ -53,3 +54,31 @@ class FakeProcessResponse(object): with patch("subprocess.run", MagicMock(return_value=FakeProcessResponse())) as run_mock: with patch("isort.api", MagicMock(side_effect=exceptions.FileSkipped("", ""))): hooks.git_hook(modify=True) + + +def test_git_hook_uses_the_configuration_file_specified_in_settings_path(tmp_path: Path) -> None: + subdirectory_path = tmp_path / "subdirectory" + configuration_file_path = subdirectory_path / ".isort.cfg" + + # Inserting the modified file in the parent directory of the configuration file ensures that it + # will not be found by the normal search routine + modified_file_path = configuration_file_path.parent / "somefile.py" + + # This section will be used to check that the configuration file was indeed loaded + section = "testsection" + + os.mkdir(subdirectory_path) + with open(configuration_file_path, "w") as fd: + fd.write("[isort]\n") + fd.write(f"sections={section}") + + with open(modified_file_path, "w") as fd: + pass + + files_modified = [str(modified_file_path.absolute())] + with patch("isort.hooks.get_lines", MagicMock(return_value=files_modified)): + with patch("isort.hooks.get_output", MagicMock(return_value="")): + with patch("isort.api.check_code_string", MagicMock()) as run_mock: + hooks.git_hook(settings_file=str(configuration_file_path)) + + assert run_mock.call_args[1]["config"].sections == (section,) From b6a96b2b79e7e50536750cdef2a7e729d3ae6fc1 Mon Sep 17 00:00:00 2001 From: Luca Di Sera Date: Sat, 22 Aug 2020 14:17:37 +0200 Subject: [PATCH 0946/1439] Corrects a typo in the tests/test_hooks.py. In `test_git_hook_uses_the_configuration_file_specified_in_settings_path`, `modified_file_path` incorrectly pointed to a file that was a sibling of the configuration file, such that the configuration file would have been found indepently of the `settings_file` configuration. `modified_file_path` now points to a file that lives one level higher than the configuration file. --- tests/test_hooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_hooks.py b/tests/test_hooks.py index 3e8bc69ff..083fbfd48 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -62,7 +62,7 @@ def test_git_hook_uses_the_configuration_file_specified_in_settings_path(tmp_pat # Inserting the modified file in the parent directory of the configuration file ensures that it # will not be found by the normal search routine - modified_file_path = configuration_file_path.parent / "somefile.py" + modified_file_path = configuration_file_path.parent.parent / "somefile.py" # This section will be used to check that the configuration file was indeed loaded section = "testsection" From c225da117a146b8ec1496840900126678e47ccc4 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 22 Aug 2020 17:22:47 -0700 Subject: [PATCH 0947/1439] Add initial integration test --- tests/test_integration.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 tests/test_integration.py diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 000000000..9f10c221b --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,11 @@ +from subprocess import check_call + +from isort.main import main + + +def test_django(tmpdir): + check_call(["git", "clone", "https://github.com/django/django", str(tmpdir)]) + isort_target_dirs = [ + str(target_dir) for target_dir in (tmpdir / "django", tmpdir / "tests", tmpdir / "scripts") + ] + main(["--check-only", "--diff", *isort_target_dirs]) From 7bbe002400fc3cb9e2342811a9e0376620b085ce Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 22 Aug 2020 17:24:20 -0700 Subject: [PATCH 0948/1439] Add githook config file support to changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 649b52446..a2c718a0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. - Fixed #1395: reverse_relative setting doesn't have any effect when combined with force_sort_within_sections. - Fixed #1399: --skip can error in the case of projects that contain recursive symlinks. - Fixed #1389: ensure_newline_before_comments doesn't work if comment is at top of section and sections don't have lines between them. + - Implemented #1397: Added support for specifying config file when using git hook (thanks @diseraluca!). ### 5.4.2 Aug 14, 2020 - Fixed #1383: Known other does not work anymore with .editorconfig. From 19221a3e3c4e17e51056d1e70cd565b16f149cea Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 22 Aug 2020 18:19:16 -0700 Subject: [PATCH 0949/1439] Fixed #1396: comments in imports with ; can keep isort from recognizing import line. --- CHANGELOG.md | 1 + isort/parse.py | 18 ++++++++++++------ tests/test_regressions.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2c718a0a..23a296a61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. - Fixed #1395: reverse_relative setting doesn't have any effect when combined with force_sort_within_sections. - Fixed #1399: --skip can error in the case of projects that contain recursive symlinks. - Fixed #1389: ensure_newline_before_comments doesn't work if comment is at top of section and sections don't have lines between them. + - Fixed #1396: comments in imports with ";" can keep isort from recognizing import line. - Implemented #1397: Added support for specifying config file when using git hook (thanks @diseraluca!). ### 5.4.2 Aug 14, 2020 diff --git a/isort/parse.py b/isort/parse.py index 2ab43acfc..1e71c6d2c 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -113,7 +113,7 @@ def skip_line( break char_index += 1 - if ";" in line and needs_import: + if ";" in line.split("#")[0] and needs_import: for part in (part.strip() for part in line.split(";")): if ( part @@ -201,10 +201,16 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte out_lines.append(line) continue - for line in ( - (line.strip() for line in line.split(";")) if ";" in line else (line,) # type: ignore - ): - line, raw_line = _normalize_line(line) + line, *end_of_line_comment = line.split("#", 1) + if ";" in line: + statements = [line.strip() for line in line.split(";")] + else: + statements = [line] + if end_of_line_comment: + statements[-1] = f"{statements[-1]}#{end_of_line_comment[0]}" + + for statement in statements: + line, raw_line = _normalize_line(statement) type_of_import = import_type(line, config) or "" if not type_of_import: out_lines.append(raw_line) @@ -224,7 +230,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte ): nested_comments[line_parts[-1]] = comments[0] - if "(" in line.split("#")[0] and index < line_count: + if "(" in line.split("#", 1)[0] and index < line_count: while not line.split("#")[0].strip().endswith(")") and index < line_count: line, new_comment = parse_comments(in_lines[index]) index += 1 diff --git a/tests/test_regressions.py b/tests/test_regressions.py index 4424f9e81..4d5758c18 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -564,6 +564,36 @@ def test_incorrect_grouping_when_comments_issue_1396(): from django.views.generic import \\ TemplateView # ListView,; DetailView,; CreateView,; View +from apps.profiler.models import Project +""" + ) + assert ( + isort.code( + """from django.contrib.auth.decorators import login_required +from django.shortcuts import render + +from apps.profiler.models import Project + +from django.views.generic import ( # ListView,; DetailView,; CreateView,; View + TemplateView, +) +""", + line_length=88, + known_first_party=["apps"], + known_django=["django"], + sections=["FUTURE", "STDLIB", "DJANGO", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"], + include_trailing_comma=True, + multi_line_output=3, + force_grid_wrap=0, + use_parentheses=True, + ensure_newline_before_comments=True, + ) + == """from django.contrib.auth.decorators import login_required +from django.shortcuts import render +from django.views.generic import ( # ListView,; DetailView,; CreateView,; View + TemplateView, +) + from apps.profiler.models import Project """ ) From 180bdf9abc91223eb9d7b9e713ead7451095c5da Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 23 Aug 2020 00:09:37 -0700 Subject: [PATCH 0950/1439] Start work to separate integration and unit tests --- .isort.cfg | 2 +- scripts/test.sh | 2 +- setup.cfg | 2 +- .../test_high_profile_projects_using_isort} | 0 tests/{ => integration}/test_hypothesmith.py | 0 tests/unit/__init__.py | 0 tests/{ => unit}/conftest.py | 2 +- tests/{ => unit}/example_crlf_file.py | 0 tests/{ => unit}/test_action_comments.py | 0 tests/{ => unit}/test_api.py | 0 tests/{ => unit}/test_comments.py | 0 tests/{ => unit}/test_deprecated_finders.py | 0 tests/{ => unit}/test_exceptions.py | 0 tests/{ => unit}/test_format.py | 0 tests/{ => unit}/test_hooks.py | 0 tests/{ => unit}/test_importable.py | 1 + tests/{ => unit}/test_io.py | 0 tests/{ => unit}/test_isort.py | 0 tests/{ => unit}/test_literal.py | 0 tests/{ => unit}/test_main.py | 0 tests/{ => unit}/test_output.py | 0 tests/{ => unit}/test_parse.py | 0 tests/{ => unit}/test_place.py | 0 tests/{ => unit}/test_pylama_isort.py | 0 tests/{ => unit}/test_regressions.py | 0 tests/{ => unit}/test_settings.py | 0 tests/{ => unit}/test_setuptools_command.py | 0 tests/{ => unit}/test_ticketed_features.py | 0 tests/{ => unit}/test_wrap.py | 0 tests/{ => unit}/test_wrap_modes.py | 0 30 files changed, 5 insertions(+), 4 deletions(-) rename tests/{test_integration.py => integration/test_high_profile_projects_using_isort} (100%) rename tests/{ => integration}/test_hypothesmith.py (100%) create mode 100644 tests/unit/__init__.py rename tests/{ => unit}/conftest.py (85%) rename tests/{ => unit}/example_crlf_file.py (100%) rename tests/{ => unit}/test_action_comments.py (100%) rename tests/{ => unit}/test_api.py (100%) rename tests/{ => unit}/test_comments.py (100%) rename tests/{ => unit}/test_deprecated_finders.py (100%) rename tests/{ => unit}/test_exceptions.py (100%) rename tests/{ => unit}/test_format.py (100%) rename tests/{ => unit}/test_hooks.py (100%) rename tests/{ => unit}/test_importable.py (99%) rename tests/{ => unit}/test_io.py (100%) rename tests/{ => unit}/test_isort.py (100%) rename tests/{ => unit}/test_literal.py (100%) rename tests/{ => unit}/test_main.py (100%) rename tests/{ => unit}/test_output.py (100%) rename tests/{ => unit}/test_parse.py (100%) rename tests/{ => unit}/test_place.py (100%) rename tests/{ => unit}/test_pylama_isort.py (100%) rename tests/{ => unit}/test_regressions.py (100%) rename tests/{ => unit}/test_settings.py (100%) rename tests/{ => unit}/test_setuptools_command.py (100%) rename tests/{ => unit}/test_ticketed_features.py (100%) rename tests/{ => unit}/test_wrap.py (100%) rename tests/{ => unit}/test_wrap_modes.py (100%) diff --git a/.isort.cfg b/.isort.cfg index 3f3b28ad5..567d1abd6 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -1,4 +1,4 @@ [settings] profile=hug src_paths=isort,test -skip=tests/example_crlf_file.py +skip=tests/unit/example_crlf_file.py diff --git a/scripts/test.sh b/scripts/test.sh index a06598520..5aa1c7648 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -2,5 +2,5 @@ set -euxo pipefail ./scripts/lint.sh -poetry run pytest tests/ -s --cov=isort/ --cov-report=term-missing ${@-} +poetry run pytest tests/unit/ -s --cov=isort/ --cov-report=term-missing ${@-} poetry run coverage html diff --git a/setup.cfg b/setup.cfg index 770db0e11..8129063fa 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,4 +25,4 @@ exclude = _vendored per-file-ignores = isort/__init__.py:F401 isort/stdlibs/__init__.py:F401 - tests/example_crlf_file.py:F401 + tests/unit/example_crlf_file.py:F401 diff --git a/tests/test_integration.py b/tests/integration/test_high_profile_projects_using_isort similarity index 100% rename from tests/test_integration.py rename to tests/integration/test_high_profile_projects_using_isort diff --git a/tests/test_hypothesmith.py b/tests/integration/test_hypothesmith.py similarity index 100% rename from tests/test_hypothesmith.py rename to tests/integration/test_hypothesmith.py diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/conftest.py b/tests/unit/conftest.py similarity index 85% rename from tests/conftest.py rename to tests/unit/conftest.py index 3b778a700..09e4acbdb 100644 --- a/tests/conftest.py +++ b/tests/unit/conftest.py @@ -5,7 +5,7 @@ import pytest TEST_DIR = os.path.dirname(os.path.abspath(__file__)) -SRC_DIR = os.path.abspath(os.path.join(TEST_DIR, "../isort/")) +SRC_DIR = os.path.abspath(os.path.join(TEST_DIR, "../../isort/")) @pytest.fixture diff --git a/tests/example_crlf_file.py b/tests/unit/example_crlf_file.py similarity index 100% rename from tests/example_crlf_file.py rename to tests/unit/example_crlf_file.py diff --git a/tests/test_action_comments.py b/tests/unit/test_action_comments.py similarity index 100% rename from tests/test_action_comments.py rename to tests/unit/test_action_comments.py diff --git a/tests/test_api.py b/tests/unit/test_api.py similarity index 100% rename from tests/test_api.py rename to tests/unit/test_api.py diff --git a/tests/test_comments.py b/tests/unit/test_comments.py similarity index 100% rename from tests/test_comments.py rename to tests/unit/test_comments.py diff --git a/tests/test_deprecated_finders.py b/tests/unit/test_deprecated_finders.py similarity index 100% rename from tests/test_deprecated_finders.py rename to tests/unit/test_deprecated_finders.py diff --git a/tests/test_exceptions.py b/tests/unit/test_exceptions.py similarity index 100% rename from tests/test_exceptions.py rename to tests/unit/test_exceptions.py diff --git a/tests/test_format.py b/tests/unit/test_format.py similarity index 100% rename from tests/test_format.py rename to tests/unit/test_format.py diff --git a/tests/test_hooks.py b/tests/unit/test_hooks.py similarity index 100% rename from tests/test_hooks.py rename to tests/unit/test_hooks.py diff --git a/tests/test_importable.py b/tests/unit/test_importable.py similarity index 99% rename from tests/test_importable.py rename to tests/unit/test_importable.py index c48ce333a..d45ca5063 100644 --- a/tests/test_importable.py +++ b/tests/unit/test_importable.py @@ -4,6 +4,7 @@ def test_importable(): """Simple smoketest to ensure all isort modules are importable""" + import isort import isort._future import isort._future._dataclasses diff --git a/tests/test_io.py b/tests/unit/test_io.py similarity index 100% rename from tests/test_io.py rename to tests/unit/test_io.py diff --git a/tests/test_isort.py b/tests/unit/test_isort.py similarity index 100% rename from tests/test_isort.py rename to tests/unit/test_isort.py diff --git a/tests/test_literal.py b/tests/unit/test_literal.py similarity index 100% rename from tests/test_literal.py rename to tests/unit/test_literal.py diff --git a/tests/test_main.py b/tests/unit/test_main.py similarity index 100% rename from tests/test_main.py rename to tests/unit/test_main.py diff --git a/tests/test_output.py b/tests/unit/test_output.py similarity index 100% rename from tests/test_output.py rename to tests/unit/test_output.py diff --git a/tests/test_parse.py b/tests/unit/test_parse.py similarity index 100% rename from tests/test_parse.py rename to tests/unit/test_parse.py diff --git a/tests/test_place.py b/tests/unit/test_place.py similarity index 100% rename from tests/test_place.py rename to tests/unit/test_place.py diff --git a/tests/test_pylama_isort.py b/tests/unit/test_pylama_isort.py similarity index 100% rename from tests/test_pylama_isort.py rename to tests/unit/test_pylama_isort.py diff --git a/tests/test_regressions.py b/tests/unit/test_regressions.py similarity index 100% rename from tests/test_regressions.py rename to tests/unit/test_regressions.py diff --git a/tests/test_settings.py b/tests/unit/test_settings.py similarity index 100% rename from tests/test_settings.py rename to tests/unit/test_settings.py diff --git a/tests/test_setuptools_command.py b/tests/unit/test_setuptools_command.py similarity index 100% rename from tests/test_setuptools_command.py rename to tests/unit/test_setuptools_command.py diff --git a/tests/test_ticketed_features.py b/tests/unit/test_ticketed_features.py similarity index 100% rename from tests/test_ticketed_features.py rename to tests/unit/test_ticketed_features.py diff --git a/tests/test_wrap.py b/tests/unit/test_wrap.py similarity index 100% rename from tests/test_wrap.py rename to tests/unit/test_wrap.py diff --git a/tests/test_wrap_modes.py b/tests/unit/test_wrap_modes.py similarity index 100% rename from tests/test_wrap_modes.py rename to tests/unit/test_wrap_modes.py From 62bf6b8b2b99e4f5a56c5485aa992bf292002816 Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Sun, 23 Aug 2020 11:56:26 +0300 Subject: [PATCH 0951/1439] Small refactoring in output.py. * remove duplication in handling `from_first` * `with_from_import` and `with_straight_import` don't need to know or write to the "global" `section_output` - remove `section_output` argument - remove `section_output.copy()` - functions return their own output without knowing about the "global" output * `sort_ignore_case` already passed in to `_with_from_imports` in `config`. Just use it from there. --- isort/output.py | 98 +++++++++++++++---------------------------------- 1 file changed, 29 insertions(+), 69 deletions(-) diff --git a/isort/output.py b/isort/output.py index 5732ae532..ecd246621 100644 --- a/isort/output.py +++ b/isort/output.py @@ -29,7 +29,6 @@ def sorted_imports( formatted_output: List[str] = parsed.lines_without_imports.copy() remove_imports = [format_simplified(removal) for removal in config.remove_imports] - sort_ignore_case = config.force_alphabetical_sort_within_sections sections: Iterable[str] = itertools.chain(parsed.sections, config.forced_separate) if config.no_sections: @@ -61,51 +60,20 @@ def sorted_imports( from_modules, key=lambda key: sorting.module_key(key, config, section_name=section) ) - section_output: List[str] = [] + straight_imports = _with_straight_imports( + parsed, config, straight_modules, section, remove_imports, import_type + ) + from_imports = _with_from_imports( + parsed, config, from_modules, section, remove_imports, import_type + ) + + lines_between = [""] * ( + config.lines_between_types if from_modules and straight_modules else 0 + ) if config.from_first: - section_output = _with_from_imports( - parsed, - config, - from_modules, - section, - section_output, - sort_ignore_case, - remove_imports, - import_type, - ) - if config.lines_between_types and from_modules and straight_modules: - section_output.extend([""] * config.lines_between_types) - section_output = _with_straight_imports( - parsed, - config, - straight_modules, - section, - section_output, - remove_imports, - import_type, - ) + section_output = from_imports + lines_between + straight_imports else: - section_output = _with_straight_imports( - parsed, - config, - straight_modules, - section, - section_output, - remove_imports, - import_type, - ) - if config.lines_between_types and from_modules and straight_modules: - section_output.extend([""] * config.lines_between_types) - section_output = _with_from_imports( - parsed, - config, - from_modules, - section, - section_output, - sort_ignore_case, - remove_imports, - import_type, - ) + section_output = straight_imports + lines_between + from_imports if config.force_sort_within_sections: # collapse comments @@ -244,12 +212,10 @@ def _with_from_imports( config: Config, from_modules: Iterable[str], section: str, - section_output: List[str], - ignore_case: bool, remove_imports: List[str], import_type: str, ) -> List[str]: - new_section_output = section_output.copy() + output: List[str] = [] for module in from_modules: if module in remove_imports: continue @@ -259,10 +225,11 @@ def _with_from_imports( if not config.no_inline_sort or ( config.force_single_line and module not in config.single_line_exclusions ): + ignore_case = config.force_alphabetical_sort_within_sections from_imports = sorting.naturally( from_imports, key=lambda key: sorting.module_key( - key, config, True, ignore_case, section_name=section + key, config, True, ignore_case, section_name=section, ), ) if remove_imports: @@ -294,7 +261,7 @@ def _with_from_imports( comments = parsed.categorized_comments["from"].pop(module, ()) above_comments = parsed.categorized_comments["above"]["from"].pop(module, None) if above_comments: - new_section_output.extend(above_comments) + output.extend(above_comments) if "*" in from_imports and config.combine_star: if config.combine_as_imports: @@ -332,13 +299,13 @@ def _with_from_imports( ) if from_import in as_imports: if parsed.imports[section]["from"][module][from_import]: - new_section_output.append( + output.append( wrap.line(single_import_line, parsed.line_separator, config) ) from_comments = parsed.categorized_comments["straight"].get( f"{module}.{from_import}" ) - new_section_output.extend( + output.extend( with_comments( from_comments, wrap.line(import_start + as_import, parsed.line_separator, config), @@ -348,9 +315,7 @@ def _with_from_imports( for as_import in sorting.naturally(as_imports[from_import]) ) else: - new_section_output.append( - wrap.line(single_import_line, parsed.line_separator, config) - ) + output.append(wrap.line(single_import_line, parsed.line_separator, config)) comments = None else: while from_imports and from_imports[0] in as_imports: @@ -360,7 +325,7 @@ def _with_from_imports( f"{module}.{from_import}" ) if parsed.imports[section]["from"][module][from_import]: - new_section_output.append( + output.append( wrap.line( with_comments( from_comments, @@ -372,7 +337,7 @@ def _with_from_imports( config, ) ) - new_section_output.extend( + output.extend( wrap.line( with_comments( from_comments, @@ -387,7 +352,7 @@ def _with_from_imports( ) if "*" in from_imports: - new_section_output.append( + output.append( with_comments( comments, f"{import_start}*", @@ -412,9 +377,7 @@ def _with_from_imports( single_import_line += ( f"{comments and ';' or config.comment_prefix} " f"{comment}" ) - new_section_output.append( - wrap.line(single_import_line, parsed.line_separator, config) - ) + output.append(wrap.line(single_import_line, parsed.line_separator, config)) from_imports.remove(from_import) comments = None @@ -482,8 +445,8 @@ def _with_from_imports( import_statement = wrap.line(import_statement, parsed.line_separator, config) if import_statement: - new_section_output.append(import_statement) - return new_section_output + output.append(import_statement) + return output def _with_straight_imports( @@ -491,11 +454,10 @@ def _with_straight_imports( config: Config, straight_modules: Iterable[str], section: str, - section_output: List[str], remove_imports: List[str], import_type: str, ) -> List[str]: - new_section_output = section_output.copy() + output: List[str] = [] for module in straight_modules: if module in remove_imports: continue @@ -513,10 +475,8 @@ def _with_straight_imports( comments_above = parsed.categorized_comments["above"]["straight"].pop(module, None) if comments_above: - if new_section_output and config.ensure_newline_before_comments: - new_section_output.append("") - new_section_output.extend(comments_above) - new_section_output.extend( + output.extend(comments_above) + output.extend( with_comments( parsed.categorized_comments["straight"].get(module), idef, @@ -526,7 +486,7 @@ def _with_straight_imports( for idef in import_definition ) - return new_section_output + return output def _output_as_string(lines: List[str], line_separator: str) -> str: From c72ca8edf6cca7e47db33edbad680bd7ec76c3b5 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 23 Aug 2020 02:26:32 -0700 Subject: [PATCH 0952/1439] Add several projects to integration test --- .../test_high_profile_projects_using_isort | 11 --- .../integration/test_projects_using_isort.py | 83 +++++++++++++++++++ 2 files changed, 83 insertions(+), 11 deletions(-) delete mode 100644 tests/integration/test_high_profile_projects_using_isort create mode 100644 tests/integration/test_projects_using_isort.py diff --git a/tests/integration/test_high_profile_projects_using_isort b/tests/integration/test_high_profile_projects_using_isort deleted file mode 100644 index 9f10c221b..000000000 --- a/tests/integration/test_high_profile_projects_using_isort +++ /dev/null @@ -1,11 +0,0 @@ -from subprocess import check_call - -from isort.main import main - - -def test_django(tmpdir): - check_call(["git", "clone", "https://github.com/django/django", str(tmpdir)]) - isort_target_dirs = [ - str(target_dir) for target_dir in (tmpdir / "django", tmpdir / "tests", tmpdir / "scripts") - ] - main(["--check-only", "--diff", *isort_target_dirs]) diff --git a/tests/integration/test_projects_using_isort.py b/tests/integration/test_projects_using_isort.py new file mode 100644 index 000000000..ba572390c --- /dev/null +++ b/tests/integration/test_projects_using_isort.py @@ -0,0 +1,83 @@ +"""Tests a projects that use isort to see if any differences are found between their current imports and +what isort suggest they be. This is an important early warning single of regressions. + +NOTE: If you use isort within a public repository, please feel empowered to add your project here! +It is important to isort that as few regressions as possible are experienced by our users. +Having your project tested here is the most sure way to keep those regressions form ever happening. +""" +from subprocess import check_call + +from isort.main import main + + +def test_django(tmpdir): + check_call(["git", "clone", "https://github.com/django/django.git", str(tmpdir)]) + isort_target_dirs = [ + str(target_dir) for target_dir in (tmpdir / "django", tmpdir / "tests", tmpdir / "scripts") + ] + main(["--check-only", "--diff", *isort_target_dirs]) + + +def test_plone(tmpdir): + check_call(["git", "clone", "https://github.com/plone/plone.app.multilingualindexes.git", str(tmpdir)]) + main(["--check-only", "--diff", str(tmpdir / "src")]) + + +def test_pandas(tmpdir): + check_call(["git", "clone", "https://github.com/pandas-dev/pandas.git", str(tmpdir)]) + main(["--check-only", "--diff", str(tmpdir / "pandas"), "--skip", "__init__.py"]) + + +def test_fastapi(tmpdir): + check_call(["git", "clone", "https://github.com/tiangolo/fastapi.git", str(tmpdir)]) + main(["--check-only", "--diff", str(tmpdir / "fastapi")]) + + +def test_zulip(tmpdir): + check_call(["git", "clone", "https://github.com/zulip/zulip.git", str(tmpdir)]) + main(["--check-only", "--diff", str(tmpdir), "--skip", "__init__.pyi"]) + + +def test_habitat_lab(tmpdir): + check_call(["git", "clone", "https://github.com/facebookresearch/habitat-lab.git", str(tmpdir)]) + main(["--check-only", "--diff", str(tmpdir)]) + + +def test_tmuxp(tmpdir): + check_call(["git", "clone", "https://github.com/tmux-python/tmuxp.git", str(tmpdir)]) + main(["--check-only", "--diff", str(tmpdir)]) + + +def test_websockets(tmpdir): + check_call(["git", "clone", "https://github.com/aaugustin/websockets.git", str(tmpdir)]) + main(["--check-only", "--diff", str(tmpdir), "--skip", "example", "--skip", "docs", "--skip", "compliance"]) + + +def test_airflow(tmpdir): + check_call(["git", "clone", "https://github.com/apache/airflow.git", str(tmpdir)]) + main(["--check-only", "--diff", str(tmpdir)]) + + +def test_typeshed(tmpdir): + check_call(["git", "clone", "https://github.com/python/typeshed.git", str(tmpdir)]) + main(["--check-only", "--diff", str(tmpdir), "--skip", "tests", "--skip", "scripts", "--skip", f"{tmpdir}/third_party/2and3/yaml/__init__.pyi"]) + + +def test_pylint(tmpdir): + check_call(["git", "clone", "https://github.com/PyCQA/pylint.git", str(tmpdir)]) + main(["--check-only", "--diff", str(tmpdir)]) + + +def test_poetry(tmpdir): + check_call(["git", "clone", "https://github.com/python-poetry/poetry.git", str(tmpdir)]) + main(["--check-only", "--diff", str(tmpdir), "--skip", "tests"]) + + +def test_hypothesis(tmpdir): + check_call(["git", "clone", "https://github.com/HypothesisWorks/hypothesis.git", str(tmpdir)]) + main(["--check-only", "--diff", str(tmpdir), "--skip", "tests"]) + + +def test_pillow(tmpdir): + check_call(["git", "clone", "https://github.com/python-pillow/Pillow.git", str(tmpdir)]) + main(["--check-only", "--diff", str(tmpdir), "--skip", "tests"]) From e42153fa6996a048759d2d9b3d2f6b55726293d6 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 23 Aug 2020 02:30:45 -0700 Subject: [PATCH 0953/1439] Add integration test CI step --- .github/workflows/integration.yml | 35 +++++++++++++++++++++++++++++++ scripts/test_integration.sh | 4 ++++ 2 files changed, 39 insertions(+) create mode 100644 .github/workflows/integration.yml create mode 100755 scripts/test_integration.sh diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml new file mode 100644 index 000000000..06780574e --- /dev/null +++ b/.github/workflows/integration.yml @@ -0,0 +1,35 @@ +name: Lint + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.8] + + steps: + - uses: actions/checkout@v2 + + - name: pip cache + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: lint-pip-${{ hashFiles('**/pyproject.toml') }} + restore-keys: | + lint-pip- + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install --upgrade poetry + poetry install + + - name: Lint + run: ./scripts/lint.sh diff --git a/scripts/test_integration.sh b/scripts/test_integration.sh new file mode 100755 index 000000000..8bbcc5e77 --- /dev/null +++ b/scripts/test_integration.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -euxo pipefail + +poetry run pytest tests/integration/ -s From aa31bb8b13e422684c2920b83f7c69262db099c7 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 23 Aug 2020 02:34:19 -0700 Subject: [PATCH 0954/1439] Fix integration test workflow, removing accidental references to linting --- .github/workflows/integration.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 06780574e..9669db86d 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -16,9 +16,9 @@ jobs: uses: actions/cache@v1 with: path: ~/.cache/pip - key: lint-pip-${{ hashFiles('**/pyproject.toml') }} + key: integration-pip-${{ hashFiles('**/pyproject.toml') }} restore-keys: | - lint-pip- + integration-pip- - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 @@ -31,5 +31,5 @@ jobs: python -m pip install --upgrade poetry poetry install - - name: Lint - run: ./scripts/lint.sh + - name: Test integration + run: ./scripts/test_integration.sh From ced04a4c04ea06f47a9a6f6dc215af1cd2fa6d8d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 23 Aug 2020 02:34:55 -0700 Subject: [PATCH 0955/1439] Fix integration test workflow, removing accidental references to linting --- .github/workflows/integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 9669db86d..283103118 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -1,4 +1,4 @@ -name: Lint +name: Integration on: [push, pull_request] From 7082189ca06bb9b2e8de3f91a281af57667af9de Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 23 Aug 2020 02:48:08 -0700 Subject: [PATCH 0956/1439] Fix linting errors due to overly long lines --- .../integration/test_projects_using_isort.py | 51 ++++++++++++++----- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/tests/integration/test_projects_using_isort.py b/tests/integration/test_projects_using_isort.py index ba572390c..f1a1f46eb 100644 --- a/tests/integration/test_projects_using_isort.py +++ b/tests/integration/test_projects_using_isort.py @@ -1,5 +1,6 @@ -"""Tests a projects that use isort to see if any differences are found between their current imports and -what isort suggest they be. This is an important early warning single of regressions. +"""Tests a projects that use isort to see if any differences are found between +their current imports and what isort suggest on the develop branch. +This is an important early warning signal of regressions. NOTE: If you use isort within a public repository, please feel empowered to add your project here! It is important to isort that as few regressions as possible are experienced by our users. @@ -19,25 +20,27 @@ def test_django(tmpdir): def test_plone(tmpdir): - check_call(["git", "clone", "https://github.com/plone/plone.app.multilingualindexes.git", str(tmpdir)]) + check_call( + ["git", "clone", "https://github.com/plone/plone.app.multilingualindexes.git", str(tmpdir)] + ) main(["--check-only", "--diff", str(tmpdir / "src")]) def test_pandas(tmpdir): check_call(["git", "clone", "https://github.com/pandas-dev/pandas.git", str(tmpdir)]) main(["--check-only", "--diff", str(tmpdir / "pandas"), "--skip", "__init__.py"]) - - + + def test_fastapi(tmpdir): check_call(["git", "clone", "https://github.com/tiangolo/fastapi.git", str(tmpdir)]) main(["--check-only", "--diff", str(tmpdir / "fastapi")]) - - + + def test_zulip(tmpdir): check_call(["git", "clone", "https://github.com/zulip/zulip.git", str(tmpdir)]) main(["--check-only", "--diff", str(tmpdir), "--skip", "__init__.pyi"]) - - + + def test_habitat_lab(tmpdir): check_call(["git", "clone", "https://github.com/facebookresearch/habitat-lab.git", str(tmpdir)]) main(["--check-only", "--diff", str(tmpdir)]) @@ -50,7 +53,19 @@ def test_tmuxp(tmpdir): def test_websockets(tmpdir): check_call(["git", "clone", "https://github.com/aaugustin/websockets.git", str(tmpdir)]) - main(["--check-only", "--diff", str(tmpdir), "--skip", "example", "--skip", "docs", "--skip", "compliance"]) + main( + [ + "--check-only", + "--diff", + str(tmpdir), + "--skip", + "example", + "--skip", + "docs", + "--skip", + "compliance", + ] + ) def test_airflow(tmpdir): @@ -60,7 +75,19 @@ def test_airflow(tmpdir): def test_typeshed(tmpdir): check_call(["git", "clone", "https://github.com/python/typeshed.git", str(tmpdir)]) - main(["--check-only", "--diff", str(tmpdir), "--skip", "tests", "--skip", "scripts", "--skip", f"{tmpdir}/third_party/2and3/yaml/__init__.pyi"]) + main( + [ + "--check-only", + "--diff", + str(tmpdir), + "--skip", + "tests", + "--skip", + "scripts", + "--skip", + f"{tmpdir}/third_party/2and3/yaml/__init__.pyi", + ] + ) def test_pylint(tmpdir): @@ -72,7 +99,7 @@ def test_poetry(tmpdir): check_call(["git", "clone", "https://github.com/python-poetry/poetry.git", str(tmpdir)]) main(["--check-only", "--diff", str(tmpdir), "--skip", "tests"]) - + def test_hypothesis(tmpdir): check_call(["git", "clone", "https://github.com/HypothesisWorks/hypothesis.git", str(tmpdir)]) main(["--check-only", "--diff", str(tmpdir), "--skip", "tests"]) From 1d416baf79e6c341c2853f2880fa920a6e688c16 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 23 Aug 2020 03:06:21 -0700 Subject: [PATCH 0957/1439] Ensure test pipeline only runts unit tests --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 71788b54b..ef93a1eea 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -57,7 +57,7 @@ jobs: - name: Test shell: bash run: | - poetry run pytest tests/ -s --cov=isort/ --cov-report=term-missing ${@-} + poetry run pytest tests/unit/ -s --cov=isort/ --cov-report=term-missing ${@-} poetry run coverage xml - name: Report Coverage if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.8' From 7caab0d99263669802448b1d6f409779e2594385 Mon Sep 17 00:00:00 2001 From: Timothy Edmund Crosley Date: Sun, 23 Aug 2020 04:56:52 -0700 Subject: [PATCH 0958/1439] Update test_projects_using_isort.py --- tests/integration/test_projects_using_isort.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/test_projects_using_isort.py b/tests/integration/test_projects_using_isort.py index f1a1f46eb..542a84fe3 100644 --- a/tests/integration/test_projects_using_isort.py +++ b/tests/integration/test_projects_using_isort.py @@ -1,4 +1,4 @@ -"""Tests a projects that use isort to see if any differences are found between +"""Tests projects that use isort to see if any differences are found between their current imports and what isort suggest on the develop branch. This is an important early warning signal of regressions. From 39bafa6a74510478cd0abeb664959a454b4ccf69 Mon Sep 17 00:00:00 2001 From: James Winegar Date: Sun, 23 Aug 2020 11:52:10 -0500 Subject: [PATCH 0959/1439] integration test clones should be shallow There's no need for history of the repo to check isort. --- .../integration/test_projects_using_isort.py | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/integration/test_projects_using_isort.py b/tests/integration/test_projects_using_isort.py index 542a84fe3..66dc5c2f6 100644 --- a/tests/integration/test_projects_using_isort.py +++ b/tests/integration/test_projects_using_isort.py @@ -12,7 +12,7 @@ def test_django(tmpdir): - check_call(["git", "clone", "https://github.com/django/django.git", str(tmpdir)]) + check_call(["git", "clone", "--depth", "1", "https://github.com/django/django.git", str(tmpdir)]) isort_target_dirs = [ str(target_dir) for target_dir in (tmpdir / "django", tmpdir / "tests", tmpdir / "scripts") ] @@ -21,38 +21,38 @@ def test_django(tmpdir): def test_plone(tmpdir): check_call( - ["git", "clone", "https://github.com/plone/plone.app.multilingualindexes.git", str(tmpdir)] + ["git", "clone", "--depth", "1", "https://github.com/plone/plone.app.multilingualindexes.git", str(tmpdir)] ) main(["--check-only", "--diff", str(tmpdir / "src")]) def test_pandas(tmpdir): - check_call(["git", "clone", "https://github.com/pandas-dev/pandas.git", str(tmpdir)]) + check_call(["git", "clone", "--depth", "1", "https://github.com/pandas-dev/pandas.git", str(tmpdir)]) main(["--check-only", "--diff", str(tmpdir / "pandas"), "--skip", "__init__.py"]) def test_fastapi(tmpdir): - check_call(["git", "clone", "https://github.com/tiangolo/fastapi.git", str(tmpdir)]) + check_call(["git", "clone", "--depth", "1", "https://github.com/tiangolo/fastapi.git", str(tmpdir)]) main(["--check-only", "--diff", str(tmpdir / "fastapi")]) def test_zulip(tmpdir): - check_call(["git", "clone", "https://github.com/zulip/zulip.git", str(tmpdir)]) + check_call(["git", "clone", "--depth", "1", "https://github.com/zulip/zulip.git", str(tmpdir)]) main(["--check-only", "--diff", str(tmpdir), "--skip", "__init__.pyi"]) def test_habitat_lab(tmpdir): - check_call(["git", "clone", "https://github.com/facebookresearch/habitat-lab.git", str(tmpdir)]) + check_call(["git", "clone", "--depth", "1", "https://github.com/facebookresearch/habitat-lab.git", str(tmpdir)]) main(["--check-only", "--diff", str(tmpdir)]) def test_tmuxp(tmpdir): - check_call(["git", "clone", "https://github.com/tmux-python/tmuxp.git", str(tmpdir)]) + check_call(["git", "clone", "--depth", "1", "https://github.com/tmux-python/tmuxp.git", str(tmpdir)]) main(["--check-only", "--diff", str(tmpdir)]) def test_websockets(tmpdir): - check_call(["git", "clone", "https://github.com/aaugustin/websockets.git", str(tmpdir)]) + check_call(["git", "clone", "--depth", "1", "https://github.com/aaugustin/websockets.git", str(tmpdir)]) main( [ "--check-only", @@ -69,12 +69,12 @@ def test_websockets(tmpdir): def test_airflow(tmpdir): - check_call(["git", "clone", "https://github.com/apache/airflow.git", str(tmpdir)]) + check_call(["git", "clone", "--depth", "1", "https://github.com/apache/airflow.git", str(tmpdir)]) main(["--check-only", "--diff", str(tmpdir)]) def test_typeshed(tmpdir): - check_call(["git", "clone", "https://github.com/python/typeshed.git", str(tmpdir)]) + check_call(["git", "clone", "--depth", "1", "https://github.com/python/typeshed.git", str(tmpdir)]) main( [ "--check-only", @@ -91,20 +91,20 @@ def test_typeshed(tmpdir): def test_pylint(tmpdir): - check_call(["git", "clone", "https://github.com/PyCQA/pylint.git", str(tmpdir)]) + check_call(["git", "clone", "--depth", "1", "https://github.com/PyCQA/pylint.git", str(tmpdir)]) main(["--check-only", "--diff", str(tmpdir)]) def test_poetry(tmpdir): - check_call(["git", "clone", "https://github.com/python-poetry/poetry.git", str(tmpdir)]) + check_call(["git", "clone", "--depth", "1", "https://github.com/python-poetry/poetry.git", str(tmpdir)]) main(["--check-only", "--diff", str(tmpdir), "--skip", "tests"]) def test_hypothesis(tmpdir): - check_call(["git", "clone", "https://github.com/HypothesisWorks/hypothesis.git", str(tmpdir)]) + check_call(["git", "clone", "--depth", "1", "https://github.com/HypothesisWorks/hypothesis.git", str(tmpdir)]) main(["--check-only", "--diff", str(tmpdir), "--skip", "tests"]) def test_pillow(tmpdir): - check_call(["git", "clone", "https://github.com/python-pillow/Pillow.git", str(tmpdir)]) + check_call(["git", "clone", "--depth", "1", "https://github.com/python-pillow/Pillow.git", str(tmpdir)]) main(["--check-only", "--diff", str(tmpdir), "--skip", "tests"]) From e22aaad104a434fefe0bb6fcafddef14eb75825d Mon Sep 17 00:00:00 2001 From: James Winegar Date: Sun, 23 Aug 2020 17:08:33 +0000 Subject: [PATCH 0960/1439] lint fixes --- .../integration/test_projects_using_isort.py | 67 +++++++++++++++---- 1 file changed, 55 insertions(+), 12 deletions(-) diff --git a/tests/integration/test_projects_using_isort.py b/tests/integration/test_projects_using_isort.py index 66dc5c2f6..036b604ce 100644 --- a/tests/integration/test_projects_using_isort.py +++ b/tests/integration/test_projects_using_isort.py @@ -12,7 +12,9 @@ def test_django(tmpdir): - check_call(["git", "clone", "--depth", "1", "https://github.com/django/django.git", str(tmpdir)]) + check_call( + ["git", "clone", "--depth", "1", "https://github.com/django/django.git", str(tmpdir)] + ) isort_target_dirs = [ str(target_dir) for target_dir in (tmpdir / "django", tmpdir / "tests", tmpdir / "scripts") ] @@ -21,18 +23,29 @@ def test_django(tmpdir): def test_plone(tmpdir): check_call( - ["git", "clone", "--depth", "1", "https://github.com/plone/plone.app.multilingualindexes.git", str(tmpdir)] + [ + "git", + "clone", + "--depth", + "1", + "https://github.com/plone/plone.app.multilingualindexes.git", + str(tmpdir), + ] ) main(["--check-only", "--diff", str(tmpdir / "src")]) def test_pandas(tmpdir): - check_call(["git", "clone", "--depth", "1", "https://github.com/pandas-dev/pandas.git", str(tmpdir)]) + check_call( + ["git", "clone", "--depth", "1", "https://github.com/pandas-dev/pandas.git", str(tmpdir)] + ) main(["--check-only", "--diff", str(tmpdir / "pandas"), "--skip", "__init__.py"]) def test_fastapi(tmpdir): - check_call(["git", "clone", "--depth", "1", "https://github.com/tiangolo/fastapi.git", str(tmpdir)]) + check_call( + ["git", "clone", "--depth", "1", "https://github.com/tiangolo/fastapi.git", str(tmpdir)] + ) main(["--check-only", "--diff", str(tmpdir / "fastapi")]) @@ -42,17 +55,30 @@ def test_zulip(tmpdir): def test_habitat_lab(tmpdir): - check_call(["git", "clone", "--depth", "1", "https://github.com/facebookresearch/habitat-lab.git", str(tmpdir)]) + check_call( + [ + "git", + "clone", + "--depth", + "1", + "https://github.com/facebookresearch/habitat-lab.git", + str(tmpdir), + ] + ) main(["--check-only", "--diff", str(tmpdir)]) def test_tmuxp(tmpdir): - check_call(["git", "clone", "--depth", "1", "https://github.com/tmux-python/tmuxp.git", str(tmpdir)]) + check_call( + ["git", "clone", "--depth", "1", "https://github.com/tmux-python/tmuxp.git", str(tmpdir)] + ) main(["--check-only", "--diff", str(tmpdir)]) def test_websockets(tmpdir): - check_call(["git", "clone", "--depth", "1", "https://github.com/aaugustin/websockets.git", str(tmpdir)]) + check_call( + ["git", "clone", "--depth", "1", "https://github.com/aaugustin/websockets.git", str(tmpdir)] + ) main( [ "--check-only", @@ -69,12 +95,16 @@ def test_websockets(tmpdir): def test_airflow(tmpdir): - check_call(["git", "clone", "--depth", "1", "https://github.com/apache/airflow.git", str(tmpdir)]) + check_call( + ["git", "clone", "--depth", "1", "https://github.com/apache/airflow.git", str(tmpdir)] + ) main(["--check-only", "--diff", str(tmpdir)]) def test_typeshed(tmpdir): - check_call(["git", "clone", "--depth", "1", "https://github.com/python/typeshed.git", str(tmpdir)]) + check_call( + ["git", "clone", "--depth", "1", "https://github.com/python/typeshed.git", str(tmpdir)] + ) main( [ "--check-only", @@ -96,15 +126,28 @@ def test_pylint(tmpdir): def test_poetry(tmpdir): - check_call(["git", "clone", "--depth", "1", "https://github.com/python-poetry/poetry.git", str(tmpdir)]) + check_call( + ["git", "clone", "--depth", "1", "https://github.com/python-poetry/poetry.git", str(tmpdir)] + ) main(["--check-only", "--diff", str(tmpdir), "--skip", "tests"]) def test_hypothesis(tmpdir): - check_call(["git", "clone", "--depth", "1", "https://github.com/HypothesisWorks/hypothesis.git", str(tmpdir)]) + check_call( + [ + "git", + "clone", + "--depth", + "1", + "https://github.com/HypothesisWorks/hypothesis.git", + str(tmpdir), + ] + ) main(["--check-only", "--diff", str(tmpdir), "--skip", "tests"]) def test_pillow(tmpdir): - check_call(["git", "clone", "--depth", "1", "https://github.com/python-pillow/Pillow.git", str(tmpdir)]) + check_call( + ["git", "clone", "--depth", "1", "https://github.com/python-pillow/Pillow.git", str(tmpdir)] + ) main(["--check-only", "--diff", str(tmpdir), "--skip", "tests"]) From ef5cdee0974c3403ee4e576e0f325f7746cc7553 Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Tue, 25 Aug 2020 09:21:13 +0300 Subject: [PATCH 0961/1439] Update timothycrosley references to pycqa --- .cruft.json | 4 +- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 4 +- README.md | 34 ++++++------- docs/configuration/options.md | 2 +- docs/contributing/1.-contributing-guide.md | 6 +-- docs/major_releases/introducing_isort_5.md | 18 +++---- docs/quick_start/0.-try.md | 8 ++-- docs/quick_start/2.-cli.md | 2 +- docs/quick_start/3.-api.md | 2 +- docs/upgrade_guides/5.0.0.md | 14 +++--- docs/warning_and_error_codes/W0500.md | 2 +- isort/main.py | 6 +-- isort/settings.py | 4 +- pyproject.toml | 10 ++-- rtd/index.md | 4 +- scripts/build_config_option_docs.py | 3 +- scripts/check_acknowledgments.py | 2 +- tests/unit/test_isort.py | 4 +- tests/unit/test_regressions.py | 56 +++++++++++----------- tests/unit/test_ticketed_features.py | 22 ++++----- 21 files changed, 105 insertions(+), 104 deletions(-) diff --git a/.cruft.json b/.cruft.json index 2879d2fc6..f70213bc6 100644 --- a/.cruft.json +++ b/.cruft.json @@ -5,7 +5,7 @@ "cookiecutter": { "full_name": "Timothy Crosley", "email": "timothy.crosley@gmail.com", - "github_username": "timothycrosley", + "github_username": "pycqa", "project_name": "isort", "description": "A Python utility / library to sort Python imports.", "version": "4.3.21", @@ -13,4 +13,4 @@ } }, "directory": "" -} \ No newline at end of file +} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 796fbb110..ad78369f1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ repos: - - repo: https://github.com/timothycrosley/isort + - repo: https://github.com/pycqa/isort rev: 5.3.0 hooks: - id: isort diff --git a/CHANGELOG.md b/CHANGELOG.md index 23a296a61..d23d02fd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -134,7 +134,7 @@ Internal Development: - Added warning for deprecated CLI flags and linked to upgrade guide. ### 5.0.3 - July 4, 2020 - - Fixed setup.py command incorrectly passing check=True as a configuration parameter (see: https://github.com/timothycrosley/isort/issues/1258) + - Fixed setup.py command incorrectly passing check=True as a configuration parameter (see: https://github.com/pycqa/isort/issues/1258) - Fixed missing patch version - Fixed issue #1253: Atomic fails when passed in not readable output stream @@ -177,7 +177,7 @@ Internal: - profile support for common project types (black, django, google, etc) -- Much much more. There is some difficulty in fully capturing the extent of changes in this release - just because of how all encompassing the release is. See: [Github Issues](https://github.com/timothycrosley/isort/issues?q=is%3Aissue+is%3Aclosed) for more. +- Much much more. There is some difficulty in fully capturing the extent of changes in this release - just because of how all encompassing the release is. See: [Github Issues](https://github.com/pycqa/isort/issues?q=is%3Aissue+is%3Aclosed) for more. ### 4.3.21 - June 25, 2019 - hot fix release - Fixed issue #957 - Long aliases and use_parentheses generates invalid syntax diff --git a/README.md b/README.md index a77e20171..354cd8d0e 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,21 @@ -[![isort - isort your imports, so you don't have to.](https://raw.githubusercontent.com/timothycrosley/isort/develop/art/logo_large.png)](https://timothycrosley.github.io/isort/) +[![isort - isort your imports, so you don't have to.](https://raw.githubusercontent.com/pycqa/isort/develop/art/logo_large.png)](https://pycqa.github.io/isort/) ------------------------------------------------------------------------ [![PyPI version](https://badge.fury.io/py/isort.svg)](https://badge.fury.io/py/isort) -[![Test Status](https://github.com/timothycrosley/isort/workflows/Test/badge.svg?branch=develop)](https://github.com/timothycrosley/isort/actions?query=workflow%3ATest) -[![Lint Status](https://github.com/timothycrosley/isort/workflows/Lint/badge.svg?branch=develop)](https://github.com/timothycrosley/isort/actions?query=workflow%3ALint) -[![Code coverage Status](https://codecov.io/gh/timothycrosley/isort/branch/develop/graph/badge.svg)](https://codecov.io/gh/timothycrosley/isort) +[![Test Status](https://github.com/pycqa/isort/workflows/Test/badge.svg?branch=develop)](https://github.com/pycqa/isort/actions?query=workflow%3ATest) +[![Lint Status](https://github.com/pycqa/isort/workflows/Lint/badge.svg?branch=develop)](https://github.com/pycqa/isort/actions?query=workflow%3ALint) +[![Code coverage Status](https://codecov.io/gh/pycqa/isort/branch/develop/graph/badge.svg)](https://codecov.io/gh/pycqa/isort) [![Maintainability](https://api.codeclimate.com/v1/badges/060372d3e77573072609/maintainability)](https://codeclimate.com/github/timothycrosley/isort/maintainability) [![License](https://img.shields.io/github/license/mashape/apistatus.svg)](https://pypi.org/project/isort/) -[![Join the chat at https://gitter.im/timothycrosley/isort](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/timothycrosley/isort?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Join the chat at https://gitter.im/pycqa/isort](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/pycqa/isort?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Downloads](https://pepy.tech/badge/isort)](https://pepy.tech/project/isort) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) -[![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://timothycrosley.github.io/isort/) -[![DeepSource](https://static.deepsource.io/deepsource-badge-light-mini.svg)](https://deepsource.io/gh/timothycrosley/isort/?ref=repository-badge) +[![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/) +[![DeepSource](https://static.deepsource.io/deepsource-badge-light-mini.svg)](https://deepsource.io/gh/pycqa/isort/?ref=repository-badge) _________________ -[Read Latest Documentation](https://timothycrosley.github.io/isort/) - [Browse GitHub Code Repository](https://github.com/timothycrosley/isort/) +[Read Latest Documentation](https://pycqa.github.io/isort/) - [Browse GitHub Code Repository](https://github.com/pycqa/isort/) _________________ isort your imports, so you don't have to. @@ -23,13 +23,13 @@ isort your imports, so you don't have to. isort is a Python utility / library to sort imports alphabetically, and automatically separated into sections and by type. It provides a command line utility, Python library and [plugins for various -editors](https://github.com/timothycrosley/isort/wiki/isort-Plugins) to +editors](https://github.com/pycqa/isort/wiki/isort-Plugins) to quickly sort all your imports. It requires Python 3.6+ to run but supports formatting Python 2 code too. -[Try isort now from your browser!](https://timothycrosley.github.io/isort/docs/quick_start/0.-try/) +[Try isort now from your browser!](https://pycqa.github.io/isort/docs/quick_start/0.-try/) -![Example Usage](https://raw.github.com/timothycrosley/isort/develop/example.gif) +![Example Usage](https://raw.github.com/pycqa/isort/develop/example.gif) Before isort: @@ -155,7 +155,7 @@ sorted_code = isort.code("import b\nimport a\n") Several plugins have been written that enable to use isort from within a variety of text-editors. You can find a full list of them [on the isort -wiki](https://github.com/timothycrosley/isort/wiki/isort-Plugins). +wiki](https://github.com/pycqa/isort/wiki/isort-Plugins). Additionally, I will enthusiastically accept pull requests that include plugins for other text editors and add documentation for them as I am notified. @@ -443,7 +443,7 @@ import b from a import a # This will always appear below because it is a from import. ``` -However, if you prefer to keep strict alphabetical sorting you can set [force sort within sections](https://timothycrosley.github.io/isort/docs/configuration/options/#force-sort-within-sections) to true. Resulting in: +However, if you prefer to keep strict alphabetical sorting you can set [force sort within sections](https://pycqa.github.io/isort/docs/configuration/options/#force-sort-within-sections) to true. Resulting in: ```python @@ -451,7 +451,7 @@ from a import a # This will now appear at top because a appears in the alphabet import b ``` -You can even tell isort to always place from imports on top, instead of the default of placing them on bottom, using [from first](https://timothycrosley.github.io/isort/docs/configuration/options/#from-first). +You can even tell isort to always place from imports on top, instead of the default of placing them on bottom, using [from first](https://pycqa.github.io/isort/docs/configuration/options/#from-first). ```python from b import b # If from first is set to True, all from imports will be placed before non-from imports. @@ -608,21 +608,21 @@ setup( ## Spread the word -[![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://timothycrosley.github.io/isort/) +[![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/) Place this badge at the top of your repository to let others know your project uses isort. For README.md: ```markdown -[![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://timothycrosley.github.io/isort/) +[![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/) ``` Or README.rst: ```rst .. image:: https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336 - :target: https://timothycrosley.github.io/isort/ + :target: https://pycqa.github.io/isort/ ``` ## Security contact information diff --git a/docs/configuration/options.md b/docs/configuration/options.md index 307169d57..712b14ae5 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -4,7 +4,7 @@ As a code formatter isort has opinions. However, it also allows you to have your isort will disagree but commit to your way of formatting. To enable this, isort exposes a plethora of options to specify how you want your imports sorted, organized, and formatted. -Too busy to build your perfect isort configuration? For curated common configurations, see isort's [built-in profiles](https://timothycrosley.github.io/isort/docs/configuration/profiles/). +Too busy to build your perfect isort configuration? For curated common configurations, see isort's [built-in profiles](https://pycqa.github.io/isort/docs/configuration/profiles/). ## Python Version diff --git a/docs/contributing/1.-contributing-guide.md b/docs/contributing/1.-contributing-guide.md index 04b3bb9b4..e51e852d8 100644 --- a/docs/contributing/1.-contributing-guide.md +++ b/docs/contributing/1.-contributing-guide.md @@ -21,7 +21,7 @@ Base System Requirements: Once you have verified that your system matches the base requirements you can start to get the project working by following these steps: -1. [Fork the project on GitHub](https://github.com/timothycrosley/isort/fork). +1. [Fork the project on GitHub](https://github.com/pycqa/isort/fork). 2. Clone your fork to your local file system: `git clone https://github.com/$GITHUB_ACCOUNT/isort.git` 3. `cd isort` @@ -57,10 +57,10 @@ A local test cycle might look like the following: ## Making a contribution Congrats! You're now ready to make a contribution! Use the following as a guide to help you reach a successful pull-request: -1. Check the [issues page](https://github.com/timothycrosley/isort/issues) on GitHub to see if the task you want to complete is listed there. +1. Check the [issues page](https://github.com/pycqa/isort/issues) on GitHub to see if the task you want to complete is listed there. - If it's listed there, write a comment letting others know you are working on it. - If it's not listed in GitHub issues, go ahead and log a new issue. Then add a comment letting everyone know you have it under control. - - If you're not sure if it's something that is good for the main isort project and want immediate feedback, you can discuss it [here](https://gitter.im/timothycrosley/isort). + - If you're not sure if it's something that is good for the main isort project and want immediate feedback, you can discuss it [here](https://gitter.im/pycqa/isort). 2. Create an issue branch for your local work `git checkout -b issue/$ISSUE-NUMBER`. 3. Do your magic here. 4. Ensure your code matches the [HOPE-8 Coding Standard](https://github.com/hugapi/HOPE/blob/master/all/HOPE-8--Style-Guide-for-Hug-Code.md#hope-8----style-guide-for-hug-code) used by the project. diff --git a/docs/major_releases/introducing_isort_5.md b/docs/major_releases/introducing_isort_5.md index bc8c96e55..a5cbdda93 100644 --- a/docs/major_releases/introducing_isort_5.md +++ b/docs/major_releases/introducing_isort_5.md @@ -1,16 +1,16 @@ # Introducing isort 5 -[![isort 5 - the best version of isort yet](https://raw.githubusercontent.com/timothycrosley/isort/develop/art/logo_5.png)](https://timothycrosley.github.io/isort/) +[![isort 5 - the best version of isort yet](https://raw.githubusercontent.com/pycqa/isort/develop/art/logo_5.png)](https://pycqa.github.io/isort/) isort 5.0.0 is the first major release of isort in over five years and the first significant refactoring of isort since it was conceived more than ten years ago. It's also the first version to require Python 3 (Python 3.6+ at that!) to run - though it can still be run on source files from any version of Python. This does mean that there may be some pain with the upgrade process, but we believe the improvements will be well worth it. -[Click here for an attempt at full changelog with a list of breaking changes.](https://timothycrosley.github.io/isort/CHANGELOG/) +[Click here for an attempt at full changelog with a list of breaking changes.](https://pycqa.github.io/isort/CHANGELOG/) -[Using isort 4.x.x? Click here for the isort 5.0.0 upgrade guide.](https://timothycrosley.github.io/isort/docs/upgrade_guides/5.0.0/) +[Using isort 4.x.x? Click here for the isort 5.0.0 upgrade guide.](https://pycqa.github.io/isort/docs/upgrade_guides/5.0.0/) -[Try isort 5 right now from your browser!](https://timothycrosley.github.io/isort/docs/quick_start/0.-try/) +[Try isort 5 right now from your browser!](https://pycqa.github.io/isort/docs/quick_start/0.-try/) So why the massive change? @@ -105,7 +105,7 @@ import c import d ``` -isort 5 adds support for [Action Comments](https://timothycrosley.github.io/isort/docs/configuration/action_comments/) which provide a quick and convient way to control the flow of parsing within single source files. +isort 5 adds support for [Action Comments](https://pycqa.github.io/isort/docs/configuration/action_comments/) which provide a quick and convient way to control the flow of parsing within single source files. # First class Python API @@ -122,21 +122,21 @@ import b """ ``` -isort now exposes its programmatic API as a first-class citizen. This API makes it easy to extend or use isort in your own Python project. You can see the full documentation for this new API [here](https://timothycrosley.github.io/isort/reference/isort/api/). +isort now exposes its programmatic API as a first-class citizen. This API makes it easy to extend or use isort in your own Python project. You can see the full documentation for this new API [here](https://pycqa.github.io/isort/reference/isort/api/). # Solid base for the future A major focus for the release was to give isort a solid foundation for the next 5-10 years of the project's life. isort has been refactored into functional components that are easily testable. The project now has 100% code coverage. It utilizes tools like [Hypothesis](https://hypothesis.readthedocs.io/en/latest/) to reduce the number of unexpected errors. -It went from fully dynamic to fully static typing using mypy. Finally, it utilizes the latest linters both on (like [DeepSource](https://deepsource.io/gh/timothycrosley/isort/)) and offline (like [Flake8](https://flake8.pycqa.org/en/latest/)) to help ensure a higher bar for all code contributions into the future. +It went from fully dynamic to fully static typing using mypy. Finally, it utilizes the latest linters both on (like [DeepSource](https://deepsource.io/gh/pycqa/isort/)) and offline (like [Flake8](https://flake8.pycqa.org/en/latest/)) to help ensure a higher bar for all code contributions into the future. # Give 5.0.0 a try! -[Try isort 5 right now from your browser!](https://timothycrosley.github.io/isort/docs/quick_start/0.-try/) +[Try isort 5 right now from your browser!](https://pycqa.github.io/isort/docs/quick_start/0.-try/) OR Install isort locally using `pip3 install isort`. -[Click here for full installation instructions.](https://timothycrosley.github.io/isort/docs/quick_start/1.-install/) +[Click here for full installation instructions.](https://pycqa.github.io/isort/docs/quick_start/1.-install/) diff --git a/docs/quick_start/0.-try.md b/docs/quick_start/0.-try.md index d2c32f6f6..61fae1535 100644 --- a/docs/quick_start/0.-try.md +++ b/docs/quick_start/0.-try.md @@ -13,7 +13,7 @@ Use our live isort editor to see how isort can help improve the formatting of yo - + @@ -32,7 +32,7 @@ import b, a
Loading...
- Configuration (Note: the below must follow JSON format). Full configuration guide is here: + Configuration (Note: the below must follow JSON format). Full configuration guide is here:
{"line_length": 80, "profile": "black", @@ -43,8 +43,8 @@ import b, a
- +
Like what you saw? Installing isort to use locally is as simple as `pip3 install isort`. -[Click here for full installation instructions.](https://timothycrosley.github.io/isort/docs/quick_start/1.-install/) +[Click here for full installation instructions.](https://pycqa.github.io/isort/docs/quick_start/1.-install/) diff --git a/docs/quick_start/2.-cli.md b/docs/quick_start/2.-cli.md index 844533f6a..f7b313464 100644 --- a/docs/quick_start/2.-cli.md +++ b/docs/quick_start/2.-cli.md @@ -3,7 +3,7 @@ Once installed, `isort` exposes a command line utility for sorting, organizing, and formatting imports within Python and Cython source files. To verify the tool is installed correctly, run `isort` from the command line and you should be given the available commands and the version of isort installed. -For a list of all CLI options type `isort --help` or view [the online configuration reference](https://timothycrosley.github.io/isort/docs/configuration/options/): +For a list of all CLI options type `isort --help` or view [the online configuration reference](https://pycqa.github.io/isort/docs/configuration/options/): diff --git a/docs/quick_start/3.-api.md b/docs/quick_start/3.-api.md index 9e6d2e963..1583357f7 100644 --- a/docs/quick_start/3.-api.md +++ b/docs/quick_start/3.-api.md @@ -19,4 +19,4 @@ Highlights include: - `isort.place_module` - Takes the name of a module as a string and returns the categorization determined for it. - `isort.place_module_with_reason` - Takes the name of a module as a string and returns the categorization determined for it and why that categorization was given. -For a full definition of the API see the [API reference documentation](https://timothycrosley.github.io/isort/reference/isort/api/) or try `help(isort)` from an interactive interpreter. +For a full definition of the API see the [API reference documentation](https://pycqa.github.io/isort/reference/isort/api/) or try `help(isort)` from an interactive interpreter. diff --git a/docs/upgrade_guides/5.0.0.md b/docs/upgrade_guides/5.0.0.md index 0b17595dd..6a68acecc 100644 --- a/docs/upgrade_guides/5.0.0.md +++ b/docs/upgrade_guides/5.0.0.md @@ -5,8 +5,8 @@ This guide is meant to help migrate projects from using isort 4.x.x unto the 5.0 Related documentation: -* [isort 5.0.0 changelog](https://timothycrosley.github.io/isort/CHANGELOG/#500-penny-july-4-2020) -* [isort 5 release document](https://timothycrosley.github.io/isort/docs/major_releases/introducing_isort_5/) +* [isort 5.0.0 changelog](https://pycqa.github.io/isort/CHANGELOG/#500-penny-july-4-2020) +* [isort 5 release document](https://pycqa.github.io/isort/docs/major_releases/introducing_isort_5/) !!! important - "If you use pre-commit remove seed-isort-config." If you currently use pre-commit, make sure to see the pre-commit section of this document. In particular, make sure to remove any `seed-isort-config` pre-step. @@ -19,7 +19,7 @@ for those of us who have gotten used to placing imports right above their usage If you want to move all imports to the top, you can use the new`--float-to-top` flag in the CLI or `float_to_top=true` option in your config file. -See: [https://timothycrosley.github.io/isort/docs/configuration/options/#float-to-top](https://timothycrosley.github.io/isort/docs/configuration/options/#float-to-top) +See: [https://pycqa.github.io/isort/docs/configuration/options/#float-to-top](https://pycqa.github.io/isort/docs/configuration/options/#float-to-top) ## Migrating CLI options @@ -46,7 +46,7 @@ The `-v` (previously for version now for verbose) and `-V` (previously for verbo The first thing to keep in mind is how isort loads config options has changed in isort 5. It will no longer merge multiple config files, instead you must have 1 isort config per a project. If you have multiple configs, they will need to be merged into 1 single one. You can see the priority order of configuration files and the manor in which they are loaded on the -[config files documentation page](https://timothycrosley.github.io/isort/docs/configuration/config_files/). +[config files documentation page](https://pycqa.github.io/isort/docs/configuration/config_files/). ### `not_skip` This is the same as the `--dont-skip` CLI option above. In an earlier version isort had a default skip of `__init__.py`. To get around that many projects wanted a way to not skip `__init__.py` or any other files that were automatically skipped in the future by isort. isort no longer has any default skips, so if the value here is `__init__.py` you can simply remove the setting. If it is something else, just make sure you aren't specifying to skip that file somewhere else in your config. @@ -58,10 +58,10 @@ The option was originally added to allow working around this, and was then turne ### `known_standard_library` isort settings no longer merge together, instead they override. The old behavior of merging together caused many hard to track down errors, but the one place it was very convenient was for adding a few additional standard library modules. -In isort 5, you can still get this behavior by moving your extra modules from the `known_standard_library` setting to [`extra_standard_library`](https://timothycrosley.github.io/isort/docs/configuration/options/#extra-standard-library). +In isort 5, you can still get this behavior by moving your extra modules from the `known_standard_library` setting to [`extra_standard_library`](https://pycqa.github.io/isort/docs/configuration/options/#extra-standard-library). ### module placement changes: `known_third_party`, `known_first_party`, `default_section`, etc... -isort has completely rewritten its logic for placing modules in 5.0.0 to ensure the same behavior across environments. You can see the details of this change [here](https://github.com/timothycrosley/isort/issues/1147). +isort has completely rewritten its logic for placing modules in 5.0.0 to ensure the same behavior across environments. You can see the details of this change [here](https://github.com/pycqa/isort/issues/1147). The TL;DR of which is that isort has now changed from `default_section=FIRSTPARTY` to `default_section=THIRDPARTY`. If you all already setting the default section to third party, your config is probably in good shape. If not, you can either use the old finding approach with `--magic-placement` in the CLI or `old_finders=True` in your config, or preferably, you are able to remove all placement options and isort will determine it correctly. If it doesn't, you should be able to just specify your projects modules with `known_first_party` and be done with it. @@ -77,7 +77,7 @@ If you have a step in your precommit called `seed-isort-config` or similar, it i isort now includes an optimized precommit configuration in the repo itself. To use it you can replace any existing isort precommit step with: ``` - - repo: https://github.com/timothycrosley/isort + - repo: https://github.com/pycqa/isort rev: 5.3.2 hooks: - id: isort diff --git a/docs/warning_and_error_codes/W0500.md b/docs/warning_and_error_codes/W0500.md index 2a2148088..a924ffc95 100644 --- a/docs/warning_and_error_codes/W0500.md +++ b/docs/warning_and_error_codes/W0500.md @@ -3,7 +3,7 @@ The W0500 error codes are reserved for warnings related to a major release of the isort project. Generally, the existence of any of these will trigger one additional warning listing the upgrade guide. -For the most recent upgrade guide, see: [The 5.0.0 Upgrade Guide.](https://timothycrosley.github.io/isort/docs/upgrade_guides/5.0.0/). +For the most recent upgrade guide, see: [The 5.0.0 Upgrade Guide.](https://pycqa.github.io/isort/docs/upgrade_guides/5.0.0/). ## W0501: Deprecated CLI flags were included that will be ignored. diff --git a/isort/main.py b/isort/main.py index da912b4c4..ed9517a6b 100644 --- a/isort/main.py +++ b/isort/main.py @@ -61,7 +61,7 @@ `isort . --check --diff` - Check to see if imports are correctly sorted within this project. `isort --help` - In-depth information about isort's available command-line options. -Visit https://timothycrosley.github.io/isort/ for complete information about how to use isort. +Visit https://pycqa.github.io/isort/ for complete information about how to use isort. """ @@ -146,7 +146,7 @@ def _build_arg_parser() -> argparse.ArgumentParser: "interactive behavior." "" "If you've used isort 4 but are new to isort 5, see the upgrading guide:" - "https://timothycrosley.github.io/isort/docs/upgrade_guides/5.0.0/." + "https://pycqa.github.io/isort/docs/upgrade_guides/5.0.0/." ) inline_args_group = parser.add_mutually_exclusive_group() parser.add_argument( @@ -908,7 +908,7 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = ) warn( "W0500: Please see the 5.0.0 Upgrade guide: " - "https://timothycrosley.github.io/isort/docs/upgrade_guides/5.0.0/" + "https://pycqa.github.io/isort/docs/upgrade_guides/5.0.0/" ) if wrong_sorted_files: diff --git a/isort/settings.py b/isort/settings.py index 500790bef..84f445884 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -329,7 +329,7 @@ def __init__( f"Can't set both {key} and {section_name} in the same config file.\n" f"Default to {section_name} if unsure." "\n\n" - "See: https://timothycrosley.github.io/isort/" + "See: https://pycqa.github.io/isort/" "#custom-sections-and-ordering." ) else: @@ -344,7 +344,7 @@ def __init__( f"`{key}` setting is defined, but {maps_to_section} is not" " included in `sections` config option:" f" {combined_config.get('sections', SECTION_DEFAULTS)}.\n\n" - "See: https://timothycrosley.github.io/isort/" + "See: https://pycqa.github.io/isort/" "#custom-sections-and-ordering." ) if key.startswith(IMPORT_HEADING_PREFIX): diff --git a/pyproject.toml b/pyproject.toml index 72ce030af..1e3e1e81b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,9 +8,9 @@ description = "A Python utility / library to sort Python imports." authors = ["Timothy Crosley "] license = "MIT" readme = "README.md" -repository = "https://github.com/timothycrosley/isort" -homepage = "https://timothycrosley.github.io/isort/" -documentation = "https://timothycrosley.github.io/isort/" +repository = "https://github.com/pycqa/isort" +homepage = "https://pycqa.github.io/isort/" +documentation = "https://pycqa.github.io/isort/" keywords = ["Refactor", "Lint", "Imports", "Sort", "Clean"] classifiers = [ "Development Status :: 6 - Mature", @@ -30,7 +30,7 @@ classifiers = [ "Topic :: Software Development :: Libraries", "Topic :: Utilities", ] -urls = { Changelog = "https://github.com/timothycrosley/isort/blob/master/CHANGELOG.md" } +urls = { Changelog = "https://github.com/pycqa/isort/blob/master/CHANGELOG.md" } packages = [ { include = "isort" }, { include = "tests", format = "sdist" }, @@ -91,7 +91,7 @@ isort = "isort.main:ISortCommand" isort = "isort = isort.pylama_isort:Linter" [tool.portray.mkdocs] -edit_uri = "https://github.com/timothycrosley/isort/edit/develop/" +edit_uri = "https://github.com/pycqa/isort/edit/develop/" extra_css = ["art/stylesheets/extra.css"] [tool.portray.mkdocs.theme] diff --git a/rtd/index.md b/rtd/index.md index 6a6451dd7..7a1c706bb 100644 --- a/rtd/index.md +++ b/rtd/index.md @@ -1,6 +1,6 @@ The latest isort docs are not on ReadTheDocs. This page contains a javascript redirect to the latest documentation. -If this redirect doesn't work, please [click here for the latest documentation](https://timothycrosley.github.io/isort/). +If this redirect doesn't work, please [click here for the latest documentation](https://pycqa.github.io/isort/). diff --git a/scripts/build_config_option_docs.py b/scripts/build_config_option_docs.py index 917732bd6..d57e96c51 100755 --- a/scripts/build_config_option_docs.py +++ b/scripts/build_config_option_docs.py @@ -21,7 +21,8 @@ isort will disagree but commit to your way of formatting. To enable this, isort exposes a plethora of options to specify how you want your imports sorted, organized, and formatted. -Too busy to build your perfect isort configuration? For curated common configurations, see isort's [built-in profiles](https://timothycrosley.github.io/isort/docs/configuration/profiles/). +Too busy to build your perfect isort configuration? For curated common configurations, see isort's [built-in +profiles](https://pycqa.github.io/isort/docs/configuration/profiles/). """ parser = _build_arg_parser() diff --git a/scripts/check_acknowledgments.py b/scripts/check_acknowledgments.py index eab364e57..27aa84c8a 100755 --- a/scripts/check_acknowledgments.py +++ b/scripts/check_acknowledgments.py @@ -10,7 +10,7 @@ IGNORED_AUTHOR_LOGINS = {"deepsource-autofix[bot]"} -REPO = "timothycrosley/isort" +REPO = "pycqa/isort" GITHUB_API_CONTRIBUTORS = f"https://api.github.com/repos/{REPO}/contributors" GITHUB_USER_CONTRIBUTIONS = f"https://github.com/{REPO}/commits?author=" GITHUB_USER_TYPE = "User" diff --git a/tests/unit/test_isort.py b/tests/unit/test_isort.py index bcbc0f3a5..363622e00 100644 --- a/tests/unit/test_isort.py +++ b/tests/unit/test_isort.py @@ -1209,7 +1209,7 @@ def test_force_single_line_imports_and_sort_within_sections() -> None: ) # Ensure force_sort_within_sections can work with length sort - # See: https://github.com/timothycrosley/isort/issues/1038 + # See: https://github.com/pycqa/isort/issues/1038 test_input = """import sympy import numpy as np import pandas as pd @@ -3964,7 +3964,7 @@ def test_isort_with_single_character_import() -> None: """Tests to ensure isort handles single capatilized single character imports as class objects by default - See Issue #376: https://github.com/timothycrosley/isort/issues/376 + See Issue #376: https://github.com/pycqa/isort/issues/376 """ test_input = "from django.db.models import CASCADE, SET_NULL, Q\n" assert isort.code(test_input) == test_input diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index 4d5758c18..98621545b 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -6,7 +6,7 @@ def test_isort_duplicating_comments_issue_1264(): """Ensure isort doesn't duplicate comments when force_sort_within_sections is set to `True` - as was the case in issue #1264: https://github.com/timothycrosley/isort/issues/1264 + as was the case in issue #1264: https://github.com/pycqa/isort/issues/1264 """ assert ( isort.code( @@ -42,7 +42,7 @@ def test_moving_comments_issue_726(): def test_blank_lined_removed_issue_1275(): """Ensure isort doesn't accidentally remove blank lines after doc strings and before imports. - See: https://github.com/timothycrosley/isort/issues/1275 + See: https://github.com/pycqa/isort/issues/1275 """ assert ( isort.code( @@ -86,7 +86,7 @@ def test_blank_lined_removed_issue_1275(): def test_blank_lined_removed_issue_1283(): """Ensure isort doesn't accidentally remove blank lines after __version__ identifiers. - See: https://github.com/timothycrosley/isort/issues/1283 + See: https://github.com/pycqa/isort/issues/1283 """ test_input = """__version__ = "0.58.1" @@ -97,7 +97,7 @@ def test_blank_lined_removed_issue_1283(): def test_extra_blank_line_added_nested_imports_issue_1290(): """Ensure isort doesn't added unecessary blank lines above nested imports. - See: https://github.com/timothycrosley/isort/issues/1290 + See: https://github.com/pycqa/isort/issues/1290 """ test_input = '''from typing import TYPE_CHECKING @@ -129,7 +129,7 @@ def func(): def test_add_imports_shouldnt_make_isort_unusable_issue_1297(): """Test to ensure add imports doesn't cause any unexpected behaviour when combined with check - See: https://github.com/timothycrosley/isort/issues/1297 + See: https://github.com/pycqa/isort/issues/1297 """ assert isort.check_code( """from __future__ import unicode_literals @@ -142,7 +142,7 @@ def test_add_imports_shouldnt_make_isort_unusable_issue_1297(): def test_no_extra_lines_for_imports_in_functions_issue_1277(): """Test to ensure isort doesn't introduce extra blank lines for imports within function. - See: https://github.com/timothycrosley/isort/issues/1277 + See: https://github.com/pycqa/isort/issues/1277 """ test_input = """ def main(): @@ -160,7 +160,7 @@ def main(): def test_no_extra_blank_lines_in_methods_issue_1293(): """Test to ensure isort isn't introducing extra lines in methods that contain imports - See: https://github.com/timothycrosley/isort/issues/1293 + See: https://github.com/pycqa/isort/issues/1293 """ test_input = """ @@ -178,7 +178,7 @@ def on_email_deleted(self, email): def test_force_single_line_shouldnt_remove_preceding_comment_lines_issue_1296(): """Tests to ensure force_single_line setting doesn't result in lost comments. - See: https://github.com/timothycrosley/isort/issues/1296 + See: https://github.com/pycqa/isort/issues/1296 """ test_input = """ # A comment @@ -195,7 +195,7 @@ def test_ensure_new_line_before_comments_mixed_with_ensure_newline_before_commen """Tests to ensure that the black profile can be used in conjunction with force_sort_within_sections. - See: https://github.com/timothycrosley/isort/issues/1295 + See: https://github.com/pycqa/isort/issues/1295 """ test_input = """ from openzwave.group import ZWaveGroup @@ -210,7 +210,7 @@ def test_ensure_new_line_before_comments_mixed_with_ensure_newline_before_commen def test_trailing_comma_doesnt_introduce_broken_code_with_comment_and_wrap_issue_1302(): """Tests to assert the combination of include_trailing_comma and a wrapped line doesnt break. - See: https://github.com/timothycrosley/isort/issues/1302. + See: https://github.com/pycqa/isort/issues/1302. """ assert ( isort.code( @@ -229,14 +229,14 @@ def test_trailing_comma_doesnt_introduce_broken_code_with_comment_and_wrap_issue def test_ensure_sre_parse_is_identified_as_stdlib_issue_1304(): """Ensure sre_parse is idenified as STDLIB. - See: https://github.com/timothycrosley/isort/issues/1304. + See: https://github.com/pycqa/isort/issues/1304. """ assert isort.place_module("sre_parse") == isort.place_module("sre") == isort.settings.STDLIB def test_add_imports_shouldnt_move_lower_comments_issue_1300(): """Ensure add_imports doesn't move comments immediately below imports. - See:: https://github.com/timothycrosley/isort/issues/1300. + See:: https://github.com/pycqa/isort/issues/1300. """ test_input = """from __future__ import unicode_literals @@ -250,7 +250,7 @@ def test_add_imports_shouldnt_move_lower_comments_issue_1300(): def test_windows_newline_issue_1277(): """Test to ensure windows new lines are correctly handled within indented scopes. - See: https://github.com/timothycrosley/isort/issues/1277 + See: https://github.com/pycqa/isort/issues/1277 """ assert ( isort.code("\ndef main():\r\n import time\r\n\n import sys\r\n") @@ -260,7 +260,7 @@ def test_windows_newline_issue_1277(): def test_windows_newline_issue_1278(): """Test to ensure windows new lines are correctly handled within indented scopes. - See: https://github.com/timothycrosley/isort/issues/1278 + See: https://github.com/pycqa/isort/issues/1278 """ assert isort.check_code( "\ntry:\r\n import datadog_agent\r\n\r\n " @@ -271,7 +271,7 @@ def test_windows_newline_issue_1278(): def test_check_never_passes_with_indented_headings_issue_1301(): """Test to ensure that test can pass even when there are indented headings. - See: https://github.com/timothycrosley/isort/issues/1301 + See: https://github.com/pycqa/isort/issues/1301 """ assert isort.check_code( """ @@ -289,7 +289,7 @@ def test_check_never_passes_with_indented_headings_issue_1301(): def test_isort_shouldnt_fail_on_long_from_with_dot_issue_1190(): """Test to ensure that isort will correctly handle formatting a long from import that contains a dot. - See: https://github.com/timothycrosley/isort/issues/1190 + See: https://github.com/pycqa/isort/issues/1190 """ assert ( isort.code( @@ -315,7 +315,7 @@ def test_isort_shouldnt_fail_on_long_from_with_dot_issue_1190(): def test_isort_shouldnt_add_extra_new_line_when_fass_and_n_issue_1315(): """Test to ensure isort doesnt add a second extra new line when combining --fss and -n options. - See: https://github.com/timothycrosley/isort/issues/1315 + See: https://github.com/pycqa/isort/issues/1315 """ assert isort.check_code( """import sys @@ -351,7 +351,7 @@ def test_isort_doesnt_rewrite_import_with_dot_to_from_import_issue_1280(): """Test to ensure isort doesn't rewrite imports in the from of import y.x into from y import x. This is because they are not technically fully equivalent to eachother and can introduce broken behaviour. - See: https://github.com/timothycrosley/isort/issues/1280 + See: https://github.com/pycqa/isort/issues/1280 """ assert isort.check_code( """ @@ -366,7 +366,7 @@ def test_isort_doesnt_rewrite_import_with_dot_to_from_import_issue_1280(): def test_isort_shouldnt_introduce_extra_lines_with_fass_issue_1322(): """Tests to ensure isort doesn't introduce extra lines when used with fass option. - See: https://github.com/timothycrosley/isort/issues/1322 + See: https://github.com/pycqa/isort/issues/1322 """ assert ( isort.code( @@ -393,7 +393,7 @@ def test_isort_shouldnt_introduce_extra_lines_with_fass_issue_1322(): def test_comments_should_cause_wrapping_on_long_lines_black_mode_issue_1219(): """Tests to ensure if isort encounters a single import line which is made too long with a comment it is wrapped when using black profile. - See: https://github.com/timothycrosley/isort/issues/1219 + See: https://github.com/pycqa/isort/issues/1219 """ assert isort.check_code( """ @@ -409,7 +409,7 @@ def test_comments_should_cause_wrapping_on_long_lines_black_mode_issue_1219(): def test_comment_blocks_should_stay_associated_without_extra_lines_issue_1156(): """Tests to ensure isort doesn't add an extra line when there are large import blocks or otherwise warp the intent. - See: https://github.com/timothycrosley/isort/issues/1156 + See: https://github.com/pycqa/isort/issues/1156 """ assert ( isort.code( @@ -434,7 +434,7 @@ def test_comment_blocks_should_stay_associated_without_extra_lines_issue_1156(): def test_comment_shouldnt_be_duplicated_with_fass_enabled_issue_1329(): """Tests to ensure isort doesn't duplicate comments when imports occur with comment on top, immediately after large comment blocks. - See: https://github.com/timothycrosley/isort/pull/1329/files. + See: https://github.com/pycqa/isort/pull/1329/files. """ assert isort.check_code( """''' @@ -469,7 +469,7 @@ def function(): def test_isort_skipped_nested_imports_issue_1339(): """Ensure `isort:skip are honored in nested imports. - See: https://github.com/timothycrosley/isort/issues/1339. + See: https://github.com/pycqa/isort/issues/1339. """ assert isort.check_code( """ @@ -484,7 +484,7 @@ def import_test(): def test_windows_diff_too_large_misrepresentative_issue_1348(test_path): """Ensure isort handles windows files correctly when it come to producing a diff with --diff. - See: https://github.com/timothycrosley/isort/issues/1348 + See: https://github.com/pycqa/isort/issues/1348 """ diff_output = StringIO() isort.file(test_path / "example_crlf_file.py", show_diff=diff_output) @@ -496,7 +496,7 @@ def test_windows_diff_too_large_misrepresentative_issue_1348(test_path): def test_combine_as_does_not_lose_comments_issue_1321(): """Test to ensure isort doesn't lose comments when --combine-as is used. - See: https://github.com/timothycrosley/isort/issues/1321 + See: https://github.com/pycqa/isort/issues/1321 """ test_input = """ from foo import * # noqa @@ -524,7 +524,7 @@ def test_combine_as_does_not_lose_comments_issue_1321(): def test_combine_as_does_not_lose_comments_issue_1381(): """Test to ensure isort doesn't lose comments when --combine-as is used. - See: https://github.com/timothycrosley/isort/issues/1381 + See: https://github.com/pycqa/isort/issues/1381 """ test_input = """ from smtplib import SMTPConnectError, SMTPNotSupportedError # important comment @@ -539,7 +539,7 @@ def test_combine_as_does_not_lose_comments_issue_1381(): def test_incorrect_grouping_when_comments_issue_1396(): """Test to ensure isort groups import correct independent of the comments present. - See: https://github.com/timothycrosley/isort/issues/1396 + See: https://github.com/pycqa/isort/issues/1396 """ assert ( isort.code( @@ -601,7 +601,7 @@ def test_incorrect_grouping_when_comments_issue_1396(): def test_reverse_relative_combined_with_force_sort_within_sections_issue_1395(): """Test to ensure reverse relative combines well with other common isort settings. - See: https://github.com/timothycrosley/isort/issues/1395. + See: https://github.com/pycqa/isort/issues/1395. """ assert isort.check_code( """from .fileA import a_var diff --git a/tests/unit/test_ticketed_features.py b/tests/unit/test_ticketed_features.py index 5828ec9d8..878b2a03a 100644 --- a/tests/unit/test_ticketed_features.py +++ b/tests/unit/test_ticketed_features.py @@ -13,7 +13,7 @@ def test_semicolon_ignored_for_dynamic_lines_after_import_issue_1178(): """Test to ensure even if a semicolon is in the decorator in the line following an import the correct line spacing detrmination will be made. - See: https://github.com/timothycrosley/isort/issues/1178. + See: https://github.com/pycqa/isort/issues/1178. """ assert isort.check_code( """ @@ -29,7 +29,7 @@ def test_thing(): pass def test_isort_automatically_removes_duplicate_aliases_issue_1193(): """Test to ensure isort can automatically remove duplicate aliases. - See: https://github.com/timothycrosley/isort/issues/1281 + See: https://github.com/pycqa/isort/issues/1281 """ assert isort.check_code("from urllib import parse as parse\n", show_diff=True) assert ( @@ -42,7 +42,7 @@ def test_isort_automatically_removes_duplicate_aliases_issue_1193(): def test_isort_enables_floating_imports_to_top_of_module_issue_1228(): """Test to ensure isort will allow floating all non-indented imports to the top of a file. - See: https://github.com/timothycrosley/isort/issues/1228. + See: https://github.com/pycqa/isort/issues/1228. """ assert ( isort.code( @@ -159,7 +159,7 @@ def my_function_2(): def test_isort_provides_official_api_for_diff_output_issue_1335(): """Test to ensure isort API for diff capturing allows capturing diff without sys.stdout. - See: https://github.com/timothycrosley/isort/issues/1335. + See: https://github.com/pycqa/isort/issues/1335. """ diff_output = StringIO() isort.code("import b\nimport a\n", show_diff=diff_output) @@ -169,7 +169,7 @@ def test_isort_provides_official_api_for_diff_output_issue_1335(): def test_isort_warns_when_known_sections_dont_match_issue_1331(): """Test to ensure that isort warns if there is a mismatch between sections and known_sections. - See: https://github.com/timothycrosley/isort/issues/1331. + See: https://github.com/pycqa/isort/issues/1331. """ assert ( isort.place_module( @@ -203,7 +203,7 @@ def test_isort_warns_when_known_sections_dont_match_issue_1331(): def test_isort_supports_append_only_imports_issue_727(): """Test to ensure isort provides a way to only add imports as an append. - See: https://github.com/timothycrosley/isort/issues/727. + See: https://github.com/pycqa/isort/issues/727. """ assert isort.code("", add_imports=["from __future__ import absolute_imports"]) == "" assert ( @@ -217,7 +217,7 @@ def test_isort_supports_append_only_imports_issue_727(): def test_isort_supports_shared_profiles_issue_970(): """Test to ensure isort provides a way to use shared profiles. - See: https://github.com/timothycrosley/isort/issues/970. + See: https://github.com/pycqa/isort/issues/970. """ assert isort.code("import a", profile="example") == "import a\n" # shared profile assert isort.code("import a", profile="black") == "import a\n" # bundled profile @@ -227,7 +227,7 @@ def test_isort_supports_shared_profiles_issue_970(): def test_isort_supports_formatting_plugins_issue_1353(): """Test to ensure isort provides a way to create and share formatting plugins. - See: https://github.com/timothycrosley/isort/issues/1353. + See: https://github.com/pycqa/isort/issues/1353. """ assert isort.code("import a", formatter="example") == "import a\n" # formatting plugin with pytest.raises(exceptions.FormattingPluginDoesNotExist): @@ -236,7 +236,7 @@ def test_isort_supports_formatting_plugins_issue_1353(): def test_treating_comments_as_code_issue_1357(): """Test to ensure isort provides a way to treat comments as code. - See: https://github.com/timothycrosley/isort/issues/1357 + See: https://github.com/pycqa/isort/issues/1357 """ assert ( isort.code( @@ -488,7 +488,7 @@ def method(): def test_isort_allows_setting_import_types_issue_1181(): """Test to ensure isort provides a way to set the type of imports. - See: https://github.com/timothycrosley/isort/issues/1181 + See: https://github.com/pycqa/isort/issues/1181 """ assert isort.code("from x import AA, Big, variable") == "from x import AA, Big, variable\n" assert ( @@ -512,7 +512,7 @@ def test_isort_allows_setting_import_types_issue_1181(): def test_isort_enables_deduping_section_headers_issue_953(): """isort should provide a way to only have identical import headings show up once. - See: https://github.com/timothycrosley/isort/issues/953 + See: https://github.com/pycqa/isort/issues/953 """ isort_code = partial( isort.code, From 570e665767f5db8067e59d8bb014e2cc4eaa83dd Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Tue, 25 Aug 2020 09:28:08 +0300 Subject: [PATCH 0962/1439] Switch back broken Deepsource and Gitter links --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 354cd8d0e..a5564312c 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,11 @@ [![Code coverage Status](https://codecov.io/gh/pycqa/isort/branch/develop/graph/badge.svg)](https://codecov.io/gh/pycqa/isort) [![Maintainability](https://api.codeclimate.com/v1/badges/060372d3e77573072609/maintainability)](https://codeclimate.com/github/timothycrosley/isort/maintainability) [![License](https://img.shields.io/github/license/mashape/apistatus.svg)](https://pypi.org/project/isort/) -[![Join the chat at https://gitter.im/pycqa/isort](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/pycqa/isort?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Join the chat at https://gitter.im/timothycrosley/isort](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/timothycrosley/isort?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Downloads](https://pepy.tech/badge/isort)](https://pepy.tech/project/isort) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/) -[![DeepSource](https://static.deepsource.io/deepsource-badge-light-mini.svg)](https://deepsource.io/gh/pycqa/isort/?ref=repository-badge) +[![DeepSource](https://static.deepsource.io/deepsource-badge-light-mini.svg)](https://deepsource.io/gh/timothycrosley/isort/?ref=repository-badge) _________________ [Read Latest Documentation](https://pycqa.github.io/isort/) - [Browse GitHub Code Repository](https://github.com/pycqa/isort/) From cf44d6720f27973cdb54a48bbe8b91bae9055821 Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Tue, 25 Aug 2020 10:34:31 +0300 Subject: [PATCH 0963/1439] Fix typo in constant name. --- isort/output.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/output.py b/isort/output.py index ecd246621..e410b7adf 100644 --- a/isort/output.py +++ b/isort/output.py @@ -9,7 +9,7 @@ from .comments import add_to_line as with_comments from .settings import DEFAULT_CONFIG, Config -STATEMENT_DECLERATIONS: Tuple[str, ...] = ("def ", "cdef ", "cpdef ", "class ", "@", "async def") +STATEMENT_DECLARATIONS: Tuple[str, ...] = ("def ", "cdef ", "cpdef ", "class ", "@", "async def") def sorted_imports( @@ -186,7 +186,7 @@ def sorted_imports( if config.lines_after_imports != -1: formatted_output[imports_tail:0] = ["" for line in range(config.lines_after_imports)] - elif extension != "pyi" and next_construct.startswith(STATEMENT_DECLERATIONS): + elif extension != "pyi" and next_construct.startswith(STATEMENT_DECLARATIONS): formatted_output[imports_tail:0] = ["", ""] else: formatted_output[imports_tail:0] = [""] From 167844cf9ada9fdfc84cc679806a23cca2fe750d Mon Sep 17 00:00:00 2001 From: Nicholas Devenish Date: Tue, 25 Aug 2020 16:14:36 +0100 Subject: [PATCH 0964/1439] Fix documentation for skip_file action comment This was documented as skip-file, whereas the default in settings.py is skip_file (with an underscore). --- docs/configuration/action_comments.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration/action_comments.md b/docs/configuration/action_comments.md index ca1edd25e..11b1a1a34 100644 --- a/docs/configuration/action_comments.md +++ b/docs/configuration/action_comments.md @@ -3,7 +3,7 @@ The most basic way to configure the flow of isort within a single file is action comments. These comments are picked up and interpreted by the isort parser during parsing. -## isort: skip-file +## isort: skip_file Tells isort to skip the entire file. @@ -11,7 +11,7 @@ Example: ```python # !/bin/python3 -# isort: skip-file +# isort: skip_file import os import sys From 1371f0429fb26d3ee79070e523a072b2c2adeccc Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Tue, 25 Aug 2020 21:15:34 +0300 Subject: [PATCH 0965/1439] Adds colors to --diff output. Resolves #1405 --- isort/api.py | 3 +++ isort/format.py | 48 ++++++++++++++++++++++++++++++++------- tests/unit/test_format.py | 42 ++++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 8 deletions(-) diff --git a/isort/api.py b/isort/api.py index 059bbf9e5..8b0ca237d 100644 --- a/isort/api.py +++ b/isort/api.py @@ -131,6 +131,7 @@ def sort_stream( file_output=_output_stream.read(), file_path=file_path, output=output_stream if show_diff is True else cast(TextIO, show_diff), + color_output=config.color_output, ) return changed @@ -233,6 +234,7 @@ def check_stream( file_output=output_stream.read(), file_path=file_path, output=None if show_diff is True else cast(TextIO, show_diff), + color_output=config.color_output, ) return False @@ -335,6 +337,7 @@ def sort_file( file_output=tmp_out.read(), file_path=file_path or source_file.path, output=None if show_diff is True else cast(TextIO, show_diff), + color_output=config.color_output, ) if show_diff or ( ask_to_apply diff --git a/isort/format.py b/isort/format.py index 3dbb19570..67c8c5b1c 100644 --- a/isort/format.py +++ b/isort/format.py @@ -1,3 +1,4 @@ +import re import sys from datetime import datetime from difflib import unified_diff @@ -13,6 +14,10 @@ colorama.init() +ADDED_LINE_PATTERN = re.compile(r"\+[^+]") +REMOVED_LINE_PATTERN = re.compile(r"-[^-]") + + def format_simplified(import_line: str) -> str: import_line = import_line.strip() if import_line.startswith("from "): @@ -37,7 +42,12 @@ def format_natural(import_line: str) -> str: def show_unified_diff( - *, file_input: str, file_output: str, file_path: Optional[Path], output: Optional[TextIO] = None + *, + file_input: str, + file_output: str, + file_path: Optional[Path], + output: Optional[TextIO] = None, + color_output: bool = False, ): """Shows a unified_diff for the provided input and output against the provided file path. @@ -45,8 +55,9 @@ def show_unified_diff( - **file_output**: A string that represents the contents of a file after changes. - **file_path**: A Path object that represents the file path of the file being changed. - **output**: A stream to output the diff to. If non is provided uses sys.stdout. + - **color_output**: Use color in output if True. """ - output = sys.stdout if output is None else output + printer = create_terminal_printer(color_output, output) file_name = "" if file_path is None else str(file_path) file_mtime = str( datetime.now() if file_path is None else datetime.fromtimestamp(file_path.stat().st_mtime) @@ -60,7 +71,7 @@ def show_unified_diff( tofiledate=str(datetime.now()), ) for line in unified_diff_lines: - output.write(line) + printer.diff_line(line) def ask_whether_to_apply_changes_to_file(file_path: str) -> bool: @@ -84,28 +95,49 @@ class BasicPrinter: ERROR = "ERROR" SUCCESS = "SUCCESS" + def __init__(self, output: Optional[TextIO] = None): + self.output = output or sys.stdout + def success(self, message: str) -> None: - print(f"{self.SUCCESS}: {message}") + print(f"{self.SUCCESS}: {message}", file=self.output) def error(self, message: str) -> None: print( f"{self.ERROR}: {message}", + file=self.output, # TODO this should print to stderr, but don't want to make it backward incompatible now # file=sys.stderr ) + def diff_line(self, line: str) -> None: + self.output.write(line) + class ColoramaPrinter(BasicPrinter): - def __init__(self): + ADDED_LINE = colorama.Fore.GREEN + REMOVED_LINE = colorama.Fore.RED + + def __init__(self, output: Optional[TextIO] = None): + self.output = output or sys.stdout self.ERROR = self.style_text("ERROR", colorama.Fore.RED) self.SUCCESS = self.style_text("SUCCESS", colorama.Fore.GREEN) @staticmethod - def style_text(text: str, style: str) -> str: + def style_text(text: str, style: Optional[str] = None) -> str: + if style is None: + return text return style + text + colorama.Style.RESET_ALL + def diff_line(self, line: str) -> None: + style = None + if re.match(ADDED_LINE_PATTERN, line): + style = self.ADDED_LINE + elif re.match(REMOVED_LINE_PATTERN, line): + style = self.REMOVED_LINE + self.output.write(self.style_text(line, style)) + -def create_terminal_printer(color: bool): +def create_terminal_printer(color: bool, output: Optional[TextIO] = None): if color and colorama_unavailable: no_colorama_message = ( "\n" @@ -118,4 +150,4 @@ def create_terminal_printer(color: bool): print(no_colorama_message, file=sys.stderr) sys.exit(1) - return ColoramaPrinter() if color else BasicPrinter() + return ColoramaPrinter(output) if color else BasicPrinter(output) diff --git a/tests/unit/test_format.py b/tests/unit/test_format.py index 3a8f7236b..a2658367d 100644 --- a/tests/unit/test_format.py +++ b/tests/unit/test_format.py @@ -1,3 +1,4 @@ +from io import StringIO from unittest.mock import MagicMock, patch import colorama @@ -29,6 +30,15 @@ def test_basic_printer(capsys): assert out == "ERROR: Some error\n" +def test_basic_printer_diff(capsys): + printer = isort.format.create_terminal_printer(color=False) + printer.diff_line("+ added line\n") + printer.diff_line("- removed line\n") + + out, _ = capsys.readouterr() + assert out == "+ added line\n- removed line\n" + + def test_colored_printer_success(capsys): printer = isort.format.create_terminal_printer(color=True) printer.success("All good!") @@ -47,6 +57,38 @@ def test_colored_printer_error(capsys): assert colorama.Fore.RED in out +def test_colored_printer_diff(capsys): + printer = isort.format.create_terminal_printer(color=True) + printer.diff_line("+++ file1\n") + printer.diff_line("--- file2\n") + printer.diff_line("+ added line\n") + printer.diff_line("normal line\n") + printer.diff_line("- removed line\n") + printer.diff_line("normal line\n") + + out, _ = capsys.readouterr() + # No color added to lines with multiple + and -'s + assert out.startswith("+++ file1\n--- file2\n") + # Added lines are green + assert colorama.Fore.GREEN + "+ added line" in out + # Removed lines are red + assert colorama.Fore.RED + "- removed line" in out + # Normal lines are resetted back + assert colorama.Style.RESET_ALL + "normal line" in out + + +def test_colored_printer_diff_output(capsys): + output = StringIO() + printer = isort.format.create_terminal_printer(color=True, output=output) + printer.diff_line("a line\n") + + out, _ = capsys.readouterr() + assert out == "" + + output.seek(0) + assert output.read().startswith("a line\n") + + @patch("isort.format.colorama_unavailable", True) def test_colorama_not_available_handled_gracefully(capsys): with pytest.raises(SystemExit) as system_exit: From bddd1d2c66bd884488aea71ee26753f77e3bcc5e Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Tue, 25 Aug 2020 22:49:14 +0300 Subject: [PATCH 0966/1439] Disable airflow integration tests temporarily. Airflow just enabled Black on api_connexion and providers packages. They've updated their pre-commit hooks, but forgot to update the isort section in setup.cfg to ignore all those files. Left a comment for them to update their setup.cfg. https://github.com/apache/airflow/pull/10543#issuecomment-680231216 --- tests/integration/test_projects_using_isort.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/integration/test_projects_using_isort.py b/tests/integration/test_projects_using_isort.py index 036b604ce..08f174fd4 100644 --- a/tests/integration/test_projects_using_isort.py +++ b/tests/integration/test_projects_using_isort.py @@ -8,6 +8,8 @@ """ from subprocess import check_call +import pytest + from isort.main import main @@ -94,6 +96,9 @@ def test_websockets(tmpdir): ) +# TODO Re-enable when airflow updates their isort config +# Reference: https://github.com/apache/airflow/pull/10543#issuecomment-680231216 +@pytest.mark.skip(reason="Airflow isort config currently broken") def test_airflow(tmpdir): check_call( ["git", "clone", "--depth", "1", "https://github.com/apache/airflow.git", str(tmpdir)] From 96a0f76db516570760107af6436eb1043c3c8b9b Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Wed, 26 Aug 2020 00:20:45 +0300 Subject: [PATCH 0967/1439] Re-enabling Airflow integration tests. Airflow's (very quick!) fix: https://github.com/apache/airflow/pull/10557 --- tests/integration/test_projects_using_isort.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/integration/test_projects_using_isort.py b/tests/integration/test_projects_using_isort.py index 08f174fd4..036b604ce 100644 --- a/tests/integration/test_projects_using_isort.py +++ b/tests/integration/test_projects_using_isort.py @@ -8,8 +8,6 @@ """ from subprocess import check_call -import pytest - from isort.main import main @@ -96,9 +94,6 @@ def test_websockets(tmpdir): ) -# TODO Re-enable when airflow updates their isort config -# Reference: https://github.com/apache/airflow/pull/10543#issuecomment-680231216 -@pytest.mark.skip(reason="Airflow isort config currently broken") def test_airflow(tmpdir): check_call( ["git", "clone", "--depth", "1", "https://github.com/apache/airflow.git", str(tmpdir)] From b21741208131420783da113a8f8b99b17e9413bb Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 25 Aug 2020 22:28:12 -0700 Subject: [PATCH 0968/1439] Initial fix for #1380: dont combine as into star as it will change application functionality --- isort/output.py | 9 ++--- isort/settings.py | 2 +- tests/unit/test_isort.py | 55 +++++++++++++++++++++++++--- tests/unit/test_ticketed_features.py | 22 +++++++++++ 4 files changed, 76 insertions(+), 12 deletions(-) diff --git a/isort/output.py b/isort/output.py index e410b7adf..445ead2a6 100644 --- a/isort/output.py +++ b/isort/output.py @@ -264,11 +264,6 @@ def _with_from_imports( output.extend(above_comments) if "*" in from_imports and config.combine_star: - if config.combine_as_imports: - comments = list(comments or ()) - comments += parsed.categorized_comments["from"].pop( - f"{module}.__combined_as__", [] - ) import_statement = wrap.line( with_comments( comments, @@ -279,7 +274,9 @@ def _with_from_imports( parsed.line_separator, config, ) - from_imports = [] + from_imports = [ + from_import for from_import in from_imports if from_import in as_imports + ] elif config.force_single_line and module not in config.single_line_exclusions: import_statement = "" while from_imports: diff --git a/isort/settings.py b/isort/settings.py index 84f445884..1f58a57e9 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -503,7 +503,7 @@ def known_patterns(self): config_key = f"{KNOWN_PREFIX}{known_placement}" known_modules = getattr(self, config_key, self.known_other.get(known_placement, ())) extra_modules = getattr(self, f"extra_{known_placement}", ()) - all_modules = set(known_modules).union(extra_modules) + all_modules = set(extra_modules).union(known_modules) known_patterns = [ pattern for known_pattern in all_modules diff --git a/tests/unit/test_isort.py b/tests/unit/test_isort.py index 363622e00..a13595b3f 100644 --- a/tests/unit/test_isort.py +++ b/tests/unit/test_isort.py @@ -3425,6 +3425,13 @@ def test_all_imports_from_single_module() -> None: "from a import i as j\n" "from a import w, x, y, z\n" ) + test_input = ( + "import a\n" + "from a import *\n" + "from a import z, x, y\n" + "from a import b\n" + "from a import w\n" + ) test_output = isort.code( code=test_input, combine_star=True, @@ -3433,6 +3440,13 @@ def test_all_imports_from_single_module() -> None: no_inline_sort=False, ) assert test_output == "import a\nfrom a import *\n" + test_input += """ +from a import b as c +from a import b as d +from a import e as f +from a import g as h +from a import i as j +""" test_output = isort.code( code=test_input, combine_star=False, @@ -3466,6 +3480,17 @@ def test_all_imports_from_single_module() -> None: "from a import y\n" "from a import z\n" ) + test_input = ( + "import a\n" + "from a import *\n" + "from a import b\n" + "from a import b as d\n" + "from a import b as c\n" + "from a import z, x, y, w\n" + "from a import i as j\n" + "from a import g as h\n" + "from a import e as f\n" + ) test_output = isort.code( code=test_input, combine_star=False, @@ -3484,6 +3509,13 @@ def test_all_imports_from_single_module() -> None: "from a import g as h\n" "from a import e as f\n" ) + test_input = ( + "import a\n" + "from a import *\n" + "from a import z, x, y\n" + "from a import b\n" + "from a import w\n" + ) test_output = isort.code( code=test_input, combine_star=True, @@ -3519,16 +3551,22 @@ def test_all_imports_from_single_module() -> None: "import a\n" "from a import *\n" "from a import b\n" - "from a import b as c\n" - "from a import b as d\n" - "from a import e as f\n" - "from a import g as h\n" - "from a import i as j\n" "from a import w\n" "from a import x\n" "from a import y\n" "from a import z\n" ) + test_input = ( + "import a\n" + "from a import *\n" + "from a import b\n" + "from a import b as d\n" + "from a import b as c\n" + "from a import z, x, y, w\n" + "from a import i as j\n" + "from a import g as h\n" + "from a import e as f\n" + ) test_output = isort.code( code=test_input, combine_star=False, @@ -3562,6 +3600,13 @@ def test_all_imports_from_single_module() -> None: "from a import y\n" "from a import z\n" ) + test_input = ( + "import a\n" + "from a import *\n" + "from a import z, x, y\n" + "from a import b\n" + "from a import w\n" + ) test_output = isort.code( code=test_input, combine_star=True, diff --git a/tests/unit/test_ticketed_features.py b/tests/unit/test_ticketed_features.py index 878b2a03a..ded0f0666 100644 --- a/tests/unit/test_ticketed_features.py +++ b/tests/unit/test_ticketed_features.py @@ -543,3 +543,25 @@ def test_isort_enables_deduping_section_headers_issue_953(): """ ) assert isort_code("import os") == "import os\n" + + +def test_isort_doesnt_remove_as_imports_when_combine_star_issue_1380(): + """Test to ensure isort will not remove as imports along side other imports + when requested to combine star imports together. + See: https://github.com/PyCQA/isort/issues/1380 + """ + assert ( + isort.code( + """ +from a import a +from a import * +from a import b as y +from a import c +""", + combine_star=True, + ) + == """ +from a import * +from a import b as y +""" + ) From 5bf0a70d46a8f2bd6ab0c2b7707d882aeac5c072 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 25 Aug 2020 22:54:13 -0700 Subject: [PATCH 0969/1439] Account for case where both as and non as of same import are made with combine_star --- isort/output.py | 12 ++++++++++-- tests/unit/test_ticketed_features.py | 13 ++++++++----- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/isort/output.py b/isort/output.py index 445ead2a6..0951fcae2 100644 --- a/isort/output.py +++ b/isort/output.py @@ -257,6 +257,7 @@ def _with_from_imports( else: from_imports[idx : (idx + 1)] = as_imports.pop(from_import) + only_show_as_imports = False while from_imports: comments = parsed.categorized_comments["from"].pop(module, ()) above_comments = parsed.categorized_comments["above"]["from"].pop(module, None) @@ -277,6 +278,7 @@ def _with_from_imports( from_imports = [ from_import for from_import in from_imports if from_import in as_imports ] + only_show_as_imports = True elif config.force_single_line and module not in config.single_line_exclusions: import_statement = "" while from_imports: @@ -295,7 +297,10 @@ def _with_from_imports( f"{comments and ';' or config.comment_prefix} " f"{comment}" ) if from_import in as_imports: - if parsed.imports[section]["from"][module][from_import]: + if ( + parsed.imports[section]["from"][module][from_import] + and not only_show_as_imports + ): output.append( wrap.line(single_import_line, parsed.line_separator, config) ) @@ -321,7 +326,10 @@ def _with_from_imports( from_comments = parsed.categorized_comments["straight"].get( f"{module}.{from_import}" ) - if parsed.imports[section]["from"][module][from_import]: + if ( + parsed.imports[section]["from"][module][from_import] + and not only_show_as_imports + ): output.append( wrap.line( with_comments( diff --git a/tests/unit/test_ticketed_features.py b/tests/unit/test_ticketed_features.py index ded0f0666..64fa18118 100644 --- a/tests/unit/test_ticketed_features.py +++ b/tests/unit/test_ticketed_features.py @@ -550,15 +550,18 @@ def test_isort_doesnt_remove_as_imports_when_combine_star_issue_1380(): when requested to combine star imports together. See: https://github.com/PyCQA/isort/issues/1380 """ - assert ( - isort.code( - """ + test_input = """ from a import a from a import * +from a import b from a import b as y from a import c -""", - combine_star=True, +""" + assert ( + isort.code(test_input, combine_star=True,) + == isort.code(test_input, combine_star=True, force_single_line=True) + == isort.code( + test_input, combine_star=True, force_single_line=True, combine_as_imports=True, ) == """ from a import * From 4fb1a5de66ff0ee1d0f981fd43efb564da526abd Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 25 Aug 2020 22:56:30 -0700 Subject: [PATCH 0970/1439] Small refactoring to avoid redoing work per a loop --- isort/output.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/isort/output.py b/isort/output.py index 0951fcae2..afb14ea25 100644 --- a/isort/output.py +++ b/isort/output.py @@ -258,11 +258,12 @@ def _with_from_imports( from_imports[idx : (idx + 1)] = as_imports.pop(from_import) only_show_as_imports = False + comments = parsed.categorized_comments["from"].pop(module, ()) + above_comments = parsed.categorized_comments["above"]["from"].pop(module, None) while from_imports: - comments = parsed.categorized_comments["from"].pop(module, ()) - above_comments = parsed.categorized_comments["above"]["from"].pop(module, None) if above_comments: output.extend(above_comments) + above_comments = None if "*" in from_imports and config.combine_star: import_statement = wrap.line( From 65d473f4fac4ebd1ee311c70502e8b07d2e5c7f6 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 25 Aug 2020 23:39:12 -0700 Subject: [PATCH 0971/1439] Fix #1407: Ensure custom groups always take precedence --- isort/settings.py | 6 ++++-- tests/unit/test_ticketed_features.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/isort/settings.py b/isort/settings.py index 1f58a57e9..940fcb4d1 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -347,6 +347,7 @@ def __init__( "See: https://pycqa.github.io/isort/" "#custom-sections-and-ordering." ) + if key.startswith(IMPORT_HEADING_PREFIX): import_headings[key[len(IMPORT_HEADING_PREFIX) :].lower()] = str(value) @@ -498,12 +499,13 @@ def known_patterns(self): return self._known_patterns self._known_patterns = [] - for placement in reversed(self.sections): + pattern_sections = [STDLIB] + [section for section in self.sections if section != STDLIB] + for placement in reversed(pattern_sections): known_placement = KNOWN_SECTION_MAPPING.get(placement, placement).lower() config_key = f"{KNOWN_PREFIX}{known_placement}" known_modules = getattr(self, config_key, self.known_other.get(known_placement, ())) extra_modules = getattr(self, f"extra_{known_placement}", ()) - all_modules = set(extra_modules).union(known_modules) + all_modules = set(known_modules).union(extra_modules) known_patterns = [ pattern for known_pattern in all_modules diff --git a/tests/unit/test_ticketed_features.py b/tests/unit/test_ticketed_features.py index 64fa18118..86b390455 100644 --- a/tests/unit/test_ticketed_features.py +++ b/tests/unit/test_ticketed_features.py @@ -568,3 +568,22 @@ def test_isort_doesnt_remove_as_imports_when_combine_star_issue_1380(): from a import b as y """ ) + + +def test_isort_support_custom_groups_above_stdlib_that_contain_stdlib_modules_issue_1407(): + """Test to ensure it is possible to declare custom groups above standard library that include + modules from the standard library. + See: https://github.com/PyCQA/isort/issues/1407 + """ + assert isort.check_code( + """ +from __future__ import annotations +from typing import * + +from pathlib import Path +""", + known_typing=["typing"], + sections=["FUTURE", "TYPING", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"], + no_lines_before=["TYPING"], + show_diff=True, + ) From 4415428db337f4f0e346678cdac98428f3647d41 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 25 Aug 2020 23:40:59 -0700 Subject: [PATCH 0972/1439] Remove unrelated changes --- isort/settings.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/isort/settings.py b/isort/settings.py index 940fcb4d1..1e10ab60e 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -347,7 +347,6 @@ def __init__( "See: https://pycqa.github.io/isort/" "#custom-sections-and-ordering." ) - if key.startswith(IMPORT_HEADING_PREFIX): import_headings[key[len(IMPORT_HEADING_PREFIX) :].lower()] = str(value) @@ -505,7 +504,7 @@ def known_patterns(self): config_key = f"{KNOWN_PREFIX}{known_placement}" known_modules = getattr(self, config_key, self.known_other.get(known_placement, ())) extra_modules = getattr(self, f"extra_{known_placement}", ()) - all_modules = set(known_modules).union(extra_modules) + all_modules = set(extra_modules).union(known_modules) known_patterns = [ pattern for known_pattern in all_modules From 904324db7cffa2582185acce64020419c210ef71 Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Wed, 26 Aug 2020 14:20:18 +0300 Subject: [PATCH 0973/1439] Bugfix: 1412 Missing colorama leads to exception Fixes #1412. --- isort/format.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/isort/format.py b/isort/format.py index 67c8c5b1c..fbe122d5c 100644 --- a/isort/format.py +++ b/isort/format.py @@ -114,13 +114,14 @@ def diff_line(self, line: str) -> None: class ColoramaPrinter(BasicPrinter): - ADDED_LINE = colorama.Fore.GREEN - REMOVED_LINE = colorama.Fore.RED - def __init__(self, output: Optional[TextIO] = None): self.output = output or sys.stdout + # Note: this constants are instance variables instead ofs class variables + # because they refer to colorama which might not be installed. self.ERROR = self.style_text("ERROR", colorama.Fore.RED) self.SUCCESS = self.style_text("SUCCESS", colorama.Fore.GREEN) + self.ADDED_LINE = colorama.Fore.GREEN + self.REMOVED_LINE = colorama.Fore.RED @staticmethod def style_text(text: str, style: Optional[str] = None) -> str: From ce6b98159904f322f1655ed4472e25b5c901d01c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 26 Aug 2020 21:09:35 -0700 Subject: [PATCH 0974/1439] Add code snipet integration test --- tests/integration/conftest.py | 51 ++++++++ .../integration/test_setting_combinations.py | 110 ++++++++++++++++++ 2 files changed, 161 insertions(+) create mode 100644 tests/integration/conftest.py create mode 100644 tests/integration/test_setting_combinations.py diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py new file mode 100644 index 000000000..e83f690c8 --- /dev/null +++ b/tests/integration/conftest.py @@ -0,0 +1,51 @@ +import isort +from hypothesis import strategies as st +from typing import get_type_hints + + +def _as_config(kw) -> isort.Config: + if "wrap_length" in kw and "line_length" in kw: + kw["wrap_length"], kw["line_length"] = sorted([kw["wrap_length"], kw["line_length"]]) + try: + return isort.Config(**kw) + except ValueError: + kw["wrap_length"] = 0 + return isort.Config(**kw) + + +def configs(**force_strategies: st.SearchStrategy) -> st.SearchStrategy[isort.Config]: + """Generate arbitrary Config objects.""" + skip = { + "line_ending", + "sections", + "known_future_library", + "forced_separate", + "lines_after_imports", + "lines_between_sections", + "lines_between_types", + "sources", + "virtual_env", + "conda_env", + "directory", + "formatter", + "formatting_function", + } + inferred_kwargs = { + k: st.from_type(v) + for k, v in get_type_hints(isort.settings._Config).items() + if k not in skip + } + specific = { + "line_length": st.integers(0, 200), + "wrap_length": st.integers(0, 200), + "indent": st.integers(0, 20).map(lambda n: n * " "), + "default_section": st.sampled_from(sorted(isort.settings.KNOWN_SECTION_MAPPING)), + "force_grid_wrap": st.integers(0, 20), + "profile": st.sampled_from(sorted(isort.settings.profiles)), + "py_version": st.sampled_from(("auto",) + isort.settings.VALID_PY_TARGETS), + } + kwargs = {**inferred_kwargs, **specific, **force_strategies} + return st.fixed_dictionaries({}, optional=kwargs).map(_as_config) + + +st.register_type_strategy(isort.Config, configs()) diff --git a/tests/integration/test_setting_combinations.py b/tests/integration/test_setting_combinations.py new file mode 100644 index 000000000..c3fae0ee9 --- /dev/null +++ b/tests/integration/test_setting_combinations.py @@ -0,0 +1,110 @@ +import isort +import hypothesis +from hypothesis import find, settings, Verbosity +from hypothesis import strategies as st + +CODE_SNIPPET = """ +''' Taken from bottle.py + +Copyright (c) 2009-2018, Marcel Hellkamp. +License: MIT (see LICENSE for details) +''' +# Lots of stdlib and builtin differences. +if py3k: + import http.client as httplib + import _thread as thread + from urllib.parse import urljoin, SplitResult as UrlSplitResult + from urllib.parse import urlencode, quote as urlquote, unquote as urlunquote + urlunquote = functools.partial(urlunquote, encoding='latin1') + from http.cookies import SimpleCookie, Morsel, CookieError + from collections.abc import MutableMapping as DictMixin + import pickle # comment number 2 + from io import BytesIO + import configparser + + basestring = str + unicode = str + json_loads = lambda s: json_lds(touni(s)) + callable = lambda x: hasattr(x, '__call__') + imap = map + + def _raise(*a): + raise a[0](a[1]).with_traceback(a[2]) +else: # 2.x + import httplib + import thread + from urlparse import urljoin, SplitResult as UrlSplitResult + from urllib import urlencode, quote as urlquote, unquote as urlunquote + from Cookie import SimpleCookie, Morsel, CookieError + from itertools import imap + import cPickle as pickle + from StringIO import StringIO as BytesIO + import ConfigParser as configparser # commentnumberone + from collections import MutableMapping as DictMixin + unicode = unicode + json_loads = json_lds + exec(compile('def _raise(*a): raise a[0], a[1], a[2]', '', 'exec')) +""" +SHOULD_RETAIN = ["""''' Taken from bottle.py + +Copyright (c) 2009-2018, Marcel Hellkamp. +License: MIT (see LICENSE for details) +'''""", +"# Lots of stdlib and builtin differences.", +"if py3k:", +"http.client", +"_thread", +"urllib.parse", +"urlencode", +"urlunquote = functools.partial(urlunquote, encoding='latin1')", +"http.cookies", +"SimpleCookie", +"collections.abc", +"pickle", +"# comment number 2", +"io", +"configparser", +"""basestring = str + unicode = str + json_loads = lambda s: json_lds(touni(s)) + callable = lambda x: hasattr(x, '__call__') + imap = map + + def _raise(*a): + raise a[0](a[1]).with_traceback(a[2]) +else: # 2.x +""", + "httplib", + "thread", + "urlparse", + "urllib", + "Cookie", + "itertools", + "cPickle", + "StringIO", + "ConfigParser", + "commentnumberone", + "collections", + """unicode = unicode + json_loads = json_lds + exec(compile('def _raise(*a): raise a[0], a[1], a[2]', '', 'exec'))""" +] + + +@hypothesis.given( + config=st.from_type(isort.Config), + disregard_skip=st.booleans(), +) +def test_isort_is_idempotent(config: isort.Config, disregard_skip: bool) -> None: + result = isort.code(CODE_SNIPPET, config=config, disregard_skip=disregard_skip) + assert result == isort.code(result, config=config, disregard_skip=disregard_skip) + + +@hypothesis.given( + config=st.from_type(isort.Config), + disregard_skip=st.booleans(), +) +def test_isort_is_doesnt_lose_imports_or_comments(config: isort.Config, disregard_skip: bool) -> None: + result = isort.code(CODE_SNIPPET, config=config, disregard_skip=disregard_skip) + for should_be_retained in SHOULD_RETAIN: + assert should_be_retained in result From 8a5c17df1240e4f19cd34e19eaae63b701a2ea1b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 26 Aug 2020 23:36:48 -0700 Subject: [PATCH 0975/1439] Initially passing test --- tests/integration/conftest.py | 10 ++++++++++ tests/integration/test_setting_combinations.py | 18 +++++++++++++----- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index e83f690c8..4b9711bc4 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -18,9 +18,15 @@ def configs(**force_strategies: st.SearchStrategy) -> st.SearchStrategy[isort.Co skip = { "line_ending", "sections", + "known_standard_library", "known_future_library", + "known_third_party", + "known_first_party", + "known_local_folder", + "extra_standard_library", "forced_separate", "lines_after_imports", + "add_imports", "lines_between_sections", "lines_between_types", "sources", @@ -29,6 +35,10 @@ def configs(**force_strategies: st.SearchStrategy) -> st.SearchStrategy[isort.Co "directory", "formatter", "formatting_function", + "comment_prefix", + "atomic", + "skip", + "src_paths", } inferred_kwargs = { k: st.from_type(v) diff --git a/tests/integration/test_setting_combinations.py b/tests/integration/test_setting_combinations.py index c3fae0ee9..82dfc6829 100644 --- a/tests/integration/test_setting_combinations.py +++ b/tests/integration/test_setting_combinations.py @@ -61,7 +61,7 @@ def _raise(*a): "SimpleCookie", "collections.abc", "pickle", -"# comment number 2", +"comment number 2", "io", "configparser", """basestring = str @@ -96,15 +96,23 @@ def _raise(*a): disregard_skip=st.booleans(), ) def test_isort_is_idempotent(config: isort.Config, disregard_skip: bool) -> None: - result = isort.code(CODE_SNIPPET, config=config, disregard_skip=disregard_skip) - assert result == isort.code(result, config=config, disregard_skip=disregard_skip) + try: + result = isort.code(CODE_SNIPPET, config=config, disregard_skip=disregard_skip) + result = isort.code(result, config=config, disregard_skip=disregard_skip) + assert result == isort.code(result, config=config, disregard_skip=disregard_skip) + except ValueError: + pass @hypothesis.given( config=st.from_type(isort.Config), disregard_skip=st.booleans(), ) -def test_isort_is_doesnt_lose_imports_or_comments(config: isort.Config, disregard_skip: bool) -> None: +def test_isort_doesnt_lose_imports_or_comments(config: isort.Config, disregard_skip: bool) -> None: result = isort.code(CODE_SNIPPET, config=config, disregard_skip=disregard_skip) for should_be_retained in SHOULD_RETAIN: - assert should_be_retained in result + if should_be_retained not in result: + if config.ignore_comments and should_be_retained.startswith("comment"): + continue + + assert should_be_retained in result From 1cda30d71b9655911e5597ddad99bbeb27a60f86 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 26 Aug 2020 23:41:54 -0700 Subject: [PATCH 0976/1439] Formatting fixes --- tests/integration/conftest.py | 6 ++- .../integration/test_setting_combinations.py | 53 +++++++++---------- 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 4b9711bc4..d7fc0fd8a 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -1,7 +1,9 @@ -import isort -from hypothesis import strategies as st from typing import get_type_hints +from hypothesis import strategies as st + +import isort + def _as_config(kw) -> isort.Config: if "wrap_length" in kw and "line_length" in kw: diff --git a/tests/integration/test_setting_combinations.py b/tests/integration/test_setting_combinations.py index 82dfc6829..a9a3beedb 100644 --- a/tests/integration/test_setting_combinations.py +++ b/tests/integration/test_setting_combinations.py @@ -1,8 +1,8 @@ -import isort import hypothesis -from hypothesis import find, settings, Verbosity from hypothesis import strategies as st +import isort + CODE_SNIPPET = """ ''' Taken from bottle.py @@ -45,26 +45,27 @@ def _raise(*a): json_loads = json_lds exec(compile('def _raise(*a): raise a[0], a[1], a[2]', '', 'exec')) """ -SHOULD_RETAIN = ["""''' Taken from bottle.py +SHOULD_RETAIN = [ + """''' Taken from bottle.py Copyright (c) 2009-2018, Marcel Hellkamp. License: MIT (see LICENSE for details) '''""", -"# Lots of stdlib and builtin differences.", -"if py3k:", -"http.client", -"_thread", -"urllib.parse", -"urlencode", -"urlunquote = functools.partial(urlunquote, encoding='latin1')", -"http.cookies", -"SimpleCookie", -"collections.abc", -"pickle", -"comment number 2", -"io", -"configparser", -"""basestring = str + "# Lots of stdlib and builtin differences.", + "if py3k:", + "http.client", + "_thread", + "urllib.parse", + "urlencode", + "urlunquote = functools.partial(urlunquote, encoding='latin1')", + "http.cookies", + "SimpleCookie", + "collections.abc", + "pickle", + "comment number 2", + "io", + "configparser", + """basestring = str unicode = str json_loads = lambda s: json_lds(touni(s)) callable = lambda x: hasattr(x, '__call__') @@ -87,26 +88,24 @@ def _raise(*a): "collections", """unicode = unicode json_loads = json_lds - exec(compile('def _raise(*a): raise a[0], a[1], a[2]', '', 'exec'))""" + exec(compile('def _raise(*a): raise a[0], a[1], a[2]', '', 'exec'))""", ] @hypothesis.given( - config=st.from_type(isort.Config), - disregard_skip=st.booleans(), + config=st.from_type(isort.Config), disregard_skip=st.booleans(), ) def test_isort_is_idempotent(config: isort.Config, disregard_skip: bool) -> None: - try: + try: result = isort.code(CODE_SNIPPET, config=config, disregard_skip=disregard_skip) result = isort.code(result, config=config, disregard_skip=disregard_skip) assert result == isort.code(result, config=config, disregard_skip=disregard_skip) except ValueError: pass - - + + @hypothesis.given( - config=st.from_type(isort.Config), - disregard_skip=st.booleans(), + config=st.from_type(isort.Config), disregard_skip=st.booleans(), ) def test_isort_doesnt_lose_imports_or_comments(config: isort.Config, disregard_skip: bool) -> None: result = isort.code(CODE_SNIPPET, config=config, disregard_skip=disregard_skip) @@ -114,5 +113,5 @@ def test_isort_doesnt_lose_imports_or_comments(config: isort.Config, disregard_s if should_be_retained not in result: if config.ignore_comments and should_be_retained.startswith("comment"): continue - + assert should_be_retained in result From b11ae9aeccaaff6cc2c521e202eca18ed2e1e85a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 27 Aug 2020 00:27:51 -0700 Subject: [PATCH 0977/1439] Force atomic off in fuzz tests for now --- tests/integration/conftest.py | 1 + tests/integration/test_setting_combinations.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index d7fc0fd8a..5be7f62eb 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -6,6 +6,7 @@ def _as_config(kw) -> isort.Config: + kw["atomic"] = False if "wrap_length" in kw and "line_length" in kw: kw["wrap_length"], kw["line_length"] = sorted([kw["wrap_length"], kw["line_length"]]) try: diff --git a/tests/integration/test_setting_combinations.py b/tests/integration/test_setting_combinations.py index a9a3beedb..591bcc4da 100644 --- a/tests/integration/test_setting_combinations.py +++ b/tests/integration/test_setting_combinations.py @@ -4,7 +4,7 @@ import isort CODE_SNIPPET = """ -''' Taken from bottle.py +'''Taken from bottle.py Copyright (c) 2009-2018, Marcel Hellkamp. License: MIT (see LICENSE for details) @@ -46,7 +46,7 @@ def _raise(*a): exec(compile('def _raise(*a): raise a[0], a[1], a[2]', '', 'exec')) """ SHOULD_RETAIN = [ - """''' Taken from bottle.py + """'''Taken from bottle.py Copyright (c) 2009-2018, Marcel Hellkamp. License: MIT (see LICENSE for details) From 7951677631ae3df90ea591444a5efdea33d0d138 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 27 Aug 2020 00:34:19 -0700 Subject: [PATCH 0978/1439] Try not using forced strategies --- tests/integration/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 5be7f62eb..0e9a315a5 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -16,7 +16,7 @@ def _as_config(kw) -> isort.Config: return isort.Config(**kw) -def configs(**force_strategies: st.SearchStrategy) -> st.SearchStrategy[isort.Config]: +def configs() -> st.SearchStrategy[isort.Config]: """Generate arbitrary Config objects.""" skip = { "line_ending", @@ -57,7 +57,7 @@ def configs(**force_strategies: st.SearchStrategy) -> st.SearchStrategy[isort.Co "profile": st.sampled_from(sorted(isort.settings.profiles)), "py_version": st.sampled_from(("auto",) + isort.settings.VALID_PY_TARGETS), } - kwargs = {**inferred_kwargs, **specific, **force_strategies} + kwargs = {**inferred_kwargs, **specific} return st.fixed_dictionaries({}, optional=kwargs).map(_as_config) From ac523e6f6a8de83510f1297d4b0f0561a2b44082 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 27 Aug 2020 00:49:57 -0700 Subject: [PATCH 0979/1439] Move setting combination strategy into test --- tests/integration/conftest.py | 64 ------------------- .../integration/test_setting_combinations.py | 61 ++++++++++++++++++ 2 files changed, 61 insertions(+), 64 deletions(-) delete mode 100644 tests/integration/conftest.py diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py deleted file mode 100644 index 0e9a315a5..000000000 --- a/tests/integration/conftest.py +++ /dev/null @@ -1,64 +0,0 @@ -from typing import get_type_hints - -from hypothesis import strategies as st - -import isort - - -def _as_config(kw) -> isort.Config: - kw["atomic"] = False - if "wrap_length" in kw and "line_length" in kw: - kw["wrap_length"], kw["line_length"] = sorted([kw["wrap_length"], kw["line_length"]]) - try: - return isort.Config(**kw) - except ValueError: - kw["wrap_length"] = 0 - return isort.Config(**kw) - - -def configs() -> st.SearchStrategy[isort.Config]: - """Generate arbitrary Config objects.""" - skip = { - "line_ending", - "sections", - "known_standard_library", - "known_future_library", - "known_third_party", - "known_first_party", - "known_local_folder", - "extra_standard_library", - "forced_separate", - "lines_after_imports", - "add_imports", - "lines_between_sections", - "lines_between_types", - "sources", - "virtual_env", - "conda_env", - "directory", - "formatter", - "formatting_function", - "comment_prefix", - "atomic", - "skip", - "src_paths", - } - inferred_kwargs = { - k: st.from_type(v) - for k, v in get_type_hints(isort.settings._Config).items() - if k not in skip - } - specific = { - "line_length": st.integers(0, 200), - "wrap_length": st.integers(0, 200), - "indent": st.integers(0, 20).map(lambda n: n * " "), - "default_section": st.sampled_from(sorted(isort.settings.KNOWN_SECTION_MAPPING)), - "force_grid_wrap": st.integers(0, 20), - "profile": st.sampled_from(sorted(isort.settings.profiles)), - "py_version": st.sampled_from(("auto",) + isort.settings.VALID_PY_TARGETS), - } - kwargs = {**inferred_kwargs, **specific} - return st.fixed_dictionaries({}, optional=kwargs).map(_as_config) - - -st.register_type_strategy(isort.Config, configs()) diff --git a/tests/integration/test_setting_combinations.py b/tests/integration/test_setting_combinations.py index 591bcc4da..c29c77425 100644 --- a/tests/integration/test_setting_combinations.py +++ b/tests/integration/test_setting_combinations.py @@ -1,8 +1,69 @@ +from typing import get_type_hints + import hypothesis from hypothesis import strategies as st import isort + +def _as_config(kw) -> isort.Config: + kw["atomic"] = False + if "wrap_length" in kw and "line_length" in kw: + kw["wrap_length"], kw["line_length"] = sorted([kw["wrap_length"], kw["line_length"]]) + try: + return isort.Config(**kw) + except ValueError: + kw["wrap_length"] = 0 + return isort.Config(**kw) + + +def configs() -> st.SearchStrategy[isort.Config]: + """Generate arbitrary Config objects.""" + skip = { + "line_ending", + "sections", + "known_standard_library", + "known_future_library", + "known_third_party", + "known_first_party", + "known_local_folder", + "extra_standard_library", + "forced_separate", + "lines_after_imports", + "add_imports", + "lines_between_sections", + "lines_between_types", + "sources", + "virtual_env", + "conda_env", + "directory", + "formatter", + "formatting_function", + "comment_prefix", + "atomic", + "skip", + "src_paths", + } + inferred_kwargs = { + k: st.from_type(v) + for k, v in get_type_hints(isort.settings._Config).items() + if k not in skip + } + specific = { + "line_length": st.integers(0, 200), + "wrap_length": st.integers(0, 200), + "indent": st.integers(0, 20).map(lambda n: n * " "), + "default_section": st.sampled_from(sorted(isort.settings.KNOWN_SECTION_MAPPING)), + "force_grid_wrap": st.integers(0, 20), + "profile": st.sampled_from(sorted(isort.settings.profiles)), + "py_version": st.sampled_from(("auto",) + isort.settings.VALID_PY_TARGETS), + } + kwargs = {**inferred_kwargs, **specific} + return st.fixed_dictionaries({}, optional=kwargs).map(_as_config) + + +st.register_type_strategy(isort.Config, configs()) + CODE_SNIPPET = """ '''Taken from bottle.py From 472d4643fe7fde1b98f56e6c21f9bc6afb94846c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 27 Aug 2020 21:06:38 -0700 Subject: [PATCH 0980/1439] Add black profile test --- tests/unit/profiles/test_black.py | 363 ++++++++++++++++++++++++++++++ 1 file changed, 363 insertions(+) create mode 100644 tests/unit/profiles/test_black.py diff --git a/tests/unit/profiles/test_black.py b/tests/unit/profiles/test_black.py new file mode 100644 index 000000000..4b909731c --- /dev/null +++ b/tests/unit/profiles/test_black.py @@ -0,0 +1,363 @@ +import black +import isort + + +def black_format(code: str, is_pyi: bool=False, line_length: int=88) -> str: + """Formats the provided code snippet using black""" + try: + return black.format_file_contents( + code, + fast=True, + mode=black.FileMode( + is_pyi=is_pyi, line_length=line_length, + ), + ) + except black.NothingChanged: + return code + + +def black_test(code: str, expected_output: str=""): + """Tests that the given code: + - Behaves the same when formatted multiple times with isort. + - Agrees with black formatting. + - Matches the desired output or itself if none is provided. + """ + expected_output = expected_output or code + + # output should stay consistent over multiple runs + output = isort.code(code, profile="black") + assert output == isort.code(code, profile="black") + + # output should agree with black + black_output = black_format(output) + assert output == black_output + + # output should match expected output + assert output == expected_output + + +def test_black_snippet_one(): + """Test consistent code formatting between isort and black for code snippet from black repository. + See: https://github.com/psf/black/blob/master/tests/test_black.py + """ + black_test("""#!/usr/bin/env python3 +import asyncio +import logging +from concurrent.futures import ThreadPoolExecutor +from contextlib import contextmanager +from dataclasses import replace +from functools import partial +import inspect +from io import BytesIO, TextIOWrapper +import os +from pathlib import Path +from platform import system +import regex as re +import sys +from tempfile import TemporaryDirectory +import types +from typing import ( + Any, + BinaryIO, + Callable, + Dict, + Generator, + List, + Tuple, + Iterator, + TypeVar, +) +import unittest +from unittest.mock import patch, MagicMock + +import click +from click import unstyle +from click.testing import CliRunner + +import black +from black import Feature, TargetVersion + +try: + import blackd + from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop + from aiohttp import web +except ImportError: + has_blackd_deps = False +else: + has_blackd_deps = True + +from pathspec import PathSpec + +# Import other test classes +from .test_primer import PrimerCLITests # noqa: F401 + + +DEFAULT_MODE = black.FileMode(experimental_string_processing=True) +""", +"""#!/usr/bin/env python3 +import asyncio +import inspect +import logging +import os +import sys +import types +import unittest +from concurrent.futures import ThreadPoolExecutor +from contextlib import contextmanager +from dataclasses import replace +from functools import partial +from io import BytesIO, TextIOWrapper +from pathlib import Path +from platform import system +from tempfile import TemporaryDirectory +from typing import ( + Any, + BinaryIO, + Callable, + Dict, + Generator, + Iterator, + List, + Tuple, + TypeVar, +) +from unittest.mock import MagicMock, patch + +import black +import click +import regex as re +from black import Feature, TargetVersion +from click import unstyle +from click.testing import CliRunner + +try: + import blackd + from aiohttp import web + from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop +except ImportError: + has_blackd_deps = False +else: + has_blackd_deps = True + +from pathspec import PathSpec + +# Import other test classes +from .test_primer import PrimerCLITests # noqa: F401 + +DEFAULT_MODE = black.FileMode(experimental_string_processing=True) +""" +) + + +def test_black_snippet_two(): + """Test consistent code formatting between isort and black for code snippet from black repository. + See: https://github.com/psf/black/blob/master/tests/test_primer.py + """ + black_test('''#!/usr/bin/env python3 + +import asyncio +import sys +import unittest +from contextlib import contextmanager +from copy import deepcopy +from io import StringIO +from os import getpid +from pathlib import Path +from platform import system +from subprocess import CalledProcessError +from tempfile import TemporaryDirectory, gettempdir +from typing import Any, Callable, Generator, Iterator, Tuple +from unittest.mock import Mock, patch + +from click.testing import CliRunner + +from black_primer import cli, lib + + +EXPECTED_ANALYSIS_OUTPUT = """\ +-- primer results 📊 -- +68 / 69 succeeded (98.55%) ✅ +1 / 69 FAILED (1.45%) 💩 + - 0 projects disabled by config + - 0 projects skipped due to Python version + - 0 skipped due to long checkout +Failed projects: +## black: + - Returned 69 + - stdout: +Black didn't work +""" +''', +'''#!/usr/bin/env python3 + +import asyncio +import sys +import unittest +from contextlib import contextmanager +from copy import deepcopy +from io import StringIO +from os import getpid +from pathlib import Path +from platform import system +from subprocess import CalledProcessError +from tempfile import TemporaryDirectory, gettempdir +from typing import Any, Callable, Generator, Iterator, Tuple +from unittest.mock import Mock, patch + +from black_primer import cli, lib +from click.testing import CliRunner + +EXPECTED_ANALYSIS_OUTPUT = """-- primer results 📊 -- +68 / 69 succeeded (98.55%) ✅ +1 / 69 FAILED (1.45%) 💩 + - 0 projects disabled by config + - 0 projects skipped due to Python version + - 0 skipped due to long checkout +Failed projects: +## black: + - Returned 69 + - stdout: +Black didn't work +""" +''' +) + +def test_black_snippet_three(): + """Test consistent code formatting between isort and black for code snippet from black repository. + See: https://github.com/psf/black/blob/master/src/black/__init__.py + """ + black_test('''import ast +import asyncio +from abc import ABC, abstractmethod +from collections import defaultdict +from concurrent.futures import Executor, ThreadPoolExecutor, ProcessPoolExecutor +from contextlib import contextmanager +from datetime import datetime +from enum import Enum +from functools import lru_cache, partial, wraps +import io +import itertools +import logging +from multiprocessing import Manager, freeze_support +import os +from pathlib import Path +import pickle +import regex as re +import signal +import sys +import tempfile +import tokenize +import traceback +from typing import ( + Any, + Callable, + Collection, + Dict, + Generator, + Generic, + Iterable, + Iterator, + List, + Optional, + Pattern, + Sequence, + Set, + Sized, + Tuple, + Type, + TypeVar, + Union, + cast, + TYPE_CHECKING, +) +from typing_extensions import Final +from mypy_extensions import mypyc_attr + +from appdirs import user_cache_dir +from dataclasses import dataclass, field, replace +import click +import toml +from typed_ast import ast3, ast27 +from pathspec import PathSpec + +# lib2to3 fork +from blib2to3.pytree import Node, Leaf, type_repr +from blib2to3 import pygram, pytree +from blib2to3.pgen2 import driver, token +from blib2to3.pgen2.grammar import Grammar +from blib2to3.pgen2.parse import ParseError + +from _black_version import version as __version__ + +if TYPE_CHECKING: + import colorama # noqa: F401 + +DEFAULT_LINE_LENGTH = 88 +''', +'''import ast +import asyncio +import io +import itertools +import logging +import os +import pickle +import signal +import sys +import tempfile +import tokenize +import traceback +from abc import ABC, abstractmethod +from collections import defaultdict +from concurrent.futures import Executor, ProcessPoolExecutor, ThreadPoolExecutor +from contextlib import contextmanager +from dataclasses import dataclass, field, replace +from datetime import datetime +from enum import Enum +from functools import lru_cache, partial, wraps +from multiprocessing import Manager, freeze_support +from pathlib import Path +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Collection, + Dict, + Generator, + Generic, + Iterable, + Iterator, + List, + Optional, + Pattern, + Sequence, + Set, + Sized, + Tuple, + Type, + TypeVar, + Union, + cast, +) + +import click +import regex as re +import toml +from _black_version import version as __version__ +from appdirs import user_cache_dir +from blib2to3 import pygram, pytree +from blib2to3.pgen2 import driver, token +from blib2to3.pgen2.grammar import Grammar +from blib2to3.pgen2.parse import ParseError + +# lib2to3 fork +from blib2to3.pytree import Leaf, Node, type_repr +from mypy_extensions import mypyc_attr +from pathspec import PathSpec +from typed_ast import ast3, ast27 +from typing_extensions import Final + +if TYPE_CHECKING: + import colorama # noqa: F401 + +DEFAULT_LINE_LENGTH = 88 +''') From 968aed0bd91f8de507ffe3d4ed291cdaf73d0225 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 27 Aug 2020 23:06:13 -0700 Subject: [PATCH 0981/1439] Update to latest version of dependencies; reformat --- .../example_isort_formatting_plugin.py | 3 +- .../pyproject.toml | 4 +- isort/_future/_dataclasses.py | 7 +- isort/output.py | 6 +- isort/wrap_modes.py | 2 +- poetry.lock | 117 ++++++++++-------- pyproject.toml | 4 +- .../integration/test_setting_combinations.py | 6 +- tests/unit/profiles/test_black.py | 51 ++++---- tests/unit/test_isort.py | 15 ++- tests/unit/test_ticketed_features.py | 10 +- 11 files changed, 129 insertions(+), 96 deletions(-) diff --git a/example_isort_formatting_plugin/example_isort_formatting_plugin.py b/example_isort_formatting_plugin/example_isort_formatting_plugin.py index ff65412f2..f63b817e8 100644 --- a/example_isort_formatting_plugin/example_isort_formatting_plugin.py +++ b/example_isort_formatting_plugin/example_isort_formatting_plugin.py @@ -15,7 +15,8 @@ def black_format_import_section( contents, fast=True, mode=black.FileMode( - is_pyi=extension.lower() == "pyi", line_length=config.line_length, + is_pyi=extension.lower() == "pyi", + line_length=config.line_length, ), ) except black.NothingChanged: diff --git a/example_isort_formatting_plugin/pyproject.toml b/example_isort_formatting_plugin/pyproject.toml index 9e060ec1f..31e3f9282 100644 --- a/example_isort_formatting_plugin/pyproject.toml +++ b/example_isort_formatting_plugin/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "example_isort_formatting_plugin" -version = "0.0.1" +version = "0.0.2" description = "An example plugin that modifies isort formatting using black." authors = ["Timothy Crosley "] license = "MIT" @@ -11,7 +11,7 @@ example = "example_isort_formatting_plugin:black_format_import_section" [tool.poetry.dependencies] python = "^3.6" isort = "^5.1.4" -black = "^19.10b0" +black = "^20.08b1" [tool.poetry.dev-dependencies] diff --git a/isort/_future/_dataclasses.py b/isort/_future/_dataclasses.py index a7b113fe2..87acb666e 100644 --- a/isort/_future/_dataclasses.py +++ b/isort/_future/_dataclasses.py @@ -1137,7 +1137,10 @@ class C(Base): name = item tp = "typing.Any" elif len(item) == 2: - name, tp, = item + ( + name, + tp, + ) = item elif len(item) == 3: name, tp, spec = item namespace[name] = spec @@ -1173,7 +1176,7 @@ class C: c = C(1, 2) c1 = replace(c, x=3) assert c1.x == 3 and c1.y == 2 - """ + """ # We're going to mutate 'changes', but that's okay because it's a # new dict, even if called with 'replace(obj, **my_changes)'. diff --git a/isort/output.py b/isort/output.py index afb14ea25..7d793ac1e 100644 --- a/isort/output.py +++ b/isort/output.py @@ -229,7 +229,11 @@ def _with_from_imports( from_imports = sorting.naturally( from_imports, key=lambda key: sorting.module_key( - key, config, True, ignore_case, section_name=section, + key, + config, + True, + ignore_case, + section_name=section, ), ) if remove_imports: diff --git a/isort/wrap_modes.py b/isort/wrap_modes.py index 92a63c3f7..8e10a9474 100644 --- a/isort/wrap_modes.py +++ b/isort/wrap_modes.py @@ -34,7 +34,7 @@ def _wrap_mode_interface( def _wrap_mode(function): """Registers an individual wrap mode. Function name and order are significant and used for - creating enum. + creating enum. """ _wrap_modes[function.__name__.upper()] = function function.__signature__ = signature(_wrap_mode_interface) diff --git a/poetry.lock b/poetry.lock index e7472fbd6..ce01d351e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -21,10 +21,10 @@ description = "Better dates & times for Python" name = "arrow" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "0.15.8" +version = "0.16.0" [package.dependencies] -python-dateutil = "*" +python-dateutil = ">=2.7.0" [[package]] category = "dev" @@ -41,13 +41,12 @@ description = "Classes Without Boilerplate" name = "attrs" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "19.3.0" +version = "20.1.0" [package.extras] -azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"] -dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"] -docs = ["sphinx", "zope.interface"] -tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] +dev = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "sphinx-rtd-theme", "pre-commit"] +docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] [[package]] category = "dev" @@ -89,18 +88,24 @@ description = "The uncompromising code formatter." name = "black" optional = false python-versions = ">=3.6" -version = "19.10b0" +version = "20.8b1" [package.dependencies] appdirs = "*" -attrs = ">=18.1.0" -click = ">=6.5" +click = ">=7.1.2" +mypy-extensions = ">=0.4.3" pathspec = ">=0.6,<1" -regex = "*" -toml = ">=0.9.4" +regex = ">=2020.1.8" +toml = ">=0.10.1" typed-ast = ">=1.4.0" +typing-extensions = ">=3.7.4" + +[package.dependencies.dataclasses] +python = "<3.7" +version = ">=0.6" [package.extras] +colorama = ["colorama (>=0.4.3)"] d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] [[package]] @@ -202,7 +207,7 @@ description = "Allows you to maintain all the necessary cruft for packaging and name = "cruft" optional = false python-versions = ">=3.6,<4.0" -version = "2.2.0" +version = "2.3.0" [package.dependencies] click = ">=7.1.2,<8.0.0" @@ -269,10 +274,10 @@ description = "An example plugin that modifies isort formatting using black." name = "example-isort-formatting-plugin" optional = false python-versions = ">=3.6,<4.0" -version = "0.0.1" +version = "0.0.2" [package.dependencies] -black = ">=19.10b0,<20.0" +black = ">=20.08b1,<21.0" isort = ">=5.1.4,<6.0.0" [[package]] @@ -417,7 +422,7 @@ description = "Chromium HSTS Preload list as a Python package and updated daily" name = "hstspreload" optional = false python-versions = ">=3.6" -version = "2020.8.12" +version = "2020.8.25" [[package]] category = "dev" @@ -475,17 +480,19 @@ description = "A library for property-based testing" name = "hypothesis" optional = false python-versions = ">=3.5.2" -version = "5.24.4" +version = "5.29.3" [package.dependencies] attrs = ">=19.2.0" sortedcontainers = ">=2.1.0,<3.0.0" [package.extras] -all = ["django (>=2.2)", "dpcontracts (>=0.4)", "lark-parser (>=0.6.5)", "numpy (>=1.9.0)", "pandas (>=0.19)", "pytest (>=4.3)", "python-dateutil (>=1.4)", "pytz (>=2014.1)"] +all = ["black (>=19.10b0)", "click (>=7.0)", "django (>=2.2)", "dpcontracts (>=0.4)", "lark-parser (>=0.6.5)", "numpy (>=1.9.0)", "pandas (>=0.19)", "pytest (>=4.3)", "python-dateutil (>=1.4)", "pytz (>=2014.1)"] +cli = ["click (>=7.0)", "black (>=19.10b0)"] dateutil = ["python-dateutil (>=1.4)"] django = ["pytz (>=2014.1)", "django (>=2.2)"] dpcontracts = ["dpcontracts (>=0.4)"] +ghostwriter = ["black (>=19.10b0)"] lark = ["lark-parser (>=0.6.5)"] numpy = ["numpy (>=1.9.0)"] pandas = ["pandas (>=0.19)"] @@ -513,7 +520,7 @@ description = "Hypothesis strategies for generating Python programs, something l name = "hypothesmith" optional = false python-versions = ">=3.6" -version = "0.1.3" +version = "0.1.4" [package.dependencies] hypothesis = ">=5.23.7" @@ -660,7 +667,7 @@ description = "A concrete syntax tree with AST-like properties for Python 3.5, 3 name = "libcst" optional = false python-versions = ">=3.6" -version = "0.3.9" +version = "0.3.10" [package.dependencies] pyyaml = ">=5.2" @@ -680,7 +687,7 @@ description = "Python LiveReload is an awesome tool for web developers" name = "livereload" optional = false python-versions = "*" -version = "2.6.2" +version = "2.6.3" [package.dependencies] six = "*" @@ -782,7 +789,7 @@ description = "A Material Design theme for MkDocs" name = "mkdocs-material" optional = false python-versions = "*" -version = "5.5.6" +version = "5.5.9" [package.dependencies] Pygments = ">=2.4" @@ -1153,7 +1160,7 @@ description = "Python docstring style checker" name = "pydocstyle" optional = false python-versions = ">=3.5" -version = "5.0.2" +version = "5.1.0" [package.dependencies] snowballstemmer = "*" @@ -1525,7 +1532,7 @@ description = "Typer, build great CLIs. Easy to code. Based on Python type hints name = "typer" optional = false python-versions = ">=3.6" -version = "0.3.1" +version = "0.3.2" [package.dependencies] click = ">=7.1.1,<7.2.0" @@ -1534,7 +1541,7 @@ click = ">=7.1.1,<7.2.0" all = ["colorama (>=0.4.3,<0.5.0)", "shellingham (>=1.3.0,<2.0.0)"] dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)"] doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=5.4.0,<6.0.0)", "markdown-include (>=0.5.1,<0.6.0)"] -test = ["shellingham (>=1.3.0,<2.0.0)", "pytest (>=4.4.0,<5.4.0)", "pytest-cov (>=2.10.0,<3.0.0)", "coverage (>=5.2,<6.0)", "pytest-xdist (>=1.32.0,<2.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "mypy (0.782)", "black (>=19.10b0,<20.0b0)", "isort (>=5.0.6,<6.0.0)"] +test = ["pytest-xdist (>=1.32.0,<2.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "mypy (0.782)", "black (>=19.10b0,<20.0b0)", "isort (>=5.0.6,<6.0.0)", "shellingham (>=1.3.0,<2.0.0)", "pytest (>=4.4.0,<5.4.0)", "pytest-cov (>=2.10.0,<3.0.0)", "coverage (>=5.2,<6.0)"] [[package]] category = "dev" @@ -1542,7 +1549,7 @@ description = "Backported and Experimental Type Hints for Python 3.5+" name = "typing-extensions" optional = false python-versions = "*" -version = "3.7.4.2" +version = "3.7.4.3" [[package]] category = "dev" @@ -1653,7 +1660,7 @@ pipfile_deprecated_finder = ["pipreqs", "tomlkit", "requirementslib"] requirements_deprecated_finder = ["pipreqs", "pip-api"] [metadata] -content-hash = "5463d8238a63216a861fd6d75b1a86842f2a1ab3574a5ecb8dbe8d9183b6a874" +content-hash = "c366831acb4de815d36de7d6072c341ae3a1a1cca409490b5c81fe1159977597" python-versions = "^3.6" [metadata.files] @@ -1666,16 +1673,16 @@ appnope = [ {file = "appnope-0.1.0.tar.gz", hash = "sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71"}, ] arrow = [ - {file = "arrow-0.15.8-py2.py3-none-any.whl", hash = "sha256:271b8e05174d48e50324ed0dc5d74796c839c7e579a4f21cf1a7394665f9e94f"}, - {file = "arrow-0.15.8.tar.gz", hash = "sha256:edc31dc051db12c95da9bac0271cd1027b8e36912daf6d4580af53b23e62721a"}, + {file = "arrow-0.16.0-py2.py3-none-any.whl", hash = "sha256:98184d8dd3e5d30b96c2df4596526f7de679ccb467f358b82b0f686436f3a6b8"}, + {file = "arrow-0.16.0.tar.gz", hash = "sha256:92aac856ea5175c804f7ccb96aca4d714d936f1c867ba59d747a8096ec30e90a"}, ] atomicwrites = [ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, ] attrs = [ - {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, - {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, + {file = "attrs-20.1.0-py2.py3-none-any.whl", hash = "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff"}, + {file = "attrs-20.1.0.tar.gz", hash = "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a"}, ] backcall = [ {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, @@ -1690,8 +1697,8 @@ binaryornot = [ {file = "binaryornot-0.4.4.tar.gz", hash = "sha256:359501dfc9d40632edc9fac890e19542db1a287bbcfa58175b66658392018061"}, ] black = [ - {file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"}, - {file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"}, + {file = "black-20.8b1-py3-none-any.whl", hash = "sha256:70b62ef1527c950db59062cda342ea224d772abdf6adc58b86a45421bab20a6b"}, + {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, ] cached-property = [ {file = "cached-property-1.5.1.tar.gz", hash = "sha256:9217a59f14a5682da7c4b8829deadbfc194ac22e9908ccf7c8820234e80a1504"}, @@ -1760,8 +1767,8 @@ coverage = [ {file = "coverage-5.2.1.tar.gz", hash = "sha256:a34cb28e0747ea15e82d13e14de606747e9e484fb28d63c999483f5d5188e89b"}, ] cruft = [ - {file = "cruft-2.2.0-py3-none-any.whl", hash = "sha256:da3dc9ee84dea1a4ea161b5d0fa86b11ecb77480eb22b4d776572f95a96ae9fc"}, - {file = "cruft-2.2.0.tar.gz", hash = "sha256:9365ae8547bf2297c9e88dec3b28a16bd4491c4931f417b8c3c1fd8140b1e0fd"}, + {file = "cruft-2.3.0-py3-none-any.whl", hash = "sha256:ca973c1ca9e4add9893483dbce02cd8930e105f8940afe0d087a14b70c6068de"}, + {file = "cruft-2.3.0.tar.gz", hash = "sha256:7c0f7682765e76fcf31adf877ea6f74372a0ab9554d8f8d6766e8e0413730e52"}, ] dataclasses = [ {file = "dataclasses-0.6-py3-none-any.whl", hash = "sha256:454a69d788c7fda44efd71e259be79577822f5e3f53f029a22d08004e951dc9f"}, @@ -1783,8 +1790,8 @@ dparse = [ {file = "dparse-0.5.1.tar.gz", hash = "sha256:a1b5f169102e1c894f9a7d5ccf6f9402a836a5d24be80a986c7ce9eaed78f367"}, ] example-isort-formatting-plugin = [ - {file = "example_isort_formatting_plugin-0.0.1-py3-none-any.whl", hash = "sha256:3c4bd66eb457480daa1320e2e1ef4160e639f2629315cfc54830b2613aa823bc"}, - {file = "example_isort_formatting_plugin-0.0.1.tar.gz", hash = "sha256:2666045920accaa0c3f7e60e85369c408658faef50c6c8971519562a14b7e7d8"}, + {file = "example_isort_formatting_plugin-0.0.2-py3-none-any.whl", hash = "sha256:ce428ab5deb4719e4bec56eae63978ff2d9c20dc2c2aa7cc39ece61044153db7"}, + {file = "example_isort_formatting_plugin-0.0.2.tar.gz", hash = "sha256:8cb6401c9efe2f97ba3e776439cb647ee964dc7880bd9790b0324be2c7a55907"}, ] example-shared-isort-profile = [ {file = "example_shared_isort_profile-0.0.1-py3-none-any.whl", hash = "sha256:3fa3e2d093e68285fc7893704b727791ed3e0969d07bdd2733e366303d1a2582"}, @@ -1850,8 +1857,8 @@ hpack = [ {file = "hpack-3.0.0.tar.gz", hash = "sha256:8eec9c1f4bfae3408a3f30500261f7e6a65912dc138526ea054f9ad98892e9d2"}, ] hstspreload = [ - {file = "hstspreload-2020.8.12-py3-none-any.whl", hash = "sha256:64f4441066d5544873faccf2e0b5757c6670217d34dc31d362ca2977f44604ff"}, - {file = "hstspreload-2020.8.12.tar.gz", hash = "sha256:3f5c324b1eb9d924e32ffeb5fe265b879806b6e346b765f57566410344f4b41e"}, + {file = "hstspreload-2020.8.25-py3-none-any.whl", hash = "sha256:c96401eca4669340b423abd711d2d5d03ddf0685461f95e9cfe500d5e9acf3d2"}, + {file = "hstspreload-2020.8.25.tar.gz", hash = "sha256:3129613419c13ea62411ec7375d79840e28004cbb6a585909ddcbeee401bea14"}, ] httpcore = [ {file = "httpcore-0.9.1-py3-none-any.whl", hash = "sha256:9850fe97a166a794d7e920590d5ec49a05488884c9fc8b5dba8561effab0c2a0"}, @@ -1870,16 +1877,16 @@ hyperframe = [ {file = "hyperframe-5.2.0.tar.gz", hash = "sha256:a9f5c17f2cc3c719b917c4f33ed1c61bd1f8dfac4b1bd23b7c80b3400971b41f"}, ] hypothesis = [ - {file = "hypothesis-5.24.4-py3-none-any.whl", hash = "sha256:4d86b1d7bbec9caffc49dbd0037fa549c456d08aa99e468dbce5871fdbf2167b"}, - {file = "hypothesis-5.24.4.tar.gz", hash = "sha256:c3ac78ae0cebe7098bc00d8b3e16b65640c97593cceb64c9eb2331ac282fa607"}, + {file = "hypothesis-5.29.3-py3-none-any.whl", hash = "sha256:07b865184494a64cf2e18090ecfb876c97d303973c2f97139a07be361b0c3a28"}, + {file = "hypothesis-5.29.3.tar.gz", hash = "sha256:e6cf92a94a5108d326e45df5a2b256dc0d57f9663d13efdebcadcfbad9accc31"}, ] hypothesis-auto = [ {file = "hypothesis-auto-1.1.4.tar.gz", hash = "sha256:5e2c2fb09dc09842512d80630bb792359a1d33d2c0473ad47ee23da0be9e32b1"}, {file = "hypothesis_auto-1.1.4-py3-none-any.whl", hash = "sha256:fea8560c4522c0fd490ed8cc17e420b95dabebb11b9b334c59bf2d768839015f"}, ] hypothesmith = [ - {file = "hypothesmith-0.1.3-py3-none-any.whl", hash = "sha256:aceb0feae6029eeaa4502cd763debec313b1aec43db8805958e5a81036c3e483"}, - {file = "hypothesmith-0.1.3.tar.gz", hash = "sha256:4cf1e2ce43407ad1c9c2ab5a940760db3ea8c3c29134435bc0600f33a4a32de4"}, + {file = "hypothesmith-0.1.4-py3-none-any.whl", hash = "sha256:bc45f45808078d2bbe6c3806af3b3604bde35624964fcc6b849cecadf254d3a9"}, + {file = "hypothesmith-0.1.4.tar.gz", hash = "sha256:5628fb1a06233c70751105635bc3cee789c82358041b4518c2cab5300e73cd65"}, ] idna = [ {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, @@ -1931,11 +1938,11 @@ lark-parser = [ {file = "lark-parser-0.9.0.tar.gz", hash = "sha256:9e7589365d6b6de1cca40b0eaec31104a3fb96a37a11a9dfd5098e95b50aa6cd"}, ] libcst = [ - {file = "libcst-0.3.9-py3-none-any.whl", hash = "sha256:ca1744d9344f51c2c9226d0472a5a3096f8b39e4fe38441ebc2ba26babd00688"}, - {file = "libcst-0.3.9.tar.gz", hash = "sha256:b5185c84f0e4a38409aac59f53a71741bec8c1b1159c874996b3266daafe63e5"}, + {file = "libcst-0.3.10-py3-none-any.whl", hash = "sha256:e9395d952a490e6fc160f2bea8df139bdf1fdcb3fe4c01b88893da279eff00de"}, + {file = "libcst-0.3.10.tar.gz", hash = "sha256:b0dccbfc1cff7bfa3214980e1d2d90b4e00b2fed002d4b276a8a411217738df3"}, ] livereload = [ - {file = "livereload-2.6.2.tar.gz", hash = "sha256:d1eddcb5c5eb8d2ca1fa1f750e580da624c0f7fcb734aa5780dc81b7dcbd89be"}, + {file = "livereload-2.6.3.tar.gz", hash = "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869"}, ] lunr = [ {file = "lunr-0.5.8-py2.py3-none-any.whl", hash = "sha256:aab3f489c4d4fab4c1294a257a30fec397db56f0a50273218ccc3efdbf01d6ca"}, @@ -1993,8 +2000,8 @@ mkdocs = [ {file = "mkdocs-1.1.2.tar.gz", hash = "sha256:f0b61e5402b99d7789efa032c7a74c90a20220a9c81749da06dbfbcbd52ffb39"}, ] mkdocs-material = [ - {file = "mkdocs-material-5.5.6.tar.gz", hash = "sha256:08af704cdfaf2a07fd5f135831df9106c589bfd422f9ef026929981433e80b9d"}, - {file = "mkdocs_material-5.5.6-py2.py3-none-any.whl", hash = "sha256:29f3637d5fb758d076344b026a67b8e316743d0c2da84b9303383f6cbeabfd5f"}, + {file = "mkdocs-material-5.5.9.tar.gz", hash = "sha256:37d60947993b939318945c170c7b3a153646976badf57648fd70befc3b54c830"}, + {file = "mkdocs_material-5.5.9-py2.py3-none-any.whl", hash = "sha256:c8cb3c8c44bf10ed7ac1eb568d93a4346efe03fee2994b6a80e96559421cec49"}, ] mkdocs-material-extensions = [ {file = "mkdocs-material-extensions-1.0.tar.gz", hash = "sha256:17d7491e189af75700310b7ec33c6c48a22060b8b445001deca040cb60471cde"}, @@ -2162,8 +2169,8 @@ pydantic = [ {file = "pydantic-1.6.1.tar.gz", hash = "sha256:54122a8ed6b75fe1dd80797f8251ad2063ea348a03b77218d73ea9fe19bd4e73"}, ] pydocstyle = [ - {file = "pydocstyle-5.0.2-py3-none-any.whl", hash = "sha256:da7831660b7355307b32778c4a0dbfb137d89254ef31a2b2978f50fc0b4d7586"}, - {file = "pydocstyle-5.0.2.tar.gz", hash = "sha256:f4f5d210610c2d153fae39093d44224c17429e2ad7da12a8b419aba5c2f614b5"}, + {file = "pydocstyle-5.1.0-py3-none-any.whl", hash = "sha256:08374b9d4d2b7164bae50b71bb24eb0d74a56b309029d5d502264092fa7db0c3"}, + {file = "pydocstyle-5.1.0.tar.gz", hash = "sha256:4ca3c7736d36f92bb215dd74ef84ac3d6c146edd795c7afc5154c10f1eb1f65a"}, ] pyflakes = [ {file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"}, @@ -2339,13 +2346,13 @@ typed-ast = [ {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, ] typer = [ - {file = "typer-0.3.1-py3-none-any.whl", hash = "sha256:778a9695e68eb26a0a0321ca9d3f1a8809783f6f083549b84c67bc2385bf014e"}, - {file = "typer-0.3.1.tar.gz", hash = "sha256:85b1e5f6369750b4220ad548ea30b881a2c502504e5a0d849db9bdf6b487bdbf"}, + {file = "typer-0.3.2-py3-none-any.whl", hash = "sha256:ba58b920ce851b12a2d790143009fa00ac1d05b3ff3257061ff69dbdfc3d161b"}, + {file = "typer-0.3.2.tar.gz", hash = "sha256:5455d750122cff96745b0dec87368f56d023725a7ebc9d2e54dd23dc86816303"}, ] typing-extensions = [ - {file = "typing_extensions-3.7.4.2-py2-none-any.whl", hash = "sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392"}, - {file = "typing_extensions-3.7.4.2-py3-none-any.whl", hash = "sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5"}, - {file = "typing_extensions-3.7.4.2.tar.gz", hash = "sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae"}, + {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, + {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, + {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, ] typing-inspect = [ {file = "typing_inspect-0.6.0-py2-none-any.whl", hash = "sha256:de08f50a22955ddec353876df7b2545994d6df08a2f45d54ac8c05e530372ca0"}, diff --git a/pyproject.toml b/pyproject.toml index 1e3e1e81b..5f648f408 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,7 +54,7 @@ vulture = "^1.0" bandit = "^1.6" safety = "^1.8" flake8-bugbear = "^19.8" -black = {version = "^19.10b0", allow-prereleases = true} +black = {version = "^20.08b1", allow-prereleases = true} mypy = "^0.761.0" ipython = "^7.7" pytest = "^5.0" @@ -79,7 +79,7 @@ smmap2 = "^3.0.1" gitdb2 = "^4.0.2" httpx = "^0.13.3" example_shared_isort_profile = "^0.0.1" -example_isort_formatting_plugin = "^0.0.1" +example_isort_formatting_plugin = "^0.0.2" [tool.poetry.scripts] isort = "isort.main:main" diff --git a/tests/integration/test_setting_combinations.py b/tests/integration/test_setting_combinations.py index c29c77425..7e236f9f9 100644 --- a/tests/integration/test_setting_combinations.py +++ b/tests/integration/test_setting_combinations.py @@ -154,7 +154,8 @@ def _raise(*a): @hypothesis.given( - config=st.from_type(isort.Config), disregard_skip=st.booleans(), + config=st.from_type(isort.Config), + disregard_skip=st.booleans(), ) def test_isort_is_idempotent(config: isort.Config, disregard_skip: bool) -> None: try: @@ -166,7 +167,8 @@ def test_isort_is_idempotent(config: isort.Config, disregard_skip: bool) -> None @hypothesis.given( - config=st.from_type(isort.Config), disregard_skip=st.booleans(), + config=st.from_type(isort.Config), + disregard_skip=st.booleans(), ) def test_isort_doesnt_lose_imports_or_comments(config: isort.Config, disregard_skip: bool) -> None: result = isort.code(CODE_SNIPPET, config=config, disregard_skip=disregard_skip) diff --git a/tests/unit/profiles/test_black.py b/tests/unit/profiles/test_black.py index 4b909731c..0e54e706b 100644 --- a/tests/unit/profiles/test_black.py +++ b/tests/unit/profiles/test_black.py @@ -1,46 +1,49 @@ import black + import isort -def black_format(code: str, is_pyi: bool=False, line_length: int=88) -> str: +def black_format(code: str, is_pyi: bool = False, line_length: int = 88) -> str: """Formats the provided code snippet using black""" try: return black.format_file_contents( code, fast=True, mode=black.FileMode( - is_pyi=is_pyi, line_length=line_length, + is_pyi=is_pyi, + line_length=line_length, ), ) except black.NothingChanged: return code -def black_test(code: str, expected_output: str=""): +def black_test(code: str, expected_output: str = ""): """Tests that the given code: - Behaves the same when formatted multiple times with isort. - Agrees with black formatting. - Matches the desired output or itself if none is provided. """ expected_output = expected_output or code - + # output should stay consistent over multiple runs output = isort.code(code, profile="black") assert output == isort.code(code, profile="black") - + # output should agree with black black_output = black_format(output) assert output == black_output - + # output should match expected output assert output == expected_output - + def test_black_snippet_one(): """Test consistent code formatting between isort and black for code snippet from black repository. See: https://github.com/psf/black/blob/master/tests/test_black.py """ - black_test("""#!/usr/bin/env python3 + black_test( + """#!/usr/bin/env python3 import asyncio import logging from concurrent.futures import ThreadPoolExecutor @@ -94,7 +97,7 @@ def test_black_snippet_one(): DEFAULT_MODE = black.FileMode(experimental_string_processing=True) """, -"""#!/usr/bin/env python3 + """#!/usr/bin/env python3 import asyncio import inspect import logging @@ -145,15 +148,16 @@ def test_black_snippet_one(): from .test_primer import PrimerCLITests # noqa: F401 DEFAULT_MODE = black.FileMode(experimental_string_processing=True) -""" -) - - +""", + ) + + def test_black_snippet_two(): """Test consistent code formatting between isort and black for code snippet from black repository. See: https://github.com/psf/black/blob/master/tests/test_primer.py """ - black_test('''#!/usr/bin/env python3 + black_test( + '''#!/usr/bin/env python3 import asyncio import sys @@ -188,7 +192,7 @@ def test_black_snippet_two(): Black didn't work """ ''', -'''#!/usr/bin/env python3 + '''#!/usr/bin/env python3 import asyncio import sys @@ -219,14 +223,16 @@ def test_black_snippet_two(): - stdout: Black didn't work """ -''' -) - +''', + ) + + def test_black_snippet_three(): """Test consistent code formatting between isort and black for code snippet from black repository. See: https://github.com/psf/black/blob/master/src/black/__init__.py """ - black_test('''import ast + black_test( + """import ast import asyncio from abc import ABC, abstractmethod from collections import defaultdict @@ -293,8 +299,8 @@ def test_black_snippet_three(): import colorama # noqa: F401 DEFAULT_LINE_LENGTH = 88 -''', -'''import ast +""", + """import ast import asyncio import io import itertools @@ -360,4 +366,5 @@ def test_black_snippet_three(): import colorama # noqa: F401 DEFAULT_LINE_LENGTH = 88 -''') +""", + ) diff --git a/tests/unit/test_isort.py b/tests/unit/test_isort.py index a13595b3f..949075bb2 100644 --- a/tests/unit/test_isort.py +++ b/tests/unit/test_isort.py @@ -2940,12 +2940,15 @@ def test_not_splitted_sections() -> None: ) # in case when THIRDPARTY section is excluded from sections list, # it's ok to merge STDLIB and FIRSTPARTY - assert isort.code( - code=test_input, - sections=["STDLIB", "FIRSTPARTY", "LOCALFOLDER"], - no_lines_before=["FIRSTPARTY"], - known_first_party=["app"], - ) == (stdlib_section + firstparty_section + whiteline + local_section + whiteline + statement) + assert ( + isort.code( + code=test_input, + sections=["STDLIB", "FIRSTPARTY", "LOCALFOLDER"], + no_lines_before=["FIRSTPARTY"], + known_first_party=["app"], + ) + == (stdlib_section + firstparty_section + whiteline + local_section + whiteline + statement) + ) # it doesn't change output, because stdlib packages don't have any whitelines before them assert ( isort.code(test_input, no_lines_before=["STDLIB"], known_first_party=["app"]) == test_input diff --git a/tests/unit/test_ticketed_features.py b/tests/unit/test_ticketed_features.py index 86b390455..b387ec63e 100644 --- a/tests/unit/test_ticketed_features.py +++ b/tests/unit/test_ticketed_features.py @@ -558,10 +558,16 @@ def test_isort_doesnt_remove_as_imports_when_combine_star_issue_1380(): from a import c """ assert ( - isort.code(test_input, combine_star=True,) + isort.code( + test_input, + combine_star=True, + ) == isort.code(test_input, combine_star=True, force_single_line=True) == isort.code( - test_input, combine_star=True, force_single_line=True, combine_as_imports=True, + test_input, + combine_star=True, + force_single_line=True, + combine_as_imports=True, ) == """ from a import * From db3b8e6b8b5298ea614d6a5c75905c2c50ee7ca3 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 28 Aug 2020 02:10:40 -0700 Subject: [PATCH 0982/1439] Add additional profile tests --- .../integration/test_projects_using_isort.py | 19 + tests/unit/profiles/__init__.py | 0 tests/unit/profiles/test_attrs.py | 102 +++++ tests/unit/profiles/test_django.py | 122 ++++++ tests/unit/profiles/test_google.py | 397 ++++++++++++++++++ tests/unit/profiles/test_hug.py | 112 +++++ tests/unit/profiles/test_open_stack.py | 134 ++++++ tests/unit/profiles/test_plone.py | 75 ++++ tests/unit/profiles/test_pycharm.py | 55 +++ tests/unit/utils.py | 14 + 10 files changed, 1030 insertions(+) create mode 100644 tests/unit/profiles/__init__.py create mode 100644 tests/unit/profiles/test_attrs.py create mode 100644 tests/unit/profiles/test_django.py create mode 100644 tests/unit/profiles/test_google.py create mode 100644 tests/unit/profiles/test_hug.py create mode 100644 tests/unit/profiles/test_open_stack.py create mode 100644 tests/unit/profiles/test_plone.py create mode 100644 tests/unit/profiles/test_pycharm.py create mode 100644 tests/unit/utils.py diff --git a/tests/integration/test_projects_using_isort.py b/tests/integration/test_projects_using_isort.py index 036b604ce..bb7d61de5 100644 --- a/tests/integration/test_projects_using_isort.py +++ b/tests/integration/test_projects_using_isort.py @@ -151,3 +151,22 @@ def test_pillow(tmpdir): ["git", "clone", "--depth", "1", "https://github.com/python-pillow/Pillow.git", str(tmpdir)] ) main(["--check-only", "--diff", str(tmpdir), "--skip", "tests"]) + + +def test_attrs(tmpdir): + check_call( + ["git", "clone", "--depth", "1", "https://github.com/python-attrs/attrs.git", str(tmpdir)] + ) + main( + [ + "--check-only", + "--diff", + str(tmpdir), + "--skip", + "tests", + "--ext", + "py", + "--skip", + "_compat.py", + ] + ) diff --git a/tests/unit/profiles/__init__.py b/tests/unit/profiles/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/profiles/test_attrs.py b/tests/unit/profiles/test_attrs.py new file mode 100644 index 000000000..c08f184ef --- /dev/null +++ b/tests/unit/profiles/test_attrs.py @@ -0,0 +1,102 @@ +from functools import partial + +from ..utils import isort_test + +attrs_isort_test = partial(isort_test, profile="attrs") + + +def test_attrs_code_snippet_one(): + attrs_isort_test( + """from __future__ import absolute_import, division, print_function + +import sys + +from functools import partial + +from . import converters, exceptions, filters, setters, validators +from ._config import get_run_validators, set_run_validators +from ._funcs import asdict, assoc, astuple, evolve, has, resolve_types +from ._make import ( + NOTHING, + Attribute, + Factory, + attrib, + attrs, + fields, + fields_dict, + make_class, + validate, +) +from ._version_info import VersionInfo + + +__version__ = "20.2.0.dev0" +""" + ) + + +def test_attrs_code_snippet_two(): + attrs_isort_test( + """from __future__ import absolute_import, division, print_function + +import copy +import linecache +import sys +import threading +import uuid +import warnings + +from operator import itemgetter + +from . import _config, setters +from ._compat import ( + PY2, + isclass, + iteritems, + metadata_proxy, + ordered_dict, + set_closure_cell, +) +from .exceptions import ( + DefaultAlreadySetError, + FrozenInstanceError, + NotAnAttrsClassError, + PythonTooOldError, + UnannotatedAttributeError, +) + + +# This is used at least twice, so cache it here. +_obj_setattr = object.__setattr__ +""" + ) + + +def test_attrs_code_snippet_three(): + attrs_isort_test( + ''' +""" +Commonly useful validators. +""" + +from __future__ import absolute_import, division, print_function + +import re + +from ._make import _AndValidator, and_, attrib, attrs +from .exceptions import NotCallableError + + +__all__ = [ + "and_", + "deep_iterable", + "deep_mapping", + "in_", + "instance_of", + "is_callable", + "matches_re", + "optional", + "provides", +] +''' + ) diff --git a/tests/unit/profiles/test_django.py b/tests/unit/profiles/test_django.py new file mode 100644 index 000000000..c2202717f --- /dev/null +++ b/tests/unit/profiles/test_django.py @@ -0,0 +1,122 @@ +from functools import partial + +from ..utils import isort_test + +django_isort_test = partial(isort_test, profile="django", known_first_party=["django"]) + + +def test_django_snippet_one(): + django_isort_test( + """import copy +import inspect +import warnings +from functools import partialmethod +from itertools import chain + +from django.apps import apps +from django.conf import settings +from django.core import checks +from django.core.exceptions import ( + NON_FIELD_ERRORS, FieldDoesNotExist, FieldError, MultipleObjectsReturned, + ObjectDoesNotExist, ValidationError, +) +from django.db import ( + DEFAULT_DB_ALIAS, DJANGO_VERSION_PICKLE_KEY, DatabaseError, connection, + connections, router, transaction, +) +from django.db.models import ( + NOT_PROVIDED, ExpressionWrapper, IntegerField, Max, Value, +) +from django.db.models.constants import LOOKUP_SEP +from django.db.models.constraints import CheckConstraint +from django.db.models.deletion import CASCADE, Collector +from django.db.models.fields.related import ( + ForeignObjectRel, OneToOneField, lazy_related_operation, resolve_relation, +) +from django.db.models.functions import Coalesce +from django.db.models.manager import Manager +from django.db.models.options import Options +from django.db.models.query import Q +from django.db.models.signals import ( + class_prepared, post_init, post_save, pre_init, pre_save, +) +from django.db.models.utils import make_model_tuple +from django.utils.encoding import force_str +from django.utils.hashable import make_hashable +from django.utils.text import capfirst, get_text_list +from django.utils.translation import gettext_lazy as _ +from django.utils.version import get_version + + +class Deferred: + def __repr__(self): + return '' + + def __str__(self): + return ''""" + ) + + +def test_django_snippet_two(): + django_isort_test( + '''from django.utils.version import get_version + +VERSION = (3, 2, 0, 'alpha', 0) + +__version__ = get_version(VERSION) + + +def setup(set_prefix=True): + """ + Configure the settings (this happens as a side effect of accessing the + first setting), configure logging and populate the app registry. + Set the thread-local urlresolvers script prefix if `set_prefix` is True. + """ + from django.apps import apps + from django.conf import settings + from django.urls import set_script_prefix + from django.utils.log import configure_logging + + configure_logging(settings.LOGGING_CONFIG, settings.LOGGING) + if set_prefix: + set_script_prefix( + '/' if settings.FORCE_SCRIPT_NAME is None else settings.FORCE_SCRIPT_NAME + ) + apps.populate(settings.INSTALLED_APPS)''' + ) + + +def test_django_snippet_three(): + django_isort_test( + """import cgi +import codecs +import copy +import warnings +from io import BytesIO +from itertools import chain +from urllib.parse import quote, urlencode, urljoin, urlsplit + +from django.conf import settings +from django.core import signing +from django.core.exceptions import ( + DisallowedHost, ImproperlyConfigured, RequestDataTooBig, +) +from django.core.files import uploadhandler +from django.http.multipartparser import MultiPartParser, MultiPartParserError +from django.utils.datastructures import ( + CaseInsensitiveMapping, ImmutableList, MultiValueDict, +) +from django.utils.deprecation import RemovedInDjango40Warning +from django.utils.encoding import escape_uri_path, iri_to_uri +from django.utils.functional import cached_property +from django.utils.http import is_same_domain, limited_parse_qsl +from django.utils.regex_helper import _lazy_re_compile + +from .multipartparser import parse_header + +RAISE_ERROR = object() + + +class UnreadablePostError(OSError): + pass""" + ) diff --git a/tests/unit/profiles/test_google.py b/tests/unit/profiles/test_google.py new file mode 100644 index 000000000..c558664dd --- /dev/null +++ b/tests/unit/profiles/test_google.py @@ -0,0 +1,397 @@ +from functools import partial + +from ..utils import isort_test + +google_isort_test = partial(isort_test, profile="google") + + +def test_google_code_snippet_one(): + google_isort_test( + '''# coding=utf-8 +# Copyright 2018 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""JAX user-facing transformations and utilities. +The transformations here mostly wrap internal transformations, providing +convenience flags to control behavior and handling Python containers of +arguments and outputs. The Python containers handled are pytrees (see +tree_util.py), which include nested tuples/lists/dicts, where the leaves are +arrays. +""" + +# flake8: noqa: F401 +import collections +import functools +import inspect +import itertools as it +import threading +import weakref +from typing import Any, Callable, Iterable, List, NamedTuple, Optional, Sequence, Tuple, TypeVar, Union +from warnings import warn + +import numpy as np +from contextlib import contextmanager, ExitStack + +from . import core +from . import linear_util as lu +from . import ad_util +from . import dtypes +from .core import eval_jaxpr +from .api_util import (wraps, flatten_fun, apply_flat_fun, flatten_fun_nokwargs, + flatten_fun_nokwargs2, argnums_partial, flatten_axes, + donation_vector, rebase_donate_argnums) +from .traceback_util import api_boundary +from .tree_util import (tree_map, tree_flatten, tree_unflatten, tree_structure, + tree_transpose, tree_leaves, tree_multimap, + treedef_is_leaf, Partial) +from .util import (unzip2, curry, partial, safe_map, safe_zip, prod, split_list, + extend_name_stack, wrap_name, cache) +from .lib import xla_bridge as xb +from .lib import xla_client as xc +# Unused imports to be exported +from .lib.xla_bridge import (device_count, local_device_count, devices, + local_devices, host_id, host_ids, host_count) +from .abstract_arrays import ConcreteArray, ShapedArray, raise_to_shaped +from .interpreters import partial_eval as pe +from .interpreters import xla +from .interpreters import pxla +from .interpreters import ad +from .interpreters import batching +from .interpreters import masking +from .interpreters import invertible_ad as iad +from .interpreters.invertible_ad import custom_ivjp +from .custom_derivatives import custom_jvp, custom_vjp +from .config import flags, config, bool_env + +AxisName = Any + +# This TypeVar is used below to express the fact that function call signatures +# are invariant under the jit, vmap, and pmap transformations. +# Specifically, we statically assert that the return type is invariant. +# Until PEP-612 is implemented, we cannot express the same invariance for +# function arguments. +# Note that the return type annotations will generally not strictly hold +# in JIT internals, as Tracer values are passed through the function. +# Should this raise any type errors for the tracing code in future, we can disable +# type checking in parts of the tracing code, or remove these annotations. +T = TypeVar("T") + +map = safe_map +zip = safe_zip + +FLAGS = flags.FLAGS +flags.DEFINE_bool("jax_disable_jit", bool_env("JAX_DISABLE_JIT", False), + "Disable JIT compilation and just call original Python.") + +''', + '''# coding=utf-8 +# Copyright 2018 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""JAX user-facing transformations and utilities. +The transformations here mostly wrap internal transformations, providing +convenience flags to control behavior and handling Python containers of +arguments and outputs. The Python containers handled are pytrees (see +tree_util.py), which include nested tuples/lists/dicts, where the leaves are +arrays. +""" + +# flake8: noqa: F401 +import collections +from contextlib import ExitStack +from contextlib import contextmanager +import functools +import inspect +import itertools as it +import threading +from typing import (Any, Callable, Iterable, List, NamedTuple, Optional, + Sequence, Tuple, TypeVar, Union) +from warnings import warn +import weakref + +import numpy as np + +from . import ad_util +from . import core +from . import dtypes +from . import linear_util as lu +from .abstract_arrays import ConcreteArray +from .abstract_arrays import ShapedArray +from .abstract_arrays import raise_to_shaped +from .api_util import apply_flat_fun +from .api_util import argnums_partial +from .api_util import donation_vector +from .api_util import flatten_axes +from .api_util import flatten_fun +from .api_util import flatten_fun_nokwargs +from .api_util import flatten_fun_nokwargs2 +from .api_util import rebase_donate_argnums +from .api_util import wraps +from .config import bool_env +from .config import config +from .config import flags +from .core import eval_jaxpr +from .custom_derivatives import custom_jvp +from .custom_derivatives import custom_vjp +from .interpreters import ad +from .interpreters import batching +from .interpreters import invertible_ad as iad +from .interpreters.invertible_ad import custom_ivjp +from .interpreters import masking +from .interpreters import partial_eval as pe +from .interpreters import pxla +from .interpreters import xla +from .lib import xla_bridge as xb +# Unused imports to be exported +from .lib.xla_bridge import device_count +from .lib.xla_bridge import devices +from .lib.xla_bridge import host_count +from .lib.xla_bridge import host_id +from .lib.xla_bridge import host_ids +from .lib.xla_bridge import local_device_count +from .lib.xla_bridge import local_devices +from .lib import xla_client as xc +from .traceback_util import api_boundary +from .tree_util import Partial +from .tree_util import tree_flatten +from .tree_util import tree_leaves +from .tree_util import tree_map +from .tree_util import tree_multimap +from .tree_util import tree_structure +from .tree_util import tree_transpose +from .tree_util import tree_unflatten +from .tree_util import treedef_is_leaf +from .util import cache +from .util import curry +from .util import extend_name_stack +from .util import partial +from .util import prod +from .util import safe_map +from .util import safe_zip +from .util import split_list +from .util import unzip2 +from .util import wrap_name + +AxisName = Any + +# This TypeVar is used below to express the fact that function call signatures +# are invariant under the jit, vmap, and pmap transformations. +# Specifically, we statically assert that the return type is invariant. +# Until PEP-612 is implemented, we cannot express the same invariance for +# function arguments. +# Note that the return type annotations will generally not strictly hold +# in JIT internals, as Tracer values are passed through the function. +# Should this raise any type errors for the tracing code in future, we can disable +# type checking in parts of the tracing code, or remove these annotations. +T = TypeVar("T") + +map = safe_map +zip = safe_zip + +FLAGS = flags.FLAGS +flags.DEFINE_bool("jax_disable_jit", bool_env("JAX_DISABLE_JIT", False), + "Disable JIT compilation and just call original Python.") + +''', + ) + + +def test_google_code_snippet_two(): + google_isort_test( + """#!/usr/bin/env python +# In[ ]: +# coding: utf-8 + +###### Searching and Downloading Google Images to the local disk ###### + +# Import Libraries +import sys +version = (3, 0) +cur_version = sys.version_info +if cur_version >= version: # If the Current Version of Python is 3.0 or above + import urllib.request + from urllib.request import Request, urlopen + from urllib.request import URLError, HTTPError + from urllib.parse import quote + import http.client + from http.client import IncompleteRead, BadStatusLine + http.client._MAXHEADERS = 1000 +else: # If the Current Version of Python is 2.x + import urllib2 + from urllib2 import Request, urlopen + from urllib2 import URLError, HTTPError + from urllib import quote + import httplib + from httplib import IncompleteRead, BadStatusLine + httplib._MAXHEADERS = 1000 +import time # Importing the time library to check the time of code execution +import os +import argparse +import ssl +import datetime +import json +import re +import codecs +import socket""", + """#!/usr/bin/env python +# In[ ]: +# coding: utf-8 + +###### Searching and Downloading Google Images to the local disk ###### + +# Import Libraries +import sys + +version = (3, 0) +cur_version = sys.version_info +if cur_version >= version: # If the Current Version of Python is 3.0 or above + import http.client + from http.client import BadStatusLine + from http.client import IncompleteRead + from urllib.parse import quote + import urllib.request + from urllib.request import HTTPError + from urllib.request import Request + from urllib.request import URLError + from urllib.request import urlopen + http.client._MAXHEADERS = 1000 +else: # If the Current Version of Python is 2.x + from urllib import quote + + import httplib + from httplib import BadStatusLine + from httplib import IncompleteRead + import urllib2 + from urllib2 import HTTPError + from urllib2 import Request + from urllib2 import URLError + from urllib2 import urlopen + httplib._MAXHEADERS = 1000 +import argparse +import codecs +import datetime +import json +import os +import re +import socket +import ssl +import time # Importing the time library to check the time of code execution +""", + ) + + +def test_code_snippet_three(): + google_isort_test( + '''# Copyright 2019 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Monitoring.""" +# pylint: disable=invalid-name +# TODO(ochang): Remove V3 from names once all metrics are migrated to +# stackdriver. + +from builtins import object +from builtins import range +from builtins import str + +import bisect +import collections +import functools +import itertools +import re +import six +import threading +import time + +try: + from google.cloud import monitoring_v3 +except (ImportError, RuntimeError): + monitoring_v3 = None + +from google.api_core import exceptions +from google.api_core import retry + +from base import errors +from base import utils +from config import local_config +from google_cloud_utils import compute_metadata +from google_cloud_utils import credentials +from metrics import logs +from system import environment''', + '''# Copyright 2019 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Monitoring.""" +# pylint: disable=invalid-name +# TODO(ochang): Remove V3 from names once all metrics are migrated to +# stackdriver. + +import bisect +from builtins import object +from builtins import range +from builtins import str +import collections +import functools +import itertools +import re +import threading +import time + +import six + +try: + from google.cloud import monitoring_v3 +except (ImportError, RuntimeError): + monitoring_v3 = None + +from base import errors +from base import utils +from config import local_config +from google.api_core import exceptions +from google.api_core import retry +from google_cloud_utils import compute_metadata +from google_cloud_utils import credentials +from metrics import logs +from system import environment +''', + ) diff --git a/tests/unit/profiles/test_hug.py b/tests/unit/profiles/test_hug.py new file mode 100644 index 000000000..f10b3ee61 --- /dev/null +++ b/tests/unit/profiles/test_hug.py @@ -0,0 +1,112 @@ +from functools import partial + +from ..utils import isort_test + +hug_isort_test = partial(isort_test, profile="hug", known_first_party=["hug"]) + + +def test_hug_code_snippet_one(): + hug_isort_test( + ''' +from __future__ import absolute_import + +import asyncio +import sys +from collections import OrderedDict, namedtuple +from distutils.util import strtobool +from functools import partial +from itertools import chain +from types import ModuleType +from wsgiref.simple_server import make_server + +import falcon +from falcon import HTTP_METHODS + +import hug.defaults +import hug.output_format +from hug import introspect +from hug._version import current + +INTRO = """ +/#######################################################################\\ + `.----``..-------..``.----. + :/:::::--:---------:--::::://. + .+::::----##/-/oo+:-##----::::// + `//::-------/oosoo-------::://. ## ## ## ## ##### + .-:------./++o/o-.------::-` ``` ## ## ## ## ## + `----.-./+o+:..----. `.:///. ######## ## ## ## + ``` `----.-::::::------ `.-:::://. ## ## ## ## ## #### + ://::--.``` -:``...-----...` `:--::::::-.` ## ## ## ## ## ## + :/:::::::::-:- ````` .:::::-.` ## ## #### ###### + ``.--:::::::. .:::.` + ``..::. .:: EMBRACE THE APIs OF THE FUTURE + ::- .:- + -::` ::- VERSION {0} + `::- -::` + -::-` -::- +\\########################################################################/ + Copyright (C) 2016 Timothy Edmund Crosley + Under the MIT License +""".format( + current +)''' + ) + + +def test_hug_code_snippet_two(): + hug_isort_test( + """from __future__ import absolute_import + +import functools +from collections import namedtuple + +from falcon import HTTP_METHODS + +import hug.api +import hug.defaults +import hug.output_format +from hug import introspect +from hug.format import underscore + + +def default_output_format( + content_type="application/json", apply_globally=False, api=None, cli=False, http=True +): +""" + ) + + +def test_hug_code_snippet_three(): + hug_isort_test( + """from __future__ import absolute_import + +import argparse +import asyncio +import os +import sys +from collections import OrderedDict +from functools import lru_cache, partial, wraps + +import falcon +from falcon import HTTP_BAD_REQUEST + +import hug._empty as empty +import hug.api +import hug.output_format +import hug.types as types +from hug import introspect +from hug.exceptions import InvalidTypeData +from hug.format import parse_content_type +from hug.types import ( + MarshmallowInputSchema, + MarshmallowReturnSchema, + Multiple, + OneOf, + SmartBoolean, + Text, + text, +) + +DOC_TYPE_MAP = {str: "String", bool: "Boolean", list: "Multiple", int: "Integer", float: "Float"} +""" + ) diff --git a/tests/unit/profiles/test_open_stack.py b/tests/unit/profiles/test_open_stack.py new file mode 100644 index 000000000..2def240f3 --- /dev/null +++ b/tests/unit/profiles/test_open_stack.py @@ -0,0 +1,134 @@ +from functools import partial + +from ..utils import isort_test + +open_stack_isort_test = partial(isort_test, profile="open_stack") + + +def test_open_stack_code_snippet_one(): + open_stack_isort_test( + """import httplib +import logging +import random +import StringIO +import time +import unittest + +import eventlet +import webob.exc + +import nova.api.ec2 +from nova.api import manager +from nova.api import openstack +from nova.auth import users +from nova.endpoint import cloud +import nova.flags +from nova.i18n import _ +from nova.i18n import _LC +from nova import test +""", + known_first_party=["nova"], + py_version="2", + order_by_type=False, + ) + + +def test_open_stack_code_snippet_two(): + open_stack_isort_test( + """# Copyright 2011 VMware, Inc +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import inspect +import os +import random + +from neutron_lib.callbacks import events +from neutron_lib.callbacks import registry +from neutron_lib.callbacks import resources +from neutron_lib import context +from neutron_lib.db import api as session +from neutron_lib.plugins import directory +from neutron_lib import rpc as n_rpc +from oslo_concurrency import processutils +from oslo_config import cfg +from oslo_log import log as logging +from oslo_messaging import server as rpc_server +from oslo_service import loopingcall +from oslo_service import service as common_service +from oslo_utils import excutils +from oslo_utils import importutils +import psutil + +from neutron.common import config +from neutron.common import profiler +from neutron.conf import service +from neutron import worker as neutron_worker +from neutron import wsgi + +service.register_service_opts(service.SERVICE_OPTS) +""", + known_first_party=["neutron"], + ) + + +def test_open_stack_code_snippet_three(): + open_stack_isort_test( + """ +# Copyright 2013 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import functools + +from oslo_log import log as logging +import oslo_messaging as messaging +from oslo_messaging.rpc import dispatcher +from oslo_serialization import jsonutils +from oslo_service import periodic_task +from oslo_utils import importutils +import six + +import nova.conf +import nova.context +import nova.exception +from nova.i18n import _ + +__all__ = [ + 'init', + 'cleanup', + 'set_defaults', + 'add_extra_exmods', + 'clear_extra_exmods', + 'get_allowed_exmods', + 'RequestContextSerializer', + 'get_client', + 'get_server', + 'get_notifier', +] + +profiler = importutils.try_import("osprofiler.profiler") +""", + known_first_party=["nova"], + ) diff --git a/tests/unit/profiles/test_plone.py b/tests/unit/profiles/test_plone.py new file mode 100644 index 000000000..ecacc1528 --- /dev/null +++ b/tests/unit/profiles/test_plone.py @@ -0,0 +1,75 @@ +from functools import partial + +from ..utils import isort_test + +plone_isort_test = partial(isort_test, profile="plone") + + +def test_plone_code_snippet_one(): + plone_isort_test( + """# -*- coding: utf-8 -*- +from plone.app.multilingual.testing import PLONE_APP_MULTILINGUAL_PRESET_FIXTURE # noqa +from plone.app.robotframework.testing import REMOTE_LIBRARY_BUNDLE_FIXTURE +from plone.app.testing import FunctionalTesting +from plone.app.testing import IntegrationTesting +from plone.app.testing import PloneWithPackageLayer +from plone.testing import z2 + +import plone.app.multilingualindexes + + +PAMI_FIXTURE = PloneWithPackageLayer( + bases=(PLONE_APP_MULTILINGUAL_PRESET_FIXTURE,), + name="PAMILayer:Fixture", + gs_profile_id="plone.app.multilingualindexes:default", + zcml_package=plone.app.multilingualindexes, + zcml_filename="configure.zcml", + additional_z2_products=["plone.app.multilingualindexes"], +) +""" + ) + + +def test_plone_code_snippet_two(): + plone_isort_test( + """# -*- coding: utf-8 -*- +from Acquisition import aq_base +from App.class_init import InitializeClass +from App.special_dtml import DTMLFile +from BTrees.OOBTree import OOTreeSet +from logging import getLogger +from plone import api +from plone.app.multilingual.events import ITranslationRegisteredEvent +from plone.app.multilingual.interfaces import ITG +from plone.app.multilingual.interfaces import ITranslatable +from plone.app.multilingual.interfaces import ITranslationManager +from plone.app.multilingualindexes.utils import get_configuration +from plone.indexer.interfaces import IIndexableObject +from Products.CMFPlone.utils import safe_hasattr +from Products.DateRecurringIndex.index import DateRecurringIndex +from Products.PluginIndexes.common.UnIndex import UnIndex +from Products.ZCatalog.Catalog import Catalog +from ZODB.POSException import ConflictError +from zope.component import getMultiAdapter +from zope.component import queryAdapter +from zope.globalrequest import getRequest + + +logger = getLogger(__name__) +""" + ) + + +def test_plone_code_snippet_three(): + plone_isort_test( + """# -*- coding: utf-8 -*- +from plone.app.querystring.interfaces import IQueryModifier +from zope.interface import provider + +import logging + + +logger = logging.getLogger(__name__) + +""" + ) diff --git a/tests/unit/profiles/test_pycharm.py b/tests/unit/profiles/test_pycharm.py new file mode 100644 index 000000000..f3ce1fd32 --- /dev/null +++ b/tests/unit/profiles/test_pycharm.py @@ -0,0 +1,55 @@ +from functools import partial + +from ..utils import isort_test + +pycharm_isort_test = partial(isort_test, profile="pycharm") + + +def test_pycharm_snippet_one(): + pycharm_isort_test( + """import shutil +import sys +from io import StringIO +from pathlib import Path +from typing import ( + Optional, + TextIO, + Union, + cast +) +from warnings import warn + +from isort import core + +from . import io +from .exceptions import ( + ExistingSyntaxErrors, + FileSkipComment, + FileSkipSetting, + IntroducedSyntaxErrors +) +from .format import ( + ask_whether_to_apply_changes_to_file, + create_terminal_printer, + show_unified_diff +) +from .io import Empty +from .place import module as place_module # noqa: F401 +from .place import module_with_reason as place_module_with_reason # noqa: F401 +from .settings import ( + DEFAULT_CONFIG, + Config +) + + +def sort_code_string( + code: str, + extension: Optional[str] = None, + config: Config = DEFAULT_CONFIG, + file_path: Optional[Path] = None, + disregard_skip: bool = False, + show_diff: Union[bool, TextIO] = False, + **config_kwargs, +): +""" + ) diff --git a/tests/unit/utils.py b/tests/unit/utils.py new file mode 100644 index 000000000..9c963d637 --- /dev/null +++ b/tests/unit/utils.py @@ -0,0 +1,14 @@ +import isort + + +def isort_test(code: str, expected_output: str = "", **config): + """Runs isort against the given code snippet and ensures that it + gives consistent output accross multiple runs, and if an expected_output + is given - that it matches that. + """ + expected_output = expected_output or code + + output = isort.code(code, **config) + assert output == expected_output + + assert output == isort.code(output, **config) From 104ed84c66bb4138c0131adb5176ccb79f7812db Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 28 Aug 2020 03:04:20 -0700 Subject: [PATCH 0983/1439] Fix #1420: Ensure isort doesn't include new imports in comparison --- isort/core.py | 2 -- tests/unit/test_regressions.py | 15 +++++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/isort/core.py b/isort/core.py index 35738abd9..908be4e12 100644 --- a/isort/core.py +++ b/isort/core.py @@ -215,9 +215,7 @@ def process( if not import_section: output_stream.write(line) line = "" - import_section += line_separator.join(add_imports) + line_separator contains_imports = True - add_imports = [] else: not_imports = True elif ( diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index 98621545b..eb10dc819 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -617,3 +617,18 @@ def test_reverse_relative_combined_with_force_sort_within_sections_issue_1395(): lines_after_imports=2, no_lines_before="LOCALFOLDER", ) + + +def test_isort_should_be_able_to_add_independent_of_doc_string_placement_issue_1420(): + """isort should be able to know when an import requested to be added is sucesfully added, + independent of where the top doc string is located. + See: https://github.com/PyCQA/isort/issues/1420 + """ + assert isort.check_code( + '''"""module docstring""" + +import os +''', + show_diff=True, + add_imports=["os"], + ) From 9baef903c5290caf19b0123d15bee92fd9b724f8 Mon Sep 17 00:00:00 2001 From: Guillaume Lostis Date: Fri, 28 Aug 2020 18:43:17 +0200 Subject: [PATCH 0984/1439] Typo fixes --- docs/configuration/options.md | 6 +++--- docs/upgrade_guides/5.0.0.md | 2 +- isort/main.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/configuration/options.md b/docs/configuration/options.md index 712b14ae5..10c1cfb8a 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -8,7 +8,7 @@ Too busy to build your perfect isort configuration? For curated common configura ## Python Version -Tells isort to set the known standard library based on the the specified Python version. Default is to assume any Python 3 version could be the target, and use a union off all stdlib modules across versions. If auto is specified, the version of the interpreter used to run isort (currently: 38) will be used. +Tells isort to set the known standard library based on the specified Python version. Default is to assume any Python 3 version could be the target, and use a union of all stdlib modules across versions. If auto is specified, the version of the interpreter used to run isort (currently: 38) will be used. **Type:** String **Default:** `py3` @@ -765,7 +765,7 @@ Tells isort to honor noqa comments to enforce skipping those comments. ## Src Paths -Add an explicitly defined source path (modules within src paths have their imports automatically catorgorized as first_party). +Add an explicitly defined source path (modules within src paths have their imports automatically categorized as first_party). **Type:** Frozenset **Default:** `frozenset()` @@ -948,7 +948,7 @@ Number of files to process in parallel. - -j - --jobs -## Dont Order By Type +## Don't Order By Type Don't order imports by type, which is determined by case, in addition to alphabetically. diff --git a/docs/upgrade_guides/5.0.0.md b/docs/upgrade_guides/5.0.0.md index 6a68acecc..d31fb9dc3 100644 --- a/docs/upgrade_guides/5.0.0.md +++ b/docs/upgrade_guides/5.0.0.md @@ -45,7 +45,7 @@ The `-v` (previously for version now for verbose) and `-V` (previously for verbo ## Migrating Config options The first thing to keep in mind is how isort loads config options has changed in isort 5. It will no longer merge multiple config files, instead you must have 1 isort config per a project. -If you have multiple configs, they will need to be merged into 1 single one. You can see the priority order of configuration files and the manor in which they are loaded on the +If you have multiple configs, they will need to be merged into 1 single one. You can see the priority order of configuration files and the manner in which they are loaded on the [config files documentation page](https://pycqa.github.io/isort/docs/configuration/config_files/). ### `not_skip` diff --git a/isort/main.py b/isort/main.py index ed9517a6b..34efd0893 100644 --- a/isort/main.py +++ b/isort/main.py @@ -144,7 +144,7 @@ def _build_arg_parser() -> argparse.ArgumentParser: "start guide, otherwise, one or more files/directories/stdin must be provided. " "Use `-` as the first argument to represent stdin. Use --interactive to use the pre 5.0.0 " "interactive behavior." - "" + " " "If you've used isort 4 but are new to isort 5, see the upgrading guide:" "https://pycqa.github.io/isort/docs/upgrade_guides/5.0.0/." ) @@ -155,7 +155,7 @@ def _build_arg_parser() -> argparse.ArgumentParser: dest="src_paths", action="append", help="Add an explicitly defined source path " - "(modules within src paths have their imports automatically catorgorized as first_party).", + "(modules within src paths have their imports automatically categorized as first_party).", ) parser.add_argument( "-a", From 1c6c9d3bcb46b5413dc0420b1bfb1b00a09e6441 Mon Sep 17 00:00:00 2001 From: Andrew Howe Date: Fri, 28 Aug 2020 18:45:41 +0100 Subject: [PATCH 0985/1439] Bugfix: warnings in isort.api.sort_file report the actual file name used, rather than sometimes "None" --- isort/api.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/isort/api.py b/isort/api.py index 8b0ca237d..cbcc3e6ea 100644 --- a/isort/api.py +++ b/isort/api.py @@ -298,6 +298,7 @@ def sort_file( - ****config_kwargs**: Any config modifications. """ with io.File.read(filename) as source_file: + actual_file_path = file_path or source_file.path changed: bool = False try: if write_to_stdout: @@ -305,7 +306,7 @@ def sort_file( input_stream=source_file.stream, output_stream=sys.stdout, config=config, - file_path=file_path or source_file.path, + file_path=actual_file_path, disregard_skip=disregard_skip, extension=extension, **config_kwargs, @@ -321,7 +322,7 @@ def sort_file( input_stream=source_file.stream, output_stream=output_stream, config=config, - file_path=file_path or source_file.path, + file_path=actual_file_path, disregard_skip=disregard_skip, extension=extension, **config_kwargs, @@ -335,7 +336,7 @@ def sort_file( show_unified_diff( file_input=source_file.stream.read(), file_output=tmp_out.read(), - file_path=file_path or source_file.path, + file_path=actual_file_path, output=None if show_diff is True else cast(TextIO, show_diff), color_output=config.color_output, ) @@ -356,9 +357,9 @@ def sort_file( except FileNotFoundError: pass except ExistingSyntaxErrors: - warn(f"{file_path} unable to sort due to existing syntax errors") + warn(f"{actual_file_path} unable to sort due to existing syntax errors") except IntroducedSyntaxErrors: # pragma: no cover - warn(f"{file_path} unable to sort as isort introduces new syntax errors") + warn(f"{actual_file_path} unable to sort as isort introduces new syntax errors") return changed From d4defa927f634a3027c2edab966bfe74024ddef6 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 28 Aug 2020 11:37:40 -0700 Subject: [PATCH 0986/1439] Populate pypo fixes from PR into file used for generating md docs --- isort/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/main.py b/isort/main.py index 34efd0893..f4328a8b6 100644 --- a/isort/main.py +++ b/isort/main.py @@ -586,9 +586,9 @@ def _build_arg_parser() -> argparse.ArgumentParser: action="store", dest="py_version", choices=tuple(VALID_PY_TARGETS) + ("auto",), - help="Tells isort to set the known standard library based on the the specified Python " + help="Tells isort to set the known standard library based on the specified Python " "version. Default is to assume any Python 3 version could be the target, and use a union " - "off all stdlib modules across versions. If auto is specified, the version of the " + "of all stdlib modules across versions. If auto is specified, the version of the " "interpreter used to run isort " f"(currently: {sys.version_info.major}{sys.version_info.minor}) will be used.", ) From d203bc34b04919fc30332c4f93fab23a14660719 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 29 Aug 2020 16:32:53 -0700 Subject: [PATCH 0987/1439] Add failing test for #1427 --- tests/unit/test_regressions.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index eb10dc819..3b35c4016 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -632,3 +632,16 @@ def test_isort_should_be_able_to_add_independent_of_doc_string_placement_issue_1 show_diff=True, add_imports=["os"], ) + + +def test_comments_should_never_be_moved_between_imports_issue_1427(): + """isort should never move comments to different import statement. + See: https://github.com/PyCQA/isort/issues/1427 + """ + assert isort.check_code( + """from package import CONSTANT +from package import * # noqa + """, + force_single_line=True, + show_diff=True, + ) From ae10a3f7ce077795e5dcb6e65d8debaf978c126c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 29 Aug 2020 17:55:46 -0700 Subject: [PATCH 0988/1439] Resolve #1427: Fixed isort incorrectly moving import statement --- isort/output.py | 12 ++++++++++-- isort/parse.py | 7 +------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/isort/output.py b/isort/output.py index 7d793ac1e..1a6c504b6 100644 --- a/isort/output.py +++ b/isort/output.py @@ -272,7 +272,7 @@ def _with_from_imports( if "*" in from_imports and config.combine_star: import_statement = wrap.line( with_comments( - comments, + _with_star_comments(parsed, module, list(comments or ())), f"{import_start}*", removed=config.ignore_comments, comment_prefix=config.comment_prefix, @@ -364,7 +364,7 @@ def _with_from_imports( if "*" in from_imports: output.append( with_comments( - comments, + _with_star_comments(parsed, module, list(comments or ())), f"{import_start}*", removed=config.ignore_comments, comment_prefix=config.comment_prefix, @@ -529,3 +529,11 @@ def is_comment(line): new_output.append("") new_output.append(line) return new_output + + +def _with_star_comments(parsed: parse.ParsedContent, module: str, comments: List[str]) -> List[str]: + star_comment = parsed.categorized_comments["nested"].get(module, {}).pop("*", None) + if star_comment: + return comments + [star_comment] + else: + return comments diff --git a/isort/parse.py b/isort/parse.py index 1e71c6d2c..913cdbb12 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -222,12 +222,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte import_string, comment = parse_comments(line) comments = [comment] if comment else [] line_parts = [part for part in _strip_syntax(import_string).strip().split(" ") if part] - if ( - type_of_import == "from" - and len(line_parts) == 2 - and line_parts[1] != "*" - and comments - ): + if type_of_import == "from" and len(line_parts) == 2 and comments: nested_comments[line_parts[-1]] = comments[0] if "(" in line.split("#", 1)[0] and index < line_count: From cda29dea2792d98073dd441fcce32d7ba223b2c5 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 30 Aug 2020 00:55:06 -0700 Subject: [PATCH 0989/1439] Resolve #1431: isort incorrectly moving comments --- isort/output.py | 47 ++++++++++++++++++++++++---------- isort/parse.py | 19 ++++++++++---- tests/unit/test_regressions.py | 14 ++++++++++ 3 files changed, 61 insertions(+), 19 deletions(-) diff --git a/isort/output.py b/isort/output.py index 1a6c504b6..8cf915f95 100644 --- a/isort/output.py +++ b/isort/output.py @@ -328,13 +328,20 @@ def _with_from_imports( while from_imports and from_imports[0] in as_imports: from_import = from_imports.pop(0) as_imports[from_import] = sorting.naturally(as_imports[from_import]) - from_comments = parsed.categorized_comments["straight"].get( - f"{module}.{from_import}" + from_comments = ( + parsed.categorized_comments["straight"].get(f"{module}.{from_import}") or [] ) if ( parsed.imports[section]["from"][module][from_import] and not only_show_as_imports ): + specific_comment = ( + parsed.categorized_comments["nested"] + .get(module, {}) + .pop(from_import, None) + ) + if specific_comment: + from_comments.append(specific_comment) output.append( wrap.line( with_comments( @@ -347,19 +354,31 @@ def _with_from_imports( config, ) ) - output.extend( - wrap.line( - with_comments( - from_comments, - import_start + as_import, - removed=config.ignore_comments, - comment_prefix=config.comment_prefix, - ), - parsed.line_separator, - config, + from_comments = [] + + for as_import in as_imports[from_import]: + specific_comment = ( + parsed.categorized_comments["nested"] + .get(module, {}) + .pop(as_import, None) ) - for as_import in as_imports[from_import] - ) + if specific_comment: + from_comments.append(specific_comment) + + output.append( + wrap.line( + with_comments( + from_comments, + import_start + as_import, + removed=config.ignore_comments, + comment_prefix=config.comment_prefix, + ), + parsed.line_separator, + config, + ) + ) + + from_comments = [] if "*" in from_imports: output.append( diff --git a/isort/parse.py b/isort/parse.py index 913cdbb12..4a5374227 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -235,7 +235,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte if ( type_of_import == "from" and stripped_line - and " " not in stripped_line + and " " not in stripped_line.replace(" as ", "") and new_comment ): nested_comments[stripped_line] = comments[-1] @@ -257,7 +257,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte if ( type_of_import == "from" and stripped_line - and " " not in stripped_line + and " " not in stripped_line.replace(" as ", "") and new_comment ): nested_comments[stripped_line] = comments[-1] @@ -272,7 +272,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte if ( type_of_import == "from" and stripped_line - and " " not in stripped_line + and " " not in stripped_line.replace(" as ", "") and new_comment ): nested_comments[stripped_line] = comments[-1] @@ -282,7 +282,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte if ( type_of_import == "from" and stripped_line - and " " not in stripped_line + and " " not in stripped_line.replace(" as ", "") and new_comment ): nested_comments[stripped_line] = comments[-1] @@ -332,6 +332,15 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte pass elif as_name not in as_map["from"][module]: as_map["from"][module].append(as_name) + + full_name = f"{nested_module} as {as_name}" + associated_comment = nested_comments.get(full_name) + if associated_comment: + categorized_comments["nested"].setdefault(top_level_module, {})[ + full_name + ] = associated_comment + if associated_comment in comments: + comments.pop(comments.index(associated_comment)) else: module = just_imports[as_index - 1] as_name = just_imports[as_index + 1] @@ -340,7 +349,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte elif as_name not in as_map["straight"][module]: as_map["straight"][module].append(as_name) - if config.combine_as_imports and nested_module: + if nested_module and config.combine_as_imports: categorized_comments["from"].setdefault( f"{top_level_module}.__combined_as__", [] ).extend(comments) diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index 3b35c4016..9a44c967e 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -645,3 +645,17 @@ def test_comments_should_never_be_moved_between_imports_issue_1427(): force_single_line=True, show_diff=True, ) + + +def test_isort_doesnt_misplace_comments_issue_1431(): + """Test to ensure isort wont misplace comments. + See: https://github.com/PyCQA/isort/issues/1431 + """ + input_text = """from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( + MyLovelyCompanyTeamProjectComponent, # NOT DRY +) +from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( + MyLovelyCompanyTeamProjectComponent as component, # DRY +) +""" + assert isort.code(input_text, profile="black") == input_text From 7ccb37aae8bf9c41386f0071960644c72206382a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 30 Aug 2020 02:22:38 -0700 Subject: [PATCH 0990/1439] Improve comment parsing logic --- isort/parse.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/isort/parse.py b/isort/parse.py index 4a5374227..869742345 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -318,6 +318,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte for item in _strip_syntax(import_string).split() ] straight_import = True + attach_comments_to: Optional[List[Any]] = None if "as" in just_imports and (just_imports.index("as") + 1) < len(just_imports): straight_import = False while "as" in just_imports: @@ -349,15 +350,17 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte elif as_name not in as_map["straight"][module]: as_map["straight"][module].append(as_name) - if nested_module and config.combine_as_imports: - categorized_comments["from"].setdefault( - f"{top_level_module}.__combined_as__", [] - ).extend(comments) - comments = [] - else: - categorized_comments["straight"][module] = comments - comments = [] + if comments and attach_comments_to is None: + if nested_module and config.combine_as_imports: + attach_comments_to = categorized_comments["from"].setdefault( + f"{top_level_module}.__combined_as__", [] + ) + else: + attach_comments_to = categorized_comments["straight"].setdefault( + module, [] + ) del just_imports[as_index : as_index + 2] + if type_of_import == "from": import_from = just_imports.pop(0) placed_module = finder(import_from) @@ -377,8 +380,8 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte ] = associated_comment if associated_comment in comments: comments.pop(comments.index(associated_comment)) - if comments: - categorized_comments["from"].setdefault(import_from, []).extend(comments) + if comments and attach_comments_to is None: + attach_comments_to = categorized_comments["from"].setdefault(import_from, []) if len(out_lines) > max(import_index, 1) - 1: last = out_lines and out_lines[-1].rstrip() or "" @@ -412,7 +415,14 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte (module, straight_import | root[import_from].get(module, False)) for module in just_imports ) + + if comments and attach_comments_to is not None: + attach_comments_to.extend(comments) else: + if attach_comments_to: + attach_comments_to.extend(comments) + comments = [] + for module in just_imports: if comments: categorized_comments["straight"][module] = comments From edd3aa0adff8ea8308c80e2315d1fc818c172a28 Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Mon, 31 Aug 2020 10:47:44 +0300 Subject: [PATCH 0991/1439] Print errors to stderr. Fixes #1429 --- isort/format.py | 7 +------ tests/unit/test_format.py | 12 ++++++------ tests/unit/test_isort.py | 6 +++--- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/isort/format.py b/isort/format.py index 67c8c5b1c..0fe116bdc 100644 --- a/isort/format.py +++ b/isort/format.py @@ -102,12 +102,7 @@ def success(self, message: str) -> None: print(f"{self.SUCCESS}: {message}", file=self.output) def error(self, message: str) -> None: - print( - f"{self.ERROR}: {message}", - file=self.output, - # TODO this should print to stderr, but don't want to make it backward incompatible now - # file=sys.stderr - ) + print(f"{self.ERROR}: {message}", file=sys.stderr) def diff_line(self, line: str) -> None: self.output.write(line) diff --git a/tests/unit/test_format.py b/tests/unit/test_format.py index a2658367d..fee61c67d 100644 --- a/tests/unit/test_format.py +++ b/tests/unit/test_format.py @@ -26,8 +26,8 @@ def test_basic_printer(capsys): out, _ = capsys.readouterr() assert out == "SUCCESS: All good!\n" printer.error("Some error") - out, _ = capsys.readouterr() - assert out == "ERROR: Some error\n" + _, err = capsys.readouterr() + assert err == "ERROR: Some error\n" def test_basic_printer_diff(capsys): @@ -51,10 +51,10 @@ def test_colored_printer_success(capsys): def test_colored_printer_error(capsys): printer = isort.format.create_terminal_printer(color=True) printer.error("Some error") - out, _ = capsys.readouterr() - assert "ERROR" in out - assert "Some error" in out - assert colorama.Fore.RED in out + _, err = capsys.readouterr() + assert "ERROR" in err + assert "Some error" in err + assert colorama.Fore.RED in err def test_colored_printer_diff(capsys): diff --git a/tests/unit/test_isort.py b/tests/unit/test_isort.py index a13595b3f..bfa550f48 100644 --- a/tests/unit/test_isort.py +++ b/tests/unit/test_isort.py @@ -2716,9 +2716,9 @@ def test_long_alias_using_paren_issue_957() -> None: def test_strict_whitespace_by_default(capsys) -> None: test_input = "import os\nfrom django.conf import settings\n" assert not api.check_code_string(test_input) - out, _ = capsys.readouterr() - assert "ERROR" in out - assert out.endswith("Imports are incorrectly sorted and/or formatted.\n") + _, err = capsys.readouterr() + assert "ERROR" in err + assert err.endswith("Imports are incorrectly sorted and/or formatted.\n") def test_strict_whitespace_no_closing_newline_issue_676(capsys) -> None: From a4dc1b38a69b9c28fe523b7bec12ab80c8000884 Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Mon, 31 Aug 2020 18:38:36 +0300 Subject: [PATCH 0992/1439] Get rid of warnings displayed when running pytest. Cleaner output when running the tests. --- setup.cfg | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/setup.cfg b/setup.cfg index 8129063fa..2c04622f0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,6 +14,10 @@ strict_optional = False [tool:pytest] testpaths = tests +filterwarnings = + ignore::DeprecationWarning:distlib + ignore::DeprecationWarning:requirementslib + ignore::hypothesis.errors.NonInteractiveExampleWarning:hypothesis [flake8] max-line-length = 100 From cb9895e98c22a5c5987d6c2d793e9da576557fd9 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Mon, 31 Aug 2020 08:55:40 -0700 Subject: [PATCH 0993/1439] Make black target Python version 3.6 Now aligns with isort lowest supported version as well as other tools like mypy. --- scripts/lint.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/lint.sh b/scripts/lint.sh index 9761425c3..b8eb14da2 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -3,8 +3,7 @@ set -euxo pipefail poetry run cruft check poetry run mypy --ignore-missing-imports isort/ -poetry run black --check isort/ tests/ -poetry run black --check example_isort_formatting_plugin/ +poetry run black --target-version py36 --check . poetry run isort --profile hug --check --diff isort/ tests/ poetry run isort --profile hug --check --diff example_isort_formatting_plugin/ poetry run flake8 isort/ tests/ From d0c3b672673caef46ad935a962caa57a94779ad7 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Mon, 31 Aug 2020 09:31:50 -0700 Subject: [PATCH 0994/1439] Remove unused optional dependency tomlkit Never imported directly by isort, so don't list it as a dependency. When it is necessary as a transient dependency, that is handled by isort's direct dependencies. --- poetry.lock | 5 +++-- pyproject.toml | 4 +--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index ce01d351e..d6e942661 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1656,11 +1656,12 @@ testing = ["jaraco.itertools", "func-timeout"] [extras] colors = ["colorama"] -pipfile_deprecated_finder = ["pipreqs", "tomlkit", "requirementslib"] +pipfile_deprecated_finder = ["pipreqs", "requirementslib"] requirements_deprecated_finder = ["pipreqs", "pip-api"] [metadata] -content-hash = "c366831acb4de815d36de7d6072c341ae3a1a1cca409490b5c81fe1159977597" +content-hash = "b0253934829c50ca3694ad1b04e96761dd3122140ccf34b251e8b1b91dea4ca6" +lock-version = "1.0" python-versions = "^3.6" [metadata.files] diff --git a/pyproject.toml b/pyproject.toml index 5f648f408..0de43c7a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,12 +40,11 @@ packages = [ python = "^3.6" pipreqs = {version = "*", optional = true} requirementslib = {version = "*", optional = true} -tomlkit = {version = ">=0.5.3", optional = true} pip-api = {version = "*", optional = true} colorama = {version = "^0.4.3", optional = true} [tool.poetry.extras] -pipfile_deprecated_finder = ["pipreqs", "tomlkit", "requirementslib", "pip-shims<=0.3.4"] +pipfile_deprecated_finder = ["pipreqs", "requirementslib", "pip-shims<=0.3.4"] requirements_deprecated_finder = ["pipreqs", "pip-api"] colors = ["colorama"] @@ -69,7 +68,6 @@ portray = { version = "^1.3.0" } pipfile = "^0.0.2" requirementslib = "^1.5" pipreqs = "^0.4.9" -tomlkit = ">=0.5.3" pip_api = "^0.0.12" numpy = "^1.16.0" pylama = "^7.7" From de63a1bbb1886f7dea1f2ed07d7dfa5963fca2ab Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Mon, 31 Aug 2020 14:18:06 -0700 Subject: [PATCH 0995/1439] Simplify flake8 configuration W503 is ignored by default. Use extend ignore to modify the defaults rather than override them. --- setup.cfg | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 8129063fa..d1011f5d3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,8 +18,7 @@ testpaths = tests [flake8] max-line-length = 100 # Ignore non PEP 8 compliant rules as suggested by black -ignore = - W503 # https://github.com/psf/black/blob/master/docs/the_black_code_style.md#line-breaks--binary-operators +extend-ignore = E203 # https://github.com/psf/black/blob/master/docs/the_black_code_style.md#slices exclude = _vendored per-file-ignores = From c13e83e8adf2cc49daff41805a6a154fbabff0af Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 31 Aug 2020 21:16:48 -0700 Subject: [PATCH 0996/1439] Alphabetically list core developers --- docs/contributing/4.-acknowledgements.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index 2ae2d8172..50511816b 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -1,5 +1,7 @@ -Original Creator & Maintainer +Core Developers =================== +- Jon Dufresne (@jdufresne) +- Tamas Szabo (@sztamas) - Timothy Edmund Crosley (@timothycrosley) Plugin Writers From 3903d3401bae8115d1e92cc8954b77f6ff05dc2d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 1 Sep 2020 03:04:21 -0700 Subject: [PATCH 0997/1439] Resovle #1434 add backslash-grid mode --- README.md | 15 +++++++++++++-- isort/wrap_modes.py | 10 ++++++++-- tests/unit/test_wrap_modes.py | 21 +++++++++++++++++++++ 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a5564312c..6bd080430 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,7 @@ notified. You will notice above the \"multi\_line\_output\" setting. This setting defines how from imports wrap when they extend past the line\_length -limit and has 6 possible settings: +limit and has 12 possible settings: **0 - Grid** @@ -285,7 +285,18 @@ from third_party import ( lib4, lib5, lib6) ``` -Note: to change the how constant indents appear - simply change the +**11 - Backslash Grid** + +Same as Mode 0 - _Grid_ but uses backslashes instead of parentheses to group imports. + +```python +from third_party import lib1, lib2, lib3, \ + lib4, lib5 +``` + +## Indentation + +To change the how constant indents appear - simply change the indent property with the following accepted formats: - Number of spaces you would like. For example: 4 would cause standard diff --git a/isort/wrap_modes.py b/isort/wrap_modes.py index 8e10a9474..b7b22f000 100644 --- a/isort/wrap_modes.py +++ b/isort/wrap_modes.py @@ -60,11 +60,11 @@ def grid(**interface): len(next_statement.split(interface["line_separator"])[-1]) + 1 > interface["line_length"] ): - lines = [f"{interface['white_space']}{next_import.split(' ')[0]}"] + lines = [f"{white_space}{next_import.split(' ')[0]}"] for part in next_import.split(" ")[1:]: new_line = f"{lines[-1]} {part}" if len(new_line) + 1 > interface["line_length"]: - lines.append(f"{interface['white_space']}{part}") + lines.append(f"{white_space}{part}") else: lines[-1] = new_line next_import = interface["line_separator"].join(lines) @@ -306,6 +306,12 @@ def hanging_indent_with_parentheses(**interface): return _hanging_indent_common(use_parentheses=True, **interface) +@_wrap_mode +def backslash_grid(**interface): + interface["indent"] = interface["white_space"][:-1] + return _hanging_indent_common(use_parentheses=False, **interface) + + WrapModes = enum.Enum( # type: ignore "WrapModes", {wrap_mode: index for index, wrap_mode in enumerate(_wrap_modes.keys())} ) diff --git a/tests/unit/test_wrap_modes.py b/tests/unit/test_wrap_modes.py index d27f92e77..7d6fbb0c7 100644 --- a/tests/unit/test_wrap_modes.py +++ b/tests/unit/test_wrap_modes.py @@ -1,5 +1,6 @@ from hypothesis_auto import auto_pytest_magic +import isort from isort import wrap_modes auto_pytest_magic(wrap_modes.grid, auto_allow_exceptions_=(ValueError,)) @@ -21,6 +22,7 @@ imports=["one", "two"], ) auto_pytest_magic(wrap_modes.hanging_indent_with_parentheses, auto_allow_exceptions_=(ValueError,)) +auto_pytest_magic(wrap_modes.backslash_grid, auto_allow_exceptions_=(ValueError,)) def test_wrap_mode_interface(): @@ -65,3 +67,22 @@ def test_auto_saved(): ) == '*\x12\x07\U0009e994🁣"\U000ae787\x0e \x00\U0001ae99\U0005c3e7\U0004d08e \x1e ' ) + + +def test_backslash_grid(): + """Tests the backslash_grid grid wrap mode, ensuring it matches formatting expectations. + See: https://github.com/PyCQA/isort/issues/1434 + """ + assert isort.code(""" +from kopf.engines import loggers, posting +from kopf.reactor import causation, daemons, effects, handling, lifecycles, registries +from kopf.storage import finalizers, states +from kopf.structs import (bodies, configuration, containers, diffs, + handlers as handlers_, patches, resources) +""", multi_line_output=11, line_length=88, combine_as_imports=True) == """ +from kopf.engines import loggers, posting +from kopf.reactor import causation, daemons, effects, handling, lifecycles, registries +from kopf.storage import finalizers, states +from kopf.structs import bodies, configuration, containers, diffs, \\ + handlers as handlers_, patches, resources +""" From b3a22bcf8ee9956ae20b575c2c6e55bcbdad46b3 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 1 Sep 2020 03:11:22 -0700 Subject: [PATCH 0998/1439] Formatting (black + isort) --- isort/wrap_modes.py | 4 ++-- tests/unit/test_wrap_modes.py | 12 ++++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/isort/wrap_modes.py b/isort/wrap_modes.py index b7b22f000..ccfc1cd5a 100644 --- a/isort/wrap_modes.py +++ b/isort/wrap_modes.py @@ -60,11 +60,11 @@ def grid(**interface): len(next_statement.split(interface["line_separator"])[-1]) + 1 > interface["line_length"] ): - lines = [f"{white_space}{next_import.split(' ')[0]}"] + lines = [f"{interface['white_space']}{next_import.split(' ')[0]}"] for part in next_import.split(" ")[1:]: new_line = f"{lines[-1]} {part}" if len(new_line) + 1 > interface["line_length"]: - lines.append(f"{white_space}{part}") + lines.append(f"{interface['white_space']}{part}") else: lines[-1] = new_line next_import = interface["line_separator"].join(lines) diff --git a/tests/unit/test_wrap_modes.py b/tests/unit/test_wrap_modes.py index 7d6fbb0c7..c95a75136 100644 --- a/tests/unit/test_wrap_modes.py +++ b/tests/unit/test_wrap_modes.py @@ -73,16 +73,24 @@ def test_backslash_grid(): """Tests the backslash_grid grid wrap mode, ensuring it matches formatting expectations. See: https://github.com/PyCQA/isort/issues/1434 """ - assert isort.code(""" + assert ( + isort.code( + """ from kopf.engines import loggers, posting from kopf.reactor import causation, daemons, effects, handling, lifecycles, registries from kopf.storage import finalizers, states from kopf.structs import (bodies, configuration, containers, diffs, handlers as handlers_, patches, resources) -""", multi_line_output=11, line_length=88, combine_as_imports=True) == """ +""", + multi_line_output=11, + line_length=88, + combine_as_imports=True, + ) + == """ from kopf.engines import loggers, posting from kopf.reactor import causation, daemons, effects, handling, lifecycles, registries from kopf.storage import finalizers, states from kopf.structs import bodies, configuration, containers, diffs, \\ handlers as handlers_, patches, resources """ + ) From 09734212dd6c661c563a062ee91c9302deacada4 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 2 Sep 2020 20:23:08 -0700 Subject: [PATCH 0999/1439] Reuse config when items passed in through stdin as used when items passed in explicitly. Resloves: #1447 --- isort/main.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/isort/main.py b/isort/main.py index f4328a8b6..990e12184 100644 --- a/isort/main.py +++ b/isort/main.py @@ -823,11 +823,10 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = print(json.dumps(config.__dict__, indent=4, separators=(",", ": "), default=_preconvert)) return elif file_names == ["-"]: - arguments.setdefault("settings_path", os.getcwd()) api.sort_stream( input_stream=sys.stdin if stdin is None else stdin, output_stream=sys.stdout, - **arguments, + config=config, ) else: skipped: List[str] = [] From c550477996153379ee64b5f7f14b4a37b60e3ea6 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 2 Sep 2020 20:28:12 -0700 Subject: [PATCH 1000/1439] Add test for #1447: error when using deprecated settings with stdin --- tests/unit/test_main.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 0d2738710..ae520b833 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -237,6 +237,9 @@ def test_main(capsys, tmpdir): with pytest.warns(UserWarning): main.main([str(python_file), "--recursive", "-fss"]) + # warnings should be displayed when streaming input is provided with old flags as well + with pytest.warns(UserWarning): + main.main(["-sp", str(config_file), "-"], stdin=input_content) def test_isort_command(): """Ensure ISortCommand got registered, otherwise setuptools error must have occured""" From c36cb905548d794322cacd41b45fcc49a3a337f1 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 2 Sep 2020 20:31:58 -0700 Subject: [PATCH 1001/1439] Formatting fix --- tests/unit/test_main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index ae520b833..3b7dd7689 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -241,6 +241,7 @@ def test_main(capsys, tmpdir): with pytest.warns(UserWarning): main.main(["-sp", str(config_file), "-"], stdin=input_content) + def test_isort_command(): """Ensure ISortCommand got registered, otherwise setuptools error must have occured""" assert main.ISortCommand From c8cb55f1c11caead57ce1ccf8c1a621533282266 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 3 Sep 2020 00:11:05 -0700 Subject: [PATCH 1002/1439] Fix comment attaching, caught by missed coverage --- isort/parse.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/isort/parse.py b/isort/parse.py index 869742345..980924f17 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -380,6 +380,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte ] = associated_comment if associated_comment in comments: comments.pop(comments.index(associated_comment)) + if comments and attach_comments_to is None: attach_comments_to = categorized_comments["from"].setdefault(import_from, []) @@ -419,7 +420,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte if comments and attach_comments_to is not None: attach_comments_to.extend(comments) else: - if attach_comments_to: + if comments and attach_comments_to is not None: attach_comments_to.extend(comments) comments = [] From 7fcfb9fcdc01147723fc79de279cabaa39016f39 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 3 Sep 2020 01:06:23 -0700 Subject: [PATCH 1003/1439] Add test cases for both #1444 and 1445 --- tests/unit/test_regressions.py | 55 ++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index 9a44c967e..9103270aa 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -659,3 +659,58 @@ def test_isort_doesnt_misplace_comments_issue_1431(): ) """ assert isort.code(input_text, profile="black") == input_text + + +def test_isort_doesnt_misplace_add_import_issue_1445(): + """Test to ensure isort won't misplace an added import depending on docstring position + See: https://github.com/PyCQA/isort/issues/1445 + """ + assert ( + isort.code( + '''#!/usr/bin/env python + +"""module docstring""" +''', + add_imports=["import os"], + ) + == '''#!/usr/bin/env python + +"""module docstring""" + +import os +''' + ) + + assert isort.check_code( + '''#!/usr/bin/env python + +"""module docstring""" + +import os + ''', + add_imports=["import os"], + show_diff=True, + ) + + +def test_isort_doesnt_mangle_code_when_adding_imports_issue_1444(): + """isort should NEVER mangle code. This particularly nasty and easy to reproduce bug, + caused isort to produce invalid code just by adding a single import statement depending + on comment placement. + See: https://github.com/PyCQA/isort/issues/1444 + """ + assert ( + isort.code( + ''' + +"""module docstring""" +''', + add_imports=["import os"], + ) + == ''' + +"""module docstring""" + +import os +''' + ) From b94f4d2b0c4b293ca4d4f4ab8f32f26cb4a994b6 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 3 Sep 2020 01:07:12 -0700 Subject: [PATCH 1004/1439] Resolve #1444 and Resolve #1445: Improve behavior of initial placement of imports --- isort/core.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/isort/core.py b/isort/core.py index 908be4e12..46900e02b 100644 --- a/isort/core.py +++ b/isort/core.py @@ -66,6 +66,8 @@ def process( code_sorting_indent: str = "" cimports: bool = False made_changes: bool = False + stripped_line: str = "" + end_of_file: bool = False if config.float_to_top: new_input = "" @@ -112,6 +114,7 @@ def process( return False not_imports = True + end_of_file = True line = "" if not line_separator: line_separator = "\n" @@ -211,13 +214,7 @@ def process( import_section += line indent = line[: -len(line.lstrip())] elif not (stripped_line or contains_imports): - if add_imports and not indent and not config.append_only: - if not import_section: - output_stream.write(line) - line = "" - contains_imports = True - else: - not_imports = True + not_imports = True elif ( not stripped_line or stripped_line.startswith("#") @@ -275,6 +272,7 @@ def process( raw_import_section: str = import_section if ( add_imports + and (stripped_line or end_of_file) and not config.append_only and not in_top_comment and not in_quote @@ -282,6 +280,8 @@ def process( and not line.lstrip().startswith(COMMENT_INDICATORS) ): import_section = line_separator.join(add_imports) + line_separator + if end_of_file and index != 0: + output_stream.write(line_separator) contains_imports = True add_imports = [] From a935c17e36da1facfdf2a46964be137e6ce60f5b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 3 Sep 2020 01:41:57 -0700 Subject: [PATCH 1005/1439] Fix docker file, add test suite --- Dockerfile | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index ab01b3200..9c2d8ac62 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,26 @@ ARG VERSION=3 FROM python:$VERSION -RUN mkdir /isort -WORKDIR /isort +# Install pip and poetry +RUN python -m pip install --upgrade pip && python -m pip install poetry +# Setup as minimal a stub project as posible, simply to allow caching base dependencies +# between builds. +# +# If error is encountered in these steps, can safely be removed locally. +RUN mkdir -p /isort/isort +RUN mkdir -p /isort/tests +RUN touch /isort/isort/__init__.py +RUN touch /isort/tests/__init__.py +RUN touch /isort/README.md +WORKDIR /isort COPY pyproject.toml poetry.lock /isort/ -RUN python -m pip install --upgrade pip && python -m pip install poetry && poetry install +RUN poetry install -COPY . /isort/ +# Install latest code for actual project +RUN rm -rf /isort +COPY . /isort RUN poetry install +# Run full test suite CMD /isort/scripts/test.sh From 3bad54f55e20ab2d943488507088e7492afd3670 Mon Sep 17 00:00:00 2001 From: anirudnits Date: Thu, 3 Sep 2020 14:27:36 +0530 Subject: [PATCH 1006/1439] added only_sections in _build_arg_parser --- isort/main.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/isort/main.py b/isort/main.py index f4328a8b6..6df337ba0 100644 --- a/isort/main.py +++ b/isort/main.py @@ -727,6 +727,15 @@ def _build_arg_parser() -> argparse.ArgumentParser: help=argparse.SUPPRESS, ) + parser.add_argument( + "--only-sections", + "--os", + dest="only_sections", + action="store_true", + help="Causes imports to be sorted only based on their sections like STDLIB,THIRDPARTY etc. " + "Imports are unaltered and keep their relative positions within the different sections.", + ) + return parser From 76a9977f071ffd5f5151684902e1e0874b53ac45 Mon Sep 17 00:00:00 2001 From: anirudnits Date: Thu, 3 Sep 2020 14:29:40 +0530 Subject: [PATCH 1007/1439] added logic to bypass sorting within sections and from_imports when only_sections options is true --- isort/output.py | 81 +++++++++++++++++++++++++++++++------------------ 1 file changed, 51 insertions(+), 30 deletions(-) diff --git a/isort/output.py b/isort/output.py index 8cf915f95..015252085 100644 --- a/isort/output.py +++ b/isort/output.py @@ -49,16 +49,19 @@ def sorted_imports( pending_lines_before = False for section in sections: straight_modules = parsed.imports[section]["straight"] - straight_modules = sorting.naturally( - straight_modules, - key=lambda key: sorting.module_key( - key, config, section_name=section, straight_import=True - ), - ) + if not config.only_sections: + straight_modules = sorting.naturally( + straight_modules, + key=lambda key: sorting.module_key( + key, config, section_name=section, straight_import=True + ), + ) + from_modules = parsed.imports[section]["from"] - from_modules = sorting.naturally( - from_modules, key=lambda key: sorting.module_key(key, config, section_name=section) - ) + if not config.only_sections: + from_modules = sorting.naturally( + from_modules, key=lambda key: sorting.module_key(key, config, section_name=section) + ) straight_imports = _with_straight_imports( parsed, config, straight_modules, section, remove_imports, import_type @@ -89,7 +92,7 @@ def sorted_imports( comments_above = [] else: new_section_output.append(line) - + # only_sections options is not imposed if force_sort_within_sections is True new_section_output = sorting.naturally( new_section_output, key=partial( @@ -226,16 +229,14 @@ def _with_from_imports( config.force_single_line and module not in config.single_line_exclusions ): ignore_case = config.force_alphabetical_sort_within_sections - from_imports = sorting.naturally( - from_imports, - key=lambda key: sorting.module_key( - key, - config, - True, - ignore_case, - section_name=section, - ), - ) + + if not config.only_sections: + from_imports = sorting.naturally( + from_imports, + key=lambda key: sorting.module_key( + key, config, True, ignore_case, section_name=section, + ), + ) if remove_imports: from_imports = [ line for line in from_imports if f"{module}.{line}" not in remove_imports @@ -252,7 +253,8 @@ def _with_from_imports( if config.combine_as_imports and not ("*" in from_imports and config.combine_star): if not config.no_inline_sort: for as_import in as_imports: - as_imports[as_import] = sorting.naturally(as_imports[as_import]) + if not config.only_sections: + as_imports[as_import] = sorting.naturally(as_imports[as_import]) for from_import in copy.copy(from_imports): if from_import in as_imports: idx = from_imports.index(from_import) @@ -312,22 +314,41 @@ def _with_from_imports( from_comments = parsed.categorized_comments["straight"].get( f"{module}.{from_import}" ) - output.extend( - with_comments( - from_comments, - wrap.line(import_start + as_import, parsed.line_separator, config), - removed=config.ignore_comments, - comment_prefix=config.comment_prefix, + + if not config.only_sections: + output.extend( + with_comments( + from_comments, + wrap.line( + import_start + as_import, parsed.line_separator, config + ), + removed=config.ignore_comments, + comment_prefix=config.comment_prefix, + ) + for as_import in sorting.naturally(as_imports[from_import]) + ) + + else: + output.extend( + with_comments( + from_comments, + wrap.line( + import_start + as_import, parsed.line_separator, config + ), + removed=config.ignore_comments, + comment_prefix=config.comment_prefix, + ) + for as_import in as_imports[from_import] ) - for as_import in sorting.naturally(as_imports[from_import]) - ) else: output.append(wrap.line(single_import_line, parsed.line_separator, config)) comments = None else: while from_imports and from_imports[0] in as_imports: from_import = from_imports.pop(0) - as_imports[from_import] = sorting.naturally(as_imports[from_import]) + + if not config.only_sections: + as_imports[from_import] = sorting.naturally(as_imports[from_import]) from_comments = ( parsed.categorized_comments["straight"].get(f"{module}.{from_import}") or [] ) From 55499157ee5192339017236358097fe099127993 Mon Sep 17 00:00:00 2001 From: anirudnits Date: Thu, 3 Sep 2020 14:30:41 +0530 Subject: [PATCH 1008/1439] added only_sections option in data schema _Config --- isort/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/isort/settings.py b/isort/settings.py index 1e10ab60e..a94a17a3d 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -189,6 +189,7 @@ class _Config: classes: FrozenSet[str] = frozenset() variables: FrozenSet[str] = frozenset() dedup_headings: bool = False + only_sections: bool = False def __post_init__(self): py_version = self.py_version From 3b7c3e2ca046b183661a70dd712c2c2f9eaef19e Mon Sep 17 00:00:00 2001 From: anirudnits Date: Thu, 3 Sep 2020 14:31:35 +0530 Subject: [PATCH 1009/1439] added tests for only_sections option --- tests/unit/test_isort.py | 52 ++++++++++++++++++++++++++++++++-------- tests/unit/test_main.py | 2 ++ 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/tests/unit/test_isort.py b/tests/unit/test_isort.py index dc7f5303e..ffbebf8e8 100644 --- a/tests/unit/test_isort.py +++ b/tests/unit/test_isort.py @@ -2940,15 +2940,12 @@ def test_not_splitted_sections() -> None: ) # in case when THIRDPARTY section is excluded from sections list, # it's ok to merge STDLIB and FIRSTPARTY - assert ( - isort.code( - code=test_input, - sections=["STDLIB", "FIRSTPARTY", "LOCALFOLDER"], - no_lines_before=["FIRSTPARTY"], - known_first_party=["app"], - ) - == (stdlib_section + firstparty_section + whiteline + local_section + whiteline + statement) - ) + assert isort.code( + code=test_input, + sections=["STDLIB", "FIRSTPARTY", "LOCALFOLDER"], + no_lines_before=["FIRSTPARTY"], + known_first_party=["app"], + ) == (stdlib_section + firstparty_section + whiteline + local_section + whiteline + statement) # it doesn't change output, because stdlib packages don't have any whitelines before them assert ( isort.code(test_input, no_lines_before=["STDLIB"], known_first_party=["app"]) == test_input @@ -3178,11 +3175,12 @@ def test_monkey_patched_urllib() -> None: def test_argument_parsing() -> None: from isort.main import parse_args - args = parse_args(["--dt", "-t", "foo", "--skip=bar", "baz.py"]) + args = parse_args(["--dt", "-t", "foo", "--skip=bar", "baz.py", "--os"]) assert args["order_by_type"] is False assert args["force_to_top"] == ["foo"] assert args["skip"] == ["bar"] assert args["files"] == ["baz.py"] + assert args["only_sections"] is False @pytest.mark.parametrize("multiprocess", (False, True)) @@ -4802,3 +4800,37 @@ def test_deprecated_settings(): """Test to ensure isort warns when deprecated settings are used, but doesn't fail to run""" with pytest.warns(UserWarning): assert isort.code("hi", not_skip=True) + + +def test_only_sections() -> None: + # test to ensure that the within sections relative position of imports are maintained + test_input = ( + "import sys\n" + "\n" + "import numpy as np\n" + "\n" + "import os\n" + "\n" + "import pandas as pd\n" + "\n" + "import math\n" + "import .views\n" + "from collections import defaultdict\n" + ) + + assert isort.code(test_input, only_sections=True) == ( + "import sys\n" + "import os\n" + "import math\n" + "from collections import defaultdict\n" + "\n" + "import numpy as np\n" + "import pandas as pd\n" + "\n" + "import .views\n" + ) + + # test to ensure that from_imports remain intact with only_sections + test_input = "from foo import b, a, c" + + assert isort.code(test_input, only_sections=True) == test_input diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 0d2738710..6514f7b50 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -41,6 +41,8 @@ def test_parse_args(): assert main.parse_args(["--multi-line", "GRID"]) == {"multi_line_output": WrapModes.GRID} assert main.parse_args(["--dont-order-by-type"]) == {"order_by_type": False} assert main.parse_args(["--dt"]) == {"order_by_type": False} + assert main.parse_args(["--only-sections"]) == {"only_sections": True} + assert main.parse_args(["--os"]) == {"only_sections": True} def test_ascii_art(capsys): From b231c20e4012158b40c71a62523ffb5e30079f5e Mon Sep 17 00:00:00 2001 From: anirudnits Date: Thu, 3 Sep 2020 14:52:59 +0530 Subject: [PATCH 1010/1439] added tests for only_sections options --- tests/unit/test_isort.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_isort.py b/tests/unit/test_isort.py index ffbebf8e8..d14bdda24 100644 --- a/tests/unit/test_isort.py +++ b/tests/unit/test_isort.py @@ -3180,7 +3180,7 @@ def test_argument_parsing() -> None: assert args["force_to_top"] == ["foo"] assert args["skip"] == ["bar"] assert args["files"] == ["baz.py"] - assert args["only_sections"] is False + assert args["only_sections"] is True @pytest.mark.parametrize("multiprocess", (False, True)) @@ -4831,6 +4831,6 @@ def test_only_sections() -> None: ) # test to ensure that from_imports remain intact with only_sections - test_input = "from foo import b, a, c" + test_input = "from foo import b, a, c\n" assert isort.code(test_input, only_sections=True) == test_input From 31f26dae70117a89612593b37d58bdb384d9149b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 3 Sep 2020 02:30:52 -0700 Subject: [PATCH 1011/1439] Add test for desired float_to_top behavior --- tests/unit/test_regressions.py | 168 +++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index 9103270aa..8fc6a96d1 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -714,3 +714,171 @@ def test_isort_doesnt_mangle_code_when_adding_imports_issue_1444(): import os ''' ) + + +def test_isort_doesnt_float_to_top_correctly_when_imports_not_at_top_issue_1382(): + """isort should float existing imports to the top, if they are currently below the top. + See: https://github.com/PyCQA/isort/issues/1382 + """ + assert isort.code(''' +def foo(): + pass + +import a + +def bar(): + pass +''', + float_to_top=True) == '''import a + + +def foo(): + pass + + +def bar(): + pass +''' + + assert isort.code(''' + + + + + + +def foo(): + pass + +import a + +def bar(): + pass +''', + float_to_top=True) == '''import a + + +def foo(): + pass + + +def bar(): + pass +''' + + assert isort.code('''"""My comment + + +""" +def foo(): + pass + +import a + +def bar(): + pass +''', + float_to_top=True) == '''"""My comment + + +""" +import a + + +def foo(): + pass + + +def bar(): + pass +''' + + + assert isort.code(''' +"""My comment + + +""" +def foo(): + pass + +import a + +def bar(): + pass +''', + float_to_top=True) == ''' +"""My comment + + +""" +import a + + +def foo(): + pass + + +def bar(): + pass +''' + + assert isort.code('''#!/bin/bash +"""My comment + + +""" +def foo(): + pass + +import a + +def bar(): + pass +''', + float_to_top=True) == '''#!/bin/bash +"""My comment + + +""" +import a + + +def foo(): + pass + + +def bar(): + pass +''' + + assert isort.code('''#!/bin/bash + +"""My comment + + +""" +def foo(): + pass + +import a + +def bar(): + pass +''', + float_to_top=True) == '''#!/bin/bash + +"""My comment + + +""" +import a + + +def foo(): + pass + + +def bar(): + pass +''' From e2b2f8d0cab23dc32b9cae25de8e9d3f10b027aa Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 3 Sep 2020 02:31:21 -0700 Subject: [PATCH 1012/1439] Resolve #1382: float to top --- isort/parse.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/isort/parse.py b/isort/parse.py index 980924f17..e15caf3e2 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -200,6 +200,10 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte if skipping_line: out_lines.append(line) continue + elif config.float_to_top and import_index == -1 and line and not in_quote and not line.strip().startswith("#"): + import_index = index - 1 + while import_index and not in_lines[import_index - 1]: + import_index -= 1 line, *end_of_line_comment = line.split("#", 1) if ";" in line: From 2c84fc2b23592defc698dcea3d39fc76f92fcb8a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 3 Sep 2020 02:39:29 -0700 Subject: [PATCH 1013/1439] Formatting with black + isort --- isort/parse.py | 8 +++- tests/unit/test_regressions.py | 63 ++++++++++++++++++++-------- tests/unit/test_ticketed_features.py | 9 ++-- 3 files changed, 56 insertions(+), 24 deletions(-) diff --git a/isort/parse.py b/isort/parse.py index e15caf3e2..613f7fa76 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -200,7 +200,13 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte if skipping_line: out_lines.append(line) continue - elif config.float_to_top and import_index == -1 and line and not in_quote and not line.strip().startswith("#"): + elif ( + config.float_to_top + and import_index == -1 + and line + and not in_quote + and not line.strip().startswith("#") + ): import_index = index - 1 while import_index and not in_lines[import_index - 1]: import_index -= 1 diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index 8fc6a96d1..a6e377ea5 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -720,7 +720,9 @@ def test_isort_doesnt_float_to_top_correctly_when_imports_not_at_top_issue_1382( """isort should float existing imports to the top, if they are currently below the top. See: https://github.com/PyCQA/isort/issues/1382 """ - assert isort.code(''' + assert ( + isort.code( + """ def foo(): pass @@ -728,8 +730,10 @@ def foo(): def bar(): pass -''', - float_to_top=True) == '''import a +""", + float_to_top=True, + ) + == """import a def foo(): @@ -738,9 +742,12 @@ def foo(): def bar(): pass -''' +""" + ) - assert isort.code(''' + assert ( + isort.code( + """ @@ -754,8 +761,10 @@ def foo(): def bar(): pass -''', - float_to_top=True) == '''import a +""", + float_to_top=True, + ) + == """import a def foo(): @@ -764,9 +773,12 @@ def foo(): def bar(): pass -''' +""" + ) - assert isort.code('''"""My comment + assert ( + isort.code( + '''"""My comment """ @@ -778,7 +790,9 @@ def foo(): def bar(): pass ''', - float_to_top=True) == '''"""My comment + float_to_top=True, + ) + == '''"""My comment """ @@ -792,9 +806,11 @@ def foo(): def bar(): pass ''' + ) - - assert isort.code(''' + assert ( + isort.code( + ''' """My comment @@ -807,7 +823,9 @@ def foo(): def bar(): pass ''', - float_to_top=True) == ''' + float_to_top=True, + ) + == ''' """My comment @@ -822,8 +840,11 @@ def foo(): def bar(): pass ''' + ) - assert isort.code('''#!/bin/bash + assert ( + isort.code( + '''#!/bin/bash """My comment @@ -836,7 +857,9 @@ def foo(): def bar(): pass ''', - float_to_top=True) == '''#!/bin/bash + float_to_top=True, + ) + == '''#!/bin/bash """My comment @@ -851,8 +874,11 @@ def foo(): def bar(): pass ''' + ) - assert isort.code('''#!/bin/bash + assert ( + isort.code( + '''#!/bin/bash """My comment @@ -866,7 +892,9 @@ def foo(): def bar(): pass ''', - float_to_top=True) == '''#!/bin/bash + float_to_top=True, + ) + == '''#!/bin/bash """My comment @@ -882,3 +910,4 @@ def foo(): def bar(): pass ''' + ) diff --git a/tests/unit/test_ticketed_features.py b/tests/unit/test_ticketed_features.py index b387ec63e..b20ac10df 100644 --- a/tests/unit/test_ticketed_features.py +++ b/tests/unit/test_ticketed_features.py @@ -60,8 +60,7 @@ def my_function_2(): """, float_to_top=True, ) - == """ -import os + == """import os import sys @@ -91,8 +90,7 @@ def my_function_2(): """, float_to_top=True, ) - == """ -import os + == """import os def my_function_1(): @@ -133,8 +131,7 @@ def my_function_2(): """, float_to_top=True, ) - == """ -import os + == """import os def my_function_1(): From 81e18d33a5704c0eb8cae7005bdf0bcb9f06a9bf Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 3 Sep 2020 03:16:39 -0700 Subject: [PATCH 1014/1439] Stash current progress --- isort/place.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/isort/place.py b/isort/place.py index 34b2eeb86..8e3e880fb 100644 --- a/isort/place.py +++ b/isort/place.py @@ -61,7 +61,39 @@ def _known_pattern(name: str, config: Config) -> Optional[Tuple[str, str]]: def _src_path(name: str, config: Config) -> Optional[Tuple[str, str]]: + search_paths = zip(config.src_paths, config.src_paths) + for index, module_part in enumerate(name.split(".")): + if index == len(module_parts): + for search_path, src_path in search_paths: + module_path = (search_path / module_part).resolve() + if ( + _is_module(module_path) + or _is_package(module_path) + or _src_path_is_module(search_path, module_part) + ): + return (sections.FIRSTPARTY, f"Found in one of the configured src_paths: {src_path}.") + else: + new_search_paths = [] + for search_path, src_path in search_paths: + if + + search_paths = [search_path for search_path in search_paths if + _is_package((src_path / root_module_name).resolve()) or + index == 0 and _src_path_is_module + + for src_path in config.src_paths: + root_module_name = name.split(".")[0] + module_path = + if ( + or + or _src_path_is_module(src_path, root_module_name) + ): + return (sections.FIRSTPARTY, f"Found in one of the configured src_paths: {src_path}.") + + for src_path in config.src_paths: + + for part in root_module_name = name.split(".")[0] module_path = (src_path / root_module_name).resolve() if ( From f8888b33504c085bd95eb1ee5bff12d29a85f583 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 3 Sep 2020 03:39:03 -0700 Subject: [PATCH 1015/1439] Bump version to 5.5.0 --- CHANGELOG.md | 22 ++++++++++++++++++++-- isort/_version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d23d02fd2..8b9dca54c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,15 +2,33 @@ Changelog ========= NOTE: isort follows the [semver](https://semver.org/) versioning standard. +Find out more about isort's release policy [here](https://pycqa.github.io/isort/docs/major_releases/release_policy/). -### 5.5.0 TBD +### 5.5.0 September 3, 2020 - Fixed #1398: isort: off comment doesn't work, if it's the top comment in the file. - Fixed #1395: reverse_relative setting doesn't have any effect when combined with force_sort_within_sections. - Fixed #1399: --skip can error in the case of projects that contain recursive symlinks. - Fixed #1389: ensure_newline_before_comments doesn't work if comment is at top of section and sections don't have lines between them. - Fixed #1396: comments in imports with ";" can keep isort from recognizing import line. + - Fixed #1380: As imports removed when `combine_star` is set. + - Fixed #1382: --float-to-top has no effect if no import is already at the top. + - Fixed #1420: isort never settles on module docstring + add import. + - Fixed #1421: Error raised when repo contains circular symlinks. + - Fixed #1427: noqa comment is moved from star import to constant import. + - Fixed #1444 & 1445: Incorrect placement of import additions. + - Fixed #1447: isort5 throws error when stdin used on Windows with deprecated args. - Implemented #1397: Added support for specifying config file when using git hook (thanks @diseraluca!). - + - Implemented #1405: Added support for coloring diff output. + - Implemented #1434: New multi-line grid mode without parentheses. + +Goal Zero (Tickets related to aspirational goal of achieving 0 regressions for remaining 5.0.0 lifespan): + - Implemented #1392: Extensive profile testing. + - Implemented #1393: Proprety based testing applied to code snippets. + - Implemented #1391: Create automated integration test that includes full code base of largest OpenSource isort users. + +Potentially breaking changes: + - Fixed #1429: --check doesn't print to stderr as the documentation says. This means if you were looking for `ERROR:` messages for files that contain incorrect imports within stdout you will now need to look in stderr. + ### 5.4.2 Aug 14, 2020 - Fixed #1383: Known other does not work anymore with .editorconfig. - Fixed: Regression in first known party path expansion. diff --git a/isort/_version.py b/isort/_version.py index cfda0f8e3..016ab6b2b 100644 --- a/isort/_version.py +++ b/isort/_version.py @@ -1 +1 @@ -__version__ = "5.4.2" +__version__ = "5.5.0" diff --git a/pyproject.toml b/pyproject.toml index 0de43c7a9..7cdcba27a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ line-length = 100 [tool.poetry] name = "isort" -version = "5.4.2" +version = "5.5.0" description = "A Python utility / library to sort Python imports." authors = ["Timothy Crosley "] license = "MIT" From c1a76c5deb7bc30ae00cd4b4e921d3c12b081e8a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 3 Sep 2020 03:52:57 -0700 Subject: [PATCH 1016/1439] =?UTF-8?q?Add=20Mat=C4=9Bj=20Nikl=20(@MatejNikl?= =?UTF-8?q?)=20to=20acknowledgement=20list=20for=20his=20help=20uncovering?= =?UTF-8?q?=20serious=20bugs=20related=20to=20add=20imports=20functionalit?= =?UTF-8?q?y?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/contributing/4.-acknowledgements.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index 50511816b..7e560093b 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -17,6 +17,7 @@ Notable Bug Reporters - @OddBloke - Martin Geisler (@mgeisler) - Tim Heap (@timheap) +- Matěj Nikl (@MatejNikl) Code Contributors =================== From ff0d4365d8f66e7c0f4b2c4994cd05645b41934f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 3 Sep 2020 03:54:06 -0700 Subject: [PATCH 1017/1439] Add new contributors from 5.5.0 release --- docs/contributing/4.-acknowledgements.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index 7e560093b..db3fde562 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -189,7 +189,11 @@ Code Contributors - Grzegorz Pstrucha (@Gricha) - Zac Hatfield-Dodds (@Zac-HD) - Jiří Škorpil (@JiriSko) - +- James Winegar (@jameswinegar) +- Abdullah Dursun (@adursun) +- Guillaume Lostis (@glostis) +- Krzysztof Jagiełło (@kjagiello) +- Nicholas Devenish (@ndevenish) Documenters =================== From aaad03bc584074e010939020fa868e789aef70ef Mon Sep 17 00:00:00 2001 From: anirudnits Date: Thu, 3 Sep 2020 21:36:56 +0530 Subject: [PATCH 1018/1439] added the logic for bypassing sorting of imports within sections when only_sections is True --- isort/output.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/isort/output.py b/isort/output.py index 015252085..d7a14009a 100644 --- a/isort/output.py +++ b/isort/output.py @@ -234,7 +234,11 @@ def _with_from_imports( from_imports = sorting.naturally( from_imports, key=lambda key: sorting.module_key( - key, config, True, ignore_case, section_name=section, + key, + config, + True, + ignore_case, + section_name=section, ), ) if remove_imports: From 8cf525cff8a99d4c2a7ca6a2c8afcb49e8658c58 Mon Sep 17 00:00:00 2001 From: anirudnits Date: Thu, 3 Sep 2020 21:37:31 +0530 Subject: [PATCH 1019/1439] added tests for only_section --- tests/unit/test_isort.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/unit/test_isort.py b/tests/unit/test_isort.py index d14bdda24..0eb0c322d 100644 --- a/tests/unit/test_isort.py +++ b/tests/unit/test_isort.py @@ -2940,12 +2940,15 @@ def test_not_splitted_sections() -> None: ) # in case when THIRDPARTY section is excluded from sections list, # it's ok to merge STDLIB and FIRSTPARTY - assert isort.code( - code=test_input, - sections=["STDLIB", "FIRSTPARTY", "LOCALFOLDER"], - no_lines_before=["FIRSTPARTY"], - known_first_party=["app"], - ) == (stdlib_section + firstparty_section + whiteline + local_section + whiteline + statement) + assert ( + isort.code( + code=test_input, + sections=["STDLIB", "FIRSTPARTY", "LOCALFOLDER"], + no_lines_before=["FIRSTPARTY"], + known_first_party=["app"], + ) + == (stdlib_section + firstparty_section + whiteline + local_section + whiteline + statement) + ) # it doesn't change output, because stdlib packages don't have any whitelines before them assert ( isort.code(test_input, no_lines_before=["STDLIB"], known_first_party=["app"]) == test_input From 0c34b9845434bd18b3abc31364242405de74da74 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 3 Sep 2020 17:18:32 -0700 Subject: [PATCH 1020/1439] Resolve #1453: isort error when float to top on almost empty file. Ensure errors return back file name when they occur. --- isort/core.py | 2 +- isort/main.py | 9 +++++++++ tests/unit/test_main.py | 11 +++++++++++ tests/unit/test_regressions.py | 15 +++++++++++++++ 4 files changed, 36 insertions(+), 1 deletion(-) diff --git a/isort/core.py b/isort/core.py index 46900e02b..08adc239c 100644 --- a/isort/core.py +++ b/isort/core.py @@ -86,7 +86,7 @@ def process( if current: parsed = parse.file_contents(current, config=config) extra_space = "" - while current[-1] == "\n": + while current and current[-1] == "\n": extra_space += "\n" current = current[:-1] extra_space = extra_space.replace("\n", "", 1) diff --git a/isort/main.py b/isort/main.py index 990e12184..e7dc61ad5 100644 --- a/isort/main.py +++ b/isort/main.py @@ -11,6 +11,7 @@ from . import __version__, api, sections from .exceptions import FileSkipped +from .format import create_terminal_printer from .logo import ASCII_ART from .profiles import profiles from .settings import VALID_PY_TARGETS, Config, WrapModes @@ -103,6 +104,14 @@ def sort_imports( except (OSError, ValueError) as error: warn(f"Unable to parse file {file_name} due to {error}") return None + except Exception: + printer = create_terminal_printer(color=config.color_output) + printer.error( + f"Unrecoverable exception thrown when parsing {file_name}! " + "This should NEVER happen.\n" + "If encountered, please open an issue: https://github.com/PyCQA/isort/issues/new" + ) + raise def iter_source_code(paths: Iterable[str], config: Config, skipped: List[str]) -> Iterator[str]: diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 3b7dd7689..ae63fd607 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -35,6 +35,17 @@ def test_sort_imports(tmpdir): assert main.sort_imports(str(tmp_file), config=skip_config, disregard_skip=False).skipped +def test_sort_imports_error_handling(tmpdir, mocker, capsys): + tmp_file = tmpdir.join("file.py") + tmp_file.write("import os, sys\n") + mocker.patch("isort.core.process").side_effect = IndexError("Example unhandled exception") + with pytest.raises(IndexError): + main.sort_imports(str(tmp_file), DEFAULT_CONFIG, check=True).incorrectly_sorted + + out, error = capsys.readouterr() + assert "Unrecoverable exception thrown when parsing" in error + + def test_parse_args(): assert main.parse_args([]) == {} assert main.parse_args(["--multi-line", "1"]) == {"multi_line_output": WrapModes.VERTICAL} diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index a6e377ea5..5feaef6fb 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -911,3 +911,18 @@ def bar(): pass ''' ) + + +def test_empty_float_to_top_shouldnt_error_issue_1453(): + """isort shouldn't error when float to top is set with a mostly empty file""" + assert isort.check_code( + """ +""", + show_diff=True, + float_to_top=True, + ) + assert isort.check_code( + """ +""", + show_diff=True, + ) From 6ac179a14662f606530ecc46965b52cfc66f6ece Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 3 Sep 2020 20:36:03 -0700 Subject: [PATCH 1021/1439] Fix issue #1454: Endless import sorting for indendent imports with section heading and preceding comment. --- isort/core.py | 15 +++++++++------ tests/unit/test_regressions.py | 23 +++++++++++++++++++++++ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/isort/core.py b/isort/core.py index 08adc239c..f3197b99d 100644 --- a/isort/core.py +++ b/isort/core.py @@ -58,7 +58,6 @@ def process( contains_imports: bool = False in_top_comment: bool = False first_import_section: bool = True - section_comments = [f"# {heading}" for heading in config.import_headings.values()] indent: str = "" isort_off: bool = False code_sorting: Union[bool, str] = False @@ -146,11 +145,11 @@ def process( if ( (index == 0 or (index in (1, 2) and not contains_imports)) and stripped_line.startswith("#") - and stripped_line not in section_comments + and stripped_line not in config.section_comments ): in_top_comment = True elif in_top_comment: - if not line.startswith("#") or stripped_line in section_comments: + if not line.startswith("#") or stripped_line in config.section_comments: in_top_comment = False first_comment_index_end = index - 1 @@ -210,8 +209,13 @@ def process( else: code_sorting_section += line line = "" - elif stripped_line in config.section_comments and not import_section: - import_section += line + elif stripped_line in config.section_comments: + if import_section and not contains_imports: + output_stream.write(import_section) + import_section = line + not_imports = False + else: + import_section += line indent = line[: -len(line.lstrip())] elif not (stripped_line or contains_imports): not_imports = True @@ -337,7 +341,6 @@ def process( line_separator=line_separator, ignore_whitespace=config.ignore_whitespace, ) - output_stream.write(sorted_import_section) if not line and not indent and next_import_section: output_stream.write(line_separator) diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index 5feaef6fb..752adc343 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -926,3 +926,26 @@ def test_empty_float_to_top_shouldnt_error_issue_1453(): """, show_diff=True, ) + + +def test_import_sorting_shouldnt_be_endless_with_headers_issue_1454(): + """isort should never enter an endless sorting loop. + See: https://github.com/PyCQA/isort/issues/1454 + """ + assert isort.check_code( + """ + +# standard library imports +import sys + +try: + # Comment about local lib + # related third party imports + from local_lib import stuff +except ImportError as e: + pass +""", + known_third_party=["local_lib"], + import_heading_thirdparty="related third party imports", + show_diff=True, + ) From 2d823d24e8f0b280b1d1b3a60e088627d40e7abb Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 4 Sep 2020 04:10:43 -0700 Subject: [PATCH 1022/1439] Resolve #1460: Exclude svn and bzr repo files by default --- isort/settings.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/isort/settings.py b/isort/settings.py index 1e10ab60e..94a14acf2 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -54,11 +54,14 @@ ".hg", ".mypy_cache", ".nox", + ".svn", + ".bzr", "_build", "buck-out", "build", "dist", ".pants.d", + ".direnv", "node_modules", } ) From 434abbf8e69767aae363995367c3621867995a2c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 4 Sep 2020 05:52:18 -0700 Subject: [PATCH 1023/1439] Formatting --- isort/wrap.py | 14 ++++-- tests/unit/test_ticketed_features.py | 67 ++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 3 deletions(-) diff --git a/isort/wrap.py b/isort/wrap.py index 872b096e7..11542fa07 100644 --- a/isort/wrap.py +++ b/isort/wrap.py @@ -75,11 +75,13 @@ def line(content: str, line_separator: str, config: Config = DEFAULT_CONFIG) -> splitter ): line_parts = re.split(exp, line_without_comment) - if comment: + if comment and not (config.use_parentheses and "noqa" in comment): _comma_maybe = ( "," if (config.include_trailing_comma and config.use_parentheses) else "" ) - line_parts[-1] = f"{line_parts[-1].strip()}{_comma_maybe} #{comment}" + line_parts[ + -1 + ] = f"{line_parts[-1].strip()}{_comma_maybe}{config.comment_prefix}{comment}" next_line = [] while (len(content) + 2) > ( config.wrap_length or config.line_length @@ -104,8 +106,14 @@ def line(content: str, line_separator: str, config: Config = DEFAULT_CONFIG) -> _separator = line_separator else: _separator = "" + _comment = "" + if comment and "noqa" in comment: + _comment = f"{config.comment_prefix}{comment}" + cont_line = cont_line.rstrip() + _comma = "," if config.include_trailing_comma else "" output = ( - f"{content}{splitter}({line_separator}{cont_line}{_comma}{_separator})" + f"{content}{splitter}({_comment}" + f"{line_separator}{cont_line}{_comma}{_separator})" ) lines = output.split(line_separator) if config.comment_prefix in lines[-1] and lines[-1].endswith(")"): diff --git a/tests/unit/test_ticketed_features.py b/tests/unit/test_ticketed_features.py index b20ac10df..dc5c77814 100644 --- a/tests/unit/test_ticketed_features.py +++ b/tests/unit/test_ticketed_features.py @@ -590,3 +590,70 @@ def test_isort_support_custom_groups_above_stdlib_that_contain_stdlib_modules_is no_lines_before=["TYPING"], show_diff=True, ) + + +def test_isort_intelligently_places_noqa_comments_issue_1456(): + assert isort.check_code( + """ +from my.horribly.long.import.line.that.just.keeps.on.going.and.going.and.going import ( # noqa + my_symbol, +) +""", + force_single_line=True, + show_diff=True, + multi_line_output=3, + include_trailing_comma=True, + force_grid_wrap=0, + use_parentheses=True, + line_length=79, + ) + + assert isort.check_code( + """ +from my.horribly.long.import.line.that.just.keeps.on.going.and.going.and.going import ( + my_symbol, +) +""", + force_single_line=True, + show_diff=True, + multi_line_output=3, + include_trailing_comma=True, + force_grid_wrap=0, + use_parentheses=True, + line_length=79, + ) + + assert isort.check_code( + """ +from my.horribly.long.import.line.that.just.keeps.on.going.and.going.and.going import ( # noqa + my_symbol +) +""", + force_single_line=True, + use_parentheses=True, + multi_line_output=3, + line_length=79, + show_diff=True, + ) + + assert isort.check_code( + """ +from my.horribly.long.import.line.that.just.keeps.on.going.and.going.and.going import ( + my_symbol +) +""", + force_single_line=True, + use_parentheses=True, + multi_line_output=3, + line_length=79, + show_diff=True, + ) + + # see: https://github.com/PyCQA/isort/issues/1415 + assert isort.check_code( + "from dials.test.algorithms.spot_prediction." + "test_scan_static_reflection_predictor import ( # noqa: F401\n" + " data as static_test,\n)\n", + profile="black", + show_diff=True, + ) From 6a3cc7646d8f54ba0d1b90958e4107bfb1b31e76 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 4 Sep 2020 06:11:34 -0700 Subject: [PATCH 1024/1439] Bump version to 5.5.1 --- CHANGELOG.md | 6 ++++++ isort/_version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b9dca54c..3c504a2fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. Find out more about isort's release policy [here](https://pycqa.github.io/isort/docs/major_releases/release_policy/). +### 5.5.1 September 4, 2020 + - Fixed #1454: Ensure indented import sections with import heading and a preceding comment don't cause import sorting loops. + - Fixed #1453: isort error when float to top on almost empty file. + - Fixed #1456 and #1415: noqa comment moved to where flake8 cant see it. + - Fixed #1460: .svn missing from default ignore list. + ### 5.5.0 September 3, 2020 - Fixed #1398: isort: off comment doesn't work, if it's the top comment in the file. - Fixed #1395: reverse_relative setting doesn't have any effect when combined with force_sort_within_sections. diff --git a/isort/_version.py b/isort/_version.py index 016ab6b2b..f680f9ffe 100644 --- a/isort/_version.py +++ b/isort/_version.py @@ -1 +1 @@ -__version__ = "5.5.0" +__version__ = "5.5.1" diff --git a/pyproject.toml b/pyproject.toml index 7cdcba27a..3aff16105 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ line-length = 100 [tool.poetry] name = "isort" -version = "5.5.0" +version = "5.5.1" description = "A Python utility / library to sort Python imports." authors = ["Timothy Crosley "] license = "MIT" From 80cc94e3de50f5dbbf0352890f832aa6dd41fa36 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 5 Sep 2020 04:34:31 -0700 Subject: [PATCH 1025/1439] Fixed #1463: Better interactive documentation for future option. --- CHANGELOG.md | 3 +++ isort/main.py | 7 ++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c504a2fb..2bb54d3cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. Find out more about isort's release policy [here](https://pycqa.github.io/isort/docs/major_releases/release_policy/). +### 5.6.0 TBD + - Fixed #1463: Better interactive documentation for future option. + ### 5.5.1 September 4, 2020 - Fixed #1454: Ensure indented import sections with import heading and a preceding comment don't cause import sorting loops. - Fixed #1453: isort error when float to top on almost empty file. diff --git a/isort/main.py b/isort/main.py index e7dc61ad5..3d9283173 100644 --- a/isort/main.py +++ b/isort/main.py @@ -267,7 +267,12 @@ def _build_arg_parser() -> argparse.ArgumentParser: "--future", dest="known_future_library", action="append", - help="Force isort to recognize a module as part of the future compatibility libraries.", + help="Force isort to recognize a module as part of Python's internal future compatibility " + "libraries. WARNING: this overrides the behavior of __future__ handling and therefore" + " can result in code that can't execute. If you're looking to add dependencies such " + "as six a better option is to create a another section below --future using custom " + "sections. See: https://github.com/PyCQA/isort#custom-sections-and-ordering and the " + "discussion here: https://github.com/PyCQA/isort/issues/1463.", ) parser.add_argument( "--fas", From 8971fe4fc2e4344b6658acf81aacad8547be49b6 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 5 Sep 2020 06:03:05 -0700 Subject: [PATCH 1026/1439] Reachieve 100% unit test coverage --- tests/unit/test_isort.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/tests/unit/test_isort.py b/tests/unit/test_isort.py index 0eb0c322d..d86156ba8 100644 --- a/tests/unit/test_isort.py +++ b/tests/unit/test_isort.py @@ -4806,13 +4806,14 @@ def test_deprecated_settings(): def test_only_sections() -> None: - # test to ensure that the within sections relative position of imports are maintained + """Test to ensure that the within sections relative position of imports are maintained""" test_input = ( "import sys\n" "\n" "import numpy as np\n" "\n" "import os\n" + "from os import path as ospath\n" "\n" "import pandas as pd\n" "\n" @@ -4821,16 +4822,21 @@ def test_only_sections() -> None: "from collections import defaultdict\n" ) - assert isort.code(test_input, only_sections=True) == ( - "import sys\n" - "import os\n" - "import math\n" - "from collections import defaultdict\n" - "\n" - "import numpy as np\n" - "import pandas as pd\n" - "\n" - "import .views\n" + assert ( + isort.code(test_input, only_sections=True) + == ( + "import sys\n" + "import os\n" + "import math\n" + "from os import path as ospath\n" + "from collections import defaultdict\n" + "\n" + "import numpy as np\n" + "import pandas as pd\n" + "\n" + "import .views\n" + ) + == isort.code(test_input, only_sections=True, force_single_line=True) ) # test to ensure that from_imports remain intact with only_sections From 5c2362bc4c873aaa6f9e5aa5cd96ee12bc704e2d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 5 Sep 2020 06:05:04 -0700 Subject: [PATCH 1027/1439] Fixed #1461: Quiet config option not respected by file API in some circumstances. --- CHANGELOG.md | 1 + isort/api.py | 3 +- tests/unit/test_ticketed_features.py | 62 ++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bb54d3cb..53b30a0e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ ### 5.6.0 TBD - Fixed #1463: Better interactive documentation for future option. + - Fixed #1461: Quiet config option not respected by file API in some circumstances. ### 5.5.1 September 4, 2020 - Fixed #1454: Ensure indented import sections with import heading and a preceding comment don't cause import sorting loops. diff --git a/isort/api.py b/isort/api.py index cbcc3e6ea..f59bc6d91 100644 --- a/isort/api.py +++ b/isort/api.py @@ -299,6 +299,7 @@ def sort_file( """ with io.File.read(filename) as source_file: actual_file_path = file_path or source_file.path + config = _config(path=actual_file_path, config=config, **config_kwargs) changed: bool = False try: if write_to_stdout: @@ -309,7 +310,6 @@ def sort_file( file_path=actual_file_path, disregard_skip=disregard_skip, extension=extension, - **config_kwargs, ) else: tmp_file = source_file.path.with_suffix(source_file.path.suffix + ".isorted") @@ -325,7 +325,6 @@ def sort_file( file_path=actual_file_path, disregard_skip=disregard_skip, extension=extension, - **config_kwargs, ) if changed: if show_diff or ask_to_apply: diff --git a/tests/unit/test_ticketed_features.py b/tests/unit/test_ticketed_features.py index dc5c77814..2fd9073eb 100644 --- a/tests/unit/test_ticketed_features.py +++ b/tests/unit/test_ticketed_features.py @@ -657,3 +657,65 @@ def test_isort_intelligently_places_noqa_comments_issue_1456(): profile="black", show_diff=True, ) + + +def test_isort_respects_quiet_from_sort_file_api_see_1461(capsys, tmpdir): + """Test to ensure isort respects the quiet API parameter when passed in via the API. + See: https://github.com/PyCQA/isort/issues/1461. + """ + settings_file = tmpdir.join(".isort.cfg") + custom_settings_file = tmpdir.join(".custom.isort.cfg") + tmp_file = tmpdir.join("file.py") + tmp_file.write("import b\nimport a\n") + isort.file(tmp_file) + + out, error = capsys.readouterr() + assert not error + assert "Fixing" in out + + # When passed in directly as a setting override + tmp_file.write("import b\nimport a\n") + isort.file(tmp_file, quiet=True) + out, error = capsys.readouterr() + assert not error + assert not out + + # Present in an automatically loaded configuration file + isort.settings._find_config.cache_clear() + settings_file.write( + """ +[isort] +quiet = true +""" + ) + tmp_file.write("import b\nimport a\n") + isort.file(tmp_file) + out, error = capsys.readouterr() + assert not error + assert not out + + # In a custom configuration file + settings_file.write( + """ +[isort] +quiet = false +""" + ) + custom_settings_file.write( + """ +[isort] +quiet = true +""" + ) + tmp_file.write("import b\nimport a\n") + isort.file(tmp_file, settings_file=str(custom_settings_file)) + out, error = capsys.readouterr() + assert not error + assert not out + + # Reused configuration object + custom_config = Config(settings_file=str(custom_settings_file)) + isort.file(tmp_file, config=custom_config) + out, error = capsys.readouterr() + assert not error + assert not out From 28872fafb0a2d47a9ab3b308e0df04786319b314 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 5 Sep 2020 07:05:31 -0700 Subject: [PATCH 1028/1439] Implemented #1433: Provide helpful feedback in case a custom config file is specified without a configuration. --- CHANGELOG.md | 1 + docs/configuration/config_files.md | 9 +++++++++ isort/settings.py | 8 ++++++++ tests/unit/test_ticketed_features.py | 27 +++++++++++++++++++++++++++ 4 files changed, 45 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53b30a0e3..bcd90378a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. Find out more about isort's release policy [here](https://pycqa.github.io/isort/docs/major_releases/release_policy/). ### 5.6.0 TBD + - Implemented #1433: Provide helpful feedback in case a custom config file is specified without a configuration. - Fixed #1463: Better interactive documentation for future option. - Fixed #1461: Quiet config option not respected by file API in some circumstances. diff --git a/docs/configuration/config_files.md b/docs/configuration/config_files.md index a3d983c2b..8ca265ae3 100644 --- a/docs/configuration/config_files.md +++ b/docs/configuration/config_files.md @@ -75,3 +75,12 @@ indent_size = 4 skip = build,.tox,venv src_paths=isort,test ``` + +## Custom config files + +Optionally, you can also create a config file with a custom name, or directly point isort to a config file that falls lower in the priority order, by using [--settings-file](https://pycqa.github.io/isort/docs/configuration/options/#settings-path). +This can be useful, for instance, if you want to have one configuration for `.py` files and another for `.pyx` - while keeping the config files at the root of your repository. + +!!! tip + Custom config files should place their configuration options inside an `[isort]` section and never a generic `[settings]` section. This is because isort can't know for sure + how other tools are utilizing the config file. diff --git a/isort/settings.py b/isort/settings.py index c9fd69173..1a0a1fc14 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -269,6 +269,14 @@ def __init__( CONFIG_SECTIONS.get(os.path.basename(settings_file), FALLBACK_CONFIG_SECTIONS), ) project_root = os.path.dirname(settings_file) + if not config_settings: + warn( + f"A custom settings file was specified: {settings_file} but no configuration " + "was found inside. This can happen when [settings] is used as the config " + "header instead of [isort]. " + "See: https://pycqa.github.io/isort/docs/configuration/config_files" + "/#custom_config_files for more information." + ) elif settings_path: if not os.path.exists(settings_path): raise InvalidSettingsPath(settings_path) diff --git a/tests/unit/test_ticketed_features.py b/tests/unit/test_ticketed_features.py index 2fd9073eb..75b43da89 100644 --- a/tests/unit/test_ticketed_features.py +++ b/tests/unit/test_ticketed_features.py @@ -719,3 +719,30 @@ def test_isort_respects_quiet_from_sort_file_api_see_1461(capsys, tmpdir): out, error = capsys.readouterr() assert not error assert not out + + +def test_isort_should_warn_on_empty_custom_config_issue_1433(tmpdir): + """Feedback should be provided when a user provides a custom settings file that has no + discoverable configuration. + See: https://github.com/PyCQA/isort/issues/1433 + """ + settings_file = tmpdir.join(".custom.cfg") + settings_file.write( + """ +[settings] +quiet = true +""" + ) + with pytest.warns(UserWarning): + assert not Config(settings_file=str(settings_file)).quiet + + isort.settings._get_config_data.cache_clear() + settings_file.write( + """ +[isort] +quiet = true +""" + ) + with pytest.warns(None) as warning: + assert Config(settings_file=str(settings_file)).quiet + assert not warning From a8d23a3c91e679499e556c939491e2b200cd268b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 5 Sep 2020 07:17:48 -0700 Subject: [PATCH 1029/1439] Resolve #1384: add documentation for how relative config file search is done --- docs/configuration/config_files.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/configuration/config_files.md b/docs/configuration/config_files.md index 8ca265ae3..7cda9cd35 100644 --- a/docs/configuration/config_files.md +++ b/docs/configuration/config_files.md @@ -1,11 +1,13 @@ Supported Config Files ======== -isort supports a variety of standard config formats, to allow customizations to easily be integrated into any project. +isort supports various standard config formats to allow customizations to be integrated into any project quickly. When applying configurations, isort looks for the closest supported config file, in the order files are listed below. -You can manually specify the settings file or path by setting `--settings-path` from the commandline, otherwise isort will +You can manually specify the settings file or path by setting `--settings-path` from the command-line. Otherwise, isort will traverse up to 25 parent directories until it finds a suitable config file. -As soon as it finds a file, it stops looking. isort **never** merges config files together due to the confusion it can cause. +As soon as it finds a file, it stops looking. The config file search is done relative to the current directory if `isort .` +or a file stream is passed in, or relative to the first path passed in if multiple paths are passed in. +isort **never** merges config files together due to the confusion it can cause. !!! tip You can always introspect the configuration settings isort determined, and find out which config file it picked up, by running `isort . --show-config` From d842244c5365403882af64fc6f20b7d76b16cde8 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 7 Sep 2020 20:40:12 -0700 Subject: [PATCH 1030/1439] Fix deep source link #1449 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6bd080430..0e1f3ce93 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ [![Downloads](https://pepy.tech/badge/isort)](https://pepy.tech/project/isort) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/) -[![DeepSource](https://static.deepsource.io/deepsource-badge-light-mini.svg)](https://deepsource.io/gh/timothycrosley/isort/?ref=repository-badge) +[![DeepSource](https://static.deepsource.io/deepsource-badge-light-mini.svg)](https://deepsource.io/gh/pycqa/isort/?ref=repository-badge) _________________ [Read Latest Documentation](https://pycqa.github.io/isort/) - [Browse GitHub Code Repository](https://github.com/pycqa/isort/) From 14a7e372f555da37973fb2e4578f6a7861297313 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 8 Sep 2020 23:26:09 -0700 Subject: [PATCH 1031/1439] Attempt to fix #1466: Include given example config, ensure deadline setting is turned off --- .isort.cfg | 2 +- tests/integration/test_setting_combinations.py | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.isort.cfg b/.isort.cfg index 567d1abd6..60ece5b8a 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -1,4 +1,4 @@ [settings] profile=hug -src_paths=isort,test +src_paths= skip=tests/unit/example_crlf_file.py diff --git a/tests/integration/test_setting_combinations.py b/tests/integration/test_setting_combinations.py index 7e236f9f9..ed693cd19 100644 --- a/tests/integration/test_setting_combinations.py +++ b/tests/integration/test_setting_combinations.py @@ -153,10 +153,15 @@ def _raise(*a): ] +@hypothesis.example( + config=isort.Config(py_version='all', force_to_top=frozenset(), skip=frozenset({'.svn', '.venv', 'build', 'dist', '.bzr', '.tox', '.hg', '.mypy_cache', '.nox', '_build', 'buck-out', 'node_modules', '.git', '.eggs', '.pants.d', 'venv', '.direnv'}), skip_glob=frozenset(), skip_gitignore=True, line_length=79, wrap_length=0, line_ending='', sections=('FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER'), no_sections=False, known_future_library=frozenset({'__future__'}), known_third_party=frozenset(), known_first_party=frozenset(), known_local_folder=frozenset(), known_standard_library=frozenset({'pwd', 'types', 'nntplib', 'jpeg', 'pyclbr', 'encodings', 'ctypes', 'macerrors', 'filecmp', 'dbm', 'mimetypes', 'statvfs', 'msvcrt', 'spwd', 'codecs', 'SimpleHTTPServer', 'compiler', 'pickletools', 'tkinter', 'pickle', 'fm', 'bsddb', 'contextvars', 'dummy_thread', 'pipes', 'heapq', 'dircache', 'commands', 'unicodedata', 'ntpath', 'marshal', 'fpformat', 'linecache', 'calendar', 'pty', 'MimeWriter', 'inspect', 'mmap', 'ic', 'tty', 'nis', 'new', 'wave', 'HTMLParser', 'anydbm', 'tracemalloc', 'pdb', 'sunau', 'GL', 'parser', 'winsound', 'dbhash', 'zlib', 'MacOS', 'pprint', 'crypt', 'aetools', 'DEVICE', 'fl', 'gettext', 'asyncore', 'copyreg', 'queue', 'resource', 'turtledemo', 'fnmatch', 'hotshot', 'trace', 'string', 'plistlib', 'gzip', 'functools', 'aepack', 'hashlib', 'imp', 'MiniAEFrame', 'getpass', 'shutil', 'ttk', 'multifile', 'operator', 'reprlib', 'subprocess', 'cgi', 'select', 'SimpleXMLRPCServer', 'audioop', 'macresource', 'stringprep', 'wsgiref', 'SUNAUDIODEV', 'atexit', 'lzma', 'asyncio', 'datetime', 'binhex', 'autoGIL', 'doctest', 'thread', 'enum', 'tempfile', 'posixfile', 'mhlib', 'html', 'itertools', 'exceptions', 'sgmllib', 'array', 'test', 'imputil', 'shlex', 'flp', 'uu', 'gdbm', 'urlparse', 'msilib', 'termios', 'modulefinder', 'ossaudiodev', 'timeit', 'binascii', 'popen2', 'ConfigParser', 'poplib', 'zipfile', 'cfmfile', 'pstats', 'AL', 'contextlib', 'code', 'zipimport', 'base64', 'platform', 'ast', 'fileinput', 'locale', 'buildtools', 'stat', 'quopri', 'readline', 'collections', 'aetypes', 'concurrent', 'runpy', 'copy_reg', 'rexec', 'cmath', 'optparse', 'dummy_threading', 'ColorPicker', 'sched', 'netrc', 'sunaudiodev', 'socketserver', 'logging', 'PixMapWrapper', 'sysconfig', 'Nav', 'copy', 'cmd', 'csv', 'chunk', 'multiprocessing', 'warnings', 'weakref', 'py_compile', 'sre', 'sre_parse', 'curses', 'threading', 're', 'FrameWork', '_thread', 'imgfile', 'cd', 'sre_constants', 'xdrlib', 'dataclasses', 'urllib2', 'StringIO', 'configparser', 'importlib', 'UserList', 'posixpath', 'mailbox', 'rfc822', 'grp', 'pydoc', 'sets', 'textwrap', 'numbers', 'W', 'gl', 'htmllib', 'macostools', 'tarfile', 'ipaddress', 'xmlrpc', 'icopen', 'traceback', '_winreg', 'random', 'CGIHTTPServer', 'dis', 'sha', 'selectors', 'statistics', 'DocXMLRPCServer', 'imghdr', 'venv', 'keyword', 'xmlrpclib', 'ftplib', 'getopt', 'posix', 'smtpd', 'profile', 'sndhdr', 'signal', 'EasyDialogs', 'dumbdbm', 'fcntl', 'SocketServer', 'distutils', 'symbol', 'pathlib', 'cStringIO', 'imaplib', 'unittest', 'al', 'cProfile', 'robotparser', 'BaseHTTPServer', 'os', 'pkgutil', 'socket', 'fractions', 'shelve', 'aifc', 'cgitb', 'xml', 'decimal', 'sre_compile', 'ssl', 'user', 'Bastion', 'formatter', 'time', 'abc', 'winreg', 'difflib', 'FL', 'bz2', 'asynchat', 'gc', 'gensuitemodule', 'symtable', 'secrets', 'Carbon', 'mailcap', 'sys', 'bdb', 'fpectl', 'httplib', 'webbrowser', 'smtplib', 'Cookie', 'whichdb', 'turtle', 'tokenize', 'UserString', 'tabnanny', 'site', 'struct', 'codeop', 'email', 'typing', 'cookielib', 'Queue', 'rlcompleter', 'errno', 'macpath', 'videoreader', 'md5', 'cPickle', 'Tix', 'io', 'faulthandler', 'Tkinter', 'glob', 'syslog', 'telnetlib', '_dummy_thread', 'hmac', 'uuid', 'imageop', 'future_builtins', 'json', 'htmlentitydefs', 'lib2to3', 'UserDict', 'mutex', 'sqlite3', 'findertools', 'bisect', 'builtins', 'urllib', 'http', 'compileall', 'argparse', 'ScrolledText', 'token', 'dl', 'applesingle', 'math', 'ensurepip', 'mimify', 'mimetools', 'colorsys', 'zipapp', '__builtin__'}), extra_standard_library=frozenset(), known_other={'other': frozenset({'', '\x10\x1bm'})}, multi_line_output=0, forced_separate=(), indent=' ', comment_prefix=' #', length_sort=True, length_sort_straight=False, length_sort_sections=frozenset(), add_imports=frozenset(), remove_imports=frozenset({'', '\U00076fe7þs\x0c\U000c8b75v\U00106541', '𥒒>\U0001960euj𒎕\x9e', '\x15\x9b', '\x02l', '\U000b71ef.\x1c', '\x7f?\U000ec91c', '\x7f,ÞoÀP8\x1b\x1e»3\x86\x94¤ÁÓ~\U00066b1a,O\U0010ab28\x90«', 'Y\x06ºZ\x04Ýì\U00078ce1.\U0010c1f9[EK\x83EÖø', ';À¨|\x1b 𑐒🍸V'}), append_only=False, reverse_relative=True, force_single_line=False, single_line_exclusions=('Y\U000347d9g\x957K', '', 'Ê\U000e8ad2\U0008fa72ùÏ\x19ç\U000eaecc𤎪.', '·o\U000d00e5\U000b36de+\x8f\U000b5953´\x08oÜ', '', ':sI¶', ''), default_section='THIRDPARTY', import_headings={}, balanced_wrapping=False, use_parentheses=True, order_by_type=True, atomic=False, lines_after_imports=-1, lines_between_sections=1, lines_between_types=0, combine_as_imports=True, combine_star=False, include_trailing_comma=False, from_first=False, verbose=False, quiet=False, force_adds=False, force_alphabetical_sort_within_sections=False, force_alphabetical_sort=False, force_grid_wrap=0, force_sort_within_sections=False, lexicographical=False, ignore_whitespace=False, no_lines_before=frozenset({'uøø', '¢', '&\x8c5Ï\U000e5f01Ø', '\U0005d415\U000a3df2h\U000f24e5\U00104d7b34¹ÒÀ', '\U000e374c8', 'w'}), no_inline_sort=False, ignore_comments=False, case_sensitive=False, sources=({'py_version': 'py3', 'force_to_top': frozenset(), 'skip': frozenset({'.svn', '.venv', 'build', 'dist', '.bzr', '.tox', '.hg', '.mypy_cache', '.nox', '_build', 'buck-out', 'node_modules', '.git', '.eggs', '.pants.d', 'venv', '.direnv'}), 'skip_glob': frozenset(), 'skip_gitignore': False, 'line_length': 79, 'wrap_length': 0, 'line_ending': '', 'sections': ('FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER'), 'no_sections': False, 'known_future_library': frozenset({'__future__'}), 'known_third_party': frozenset(), 'known_first_party': frozenset(), 'known_local_folder': frozenset(), 'known_standard_library': frozenset({'pwd', 'copy', 'cmd', 'csv', 'chunk', 'multiprocessing', 'warnings', 'types', 'weakref', 'nntplib', 'pyclbr', 'encodings', 'py_compile', 'sre', 'ctypes', 'sre_parse', 'filecmp', 'curses', 'threading', 'dbm', 're', '_thread', 'sre_constants', 'xdrlib', 'dataclasses', 'mimetypes', 'configparser', 'importlib', 'msvcrt', 'spwd', 'posixpath', 'mailbox', 'codecs', 'grp', 'pickletools', 'tkinter', 'pickle', 'contextvars', 'pydoc', 'textwrap', 'numbers', 'pipes', 'heapq', 'tarfile', 'unicodedata', 'ntpath', 'ipaddress', 'marshal', 'xmlrpc', 'traceback', 'linecache', 'calendar', 'pty', 'random', 'dis', 'selectors', 'statistics', 'imghdr', 'venv', 'inspect', 'mmap', 'keyword', 'ftplib', 'tty', 'nis', 'getopt', 'posix', 'smtpd', 'wave', 'profile', 'sndhdr', 'signal', 'tracemalloc', 'pdb', 'sunau', 'winsound', 'parser', 'zlib', 'fcntl', 'pprint', 'distutils', 'crypt', 'symbol', 'gettext', 'pathlib', 'asyncore', 'copyreg', 'imaplib', 'unittest', 'queue', 'resource', 'turtledemo', 'fnmatch', 'cProfile', 'os', 'pkgutil', 'socket', 'trace', 'fractions', 'string', 'shelve', 'plistlib', 'aifc', 'gzip', 'functools', 'cgitb', 'xml', 'hashlib', 'decimal', 'imp', 'sre_compile', 'ssl', 'formatter', 'winreg', 'time', 'getpass', 'shutil', 'abc', 'difflib', 'bz2', 'operator', 'reprlib', 'subprocess', 'cgi', 'select', 'asynchat', 'audioop', 'gc', 'secrets', 'symtable', 'mailcap', 'sys', 'bdb', 'fpectl', 'stringprep', 'webbrowser', 'smtplib', 'wsgiref', 'atexit', 'lzma', 'asyncio', 'datetime', 'binhex', 'doctest', 'turtle', 'enum', 'tempfile', 'tokenize', 'tabnanny', 'site', 'html', 'struct', 'itertools', 'codeop', 'email', 'array', 'test', 'typing', 'shlex', 'uu', 'msilib', 'termios', 'rlcompleter', 'modulefinder', 'ossaudiodev', 'timeit', 'binascii', 'poplib', 'errno', 'macpath', 'zipfile', 'io', 'faulthandler', 'pstats', 'contextlib', 'code', 'glob', 'zipimport', 'base64', 'syslog', 'platform', 'ast', 'fileinput', 'telnetlib', 'locale', '_dummy_thread', 'hmac', 'stat', 'uuid', 'quopri', 'readline', 'collections', 'json', 'concurrent', 'lib2to3', 'sqlite3', 'runpy', 'cmath', 'optparse', 'bisect', 'builtins', 'urllib', 'dummy_threading', 'http', 'compileall', 'argparse', 'token', 'sched', 'netrc', 'math', 'ensurepip', 'socketserver', 'colorsys', 'zipapp', 'logging', 'sysconfig'}), 'extra_standard_library': frozenset(), 'known_other': {}, 'multi_line_output': 0, 'forced_separate': (), 'indent': ' ', 'comment_prefix': ' #', 'length_sort': False, 'length_sort_straight': False, 'length_sort_sections': frozenset(), 'add_imports': frozenset(), 'remove_imports': frozenset(), 'append_only': False, 'reverse_relative': False, 'force_single_line': False, 'single_line_exclusions': (), 'default_section': 'THIRDPARTY', 'import_headings': {}, 'balanced_wrapping': False, 'use_parentheses': False, 'order_by_type': True, 'atomic': False, 'lines_after_imports': -1, 'lines_between_sections': 1, 'lines_between_types': 0, 'combine_as_imports': False, 'combine_star': False, 'include_trailing_comma': False, 'from_first': False, 'verbose': False, 'quiet': False, 'force_adds': False, 'force_alphabetical_sort_within_sections': False, 'force_alphabetical_sort': False, 'force_grid_wrap': 0, 'force_sort_within_sections': False, 'lexicographical': False, 'ignore_whitespace': False, 'no_lines_before': frozenset(), 'no_inline_sort': False, 'ignore_comments': False, 'case_sensitive': False, 'sources': (), 'virtual_env': '', 'conda_env': '', 'ensure_newline_before_comments': False, 'directory': '', 'profile': '', 'honor_noqa': False, 'src_paths': frozenset(), 'old_finders': False, 'remove_redundant_aliases': False, 'float_to_top': False, 'filter_files': False, 'formatter': '', 'formatting_function': None, 'color_output': False, 'treat_comments_as_code': frozenset(), 'treat_all_comments_as_code': False, 'supported_extensions': frozenset({'py', 'pyx', 'pyi'}), 'blocked_extensions': frozenset({'pex'}), 'constants': frozenset(), 'classes': frozenset(), 'variables': frozenset(), 'dedup_headings': False, 'source': 'defaults'}, {'classes': frozenset({'\U000eb6c6\x9eÑ\U0008297dâhï\x8eÆ', 'C', '\x8e\U000422ac±\U000b5a1f\U000c4166', 'ùÚ'}), 'single_line_exclusions': ('Y\U000347d9g\x957K', '', 'Ê\U000e8ad2\U0008fa72ùÏ\x19ç\U000eaecc𤎪.', '·o\U000d00e5\U000b36de+\x8f\U000b5953´\x08oÜ', '', ':sI¶', ''), 'indent': ' ', 'no_lines_before': frozenset({'uøø', '¢', '&\x8c5Ï\U000e5f01Ø', '\U0005d415\U000a3df2h\U000f24e5\U00104d7b34¹ÒÀ', '\U000e374c8', 'w'}), 'quiet': False, 'honor_noqa': False, 'dedup_headings': True, 'known_other': {'\x10\x1bm': frozenset({'\U000682a49\U000e1a63²KǶ4', '', '\x1a', '©'}), '': frozenset({'íå\x94Ì', '\U000cf258'})}, 'treat_comments_as_code': frozenset({''}), 'length_sort': True, 'reverse_relative': True, 'combine_as_imports': True, 'py_version': 'all', 'use_parentheses': True, 'skip_gitignore': True, 'remove_imports': frozenset({'', '\U00076fe7þs\x0c\U000c8b75v\U00106541', '𥒒>\U0001960euj𒎕\x9e', '\x15\x9b', '\x02l', '\U000b71ef.\x1c', '\x7f?\U000ec91c', '\x7f,ÞoÀP8\x1b\x1e»3\x86\x94¤ÁÓ~\U00066b1a,O\U0010ab28\x90«', 'Y\x06ºZ\x04Ýì\U00078ce1.\U0010c1f9[EK\x83EÖø', ';À¨|\x1b 𑐒🍸V'}), 'atomic': False, 'source': 'runtime'}), virtual_env='', conda_env='', ensure_newline_before_comments=False, directory='/home/abuild/rpmbuild/BUILD/isort-5.5.1', profile='', honor_noqa=False, old_finders=False, remove_redundant_aliases=False, float_to_top=False, filter_files=False, formatting_function=None, color_output=False, treat_comments_as_code=frozenset({''}), treat_all_comments_as_code=False, supported_extensions=frozenset({'py', 'pyx', 'pyi'}), blocked_extensions=frozenset({'pex'}), constants=frozenset(), classes=frozenset({'\U000eb6c6\x9eÑ\U0008297dâhï\x8eÆ', 'C', '\x8e\U000422ac±\U000b5a1f\U000c4166', 'ùÚ'}), variables=frozenset(), dedup_headings=True), + disregard_skip=True +) @hypothesis.given( config=st.from_type(isort.Config), disregard_skip=st.booleans(), ) +@hypothesis.settings(deadline=None) def test_isort_is_idempotent(config: isort.Config, disregard_skip: bool) -> None: try: result = isort.code(CODE_SNIPPET, config=config, disregard_skip=disregard_skip) @@ -166,10 +171,15 @@ def test_isort_is_idempotent(config: isort.Config, disregard_skip: bool) -> None pass +@hypothesis.example( + config=isort.Config(py_version='all', force_to_top=frozenset(), skip=frozenset({'.svn', '.venv', 'build', 'dist', '.bzr', '.tox', '.hg', '.mypy_cache', '.nox', '_build', 'buck-out', 'node_modules', '.git', '.eggs', '.pants.d', 'venv', '.direnv'}), skip_glob=frozenset(), skip_gitignore=True, line_length=79, wrap_length=0, line_ending='', sections=('FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER'), no_sections=False, known_future_library=frozenset({'__future__'}), known_third_party=frozenset(), known_first_party=frozenset(), known_local_folder=frozenset(), known_standard_library=frozenset({'pwd', 'types', 'nntplib', 'jpeg', 'pyclbr', 'encodings', 'ctypes', 'macerrors', 'filecmp', 'dbm', 'mimetypes', 'statvfs', 'msvcrt', 'spwd', 'codecs', 'SimpleHTTPServer', 'compiler', 'pickletools', 'tkinter', 'pickle', 'fm', 'bsddb', 'contextvars', 'dummy_thread', 'pipes', 'heapq', 'dircache', 'commands', 'unicodedata', 'ntpath', 'marshal', 'fpformat', 'linecache', 'calendar', 'pty', 'MimeWriter', 'inspect', 'mmap', 'ic', 'tty', 'nis', 'new', 'wave', 'HTMLParser', 'anydbm', 'tracemalloc', 'pdb', 'sunau', 'GL', 'parser', 'winsound', 'dbhash', 'zlib', 'MacOS', 'pprint', 'crypt', 'aetools', 'DEVICE', 'fl', 'gettext', 'asyncore', 'copyreg', 'queue', 'resource', 'turtledemo', 'fnmatch', 'hotshot', 'trace', 'string', 'plistlib', 'gzip', 'functools', 'aepack', 'hashlib', 'imp', 'MiniAEFrame', 'getpass', 'shutil', 'ttk', 'multifile', 'operator', 'reprlib', 'subprocess', 'cgi', 'select', 'SimpleXMLRPCServer', 'audioop', 'macresource', 'stringprep', 'wsgiref', 'SUNAUDIODEV', 'atexit', 'lzma', 'asyncio', 'datetime', 'binhex', 'autoGIL', 'doctest', 'thread', 'enum', 'tempfile', 'posixfile', 'mhlib', 'html', 'itertools', 'exceptions', 'sgmllib', 'array', 'test', 'imputil', 'shlex', 'flp', 'uu', 'gdbm', 'urlparse', 'msilib', 'termios', 'modulefinder', 'ossaudiodev', 'timeit', 'binascii', 'popen2', 'ConfigParser', 'poplib', 'zipfile', 'cfmfile', 'pstats', 'AL', 'contextlib', 'code', 'zipimport', 'base64', 'platform', 'ast', 'fileinput', 'locale', 'buildtools', 'stat', 'quopri', 'readline', 'collections', 'aetypes', 'concurrent', 'runpy', 'copy_reg', 'rexec', 'cmath', 'optparse', 'dummy_threading', 'ColorPicker', 'sched', 'netrc', 'sunaudiodev', 'socketserver', 'logging', 'PixMapWrapper', 'sysconfig', 'Nav', 'copy', 'cmd', 'csv', 'chunk', 'multiprocessing', 'warnings', 'weakref', 'py_compile', 'sre', 'sre_parse', 'curses', 'threading', 're', 'FrameWork', '_thread', 'imgfile', 'cd', 'sre_constants', 'xdrlib', 'dataclasses', 'urllib2', 'StringIO', 'configparser', 'importlib', 'UserList', 'posixpath', 'mailbox', 'rfc822', 'grp', 'pydoc', 'sets', 'textwrap', 'numbers', 'W', 'gl', 'htmllib', 'macostools', 'tarfile', 'ipaddress', 'xmlrpc', 'icopen', 'traceback', '_winreg', 'random', 'CGIHTTPServer', 'dis', 'sha', 'selectors', 'statistics', 'DocXMLRPCServer', 'imghdr', 'venv', 'keyword', 'xmlrpclib', 'ftplib', 'getopt', 'posix', 'smtpd', 'profile', 'sndhdr', 'signal', 'EasyDialogs', 'dumbdbm', 'fcntl', 'SocketServer', 'distutils', 'symbol', 'pathlib', 'cStringIO', 'imaplib', 'unittest', 'al', 'cProfile', 'robotparser', 'BaseHTTPServer', 'os', 'pkgutil', 'socket', 'fractions', 'shelve', 'aifc', 'cgitb', 'xml', 'decimal', 'sre_compile', 'ssl', 'user', 'Bastion', 'formatter', 'time', 'abc', 'winreg', 'difflib', 'FL', 'bz2', 'asynchat', 'gc', 'gensuitemodule', 'symtable', 'secrets', 'Carbon', 'mailcap', 'sys', 'bdb', 'fpectl', 'httplib', 'webbrowser', 'smtplib', 'Cookie', 'whichdb', 'turtle', 'tokenize', 'UserString', 'tabnanny', 'site', 'struct', 'codeop', 'email', 'typing', 'cookielib', 'Queue', 'rlcompleter', 'errno', 'macpath', 'videoreader', 'md5', 'cPickle', 'Tix', 'io', 'faulthandler', 'Tkinter', 'glob', 'syslog', 'telnetlib', '_dummy_thread', 'hmac', 'uuid', 'imageop', 'future_builtins', 'json', 'htmlentitydefs', 'lib2to3', 'UserDict', 'mutex', 'sqlite3', 'findertools', 'bisect', 'builtins', 'urllib', 'http', 'compileall', 'argparse', 'ScrolledText', 'token', 'dl', 'applesingle', 'math', 'ensurepip', 'mimify', 'mimetools', 'colorsys', 'zipapp', '__builtin__'}), extra_standard_library=frozenset(), known_other={'other': frozenset({'', '\x10\x1bm'})}, multi_line_output=0, forced_separate=(), indent=' ', comment_prefix=' #', length_sort=True, length_sort_straight=False, length_sort_sections=frozenset(), add_imports=frozenset(), remove_imports=frozenset({'', '\U00076fe7þs\x0c\U000c8b75v\U00106541', '𥒒>\U0001960euj𒎕\x9e', '\x15\x9b', '\x02l', '\U000b71ef.\x1c', '\x7f?\U000ec91c', '\x7f,ÞoÀP8\x1b\x1e»3\x86\x94¤ÁÓ~\U00066b1a,O\U0010ab28\x90«', 'Y\x06ºZ\x04Ýì\U00078ce1.\U0010c1f9[EK\x83EÖø', ';À¨|\x1b 𑐒🍸V'}), append_only=False, reverse_relative=True, force_single_line=False, single_line_exclusions=('Y\U000347d9g\x957K', '', 'Ê\U000e8ad2\U0008fa72ùÏ\x19ç\U000eaecc𤎪.', '·o\U000d00e5\U000b36de+\x8f\U000b5953´\x08oÜ', '', ':sI¶', ''), default_section='THIRDPARTY', import_headings={}, balanced_wrapping=False, use_parentheses=True, order_by_type=True, atomic=False, lines_after_imports=-1, lines_between_sections=1, lines_between_types=0, combine_as_imports=True, combine_star=False, include_trailing_comma=False, from_first=False, verbose=False, quiet=False, force_adds=False, force_alphabetical_sort_within_sections=False, force_alphabetical_sort=False, force_grid_wrap=0, force_sort_within_sections=False, lexicographical=False, ignore_whitespace=False, no_lines_before=frozenset({'uøø', '¢', '&\x8c5Ï\U000e5f01Ø', '\U0005d415\U000a3df2h\U000f24e5\U00104d7b34¹ÒÀ', '\U000e374c8', 'w'}), no_inline_sort=False, ignore_comments=False, case_sensitive=False, sources=({'py_version': 'py3', 'force_to_top': frozenset(), 'skip': frozenset({'.svn', '.venv', 'build', 'dist', '.bzr', '.tox', '.hg', '.mypy_cache', '.nox', '_build', 'buck-out', 'node_modules', '.git', '.eggs', '.pants.d', 'venv', '.direnv'}), 'skip_glob': frozenset(), 'skip_gitignore': False, 'line_length': 79, 'wrap_length': 0, 'line_ending': '', 'sections': ('FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER'), 'no_sections': False, 'known_future_library': frozenset({'__future__'}), 'known_third_party': frozenset(), 'known_first_party': frozenset(), 'known_local_folder': frozenset(), 'known_standard_library': frozenset({'pwd', 'copy', 'cmd', 'csv', 'chunk', 'multiprocessing', 'warnings', 'types', 'weakref', 'nntplib', 'pyclbr', 'encodings', 'py_compile', 'sre', 'ctypes', 'sre_parse', 'filecmp', 'curses', 'threading', 'dbm', 're', '_thread', 'sre_constants', 'xdrlib', 'dataclasses', 'mimetypes', 'configparser', 'importlib', 'msvcrt', 'spwd', 'posixpath', 'mailbox', 'codecs', 'grp', 'pickletools', 'tkinter', 'pickle', 'contextvars', 'pydoc', 'textwrap', 'numbers', 'pipes', 'heapq', 'tarfile', 'unicodedata', 'ntpath', 'ipaddress', 'marshal', 'xmlrpc', 'traceback', 'linecache', 'calendar', 'pty', 'random', 'dis', 'selectors', 'statistics', 'imghdr', 'venv', 'inspect', 'mmap', 'keyword', 'ftplib', 'tty', 'nis', 'getopt', 'posix', 'smtpd', 'wave', 'profile', 'sndhdr', 'signal', 'tracemalloc', 'pdb', 'sunau', 'winsound', 'parser', 'zlib', 'fcntl', 'pprint', 'distutils', 'crypt', 'symbol', 'gettext', 'pathlib', 'asyncore', 'copyreg', 'imaplib', 'unittest', 'queue', 'resource', 'turtledemo', 'fnmatch', 'cProfile', 'os', 'pkgutil', 'socket', 'trace', 'fractions', 'string', 'shelve', 'plistlib', 'aifc', 'gzip', 'functools', 'cgitb', 'xml', 'hashlib', 'decimal', 'imp', 'sre_compile', 'ssl', 'formatter', 'winreg', 'time', 'getpass', 'shutil', 'abc', 'difflib', 'bz2', 'operator', 'reprlib', 'subprocess', 'cgi', 'select', 'asynchat', 'audioop', 'gc', 'secrets', 'symtable', 'mailcap', 'sys', 'bdb', 'fpectl', 'stringprep', 'webbrowser', 'smtplib', 'wsgiref', 'atexit', 'lzma', 'asyncio', 'datetime', 'binhex', 'doctest', 'turtle', 'enum', 'tempfile', 'tokenize', 'tabnanny', 'site', 'html', 'struct', 'itertools', 'codeop', 'email', 'array', 'test', 'typing', 'shlex', 'uu', 'msilib', 'termios', 'rlcompleter', 'modulefinder', 'ossaudiodev', 'timeit', 'binascii', 'poplib', 'errno', 'macpath', 'zipfile', 'io', 'faulthandler', 'pstats', 'contextlib', 'code', 'glob', 'zipimport', 'base64', 'syslog', 'platform', 'ast', 'fileinput', 'telnetlib', 'locale', '_dummy_thread', 'hmac', 'stat', 'uuid', 'quopri', 'readline', 'collections', 'json', 'concurrent', 'lib2to3', 'sqlite3', 'runpy', 'cmath', 'optparse', 'bisect', 'builtins', 'urllib', 'dummy_threading', 'http', 'compileall', 'argparse', 'token', 'sched', 'netrc', 'math', 'ensurepip', 'socketserver', 'colorsys', 'zipapp', 'logging', 'sysconfig'}), 'extra_standard_library': frozenset(), 'known_other': {}, 'multi_line_output': 0, 'forced_separate': (), 'indent': ' ', 'comment_prefix': ' #', 'length_sort': False, 'length_sort_straight': False, 'length_sort_sections': frozenset(), 'add_imports': frozenset(), 'remove_imports': frozenset(), 'append_only': False, 'reverse_relative': False, 'force_single_line': False, 'single_line_exclusions': (), 'default_section': 'THIRDPARTY', 'import_headings': {}, 'balanced_wrapping': False, 'use_parentheses': False, 'order_by_type': True, 'atomic': False, 'lines_after_imports': -1, 'lines_between_sections': 1, 'lines_between_types': 0, 'combine_as_imports': False, 'combine_star': False, 'include_trailing_comma': False, 'from_first': False, 'verbose': False, 'quiet': False, 'force_adds': False, 'force_alphabetical_sort_within_sections': False, 'force_alphabetical_sort': False, 'force_grid_wrap': 0, 'force_sort_within_sections': False, 'lexicographical': False, 'ignore_whitespace': False, 'no_lines_before': frozenset(), 'no_inline_sort': False, 'ignore_comments': False, 'case_sensitive': False, 'sources': (), 'virtual_env': '', 'conda_env': '', 'ensure_newline_before_comments': False, 'directory': '', 'profile': '', 'honor_noqa': False, 'src_paths': frozenset(), 'old_finders': False, 'remove_redundant_aliases': False, 'float_to_top': False, 'filter_files': False, 'formatter': '', 'formatting_function': None, 'color_output': False, 'treat_comments_as_code': frozenset(), 'treat_all_comments_as_code': False, 'supported_extensions': frozenset({'py', 'pyx', 'pyi'}), 'blocked_extensions': frozenset({'pex'}), 'constants': frozenset(), 'classes': frozenset(), 'variables': frozenset(), 'dedup_headings': False, 'source': 'defaults'}, {'classes': frozenset({'\U000eb6c6\x9eÑ\U0008297dâhï\x8eÆ', 'C', '\x8e\U000422ac±\U000b5a1f\U000c4166', 'ùÚ'}), 'single_line_exclusions': ('Y\U000347d9g\x957K', '', 'Ê\U000e8ad2\U0008fa72ùÏ\x19ç\U000eaecc𤎪.', '·o\U000d00e5\U000b36de+\x8f\U000b5953´\x08oÜ', '', ':sI¶', ''), 'indent': ' ', 'no_lines_before': frozenset({'uøø', '¢', '&\x8c5Ï\U000e5f01Ø', '\U0005d415\U000a3df2h\U000f24e5\U00104d7b34¹ÒÀ', '\U000e374c8', 'w'}), 'quiet': False, 'honor_noqa': False, 'dedup_headings': True, 'known_other': {'\x10\x1bm': frozenset({'\U000682a49\U000e1a63²KǶ4', '', '\x1a', '©'}), '': frozenset({'íå\x94Ì', '\U000cf258'})}, 'treat_comments_as_code': frozenset({''}), 'length_sort': True, 'reverse_relative': True, 'combine_as_imports': True, 'py_version': 'all', 'use_parentheses': True, 'skip_gitignore': True, 'remove_imports': frozenset({'', '\U00076fe7þs\x0c\U000c8b75v\U00106541', '𥒒>\U0001960euj𒎕\x9e', '\x15\x9b', '\x02l', '\U000b71ef.\x1c', '\x7f?\U000ec91c', '\x7f,ÞoÀP8\x1b\x1e»3\x86\x94¤ÁÓ~\U00066b1a,O\U0010ab28\x90«', 'Y\x06ºZ\x04Ýì\U00078ce1.\U0010c1f9[EK\x83EÖø', ';À¨|\x1b 𑐒🍸V'}), 'atomic': False, 'source': 'runtime'}), virtual_env='', conda_env='', ensure_newline_before_comments=False, directory='/home/abuild/rpmbuild/BUILD/isort-5.5.1', profile='', honor_noqa=False, old_finders=False, remove_redundant_aliases=False, float_to_top=False, filter_files=False, formatting_function=None, color_output=False, treat_comments_as_code=frozenset({''}), treat_all_comments_as_code=False, supported_extensions=frozenset({'py', 'pyx', 'pyi'}), blocked_extensions=frozenset({'pex'}), constants=frozenset(), classes=frozenset({'\U000eb6c6\x9eÑ\U0008297dâhï\x8eÆ', 'C', '\x8e\U000422ac±\U000b5a1f\U000c4166', 'ùÚ'}), variables=frozenset(), dedup_headings=True), + disregard_skip=True +) @hypothesis.given( config=st.from_type(isort.Config), disregard_skip=st.booleans(), ) +@hypothesis.settings(deadline=None) def test_isort_doesnt_lose_imports_or_comments(config: isort.Config, disregard_skip: bool) -> None: result = isort.code(CODE_SNIPPET, config=config, disregard_skip=disregard_skip) for should_be_retained in SHOULD_RETAIN: From 8405eb743467a084fbb0a340770583311437d2b4 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 8 Sep 2020 23:27:00 -0700 Subject: [PATCH 1032/1439] Restore src_paths --- .isort.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.isort.cfg b/.isort.cfg index 60ece5b8a..567d1abd6 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -1,4 +1,4 @@ [settings] profile=hug -src_paths= +src_paths=isort,test skip=tests/unit/example_crlf_file.py From 7a550f7bd8a2ab00a2b5db893709b84eea23cdc0 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 8 Sep 2020 23:32:54 -0700 Subject: [PATCH 1033/1439] Black formatting --- .../integration/test_setting_combinations.py | 1680 ++++++++++++++++- 1 file changed, 1676 insertions(+), 4 deletions(-) diff --git a/tests/integration/test_setting_combinations.py b/tests/integration/test_setting_combinations.py index ed693cd19..929b877a9 100644 --- a/tests/integration/test_setting_combinations.py +++ b/tests/integration/test_setting_combinations.py @@ -154,8 +154,844 @@ def _raise(*a): @hypothesis.example( - config=isort.Config(py_version='all', force_to_top=frozenset(), skip=frozenset({'.svn', '.venv', 'build', 'dist', '.bzr', '.tox', '.hg', '.mypy_cache', '.nox', '_build', 'buck-out', 'node_modules', '.git', '.eggs', '.pants.d', 'venv', '.direnv'}), skip_glob=frozenset(), skip_gitignore=True, line_length=79, wrap_length=0, line_ending='', sections=('FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER'), no_sections=False, known_future_library=frozenset({'__future__'}), known_third_party=frozenset(), known_first_party=frozenset(), known_local_folder=frozenset(), known_standard_library=frozenset({'pwd', 'types', 'nntplib', 'jpeg', 'pyclbr', 'encodings', 'ctypes', 'macerrors', 'filecmp', 'dbm', 'mimetypes', 'statvfs', 'msvcrt', 'spwd', 'codecs', 'SimpleHTTPServer', 'compiler', 'pickletools', 'tkinter', 'pickle', 'fm', 'bsddb', 'contextvars', 'dummy_thread', 'pipes', 'heapq', 'dircache', 'commands', 'unicodedata', 'ntpath', 'marshal', 'fpformat', 'linecache', 'calendar', 'pty', 'MimeWriter', 'inspect', 'mmap', 'ic', 'tty', 'nis', 'new', 'wave', 'HTMLParser', 'anydbm', 'tracemalloc', 'pdb', 'sunau', 'GL', 'parser', 'winsound', 'dbhash', 'zlib', 'MacOS', 'pprint', 'crypt', 'aetools', 'DEVICE', 'fl', 'gettext', 'asyncore', 'copyreg', 'queue', 'resource', 'turtledemo', 'fnmatch', 'hotshot', 'trace', 'string', 'plistlib', 'gzip', 'functools', 'aepack', 'hashlib', 'imp', 'MiniAEFrame', 'getpass', 'shutil', 'ttk', 'multifile', 'operator', 'reprlib', 'subprocess', 'cgi', 'select', 'SimpleXMLRPCServer', 'audioop', 'macresource', 'stringprep', 'wsgiref', 'SUNAUDIODEV', 'atexit', 'lzma', 'asyncio', 'datetime', 'binhex', 'autoGIL', 'doctest', 'thread', 'enum', 'tempfile', 'posixfile', 'mhlib', 'html', 'itertools', 'exceptions', 'sgmllib', 'array', 'test', 'imputil', 'shlex', 'flp', 'uu', 'gdbm', 'urlparse', 'msilib', 'termios', 'modulefinder', 'ossaudiodev', 'timeit', 'binascii', 'popen2', 'ConfigParser', 'poplib', 'zipfile', 'cfmfile', 'pstats', 'AL', 'contextlib', 'code', 'zipimport', 'base64', 'platform', 'ast', 'fileinput', 'locale', 'buildtools', 'stat', 'quopri', 'readline', 'collections', 'aetypes', 'concurrent', 'runpy', 'copy_reg', 'rexec', 'cmath', 'optparse', 'dummy_threading', 'ColorPicker', 'sched', 'netrc', 'sunaudiodev', 'socketserver', 'logging', 'PixMapWrapper', 'sysconfig', 'Nav', 'copy', 'cmd', 'csv', 'chunk', 'multiprocessing', 'warnings', 'weakref', 'py_compile', 'sre', 'sre_parse', 'curses', 'threading', 're', 'FrameWork', '_thread', 'imgfile', 'cd', 'sre_constants', 'xdrlib', 'dataclasses', 'urllib2', 'StringIO', 'configparser', 'importlib', 'UserList', 'posixpath', 'mailbox', 'rfc822', 'grp', 'pydoc', 'sets', 'textwrap', 'numbers', 'W', 'gl', 'htmllib', 'macostools', 'tarfile', 'ipaddress', 'xmlrpc', 'icopen', 'traceback', '_winreg', 'random', 'CGIHTTPServer', 'dis', 'sha', 'selectors', 'statistics', 'DocXMLRPCServer', 'imghdr', 'venv', 'keyword', 'xmlrpclib', 'ftplib', 'getopt', 'posix', 'smtpd', 'profile', 'sndhdr', 'signal', 'EasyDialogs', 'dumbdbm', 'fcntl', 'SocketServer', 'distutils', 'symbol', 'pathlib', 'cStringIO', 'imaplib', 'unittest', 'al', 'cProfile', 'robotparser', 'BaseHTTPServer', 'os', 'pkgutil', 'socket', 'fractions', 'shelve', 'aifc', 'cgitb', 'xml', 'decimal', 'sre_compile', 'ssl', 'user', 'Bastion', 'formatter', 'time', 'abc', 'winreg', 'difflib', 'FL', 'bz2', 'asynchat', 'gc', 'gensuitemodule', 'symtable', 'secrets', 'Carbon', 'mailcap', 'sys', 'bdb', 'fpectl', 'httplib', 'webbrowser', 'smtplib', 'Cookie', 'whichdb', 'turtle', 'tokenize', 'UserString', 'tabnanny', 'site', 'struct', 'codeop', 'email', 'typing', 'cookielib', 'Queue', 'rlcompleter', 'errno', 'macpath', 'videoreader', 'md5', 'cPickle', 'Tix', 'io', 'faulthandler', 'Tkinter', 'glob', 'syslog', 'telnetlib', '_dummy_thread', 'hmac', 'uuid', 'imageop', 'future_builtins', 'json', 'htmlentitydefs', 'lib2to3', 'UserDict', 'mutex', 'sqlite3', 'findertools', 'bisect', 'builtins', 'urllib', 'http', 'compileall', 'argparse', 'ScrolledText', 'token', 'dl', 'applesingle', 'math', 'ensurepip', 'mimify', 'mimetools', 'colorsys', 'zipapp', '__builtin__'}), extra_standard_library=frozenset(), known_other={'other': frozenset({'', '\x10\x1bm'})}, multi_line_output=0, forced_separate=(), indent=' ', comment_prefix=' #', length_sort=True, length_sort_straight=False, length_sort_sections=frozenset(), add_imports=frozenset(), remove_imports=frozenset({'', '\U00076fe7þs\x0c\U000c8b75v\U00106541', '𥒒>\U0001960euj𒎕\x9e', '\x15\x9b', '\x02l', '\U000b71ef.\x1c', '\x7f?\U000ec91c', '\x7f,ÞoÀP8\x1b\x1e»3\x86\x94¤ÁÓ~\U00066b1a,O\U0010ab28\x90«', 'Y\x06ºZ\x04Ýì\U00078ce1.\U0010c1f9[EK\x83EÖø', ';À¨|\x1b 𑐒🍸V'}), append_only=False, reverse_relative=True, force_single_line=False, single_line_exclusions=('Y\U000347d9g\x957K', '', 'Ê\U000e8ad2\U0008fa72ùÏ\x19ç\U000eaecc𤎪.', '·o\U000d00e5\U000b36de+\x8f\U000b5953´\x08oÜ', '', ':sI¶', ''), default_section='THIRDPARTY', import_headings={}, balanced_wrapping=False, use_parentheses=True, order_by_type=True, atomic=False, lines_after_imports=-1, lines_between_sections=1, lines_between_types=0, combine_as_imports=True, combine_star=False, include_trailing_comma=False, from_first=False, verbose=False, quiet=False, force_adds=False, force_alphabetical_sort_within_sections=False, force_alphabetical_sort=False, force_grid_wrap=0, force_sort_within_sections=False, lexicographical=False, ignore_whitespace=False, no_lines_before=frozenset({'uøø', '¢', '&\x8c5Ï\U000e5f01Ø', '\U0005d415\U000a3df2h\U000f24e5\U00104d7b34¹ÒÀ', '\U000e374c8', 'w'}), no_inline_sort=False, ignore_comments=False, case_sensitive=False, sources=({'py_version': 'py3', 'force_to_top': frozenset(), 'skip': frozenset({'.svn', '.venv', 'build', 'dist', '.bzr', '.tox', '.hg', '.mypy_cache', '.nox', '_build', 'buck-out', 'node_modules', '.git', '.eggs', '.pants.d', 'venv', '.direnv'}), 'skip_glob': frozenset(), 'skip_gitignore': False, 'line_length': 79, 'wrap_length': 0, 'line_ending': '', 'sections': ('FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER'), 'no_sections': False, 'known_future_library': frozenset({'__future__'}), 'known_third_party': frozenset(), 'known_first_party': frozenset(), 'known_local_folder': frozenset(), 'known_standard_library': frozenset({'pwd', 'copy', 'cmd', 'csv', 'chunk', 'multiprocessing', 'warnings', 'types', 'weakref', 'nntplib', 'pyclbr', 'encodings', 'py_compile', 'sre', 'ctypes', 'sre_parse', 'filecmp', 'curses', 'threading', 'dbm', 're', '_thread', 'sre_constants', 'xdrlib', 'dataclasses', 'mimetypes', 'configparser', 'importlib', 'msvcrt', 'spwd', 'posixpath', 'mailbox', 'codecs', 'grp', 'pickletools', 'tkinter', 'pickle', 'contextvars', 'pydoc', 'textwrap', 'numbers', 'pipes', 'heapq', 'tarfile', 'unicodedata', 'ntpath', 'ipaddress', 'marshal', 'xmlrpc', 'traceback', 'linecache', 'calendar', 'pty', 'random', 'dis', 'selectors', 'statistics', 'imghdr', 'venv', 'inspect', 'mmap', 'keyword', 'ftplib', 'tty', 'nis', 'getopt', 'posix', 'smtpd', 'wave', 'profile', 'sndhdr', 'signal', 'tracemalloc', 'pdb', 'sunau', 'winsound', 'parser', 'zlib', 'fcntl', 'pprint', 'distutils', 'crypt', 'symbol', 'gettext', 'pathlib', 'asyncore', 'copyreg', 'imaplib', 'unittest', 'queue', 'resource', 'turtledemo', 'fnmatch', 'cProfile', 'os', 'pkgutil', 'socket', 'trace', 'fractions', 'string', 'shelve', 'plistlib', 'aifc', 'gzip', 'functools', 'cgitb', 'xml', 'hashlib', 'decimal', 'imp', 'sre_compile', 'ssl', 'formatter', 'winreg', 'time', 'getpass', 'shutil', 'abc', 'difflib', 'bz2', 'operator', 'reprlib', 'subprocess', 'cgi', 'select', 'asynchat', 'audioop', 'gc', 'secrets', 'symtable', 'mailcap', 'sys', 'bdb', 'fpectl', 'stringprep', 'webbrowser', 'smtplib', 'wsgiref', 'atexit', 'lzma', 'asyncio', 'datetime', 'binhex', 'doctest', 'turtle', 'enum', 'tempfile', 'tokenize', 'tabnanny', 'site', 'html', 'struct', 'itertools', 'codeop', 'email', 'array', 'test', 'typing', 'shlex', 'uu', 'msilib', 'termios', 'rlcompleter', 'modulefinder', 'ossaudiodev', 'timeit', 'binascii', 'poplib', 'errno', 'macpath', 'zipfile', 'io', 'faulthandler', 'pstats', 'contextlib', 'code', 'glob', 'zipimport', 'base64', 'syslog', 'platform', 'ast', 'fileinput', 'telnetlib', 'locale', '_dummy_thread', 'hmac', 'stat', 'uuid', 'quopri', 'readline', 'collections', 'json', 'concurrent', 'lib2to3', 'sqlite3', 'runpy', 'cmath', 'optparse', 'bisect', 'builtins', 'urllib', 'dummy_threading', 'http', 'compileall', 'argparse', 'token', 'sched', 'netrc', 'math', 'ensurepip', 'socketserver', 'colorsys', 'zipapp', 'logging', 'sysconfig'}), 'extra_standard_library': frozenset(), 'known_other': {}, 'multi_line_output': 0, 'forced_separate': (), 'indent': ' ', 'comment_prefix': ' #', 'length_sort': False, 'length_sort_straight': False, 'length_sort_sections': frozenset(), 'add_imports': frozenset(), 'remove_imports': frozenset(), 'append_only': False, 'reverse_relative': False, 'force_single_line': False, 'single_line_exclusions': (), 'default_section': 'THIRDPARTY', 'import_headings': {}, 'balanced_wrapping': False, 'use_parentheses': False, 'order_by_type': True, 'atomic': False, 'lines_after_imports': -1, 'lines_between_sections': 1, 'lines_between_types': 0, 'combine_as_imports': False, 'combine_star': False, 'include_trailing_comma': False, 'from_first': False, 'verbose': False, 'quiet': False, 'force_adds': False, 'force_alphabetical_sort_within_sections': False, 'force_alphabetical_sort': False, 'force_grid_wrap': 0, 'force_sort_within_sections': False, 'lexicographical': False, 'ignore_whitespace': False, 'no_lines_before': frozenset(), 'no_inline_sort': False, 'ignore_comments': False, 'case_sensitive': False, 'sources': (), 'virtual_env': '', 'conda_env': '', 'ensure_newline_before_comments': False, 'directory': '', 'profile': '', 'honor_noqa': False, 'src_paths': frozenset(), 'old_finders': False, 'remove_redundant_aliases': False, 'float_to_top': False, 'filter_files': False, 'formatter': '', 'formatting_function': None, 'color_output': False, 'treat_comments_as_code': frozenset(), 'treat_all_comments_as_code': False, 'supported_extensions': frozenset({'py', 'pyx', 'pyi'}), 'blocked_extensions': frozenset({'pex'}), 'constants': frozenset(), 'classes': frozenset(), 'variables': frozenset(), 'dedup_headings': False, 'source': 'defaults'}, {'classes': frozenset({'\U000eb6c6\x9eÑ\U0008297dâhï\x8eÆ', 'C', '\x8e\U000422ac±\U000b5a1f\U000c4166', 'ùÚ'}), 'single_line_exclusions': ('Y\U000347d9g\x957K', '', 'Ê\U000e8ad2\U0008fa72ùÏ\x19ç\U000eaecc𤎪.', '·o\U000d00e5\U000b36de+\x8f\U000b5953´\x08oÜ', '', ':sI¶', ''), 'indent': ' ', 'no_lines_before': frozenset({'uøø', '¢', '&\x8c5Ï\U000e5f01Ø', '\U0005d415\U000a3df2h\U000f24e5\U00104d7b34¹ÒÀ', '\U000e374c8', 'w'}), 'quiet': False, 'honor_noqa': False, 'dedup_headings': True, 'known_other': {'\x10\x1bm': frozenset({'\U000682a49\U000e1a63²KǶ4', '', '\x1a', '©'}), '': frozenset({'íå\x94Ì', '\U000cf258'})}, 'treat_comments_as_code': frozenset({''}), 'length_sort': True, 'reverse_relative': True, 'combine_as_imports': True, 'py_version': 'all', 'use_parentheses': True, 'skip_gitignore': True, 'remove_imports': frozenset({'', '\U00076fe7þs\x0c\U000c8b75v\U00106541', '𥒒>\U0001960euj𒎕\x9e', '\x15\x9b', '\x02l', '\U000b71ef.\x1c', '\x7f?\U000ec91c', '\x7f,ÞoÀP8\x1b\x1e»3\x86\x94¤ÁÓ~\U00066b1a,O\U0010ab28\x90«', 'Y\x06ºZ\x04Ýì\U00078ce1.\U0010c1f9[EK\x83EÖø', ';À¨|\x1b 𑐒🍸V'}), 'atomic': False, 'source': 'runtime'}), virtual_env='', conda_env='', ensure_newline_before_comments=False, directory='/home/abuild/rpmbuild/BUILD/isort-5.5.1', profile='', honor_noqa=False, old_finders=False, remove_redundant_aliases=False, float_to_top=False, filter_files=False, formatting_function=None, color_output=False, treat_comments_as_code=frozenset({''}), treat_all_comments_as_code=False, supported_extensions=frozenset({'py', 'pyx', 'pyi'}), blocked_extensions=frozenset({'pex'}), constants=frozenset(), classes=frozenset({'\U000eb6c6\x9eÑ\U0008297dâhï\x8eÆ', 'C', '\x8e\U000422ac±\U000b5a1f\U000c4166', 'ùÚ'}), variables=frozenset(), dedup_headings=True), - disregard_skip=True + config=isort.Config( + py_version="all", + force_to_top=frozenset(), + skip=frozenset( + { + ".svn", + ".venv", + "build", + "dist", + ".bzr", + ".tox", + ".hg", + ".mypy_cache", + ".nox", + "_build", + "buck-out", + "node_modules", + ".git", + ".eggs", + ".pants.d", + "venv", + ".direnv", + } + ), + skip_glob=frozenset(), + skip_gitignore=True, + line_length=79, + wrap_length=0, + line_ending="", + sections=("FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"), + no_sections=False, + known_future_library=frozenset({"__future__"}), + known_third_party=frozenset(), + known_first_party=frozenset(), + known_local_folder=frozenset(), + known_standard_library=frozenset( + { + "pwd", + "types", + "nntplib", + "jpeg", + "pyclbr", + "encodings", + "ctypes", + "macerrors", + "filecmp", + "dbm", + "mimetypes", + "statvfs", + "msvcrt", + "spwd", + "codecs", + "SimpleHTTPServer", + "compiler", + "pickletools", + "tkinter", + "pickle", + "fm", + "bsddb", + "contextvars", + "dummy_thread", + "pipes", + "heapq", + "dircache", + "commands", + "unicodedata", + "ntpath", + "marshal", + "fpformat", + "linecache", + "calendar", + "pty", + "MimeWriter", + "inspect", + "mmap", + "ic", + "tty", + "nis", + "new", + "wave", + "HTMLParser", + "anydbm", + "tracemalloc", + "pdb", + "sunau", + "GL", + "parser", + "winsound", + "dbhash", + "zlib", + "MacOS", + "pprint", + "crypt", + "aetools", + "DEVICE", + "fl", + "gettext", + "asyncore", + "copyreg", + "queue", + "resource", + "turtledemo", + "fnmatch", + "hotshot", + "trace", + "string", + "plistlib", + "gzip", + "functools", + "aepack", + "hashlib", + "imp", + "MiniAEFrame", + "getpass", + "shutil", + "ttk", + "multifile", + "operator", + "reprlib", + "subprocess", + "cgi", + "select", + "SimpleXMLRPCServer", + "audioop", + "macresource", + "stringprep", + "wsgiref", + "SUNAUDIODEV", + "atexit", + "lzma", + "asyncio", + "datetime", + "binhex", + "autoGIL", + "doctest", + "thread", + "enum", + "tempfile", + "posixfile", + "mhlib", + "html", + "itertools", + "exceptions", + "sgmllib", + "array", + "test", + "imputil", + "shlex", + "flp", + "uu", + "gdbm", + "urlparse", + "msilib", + "termios", + "modulefinder", + "ossaudiodev", + "timeit", + "binascii", + "popen2", + "ConfigParser", + "poplib", + "zipfile", + "cfmfile", + "pstats", + "AL", + "contextlib", + "code", + "zipimport", + "base64", + "platform", + "ast", + "fileinput", + "locale", + "buildtools", + "stat", + "quopri", + "readline", + "collections", + "aetypes", + "concurrent", + "runpy", + "copy_reg", + "rexec", + "cmath", + "optparse", + "dummy_threading", + "ColorPicker", + "sched", + "netrc", + "sunaudiodev", + "socketserver", + "logging", + "PixMapWrapper", + "sysconfig", + "Nav", + "copy", + "cmd", + "csv", + "chunk", + "multiprocessing", + "warnings", + "weakref", + "py_compile", + "sre", + "sre_parse", + "curses", + "threading", + "re", + "FrameWork", + "_thread", + "imgfile", + "cd", + "sre_constants", + "xdrlib", + "dataclasses", + "urllib2", + "StringIO", + "configparser", + "importlib", + "UserList", + "posixpath", + "mailbox", + "rfc822", + "grp", + "pydoc", + "sets", + "textwrap", + "numbers", + "W", + "gl", + "htmllib", + "macostools", + "tarfile", + "ipaddress", + "xmlrpc", + "icopen", + "traceback", + "_winreg", + "random", + "CGIHTTPServer", + "dis", + "sha", + "selectors", + "statistics", + "DocXMLRPCServer", + "imghdr", + "venv", + "keyword", + "xmlrpclib", + "ftplib", + "getopt", + "posix", + "smtpd", + "profile", + "sndhdr", + "signal", + "EasyDialogs", + "dumbdbm", + "fcntl", + "SocketServer", + "distutils", + "symbol", + "pathlib", + "cStringIO", + "imaplib", + "unittest", + "al", + "cProfile", + "robotparser", + "BaseHTTPServer", + "os", + "pkgutil", + "socket", + "fractions", + "shelve", + "aifc", + "cgitb", + "xml", + "decimal", + "sre_compile", + "ssl", + "user", + "Bastion", + "formatter", + "time", + "abc", + "winreg", + "difflib", + "FL", + "bz2", + "asynchat", + "gc", + "gensuitemodule", + "symtable", + "secrets", + "Carbon", + "mailcap", + "sys", + "bdb", + "fpectl", + "httplib", + "webbrowser", + "smtplib", + "Cookie", + "whichdb", + "turtle", + "tokenize", + "UserString", + "tabnanny", + "site", + "struct", + "codeop", + "email", + "typing", + "cookielib", + "Queue", + "rlcompleter", + "errno", + "macpath", + "videoreader", + "md5", + "cPickle", + "Tix", + "io", + "faulthandler", + "Tkinter", + "glob", + "syslog", + "telnetlib", + "_dummy_thread", + "hmac", + "uuid", + "imageop", + "future_builtins", + "json", + "htmlentitydefs", + "lib2to3", + "UserDict", + "mutex", + "sqlite3", + "findertools", + "bisect", + "builtins", + "urllib", + "http", + "compileall", + "argparse", + "ScrolledText", + "token", + "dl", + "applesingle", + "math", + "ensurepip", + "mimify", + "mimetools", + "colorsys", + "zipapp", + "__builtin__", + } + ), + extra_standard_library=frozenset(), + known_other={"other": frozenset({"", "\x10\x1bm"})}, + multi_line_output=0, + forced_separate=(), + indent=" ", + comment_prefix=" #", + length_sort=True, + length_sort_straight=False, + length_sort_sections=frozenset(), + add_imports=frozenset(), + remove_imports=frozenset( + { + "", + "\U00076fe7þs\x0c\U000c8b75v\U00106541", + "𥒒>\U0001960euj𒎕\x9e", + "\x15\x9b", + "\x02l", + "\U000b71ef.\x1c", + "\x7f?\U000ec91c", + "\x7f,ÞoÀP8\x1b\x1e»3\x86\x94¤ÁÓ~\U00066b1a,O\U0010ab28\x90«", + "Y\x06ºZ\x04Ýì\U00078ce1.\U0010c1f9[EK\x83EÖø", + ";À¨|\x1b 𑐒🍸V", + } + ), + append_only=False, + reverse_relative=True, + force_single_line=False, + single_line_exclusions=( + "Y\U000347d9g\x957K", + "", + "Ê\U000e8ad2\U0008fa72ùÏ\x19ç\U000eaecc𤎪.", + "·o\U000d00e5\U000b36de+\x8f\U000b5953´\x08oÜ", + "", + ":sI¶", + "", + ), + default_section="THIRDPARTY", + import_headings={}, + balanced_wrapping=False, + use_parentheses=True, + order_by_type=True, + atomic=False, + lines_after_imports=-1, + lines_between_sections=1, + lines_between_types=0, + combine_as_imports=True, + combine_star=False, + include_trailing_comma=False, + from_first=False, + verbose=False, + quiet=False, + force_adds=False, + force_alphabetical_sort_within_sections=False, + force_alphabetical_sort=False, + force_grid_wrap=0, + force_sort_within_sections=False, + lexicographical=False, + ignore_whitespace=False, + no_lines_before=frozenset( + { + "uøø", + "¢", + "&\x8c5Ï\U000e5f01Ø", + "\U0005d415\U000a3df2h\U000f24e5\U00104d7b34¹ÒÀ", + "\U000e374c8", + "w", + } + ), + no_inline_sort=False, + ignore_comments=False, + case_sensitive=False, + sources=( + { + "py_version": "py3", + "force_to_top": frozenset(), + "skip": frozenset( + { + ".svn", + ".venv", + "build", + "dist", + ".bzr", + ".tox", + ".hg", + ".mypy_cache", + ".nox", + "_build", + "buck-out", + "node_modules", + ".git", + ".eggs", + ".pants.d", + "venv", + ".direnv", + } + ), + "skip_glob": frozenset(), + "skip_gitignore": False, + "line_length": 79, + "wrap_length": 0, + "line_ending": "", + "sections": ("FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"), + "no_sections": False, + "known_future_library": frozenset({"__future__"}), + "known_third_party": frozenset(), + "known_first_party": frozenset(), + "known_local_folder": frozenset(), + "known_standard_library": frozenset( + { + "pwd", + "copy", + "cmd", + "csv", + "chunk", + "multiprocessing", + "warnings", + "types", + "weakref", + "nntplib", + "pyclbr", + "encodings", + "py_compile", + "sre", + "ctypes", + "sre_parse", + "filecmp", + "curses", + "threading", + "dbm", + "re", + "_thread", + "sre_constants", + "xdrlib", + "dataclasses", + "mimetypes", + "configparser", + "importlib", + "msvcrt", + "spwd", + "posixpath", + "mailbox", + "codecs", + "grp", + "pickletools", + "tkinter", + "pickle", + "contextvars", + "pydoc", + "textwrap", + "numbers", + "pipes", + "heapq", + "tarfile", + "unicodedata", + "ntpath", + "ipaddress", + "marshal", + "xmlrpc", + "traceback", + "linecache", + "calendar", + "pty", + "random", + "dis", + "selectors", + "statistics", + "imghdr", + "venv", + "inspect", + "mmap", + "keyword", + "ftplib", + "tty", + "nis", + "getopt", + "posix", + "smtpd", + "wave", + "profile", + "sndhdr", + "signal", + "tracemalloc", + "pdb", + "sunau", + "winsound", + "parser", + "zlib", + "fcntl", + "pprint", + "distutils", + "crypt", + "symbol", + "gettext", + "pathlib", + "asyncore", + "copyreg", + "imaplib", + "unittest", + "queue", + "resource", + "turtledemo", + "fnmatch", + "cProfile", + "os", + "pkgutil", + "socket", + "trace", + "fractions", + "string", + "shelve", + "plistlib", + "aifc", + "gzip", + "functools", + "cgitb", + "xml", + "hashlib", + "decimal", + "imp", + "sre_compile", + "ssl", + "formatter", + "winreg", + "time", + "getpass", + "shutil", + "abc", + "difflib", + "bz2", + "operator", + "reprlib", + "subprocess", + "cgi", + "select", + "asynchat", + "audioop", + "gc", + "secrets", + "symtable", + "mailcap", + "sys", + "bdb", + "fpectl", + "stringprep", + "webbrowser", + "smtplib", + "wsgiref", + "atexit", + "lzma", + "asyncio", + "datetime", + "binhex", + "doctest", + "turtle", + "enum", + "tempfile", + "tokenize", + "tabnanny", + "site", + "html", + "struct", + "itertools", + "codeop", + "email", + "array", + "test", + "typing", + "shlex", + "uu", + "msilib", + "termios", + "rlcompleter", + "modulefinder", + "ossaudiodev", + "timeit", + "binascii", + "poplib", + "errno", + "macpath", + "zipfile", + "io", + "faulthandler", + "pstats", + "contextlib", + "code", + "glob", + "zipimport", + "base64", + "syslog", + "platform", + "ast", + "fileinput", + "telnetlib", + "locale", + "_dummy_thread", + "hmac", + "stat", + "uuid", + "quopri", + "readline", + "collections", + "json", + "concurrent", + "lib2to3", + "sqlite3", + "runpy", + "cmath", + "optparse", + "bisect", + "builtins", + "urllib", + "dummy_threading", + "http", + "compileall", + "argparse", + "token", + "sched", + "netrc", + "math", + "ensurepip", + "socketserver", + "colorsys", + "zipapp", + "logging", + "sysconfig", + } + ), + "extra_standard_library": frozenset(), + "known_other": {}, + "multi_line_output": 0, + "forced_separate": (), + "indent": " ", + "comment_prefix": " #", + "length_sort": False, + "length_sort_straight": False, + "length_sort_sections": frozenset(), + "add_imports": frozenset(), + "remove_imports": frozenset(), + "append_only": False, + "reverse_relative": False, + "force_single_line": False, + "single_line_exclusions": (), + "default_section": "THIRDPARTY", + "import_headings": {}, + "balanced_wrapping": False, + "use_parentheses": False, + "order_by_type": True, + "atomic": False, + "lines_after_imports": -1, + "lines_between_sections": 1, + "lines_between_types": 0, + "combine_as_imports": False, + "combine_star": False, + "include_trailing_comma": False, + "from_first": False, + "verbose": False, + "quiet": False, + "force_adds": False, + "force_alphabetical_sort_within_sections": False, + "force_alphabetical_sort": False, + "force_grid_wrap": 0, + "force_sort_within_sections": False, + "lexicographical": False, + "ignore_whitespace": False, + "no_lines_before": frozenset(), + "no_inline_sort": False, + "ignore_comments": False, + "case_sensitive": False, + "sources": (), + "virtual_env": "", + "conda_env": "", + "ensure_newline_before_comments": False, + "directory": "", + "profile": "", + "honor_noqa": False, + "src_paths": frozenset(), + "old_finders": False, + "remove_redundant_aliases": False, + "float_to_top": False, + "filter_files": False, + "formatter": "", + "formatting_function": None, + "color_output": False, + "treat_comments_as_code": frozenset(), + "treat_all_comments_as_code": False, + "supported_extensions": frozenset({"py", "pyx", "pyi"}), + "blocked_extensions": frozenset({"pex"}), + "constants": frozenset(), + "classes": frozenset(), + "variables": frozenset(), + "dedup_headings": False, + "source": "defaults", + }, + { + "classes": frozenset( + { + "\U000eb6c6\x9eÑ\U0008297dâhï\x8eÆ", + "C", + "\x8e\U000422ac±\U000b5a1f\U000c4166", + "ùÚ", + } + ), + "single_line_exclusions": ( + "Y\U000347d9g\x957K", + "", + "Ê\U000e8ad2\U0008fa72ùÏ\x19ç\U000eaecc𤎪.", + "·o\U000d00e5\U000b36de+\x8f\U000b5953´\x08oÜ", + "", + ":sI¶", + "", + ), + "indent": " ", + "no_lines_before": frozenset( + { + "uøø", + "¢", + "&\x8c5Ï\U000e5f01Ø", + "\U0005d415\U000a3df2h\U000f24e5\U00104d7b34¹ÒÀ", + "\U000e374c8", + "w", + } + ), + "quiet": False, + "honor_noqa": False, + "dedup_headings": True, + "known_other": { + "\x10\x1bm": frozenset({"\U000682a49\U000e1a63²KǶ4", "", "\x1a", "©"}), + "": frozenset({"íå\x94Ì", "\U000cf258"}), + }, + "treat_comments_as_code": frozenset({""}), + "length_sort": True, + "reverse_relative": True, + "combine_as_imports": True, + "py_version": "all", + "use_parentheses": True, + "skip_gitignore": True, + "remove_imports": frozenset( + { + "", + "\U00076fe7þs\x0c\U000c8b75v\U00106541", + "𥒒>\U0001960euj𒎕\x9e", + "\x15\x9b", + "\x02l", + "\U000b71ef.\x1c", + "\x7f?\U000ec91c", + "\x7f,ÞoÀP8\x1b\x1e»3\x86\x94¤ÁÓ~\U00066b1a,O\U0010ab28\x90«", + "Y\x06ºZ\x04Ýì\U00078ce1.\U0010c1f9[EK\x83EÖø", + ";À¨|\x1b 𑐒🍸V", + } + ), + "atomic": False, + "source": "runtime", + }, + ), + virtual_env="", + conda_env="", + ensure_newline_before_comments=False, + directory="/home/abuild/rpmbuild/BUILD/isort-5.5.1", + profile="", + honor_noqa=False, + old_finders=False, + remove_redundant_aliases=False, + float_to_top=False, + filter_files=False, + formatting_function=None, + color_output=False, + treat_comments_as_code=frozenset({""}), + treat_all_comments_as_code=False, + supported_extensions=frozenset({"py", "pyx", "pyi"}), + blocked_extensions=frozenset({"pex"}), + constants=frozenset(), + classes=frozenset( + {"\U000eb6c6\x9eÑ\U0008297dâhï\x8eÆ", "C", "\x8e\U000422ac±\U000b5a1f\U000c4166", "ùÚ"} + ), + variables=frozenset(), + dedup_headings=True, + ), + disregard_skip=True, ) @hypothesis.given( config=st.from_type(isort.Config), @@ -172,8 +1008,844 @@ def test_isort_is_idempotent(config: isort.Config, disregard_skip: bool) -> None @hypothesis.example( - config=isort.Config(py_version='all', force_to_top=frozenset(), skip=frozenset({'.svn', '.venv', 'build', 'dist', '.bzr', '.tox', '.hg', '.mypy_cache', '.nox', '_build', 'buck-out', 'node_modules', '.git', '.eggs', '.pants.d', 'venv', '.direnv'}), skip_glob=frozenset(), skip_gitignore=True, line_length=79, wrap_length=0, line_ending='', sections=('FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER'), no_sections=False, known_future_library=frozenset({'__future__'}), known_third_party=frozenset(), known_first_party=frozenset(), known_local_folder=frozenset(), known_standard_library=frozenset({'pwd', 'types', 'nntplib', 'jpeg', 'pyclbr', 'encodings', 'ctypes', 'macerrors', 'filecmp', 'dbm', 'mimetypes', 'statvfs', 'msvcrt', 'spwd', 'codecs', 'SimpleHTTPServer', 'compiler', 'pickletools', 'tkinter', 'pickle', 'fm', 'bsddb', 'contextvars', 'dummy_thread', 'pipes', 'heapq', 'dircache', 'commands', 'unicodedata', 'ntpath', 'marshal', 'fpformat', 'linecache', 'calendar', 'pty', 'MimeWriter', 'inspect', 'mmap', 'ic', 'tty', 'nis', 'new', 'wave', 'HTMLParser', 'anydbm', 'tracemalloc', 'pdb', 'sunau', 'GL', 'parser', 'winsound', 'dbhash', 'zlib', 'MacOS', 'pprint', 'crypt', 'aetools', 'DEVICE', 'fl', 'gettext', 'asyncore', 'copyreg', 'queue', 'resource', 'turtledemo', 'fnmatch', 'hotshot', 'trace', 'string', 'plistlib', 'gzip', 'functools', 'aepack', 'hashlib', 'imp', 'MiniAEFrame', 'getpass', 'shutil', 'ttk', 'multifile', 'operator', 'reprlib', 'subprocess', 'cgi', 'select', 'SimpleXMLRPCServer', 'audioop', 'macresource', 'stringprep', 'wsgiref', 'SUNAUDIODEV', 'atexit', 'lzma', 'asyncio', 'datetime', 'binhex', 'autoGIL', 'doctest', 'thread', 'enum', 'tempfile', 'posixfile', 'mhlib', 'html', 'itertools', 'exceptions', 'sgmllib', 'array', 'test', 'imputil', 'shlex', 'flp', 'uu', 'gdbm', 'urlparse', 'msilib', 'termios', 'modulefinder', 'ossaudiodev', 'timeit', 'binascii', 'popen2', 'ConfigParser', 'poplib', 'zipfile', 'cfmfile', 'pstats', 'AL', 'contextlib', 'code', 'zipimport', 'base64', 'platform', 'ast', 'fileinput', 'locale', 'buildtools', 'stat', 'quopri', 'readline', 'collections', 'aetypes', 'concurrent', 'runpy', 'copy_reg', 'rexec', 'cmath', 'optparse', 'dummy_threading', 'ColorPicker', 'sched', 'netrc', 'sunaudiodev', 'socketserver', 'logging', 'PixMapWrapper', 'sysconfig', 'Nav', 'copy', 'cmd', 'csv', 'chunk', 'multiprocessing', 'warnings', 'weakref', 'py_compile', 'sre', 'sre_parse', 'curses', 'threading', 're', 'FrameWork', '_thread', 'imgfile', 'cd', 'sre_constants', 'xdrlib', 'dataclasses', 'urllib2', 'StringIO', 'configparser', 'importlib', 'UserList', 'posixpath', 'mailbox', 'rfc822', 'grp', 'pydoc', 'sets', 'textwrap', 'numbers', 'W', 'gl', 'htmllib', 'macostools', 'tarfile', 'ipaddress', 'xmlrpc', 'icopen', 'traceback', '_winreg', 'random', 'CGIHTTPServer', 'dis', 'sha', 'selectors', 'statistics', 'DocXMLRPCServer', 'imghdr', 'venv', 'keyword', 'xmlrpclib', 'ftplib', 'getopt', 'posix', 'smtpd', 'profile', 'sndhdr', 'signal', 'EasyDialogs', 'dumbdbm', 'fcntl', 'SocketServer', 'distutils', 'symbol', 'pathlib', 'cStringIO', 'imaplib', 'unittest', 'al', 'cProfile', 'robotparser', 'BaseHTTPServer', 'os', 'pkgutil', 'socket', 'fractions', 'shelve', 'aifc', 'cgitb', 'xml', 'decimal', 'sre_compile', 'ssl', 'user', 'Bastion', 'formatter', 'time', 'abc', 'winreg', 'difflib', 'FL', 'bz2', 'asynchat', 'gc', 'gensuitemodule', 'symtable', 'secrets', 'Carbon', 'mailcap', 'sys', 'bdb', 'fpectl', 'httplib', 'webbrowser', 'smtplib', 'Cookie', 'whichdb', 'turtle', 'tokenize', 'UserString', 'tabnanny', 'site', 'struct', 'codeop', 'email', 'typing', 'cookielib', 'Queue', 'rlcompleter', 'errno', 'macpath', 'videoreader', 'md5', 'cPickle', 'Tix', 'io', 'faulthandler', 'Tkinter', 'glob', 'syslog', 'telnetlib', '_dummy_thread', 'hmac', 'uuid', 'imageop', 'future_builtins', 'json', 'htmlentitydefs', 'lib2to3', 'UserDict', 'mutex', 'sqlite3', 'findertools', 'bisect', 'builtins', 'urllib', 'http', 'compileall', 'argparse', 'ScrolledText', 'token', 'dl', 'applesingle', 'math', 'ensurepip', 'mimify', 'mimetools', 'colorsys', 'zipapp', '__builtin__'}), extra_standard_library=frozenset(), known_other={'other': frozenset({'', '\x10\x1bm'})}, multi_line_output=0, forced_separate=(), indent=' ', comment_prefix=' #', length_sort=True, length_sort_straight=False, length_sort_sections=frozenset(), add_imports=frozenset(), remove_imports=frozenset({'', '\U00076fe7þs\x0c\U000c8b75v\U00106541', '𥒒>\U0001960euj𒎕\x9e', '\x15\x9b', '\x02l', '\U000b71ef.\x1c', '\x7f?\U000ec91c', '\x7f,ÞoÀP8\x1b\x1e»3\x86\x94¤ÁÓ~\U00066b1a,O\U0010ab28\x90«', 'Y\x06ºZ\x04Ýì\U00078ce1.\U0010c1f9[EK\x83EÖø', ';À¨|\x1b 𑐒🍸V'}), append_only=False, reverse_relative=True, force_single_line=False, single_line_exclusions=('Y\U000347d9g\x957K', '', 'Ê\U000e8ad2\U0008fa72ùÏ\x19ç\U000eaecc𤎪.', '·o\U000d00e5\U000b36de+\x8f\U000b5953´\x08oÜ', '', ':sI¶', ''), default_section='THIRDPARTY', import_headings={}, balanced_wrapping=False, use_parentheses=True, order_by_type=True, atomic=False, lines_after_imports=-1, lines_between_sections=1, lines_between_types=0, combine_as_imports=True, combine_star=False, include_trailing_comma=False, from_first=False, verbose=False, quiet=False, force_adds=False, force_alphabetical_sort_within_sections=False, force_alphabetical_sort=False, force_grid_wrap=0, force_sort_within_sections=False, lexicographical=False, ignore_whitespace=False, no_lines_before=frozenset({'uøø', '¢', '&\x8c5Ï\U000e5f01Ø', '\U0005d415\U000a3df2h\U000f24e5\U00104d7b34¹ÒÀ', '\U000e374c8', 'w'}), no_inline_sort=False, ignore_comments=False, case_sensitive=False, sources=({'py_version': 'py3', 'force_to_top': frozenset(), 'skip': frozenset({'.svn', '.venv', 'build', 'dist', '.bzr', '.tox', '.hg', '.mypy_cache', '.nox', '_build', 'buck-out', 'node_modules', '.git', '.eggs', '.pants.d', 'venv', '.direnv'}), 'skip_glob': frozenset(), 'skip_gitignore': False, 'line_length': 79, 'wrap_length': 0, 'line_ending': '', 'sections': ('FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER'), 'no_sections': False, 'known_future_library': frozenset({'__future__'}), 'known_third_party': frozenset(), 'known_first_party': frozenset(), 'known_local_folder': frozenset(), 'known_standard_library': frozenset({'pwd', 'copy', 'cmd', 'csv', 'chunk', 'multiprocessing', 'warnings', 'types', 'weakref', 'nntplib', 'pyclbr', 'encodings', 'py_compile', 'sre', 'ctypes', 'sre_parse', 'filecmp', 'curses', 'threading', 'dbm', 're', '_thread', 'sre_constants', 'xdrlib', 'dataclasses', 'mimetypes', 'configparser', 'importlib', 'msvcrt', 'spwd', 'posixpath', 'mailbox', 'codecs', 'grp', 'pickletools', 'tkinter', 'pickle', 'contextvars', 'pydoc', 'textwrap', 'numbers', 'pipes', 'heapq', 'tarfile', 'unicodedata', 'ntpath', 'ipaddress', 'marshal', 'xmlrpc', 'traceback', 'linecache', 'calendar', 'pty', 'random', 'dis', 'selectors', 'statistics', 'imghdr', 'venv', 'inspect', 'mmap', 'keyword', 'ftplib', 'tty', 'nis', 'getopt', 'posix', 'smtpd', 'wave', 'profile', 'sndhdr', 'signal', 'tracemalloc', 'pdb', 'sunau', 'winsound', 'parser', 'zlib', 'fcntl', 'pprint', 'distutils', 'crypt', 'symbol', 'gettext', 'pathlib', 'asyncore', 'copyreg', 'imaplib', 'unittest', 'queue', 'resource', 'turtledemo', 'fnmatch', 'cProfile', 'os', 'pkgutil', 'socket', 'trace', 'fractions', 'string', 'shelve', 'plistlib', 'aifc', 'gzip', 'functools', 'cgitb', 'xml', 'hashlib', 'decimal', 'imp', 'sre_compile', 'ssl', 'formatter', 'winreg', 'time', 'getpass', 'shutil', 'abc', 'difflib', 'bz2', 'operator', 'reprlib', 'subprocess', 'cgi', 'select', 'asynchat', 'audioop', 'gc', 'secrets', 'symtable', 'mailcap', 'sys', 'bdb', 'fpectl', 'stringprep', 'webbrowser', 'smtplib', 'wsgiref', 'atexit', 'lzma', 'asyncio', 'datetime', 'binhex', 'doctest', 'turtle', 'enum', 'tempfile', 'tokenize', 'tabnanny', 'site', 'html', 'struct', 'itertools', 'codeop', 'email', 'array', 'test', 'typing', 'shlex', 'uu', 'msilib', 'termios', 'rlcompleter', 'modulefinder', 'ossaudiodev', 'timeit', 'binascii', 'poplib', 'errno', 'macpath', 'zipfile', 'io', 'faulthandler', 'pstats', 'contextlib', 'code', 'glob', 'zipimport', 'base64', 'syslog', 'platform', 'ast', 'fileinput', 'telnetlib', 'locale', '_dummy_thread', 'hmac', 'stat', 'uuid', 'quopri', 'readline', 'collections', 'json', 'concurrent', 'lib2to3', 'sqlite3', 'runpy', 'cmath', 'optparse', 'bisect', 'builtins', 'urllib', 'dummy_threading', 'http', 'compileall', 'argparse', 'token', 'sched', 'netrc', 'math', 'ensurepip', 'socketserver', 'colorsys', 'zipapp', 'logging', 'sysconfig'}), 'extra_standard_library': frozenset(), 'known_other': {}, 'multi_line_output': 0, 'forced_separate': (), 'indent': ' ', 'comment_prefix': ' #', 'length_sort': False, 'length_sort_straight': False, 'length_sort_sections': frozenset(), 'add_imports': frozenset(), 'remove_imports': frozenset(), 'append_only': False, 'reverse_relative': False, 'force_single_line': False, 'single_line_exclusions': (), 'default_section': 'THIRDPARTY', 'import_headings': {}, 'balanced_wrapping': False, 'use_parentheses': False, 'order_by_type': True, 'atomic': False, 'lines_after_imports': -1, 'lines_between_sections': 1, 'lines_between_types': 0, 'combine_as_imports': False, 'combine_star': False, 'include_trailing_comma': False, 'from_first': False, 'verbose': False, 'quiet': False, 'force_adds': False, 'force_alphabetical_sort_within_sections': False, 'force_alphabetical_sort': False, 'force_grid_wrap': 0, 'force_sort_within_sections': False, 'lexicographical': False, 'ignore_whitespace': False, 'no_lines_before': frozenset(), 'no_inline_sort': False, 'ignore_comments': False, 'case_sensitive': False, 'sources': (), 'virtual_env': '', 'conda_env': '', 'ensure_newline_before_comments': False, 'directory': '', 'profile': '', 'honor_noqa': False, 'src_paths': frozenset(), 'old_finders': False, 'remove_redundant_aliases': False, 'float_to_top': False, 'filter_files': False, 'formatter': '', 'formatting_function': None, 'color_output': False, 'treat_comments_as_code': frozenset(), 'treat_all_comments_as_code': False, 'supported_extensions': frozenset({'py', 'pyx', 'pyi'}), 'blocked_extensions': frozenset({'pex'}), 'constants': frozenset(), 'classes': frozenset(), 'variables': frozenset(), 'dedup_headings': False, 'source': 'defaults'}, {'classes': frozenset({'\U000eb6c6\x9eÑ\U0008297dâhï\x8eÆ', 'C', '\x8e\U000422ac±\U000b5a1f\U000c4166', 'ùÚ'}), 'single_line_exclusions': ('Y\U000347d9g\x957K', '', 'Ê\U000e8ad2\U0008fa72ùÏ\x19ç\U000eaecc𤎪.', '·o\U000d00e5\U000b36de+\x8f\U000b5953´\x08oÜ', '', ':sI¶', ''), 'indent': ' ', 'no_lines_before': frozenset({'uøø', '¢', '&\x8c5Ï\U000e5f01Ø', '\U0005d415\U000a3df2h\U000f24e5\U00104d7b34¹ÒÀ', '\U000e374c8', 'w'}), 'quiet': False, 'honor_noqa': False, 'dedup_headings': True, 'known_other': {'\x10\x1bm': frozenset({'\U000682a49\U000e1a63²KǶ4', '', '\x1a', '©'}), '': frozenset({'íå\x94Ì', '\U000cf258'})}, 'treat_comments_as_code': frozenset({''}), 'length_sort': True, 'reverse_relative': True, 'combine_as_imports': True, 'py_version': 'all', 'use_parentheses': True, 'skip_gitignore': True, 'remove_imports': frozenset({'', '\U00076fe7þs\x0c\U000c8b75v\U00106541', '𥒒>\U0001960euj𒎕\x9e', '\x15\x9b', '\x02l', '\U000b71ef.\x1c', '\x7f?\U000ec91c', '\x7f,ÞoÀP8\x1b\x1e»3\x86\x94¤ÁÓ~\U00066b1a,O\U0010ab28\x90«', 'Y\x06ºZ\x04Ýì\U00078ce1.\U0010c1f9[EK\x83EÖø', ';À¨|\x1b 𑐒🍸V'}), 'atomic': False, 'source': 'runtime'}), virtual_env='', conda_env='', ensure_newline_before_comments=False, directory='/home/abuild/rpmbuild/BUILD/isort-5.5.1', profile='', honor_noqa=False, old_finders=False, remove_redundant_aliases=False, float_to_top=False, filter_files=False, formatting_function=None, color_output=False, treat_comments_as_code=frozenset({''}), treat_all_comments_as_code=False, supported_extensions=frozenset({'py', 'pyx', 'pyi'}), blocked_extensions=frozenset({'pex'}), constants=frozenset(), classes=frozenset({'\U000eb6c6\x9eÑ\U0008297dâhï\x8eÆ', 'C', '\x8e\U000422ac±\U000b5a1f\U000c4166', 'ùÚ'}), variables=frozenset(), dedup_headings=True), - disregard_skip=True + config=isort.Config( + py_version="all", + force_to_top=frozenset(), + skip=frozenset( + { + ".svn", + ".venv", + "build", + "dist", + ".bzr", + ".tox", + ".hg", + ".mypy_cache", + ".nox", + "_build", + "buck-out", + "node_modules", + ".git", + ".eggs", + ".pants.d", + "venv", + ".direnv", + } + ), + skip_glob=frozenset(), + skip_gitignore=True, + line_length=79, + wrap_length=0, + line_ending="", + sections=("FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"), + no_sections=False, + known_future_library=frozenset({"__future__"}), + known_third_party=frozenset(), + known_first_party=frozenset(), + known_local_folder=frozenset(), + known_standard_library=frozenset( + { + "pwd", + "types", + "nntplib", + "jpeg", + "pyclbr", + "encodings", + "ctypes", + "macerrors", + "filecmp", + "dbm", + "mimetypes", + "statvfs", + "msvcrt", + "spwd", + "codecs", + "SimpleHTTPServer", + "compiler", + "pickletools", + "tkinter", + "pickle", + "fm", + "bsddb", + "contextvars", + "dummy_thread", + "pipes", + "heapq", + "dircache", + "commands", + "unicodedata", + "ntpath", + "marshal", + "fpformat", + "linecache", + "calendar", + "pty", + "MimeWriter", + "inspect", + "mmap", + "ic", + "tty", + "nis", + "new", + "wave", + "HTMLParser", + "anydbm", + "tracemalloc", + "pdb", + "sunau", + "GL", + "parser", + "winsound", + "dbhash", + "zlib", + "MacOS", + "pprint", + "crypt", + "aetools", + "DEVICE", + "fl", + "gettext", + "asyncore", + "copyreg", + "queue", + "resource", + "turtledemo", + "fnmatch", + "hotshot", + "trace", + "string", + "plistlib", + "gzip", + "functools", + "aepack", + "hashlib", + "imp", + "MiniAEFrame", + "getpass", + "shutil", + "ttk", + "multifile", + "operator", + "reprlib", + "subprocess", + "cgi", + "select", + "SimpleXMLRPCServer", + "audioop", + "macresource", + "stringprep", + "wsgiref", + "SUNAUDIODEV", + "atexit", + "lzma", + "asyncio", + "datetime", + "binhex", + "autoGIL", + "doctest", + "thread", + "enum", + "tempfile", + "posixfile", + "mhlib", + "html", + "itertools", + "exceptions", + "sgmllib", + "array", + "test", + "imputil", + "shlex", + "flp", + "uu", + "gdbm", + "urlparse", + "msilib", + "termios", + "modulefinder", + "ossaudiodev", + "timeit", + "binascii", + "popen2", + "ConfigParser", + "poplib", + "zipfile", + "cfmfile", + "pstats", + "AL", + "contextlib", + "code", + "zipimport", + "base64", + "platform", + "ast", + "fileinput", + "locale", + "buildtools", + "stat", + "quopri", + "readline", + "collections", + "aetypes", + "concurrent", + "runpy", + "copy_reg", + "rexec", + "cmath", + "optparse", + "dummy_threading", + "ColorPicker", + "sched", + "netrc", + "sunaudiodev", + "socketserver", + "logging", + "PixMapWrapper", + "sysconfig", + "Nav", + "copy", + "cmd", + "csv", + "chunk", + "multiprocessing", + "warnings", + "weakref", + "py_compile", + "sre", + "sre_parse", + "curses", + "threading", + "re", + "FrameWork", + "_thread", + "imgfile", + "cd", + "sre_constants", + "xdrlib", + "dataclasses", + "urllib2", + "StringIO", + "configparser", + "importlib", + "UserList", + "posixpath", + "mailbox", + "rfc822", + "grp", + "pydoc", + "sets", + "textwrap", + "numbers", + "W", + "gl", + "htmllib", + "macostools", + "tarfile", + "ipaddress", + "xmlrpc", + "icopen", + "traceback", + "_winreg", + "random", + "CGIHTTPServer", + "dis", + "sha", + "selectors", + "statistics", + "DocXMLRPCServer", + "imghdr", + "venv", + "keyword", + "xmlrpclib", + "ftplib", + "getopt", + "posix", + "smtpd", + "profile", + "sndhdr", + "signal", + "EasyDialogs", + "dumbdbm", + "fcntl", + "SocketServer", + "distutils", + "symbol", + "pathlib", + "cStringIO", + "imaplib", + "unittest", + "al", + "cProfile", + "robotparser", + "BaseHTTPServer", + "os", + "pkgutil", + "socket", + "fractions", + "shelve", + "aifc", + "cgitb", + "xml", + "decimal", + "sre_compile", + "ssl", + "user", + "Bastion", + "formatter", + "time", + "abc", + "winreg", + "difflib", + "FL", + "bz2", + "asynchat", + "gc", + "gensuitemodule", + "symtable", + "secrets", + "Carbon", + "mailcap", + "sys", + "bdb", + "fpectl", + "httplib", + "webbrowser", + "smtplib", + "Cookie", + "whichdb", + "turtle", + "tokenize", + "UserString", + "tabnanny", + "site", + "struct", + "codeop", + "email", + "typing", + "cookielib", + "Queue", + "rlcompleter", + "errno", + "macpath", + "videoreader", + "md5", + "cPickle", + "Tix", + "io", + "faulthandler", + "Tkinter", + "glob", + "syslog", + "telnetlib", + "_dummy_thread", + "hmac", + "uuid", + "imageop", + "future_builtins", + "json", + "htmlentitydefs", + "lib2to3", + "UserDict", + "mutex", + "sqlite3", + "findertools", + "bisect", + "builtins", + "urllib", + "http", + "compileall", + "argparse", + "ScrolledText", + "token", + "dl", + "applesingle", + "math", + "ensurepip", + "mimify", + "mimetools", + "colorsys", + "zipapp", + "__builtin__", + } + ), + extra_standard_library=frozenset(), + known_other={"other": frozenset({"", "\x10\x1bm"})}, + multi_line_output=0, + forced_separate=(), + indent=" ", + comment_prefix=" #", + length_sort=True, + length_sort_straight=False, + length_sort_sections=frozenset(), + add_imports=frozenset(), + remove_imports=frozenset( + { + "", + "\U00076fe7þs\x0c\U000c8b75v\U00106541", + "𥒒>\U0001960euj𒎕\x9e", + "\x15\x9b", + "\x02l", + "\U000b71ef.\x1c", + "\x7f?\U000ec91c", + "\x7f,ÞoÀP8\x1b\x1e»3\x86\x94¤ÁÓ~\U00066b1a,O\U0010ab28\x90«", + "Y\x06ºZ\x04Ýì\U00078ce1.\U0010c1f9[EK\x83EÖø", + ";À¨|\x1b 𑐒🍸V", + } + ), + append_only=False, + reverse_relative=True, + force_single_line=False, + single_line_exclusions=( + "Y\U000347d9g\x957K", + "", + "Ê\U000e8ad2\U0008fa72ùÏ\x19ç\U000eaecc𤎪.", + "·o\U000d00e5\U000b36de+\x8f\U000b5953´\x08oÜ", + "", + ":sI¶", + "", + ), + default_section="THIRDPARTY", + import_headings={}, + balanced_wrapping=False, + use_parentheses=True, + order_by_type=True, + atomic=False, + lines_after_imports=-1, + lines_between_sections=1, + lines_between_types=0, + combine_as_imports=True, + combine_star=False, + include_trailing_comma=False, + from_first=False, + verbose=False, + quiet=False, + force_adds=False, + force_alphabetical_sort_within_sections=False, + force_alphabetical_sort=False, + force_grid_wrap=0, + force_sort_within_sections=False, + lexicographical=False, + ignore_whitespace=False, + no_lines_before=frozenset( + { + "uøø", + "¢", + "&\x8c5Ï\U000e5f01Ø", + "\U0005d415\U000a3df2h\U000f24e5\U00104d7b34¹ÒÀ", + "\U000e374c8", + "w", + } + ), + no_inline_sort=False, + ignore_comments=False, + case_sensitive=False, + sources=( + { + "py_version": "py3", + "force_to_top": frozenset(), + "skip": frozenset( + { + ".svn", + ".venv", + "build", + "dist", + ".bzr", + ".tox", + ".hg", + ".mypy_cache", + ".nox", + "_build", + "buck-out", + "node_modules", + ".git", + ".eggs", + ".pants.d", + "venv", + ".direnv", + } + ), + "skip_glob": frozenset(), + "skip_gitignore": False, + "line_length": 79, + "wrap_length": 0, + "line_ending": "", + "sections": ("FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"), + "no_sections": False, + "known_future_library": frozenset({"__future__"}), + "known_third_party": frozenset(), + "known_first_party": frozenset(), + "known_local_folder": frozenset(), + "known_standard_library": frozenset( + { + "pwd", + "copy", + "cmd", + "csv", + "chunk", + "multiprocessing", + "warnings", + "types", + "weakref", + "nntplib", + "pyclbr", + "encodings", + "py_compile", + "sre", + "ctypes", + "sre_parse", + "filecmp", + "curses", + "threading", + "dbm", + "re", + "_thread", + "sre_constants", + "xdrlib", + "dataclasses", + "mimetypes", + "configparser", + "importlib", + "msvcrt", + "spwd", + "posixpath", + "mailbox", + "codecs", + "grp", + "pickletools", + "tkinter", + "pickle", + "contextvars", + "pydoc", + "textwrap", + "numbers", + "pipes", + "heapq", + "tarfile", + "unicodedata", + "ntpath", + "ipaddress", + "marshal", + "xmlrpc", + "traceback", + "linecache", + "calendar", + "pty", + "random", + "dis", + "selectors", + "statistics", + "imghdr", + "venv", + "inspect", + "mmap", + "keyword", + "ftplib", + "tty", + "nis", + "getopt", + "posix", + "smtpd", + "wave", + "profile", + "sndhdr", + "signal", + "tracemalloc", + "pdb", + "sunau", + "winsound", + "parser", + "zlib", + "fcntl", + "pprint", + "distutils", + "crypt", + "symbol", + "gettext", + "pathlib", + "asyncore", + "copyreg", + "imaplib", + "unittest", + "queue", + "resource", + "turtledemo", + "fnmatch", + "cProfile", + "os", + "pkgutil", + "socket", + "trace", + "fractions", + "string", + "shelve", + "plistlib", + "aifc", + "gzip", + "functools", + "cgitb", + "xml", + "hashlib", + "decimal", + "imp", + "sre_compile", + "ssl", + "formatter", + "winreg", + "time", + "getpass", + "shutil", + "abc", + "difflib", + "bz2", + "operator", + "reprlib", + "subprocess", + "cgi", + "select", + "asynchat", + "audioop", + "gc", + "secrets", + "symtable", + "mailcap", + "sys", + "bdb", + "fpectl", + "stringprep", + "webbrowser", + "smtplib", + "wsgiref", + "atexit", + "lzma", + "asyncio", + "datetime", + "binhex", + "doctest", + "turtle", + "enum", + "tempfile", + "tokenize", + "tabnanny", + "site", + "html", + "struct", + "itertools", + "codeop", + "email", + "array", + "test", + "typing", + "shlex", + "uu", + "msilib", + "termios", + "rlcompleter", + "modulefinder", + "ossaudiodev", + "timeit", + "binascii", + "poplib", + "errno", + "macpath", + "zipfile", + "io", + "faulthandler", + "pstats", + "contextlib", + "code", + "glob", + "zipimport", + "base64", + "syslog", + "platform", + "ast", + "fileinput", + "telnetlib", + "locale", + "_dummy_thread", + "hmac", + "stat", + "uuid", + "quopri", + "readline", + "collections", + "json", + "concurrent", + "lib2to3", + "sqlite3", + "runpy", + "cmath", + "optparse", + "bisect", + "builtins", + "urllib", + "dummy_threading", + "http", + "compileall", + "argparse", + "token", + "sched", + "netrc", + "math", + "ensurepip", + "socketserver", + "colorsys", + "zipapp", + "logging", + "sysconfig", + } + ), + "extra_standard_library": frozenset(), + "known_other": {}, + "multi_line_output": 0, + "forced_separate": (), + "indent": " ", + "comment_prefix": " #", + "length_sort": False, + "length_sort_straight": False, + "length_sort_sections": frozenset(), + "add_imports": frozenset(), + "remove_imports": frozenset(), + "append_only": False, + "reverse_relative": False, + "force_single_line": False, + "single_line_exclusions": (), + "default_section": "THIRDPARTY", + "import_headings": {}, + "balanced_wrapping": False, + "use_parentheses": False, + "order_by_type": True, + "atomic": False, + "lines_after_imports": -1, + "lines_between_sections": 1, + "lines_between_types": 0, + "combine_as_imports": False, + "combine_star": False, + "include_trailing_comma": False, + "from_first": False, + "verbose": False, + "quiet": False, + "force_adds": False, + "force_alphabetical_sort_within_sections": False, + "force_alphabetical_sort": False, + "force_grid_wrap": 0, + "force_sort_within_sections": False, + "lexicographical": False, + "ignore_whitespace": False, + "no_lines_before": frozenset(), + "no_inline_sort": False, + "ignore_comments": False, + "case_sensitive": False, + "sources": (), + "virtual_env": "", + "conda_env": "", + "ensure_newline_before_comments": False, + "directory": "", + "profile": "", + "honor_noqa": False, + "src_paths": frozenset(), + "old_finders": False, + "remove_redundant_aliases": False, + "float_to_top": False, + "filter_files": False, + "formatter": "", + "formatting_function": None, + "color_output": False, + "treat_comments_as_code": frozenset(), + "treat_all_comments_as_code": False, + "supported_extensions": frozenset({"py", "pyx", "pyi"}), + "blocked_extensions": frozenset({"pex"}), + "constants": frozenset(), + "classes": frozenset(), + "variables": frozenset(), + "dedup_headings": False, + "source": "defaults", + }, + { + "classes": frozenset( + { + "\U000eb6c6\x9eÑ\U0008297dâhï\x8eÆ", + "C", + "\x8e\U000422ac±\U000b5a1f\U000c4166", + "ùÚ", + } + ), + "single_line_exclusions": ( + "Y\U000347d9g\x957K", + "", + "Ê\U000e8ad2\U0008fa72ùÏ\x19ç\U000eaecc𤎪.", + "·o\U000d00e5\U000b36de+\x8f\U000b5953´\x08oÜ", + "", + ":sI¶", + "", + ), + "indent": " ", + "no_lines_before": frozenset( + { + "uøø", + "¢", + "&\x8c5Ï\U000e5f01Ø", + "\U0005d415\U000a3df2h\U000f24e5\U00104d7b34¹ÒÀ", + "\U000e374c8", + "w", + } + ), + "quiet": False, + "honor_noqa": False, + "dedup_headings": True, + "known_other": { + "\x10\x1bm": frozenset({"\U000682a49\U000e1a63²KǶ4", "", "\x1a", "©"}), + "": frozenset({"íå\x94Ì", "\U000cf258"}), + }, + "treat_comments_as_code": frozenset({""}), + "length_sort": True, + "reverse_relative": True, + "combine_as_imports": True, + "py_version": "all", + "use_parentheses": True, + "skip_gitignore": True, + "remove_imports": frozenset( + { + "", + "\U00076fe7þs\x0c\U000c8b75v\U00106541", + "𥒒>\U0001960euj𒎕\x9e", + "\x15\x9b", + "\x02l", + "\U000b71ef.\x1c", + "\x7f?\U000ec91c", + "\x7f,ÞoÀP8\x1b\x1e»3\x86\x94¤ÁÓ~\U00066b1a,O\U0010ab28\x90«", + "Y\x06ºZ\x04Ýì\U00078ce1.\U0010c1f9[EK\x83EÖø", + ";À¨|\x1b 𑐒🍸V", + } + ), + "atomic": False, + "source": "runtime", + }, + ), + virtual_env="", + conda_env="", + ensure_newline_before_comments=False, + directory="/home/abuild/rpmbuild/BUILD/isort-5.5.1", + profile="", + honor_noqa=False, + old_finders=False, + remove_redundant_aliases=False, + float_to_top=False, + filter_files=False, + formatting_function=None, + color_output=False, + treat_comments_as_code=frozenset({""}), + treat_all_comments_as_code=False, + supported_extensions=frozenset({"py", "pyx", "pyi"}), + blocked_extensions=frozenset({"pex"}), + constants=frozenset(), + classes=frozenset( + {"\U000eb6c6\x9eÑ\U0008297dâhï\x8eÆ", "C", "\x8e\U000422ac±\U000b5a1f\U000c4166", "ùÚ"} + ), + variables=frozenset(), + dedup_headings=True, + ), + disregard_skip=True, ) @hypothesis.given( config=st.from_type(isort.Config), From 77c6a5e34ed8f785ed264b4a16872ba5505f4509 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 8 Sep 2020 23:35:44 -0700 Subject: [PATCH 1034/1439] Remove code climate link; Resolve #1449 --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 0e1f3ce93..153772603 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,6 @@ [![Test Status](https://github.com/pycqa/isort/workflows/Test/badge.svg?branch=develop)](https://github.com/pycqa/isort/actions?query=workflow%3ATest) [![Lint Status](https://github.com/pycqa/isort/workflows/Lint/badge.svg?branch=develop)](https://github.com/pycqa/isort/actions?query=workflow%3ALint) [![Code coverage Status](https://codecov.io/gh/pycqa/isort/branch/develop/graph/badge.svg)](https://codecov.io/gh/pycqa/isort) -[![Maintainability](https://api.codeclimate.com/v1/badges/060372d3e77573072609/maintainability)](https://codeclimate.com/github/timothycrosley/isort/maintainability) [![License](https://img.shields.io/github/license/mashape/apistatus.svg)](https://pypi.org/project/isort/) [![Join the chat at https://gitter.im/timothycrosley/isort](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/timothycrosley/isort?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Downloads](https://pepy.tech/badge/isort)](https://pepy.tech/project/isort) From 7431db7744ca858e8b6787b571c66c5140582918 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 9 Sep 2020 20:52:20 -0700 Subject: [PATCH 1035/1439] Fixed #1469: --diff option is ignored when input is from stdin. Release hot fix release 5.5.2 --- CHANGELOG.md | 3 +++ isort/_version.py | 2 +- isort/main.py | 1 + pyproject.toml | 2 +- tests/unit/test_main.py | 17 +++++++++++++++++ 5 files changed, 23 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c504a2fb..f6a0ce5a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. Find out more about isort's release policy [here](https://pycqa.github.io/isort/docs/major_releases/release_policy/). +### 5.5.2 [Hotfix] September 9, 2020 + - Fixed #1469: --diff option is ignored when input is from stdin. + ### 5.5.1 September 4, 2020 - Fixed #1454: Ensure indented import sections with import heading and a preceding comment don't cause import sorting loops. - Fixed #1453: isort error when float to top on almost empty file. diff --git a/isort/_version.py b/isort/_version.py index f680f9ffe..9fe27eebf 100644 --- a/isort/_version.py +++ b/isort/_version.py @@ -1 +1 @@ -__version__ = "5.5.1" +__version__ = "5.5.2" diff --git a/isort/main.py b/isort/main.py index e7dc61ad5..5dc2ef188 100644 --- a/isort/main.py +++ b/isort/main.py @@ -836,6 +836,7 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = input_stream=sys.stdin if stdin is None else stdin, output_stream=sys.stdout, config=config, + show_diff=show_diff, ) else: skipped: List[str] = [] diff --git a/pyproject.toml b/pyproject.toml index 3aff16105..31e126565 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ line-length = 100 [tool.poetry] name = "isort" -version = "5.5.1" +version = "5.5.2" description = "A Python utility / library to sort Python imports." authors = ["Timothy Crosley "] license = "MIT" diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index ae63fd607..2457f2cb1 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -165,6 +165,23 @@ def test_main(capsys, tmpdir): """ ) + # Should be able to stream diff + input_content = TextIOWrapper( + BytesIO( + b""" +import b +import a +""" + ) + ) + main.main(config_args + ["-", "--diff"], stdin=input_content) + out, error = capsys.readouterr() + assert not error + assert "+" in out + assert "-" in out + assert "import a" in out + assert "import b" in out + # Should be able to run with just a file python_file = tmpdir.join("has_imports.py") python_file.write( From 24efea1e6cb5247f696e072c8ff40c350b425ce0 Mon Sep 17 00:00:00 2001 From: Alexandre Yang Date: Thu, 10 Sep 2020 16:30:56 +0200 Subject: [PATCH 1036/1439] Test Datadog integrations-core --- tests/integration/test_projects_using_isort.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/integration/test_projects_using_isort.py b/tests/integration/test_projects_using_isort.py index bb7d61de5..417a6c72e 100644 --- a/tests/integration/test_projects_using_isort.py +++ b/tests/integration/test_projects_using_isort.py @@ -170,3 +170,18 @@ def test_attrs(tmpdir): "_compat.py", ] ) + + +def test_datadog_integrations_core(tmpdir): + check_call( + [ + "git", + "clone", + "--depth", + "1", + "https://github.com/DataDog/integrations-core.git", + str(tmpdir), + ] + ) + + main(["--check-only", "--diff", str(tmpdir), "--skip", "tests"]) From 8feacc297cc295edfb828a3b95898cce5abe4c66 Mon Sep 17 00:00:00 2001 From: Alexandre Yang Date: Thu, 10 Sep 2020 16:50:50 +0200 Subject: [PATCH 1037/1439] remove skip tests --- tests/integration/test_projects_using_isort.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/test_projects_using_isort.py b/tests/integration/test_projects_using_isort.py index 417a6c72e..1b877fdad 100644 --- a/tests/integration/test_projects_using_isort.py +++ b/tests/integration/test_projects_using_isort.py @@ -184,4 +184,4 @@ def test_datadog_integrations_core(tmpdir): ] ) - main(["--check-only", "--diff", str(tmpdir), "--skip", "tests"]) + main(["--check-only", "--diff", str(tmpdir)]) From 95367522ca8dd7e2a1cda56ec7e86972bf024536 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 10 Sep 2020 20:19:38 -0700 Subject: [PATCH 1038/1439] Update precommit version --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ad78369f1..586a5edda 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ repos: - repo: https://github.com/pycqa/isort - rev: 5.3.0 + rev: 5.5.2 hooks: - id: isort From 5f81e6216c6fa5d14ee86f4e80616943620c1453 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 10 Sep 2020 22:42:21 -0700 Subject: [PATCH 1039/1439] Resolve #1457: Improve handling of unsupported configuration option errors --- CHANGELOG.md | 1 + isort/exceptions.py | 24 ++++++++++++++++++++++++ isort/settings.py | 20 +++++++++++++++++++- tests/unit/test_exceptions.py | 8 ++++++++ tests/unit/test_settings.py | 8 ++++++++ 5 files changed, 60 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 682c6a347..96503547c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ ### 5.6.0 TBD - Implemented #1433: Provide helpful feedback in case a custom config file is specified without a configuration. + - Improved handling of unsupported configuration option errors (see #1475). - Fixed #1463: Better interactive documentation for future option. - Fixed #1461: Quiet config option not respected by file API in some circumstances. diff --git a/isort/exceptions.py b/isort/exceptions.py index 9f45744c7..3d152587b 100644 --- a/isort/exceptions.py +++ b/isort/exceptions.py @@ -1,4 +1,6 @@ """All isort specific exception classes should be defined here""" +from typing import Any, Dict + from .profiles import profiles @@ -132,3 +134,25 @@ def __init__(self, code: str): "...\n\n" ) self.code = code + + +class UnsupportedSettings(ISortError): + """Raised when settings are passed into isort (either from config, CLI, or runtime) + that it doesn't support. + """ + + def _format_option(self, name: str, value: Any, source: str) -> str: + return f"\t- {name} = {value} (source: '{source}')" + + def __init__(self, unsupported_settings: Dict[str, Dict[str, str]]): + errors = "\n".join( + self._format_option(name, **option) for name, option in unsupported_settings.items() + ) + + super().__init__( + "isort was provided settings that it doesn't support:\n\n" + f"{errors}\n\n" + "For a complete and up-to-date listing of supported settings see: " + "https://pycqa.github.io/isort/docs/configuration/options/.\n" + ) + self.unsupported_settings = unsupported_settings diff --git a/isort/settings.py b/isort/settings.py index 1a0a1fc14..126934c13 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -18,7 +18,12 @@ from . import stdlibs from ._future import dataclass, field from ._vendored import toml -from .exceptions import FormattingPluginDoesNotExist, InvalidSettingsPath, ProfileDoesNotExist +from .exceptions import ( + FormattingPluginDoesNotExist, + InvalidSettingsPath, + ProfileDoesNotExist, + UnsupportedSettings, +) from .profiles import profiles from .sections import DEFAULT as SECTION_DEFAULTS from .sections import FIRSTPARTY, FUTURE, LOCALFOLDER, STDLIB, THIRDPARTY @@ -432,6 +437,19 @@ def __init__( combined_config.pop(f"{IMPORT_HEADING_PREFIX}{import_heading_key}") combined_config["import_headings"] = import_headings + unsupported_config_errors = {} + for option in set(combined_config.keys()).difference( + getattr(_Config, "__dataclass_fields__", {}).keys() + ): + for source in reversed(sources): + if option in source: + unsupported_config_errors[option] = { + "value": source[option], + "source": source["source"], + } + if unsupported_config_errors: + raise UnsupportedSettings(unsupported_config_errors) + super().__init__(sources=tuple(sources), **combined_config) # type: ignore def is_supported_filetype(self, file_name: str): diff --git a/tests/unit/test_exceptions.py b/tests/unit/test_exceptions.py index 8a6f60fcc..bb6ee2a31 100644 --- a/tests/unit/test_exceptions.py +++ b/tests/unit/test_exceptions.py @@ -82,3 +82,11 @@ def setup_class(self): def test_variables(self): assert self.instance.code == "print x" + + +class TestUnsupportedSettings(TestISortError): + def setup_class(self): + self.instance = exceptions.UnsupportedSettings({"apply": {"value": "true", "source": "/"}}) + + def test_variables(self): + assert self.instance.unsupported_settings == {"apply": {"value": "true", "source": "/"}} diff --git a/tests/unit/test_settings.py b/tests/unit/test_settings.py index fb2316181..1f6a52080 100644 --- a/tests/unit/test_settings.py +++ b/tests/unit/test_settings.py @@ -14,6 +14,14 @@ class TestConfig: def test_init(self): assert Config() + def test_init_unsupported_settings_fails_gracefully(self): + with pytest.raises(exceptions.UnsupportedSettings): + Config(apply=True) + try: + Config(apply=True) + except exceptions.UnsupportedSettings as error: + assert error.unsupported_settings == {"apply": {"value": True, "source": "runtime"}} + def test_known_settings(self): assert Config(known_third_party=["one"]).known_third_party == frozenset({"one"}) assert Config(known_thirdparty=["two"]).known_third_party == frozenset({"two"}) From 1e9bea3c84d24867b60751f7fb4e001285e35151 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 10 Sep 2020 22:52:13 -0700 Subject: [PATCH 1040/1439] Fix errors found by deepsource --- .deepsource.toml | 1 + isort/exceptions.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.deepsource.toml b/.deepsource.toml index 81eded698..cfbbec30a 100644 --- a/.deepsource.toml +++ b/.deepsource.toml @@ -14,3 +14,4 @@ enabled = true [analyzers.meta] runtime_version = "3.x.x" + max_line_length = 100 diff --git a/isort/exceptions.py b/isort/exceptions.py index 3d152587b..265928e98 100644 --- a/isort/exceptions.py +++ b/isort/exceptions.py @@ -141,7 +141,8 @@ class UnsupportedSettings(ISortError): that it doesn't support. """ - def _format_option(self, name: str, value: Any, source: str) -> str: + @staticmethod + def _format_option(name: str, value: Any, source: str) -> str: return f"\t- {name} = {value} (source: '{source}')" def __init__(self, unsupported_settings: Dict[str, Dict[str, str]]): From 1f64554e7e94a94734ce832ebd06b06226a4d0be Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 10 Sep 2020 23:23:00 -0700 Subject: [PATCH 1041/1439] Resolve #1471: Confusing documentation for force-grid-wrap --- docs/configuration/options.md | 76 +++++++++++++++++++++++++++++------ isort/main.py | 7 ++-- 2 files changed, 67 insertions(+), 16 deletions(-) diff --git a/docs/configuration/options.md b/docs/configuration/options.md index 10c1cfb8a..bf2205243 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -4,7 +4,8 @@ As a code formatter isort has opinions. However, it also allows you to have your isort will disagree but commit to your way of formatting. To enable this, isort exposes a plethora of options to specify how you want your imports sorted, organized, and formatted. -Too busy to build your perfect isort configuration? For curated common configurations, see isort's [built-in profiles](https://pycqa.github.io/isort/docs/configuration/profiles/). +Too busy to build your perfect isort configuration? For curated common configurations, see isort's [built-in +profiles](https://pycqa.github.io/isort/docs/configuration/profiles/). ## Python Version @@ -35,7 +36,7 @@ Force specific imports to the top of their appropriate section. Files that sort imports should skip over. If you want to skip multiple files you should specify twice: --skip file1 --skip file2. **Type:** Frozenset -**Default:** `('.eggs', '.git', '.hg', '.mypy_cache', '.nox', '.pants.d', '.tox', '.venv', '_build', 'buck-out', 'build', 'dist', 'node_modules', 'venv')` +**Default:** `('.bzr', '.direnv', '.eggs', '.git', '.hg', '.mypy_cache', '.nox', '.pants.d', '.svn', '.tox', '.venv', '_build', 'buck-out', 'build', 'dist', 'node_modules', 'venv')` **Python & Config File Name:** skip **CLI Flags:** @@ -128,7 +129,7 @@ Put all imports into the same section bucket ## Known Future Library -Force isort to recognize a module as part of the future compatibility libraries. +Force isort to recognize a module as part of Python's internal future compatibility libraries. WARNING: this overrides the behavior of __future__ handling and therefore can result in code that can't execute. If you're looking to add dependencies such as six a better option is to create a another section below --future using custom sections. See: https://github.com/PyCQA/isort#custom-sections-and-ordering and the discussion here: https://github.com/PyCQA/isort/issues/1463. **Type:** Frozenset **Default:** `('__future__',)` @@ -293,14 +294,13 @@ Sort imports by their string length. - --ls - --length-sort -## Length Sort Straight Imports +## Length Sort Straight -Sort straight imports by their string length. Similar to `length_sort` but applies only to -straight imports and doesn't affect from imports. +Sort straight imports by their string length. -**Type:** Bool -**Default:** `False` -**Python & Config File Name:** length_sort_straight +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** length_sort_straight **CLI Flags:** - --lss @@ -602,7 +602,7 @@ Force all imports to be sorted as a single section ## Force Grid Wrap -Force number of from imports (defaults to 2) to be grid wrapped regardless of line length +Force number of from imports (defaults to 2 when passed as CLI flag without value) to be grid wrapped regardless of line length. If 0 is passed in (the global default) only line length is considered. **Type:** Int **Default:** `0` @@ -880,7 +880,7 @@ Tells isort to treat all single line comments as if they are code. Specifies what extensions isort can be ran against. **Type:** Frozenset -**Default:** `('.py', '.pyi', '.pyx')` +**Default:** `('py', 'pyi', 'pyx')` **Python & Config File Name:** supported_extensions **CLI Flags:** @@ -893,12 +893,62 @@ Specifies what extensions isort can be ran against. Specifies what extensions isort can never be ran against. **Type:** Frozenset -**Default:** `('.pex',)` +**Default:** `('pex',)` **Python & Config File Name:** blocked_extensions **CLI Flags:** - --blocked-extension +## Constants + +**No Description** + +**Type:** Frozenset +**Default:** `frozenset()` +**Python & Config File Name:** constants +**CLI Flags:** **Not Supported** + +## Classes + +**No Description** + +**Type:** Frozenset +**Default:** `frozenset()` +**Python & Config File Name:** classes +**CLI Flags:** **Not Supported** + +## Variables + +**No Description** + +**Type:** Frozenset +**Default:** `frozenset()` +**Python & Config File Name:** variables +**CLI Flags:** **Not Supported** + +## Dedup Headings + +Tells isort to only show an identical custom import heading comment once, even if there are multiple sections with the comment set. + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** dedup_headings +**CLI Flags:** + +- --dedup-headings + +## Only Sections + +Causes imports to be sorted only based on their sections like STDLIB,THIRDPARTY etc. Imports are unaltered and keep their relative positions within the different sections. + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** only_sections +**CLI Flags:** + +- --only-sections +- --os + ## Check Checks the file for unsorted / unformatted imports and prints them to the command line without modifying the file. @@ -948,7 +998,7 @@ Number of files to process in parallel. - -j - --jobs -## Don't Order By Type +## Dont Order By Type Don't order imports by type, which is determined by case, in addition to alphabetically. diff --git a/isort/main.py b/isort/main.py index c1bf35ebb..56f015837 100644 --- a/isort/main.py +++ b/isort/main.py @@ -302,8 +302,8 @@ def _build_arg_parser() -> argparse.ArgumentParser: const=2, type=int, dest="force_grid_wrap", - help="Force number of from imports (defaults to 2) to be grid wrapped regardless of line " - "length", + help="Force number of from imports (defaults to 2 when passed as CLI flag without value) to be grid wrapped regardless of line " + "length. If 0 is passed in (the global default) only line length is considered.", ) parser.add_argument( "--fss", @@ -343,7 +343,8 @@ def _build_arg_parser() -> argparse.ArgumentParser: parser.add_argument( "--lss", "--length-sort-straight", - help="Sort straight imports by their string length.", + help="Sort straight imports by their string length. Similar to `length_sort` " + "but applies only to straight imports and doesn't affect from imports.", dest="length_sort_straight", action="store_true", ) From b5a9b61029ad09f7a8d3c1ed02ca8c6c63fdab5b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 10 Sep 2020 23:29:39 -0700 Subject: [PATCH 1042/1439] Add missing contributors (Aniruddha Bhattacharjee (@anirudnits), Alexandre Yang (@AlexandreYang), and Andrew Howe (@howeaj) --- docs/contributing/4.-acknowledgements.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index db3fde562..ef8aecb56 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -194,6 +194,9 @@ Code Contributors - Guillaume Lostis (@glostis) - Krzysztof Jagiełło (@kjagiello) - Nicholas Devenish (@ndevenish) +- Aniruddha Bhattacharjee (@anirudnits) +- Alexandre Yang (@AlexandreYang) +- Andrew Howe (@howeaj) Documenters =================== From 23f5fd69ef8e9ea7149309a034feb83ecd778704 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 11 Sep 2020 00:45:38 -0700 Subject: [PATCH 1043/1439] Fix formatting --- isort/main.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/isort/main.py b/isort/main.py index 56f015837..cd71a40a1 100644 --- a/isort/main.py +++ b/isort/main.py @@ -302,7 +302,8 @@ def _build_arg_parser() -> argparse.ArgumentParser: const=2, type=int, dest="force_grid_wrap", - help="Force number of from imports (defaults to 2 when passed as CLI flag without value) to be grid wrapped regardless of line " + help="Force number of from imports (defaults to 2 when passed as CLI flag without value)" + "to be grid wrapped regardless of line " "length. If 0 is passed in (the global default) only line length is considered.", ) parser.add_argument( @@ -344,7 +345,7 @@ def _build_arg_parser() -> argparse.ArgumentParser: "--lss", "--length-sort-straight", help="Sort straight imports by their string length. Similar to `length_sort` " - "but applies only to straight imports and doesn't affect from imports.", + "but applies only to straight imports and doesn't affect from imports.", dest="length_sort_straight", action="store_true", ) From b0e555fa3001c777990114b9f79015994d47157f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 12 Sep 2020 23:55:53 -0700 Subject: [PATCH 1044/1439] Update cruft template --- .cruft.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.cruft.json b/.cruft.json index f70213bc6..dcbd6c8eb 100644 --- a/.cruft.json +++ b/.cruft.json @@ -1,6 +1,6 @@ { "template": "https://github.com/timothycrosley/cookiecutter-python/", - "commit": "4fe165a760a98a06d3fbef89aae3149767e489f3", + "commit": "ff6836bfaa247c65ff50b39c520ed12d91bf5a20", "context": { "cookiecutter": { "full_name": "Timothy Crosley", @@ -13,4 +13,4 @@ } }, "directory": "" -} +} \ No newline at end of file From 7071d038feb43f3a2db19d735d7a9910ef0808fc Mon Sep 17 00:00:00 2001 From: anirudnits Date: Sun, 13 Sep 2020 15:47:59 +0530 Subject: [PATCH 1045/1439] omitted the coverage for deprecated directory --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 56861b744..b9a746519 100644 --- a/.coveragerc +++ b/.coveragerc @@ -4,6 +4,7 @@ omit = isort/_vendored/* except ImportError: tests/* + isort/deprecated/* [report] exclude_lines = From 31c2d3d1b85714b115bc2a32279fc7a605cd0faf Mon Sep 17 00:00:00 2001 From: anirudnits Date: Sun, 13 Sep 2020 15:48:33 +0530 Subject: [PATCH 1046/1439] removed deprecated_finders.py from test suite --- setup.cfg | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/setup.cfg b/setup.cfg index a75e3dffd..8a59aaffd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,10 +14,7 @@ strict_optional = False [tool:pytest] testpaths = tests -filterwarnings = - ignore::DeprecationWarning:distlib - ignore::DeprecationWarning:requirementslib - ignore::hypothesis.errors.NonInteractiveExampleWarning:hypothesis + [flake8] max-line-length = 100 From 3d1ee23caaff702bf09e1615cb67be8e4a2906fd Mon Sep 17 00:00:00 2001 From: anirudnits Date: Sun, 13 Sep 2020 15:49:45 +0530 Subject: [PATCH 1047/1439] moved property testing from hypothesis-auto to hypothesis using ghostwriter --- tests/unit/test_comments.py | 36 ++- tests/unit/test_format.py | 28 +- tests/unit/test_main.py | 23 +- tests/unit/test_output.py | 23 +- tests/unit/test_parse.py | 41 ++- tests/unit/test_wrap_modes.py | 520 ++++++++++++++++++++++++++++++++-- 6 files changed, 630 insertions(+), 41 deletions(-) diff --git a/tests/unit/test_comments.py b/tests/unit/test_comments.py index 4098f8ec6..31ae8a6c4 100644 --- a/tests/unit/test_comments.py +++ b/tests/unit/test_comments.py @@ -1,10 +1,34 @@ -from hypothesis_auto import auto_pytest_magic +from hypothesis import given +from hypothesis import strategies as st -from isort import comments - -auto_pytest_magic(comments.parse) -auto_pytest_magic(comments.add_to_line) +import isort.comments def test_add_to_line(): - assert comments.add_to_line([], "import os # comment", removed=True).strip() == "import os" + assert ( + isort.comments.add_to_line([], "import os # comment", removed=True).strip() == "import os" + ) + + +# These tests were written by the `hypothesis.extra.ghostwriter` module +# and is provided under the Creative Commons Zero public domain dedication. + + +@given( + comments=st.one_of(st.none(), st.lists(st.text())), + original_string=st.text(), + removed=st.booleans(), + comment_prefix=st.text(), +) +def test_fuzz_add_to_line(comments, original_string, removed, comment_prefix): + isort.comments.add_to_line( + comments=comments, + original_string=original_string, + removed=removed, + comment_prefix=comment_prefix, + ) + + +@given(line=st.text()) +def test_fuzz_parse(line): + isort.comments.parse(line=line) diff --git a/tests/unit/test_format.py b/tests/unit/test_format.py index fee61c67d..f331578a8 100644 --- a/tests/unit/test_format.py +++ b/tests/unit/test_format.py @@ -1,14 +1,14 @@ from io import StringIO +from pathlib import Path from unittest.mock import MagicMock, patch import colorama import pytest -from hypothesis_auto import auto_pytest_magic +from hypothesis import given, reject +from hypothesis import strategies as st import isort.format -auto_pytest_magic(isort.format.show_unified_diff, auto_allow_exceptions_=(UnicodeEncodeError,)) - def test_ask_whether_to_apply_changes_to_file(): with patch("isort.format.input", MagicMock(return_value="y")): @@ -97,3 +97,25 @@ def test_colorama_not_available_handled_gracefully(capsys): _, err = capsys.readouterr() assert "colorama" in err assert "colors extra" in err + + +# This test code was written by the `hypothesis.extra.ghostwriter` module +# and is provided under the Creative Commons Zero public domain dedication. + + +@given( + file_input=st.text(), + file_output=st.text(), + file_path=st.one_of(st.none(), st.builds(Path)), + output=st.one_of(st.none(), st.builds(StringIO, st.text())), +) +def test_fuzz_show_unified_diff(file_input, file_output, file_path, output): + try: + isort.format.show_unified_diff( + file_input=file_input, + file_output=file_output, + file_path=file_path, + output=output, + ) + except UnicodeEncodeError: + reject() diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index e4193e516..92faafd4b 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -4,7 +4,8 @@ from io import BytesIO, TextIOWrapper import pytest -from hypothesis_auto import auto_pytest_magic +from hypothesis import given +from hypothesis import strategies as st from isort import main from isort._version import __version__ @@ -12,7 +13,25 @@ from isort.settings import DEFAULT_CONFIG, Config from isort.wrap_modes import WrapModes -auto_pytest_magic(main.sort_imports) +# This test code was written by the `hypothesis.extra.ghostwriter` module +# and is provided under the Creative Commons Zero public domain dedication. + + +@given( + file_name=st.text(), + config=st.builds(Config), + check=st.booleans(), + ask_to_apply=st.booleans(), + write_to_stdout=st.booleans(), +) +def test_fuzz_sort_imports(file_name, config, check, ask_to_apply, write_to_stdout): + main.sort_imports( + file_name=file_name, + config=config, + check=check, + ask_to_apply=ask_to_apply, + write_to_stdout=write_to_stdout, + ) def test_iter_source_code(tmpdir): diff --git a/tests/unit/test_output.py b/tests/unit/test_output.py index 2457f70d7..b4d8ed1a4 100644 --- a/tests/unit/test_output.py +++ b/tests/unit/test_output.py @@ -1,5 +1,22 @@ -from hypothesis_auto import auto_pytest_magic +from hypothesis import given, reject +from hypothesis import strategies as st -from isort import output +import isort.comments -auto_pytest_magic(output.with_comments, auto_allow_exceptions_=(ValueError,)) + +@given( + comments=st.one_of(st.none(), st.lists(st.text())), + original_string=st.text(), + removed=st.booleans(), + comment_prefix=st.text(), +) +def test_fuzz_add_to_line(comments, original_string, removed, comment_prefix): + try: + isort.comments.add_to_line( + comments=comments, + original_string=original_string, + removed=removed, + comment_prefix=comment_prefix, + ) + except ValueError: + reject() diff --git a/tests/unit/test_parse.py b/tests/unit/test_parse.py index 685ebcf73..98183617d 100644 --- a/tests/unit/test_parse.py +++ b/tests/unit/test_parse.py @@ -1,4 +1,5 @@ -from hypothesis_auto import auto_pytest_magic +from hypothesis import given +from hypothesis import strategies as st from isort import parse from isort.settings import Config @@ -44,7 +45,37 @@ def test_file_contents(): assert original_line_count == len(in_lines) -auto_pytest_magic(parse.import_type) -auto_pytest_magic(parse.skip_line) -auto_pytest_magic(parse._strip_syntax) -auto_pytest_magic(parse._infer_line_separator) +# These tests were written by the `hypothesis.extra.ghostwriter` module +# and is provided under the Creative Commons Zero public domain dedication. + + +@given(contents=st.text()) +def test_fuzz__infer_line_separator(contents): + parse._infer_line_separator(contents=contents) + + +@given(import_string=st.text()) +def test_fuzz__strip_syntax(import_string): + parse._strip_syntax(import_string=import_string) + + +@given(line=st.text(), config=st.builds(Config)) +def test_fuzz_import_type(line, config): + parse.import_type(line=line, config=config) + + +@given( + line=st.text(), + in_quote=st.text(), + index=st.integers(), + section_comments=st.lists(st.text()).map(tuple), + needs_import=st.booleans(), +) +def test_fuzz_skip_line(line, in_quote, index, section_comments, needs_import): + parse.skip_line( + line=line, + in_quote=in_quote, + index=index, + section_comments=section_comments, + needs_import=needs_import, + ) diff --git a/tests/unit/test_wrap_modes.py b/tests/unit/test_wrap_modes.py index c95a75136..3ef01ae0c 100644 --- a/tests/unit/test_wrap_modes.py +++ b/tests/unit/test_wrap_modes.py @@ -1,29 +1,9 @@ -from hypothesis_auto import auto_pytest_magic +from hypothesis import given, reject +from hypothesis import strategies as st import isort from isort import wrap_modes -auto_pytest_magic(wrap_modes.grid, auto_allow_exceptions_=(ValueError,)) -auto_pytest_magic(wrap_modes.vertical, auto_allow_exceptions_=(ValueError,)) -auto_pytest_magic(wrap_modes.hanging_indent, auto_allow_exceptions_=(ValueError,)) -auto_pytest_magic(wrap_modes.vertical_hanging_indent, auto_allow_exceptions_=(ValueError,)) -auto_pytest_magic(wrap_modes.vertical_grid, auto_allow_exceptions_=(ValueError,)) -auto_pytest_magic(wrap_modes.vertical_grid_grouped, auto_allow_exceptions_=(ValueError,)) -auto_pytest_magic(wrap_modes.vertical_grid_grouped_no_comma, auto_allow_exceptions_=(ValueError,)) -auto_pytest_magic(wrap_modes.noqa, auto_allow_exceptions_=(ValueError,)) -auto_pytest_magic(wrap_modes.noqa, auto_allow_exceptions_=(ValueError,), comments=["NOQA"]) -auto_pytest_magic( - wrap_modes.vertical_prefix_from_module_import, auto_allow_exceptions_=(ValueError,) -) -auto_pytest_magic(wrap_modes.vertical_hanging_indent_bracket, auto_allow_exceptions_=(ValueError,)) -auto_pytest_magic( - wrap_modes.vertical_hanging_indent_bracket, - auto_allow_exceptions_=(ValueError,), - imports=["one", "two"], -) -auto_pytest_magic(wrap_modes.hanging_indent_with_parentheses, auto_allow_exceptions_=(ValueError,)) -auto_pytest_magic(wrap_modes.backslash_grid, auto_allow_exceptions_=(ValueError,)) - def test_wrap_mode_interface(): assert ( @@ -94,3 +74,499 @@ def test_backslash_grid(): handlers as handlers_, patches, resources """ ) + + +# This test code was written by the `hypothesis.extra.ghostwriter` module +# and is provided under the Creative Commons Zero public domain dedication. + + +@given( + statement=st.text(), + imports=st.lists(st.text()), + white_space=st.text(), + indent=st.text(), + line_length=st.integers(), + comments=st.lists(st.text()), + line_separator=st.text(), + comment_prefix=st.text(), + include_trailing_comma=st.booleans(), + remove_comments=st.booleans(), +) +def test_fuzz_backslash_grid( + statement, + imports, + white_space, + indent, + line_length, + comments, + line_separator, + comment_prefix, + include_trailing_comma, + remove_comments, +): + try: + isort.wrap_modes.backslash_grid( + statement=statement, + imports=imports, + white_space=white_space, + indent=indent, + line_length=line_length, + comments=comments, + line_separator=line_separator, + comment_prefix=comment_prefix, + include_trailing_comma=include_trailing_comma, + remove_comments=remove_comments, + ) + except ValueError: + reject() + + +@given( + statement=st.text(), + imports=st.lists(st.text()), + white_space=st.text(), + indent=st.text(), + line_length=st.integers(), + comments=st.lists(st.text()), + line_separator=st.text(), + comment_prefix=st.text(), + include_trailing_comma=st.booleans(), + remove_comments=st.booleans(), +) +def test_fuzz_grid( + statement, + imports, + white_space, + indent, + line_length, + comments, + line_separator, + comment_prefix, + include_trailing_comma, + remove_comments, +): + try: + isort.wrap_modes.grid( + statement=statement, + imports=imports, + white_space=white_space, + indent=indent, + line_length=line_length, + comments=comments, + line_separator=line_separator, + comment_prefix=comment_prefix, + include_trailing_comma=include_trailing_comma, + remove_comments=remove_comments, + ) + except ValueError: + reject() + + +@given( + statement=st.text(), + imports=st.lists(st.text()), + white_space=st.text(), + indent=st.text(), + line_length=st.integers(), + comments=st.lists(st.text()), + line_separator=st.text(), + comment_prefix=st.text(), + include_trailing_comma=st.booleans(), + remove_comments=st.booleans(), +) +def test_fuzz_hanging_indent( + statement, + imports, + white_space, + indent, + line_length, + comments, + line_separator, + comment_prefix, + include_trailing_comma, + remove_comments, +): + try: + isort.wrap_modes.hanging_indent( + statement=statement, + imports=imports, + white_space=white_space, + indent=indent, + line_length=line_length, + comments=comments, + line_separator=line_separator, + comment_prefix=comment_prefix, + include_trailing_comma=include_trailing_comma, + remove_comments=remove_comments, + ) + except ValueError: + reject() + + +@given( + statement=st.text(), + imports=st.lists(st.text()), + white_space=st.text(), + indent=st.text(), + line_length=st.integers(), + comments=st.lists(st.text()), + line_separator=st.text(), + comment_prefix=st.text(), + include_trailing_comma=st.booleans(), + remove_comments=st.booleans(), +) +def test_fuzz_hanging_indent_with_parentheses( + statement, + imports, + white_space, + indent, + line_length, + comments, + line_separator, + comment_prefix, + include_trailing_comma, + remove_comments, +): + try: + isort.wrap_modes.hanging_indent_with_parentheses( + statement=statement, + imports=imports, + white_space=white_space, + indent=indent, + line_length=line_length, + comments=comments, + line_separator=line_separator, + comment_prefix=comment_prefix, + include_trailing_comma=include_trailing_comma, + remove_comments=remove_comments, + ) + except ValueError: + reject() + + +@given( + statement=st.text(), + imports=st.lists(st.text()), + white_space=st.text(), + indent=st.text(), + line_length=st.integers(), + comments=st.lists(st.text()), + line_separator=st.text(), + comment_prefix=st.text(), + include_trailing_comma=st.booleans(), + remove_comments=st.booleans(), +) +def test_fuzz_noqa( + statement, + imports, + white_space, + indent, + line_length, + comments, + line_separator, + comment_prefix, + include_trailing_comma, + remove_comments, +): + try: + isort.wrap_modes.noqa( + statement=statement, + imports=imports, + white_space=white_space, + indent=indent, + line_length=line_length, + comments=comments, + line_separator=line_separator, + comment_prefix=comment_prefix, + include_trailing_comma=include_trailing_comma, + remove_comments=remove_comments, + ) + except ValueError: + reject() + + +@given( + statement=st.text(), + imports=st.lists(st.text()), + white_space=st.text(), + indent=st.text(), + line_length=st.integers(), + comments=st.lists(st.text()), + line_separator=st.text(), + comment_prefix=st.text(), + include_trailing_comma=st.booleans(), + remove_comments=st.booleans(), +) +def test_fuzz_vertical( + statement, + imports, + white_space, + indent, + line_length, + comments, + line_separator, + comment_prefix, + include_trailing_comma, + remove_comments, +): + try: + isort.wrap_modes.vertical( + statement=statement, + imports=imports, + white_space=white_space, + indent=indent, + line_length=line_length, + comments=comments, + line_separator=line_separator, + comment_prefix=comment_prefix, + include_trailing_comma=include_trailing_comma, + remove_comments=remove_comments, + ) + except ValueError: + reject() + + +@given( + statement=st.text(), + imports=st.lists(st.text()), + white_space=st.text(), + indent=st.text(), + line_length=st.integers(), + comments=st.lists(st.text()), + line_separator=st.text(), + comment_prefix=st.text(), + include_trailing_comma=st.booleans(), + remove_comments=st.booleans(), +) +def test_fuzz_vertical_grid( + statement, + imports, + white_space, + indent, + line_length, + comments, + line_separator, + comment_prefix, + include_trailing_comma, + remove_comments, +): + try: + isort.wrap_modes.vertical_grid( + statement=statement, + imports=imports, + white_space=white_space, + indent=indent, + line_length=line_length, + comments=comments, + line_separator=line_separator, + comment_prefix=comment_prefix, + include_trailing_comma=include_trailing_comma, + remove_comments=remove_comments, + ) + except ValueError: + reject() + + +@given( + statement=st.text(), + imports=st.lists(st.text()), + white_space=st.text(), + indent=st.text(), + line_length=st.integers(), + comments=st.lists(st.text()), + line_separator=st.text(), + comment_prefix=st.text(), + include_trailing_comma=st.booleans(), + remove_comments=st.booleans(), +) +def test_fuzz_vertical_grid_grouped( + statement, + imports, + white_space, + indent, + line_length, + comments, + line_separator, + comment_prefix, + include_trailing_comma, + remove_comments, +): + try: + isort.wrap_modes.vertical_grid_grouped( + statement=statement, + imports=imports, + white_space=white_space, + indent=indent, + line_length=line_length, + comments=comments, + line_separator=line_separator, + comment_prefix=comment_prefix, + include_trailing_comma=include_trailing_comma, + remove_comments=remove_comments, + ) + except ValueError: + reject() + + +@given( + statement=st.text(), + imports=st.lists(st.text()), + white_space=st.text(), + indent=st.text(), + line_length=st.integers(), + comments=st.lists(st.text()), + line_separator=st.text(), + comment_prefix=st.text(), + include_trailing_comma=st.booleans(), + remove_comments=st.booleans(), +) +def test_fuzz_vertical_grid_grouped_no_comma( + statement, + imports, + white_space, + indent, + line_length, + comments, + line_separator, + comment_prefix, + include_trailing_comma, + remove_comments, +): + try: + isort.wrap_modes.vertical_grid_grouped_no_comma( + statement=statement, + imports=imports, + white_space=white_space, + indent=indent, + line_length=line_length, + comments=comments, + line_separator=line_separator, + comment_prefix=comment_prefix, + include_trailing_comma=include_trailing_comma, + remove_comments=remove_comments, + ) + except ValueError: + reject() + + +@given( + statement=st.text(), + imports=st.lists(st.text()), + white_space=st.text(), + indent=st.text(), + line_length=st.integers(), + comments=st.lists(st.text()), + line_separator=st.text(), + comment_prefix=st.text(), + include_trailing_comma=st.booleans(), + remove_comments=st.booleans(), +) +def test_fuzz_vertical_hanging_indent( + statement, + imports, + white_space, + indent, + line_length, + comments, + line_separator, + comment_prefix, + include_trailing_comma, + remove_comments, +): + try: + isort.wrap_modes.vertical_hanging_indent( + statement=statement, + imports=imports, + white_space=white_space, + indent=indent, + line_length=line_length, + comments=comments, + line_separator=line_separator, + comment_prefix=comment_prefix, + include_trailing_comma=include_trailing_comma, + remove_comments=remove_comments, + ) + except ValueError: + reject() + + +@given( + statement=st.text(), + imports=st.lists(st.text()), + white_space=st.text(), + indent=st.text(), + line_length=st.integers(), + comments=st.lists(st.text()), + line_separator=st.text(), + comment_prefix=st.text(), + include_trailing_comma=st.booleans(), + remove_comments=st.booleans(), +) +def test_fuzz_vertical_hanging_indent_bracket( + statement, + imports, + white_space, + indent, + line_length, + comments, + line_separator, + comment_prefix, + include_trailing_comma, + remove_comments, +): + try: + isort.wrap_modes.vertical_hanging_indent_bracket( + statement=statement, + imports=imports, + white_space=white_space, + indent=indent, + line_length=line_length, + comments=comments, + line_separator=line_separator, + comment_prefix=comment_prefix, + include_trailing_comma=include_trailing_comma, + remove_comments=remove_comments, + ) + except ValueError: + reject() + + +@given( + statement=st.text(), + imports=st.lists(st.text()), + white_space=st.text(), + indent=st.text(), + line_length=st.integers(), + comments=st.lists(st.text()), + line_separator=st.text(), + comment_prefix=st.text(), + include_trailing_comma=st.booleans(), + remove_comments=st.booleans(), +) +def test_fuzz_vertical_prefix_from_module_import( + statement, + imports, + white_space, + indent, + line_length, + comments, + line_separator, + comment_prefix, + include_trailing_comma, + remove_comments, +): + try: + isort.wrap_modes.vertical_prefix_from_module_import( + statement=statement, + imports=imports, + white_space=white_space, + indent=indent, + line_length=line_length, + comments=comments, + line_separator=line_separator, + comment_prefix=comment_prefix, + include_trailing_comma=include_trailing_comma, + remove_comments=remove_comments, + ) + except ValueError: + reject() From dd8d0df389c5c211da34a333d2cdb6447a79a7c3 Mon Sep 17 00:00:00 2001 From: anirudnits Date: Sun, 13 Sep 2020 15:53:54 +0530 Subject: [PATCH 1048/1439] moved testing of deprecated_finders outside of test suite --- scripts/test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/test.sh b/scripts/test.sh index 5aa1c7648..d76d8cce4 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -2,5 +2,5 @@ set -euxo pipefail ./scripts/lint.sh -poetry run pytest tests/unit/ -s --cov=isort/ --cov-report=term-missing ${@-} +poetry run pytest tests/unit/ -s --cov=isort/ --cov-report=term-missing ${@-} --ignore=tests/unit/test_deprecated_finders.py poetry run coverage html From 59c4ca0469598e8891b495519b07cb27e726a4c2 Mon Sep 17 00:00:00 2001 From: swedge Date: Sun, 13 Sep 2020 17:14:29 -0400 Subject: [PATCH 1049/1439] Fixes #1479 --- tests/unit/test_main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 92faafd4b..fc5938289 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -292,5 +292,5 @@ def test_main(capsys, tmpdir): def test_isort_command(): - """Ensure ISortCommand got registered, otherwise setuptools error must have occured""" + """Ensure ISortCommand got registered, otherwise setuptools error must have occurred""" assert main.ISortCommand From fc8f7841203abaf8d557ceecadc19c175138d31f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 14 Sep 2020 23:17:35 -0700 Subject: [PATCH 1050/1439] Fix plugin definition to match pylama expectation --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 31e126565..4b3bdff4d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,7 +86,7 @@ isort = "isort.main:main" isort = "isort.main:ISortCommand" [tool.poetry.plugins."pylama.linter"] -isort = "isort = isort.pylama_isort:Linter" +isort = "isort.pylama_isort:Linter" [tool.portray.mkdocs] edit_uri = "https://github.com/pycqa/isort/edit/develop/" From 7d6580f2eb0e142a7ff7c77e6fc1d75f2a3d71b3 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 14 Sep 2020 23:19:05 -0700 Subject: [PATCH 1051/1439] Fix pylama integration to work with file skip comments --- isort/pylama_isort.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/isort/pylama_isort.py b/isort/pylama_isort.py index 0e14d5696..4d4ffb922 100644 --- a/isort/pylama_isort.py +++ b/isort/pylama_isort.py @@ -5,6 +5,8 @@ from pylama.lint import Linter as BaseLinter +from isort.exceptions import FileSkipped + from . import api @@ -25,9 +27,17 @@ def allow(self, path: str) -> bool: def run(self, path: str, **meta: Any) -> List[Dict[str, Any]]: """Lint the file. Return an array of error dicts if appropriate.""" with supress_stdout(): - if not api.check_file(path): - return [ - {"lnum": 0, "col": 0, "text": "Incorrectly sorted imports.", "type": "ISORT"} - ] - else: - return [] + try: + if not api.check_file(path, disregard_skip=False): + return [ + { + "lnum": 0, + "col": 0, + "text": "Incorrectly sorted imports.", + "type": "ISORT", + } + ] + except FileSkipped: + pass + + return [] From d34620af3434aeb42aa98cf17af5d9dcec18b89d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 14 Sep 2020 23:19:39 -0700 Subject: [PATCH 1052/1439] Update to latest cruft --- .cruft.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.cruft.json b/.cruft.json index f70213bc6..dcbd6c8eb 100644 --- a/.cruft.json +++ b/.cruft.json @@ -1,6 +1,6 @@ { "template": "https://github.com/timothycrosley/cookiecutter-python/", - "commit": "4fe165a760a98a06d3fbef89aae3149767e489f3", + "commit": "ff6836bfaa247c65ff50b39c520ed12d91bf5a20", "context": { "cookiecutter": { "full_name": "Timothy Crosley", @@ -13,4 +13,4 @@ } }, "directory": "" -} +} \ No newline at end of file From 48081a925d5b69e18a1f04c74cbe98b590e77c5b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 14 Sep 2020 23:26:48 -0700 Subject: [PATCH 1053/1439] Add a test for skip functionality --- tests/unit/test_pylama_isort.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/unit/test_pylama_isort.py b/tests/unit/test_pylama_isort.py index b7b78c138..1fe19bfa1 100644 --- a/tests/unit/test_pylama_isort.py +++ b/tests/unit/test_pylama_isort.py @@ -17,3 +17,8 @@ def test_run(self, src_dir, tmpdir): incorrect = tmpdir.join("incorrect.py") incorrect.write("import b\nimport a\n") assert self.instance.run(str(incorrect)) + + def test_skip(self, src_dir, tmpdir): + incorrect = tmpdir.join("incorrect.py") + incorrect.write("# isort: skip_file\nimport b\nimport a\n") + assert not self.instance.run(str(incorrect)) From 226f18ab0a2cfde43748aeb577e77a4bc02ba598 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 14 Sep 2020 23:27:55 -0700 Subject: [PATCH 1054/1439] Mark Fix #1482: pylama integration - in changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96503547c..ac76e56ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ - Improved handling of unsupported configuration option errors (see #1475). - Fixed #1463: Better interactive documentation for future option. - Fixed #1461: Quiet config option not respected by file API in some circumstances. + - Fixed #1482: pylama integration is not working correctly out-of-the-box. ### 5.5.2 [Hotfix] September 9, 2020 - Fixed #1469: --diff option is ignored when input is from stdin. From 0349b3685061e22835bb93335d778d2d13831b76 Mon Sep 17 00:00:00 2001 From: Sang-Heon Jeon Date: Thu, 17 Sep 2020 00:25:22 +0900 Subject: [PATCH 1055/1439] Exit non zero if all of passed path is broken - Check that path is broken at `iter_source_code` - Only exit all passed path is broken --- isort/main.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/isort/main.py b/isort/main.py index cd71a40a1..95e910d5d 100644 --- a/isort/main.py +++ b/isort/main.py @@ -114,7 +114,9 @@ def sort_imports( raise -def iter_source_code(paths: Iterable[str], config: Config, skipped: List[str]) -> Iterator[str]: +def iter_source_code( + paths: Iterable[str], config: Config, skipped: List[str], broken: List[str] +) -> Iterator[str]: """Iterate over all Python source files defined in paths.""" visited_dirs: Set[Path] = set() @@ -142,6 +144,8 @@ def iter_source_code(paths: Iterable[str], config: Config, skipped: List[str]) - skipped.append(filename) else: yield filepath + elif not os.path.exists(path): + broken.append(path) else: yield path @@ -837,6 +841,7 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = deprecated_flags = config_dict.pop("deprecated_flags", False) remapped_deprecated_args = config_dict.pop("remapped_deprecated_args", False) wrong_sorted_files = False + all_attempt_broken = False if "src_paths" in config_dict: config_dict["src_paths"] = { @@ -856,6 +861,7 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = ) else: skipped: List[str] = [] + broken: List[str] = [] if config.filter_files: filtered_files = [] @@ -866,8 +872,9 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = filtered_files.append(file_name) file_names = filtered_files - file_names = iter_source_code(file_names, config, skipped) + file_names = iter_source_code(file_names, config, skipped, broken) num_skipped = 0 + num_broken = 0 if config.verbose: print(ASCII_ART) @@ -899,6 +906,8 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = for file_name in file_names ) + # If any files passed in are missing considered as error, should be removed + is_no_attempt = True for sort_attempt in attempt_iterator: if not sort_attempt: continue # pragma: no cover - shouldn't happen, satisfies type constraint @@ -909,6 +918,7 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = num_skipped += ( 1 # pragma: no cover - shouldn't happen, due to skip in iter_source_code ) + is_no_attempt = False num_skipped += len(skipped) if num_skipped and not arguments.get("quiet", False): @@ -920,6 +930,16 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = ) print(f"Skipped {num_skipped} files") + num_broken += len(broken) + if num_broken and not arguments.get("quite", False): + if config.verbose: + for was_broken in broken: + warn(f"{was_broken} was broken path, make sure it exists correctly") + print(f"Broken {num_broken} paths") + + if num_broken > 0 and is_no_attempt: + all_attempt_broken = True + if not config.quiet and (remapped_deprecated_args or deprecated_flags): if remapped_deprecated_args: warn( @@ -939,6 +959,9 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = if wrong_sorted_files: sys.exit(1) + if all_attempt_broken: + sys.exit(1) + if __name__ == "__main__": main() From eaad31ae0b3625959aafeee7fb269b8fa659d3bd Mon Sep 17 00:00:00 2001 From: Sang-Heon Jeon Date: Thu, 17 Sep 2020 00:26:00 +0900 Subject: [PATCH 1056/1439] Add testcase that process broken path --- tests/unit/test_isort.py | 26 ++++++++++++++++++++++---- tests/unit/test_main.py | 11 ++++++++++- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/tests/unit/test_isort.py b/tests/unit/test_isort.py index d86156ba8..91eb1d610 100644 --- a/tests/unit/test_isort.py +++ b/tests/unit/test_isort.py @@ -3243,13 +3243,14 @@ def test_safety_skips(tmpdir, enabled: bool) -> None: else: config = Config(skip=[], directory=str(tmpdir)) skipped = [] # type: List[str] + broken = [] # type: List[str] codes = [str(tmpdir)] - main.iter_source_code(codes, config, skipped) + main.iter_source_code(codes, config, skipped, broken) # if enabled files within nested unsafe directories should be skipped file_names = { os.path.relpath(f, str(tmpdir)) - for f in main.iter_source_code([str(tmpdir)], config, skipped) + for f in main.iter_source_code([str(tmpdir)], config, skipped, broken) } if enabled: assert file_names == {"victim.py"} @@ -3266,7 +3267,9 @@ def test_safety_skips(tmpdir, enabled: bool) -> None: # directly pointing to files within unsafe directories shouldn't skip them either way file_names = { os.path.relpath(f, str(toxdir)) - for f in main.iter_source_code([str(toxdir)], Config(directory=str(toxdir)), skipped) + for f in main.iter_source_code( + [str(toxdir)], Config(directory=str(toxdir)), skipped, broken + ) } assert file_names == {"verysafe.py"} @@ -3287,14 +3290,29 @@ def test_skip_glob(tmpdir, skip_glob_assert: Tuple[List[str], int, Set[str]]) -> config = Config(skip_glob=skip_glob, directory=str(base_dir)) skipped = [] # type: List[str] + broken = [] # type: List[str] file_names = { os.path.relpath(f, str(base_dir)) - for f in main.iter_source_code([str(base_dir)], config, skipped) + for f in main.iter_source_code([str(base_dir)], config, skipped, broken) } assert len(skipped) == skipped_count assert file_names == file_names_expected +def test_broken(tmpdir) -> None: + base_dir = tmpdir.mkdir("broken") + + config = Config(directory=str(base_dir)) + skipped = [] # type: List[str] + broken = [] # type: List[str] + file_names = { + os.path.relpath(f, str(base_dir)) + for f in main.iter_source_code(["not-exist"], config, skipped, broken) + } + assert len(broken) == 1 + assert file_names == set() + + def test_comments_not_removed_issue_576() -> None: test_input = ( "import distutils\n" diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index fc5938289..82561b38c 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -37,7 +37,7 @@ def test_fuzz_sort_imports(file_name, config, check, ask_to_apply, write_to_stdo def test_iter_source_code(tmpdir): tmp_file = tmpdir.join("file.py") tmp_file.write("import os, sys\n") - assert tuple(main.iter_source_code((tmp_file,), DEFAULT_CONFIG, [])) == (tmp_file,) + assert tuple(main.iter_source_code((tmp_file,), DEFAULT_CONFIG, [], [])) == (tmp_file,) def test_sort_imports(tmpdir): @@ -268,6 +268,15 @@ def test_main(capsys, tmpdir): # without filter options passed in should successfully sort files main.main([str(python_file), str(should_skip), "--verbose", "--atomic"]) + # Should raise a system exit if all passed path is broken + with pytest.raises(SystemExit): + main.main(["not-exist", "--check-only"]) + + # Should not raise system exit if any of passed path is not broken + main.main([str(python_file), "not-exist", "--verbose", "--check-only"]) + out, error = capsys.readouterr() + assert "Broken" in out + # should respect gitignore if requested. out, error = capsys.readouterr() # clear sysoutput before tests subprocess.run(["git", "init", str(tmpdir)]) From 01ea371c37e649289c98ec801c61695fed955505 Mon Sep 17 00:00:00 2001 From: Sang-Heon Jeon Date: Fri, 18 Sep 2020 00:03:11 +0900 Subject: [PATCH 1057/1439] Cleanup old style typing comments --- tests/unit/test_isort.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/unit/test_isort.py b/tests/unit/test_isort.py index 91eb1d610..b2fedb8cb 100644 --- a/tests/unit/test_isort.py +++ b/tests/unit/test_isort.py @@ -3242,8 +3242,8 @@ def test_safety_skips(tmpdir, enabled: bool) -> None: config = Config(directory=str(tmpdir)) else: config = Config(skip=[], directory=str(tmpdir)) - skipped = [] # type: List[str] - broken = [] # type: List[str] + skipped: List[str] = [] + broken: List[str] = [] codes = [str(tmpdir)] main.iter_source_code(codes, config, skipped, broken) @@ -3289,8 +3289,8 @@ def test_skip_glob(tmpdir, skip_glob_assert: Tuple[List[str], int, Set[str]]) -> code_dir.join("file.py").write("import os") config = Config(skip_glob=skip_glob, directory=str(base_dir)) - skipped = [] # type: List[str] - broken = [] # type: List[str] + skipped: List[str] = [] + broken: List[str] = [] file_names = { os.path.relpath(f, str(base_dir)) for f in main.iter_source_code([str(base_dir)], config, skipped, broken) @@ -3303,8 +3303,8 @@ def test_broken(tmpdir) -> None: base_dir = tmpdir.mkdir("broken") config = Config(directory=str(base_dir)) - skipped = [] # type: List[str] - broken = [] # type: List[str] + skipped: List[str] = [] + broken: List[str] = [] file_names = { os.path.relpath(f, str(base_dir)) for f in main.iter_source_code(["not-exist"], config, skipped, broken) From 37d6d52b343229179f6f6d96d2247dead180d7bd Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 17 Sep 2020 21:35:29 -0700 Subject: [PATCH 1058/1439] Add Sang-Heon Jeon (@lntuition) and @scottwedge to acknowledgements --- docs/contributing/4.-acknowledgements.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index ef8aecb56..f4588d763 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -197,6 +197,7 @@ Code Contributors - Aniruddha Bhattacharjee (@anirudnits) - Alexandre Yang (@AlexandreYang) - Andrew Howe (@howeaj) +- Sang-Heon Jeon (@lntuition) Documenters =================== @@ -218,6 +219,7 @@ Documenters - Kosei Kitahara (@Surgo) - Marat Sharafutdinov (@decaz) - Abtin (@abtinmo) +- @scottwedge -------------------------------------------- From 2c4e6d507fb154b9f3c4f550af2efb61530286f5 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 18 Sep 2020 21:42:44 -0700 Subject: [PATCH 1059/1439] Add failing test for issue #1488 --- tests/unit/test_regressions.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index 752adc343..849ec2b1f 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -949,3 +949,21 @@ def test_import_sorting_shouldnt_be_endless_with_headers_issue_1454(): import_heading_thirdparty="related third party imports", show_diff=True, ) + + +def test_isort_should_leave_non_import_from_lines_alone(): + """isort should never mangle non-import from statements. + See: https://github.com/PyCQA/isort/issues/1488 + """ + raise_from_should_be_ignored = """ + raise SomeException("Blah") \ + from exceptionsInfo.popitem()[1] +""" + assert isort.check(raise_from_should_be_ignored, show_diff=True) + + yield_from_should_be_ignored = """ +def generator_function(): + yield \ + from [] +""" + assert isort.check(raise_from_should_be_ignored, show_diff=True) From 15f7723e180e790b2391d19e1c5bf51f967bd6d6 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 19 Sep 2020 02:22:08 -0700 Subject: [PATCH 1060/1439] Update precommit --- .pre-commit-config.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 586a5edda..6ff5151ab 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,3 +3,6 @@ repos: rev: 5.5.2 hooks: - id: isort + files: 'isort/.*' + - id: isort + files: 'tests/.*' From 3eae619677be662a5980741a3d80f40fff2f23f7 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 19 Sep 2020 20:25:29 -0700 Subject: [PATCH 1061/1439] Initial approach to fix line continuation import identification error --- isort/core.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/isort/core.py b/isort/core.py index f3197b99d..756bfe019 100644 --- a/isort/core.py +++ b/isort/core.py @@ -67,6 +67,7 @@ def process( made_changes: bool = False stripped_line: str = "" end_of_file: bool = False + in_line_continuation: bool = False if config.float_to_top: new_input = "" @@ -227,7 +228,9 @@ def process( and stripped_line not in config.treat_comments_as_code ): import_section += line - elif stripped_line.startswith(IMPORT_START_IDENTIFIERS): + elif ( + stripped_line.startswith(IMPORT_START_IDENTIFIERS) and not in_line_continuation + ): contains_imports = True new_indent = line[: -len(line.lstrip())] @@ -270,6 +273,10 @@ def process( indent = new_indent import_section += import_statement else: + if stripped_line.endswith("\\"): + in_line_continuation = True + else: + in_line_continuation = False not_imports = True if not_imports: From c24b12022cb5111696d4b9b3ca880c4cac02634f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 19 Sep 2020 20:25:48 -0700 Subject: [PATCH 1062/1439] Update cruft --- .cruft.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.cruft.json b/.cruft.json index f70213bc6..dcbd6c8eb 100644 --- a/.cruft.json +++ b/.cruft.json @@ -1,6 +1,6 @@ { "template": "https://github.com/timothycrosley/cookiecutter-python/", - "commit": "4fe165a760a98a06d3fbef89aae3149767e489f3", + "commit": "ff6836bfaa247c65ff50b39c520ed12d91bf5a20", "context": { "cookiecutter": { "full_name": "Timothy Crosley", @@ -13,4 +13,4 @@ } }, "directory": "" -} +} \ No newline at end of file From e74dc829def793634951bb7ed6a652b8423b23a6 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 19 Sep 2020 20:54:40 -0700 Subject: [PATCH 1063/1439] Improve test cases, add test for comment that looks like line continuation --- tests/unit/test_regressions.py | 36 +++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index 849ec2b1f..cd5b9ec45 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -956,14 +956,40 @@ def test_isort_should_leave_non_import_from_lines_alone(): See: https://github.com/PyCQA/isort/issues/1488 """ raise_from_should_be_ignored = """ - raise SomeException("Blah") \ - from exceptionsInfo.popitem()[1] +raise SomeException("Blah") \\ + from exceptionsInfo.popitem()[1] """ - assert isort.check(raise_from_should_be_ignored, show_diff=True) + assert isort.check_code(raise_from_should_be_ignored, show_diff=True) yield_from_should_be_ignored = """ def generator_function(): - yield \ + yield \\ from [] """ - assert isort.check(raise_from_should_be_ignored, show_diff=True) + assert isort.check_code(yield_from_should_be_ignored, show_diff=True) + + comment_should_not_cause_ignore = """ +# one + +# two + + +def function(): + # one \\ + import b + import a +""" + assert ( + isort.code(comment_should_not_cause_ignore) + == """ +# one + +# two + + +def function(): + # one \\ + import a + import b +""" + ) From 67fb11ba05d2502d8b4367cfe98ac37cc3186488 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 19 Sep 2020 21:04:27 -0700 Subject: [PATCH 1064/1439] Extend testing to include paren case --- tests/unit/test_regressions.py | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index cd5b9ec45..72e041da4 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -964,23 +964,23 @@ def test_isort_should_leave_non_import_from_lines_alone(): yield_from_should_be_ignored = """ def generator_function(): yield \\ - from [] + from other_function()[1] """ assert isort.check_code(yield_from_should_be_ignored, show_diff=True) - comment_should_not_cause_ignore = """ + wont_ignore_comment_contiuation = """ # one # two def function(): - # one \\ + # three \\ import b import a """ assert ( - isort.code(comment_should_not_cause_ignore) + isort.code(wont_ignore_comment_contiuation) == """ # one @@ -988,8 +988,31 @@ def function(): def function(): - # one \\ + # three \\ import a import b """ ) + + will_ignore_if_non_comment_continuation = """ +# one + +# two + + +def function(): + print \\ + import b + import a +""" + assert isort.check_code(will_ignore_if_non_comment_continuation, show_diff=True) + + yield_from_parens_should_be_ignored = """ +def generator_function(): + ( + yield + from other_function()[1] + ) +""" + assert isort.check_code(yield_from_parens_should_be_ignored, show_diff=True) + From 7960bf77bba40a492eb868f14b83068f0e6790f3 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 19 Sep 2020 21:12:02 -0700 Subject: [PATCH 1065/1439] Simpler strategy for following line continuations --- isort/core.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/isort/core.py b/isort/core.py index 756bfe019..941ed7a12 100644 --- a/isort/core.py +++ b/isort/core.py @@ -67,7 +67,6 @@ def process( made_changes: bool = False stripped_line: str = "" end_of_file: bool = False - in_line_continuation: bool = False if config.float_to_top: new_input = "" @@ -229,7 +228,7 @@ def process( ): import_section += line elif ( - stripped_line.startswith(IMPORT_START_IDENTIFIERS) and not in_line_continuation + stripped_line.startswith(IMPORT_START_IDENTIFIERS) ): contains_imports = True @@ -273,10 +272,9 @@ def process( indent = new_indent import_section += import_statement else: - if stripped_line.endswith("\\"): - in_line_continuation = True - else: - in_line_continuation = False + while stripped_line.endswith("\\"): + line += input_stream.readline() + stripped_line = line.strip().split("#")[0] not_imports = True if not_imports: From d4ae7cc8d75a130daa3e0e33b657c638131abaea Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 19 Sep 2020 21:35:22 -0700 Subject: [PATCH 1066/1439] Add support for skipping parens --- isort/core.py | 19 ++++++++++++++----- tests/unit/test_regressions.py | 3 +-- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/isort/core.py b/isort/core.py index 941ed7a12..db5cfcdf6 100644 --- a/isort/core.py +++ b/isort/core.py @@ -227,9 +227,7 @@ def process( and stripped_line not in config.treat_comments_as_code ): import_section += line - elif ( - stripped_line.startswith(IMPORT_START_IDENTIFIERS) - ): + elif stripped_line.startswith(IMPORT_START_IDENTIFIERS): contains_imports = True new_indent = line[: -len(line.lstrip())] @@ -272,9 +270,20 @@ def process( indent = new_indent import_section += import_statement else: + if not import_section and "(" in stripped_line: + while ")" not in stripped_line: + new_line = input_stream.readline() + if not new_line: + break + + line += new_line + stripped_line = new_line.strip().split("#")[0] + while stripped_line.endswith("\\"): - line += input_stream.readline() - stripped_line = line.strip().split("#")[0] + new_line = input_stream.readline() + line += new_line + stripped_line = new_line.strip().split("#")[0] + not_imports = True if not_imports: diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index 72e041da4..e5a58685d 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -1006,7 +1006,7 @@ def function(): import a """ assert isort.check_code(will_ignore_if_non_comment_continuation, show_diff=True) - + yield_from_parens_should_be_ignored = """ def generator_function(): ( @@ -1015,4 +1015,3 @@ def generator_function(): ) """ assert isort.check_code(yield_from_parens_should_be_ignored, show_diff=True) - From e2cc14850acee0cc6aa3542bf513c9da774e4bbc Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 20 Sep 2020 15:18:06 -0700 Subject: [PATCH 1067/1439] Add more extensive testing for raise issue --- tests/unit/test_regressions.py | 68 +++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index e5a58685d..b56b6eeb1 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -1001,7 +1001,7 @@ def function(): def function(): - print \\ + raise \\ import b import a """ @@ -1015,3 +1015,69 @@ def generator_function(): ) """ assert isort.check_code(yield_from_parens_should_be_ignored, show_diff=True) + + yield_from_lots_of_parens_and_space_should_be_ignored = """ +def generator_function(): + ( + ( + (((( + ((((( + (( + ((( + yield + + + + from other_function()[1] + ))))))))))))) + ))) +""" + assert isort.check_code(yield_from_lots_of_parens_and_space_should_be_ignored, show_diff=True) + + yield_from_should_be_ignored_when_following_import_statement = """ +def generator_function(): + import os + + yield \\ + from other_function()[1] +""" + assert isort.check_code( + yield_from_should_be_ignored_when_following_import_statement, show_diff=True + ) + + yield_at_file_end_ignored = """ +def generator_function(): + ( + ( + (((( + ((((( + (( + ((( + yield +""" + assert isort.check_code(yield_at_file_end_ignored, show_diff=True) + + raise_at_file_end_ignored = """ +def generator_function(): + ( + ( + (((( + ((((( + (( + ((( + raise ( +""" + assert isort.check_code(raise_at_file_end_ignored, show_diff=True) + + raise_from_at_file_end_ignored = """ +def generator_function(): + ( + ( + (((( + ((((( + (( + ((( + raise \\ + from \\ +""" + assert isort.check_code(raise_from_at_file_end_ignored, show_diff=True) From c7afdcd1353e33c7a1fe67e507bb6b5150e8ddfa Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 20 Sep 2020 15:18:39 -0700 Subject: [PATCH 1068/1439] Fix #1488: isort should never mangle non-import from statements --- isort/core.py | 43 +++++++++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/isort/core.py b/isort/core.py index db5cfcdf6..d1e945d48 100644 --- a/isort/core.py +++ b/isort/core.py @@ -270,20 +270,6 @@ def process( indent = new_indent import_section += import_statement else: - if not import_section and "(" in stripped_line: - while ")" not in stripped_line: - new_line = input_stream.readline() - if not new_line: - break - - line += new_line - stripped_line = new_line.strip().split("#")[0] - - while stripped_line.endswith("\\"): - new_line = input_stream.readline() - line += new_line - stripped_line = new_line.strip().split("#")[0] - not_imports = True if not_imports: @@ -320,6 +306,7 @@ def process( raw_import_section += line if not contains_imports: output_stream.write(import_section) + else: leading_whitespace = import_section[: -len(import_section.lstrip())] trailing_whitespace = import_section[len(import_section.rstrip()) :] @@ -375,6 +362,34 @@ def process( output_stream.write(line) not_imports = False + if stripped_line and not in_quote and not import_section and not next_import_section: + if stripped_line == "yield": + while not stripped_line or stripped_line == "yield": + new_line = input_stream.readline() + if not new_line: + break + + output_stream.write(new_line) + stripped_line = new_line.strip().split("#")[0] + + if stripped_line.startswith("raise") or stripped_line.startswith("yield"): + if "(" in stripped_line: + while ")" not in stripped_line: + new_line = input_stream.readline() + if not new_line: + break + + output_stream.write(new_line) + stripped_line = new_line.strip().split("#")[0] + + while stripped_line.endswith("\\"): + new_line = input_stream.readline() + if not new_line: + break + + output_stream.write(new_line) + stripped_line = new_line.strip().split("#")[0] + return made_changes From d2f858ff70d70d095e83ca55c4b8cba2d592127b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 20 Sep 2020 15:21:00 -0700 Subject: [PATCH 1069/1439] Add note to changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6a0ce5a1..1d8a50bd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. Find out more about isort's release policy [here](https://pycqa.github.io/isort/docs/major_releases/release_policy/). +### 5.5.3 [Hotfix] September 20, 2020 + - Fixed #1488: in rare cases isort can mangle `yield from` or `raise from` statements. + ### 5.5.2 [Hotfix] September 9, 2020 - Fixed #1469: --diff option is ignored when input is from stdin. From 8095ede4bf35ceb89c2df3267c65b0accddc2ce6 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 20 Sep 2020 15:36:19 -0700 Subject: [PATCH 1070/1439] Match format of surrounding tests --- tests/unit/test_regressions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index b56b6eeb1..4869d444f 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -951,7 +951,7 @@ def test_import_sorting_shouldnt_be_endless_with_headers_issue_1454(): ) -def test_isort_should_leave_non_import_from_lines_alone(): +def test_isort_should_leave_non_import_from_lines_alone_issue_1488(): """isort should never mangle non-import from statements. See: https://github.com/PyCQA/isort/issues/1488 """ From ec78bf395883bc396bf378a2290af6d8004ea6c3 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 20 Sep 2020 15:50:02 -0700 Subject: [PATCH 1071/1439] Bump version to 5.5.3 --- isort/_version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/_version.py b/isort/_version.py index 9fe27eebf..16b899cb9 100644 --- a/isort/_version.py +++ b/isort/_version.py @@ -1 +1 @@ -__version__ = "5.5.2" +__version__ = "5.5.3" diff --git a/pyproject.toml b/pyproject.toml index 31e126565..3409893d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ line-length = 100 [tool.poetry] name = "isort" -version = "5.5.2" +version = "5.5.3" description = "A Python utility / library to sort Python imports." authors = ["Timothy Crosley "] license = "MIT" From cc34b2c0874094cc419c07d06a2a9cc4bae54255 Mon Sep 17 00:00:00 2001 From: anirudnits Date: Tue, 22 Sep 2020 00:15:49 +0530 Subject: [PATCH 1072/1439] extended check-only option to work with stdin --- isort/main.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/isort/main.py b/isort/main.py index 95e910d5d..064be1e23 100644 --- a/isort/main.py +++ b/isort/main.py @@ -853,6 +853,15 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = print(json.dumps(config.__dict__, indent=4, separators=(",", ": "), default=_preconvert)) return elif file_names == ["-"]: + if check: + incorrectly_sorted = not api.check_stream( + input_stream=sys.stdin if stdin is None else stdin, + config=config, + show_diff=show_diff, + ) + + wrong_sorted_files = incorrectly_sorted + api.sort_stream( input_stream=sys.stdin if stdin is None else stdin, output_stream=sys.stdout, From bec1b6c1b2d23eda302a2bdca40bc65b2564653a Mon Sep 17 00:00:00 2001 From: anirudnits Date: Tue, 22 Sep 2020 00:16:26 +0530 Subject: [PATCH 1073/1439] added test for check-only option with stdin --- tests/unit/test_main.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 82561b38c..44e1f7efc 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -203,6 +203,22 @@ def test_main(capsys, tmpdir): assert "import a" in out assert "import b" in out + # check should work with stdin + + input_content_check = TextIOWrapper( + BytesIO( + b""" +import b +import a +""" + ) + ) + + with pytest.raises(SystemExit): + main.main(config_args + ["-", "--check-only"], stdin=input_content_check) + out, error = capsys.readouterr() + assert error == "ERROR: Imports are incorrectly sorted and/or formatted.\n" + # Should be able to run with just a file python_file = tmpdir.join("has_imports.py") python_file.write( From ce397f6880eca421e9a73ed87ff0d0b730053634 Mon Sep 17 00:00:00 2001 From: Timothy Edmund Crosley Date: Mon, 21 Sep 2020 20:21:22 -0700 Subject: [PATCH 1074/1439] Update main.py Either check or sort, not both --- isort/main.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/isort/main.py b/isort/main.py index 064be1e23..2db643fbc 100644 --- a/isort/main.py +++ b/isort/main.py @@ -861,13 +861,13 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = ) wrong_sorted_files = incorrectly_sorted - - api.sort_stream( - input_stream=sys.stdin if stdin is None else stdin, - output_stream=sys.stdout, - config=config, - show_diff=show_diff, - ) + else: + api.sort_stream( + input_stream=sys.stdin if stdin is None else stdin, + output_stream=sys.stdout, + config=config, + show_diff=show_diff, + ) else: skipped: List[str] = [] broken: List[str] = [] From 958fed9264ec3d3c7a80419dfd27517ecbeafc4c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 21 Sep 2020 20:37:34 -0700 Subject: [PATCH 1075/1439] Mark Fixed #1492: --check does not work with stdin source. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b22f81d83..3d6809282 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ - Fixed #1463: Better interactive documentation for future option. - Fixed #1461: Quiet config option not respected by file API in some circumstances. - Fixed #1482: pylama integration is not working correctly out-of-the-box. + - Fixed #1492: --check does not work with stdin source. ### 5.5.3 [Hotfix] September 20, 2020 - Fixed #1488: in rare cases isort can mangle `yield from` or `raise from` statements. From 2c51557632643ca1f02bc6ac06d43f1ef87a143b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 23 Sep 2020 21:24:04 -0700 Subject: [PATCH 1076/1439] Fix issue #1494: pxd files should be sorted by default --- CHANGELOG.md | 1 + isort/settings.py | 2 +- tests/unit/test_settings.py | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d6809282..ebfb1f38e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ ### 5.6.0 TBD - Implemented #1433: Provide helpful feedback in case a custom config file is specified without a configuration. + - Implemented #1494: Default to sorting imports within `.pxd` files. - Improved handling of unsupported configuration option errors (see #1475). - Fixed #1463: Better interactive documentation for future option. - Fixed #1461: Quiet config option not respected by file API in some circumstances. diff --git a/isort/settings.py b/isort/settings.py index 126934c13..a173366a9 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -31,7 +31,7 @@ from .wrap_modes import from_string as wrap_mode_from_string _SHEBANG_RE = re.compile(br"^#!.*\bpython[23w]?\b") -SUPPORTED_EXTENSIONS = frozenset({"py", "pyi", "pyx"}) +SUPPORTED_EXTENSIONS = frozenset({"py", "pyi", "pyx", "pxd"}) BLOCKED_EXTENSIONS = frozenset({"pex"}) FILE_SKIP_COMMENTS: Tuple[str, ...] = ( "isort:" + "skip_file", diff --git a/tests/unit/test_settings.py b/tests/unit/test_settings.py index 1f6a52080..462b667d0 100644 --- a/tests/unit/test_settings.py +++ b/tests/unit/test_settings.py @@ -49,6 +49,7 @@ def test_is_supported_filetype(self): assert self.instance.is_supported_filetype("file.py") assert self.instance.is_supported_filetype("file.pyi") assert self.instance.is_supported_filetype("file.pyx") + assert self.instance.is_supported_filetype("file.pxd") assert not self.instance.is_supported_filetype("file.pyc") assert not self.instance.is_supported_filetype("file.txt") assert not self.instance.is_supported_filetype("file.pex") From f398389b672b397b84826c7f3947fe280c94e476 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 23 Sep 2020 22:01:22 -0700 Subject: [PATCH 1077/1439] Update integration test to skip pxd files on pandas project until they are sorted within the project --- tests/integration/test_projects_using_isort.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/integration/test_projects_using_isort.py b/tests/integration/test_projects_using_isort.py index 1b877fdad..756ea3030 100644 --- a/tests/integration/test_projects_using_isort.py +++ b/tests/integration/test_projects_using_isort.py @@ -36,10 +36,14 @@ def test_plone(tmpdir): def test_pandas(tmpdir): + # Need to limit extensions as isort has just made sorting pxd the default, and pandas + # will have not picked it up yet + # TODO: Remove below line as soon as these files are sorted on the mainline pandas project + limit_extensions = ["--ext", "py", "--ext", "pyi", "--ext", "pyx"] check_call( ["git", "clone", "--depth", "1", "https://github.com/pandas-dev/pandas.git", str(tmpdir)] ) - main(["--check-only", "--diff", str(tmpdir / "pandas"), "--skip", "__init__.py"]) + main(["--check-only", "--diff", str(tmpdir / "pandas"), "--skip", "__init__.py"] + limit_extensions) def test_fastapi(tmpdir): From abcaff93f8215addabdd12afdb594702ec816ea4 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 23 Sep 2020 22:14:47 -0700 Subject: [PATCH 1078/1439] Formatting fix --- tests/integration/test_projects_using_isort.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/integration/test_projects_using_isort.py b/tests/integration/test_projects_using_isort.py index 756ea3030..77537adaf 100644 --- a/tests/integration/test_projects_using_isort.py +++ b/tests/integration/test_projects_using_isort.py @@ -43,7 +43,10 @@ def test_pandas(tmpdir): check_call( ["git", "clone", "--depth", "1", "https://github.com/pandas-dev/pandas.git", str(tmpdir)] ) - main(["--check-only", "--diff", str(tmpdir / "pandas"), "--skip", "__init__.py"] + limit_extensions) + main( + ["--check-only", "--diff", str(tmpdir / "pandas"), "--skip", "__init__.py"] + + limit_extensions + ) def test_fastapi(tmpdir): From e50bd1e774dd15161cac7c102a4a51007af9577e Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 24 Sep 2020 23:02:08 -0700 Subject: [PATCH 1079/1439] Resolve #1495 --- isort/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/isort/main.py b/isort/main.py index 2db643fbc..fb5f3200e 100644 --- a/isort/main.py +++ b/isort/main.py @@ -665,7 +665,8 @@ def _build_arg_parser() -> argparse.ArgumentParser: dest="float_to_top", action="store_true", help="Causes all non-indented imports to float to the top of the file having its imports " - "sorted. It can be an excellent shortcut for collecting imports every once in a while " + "sorted (immediately below the top of file comment).\n" + "This can be an excellent shortcut for collecting imports every once in a while " "when you place them in the middle of a file to avoid context switching.\n\n" "*NOTE*: It currently doesn't work with cimports and introduces some extra over-head " "and a performance penalty.", From 0ea8a95eaad4698e78ab8fc0a16196778dfe9922 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 24 Sep 2020 23:19:40 -0700 Subject: [PATCH 1080/1439] Move chdir util to deprecated finders, since it is only used there and otherwise counts against coverage --- isort/deprecated/finders.py | 14 +++++++++++++- isort/utils.py | 13 ------------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/isort/deprecated/finders.py b/isort/deprecated/finders.py index 77eb23fa4..dbb6fec02 100644 --- a/isort/deprecated/finders.py +++ b/isort/deprecated/finders.py @@ -7,6 +7,7 @@ import sys import sysconfig from abc import ABCMeta, abstractmethod +from contextlib import contextmanager from fnmatch import fnmatch from functools import lru_cache from glob import glob @@ -15,7 +16,7 @@ from isort import sections from isort.settings import KNOWN_SECTION_MAPPING, Config -from isort.utils import chdir, exists_case_sensitive +from isort.utils import exists_case_sensitive try: from pipreqs import pipreqs @@ -36,6 +37,17 @@ Pipfile = None +@contextmanager +def chdir(path: str) -> Iterator[None]: + """Context manager for changing dir and restoring previous workdir after exit.""" + curdir = os.getcwd() + os.chdir(path) + try: + yield + finally: + os.chdir(curdir) + + class BaseFinder(metaclass=ABCMeta): def __init__(self, config: Config) -> None: self.config = config diff --git a/isort/utils.py b/isort/utils.py index 27f17b4a5..63b519902 100644 --- a/isort/utils.py +++ b/isort/utils.py @@ -1,7 +1,5 @@ import os import sys -from contextlib import contextmanager -from typing import Iterator def exists_case_sensitive(path: str) -> bool: @@ -16,14 +14,3 @@ def exists_case_sensitive(path: str) -> bool: directory, basename = os.path.split(path) result = basename in os.listdir(directory) return result - - -@contextmanager -def chdir(path: str) -> Iterator[None]: - """Context manager for changing dir and restoring previous workdir after exit.""" - curdir = os.getcwd() - os.chdir(path) - try: - yield - finally: - os.chdir(curdir) From 856b53043c6b841a448090f8b8147a421250083c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 24 Sep 2020 23:41:34 -0700 Subject: [PATCH 1081/1439] Add additional test for noqa wap mode to ensure 100% test coverage --- tests/unit/test_wrap_modes.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/unit/test_wrap_modes.py b/tests/unit/test_wrap_modes.py index 3ef01ae0c..29ecfd0df 100644 --- a/tests/unit/test_wrap_modes.py +++ b/tests/unit/test_wrap_modes.py @@ -47,6 +47,23 @@ def test_auto_saved(): ) == '*\x12\x07\U0009e994🁣"\U000ae787\x0e \x00\U0001ae99\U0005c3e7\U0004d08e \x1e ' ) + assert ( + wrap_modes.noqa( + **{ + "comment_prefix": " #", + "comments": ["NOQA", "THERE"], + "imports": [], + "include_trailing_comma": False, + "indent": "0\x19", + "line_length": -19659, + "line_separator": "\n", + "remove_comments": False, + "statement": "hi", + "white_space": " ", + } + ) + == "hi # NOQA THERE" + ) def test_backslash_grid(): From e2a95eb4afd7cfe6c549aeb82551b65addb4f68d Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Fri, 25 Sep 2020 16:39:50 +0200 Subject: [PATCH 1082/1439] Fix typo in README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 153772603..38f735faf 100644 --- a/README.md +++ b/README.md @@ -533,9 +533,9 @@ isort --rm "os.system" *.py The `--check-only` option ------------------------- -isort can also be used to used to verify that code is correctly -formatted by running it with `-c`. Any files that contain incorrectly -sorted and/or formatted imports will be outputted to `stderr`. +isort can also be used to verify that code is correctly formatted +by running it with `-c`. Any files that contain incorrectly sorted +and/or formatted imports will be outputted to `stderr`. ```bash isort **/*.py -c -v From 0f8eff147a1e6d6d1fa081e4738a97dd3c02e13d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 25 Sep 2020 20:42:56 -0700 Subject: [PATCH 1083/1439] Add test case for #1499: single-line multi-line string comment confuses float-to-top --- tests/unit/test_regressions.py | 77 ++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index 4869d444f..5c609d200 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -1081,3 +1081,80 @@ def generator_function(): from \\ """ assert isort.check_code(raise_from_at_file_end_ignored, show_diff=True) + + +def test_isort_float_to_top_correctly_identifies_single_line_comments_1499(): + """Test to ensure isort correctly handles the case where float to top is used + to push imports to the top and the top comment is a multiline type but only + one line. + See: https://github.com/PyCQA/isort/issues/1499 + """ + assert ( + isort.code( + '''#!/bin/bash +"""My comment""" +def foo(): + pass + +import a + +def bar(): + pass +''', + float_to_top=True, + ) + == ( + '''#!/bin/bash +"""My comment""" +import a + + +def foo(): + pass + + +def bar(): + pass +''' + ) + ) + assert ( + isort.code( + """#!/bin/bash +'''My comment''' +def foo(): + pass + +import a + +def bar(): + pass +""", + float_to_top=True, + ) + == ( + """#!/bin/bash +'''My comment''' +import a + + +def foo(): + pass + + +def bar(): + pass +""" + ) + ) + + assert isort.check_code( + """#!/bin/bash +'''My comment''' +import a + +x = 1 +""", + float_to_top=True, + show_diff=True, + ) From d9379fbb4841c57b9611954228275d4c7c82264a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 25 Sep 2020 20:43:33 -0700 Subject: [PATCH 1084/1439] Fix #1499: detect start of tripple quote comments --- isort/parse.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/isort/parse.py b/isort/parse.py index 613f7fa76..d9ab60e2c 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -200,12 +200,16 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte if skipping_line: out_lines.append(line) continue - elif ( + + lstripped_line = line.lstrip() + if ( config.float_to_top and import_index == -1 and line and not in_quote - and not line.strip().startswith("#") + and not lstripped_line.startswith("#") + and not lstripped_line.startswith("'''") + and not lstripped_line.startswith('"""') ): import_index = index - 1 while import_index and not in_lines[import_index - 1]: From ac5f198275cb98a7d49081f7399f1382beb44ee1 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 25 Sep 2020 20:44:17 -0700 Subject: [PATCH 1085/1439] Mark #1499: as fixed in changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebfb1f38e..b820a0325 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ - Fixed #1461: Quiet config option not respected by file API in some circumstances. - Fixed #1482: pylama integration is not working correctly out-of-the-box. - Fixed #1492: --check does not work with stdin source. + - Fixed #1499: isort gets confused by single line, multi-line style comments when using float-to-top. ### 5.5.3 [Hotfix] September 20, 2020 - Fixed #1488: in rare cases isort can mangle `yield from` or `raise from` statements. From 11ca48b4accb2da22302bb3a948f1f7f704da613 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 26 Sep 2020 22:00:16 -0700 Subject: [PATCH 1086/1439] Set google profile not to order by type (#1486) --- isort/profiles.py | 1 + tests/unit/profiles/test_google.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/isort/profiles.py b/isort/profiles.py index cd976cd29..77afef242 100644 --- a/isort/profiles.py +++ b/isort/profiles.py @@ -21,6 +21,7 @@ "force_sort_within_sections": True, "lexicographical": True, "single_line_exclusions": ("typing",), + "order_by_type": False, } open_stack = { "force_single_line": True, diff --git a/tests/unit/profiles/test_google.py b/tests/unit/profiles/test_google.py index c558664dd..3a40bbc30 100644 --- a/tests/unit/profiles/test_google.py +++ b/tests/unit/profiles/test_google.py @@ -118,8 +118,8 @@ def test_google_code_snippet_one(): # flake8: noqa: F401 import collections -from contextlib import ExitStack from contextlib import contextmanager +from contextlib import ExitStack import functools import inspect import itertools as it @@ -136,8 +136,8 @@ def test_google_code_snippet_one(): from . import dtypes from . import linear_util as lu from .abstract_arrays import ConcreteArray -from .abstract_arrays import ShapedArray from .abstract_arrays import raise_to_shaped +from .abstract_arrays import ShapedArray from .api_util import apply_flat_fun from .api_util import argnums_partial from .api_util import donation_vector From 52ea01b4d967bbd9d06fe6d7220c0ccd1e67cd4e Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 27 Sep 2020 15:06:18 -0700 Subject: [PATCH 1087/1439] Resolved #1502: Improved float-to-top behavior when there is an existing import section present at top-of-file. --- CHANGELOG.md | 1 + isort/core.py | 3 + isort/parse.py | 2 + tests/unit/test_ticketed_features.py | 122 ++++++++++++++++++++++++--- 4 files changed, 118 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b820a0325..68d114f3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ ### 5.6.0 TBD - Implemented #1433: Provide helpful feedback in case a custom config file is specified without a configuration. - Implemented #1494: Default to sorting imports within `.pxd` files. + - Implemented #1502: Improved float-to-top behavior when there is an existing import section present at top-of-file. - Improved handling of unsupported configuration option errors (see #1475). - Fixed #1463: Better interactive documentation for future option. - Fixed #1461: Quiet config option not respected by file API in some circumstances. diff --git a/isort/core.py b/isort/core.py index d1e945d48..11ae35f95 100644 --- a/isort/core.py +++ b/isort/core.py @@ -83,6 +83,9 @@ def process( if line == "# isort: off\n": isort_off = True if current: + if add_imports: + current += line_separator + line_separator.join(add_imports) + add_imports = [] parsed = parse.file_contents(current, config=config) extra_space = "" while current and current[-1] == "\n": diff --git a/isort/parse.py b/isort/parse.py index d9ab60e2c..6f121c6bd 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -210,6 +210,8 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte and not lstripped_line.startswith("#") and not lstripped_line.startswith("'''") and not lstripped_line.startswith('"""') + and not lstripped_line.startswith("import") + and not lstripped_line.startswith("from") ): import_index = index - 1 while import_index and not in_lines[import_index - 1]: diff --git a/tests/unit/test_ticketed_features.py b/tests/unit/test_ticketed_features.py index 75b43da89..f952bdc51 100644 --- a/tests/unit/test_ticketed_features.py +++ b/tests/unit/test_ticketed_features.py @@ -60,7 +60,8 @@ def my_function_2(): """, float_to_top=True, ) - == """import os + == """ +import os import sys @@ -90,7 +91,8 @@ def my_function_2(): """, float_to_top=True, ) - == """import os + == """ +import os def my_function_1(): @@ -105,10 +107,9 @@ def my_function_2(): """ ) - -assert ( - isort.code( - """ + assert ( + isort.code( + """ import os @@ -129,9 +130,10 @@ def my_function_2(): import a """, - float_to_top=True, - ) - == """import os + float_to_top=True, + ) + == """ +import os def my_function_1(): @@ -151,7 +153,7 @@ def y(): def my_function_2(): pass """ -) + ) def test_isort_provides_official_api_for_diff_output_issue_1335(): @@ -746,3 +748,103 @@ def test_isort_should_warn_on_empty_custom_config_issue_1433(tmpdir): with pytest.warns(None) as warning: assert Config(settings_file=str(settings_file)).quiet assert not warning + + +def test_float_to_top_should_respect_existing_newlines_between_imports_issue_1502(): + """When a file has an existing top of file import block before code but after comments + isort's float to top feature should respect the existing spacing between the top file comment + and the import statements. + See: https://github.com/PyCQA/isort/issues/1502 + """ + assert isort.check_code( + """#!/bin/bash +'''My comment''' + +import a + +x = 1 +""", + float_to_top=True, + show_diff=True, + ) + assert isort.check_code( + """#!/bin/bash +'''My comment''' + + +import a + +x = 1 +""", + float_to_top=True, + show_diff=True, + ) + assert ( + isort.code( + """#!/bin/bash +'''My comment''' + + +import a + +x = 1 +""", + float_to_top=True, + add_imports=["import b"], + ) + == """#!/bin/bash +'''My comment''' + + +import a +import b + +x = 1 +""" + ) + + assert ( + isort.code( + """#!/bin/bash +'''My comment''' + + +def my_function(): + pass + + +import a +""", + float_to_top=True, + ) + == """#!/bin/bash +'''My comment''' +import a + + +def my_function(): + pass +""" + ) + + assert ( + isort.code( + """#!/bin/bash +'''My comment''' + + +def my_function(): + pass +""", + add_imports=["import os"], + float_to_top=True, + ) + == """#!/bin/bash +'''My comment''' +import os + + +def my_function(): + pass +""" + ) From 85c2537452f457b9298e54f5cb3e933cf8058694 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 28 Sep 2020 22:54:12 -0700 Subject: [PATCH 1088/1439] Refactor integration tests to make it easier to add additional projects --- .../integration/test_projects_using_isort.py | 165 ++++++------------ 1 file changed, 50 insertions(+), 115 deletions(-) diff --git a/tests/integration/test_projects_using_isort.py b/tests/integration/test_projects_using_isort.py index 77537adaf..325498860 100644 --- a/tests/integration/test_projects_using_isort.py +++ b/tests/integration/test_projects_using_isort.py @@ -6,116 +6,78 @@ It is important to isort that as few regressions as possible are experienced by our users. Having your project tested here is the most sure way to keep those regressions form ever happening. """ +from pathlib import Path from subprocess import check_call +from typing import Sequence from isort.main import main +def git_clone(repository_url: str, directory: Path): + """Clones the given repository into the given directory path""" + check_call(["git", "clone", "--depth", "1", repository_url, str(directory)]) + + +def run_isort(arguments: Sequence[str]): + """Runs isort in diff and check mode with the given arguments""" + main(["--check-only", "--diff", *arguments]) + + def test_django(tmpdir): - check_call( - ["git", "clone", "--depth", "1", "https://github.com/django/django.git", str(tmpdir)] - ) - isort_target_dirs = [ + git_clone("https://github.com/django/django.git", tmpdir) + run_isort( str(target_dir) for target_dir in (tmpdir / "django", tmpdir / "tests", tmpdir / "scripts") - ] - main(["--check-only", "--diff", *isort_target_dirs]) + ) def test_plone(tmpdir): - check_call( - [ - "git", - "clone", - "--depth", - "1", - "https://github.com/plone/plone.app.multilingualindexes.git", - str(tmpdir), - ] - ) - main(["--check-only", "--diff", str(tmpdir / "src")]) + git_clone("https://github.com/plone/plone.app.multilingualindexes.git", tmpdir) + run_isort([str(tmpdir / "src")]) def test_pandas(tmpdir): # Need to limit extensions as isort has just made sorting pxd the default, and pandas # will have not picked it up yet # TODO: Remove below line as soon as these files are sorted on the mainline pandas project - limit_extensions = ["--ext", "py", "--ext", "pyi", "--ext", "pyx"] - check_call( - ["git", "clone", "--depth", "1", "https://github.com/pandas-dev/pandas.git", str(tmpdir)] - ) - main( - ["--check-only", "--diff", str(tmpdir / "pandas"), "--skip", "__init__.py"] - + limit_extensions - ) + git_clone("https://github.com/pandas-dev/pandas.git", tmpdir) + limit_extensions = ("--ext", "py", "--ext", "pyi", "--ext", "pyx") + run_isort((str(tmpdir / "pandas"), "--skip", "__init__.py", *limit_extensions)) def test_fastapi(tmpdir): - check_call( - ["git", "clone", "--depth", "1", "https://github.com/tiangolo/fastapi.git", str(tmpdir)] - ) - main(["--check-only", "--diff", str(tmpdir / "fastapi")]) + git_clone("https://github.com/tiangolo/fastapi.git", tmpdir) + run_isort([str(tmpdir / "fastapi")]) def test_zulip(tmpdir): - check_call(["git", "clone", "--depth", "1", "https://github.com/zulip/zulip.git", str(tmpdir)]) - main(["--check-only", "--diff", str(tmpdir), "--skip", "__init__.pyi"]) + git_clone("https://github.com/zulip/zulip.git", tmpdir) + run_isort((str(tmpdir), "--skip", "__init__.pyi")) def test_habitat_lab(tmpdir): - check_call( - [ - "git", - "clone", - "--depth", - "1", - "https://github.com/facebookresearch/habitat-lab.git", - str(tmpdir), - ] - ) - main(["--check-only", "--diff", str(tmpdir)]) + git_clone("https://github.com/facebookresearch/habitat-lab.git", tmpdir) + run_isort([str(tmpdir)]) def test_tmuxp(tmpdir): - check_call( - ["git", "clone", "--depth", "1", "https://github.com/tmux-python/tmuxp.git", str(tmpdir)] - ) - main(["--check-only", "--diff", str(tmpdir)]) + git_clone("https://github.com/tmux-python/tmuxp.git", tmpdir) + run_isort([str(tmpdir)]) def test_websockets(tmpdir): - check_call( - ["git", "clone", "--depth", "1", "https://github.com/aaugustin/websockets.git", str(tmpdir)] - ) - main( - [ - "--check-only", - "--diff", - str(tmpdir), - "--skip", - "example", - "--skip", - "docs", - "--skip", - "compliance", - ] - ) + git_clone("https://github.com/aaugustin/websockets.git", tmpdir) + run_isort((str(tmpdir), "--skip", "example", "--skip", "docs", "--skip", "compliance")) def test_airflow(tmpdir): - check_call( - ["git", "clone", "--depth", "1", "https://github.com/apache/airflow.git", str(tmpdir)] - ) - main(["--check-only", "--diff", str(tmpdir)]) + git_clone("https://github.com/apache/airflow.git", tmpdir) + run_isort([str(tmpdir)]) def test_typeshed(tmpdir): - check_call( - ["git", "clone", "--depth", "1", "https://github.com/python/typeshed.git", str(tmpdir)] - ) - main( - [ - "--check-only", - "--diff", + git_clone("https://github.com/python/typeshed.git", tmpdir) + run_isort( + ( str(tmpdir), "--skip", "tests", @@ -123,51 +85,34 @@ def test_typeshed(tmpdir): "scripts", "--skip", f"{tmpdir}/third_party/2and3/yaml/__init__.pyi", - ] + ) ) def test_pylint(tmpdir): - check_call(["git", "clone", "--depth", "1", "https://github.com/PyCQA/pylint.git", str(tmpdir)]) - main(["--check-only", "--diff", str(tmpdir)]) + git_clone("https://github.com/PyCQA/pylint.git", tmpdir) + run_isort([str(tmpdir)]) def test_poetry(tmpdir): - check_call( - ["git", "clone", "--depth", "1", "https://github.com/python-poetry/poetry.git", str(tmpdir)] - ) - main(["--check-only", "--diff", str(tmpdir), "--skip", "tests"]) + git_clone("https://github.com/python-poetry/poetry.git", tmpdir) + run_isort((str(tmpdir), "--skip", "tests")) def test_hypothesis(tmpdir): - check_call( - [ - "git", - "clone", - "--depth", - "1", - "https://github.com/HypothesisWorks/hypothesis.git", - str(tmpdir), - ] - ) - main(["--check-only", "--diff", str(tmpdir), "--skip", "tests"]) + git_clone("https://github.com/HypothesisWorks/hypothesis.git", tmpdir) + run_isort((str(tmpdir), "--skip", "tests")) def test_pillow(tmpdir): - check_call( - ["git", "clone", "--depth", "1", "https://github.com/python-pillow/Pillow.git", str(tmpdir)] - ) - main(["--check-only", "--diff", str(tmpdir), "--skip", "tests"]) + git_clone("https://github.com/python-pillow/Pillow.git", tmpdir) + run_isort((str(tmpdir), "--skip", "tests")) def test_attrs(tmpdir): - check_call( - ["git", "clone", "--depth", "1", "https://github.com/python-attrs/attrs.git", str(tmpdir)] - ) - main( - [ - "--check-only", - "--diff", + git_clone("https://github.com/python-attrs/attrs.git", tmpdir) + run_isort( + ( str(tmpdir), "--skip", "tests", @@ -175,20 +120,10 @@ def test_attrs(tmpdir): "py", "--skip", "_compat.py", - ] + ) ) def test_datadog_integrations_core(tmpdir): - check_call( - [ - "git", - "clone", - "--depth", - "1", - "https://github.com/DataDog/integrations-core.git", - str(tmpdir), - ] - ) - - main(["--check-only", "--diff", str(tmpdir)]) + git_clone("https://github.com/DataDog/integrations-core.git", tmpdir) + run_isort([str(tmpdir)]) From 9a4da5fe559db5701e6364c1e8911c8fde75695e Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 28 Sep 2020 22:55:17 -0700 Subject: [PATCH 1089/1439] Add Pyramid project to isort integration tests --- tests/integration/test_projects_using_isort.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/integration/test_projects_using_isort.py b/tests/integration/test_projects_using_isort.py index 325498860..2dd5ee62c 100644 --- a/tests/integration/test_projects_using_isort.py +++ b/tests/integration/test_projects_using_isort.py @@ -127,3 +127,11 @@ def test_attrs(tmpdir): def test_datadog_integrations_core(tmpdir): git_clone("https://github.com/DataDog/integrations-core.git", tmpdir) run_isort([str(tmpdir)]) + + +def test_pyramid(tmpdir): + git_clone("https://github.com/Pylons/pyramid.git", tmpdir) + run_isort( + str(target_dir) + for target_dir in (tmpdir / "src" / "pyramid", tmpdir / "tests", tmpdir / "setup.py") + ) From a8da08a6cf3bff12ce1668c5f4f99710317e42f0 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 28 Sep 2020 23:03:19 -0700 Subject: [PATCH 1090/1439] Add unit test case for desired placement behavior --- tests/unit/test_place.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/unit/test_place.py b/tests/unit/test_place.py index cc231d5f5..b1159471f 100644 --- a/tests/unit/test_place.py +++ b/tests/unit/test_place.py @@ -20,3 +20,10 @@ def test_extra_standard_library(src_path): ) assert place_tester("os") == sections.STDLIB assert place_tester("hug") == sections.STDLIB + + +def test_no_standard_library_placement(): + assert place.module_with_reason( + "pathlib", config=Config(sections=["THIRDPARTY"], default_section="THIRDPARTY") + ) == ("THIRDPARTY", "Default option in Config or universal default.") + assert place.module("pathlib") == "STDLIB" From 012b080df9fce9e3d645680b94b8c18c092b1211 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 28 Sep 2020 23:04:04 -0700 Subject: [PATCH 1091/1439] Improve placement logic to ensure it takes into account the case where known leads to non existant section --- isort/place.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/place.py b/isort/place.py index 34b2eeb86..117428aeb 100644 --- a/isort/place.py +++ b/isort/place.py @@ -54,7 +54,7 @@ def _known_pattern(name: str, config: Config) -> Optional[Tuple[str, str]]: module_names_to_check = (".".join(parts[:first_k]) for first_k in range(len(parts), 0, -1)) for module_name_to_check in module_names_to_check: for pattern, placement in config.known_patterns: - if pattern.match(module_name_to_check): + if placement in config.sections and pattern.match(module_name_to_check): return (placement, f"Matched configured known pattern {pattern}") return None From 1466a4bd03cce569da0eccdae43c9d2f10562203 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 28 Sep 2020 23:05:14 -0700 Subject: [PATCH 1092/1439] Mark Fixed #1505: Support case where known_SECTION points to a section not listed in sections. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68d114f3a..6f40c3fde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ - Fixed #1482: pylama integration is not working correctly out-of-the-box. - Fixed #1492: --check does not work with stdin source. - Fixed #1499: isort gets confused by single line, multi-line style comments when using float-to-top. + - Fixed #1505: Support case where known_SECTION points to a section not listed in sections. ### 5.5.3 [Hotfix] September 20, 2020 - Fixed #1488: in rare cases isort can mangle `yield from` or `raise from` statements. From 57655279f5aff39511fc4f7b88fc71c25c980fd1 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 28 Sep 2020 23:04:04 -0700 Subject: [PATCH 1093/1439] Improve placement logic to ensure it takes into account the case where known leads to non existant section --- isort/place.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/place.py b/isort/place.py index 34b2eeb86..117428aeb 100644 --- a/isort/place.py +++ b/isort/place.py @@ -54,7 +54,7 @@ def _known_pattern(name: str, config: Config) -> Optional[Tuple[str, str]]: module_names_to_check = (".".join(parts[:first_k]) for first_k in range(len(parts), 0, -1)) for module_name_to_check in module_names_to_check: for pattern, placement in config.known_patterns: - if pattern.match(module_name_to_check): + if placement in config.sections and pattern.match(module_name_to_check): return (placement, f"Matched configured known pattern {pattern}") return None From 58cc02faf08ec499f0d9239959b6d50a1b2b90ad Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 28 Sep 2020 23:03:19 -0700 Subject: [PATCH 1094/1439] Add unit test case for desired placement behavior --- tests/unit/test_place.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/unit/test_place.py b/tests/unit/test_place.py index cc231d5f5..b1159471f 100644 --- a/tests/unit/test_place.py +++ b/tests/unit/test_place.py @@ -20,3 +20,10 @@ def test_extra_standard_library(src_path): ) assert place_tester("os") == sections.STDLIB assert place_tester("hug") == sections.STDLIB + + +def test_no_standard_library_placement(): + assert place.module_with_reason( + "pathlib", config=Config(sections=["THIRDPARTY"], default_section="THIRDPARTY") + ) == ("THIRDPARTY", "Default option in Config or universal default.") + assert place.module("pathlib") == "STDLIB" From 5282e8bec2abc5ac3eccf703e14ca668f7a9c693 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 29 Sep 2020 21:25:38 -0700 Subject: [PATCH 1095/1439] Hot fix release 5.5.4 --- CHANGELOG.md | 4 ++++ isort/_version.py | 2 +- isort/core.py | 9 -------- tests/unit/test_regressions.py | 40 ++++++++++++++++++++++++++++++++++ 4 files changed, 45 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d8a50bd4..9c5ee4d73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. Find out more about isort's release policy [here](https://pycqa.github.io/isort/docs/major_releases/release_policy/). +### 5.5.4 [Hotfix] September 29, 2020 + - Fixed #1507: in rare cases isort changes the content of multiline strings after a yield statement. + - Fixed #1505: Support case where known_SECTION points to a section not listed in sections. + ### 5.5.3 [Hotfix] September 20, 2020 - Fixed #1488: in rare cases isort can mangle `yield from` or `raise from` statements. diff --git a/isort/_version.py b/isort/_version.py index 16b899cb9..e82c92eb4 100644 --- a/isort/_version.py +++ b/isort/_version.py @@ -1 +1 @@ -__version__ = "5.5.3" +__version__ = "5.5.4" diff --git a/isort/core.py b/isort/core.py index d1e945d48..696e5d751 100644 --- a/isort/core.py +++ b/isort/core.py @@ -373,15 +373,6 @@ def process( stripped_line = new_line.strip().split("#")[0] if stripped_line.startswith("raise") or stripped_line.startswith("yield"): - if "(" in stripped_line: - while ")" not in stripped_line: - new_line = input_stream.readline() - if not new_line: - break - - output_stream.write(new_line) - stripped_line = new_line.strip().split("#")[0] - while stripped_line.endswith("\\"): new_line = input_stream.readline() if not new_line: diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index 4869d444f..78888c409 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -1081,3 +1081,43 @@ def generator_function(): from \\ """ assert isort.check_code(raise_from_at_file_end_ignored, show_diff=True) + + +def test_isort_shouldnt_mangle_from_multi_line_string_issue_1507(): + """isort was seen mangling lines that happened to contain the word from after + a yield happened to be in a file. Clearly this shouldn't happen. + See: https://github.com/PyCQA/isort/issues/1507. + """ + assert isort.check_code( + ''' +def a(): + yield f( + """ + select %s from (values %%s) as t(%s) + """ + ) + +def b(): + return ( + """ + select name + from foo + """ + % main_table + ) + +def c(): + query = ( + """ + select {keys} + from (values %s) as t(id) + """ + ) + +def d(): + query = f"""select t.id + from {table} t + {extra}""" +''', + show_diff=True, + ) From 2e02c195afdb499be9068ca0d08c9d7d45912d4d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 29 Sep 2020 21:31:40 -0700 Subject: [PATCH 1096/1439] Bump version to 5.5.4 in pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3409893d1..75fb08633 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ line-length = 100 [tool.poetry] name = "isort" -version = "5.5.3" +version = "5.5.4" description = "A Python utility / library to sort Python imports." authors = ["Timothy Crosley "] license = "MIT" From b8db9898857fe9464495642f9a8a4fb981765e06 Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Wed, 30 Sep 2020 12:43:17 +0300 Subject: [PATCH 1097/1439] Fixes syntax highlight in README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 38f735faf..c2c6a8244 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ run against code written using a different version of Python) **From within Python**: -```bash +```python import isort isort.file("pythonfile.py") @@ -144,7 +144,7 @@ isort.file("pythonfile.py") or: -```bash +```python import isort sorted_code = isort.code("import b\nimport a\n") From 607438dc4ec945dac14a3f246310e6e77effce92 Mon Sep 17 00:00:00 2001 From: anirudnits Date: Wed, 30 Sep 2020 23:06:20 +0530 Subject: [PATCH 1098/1439] added cli flag tests with stdin --- tests/unit/test_main.py | 384 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 384 insertions(+) diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 44e1f7efc..ddca98dd4 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -319,3 +319,387 @@ def test_main(capsys, tmpdir): def test_isort_command(): """Ensure ISortCommand got registered, otherwise setuptools error must have occurred""" assert main.ISortCommand + + +def test_isort_with_stdin(capsys): + # ensures that isort sorts stdin without any flags + + input_content = TextIOWrapper( + BytesIO( + b""" +import b +import a +""" + ) + ) + + main.main(["-"], stdin=input_content) + out, error = capsys.readouterr() + + assert out == ( + """ +import a +import b +""" + ) + + input_content_from = TextIOWrapper( + BytesIO( + b""" +import c +import b +from a import z, y, x +""" + ) + ) + + main.main(["-"], stdin=input_content_from) + out, error = capsys.readouterr() + + assert out == ( + """ +import b +import c +from a import x, y, z +""" + ) + + # ensures that isort correctly sorts stdin with --fas flag + + input_content = TextIOWrapper( + BytesIO( + b""" +import sys +import pandas +from z import abc +from a import xyz +""" + ) + ) + + main.main(["-", "--fas"], stdin=input_content) + out, error = capsys.readouterr() + + assert out == ( + """ +from a import xyz +from z import abc + +import pandas +import sys +""" + ) + + # ensures that isort correctly sorts stdin with --fass flag + + input_content = TextIOWrapper( + BytesIO( + b""" +from a import Path, abc +""" + ) + ) + + main.main(["-", "--fass"], stdin=input_content) + out, error = capsys.readouterr() + + assert out == ( + """ +from a import abc, Path +""" + ) + + # ensures that isort correctly sorts stdin with --ff flag + + input_content = TextIOWrapper( + BytesIO( + b""" +import b +from c import x +from a import y +""" + ) + ) + + main.main(["-", "--ff", "FROM_FIRST"], stdin=input_content) + out, error = capsys.readouterr() + + assert out == ( + """ +from a import y +from c import x +import b +""" + ) + + # ensures that isort correctly sorts stdin with -fss flag + + input_content = TextIOWrapper( + BytesIO( + b""" +import b +from a import a +""" + ) + ) + + main.main(["-", "--fss"], stdin=input_content) + out, error = capsys.readouterr() + + assert out == ( + """ +from a import a +import b +""" + ) + + input_content = TextIOWrapper( + BytesIO( + b""" +import a +from b import c +""" + ) + ) + + main.main(["-", "--fss"], stdin=input_content) + out, error = capsys.readouterr() + + assert out == ( + """ +import a +from b import c +""" + ) + + # ensures that isort correctly sorts stdin with --ds flag + + input_content = TextIOWrapper( + BytesIO( + b""" +import sys +import pandas +import a +""" + ) + ) + + main.main(["-", "--ds"], stdin=input_content) + out, error = capsys.readouterr() + + assert out == ( + """ +import a +import pandas +import sys +""" + ) + + # ensures that isort correctly sorts stdin with --cs flag + + input_content = TextIOWrapper( + BytesIO( + b""" +from a import b +from a import * +""" + ) + ) + + main.main(["-", "--cs"], stdin=input_content) + out, error = capsys.readouterr() + + assert out == ( + """ +from a import * +""" + ) + + # ensures that isort correctly sorts stdin with --ca flag + + input_content = TextIOWrapper( + BytesIO( + b""" +from a import x as X +from a import y as Y +""" + ) + ) + + main.main(["-", "--ca"], stdin=input_content) + out, error = capsys.readouterr() + + assert out == ( + """ +from a import x as X, y as Y +""" + ) + + # ensures that isort works consistently with check and ws flags + + input_content = TextIOWrapper( + BytesIO( + b""" +import os +import a +import b +""" + ) + ) + + main.main(["-", "--check-only", "--ws"], stdin=input_content) + out, error = capsys.readouterr() + + assert not error + + # ensures that isort correctly sorts stdin with --ls flag + + input_content = TextIOWrapper( + BytesIO( + b""" +import abcdef +import x +""" + ) + ) + + main.main(["-", "--ls"], stdin=input_content) + out, error = capsys.readouterr() + + assert out == ( + """ +import x +import abcdef +""" + ) + + # ensures that isort correctly sorts stdin with --nis flag + + input_content = TextIOWrapper( + BytesIO( + b""" +from z import b, c, a +""" + ) + ) + + main.main(["-", "--nis"], stdin=input_content) + out, error = capsys.readouterr() + + assert out == ( + """ +from z import b, c, a +""" + ) + + # ensures that isort correctly sorts stdin with --sl flag + + input_content = TextIOWrapper( + BytesIO( + b""" +from z import b, c, a +""" + ) + ) + + main.main(["-", "--sl"], stdin=input_content) + out, error = capsys.readouterr() + + assert out == ( + """ +from z import a +from z import b +from z import c +""" + ) + + # ensures that isort correctly sorts stdin with --top flag + + input_content = TextIOWrapper( + BytesIO( + b""" +import os +import sys +""" + ) + ) + + main.main(["-", "--top", "sys"], stdin=input_content) + out, error = capsys.readouterr() + + assert out == ( + """ +import sys +import os +""" + ) + + # ensure that isort correctly sorts stdin with --os flag + + input_content = TextIOWrapper( + BytesIO( + b""" +import sys +import os +import z +from a import b, e, c +""" + ) + ) + + main.main(["-", "--os"], stdin=input_content) + out, error = capsys.readouterr() + + assert out == ( + """ +import sys +import os + +import z +from a import b, e, c +""" + ) + + # ensures that isort warns with deprecated flags with stdin + + input_content = TextIOWrapper( + BytesIO( + b""" +import sys +import os +""" + ) + ) + + with pytest.warns(UserWarning): + main.main(["-", "-ns"], stdin=input_content) + + out, error = capsys.readouterr() + + assert out == ( + """ +import os +import sys +""" + ) + + input_content = TextIOWrapper( + BytesIO( + b""" +import sys +import os +""" + ) + ) + + with pytest.warns(UserWarning): + main.main(["-", "-k"], stdin=input_content) + + out, error = capsys.readouterr() + + assert out == ( + """ +import os +import sys +""" + ) From c2473cf30fa37d72bb8918bb365b0fae281a451f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 30 Sep 2020 21:31:06 -0700 Subject: [PATCH 1099/1439] Add note about fix for #1472 --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60c8dac39..87baa08a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ - Fixed #1492: --check does not work with stdin source. - Fixed #1499: isort gets confused by single line, multi-line style comments when using float-to-top. + Goal Zero: (Tickets related to aspirational goal of achieving 0 regressions for remaining 5.0.0 lifespan): + - Implemented #1472: Full testing of stdin CLI Options + ### 5.5.4 [Hotfix] September 29, 2020 - Fixed #1507: in rare cases isort changes the content of multiline strings after a yield statement. - Fixed #1505: Support case where known_SECTION points to a section not listed in sections. From a5df07f146bab07da2df7d581b44d0942add362c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 30 Sep 2020 21:40:28 -0700 Subject: [PATCH 1100/1439] Implemented #1511: Support for easily seeing all files isort will be ran against --- CHANGELOG.md | 1 + isort/main.py | 16 ++++++++++++++++ tests/unit/test_main.py | 24 ++++++++++++++++++++---- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87baa08a3..712e554d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ - Implemented #1433: Provide helpful feedback in case a custom config file is specified without a configuration. - Implemented #1494: Default to sorting imports within `.pxd` files. - Implemented #1502: Improved float-to-top behavior when there is an existing import section present at top-of-file. + - Implemented #1511: Support for easily seeing all files isort will be ran against using `isort . --show-files`. - Improved handling of unsupported configuration option errors (see #1475). - Fixed #1463: Better interactive documentation for future option. - Fixed #1461: Quiet config option not respected by file API in some circumstances. diff --git a/isort/main.py b/isort/main.py index fb5f3200e..c518a92dc 100644 --- a/isort/main.py +++ b/isort/main.py @@ -638,6 +638,12 @@ def _build_arg_parser() -> argparse.ArgumentParser: action="store_true", help="See isort's determined config, as well as sources of config options.", ) + parser.add_argument( + "--show-files", + dest="show_files", + action="store_true", + help="See the files isort will be ran against with the current config options.", + ) parser.add_argument( "--honor-noqa", dest="honor_noqa", @@ -805,6 +811,9 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = return show_config: bool = arguments.pop("show_config", False) + show_files: bool = arguments.pop("show_files", False) + if show_config and show_files: + sys.exit("Error: either specify show-config or show-files not both.") if "settings_path" in arguments: if os.path.isfile(arguments["settings_path"]): @@ -854,6 +863,9 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = print(json.dumps(config.__dict__, indent=4, separators=(",", ": "), default=_preconvert)) return elif file_names == ["-"]: + if show_files: + sys.exit("Error: can't show files for streaming input.") + if check: incorrectly_sorted = not api.check_stream( input_stream=sys.stdin if stdin is None else stdin, @@ -883,6 +895,10 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = file_names = filtered_files file_names = iter_source_code(file_names, config, skipped, broken) + if show_files: + for file_name in file_names: + print(file_name) + return num_skipped = 0 num_broken = 0 if config.verbose: diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index ddca98dd4..ef837b614 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -13,9 +13,6 @@ from isort.settings import DEFAULT_CONFIG, Config from isort.wrap_modes import WrapModes -# This test code was written by the `hypothesis.extra.ghostwriter` module -# and is provided under the Creative Commons Zero public domain dedication. - @given( file_name=st.text(), @@ -104,6 +101,26 @@ def test_preconvert(): main._preconvert(datetime.now()) +def test_show_files(capsys, tmpdir): + tmpdir.join("a.py").write("import a") + tmpdir.join("b.py").write("import b") + + # show files should list the files isort would sort + main.main([str(tmpdir), "--show-files"]) + out, error = capsys.readouterr() + assert "a.py" in out + assert "b.py" in out + assert not error + + # can not be used for stream + with pytest.raises(SystemExit): + main.main(["-", "--show-files"]) + + # can not be used with show-config + with pytest.raises(SystemExit): + main.main([str(tmpdir), "--show-files", "--show-config"]) + + def test_main(capsys, tmpdir): base_args = [ "-sp", @@ -661,7 +678,6 @@ def test_isort_with_stdin(capsys): ) # ensures that isort warns with deprecated flags with stdin - input_content = TextIOWrapper( BytesIO( b""" From 28ccbe23c5629a14c43bfb62e5f6c7ce78dfbd0e Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Thu, 1 Oct 2020 10:56:41 +0300 Subject: [PATCH 1101/1439] Fixes bug in isort-assignments sorting. Fixes #1515. Bonus: increases branch coverage of literals.py to 100% --- isort/literal.py | 21 +++++++++++---------- tests/unit/test_literal.py | 8 ++++++-- tests/unit/test_ticketed_features.py | 8 ++++---- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/isort/literal.py b/isort/literal.py index 28e0855c3..01bd05e79 100644 --- a/isort/literal.py +++ b/isort/literal.py @@ -21,17 +21,18 @@ def __init__(self, config: Config): def assignments(code: str) -> str: - sort_assignments = {} + values = {} for line in code.splitlines(keepends=True): - if line: - if " = " not in line: - raise AssignmentsFormatMismatch(code) - else: - variable_name, value = line.split(" = ", 1) - sort_assignments[variable_name] = value - - sorted_assignments = dict(sorted(sort_assignments.items(), key=lambda item: item[1])) - return "".join(f"{key} = {value}" for key, value in sorted_assignments.items()) + if not line.strip(): + continue + if " = " not in line: + raise AssignmentsFormatMismatch(code) + variable_name, value = line.split(" = ", 1) + values[variable_name] = value + + return "".join( + f"{variable_name} = {values[variable_name]}" for variable_name in sorted(values.keys()) + ) def assignment(code: str, sort_type: str, extension: str, config: Config = DEFAULT_CONFIG) -> str: diff --git a/tests/unit/test_literal.py b/tests/unit/test_literal.py index 0dd7458c7..ee623927a 100644 --- a/tests/unit/test_literal.py +++ b/tests/unit/test_literal.py @@ -20,7 +20,7 @@ def test_invalid_sort_type(): isort.literal.assignment("x = [1, 2, 3", "tuple-list-not-exist", "py") -def test_value_assignment(): +def test_value_assignment_list(): assert isort.literal.assignment("x = ['b', 'a']", "list", "py") == "x = ['a', 'b']" assert ( isort.literal.assignment("x = ['b', 'a']", "list", "py", Config(formatter="example")) @@ -28,6 +28,10 @@ def test_value_assignment(): ) +def test_value_assignment_assignments(): + assert isort.literal.assignment("b = 1\na = 2\n", "assignments", "py") == "a = 2\nb = 1\n" + + def test_assignments_invalid_section(): with pytest.raises(exceptions.AssignmentsFormatMismatch): - isort.literal.assignment("x++", "assignments", "py") + isort.literal.assignment("\n\nx = 1\nx++", "assignments", "py") diff --git a/tests/unit/test_ticketed_features.py b/tests/unit/test_ticketed_features.py index f952bdc51..51b130095 100644 --- a/tests/unit/test_ticketed_features.py +++ b/tests/unit/test_ticketed_features.py @@ -442,9 +442,9 @@ def method(): # isort: assignments -d = x +d = 1 b = 2 -a = 1 +a = 3 # isort: dict y = {"z": "z", "b": "b", "b": "c"}""", @@ -476,9 +476,9 @@ def method(): # isort: assignments -a = 1 +a = 3 b = 2 -d = x +d = 1 # isort: dict y = {"b": "c", "z": "z"}""" From 59730a8246f8a4ebd9a2e5180ce43342ed913b15 Mon Sep 17 00:00:00 2001 From: anirudnits Date: Fri, 2 Oct 2020 00:38:15 +0530 Subject: [PATCH 1102/1439] Added the only-modified option in the config data schema --- isort/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/isort/settings.py b/isort/settings.py index a173366a9..a356a7dee 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -198,6 +198,7 @@ class _Config: variables: FrozenSet[str] = frozenset() dedup_headings: bool = False only_sections: bool = False + only_modified: bool = False def __post_init__(self): py_version = self.py_version From 1e634eabf28eb8acf9658acf82ad215f9c1cb7f3 Mon Sep 17 00:00:00 2001 From: anirudnits Date: Fri, 2 Oct 2020 00:38:54 +0530 Subject: [PATCH 1103/1439] Added only-modified flag in parse.args --- isort/main.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/isort/main.py b/isort/main.py index c518a92dc..1cfdb56f7 100644 --- a/isort/main.py +++ b/isort/main.py @@ -763,6 +763,14 @@ def _build_arg_parser() -> argparse.ArgumentParser: "Imports are unaltered and keep their relative positions within the different sections.", ) + parser.add_argument( + "--only-modified", + "--om", + dest="only_modified", + action="store_true", + help="Suppresses verbose output for non-modified files.", + ) + return parser From b6c3c5997c0c1079a18ef2f26376875efa279e69 Mon Sep 17 00:00:00 2001 From: anirudnits Date: Fri, 2 Oct 2020 00:40:03 +0530 Subject: [PATCH 1104/1439] Added attribute verbose_output in ParsedContent class --- isort/parse.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/isort/parse.py b/isort/parse.py index 6f121c6bd..9348267c3 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -138,6 +138,7 @@ class ParsedContent(NamedTuple): original_line_count: int line_separator: str sections: Any + verbose_output: List[str] def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedContent: @@ -163,6 +164,8 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte "from": defaultdict(list), } imports: OrderedDict[str, Dict[str, Any]] = OrderedDict() + verbose_output: List[str] = [] + for section in chain(config.sections, config.forced_separate): imports[section] = {"straight": OrderedDict(), "from": OrderedDict()} categorized_comments: CommentsDict = { @@ -380,8 +383,13 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte if type_of_import == "from": import_from = just_imports.pop(0) placed_module = finder(import_from) - if config.verbose: + if config.verbose and not config.only_modified: print(f"from-type place_module for {import_from} returned {placed_module}") + + elif config.verbose: + verbose_output.append( + f"from-type place_module for {import_from} returned {placed_module}" + ) if placed_module == "": warn( f"could not place module {import_from} of line {line} --" @@ -469,8 +477,13 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte categorized_comments["above"]["straight"].get(module, []) ) placed_module = finder(module) - if config.verbose: + if config.verbose and not config.only_modified: print(f"else-type place_module for {module} returned {placed_module}") + + elif config.verbose: + verbose_output.append( + f"else-type place_module for {module} returned {placed_module}" + ) if placed_module == "": warn( f"could not place module {module} of line {line} --" @@ -497,4 +510,5 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte original_line_count=original_line_count, line_separator=line_separator, sections=config.sections, + verbose_output=verbose_output, ) From 6d32601a998cc55a2b72c90f2b91102202ce864c Mon Sep 17 00:00:00 2001 From: anirudnits Date: Fri, 2 Oct 2020 00:41:10 +0530 Subject: [PATCH 1105/1439] Made sure that verbose output is only shown for modified files --- isort/api.py | 2 +- isort/core.py | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/isort/api.py b/isort/api.py index f59bc6d91..6c5876be4 100644 --- a/isort/api.py +++ b/isort/api.py @@ -210,7 +210,7 @@ def check_stream( ) printer = create_terminal_printer(color=config.color_output) if not changed: - if config.verbose: + if config.verbose and not config.only_modified: printer.success(f"{file_path or ''} Everything Looks Good!") return True else: diff --git a/isort/core.py b/isort/core.py index 22cba49e0..7f4c2c8ad 100644 --- a/isort/core.py +++ b/isort/core.py @@ -67,6 +67,7 @@ def process( made_changes: bool = False stripped_line: str = "" end_of_file: bool = False + verbose_output: List[str] = [] if config.float_to_top: new_input = "" @@ -87,6 +88,7 @@ def process( current += line_separator + line_separator.join(add_imports) add_imports = [] parsed = parse.file_contents(current, config=config) + verbose_output += parsed.verbose_output extra_space = "" while current and current[-1] == "\n": extra_space += "\n" @@ -325,8 +327,11 @@ def process( line[len(indent) :] for line in import_section.splitlines(keepends=True) ) + parsed_content = parse.file_contents(import_section, config=config) + verbose_output += parsed_content.verbose_output + sorted_import_section = output.sorted_imports( - parse.file_contents(import_section, config=config), + parsed_content, _indented_config(config, indent), extension, import_type="cimport" if cimports else "import", @@ -384,6 +389,10 @@ def process( output_stream.write(new_line) stripped_line = new_line.strip().split("#")[0] + if made_changes and config.only_modified: + for output_str in verbose_output: + print(output_str) + return made_changes From 9f09ca6255a23d5a8c3f58aceb5a2dfc116affba Mon Sep 17 00:00:00 2001 From: anirudnits Date: Fri, 2 Oct 2020 00:41:56 +0530 Subject: [PATCH 1106/1439] added tests for only-modified flag --- tests/unit/test_main.py | 103 +++++++++++++++++++++++++++++++++++++++ tests/unit/test_parse.py | 1 + 2 files changed, 104 insertions(+) diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index ef837b614..2490cd0ed 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -70,6 +70,8 @@ def test_parse_args(): assert main.parse_args(["--dt"]) == {"order_by_type": False} assert main.parse_args(["--only-sections"]) == {"only_sections": True} assert main.parse_args(["--os"]) == {"only_sections": True} + assert main.parse_args(["--om"]) == {"only_modified": True} + assert main.parse_args(["--only-modified"]) == {"only_modified": True} def test_ascii_art(capsys): @@ -719,3 +721,104 @@ def test_isort_with_stdin(capsys): import sys """ ) + + # ensures that only-modified flag works with stdin + input_content = TextIOWrapper( + BytesIO( + b""" +import a +import b +""" + ) + ) + + main.main(["-", "--verbose", "--only-modified"], stdin=input_content) + out, error = capsys.readouterr() + + assert "else-type place_module for a returned THIRDPARTY" not in out + assert "else-type place_module for b returned THIRDPARTY" not in out + + +def test_only_modified_flag(tmpdir, capsys): + # ensures there is no verbose output for correct files with only-modified flag + + file1 = tmpdir.join("file1.py") + file1.write( + """ +import a +import b +""" + ) + + file2 = tmpdir.join("file2.py") + file2.write( + """ +import math + +import pandas as pd +""" + ) + + main.main([str(file1), str(file2), "--verbose", "--only-modified"]) + out, error = capsys.readouterr() + + assert ( + out + == f""" + _ _ + (_) ___ ___ _ __| |_ + | |/ _/ / _ \\/ '__ _/ + | |\\__ \\/\\_\\/| | | |_ + |_|\\___/\\___/\\_/ \\_/ + + isort your imports, so you don't have to. + + VERSION {__version__} + +""" + ) + + assert not error + + # ensures that verbose output is only for modified file(s) with only-modified flag + + file3 = tmpdir.join("file3.py") + file3.write( + """ +import sys +import os +""" + ) + + main.main([str(file1), str(file2), str(file3), "--verbose", "--only-modified"]) + out, error = capsys.readouterr() + + assert "else-type place_module for sys returned STDLIB" in out + assert "else-type place_module for os returned STDLIB" in out + assert "else-type place_module for math returned STDLIB" not in out + assert "else-type place_module for pandas returned THIRDPARTY" not in out + + assert not error + + # ensures that the behaviour is consistent for check flag with only-modified flag + + main.main([str(file1), str(file2), "--check-only", "--verbose", "--only-modified"]) + out, error = capsys.readouterr() + + assert ( + out + == f""" + _ _ + (_) ___ ___ _ __| |_ + | |/ _/ / _ \\/ '__ _/ + | |\\__ \\/\\_\\/| | | |_ + |_|\\___/\\___/\\_/ \\_/ + + isort your imports, so you don't have to. + + VERSION {__version__} + +""" + ) + + assert not error diff --git a/tests/unit/test_parse.py b/tests/unit/test_parse.py index 98183617d..0becac900 100644 --- a/tests/unit/test_parse.py +++ b/tests/unit/test_parse.py @@ -37,6 +37,7 @@ def test_file_contents(): original_line_count, _, _, + _, ) = parse.file_contents(TEST_CONTENTS, config=Config(default_section="")) assert "\n".join(in_lines) == TEST_CONTENTS assert "import" not in "\n".join(out_lines) From b583c12d8218bf499b5063b2a3fe657a6b2b2a3b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 1 Oct 2020 23:04:25 -0700 Subject: [PATCH 1107/1439] Fix #1486: Google profile not quite correct. --- CHANGELOG.md | 2 ++ isort/output.py | 1 + isort/profiles.py | 1 + isort/settings.py | 1 + isort/sorting.py | 3 +++ tests/unit/profiles/test_google.py | 20 ++++++++++++++++++-- 6 files changed, 26 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 712e554d0..150256ea0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ - Fixed #1482: pylama integration is not working correctly out-of-the-box. - Fixed #1492: --check does not work with stdin source. - Fixed #1499: isort gets confused by single line, multi-line style comments when using float-to-top. +Potentially breaking changes: + - Fixed #1486: "Google" profile is not quite Google style. Goal Zero: (Tickets related to aspirational goal of achieving 0 regressions for remaining 5.0.0 lifespan): - Implemented #1472: Full testing of stdin CLI Options diff --git a/isort/output.py b/isort/output.py index d7a14009a..d2633ffdd 100644 --- a/isort/output.py +++ b/isort/output.py @@ -102,6 +102,7 @@ def sorted_imports( lexicographical=config.lexicographical, length_sort=config.length_sort, reverse_relative=config.reverse_relative, + group_by_package=config.group_by_package, ), ) diff --git a/isort/profiles.py b/isort/profiles.py index 77afef242..bfc9b4f7b 100644 --- a/isort/profiles.py +++ b/isort/profiles.py @@ -22,6 +22,7 @@ "lexicographical": True, "single_line_exclusions": ("typing",), "order_by_type": False, + "group_by_package": True, } open_stack = { "force_single_line": True, diff --git a/isort/settings.py b/isort/settings.py index a173366a9..a4e5905ad 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -169,6 +169,7 @@ class _Config: force_grid_wrap: int = 0 force_sort_within_sections: bool = False lexicographical: bool = False + group_by_package: bool = False ignore_whitespace: bool = False no_lines_before: FrozenSet[str] = frozenset() no_inline_sort: bool = False diff --git a/isort/sorting.py b/isort/sorting.py index 780747a3d..3d3961367 100644 --- a/isort/sorting.py +++ b/isort/sorting.py @@ -58,6 +58,7 @@ def section_key( lexicographical: bool = False, length_sort: bool = False, reverse_relative: bool = False, + group_by_package: bool = False, ) -> str: section = "B" @@ -65,6 +66,8 @@ def section_key( match = re.match(r"^from (\.+)\s*(.*)", line) if match: line = f"from {' '.join(match.groups())}" + if group_by_package and line.strip().startswith("from"): + line = line.split(" import", 1)[0] if lexicographical: line = _import_line_intro_re.sub("", _import_line_midline_import_re.sub(".", line)) diff --git a/tests/unit/profiles/test_google.py b/tests/unit/profiles/test_google.py index 3a40bbc30..4a3b41313 100644 --- a/tests/unit/profiles/test_google.py +++ b/tests/unit/profiles/test_google.py @@ -5,6 +5,22 @@ google_isort_test = partial(isort_test, profile="google") +def test_google_code_snippet_shared_example(): + """Tests snippet examples directly shared with the isort project. + See: https://github.com/PyCQA/isort/issues/1486. + """ + google_isort_test( + """import collections +import cProfile +""" + ) + google_isort_test( + """from a import z +from a.b import c +""" + ) + + def test_google_code_snippet_one(): google_isort_test( '''# coding=utf-8 @@ -156,12 +172,13 @@ def test_google_code_snippet_one(): from .interpreters import ad from .interpreters import batching from .interpreters import invertible_ad as iad -from .interpreters.invertible_ad import custom_ivjp from .interpreters import masking from .interpreters import partial_eval as pe from .interpreters import pxla from .interpreters import xla +from .interpreters.invertible_ad import custom_ivjp from .lib import xla_bridge as xb +from .lib import xla_client as xc # Unused imports to be exported from .lib.xla_bridge import device_count from .lib.xla_bridge import devices @@ -170,7 +187,6 @@ def test_google_code_snippet_one(): from .lib.xla_bridge import host_ids from .lib.xla_bridge import local_device_count from .lib.xla_bridge import local_devices -from .lib import xla_client as xc from .traceback_util import api_boundary from .tree_util import Partial from .tree_util import tree_flatten From a00ac1c8825c901b4692ea03f4786ac4f20a0947 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 1 Oct 2020 23:25:51 -0700 Subject: [PATCH 1108/1439] Fix #1481: more prominent callout for how config files are loaded --- docs/upgrade_guides/5.0.0.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/upgrade_guides/5.0.0.md b/docs/upgrade_guides/5.0.0.md index d31fb9dc3..fb767c2c4 100644 --- a/docs/upgrade_guides/5.0.0.md +++ b/docs/upgrade_guides/5.0.0.md @@ -48,6 +48,10 @@ The first thing to keep in mind is how isort loads config options has changed in If you have multiple configs, they will need to be merged into 1 single one. You can see the priority order of configuration files and the manner in which they are loaded on the [config files documentation page](https://pycqa.github.io/isort/docs/configuration/config_files/). +!!! tip - "Config options are loaded relative to the file, not the isort instance." + isort looks for a config file based on the path of the file you request to sort. If you have your config placed outside of the project, you can use `--settings-path` to manually specify the config location instead. Full information about how config files are loaded is in the linked config files documentation page. + + ### `not_skip` This is the same as the `--dont-skip` CLI option above. In an earlier version isort had a default skip of `__init__.py`. To get around that many projects wanted a way to not skip `__init__.py` or any other files that were automatically skipped in the future by isort. isort no longer has any default skips, so if the value here is `__init__.py` you can simply remove the setting. If it is something else, just make sure you aren't specifying to skip that file somewhere else in your config. From 6d556bd206b1165b2b6d674c7e4fe0ad77946551 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 1 Oct 2020 23:48:38 -0700 Subject: [PATCH 1109/1439] Skip tests that are failing on github for windows only --- tests/unit/test_isort.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit/test_isort.py b/tests/unit/test_isort.py index b2fedb8cb..19e81b09c 100644 --- a/tests/unit/test_isort.py +++ b/tests/unit/test_isort.py @@ -3732,6 +3732,7 @@ def test_standard_library_deprecates_user_issue_778() -> None: assert isort.code(test_input) == test_input +@pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows") def test_settings_path_skip_issue_909(tmpdir) -> None: base_dir = tmpdir.mkdir("project") config_dir = base_dir.mkdir("conf") @@ -3762,6 +3763,7 @@ def test_settings_path_skip_issue_909(tmpdir) -> None: assert b"skipped 2" in result.stdout.lower() +@pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows") def test_skip_paths_issue_938(tmpdir) -> None: base_dir = tmpdir.mkdir("project") config_dir = base_dir.mkdir("conf") From a397eb698f245dd6b46c47e426a4f80927867446 Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Sat, 3 Oct 2020 10:27:30 +0300 Subject: [PATCH 1110/1439] Adds characterization tests for float_to_top and on/off. --- tests/unit/test_regressions.py | 75 ++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index 5dc2fc81b..757595224 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -716,6 +716,81 @@ def test_isort_doesnt_mangle_code_when_adding_imports_issue_1444(): ) +def test_isort_float_to_top_with_sort_on_off_tests(): + """Characterization test for current behaviour of float-to-top on isort: on/off sections. + - imports in isort:off sections stay where they are + - imports in isort:on sections float up, but to the top of the isort:on section (not the + top of the file)""" + assert ( + isort.code( + """ +def foo(): + pass + +import a + +# isort: off +import stays_in_section + +x = 1 + +import stays_in_place + +# isort: on + +def bar(): + pass + +import floats_to_top_of_section + +def baz(): + pass +""", + float_to_top=True, + ) + == """import a + + +def foo(): + pass + +# isort: off +import stays_in_section + +x = 1 + +import stays_in_place + +# isort: on +import floats_to_top_of_section + + +def bar(): + pass + + +def baz(): + pass +""" + ) + + to_sort = """# isort: off + +def foo(): + pass + +import stays_in_place +import no_float_to_to_top +import no_ordering + +def bar(): + pass +""" + + # No changes if isort is off + assert isort.code(to_sort, float_to_top=True) == to_sort + + def test_isort_doesnt_float_to_top_correctly_when_imports_not_at_top_issue_1382(): """isort should float existing imports to the top, if they are currently below the top. See: https://github.com/PyCQA/isort/issues/1382 From dbeaaa63611c19b942261ec0115c57290c8f8719 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 3 Oct 2020 01:01:49 -0700 Subject: [PATCH 1111/1439] Implemented #1487: Improved handling of encoding errors. --- CHANGELOG.md | 3 ++- isort/exceptions.py | 14 +++++++++++++- isort/io.py | 15 ++++++++++++--- isort/main.py | 29 +++++++++++++++++++++++++---- tests/unit/test_exceptions.py | 8 ++++++++ tests/unit/test_main.py | 31 +++++++++++++++++++++++++++++++ 6 files changed, 91 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 150256ea0..e7fb1aab1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ - Implemented #1494: Default to sorting imports within `.pxd` files. - Implemented #1502: Improved float-to-top behavior when there is an existing import section present at top-of-file. - Implemented #1511: Support for easily seeing all files isort will be ran against using `isort . --show-files`. + - Implemented #1487: Improved handling of encoding errors. - Improved handling of unsupported configuration option errors (see #1475). - Fixed #1463: Better interactive documentation for future option. - Fixed #1461: Quiet config option not respected by file API in some circumstances. @@ -16,7 +17,7 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ - Fixed #1492: --check does not work with stdin source. - Fixed #1499: isort gets confused by single line, multi-line style comments when using float-to-top. Potentially breaking changes: - - Fixed #1486: "Google" profile is not quite Google style. + - Fixed #1486: "Google" profile is not quite Google style. Goal Zero: (Tickets related to aspirational goal of achieving 0 regressions for remaining 5.0.0 lifespan): - Implemented #1472: Full testing of stdin CLI Options diff --git a/isort/exceptions.py b/isort/exceptions.py index 265928e98..b98454a2c 100644 --- a/isort/exceptions.py +++ b/isort/exceptions.py @@ -1,5 +1,6 @@ """All isort specific exception classes should be defined here""" -from typing import Any, Dict +from pathlib import Path +from typing import Any, Dict, Union from .profiles import profiles @@ -157,3 +158,14 @@ def __init__(self, unsupported_settings: Dict[str, Dict[str, str]]): "https://pycqa.github.io/isort/docs/configuration/options/.\n" ) self.unsupported_settings = unsupported_settings + + +class UnsupportedEncoding(ISortError): + """Raised when isort encounters an encoding error while trying to read a file""" + + def __init__( + self, + filename: Union[str, Path], + ): + super().__init__(f"Unknown or unsupported encoding in {filename}") + self.filename = filename diff --git a/isort/io.py b/isort/io.py index a0357347b..7ff2807d2 100644 --- a/isort/io.py +++ b/isort/io.py @@ -4,7 +4,9 @@ from contextlib import contextmanager from io import BytesIO, StringIO, TextIOWrapper from pathlib import Path -from typing import Iterator, NamedTuple, TextIO, Union +from typing import Callable, Iterator, NamedTuple, TextIO, Union + +from isort.exceptions import UnsupportedEncoding _ENCODING_PATTERN = re.compile(br"^[ \t\f]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)") @@ -14,9 +16,16 @@ class File(NamedTuple): path: Path encoding: str + @staticmethod + def detect_encoding(filename: str, readline: Callable[[], bytes]): + try: + return tokenize.detect_encoding(readline)[0] + except Exception: + raise UnsupportedEncoding(filename) + @staticmethod def from_contents(contents: str, filename: str) -> "File": - encoding, _ = tokenize.detect_encoding(BytesIO(contents.encode("utf-8")).readline) + encoding = File.detect_encoding(filename, BytesIO(contents.encode("utf-8")).readline) return File(StringIO(contents), path=Path(filename).resolve(), encoding=encoding) @property @@ -30,7 +39,7 @@ def _open(filename): """ buffer = open(filename, "rb") try: - encoding, _ = tokenize.detect_encoding(buffer.readline) + encoding = File.detect_encoding(filename, buffer.readline) buffer.seek(0) text = TextIOWrapper(buffer, encoding, line_buffering=True, newline="") text.mode = "r" # type: ignore diff --git a/isort/main.py b/isort/main.py index c518a92dc..841caca52 100644 --- a/isort/main.py +++ b/isort/main.py @@ -10,7 +10,7 @@ from warnings import warn from . import __version__, api, sections -from .exceptions import FileSkipped +from .exceptions import FileSkipped, UnsupportedEncoding from .format import create_terminal_printer from .logo import ASCII_ART from .profiles import profiles @@ -67,9 +67,10 @@ class SortAttempt: - def __init__(self, incorrectly_sorted: bool, skipped: bool) -> None: + def __init__(self, incorrectly_sorted: bool, skipped: bool, supported_encoding: bool) -> None: self.incorrectly_sorted = incorrectly_sorted self.skipped = skipped + self.supported_encoding = supported_encoding def sort_imports( @@ -88,7 +89,7 @@ def sort_imports( incorrectly_sorted = not api.check_file(file_name, config=config, **kwargs) except FileSkipped: skipped = True - return SortAttempt(incorrectly_sorted, skipped) + return SortAttempt(incorrectly_sorted, skipped, True) else: try: incorrectly_sorted = not api.sort_file( @@ -100,10 +101,14 @@ def sort_imports( ) except FileSkipped: skipped = True - return SortAttempt(incorrectly_sorted, skipped) + return SortAttempt(incorrectly_sorted, skipped, True) except (OSError, ValueError) as error: warn(f"Unable to parse file {file_name} due to {error}") return None + except UnsupportedEncoding: + if config.verbose: + warn(f"Encoding not supported for {file_name}") + return SortAttempt(incorrectly_sorted, skipped, False) except Exception: printer = create_terminal_printer(color=config.color_output) printer.error( @@ -852,6 +857,7 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = remapped_deprecated_args = config_dict.pop("remapped_deprecated_args", False) wrong_sorted_files = False all_attempt_broken = False + no_valid_encodings = False if "src_paths" in config_dict: config_dict["src_paths"] = { @@ -901,6 +907,7 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = return num_skipped = 0 num_broken = 0 + num_invalid_encoding = 0 if config.verbose: print(ASCII_ART) @@ -934,6 +941,7 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = # If any files passed in are missing considered as error, should be removed is_no_attempt = True + any_encoding_valid = False for sort_attempt in attempt_iterator: if not sort_attempt: continue # pragma: no cover - shouldn't happen, satisfies type constraint @@ -944,6 +952,12 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = num_skipped += ( 1 # pragma: no cover - shouldn't happen, due to skip in iter_source_code ) + + if not sort_attempt.supported_encoding: + num_invalid_encoding += 1 + else: + any_encoding_valid = True + is_no_attempt = False num_skipped += len(skipped) @@ -965,6 +979,8 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = if num_broken > 0 and is_no_attempt: all_attempt_broken = True + if num_invalid_encoding > 0 and not any_encoding_valid: + no_valid_encodings = True if not config.quiet and (remapped_deprecated_args or deprecated_flags): if remapped_deprecated_args: @@ -988,6 +1004,11 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = if all_attempt_broken: sys.exit(1) + if no_valid_encodings: + printer = create_terminal_printer(color=config.color_output) + printer.error("No valid encodings.") + sys.exit(1) + if __name__ == "__main__": main() diff --git a/tests/unit/test_exceptions.py b/tests/unit/test_exceptions.py index bb6ee2a31..d9eae8bf8 100644 --- a/tests/unit/test_exceptions.py +++ b/tests/unit/test_exceptions.py @@ -90,3 +90,11 @@ def setup_class(self): def test_variables(self): assert self.instance.unsupported_settings == {"apply": {"value": "true", "source": "/"}} + + +class TestUnsupportedEncoding(TestISortError): + def setup_class(self): + self.instance = exceptions.UnsupportedEncoding("file.py") + + def test_variables(self): + assert self.instance.filename == "file.py" diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index ef837b614..28da37eb8 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -719,3 +719,34 @@ def test_isort_with_stdin(capsys): import sys """ ) + + +def test_unsupported_encodings(tmpdir, capsys): + tmp_file = tmpdir.join("file.py") + # fmt: off + tmp_file.write( + u''' +# [syntax-error]\ +# -*- coding: IBO-8859-1 -*- +""" check correct unknown encoding declaration +""" +__revision__ = 'יייי' +''' + ) + # fmt: on + + # should throw an error if only unsupported encoding provided + with pytest.raises(SystemExit): + main.main([str(tmp_file)]) + out, error = capsys.readouterr() + + assert "No valid encodings." in error + + # should not throw an error if at least one valid encoding found + normal_file = tmpdir.join("file1.py") + normal_file.write("import os\nimport sys") + + main.main([str(tmp_file), str(normal_file), "--verbose"]) + out, error = capsys.readouterr() + + assert not error From 82d76ef81b90ed213f9ce3f46d3c97e1322025be Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 3 Oct 2020 01:11:58 -0700 Subject: [PATCH 1112/1439] Fix how unsupported encoding file is created --- tests/unit/test_main.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 28da37eb8..9c2da466c 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -724,14 +724,15 @@ def test_isort_with_stdin(capsys): def test_unsupported_encodings(tmpdir, capsys): tmp_file = tmpdir.join("file.py") # fmt: off - tmp_file.write( - u''' + tmp_file.write_text( + ''' # [syntax-error]\ # -*- coding: IBO-8859-1 -*- """ check correct unknown encoding declaration """ __revision__ = 'יייי' -''' +''', + encoding="utf8" ) # fmt: on From 36e592975d175cb03b92058cf5921513796e1ceb Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 3 Oct 2020 01:24:50 -0700 Subject: [PATCH 1113/1439] Remove uneccesary spacing --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7fb1aab1..1a7e9f966 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ - Fixed #1492: --check does not work with stdin source. - Fixed #1499: isort gets confused by single line, multi-line style comments when using float-to-top. Potentially breaking changes: - - Fixed #1486: "Google" profile is not quite Google style. + - Fixed #1486: "Google" profile is not quite Google style. Goal Zero: (Tickets related to aspirational goal of achieving 0 regressions for remaining 5.0.0 lifespan): - Implemented #1472: Full testing of stdin CLI Options From 4045450a106405e97fc71e0ea4b0852ddeb299bb Mon Sep 17 00:00:00 2001 From: anirudnits Date: Sun, 4 Oct 2020 17:41:17 +0530 Subject: [PATCH 1114/1439] added another test for full coverage of only-modified flag with check and also corrected a misplaced test --- tests/unit/test_main.py | 49 +++++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 3fb75ed64..caf20e7ed 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -722,6 +722,22 @@ def test_isort_with_stdin(capsys): """ ) + # ensures that only-modified flag works with stdin + input_content = TextIOWrapper( + BytesIO( + b""" +import a +import b +""" + ) + ) + + main.main(["-", "--verbose", "--only-modified"], stdin=input_content) + out, error = capsys.readouterr() + + assert "else-type place_module for a returned THIRDPARTY" not in out + assert "else-type place_module for b returned THIRDPARTY" not in out + def test_unsupported_encodings(tmpdir, capsys): tmp_file = tmpdir.join("file.py") @@ -752,22 +768,6 @@ def test_unsupported_encodings(tmpdir, capsys): main.main([str(tmp_file), str(normal_file), "--verbose"]) out, error = capsys.readouterr() - # ensures that only-modified flag works with stdin - input_content = TextIOWrapper( - BytesIO( - b""" -import a -import b -""" - ) - ) - - main.main(["-", "--verbose", "--only-modified"], stdin=input_content) - out, error = capsys.readouterr() - - assert "else-type place_module for a returned THIRDPARTY" not in out - assert "else-type place_module for b returned THIRDPARTY" not in out - def test_only_modified_flag(tmpdir, capsys): # ensures there is no verbose output for correct files with only-modified flag @@ -852,3 +852,20 @@ def test_only_modified_flag(tmpdir, capsys): ) assert not error + + file4 = tmpdir.join("file4.py") + file4.write( + """ +import sys +import os +""" + ) + + with pytest.raises(SystemExit): + main.main([str(file2), str(file4), "--check-only", "--verbose", "--only-modified"]) + out, error = capsys.readouterr() + + assert "else-type place_module for sys returned STDLIB" in out + assert "else-type place_module for os returned STDLIB" in out + assert "else-type place_module for math returned STDLIB" not in out + assert "else-type place_module for pandas returned THIRDPARTY" not in out From 2c796710b3f44f0858291a4754bb07ea3f99be02 Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Sun, 4 Oct 2020 20:58:29 +0300 Subject: [PATCH 1115/1439] Improve test coverage api.py. --- isort/api.py | 2 +- tests/unit/test_api.py | 51 ++++++++++++++++++++++++++++++++---------- 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/isort/api.py b/isort/api.py index 6c5876be4..a8cecb623 100644 --- a/isort/api.py +++ b/isort/api.py @@ -354,7 +354,7 @@ def sort_file( try: # Python 3.8+: use `missing_ok=True` instead of try except. tmp_file.unlink() except FileNotFoundError: - pass + pass # pragma: no cover except ExistingSyntaxErrors: warn(f"{actual_file_path} unable to sort due to existing syntax errors") except IntroducedSyntaxErrors: # pragma: no cover diff --git a/tests/unit/test_api.py b/tests/unit/test_api.py index 1b3ed3701..baec283a1 100644 --- a/tests/unit/test_api.py +++ b/tests/unit/test_api.py @@ -7,36 +7,63 @@ from isort import api from isort.settings import Config +imperfect_content = "import b\nimport a\n" +fixed_content = "import a\nimport b\n" +fixed_diff = "\n+import a\n import b\n-import a\n" -def test_sort_file(tmpdir) -> None: + +@pytest.fixture +def imperfect(tmpdir) -> None: + imperfect_file = tmpdir.join("test_needs_changes.py") + imperfect_file.write_text(imperfect_content, "utf8") + return imperfect_file + + +def test_sort_file_with_bad_syntax(tmpdir) -> None: tmp_file = tmpdir.join("test_bad_syntax.py") - tmp_file.write_text("""print('mismathing quotes")""", "utf8") + tmp_file.write_text("""print('mismatching quotes")""", "utf8") with pytest.warns(UserWarning): api.sort_file(tmp_file, atomic=True) with pytest.warns(UserWarning): api.sort_file(tmp_file, atomic=True, write_to_stdout=True) - imperfect = tmpdir.join("test_needs_changes.py") - imperfect.write_text("import b\nimport a\n", "utf8") - api.sort_file(imperfect, write_to_stdout=True, show_diff=True) +def test_sort_file(imperfect) -> None: + assert api.sort_file(imperfect) + assert imperfect.read() == fixed_content + + +def test_sort_file_to_stdout(capsys, imperfect) -> None: + assert api.sort_file(imperfect, write_to_stdout=True) + out, _ = capsys.readouterr() + assert out == fixed_content + + +def test_other_ask_to_apply(imperfect) -> None: # First show diff, but ensure change wont get written by asking to apply # and ensuring answer is no. with patch("isort.format.input", MagicMock(return_value="n")): - api.sort_file(imperfect, show_diff=True, ask_to_apply=True) + assert not api.sort_file(imperfect, ask_to_apply=True) + assert imperfect.read() == imperfect_content - # Then run again, but apply the change without asking - api.sort_file(imperfect, show_diff=True) + # Then run again, but apply the change (answer is yes) + with patch("isort.format.input", MagicMock(return_value="y")): + assert api.sort_file(imperfect, ask_to_apply=True) + assert imperfect.read() == fixed_content -def test_check_file(tmpdir) -> None: +def test_check_file_no_changes(capsys, tmpdir) -> None: perfect = tmpdir.join("test_no_changes.py") perfect.write_text("import a\nimport b\n", "utf8") assert api.check_file(perfect, show_diff=True) + out, _ = capsys.readouterr() + assert not out + - imperfect = tmpdir.join("test_needs_changes.py") - imperfect.write_text("import b\nimport a\n", "utf8") +def test_check_file_with_changes(capsys, imperfect) -> None: assert not api.check_file(imperfect, show_diff=True) + out, _ = capsys.readouterr() + assert fixed_diff in out def test_sorted_imports_multiple_configs() -> None: @@ -48,7 +75,7 @@ def test_diff_stream() -> None: output = StringIO() assert api.sort_stream(StringIO("import b\nimport a\n"), output, show_diff=True) output.seek(0) - assert "import a\n import b\n" in output.read() + assert fixed_diff in output.read() def test_sort_code_string_mixed_newlines(): From 533c26f0d45324bb5e4c1a157f46ddb2e313f37e Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Sun, 4 Oct 2020 22:10:17 +0300 Subject: [PATCH 1116/1439] Fix warnings can't surpressed with quiet. Fixes #1525 --- isort/settings.py | 16 +++++++++------- tests/unit/test_isort.py | 6 ++++++ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/isort/settings.py b/isort/settings.py index 937e6e067..a89f6d69f 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -266,6 +266,11 @@ def __init__( super().__init__(**config_vars) # type: ignore return + # We can't use self.quiet to conditionally show warnings before super.__init__() is called + # at the end of this method. _Config is also frozen so setting self.quiet isn't possible. + # Therefore we extract quiet early here in a variable and use that in warning conditions. + quiet = config_overrides.get("quiet", False) + sources: List[Dict[str, Any]] = [_DEFAULT_SETTINGS] config_settings: Dict[str, Any] @@ -276,7 +281,7 @@ def __init__( CONFIG_SECTIONS.get(os.path.basename(settings_file), FALLBACK_CONFIG_SECTIONS), ) project_root = os.path.dirname(settings_file) - if not config_settings: + if not config_settings and not quiet: warn( f"A custom settings file was specified: {settings_file} but no configuration " "was found inside. This can happen when [settings] is used as the config " @@ -343,7 +348,7 @@ def __init__( combined_config.pop(key) if maps_to_section in KNOWN_SECTION_MAPPING: section_name = f"known_{KNOWN_SECTION_MAPPING[maps_to_section].lower()}" - if section_name in combined_config and not self.quiet: + if section_name in combined_config and not quiet: warn( f"Can't set both {key} and {section_name} in the same config file.\n" f"Default to {section_name} if unsure." @@ -355,10 +360,7 @@ def __init__( combined_config[section_name] = frozenset(value) else: known_other[import_heading] = frozenset(value) - if ( - maps_to_section not in combined_config.get("sections", ()) - and not self.quiet - ): + if maps_to_section not in combined_config.get("sections", ()) and not quiet: warn( f"`{key}` setting is defined, but {maps_to_section} is not" " included in `sections` config option:" @@ -425,7 +427,7 @@ def __init__( if deprecated_options_used: for deprecated_option in deprecated_options_used: combined_config.pop(deprecated_option) - if not self.quiet: + if not quiet: warn( "W0503: Deprecated config options were used: " f"{', '.join(deprecated_options_used)}." diff --git a/tests/unit/test_isort.py b/tests/unit/test_isort.py index 19e81b09c..31550f953 100644 --- a/tests/unit/test_isort.py +++ b/tests/unit/test_isort.py @@ -4825,6 +4825,12 @@ def test_deprecated_settings(): assert isort.code("hi", not_skip=True) +def test_deprecated_settings_no_warn_in_quiet_mode(recwarn): + """Test to ensure isort does NOT warn in quiet mode even though settings are deprecated""" + assert isort.code("hi", not_skip=True, quiet=True) + assert not recwarn + + def test_only_sections() -> None: """Test to ensure that the within sections relative position of imports are maintained""" test_input = ( From 97686bc4d9034695407682362af8511823574a0e Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Sun, 4 Oct 2020 20:59:44 +0300 Subject: [PATCH 1117/1439] Improve test coverage hooks.py --- tests/unit/test_hooks.py | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/tests/unit/test_hooks.py b/tests/unit/test_hooks.py index 083fbfd48..ddaff0e25 100644 --- a/tests/unit/test_hooks.py +++ b/tests/unit/test_hooks.py @@ -31,17 +31,34 @@ def test_git_hook(src_dir): "HEAD", ] - # Test with incorrectly sorted file returned from git + # Test that non python files aren't processed with patch( - "isort.hooks.get_lines", MagicMock(return_value=[os.path.join(src_dir, "main.py")]) - ) as run_mock: + "isort.hooks.get_lines", + MagicMock(return_value=["README.md", "setup.cfg", "LICDENSE", "mkdocs.yml", "test"]), + ): + with patch("subprocess.run", MagicMock()) as run_mock: + hooks.git_hook(modify=True) + run_mock.assert_not_called() - class FakeProcessResponse(object): - stdout = b"import b\nimport a" + mock_main_py = MagicMock(return_value=[os.path.join(src_dir, "main.py")]) - with patch("subprocess.run", MagicMock(return_value=FakeProcessResponse())) as run_mock: - with patch("isort.api", MagicMock(return_value=False)): + mock_imperfect = MagicMock() + mock_imperfect.return_value.stdout = b"import b\nimport a" + + # Test with incorrectly sorted file returned from git + with patch("isort.hooks.get_lines", mock_main_py): + with patch("subprocess.run", mock_imperfect): + with patch("isort.api.sort_file", MagicMock(return_value=False)) as api_mock: hooks.git_hook(modify=True) + api_mock.assert_called_once() + assert api_mock.call_args.args[0] == mock_main_py.return_value[0] + + # Test with sorted file returned from git and modify=False + with patch("isort.hooks.get_lines", mock_main_py): + with patch("subprocess.run", mock_imperfect): + with patch("isort.api.sort_file", MagicMock(return_value=False)) as api_mock: + hooks.git_hook(modify=False) + api_mock.assert_not_called() # Test with skipped file returned from git with patch( From d8f1f1dfa8b910ec8195d1ebdf18407af1de3060 Mon Sep 17 00:00:00 2001 From: Denis Veselov Date: Sun, 4 Oct 2020 22:41:41 +0300 Subject: [PATCH 1118/1439] Improve PyCharm Profile --- docs/configuration/profiles.md | 1 + isort/profiles.py | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/configuration/profiles.md b/docs/configuration/profiles.md index 3b0e4cc47..1b6d6f75e 100644 --- a/docs/configuration/profiles.md +++ b/docs/configuration/profiles.md @@ -30,6 +30,7 @@ To use any of the listed profiles, use `isort --profile PROFILE_NAME` from the c - **multi_line_output**: `3` - **force_grid_wrap**: `2` + - **lines_after_imports**: `2` #google diff --git a/isort/profiles.py b/isort/profiles.py index bfc9b4f7b..cb8cb5688 100644 --- a/isort/profiles.py +++ b/isort/profiles.py @@ -15,7 +15,11 @@ "multi_line_output": 5, "line_length": 79, } -pycharm = {"multi_line_output": 3, "force_grid_wrap": 2} +pycharm = { + "multi_line_output": 3, + "force_grid_wrap": 2, + "lines_after_imports": 2, +} google = { "force_single_line": True, "force_sort_within_sections": True, From a9853b95e09c98babed94c95d122ac751de60fbe Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Sun, 4 Oct 2020 22:59:49 +0300 Subject: [PATCH 1119/1439] Improve test coverage of place.py. More characterisation around forced_separate: - what happens when we use * to match the end of packages - changing order of forced_separate sections (ie. making django.utils come before django.contrib) --- tests/unit/test_isort.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_isort.py b/tests/unit/test_isort.py index 19e81b09c..cb911b5ab 100644 --- a/tests/unit/test_isort.py +++ b/tests/unit/test_isort.py @@ -980,6 +980,7 @@ def test_forced_separate() -> None: "from django.db import models\n" "from django.db.models.fields import FieldDoesNotExist\n" "from django.utils import six\n" + "\n" "from django.utils.deprecation import RenameMethodsBase\n" "from django.utils.encoding import force_str, force_text\n" "from django.utils.http import urlencode\n" @@ -993,7 +994,7 @@ def test_forced_separate() -> None: assert ( isort.code( code=test_input, - forced_separate=["django.contrib"], + forced_separate=["django.utils.*", "django.contrib"], known_third_party=["django"], line_length=120, order_by_type=False, @@ -1003,7 +1004,7 @@ def test_forced_separate() -> None: assert ( isort.code( code=test_input, - forced_separate=["django.contrib"], + forced_separate=["django.utils.*", "django.contrib"], known_third_party=["django"], line_length=120, order_by_type=False, From e94d2e3192ce6f2e92309a0706227932e5ef018c Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Mon, 5 Oct 2020 00:27:06 +0300 Subject: [PATCH 1120/1439] Improve test coverage of sorting.py and setuptools_commands.py pragma: no cover for these. Setuptools commands would be hard to test and sorting.py has an if that should never be reached. --- isort/setuptools_commands.py | 4 ++-- isort/sorting.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/isort/setuptools_commands.py b/isort/setuptools_commands.py index f67008877..96e41dd0b 100644 --- a/isort/setuptools_commands.py +++ b/isort/setuptools_commands.py @@ -31,13 +31,13 @@ def finalize_options(self) -> None: def distribution_files(self) -> Iterator[str]: """Find distribution packages.""" # This is verbatim from flake8 - if self.distribution.packages: + if self.distribution.packages: # pragma: no cover package_dirs = self.distribution.package_dir or {} for package in self.distribution.packages: pkg_dir = package if package in package_dirs: pkg_dir = package_dirs[package] - elif "" in package_dirs: + elif "" in package_dirs: # pragma: no cover pkg_dir = package_dirs[""] + os.path.sep + pkg_dir yield pkg_dir.replace(".", os.path.sep) diff --git a/isort/sorting.py b/isort/sorting.py index 3d3961367..cab77011b 100644 --- a/isort/sorting.py +++ b/isort/sorting.py @@ -64,7 +64,7 @@ def section_key( if reverse_relative and line.startswith("from ."): match = re.match(r"^from (\.+)\s*(.*)", line) - if match: + if match: # pragma: no cover - regex always matches if line starts with "from ." line = f"from {' '.join(match.groups())}" if group_by_package and line.strip().startswith("from"): line = line.split(" import", 1)[0] From c92b7608c3f7b40e991a70388e128a20eed42eb8 Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Mon, 5 Oct 2020 01:19:55 +0300 Subject: [PATCH 1121/1439] Fix for test_hooks.py Mock.call_args.args can be used only on Python 3.8, fails on 3.6 --- tests/unit/test_hooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_hooks.py b/tests/unit/test_hooks.py index ddaff0e25..2757f414f 100644 --- a/tests/unit/test_hooks.py +++ b/tests/unit/test_hooks.py @@ -51,7 +51,7 @@ def test_git_hook(src_dir): with patch("isort.api.sort_file", MagicMock(return_value=False)) as api_mock: hooks.git_hook(modify=True) api_mock.assert_called_once() - assert api_mock.call_args.args[0] == mock_main_py.return_value[0] + assert api_mock.call_args[0][0] == mock_main_py.return_value[0] # Test with sorted file returned from git and modify=False with patch("isort.hooks.get_lines", mock_main_py): From bdd1ebf0b50d8b0e8fc43a624db73966319c8ef6 Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Mon, 5 Oct 2020 01:37:40 +0300 Subject: [PATCH 1122/1439] Fix test failures on Windows. --- tests/unit/test_api.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_api.py b/tests/unit/test_api.py index baec283a1..e888b24e7 100644 --- a/tests/unit/test_api.py +++ b/tests/unit/test_api.py @@ -1,5 +1,6 @@ """Tests the isort API module""" from io import StringIO +import os from unittest.mock import MagicMock, patch import pytest @@ -8,8 +9,8 @@ from isort.settings import Config imperfect_content = "import b\nimport a\n" -fixed_content = "import a\nimport b\n" -fixed_diff = "\n+import a\n import b\n-import a\n" +fixed_content = "import a\nimport b\n".replace('\n', os.linesep) +fixed_diff = "\n+import a\n import b\n-import a\n".replace ('\n', os.linesep) @pytest.fixture From 7bd1511a7daf5dadc1234faef4cc88c8f7ec61cd Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Mon, 5 Oct 2020 01:42:52 +0300 Subject: [PATCH 1123/1439] Linting. --- tests/unit/test_api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_api.py b/tests/unit/test_api.py index e888b24e7..920890568 100644 --- a/tests/unit/test_api.py +++ b/tests/unit/test_api.py @@ -1,6 +1,6 @@ """Tests the isort API module""" -from io import StringIO import os +from io import StringIO from unittest.mock import MagicMock, patch import pytest @@ -9,8 +9,8 @@ from isort.settings import Config imperfect_content = "import b\nimport a\n" -fixed_content = "import a\nimport b\n".replace('\n', os.linesep) -fixed_diff = "\n+import a\n import b\n-import a\n".replace ('\n', os.linesep) +fixed_content = "import a\nimport b\n".replace("\n", os.linesep) +fixed_diff = "\n+import a\n import b\n-import a\n".replace("\n", os.linesep) @pytest.fixture From fcd020695c95647d77e57d508fb905d6d0cab13b Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Mon, 5 Oct 2020 02:10:19 +0300 Subject: [PATCH 1124/1439] Another go at fixing the WIndows tests. --- tests/unit/test_api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_api.py b/tests/unit/test_api.py index 920890568..c60b19f8d 100644 --- a/tests/unit/test_api.py +++ b/tests/unit/test_api.py @@ -9,8 +9,8 @@ from isort.settings import Config imperfect_content = "import b\nimport a\n" -fixed_content = "import a\nimport b\n".replace("\n", os.linesep) -fixed_diff = "\n+import a\n import b\n-import a\n".replace("\n", os.linesep) +fixed_content = "import a\nimport b\n" +fixed_diff = "\n+import a\n import b\n-import a\n" @pytest.fixture @@ -37,7 +37,7 @@ def test_sort_file(imperfect) -> None: def test_sort_file_to_stdout(capsys, imperfect) -> None: assert api.sort_file(imperfect, write_to_stdout=True) out, _ = capsys.readouterr() - assert out == fixed_content + assert out == fixed_content.replace("\n", os.linesep) def test_other_ask_to_apply(imperfect) -> None: From fa65b2d2500692d7d3ff97a9805d0ee56b917a50 Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Mon, 5 Oct 2020 02:20:28 +0300 Subject: [PATCH 1125/1439] Another go at fixing the Windows tests. --- tests/unit/test_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_api.py b/tests/unit/test_api.py index c60b19f8d..1235626ed 100644 --- a/tests/unit/test_api.py +++ b/tests/unit/test_api.py @@ -64,7 +64,7 @@ def test_check_file_no_changes(capsys, tmpdir) -> None: def test_check_file_with_changes(capsys, imperfect) -> None: assert not api.check_file(imperfect, show_diff=True) out, _ = capsys.readouterr() - assert fixed_diff in out + assert fixed_diff.replace("\n", os.linesep) in out def test_sorted_imports_multiple_configs() -> None: From 4085dd5f1bc79dce8c2c3116ca1e6f36c15a43e3 Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Mon, 5 Oct 2020 02:31:14 +0300 Subject: [PATCH 1126/1439] And another go to fix the Windows tests. ... it is getting late :-) --- tests/unit/test_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_api.py b/tests/unit/test_api.py index 1235626ed..3d257e705 100644 --- a/tests/unit/test_api.py +++ b/tests/unit/test_api.py @@ -10,7 +10,7 @@ imperfect_content = "import b\nimport a\n" fixed_content = "import a\nimport b\n" -fixed_diff = "\n+import a\n import b\n-import a\n" +fixed_diff = "+import a\n import b\n-import a\n" @pytest.fixture From 6c2cfdbc06164d60473b543eefbded99fdb4dcc3 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 4 Oct 2020 16:43:10 -0700 Subject: [PATCH 1127/1439] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a7e9f966..a33d78fc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,8 +16,10 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ - Fixed #1482: pylama integration is not working correctly out-of-the-box. - Fixed #1492: --check does not work with stdin source. - Fixed #1499: isort gets confused by single line, multi-line style comments when using float-to-top. + - Fixed #1525: Some warnings can't be disabled with --quiet. Potentially breaking changes: - Fixed #1486: "Google" profile is not quite Google style. + - Fixed "PyCharm" profile to always add 2 lines to be consistent with what PyCharm "Optimize Imports" does. Goal Zero: (Tickets related to aspirational goal of achieving 0 regressions for remaining 5.0.0 lifespan): - Implemented #1472: Full testing of stdin CLI Options From 080a9038ee4ccefdf6b70800c10e40836e4015cc Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 4 Oct 2020 17:08:46 -0700 Subject: [PATCH 1128/1439] Add failing test for #1523 --- tests/unit/test_regressions.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index 757595224..3a1d94e06 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -1273,3 +1273,14 @@ def d(): ''', show_diff=True, ) + + +def test_isort_should_keep_all_as_and_non_as_imports_issue_1523(): + """isort should keep as and non-as imports of the same path that happen to exist within the + same statement. + See: https://github.com/PyCQA/isort/issues/1523. + """ + assert isort.check_code( + """ +from selenium.webdriver import Remote, Remote as Driver +""", show_diff=True, combine_as_imports=True) From 0971e635720ea3af4ecd2c18b8359dd30e2b8218 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 4 Oct 2020 21:08:15 -0700 Subject: [PATCH 1129/1439] Initial approach for fixing handling of as mixed with non as imports of same path --- isort/parse.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/isort/parse.py b/isort/parse.py index 9348267c3..d7e22a2d8 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -336,10 +336,10 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte item.replace("{|", "{ ").replace("|}", " }") for item in _strip_syntax(import_string).split() ] - straight_import = True + attach_comments_to: Optional[List[Any]] = None if "as" in just_imports and (just_imports.index("as") + 1) < len(just_imports): - straight_import = False + straight_imports = set() while "as" in just_imports: nested_module = None as_index = just_imports.index("as") @@ -379,6 +379,8 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte module, [] ) del just_imports[as_index : as_index + 2] + else: + straight_imports = set(just_imports[1:]) if type_of_import == "from": import_from = just_imports.pop(0) From bacf2fd56c807e8a62305c2d15fe11e402e840f0 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 4 Oct 2020 23:20:11 -0700 Subject: [PATCH 1130/1439] Fix issue #1523: isort should keep all as and non as imports --- isort/parse.py | 13 ++++++++----- tests/unit/test_isort.py | 23 +++++++++++++++++++++++ tests/unit/test_regressions.py | 5 ++++- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/isort/parse.py b/isort/parse.py index d7e22a2d8..9a80e97b9 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -338,8 +338,10 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte ] attach_comments_to: Optional[List[Any]] = None + direct_imports = just_imports[1:] + straight_import = True if "as" in just_imports and (just_imports.index("as") + 1) < len(just_imports): - straight_imports = set() + straight_import = False while "as" in just_imports: nested_module = None as_index = just_imports.index("as") @@ -348,6 +350,9 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte top_level_module = just_imports[0] module = top_level_module + "." + nested_module as_name = just_imports[as_index + 1] + direct_imports.remove(nested_module) + direct_imports.remove(as_name) + direct_imports.remove("as") if nested_module == as_name and config.remove_redundant_aliases: pass elif as_name not in as_map["from"][module]: @@ -379,8 +384,6 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte module, [] ) del just_imports[as_index : as_index + 2] - else: - straight_imports = set(just_imports[1:]) if type_of_import == "from": import_from = just_imports.pop(0) @@ -435,11 +438,11 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte if import_from not in root: root[import_from] = OrderedDict( - (module, straight_import) for module in just_imports + (module, module in direct_imports) for module in just_imports ) else: root[import_from].update( - (module, straight_import | root[import_from].get(module, False)) + (module, root[import_from].get(module, False) or module in direct_imports) for module in just_imports ) diff --git a/tests/unit/test_isort.py b/tests/unit/test_isort.py index 4598de8cd..c07d655d7 100644 --- a/tests/unit/test_isort.py +++ b/tests/unit/test_isort.py @@ -966,6 +966,29 @@ def test_check_newline_in_imports(capsys) -> None: out, _ = capsys.readouterr() assert "SUCCESS" in out + # if the verbose is only on modified outputs no output will be given + assert api.check_code_string( + code=test_input, + multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, + line_length=20, + verbose=True, + only_modified=True, + ) + out, _ = capsys.readouterr() + assert not out + + # we can make the input invalid to again see output + test_input = "from lib1 import (\n sub2,\n sub1,\n sub3\n)\n" + assert not api.check_code_string( + code=test_input, + multi_line_output=WrapModes.VERTICAL_HANGING_INDENT, + line_length=20, + verbose=True, + only_modified=True, + ) + out, _ = capsys.readouterr() + assert out + def test_forced_separate() -> None: """Ensure that forcing certain sub modules to show separately works as expected.""" diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index 3a1d94e06..f334e4827 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -1283,4 +1283,7 @@ def test_isort_should_keep_all_as_and_non_as_imports_issue_1523(): assert isort.check_code( """ from selenium.webdriver import Remote, Remote as Driver -""", show_diff=True, combine_as_imports=True) +""", + show_diff=True, + combine_as_imports=True, + ) From 8bad5b06e5bb1b5eabe8858ddc8622e48ed4db5d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 4 Oct 2020 23:29:49 -0700 Subject: [PATCH 1131/1439] Add #1523 to changelog --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a33d78fc6..9734f518b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,9 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ - Fixed #1492: --check does not work with stdin source. - Fixed #1499: isort gets confused by single line, multi-line style comments when using float-to-top. - Fixed #1525: Some warnings can't be disabled with --quiet. -Potentially breaking changes: + - Fixed #1523: in rare cases isort can ignore direct from import if as import is also on same line. + + Potentially breaking changes: - Fixed #1486: "Google" profile is not quite Google style. - Fixed "PyCharm" profile to always add 2 lines to be consistent with what PyCharm "Optimize Imports" does. From 82ff239e462338b89e1baa96d78f0f5efddab856 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 5 Oct 2020 01:27:43 -0700 Subject: [PATCH 1132/1439] Add @jugmac00's Products.ZopeTree to integration test suite --- tests/integration/test_projects_using_isort.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/integration/test_projects_using_isort.py b/tests/integration/test_projects_using_isort.py index 2dd5ee62c..95f256b29 100644 --- a/tests/integration/test_projects_using_isort.py +++ b/tests/integration/test_projects_using_isort.py @@ -135,3 +135,8 @@ def test_pyramid(tmpdir): str(target_dir) for target_dir in (tmpdir / "src" / "pyramid", tmpdir / "tests", tmpdir / "setup.py") ) + + +def test_products_zopetree(tmpdir): + git_clone("https://github.com/jugmac00/Products.ZopeTree.git", tmpdir) + run_isort([str(tmpdir)]) From 49040889a94308c201e6c5a2c42d176cec8205c6 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 5 Oct 2020 23:41:11 -0700 Subject: [PATCH 1133/1439] Add Hasan Ramezani (@hramezani) and Denis Veselov (@saippuakauppias) to acknowledgements. --- docs/contributing/4.-acknowledgements.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index f4588d763..0d8ef7b85 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -198,6 +198,7 @@ Code Contributors - Alexandre Yang (@AlexandreYang) - Andrew Howe (@howeaj) - Sang-Heon Jeon (@lntuition) +- Denis Veselov (@saippuakauppias) Documenters =================== @@ -220,6 +221,7 @@ Documenters - Marat Sharafutdinov (@decaz) - Abtin (@abtinmo) - @scottwedge +- Hasan Ramezani (@hramezani) -------------------------------------------- From d2954516ef4de49fd3ded305151a5bfb84827b1d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 5 Oct 2020 23:43:11 -0700 Subject: [PATCH 1134/1439] Add Aniruddha Bhattacharjee (@anirudnits) to core developer list --- docs/contributing/4.-acknowledgements.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index 0d8ef7b85..9f0ccae89 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -1,5 +1,6 @@ Core Developers =================== +- Aniruddha Bhattacharjee (@anirudnits) - Jon Dufresne (@jdufresne) - Tamas Szabo (@sztamas) - Timothy Edmund Crosley (@timothycrosley) From e3dc4bdcd8d33bc2f2e1c390cd0c34a5071434b3 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 6 Oct 2020 21:20:42 -0700 Subject: [PATCH 1135/1439] Add example namespace packages for testing --- tests/unit/example_projects/namespaces/implicit/.isort.cfg | 0 .../example_projects/namespaces/implicit/root/nested/__init__.py | 0 tests/unit/example_projects/namespaces/implicit/root/nested/x.py | 0 tests/unit/example_projects/namespaces/pkg_resource/.isort.cfg | 0 .../example_projects/namespaces/pkg_resource/root/__init__.py | 1 + .../namespaces/pkg_resource/root/nested/__init__.py | 0 .../example_projects/namespaces/pkg_resource/root/nested/x.py | 0 tests/unit/example_projects/namespaces/pkgutil/.isort.cfg | 0 tests/unit/example_projects/namespaces/pkgutil/root/__init__.py | 1 + .../example_projects/namespaces/pkgutil/root/nested/__init__.py | 0 tests/unit/example_projects/namespaces/pkgutil/root/nested/x.py | 0 11 files changed, 2 insertions(+) create mode 100644 tests/unit/example_projects/namespaces/implicit/.isort.cfg create mode 100644 tests/unit/example_projects/namespaces/implicit/root/nested/__init__.py create mode 100644 tests/unit/example_projects/namespaces/implicit/root/nested/x.py create mode 100644 tests/unit/example_projects/namespaces/pkg_resource/.isort.cfg create mode 100644 tests/unit/example_projects/namespaces/pkg_resource/root/__init__.py create mode 100644 tests/unit/example_projects/namespaces/pkg_resource/root/nested/__init__.py create mode 100644 tests/unit/example_projects/namespaces/pkg_resource/root/nested/x.py create mode 100644 tests/unit/example_projects/namespaces/pkgutil/.isort.cfg create mode 100644 tests/unit/example_projects/namespaces/pkgutil/root/__init__.py create mode 100644 tests/unit/example_projects/namespaces/pkgutil/root/nested/__init__.py create mode 100644 tests/unit/example_projects/namespaces/pkgutil/root/nested/x.py diff --git a/tests/unit/example_projects/namespaces/implicit/.isort.cfg b/tests/unit/example_projects/namespaces/implicit/.isort.cfg new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/example_projects/namespaces/implicit/root/nested/__init__.py b/tests/unit/example_projects/namespaces/implicit/root/nested/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/example_projects/namespaces/implicit/root/nested/x.py b/tests/unit/example_projects/namespaces/implicit/root/nested/x.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/example_projects/namespaces/pkg_resource/.isort.cfg b/tests/unit/example_projects/namespaces/pkg_resource/.isort.cfg new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/example_projects/namespaces/pkg_resource/root/__init__.py b/tests/unit/example_projects/namespaces/pkg_resource/root/__init__.py new file mode 100644 index 000000000..de40ea7ca --- /dev/null +++ b/tests/unit/example_projects/namespaces/pkg_resource/root/__init__.py @@ -0,0 +1 @@ +__import__('pkg_resources').declare_namespace(__name__) diff --git a/tests/unit/example_projects/namespaces/pkg_resource/root/nested/__init__.py b/tests/unit/example_projects/namespaces/pkg_resource/root/nested/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/example_projects/namespaces/pkg_resource/root/nested/x.py b/tests/unit/example_projects/namespaces/pkg_resource/root/nested/x.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/example_projects/namespaces/pkgutil/.isort.cfg b/tests/unit/example_projects/namespaces/pkgutil/.isort.cfg new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/example_projects/namespaces/pkgutil/root/__init__.py b/tests/unit/example_projects/namespaces/pkgutil/root/__init__.py new file mode 100644 index 000000000..69e3be50d --- /dev/null +++ b/tests/unit/example_projects/namespaces/pkgutil/root/__init__.py @@ -0,0 +1 @@ +__path__ = __import__('pkgutil').extend_path(__path__, __name__) diff --git a/tests/unit/example_projects/namespaces/pkgutil/root/nested/__init__.py b/tests/unit/example_projects/namespaces/pkgutil/root/nested/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/example_projects/namespaces/pkgutil/root/nested/x.py b/tests/unit/example_projects/namespaces/pkgutil/root/nested/x.py new file mode 100644 index 000000000..e69de29bb From 4a7a1ef80f85b1abe155b5d4995a54ab171a9be7 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 7 Oct 2020 02:43:48 -0700 Subject: [PATCH 1136/1439] Add a lack of namespace package example --- tests/unit/example_projects/namespaces/none/.isort.cfg | 0 tests/unit/example_projects/namespaces/none/root/__init__.py | 0 .../unit/example_projects/namespaces/none/root/nested/__init__.py | 0 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/unit/example_projects/namespaces/none/.isort.cfg create mode 100644 tests/unit/example_projects/namespaces/none/root/__init__.py create mode 100644 tests/unit/example_projects/namespaces/none/root/nested/__init__.py diff --git a/tests/unit/example_projects/namespaces/none/.isort.cfg b/tests/unit/example_projects/namespaces/none/.isort.cfg new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/example_projects/namespaces/none/root/__init__.py b/tests/unit/example_projects/namespaces/none/root/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/example_projects/namespaces/none/root/nested/__init__.py b/tests/unit/example_projects/namespaces/none/root/nested/__init__.py new file mode 100644 index 000000000..e69de29bb From 786d9209ab1fb3ade9ab24303f0038e5760fb260 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 7 Oct 2020 02:44:34 -0700 Subject: [PATCH 1137/1439] Add isortcfgs for namespace examples --- tests/unit/example_projects/namespaces/implicit/.isort.cfg | 2 ++ tests/unit/example_projects/namespaces/none/.isort.cfg | 2 ++ tests/unit/example_projects/namespaces/pkg_resource/.isort.cfg | 2 ++ .../example_projects/namespaces/pkg_resource/root/__init__.py | 2 +- tests/unit/example_projects/namespaces/pkgutil/.isort.cfg | 2 ++ tests/unit/example_projects/namespaces/pkgutil/root/__init__.py | 2 +- 6 files changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/unit/example_projects/namespaces/implicit/.isort.cfg b/tests/unit/example_projects/namespaces/implicit/.isort.cfg index e69de29bb..d3ae4c35a 100644 --- a/tests/unit/example_projects/namespaces/implicit/.isort.cfg +++ b/tests/unit/example_projects/namespaces/implicit/.isort.cfg @@ -0,0 +1,2 @@ +[settings] +src_paths=root diff --git a/tests/unit/example_projects/namespaces/none/.isort.cfg b/tests/unit/example_projects/namespaces/none/.isort.cfg index e69de29bb..d3ae4c35a 100644 --- a/tests/unit/example_projects/namespaces/none/.isort.cfg +++ b/tests/unit/example_projects/namespaces/none/.isort.cfg @@ -0,0 +1,2 @@ +[settings] +src_paths=root diff --git a/tests/unit/example_projects/namespaces/pkg_resource/.isort.cfg b/tests/unit/example_projects/namespaces/pkg_resource/.isort.cfg index e69de29bb..d3ae4c35a 100644 --- a/tests/unit/example_projects/namespaces/pkg_resource/.isort.cfg +++ b/tests/unit/example_projects/namespaces/pkg_resource/.isort.cfg @@ -0,0 +1,2 @@ +[settings] +src_paths=root diff --git a/tests/unit/example_projects/namespaces/pkg_resource/root/__init__.py b/tests/unit/example_projects/namespaces/pkg_resource/root/__init__.py index de40ea7ca..5284146eb 100644 --- a/tests/unit/example_projects/namespaces/pkg_resource/root/__init__.py +++ b/tests/unit/example_projects/namespaces/pkg_resource/root/__init__.py @@ -1 +1 @@ -__import__('pkg_resources').declare_namespace(__name__) +__import__("pkg_resources").declare_namespace(__name__) diff --git a/tests/unit/example_projects/namespaces/pkgutil/.isort.cfg b/tests/unit/example_projects/namespaces/pkgutil/.isort.cfg index e69de29bb..d3ae4c35a 100644 --- a/tests/unit/example_projects/namespaces/pkgutil/.isort.cfg +++ b/tests/unit/example_projects/namespaces/pkgutil/.isort.cfg @@ -0,0 +1,2 @@ +[settings] +src_paths=root diff --git a/tests/unit/example_projects/namespaces/pkgutil/root/__init__.py b/tests/unit/example_projects/namespaces/pkgutil/root/__init__.py index 69e3be50d..8db66d3d0 100644 --- a/tests/unit/example_projects/namespaces/pkgutil/root/__init__.py +++ b/tests/unit/example_projects/namespaces/pkgutil/root/__init__.py @@ -1 +1 @@ -__path__ = __import__('pkgutil').extend_path(__path__, __name__) +__path__ = __import__("pkgutil").extend_path(__path__, __name__) From 81dbbba88c39c365dedcea9e1f06ca9349c0c679 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 7 Oct 2020 03:12:16 -0700 Subject: [PATCH 1138/1439] Add test cases for intended placement support of namespace packages --- tests/unit/conftest.py | 5 +++++ tests/unit/test_place.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 09e4acbdb..1e0bd9dfa 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -26,3 +26,8 @@ def test_path(): @pytest.fixture def src_path(): return Path(SRC_DIR).resolve() + + +@pytest.fixture +def examples_path(): + return Path(TEST_DIR).resolve() / "example_projects" diff --git a/tests/unit/test_place.py b/tests/unit/test_place.py index b1159471f..b2cc28d42 100644 --- a/tests/unit/test_place.py +++ b/tests/unit/test_place.py @@ -27,3 +27,32 @@ def test_no_standard_library_placement(): "pathlib", config=Config(sections=["THIRDPARTY"], default_section="THIRDPARTY") ) == ("THIRDPARTY", "Default option in Config or universal default.") assert place.module("pathlib") == "STDLIB" + + +def test_namespace_package_placement(examples_path): + namespace_examples = examples_path / "namespaces" + + implicit = namespace_examples / "implicit" + pkg_resource = namespace_examples / "pkg_resource" + pkgutil = namespace_examples / "pkgutil" + for namespace_test in (implicit, pkg_resource, pkgutil): + print(namespace_test) + config = Config(settings_path=namespace_test) + no_namespaces = Config(settings_path=namespace_test, auto_identify_namespace_packages=False) + namespace_override = Config(settings_path=namespace_test, known_firstparty=["root.name"]) + assert place.module("root.name", config=config) == "THIRDPARTY" + assert place.module("root.nested", config=config) == "FIRSTPARTY" + assert place.module("root.name", config=no_namespaces) == "FIRSTPARTY" + assert place.module("root.name", config=namespace_override) == "FIRSTPARTY" + + no_namespace = namespace_examples / "none" + config = Config(settings_path=no_namespace) + manual_namespace = Config(settings_path=no_namespace, namespace_packages=["root"]) + assert place.module("root.name", config=config) == "FIRSTPARTY" + assert place.module("root.nested", config=config) == "FIRSTPARTY" + assert place.module("root.name", config=manual_namespace) == "THIRDPARTY" + assert place.module("root.nested", config=config) == "FIRSTPARTY" + + + + From f02dbf046917bafe1774bb9c15f9553be20f2e34 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 7 Oct 2020 03:12:44 -0700 Subject: [PATCH 1139/1439] Add initial namespace support --- isort/place.py | 86 +++++++++++++++++++++++++++-------------------- isort/settings.py | 2 ++ 2 files changed, 51 insertions(+), 37 deletions(-) diff --git a/isort/place.py b/isort/place.py index fcb3dcbdd..8b80cd22e 100644 --- a/isort/place.py +++ b/isort/place.py @@ -3,7 +3,7 @@ from fnmatch import fnmatch from functools import lru_cache from pathlib import Path -from typing import Optional, Tuple +from typing import FrozenSet, Iterable, Optional, Tuple from isort import sections from isort.settings import DEFAULT_CONFIG, Config @@ -60,43 +60,32 @@ def _known_pattern(name: str, config: Config) -> Optional[Tuple[str, str]]: return None -def _src_path(name: str, config: Config) -> Optional[Tuple[str, str]]: - search_paths = zip(config.src_paths, config.src_paths) - for index, module_part in enumerate(name.split(".")): - if index == len(module_parts): - for search_path, src_path in search_paths: - module_path = (search_path / module_part).resolve() - if ( - _is_module(module_path) - or _is_package(module_path) - or _src_path_is_module(search_path, module_part) - ): - return (sections.FIRSTPARTY, f"Found in one of the configured src_paths: {src_path}.") - else: - new_search_paths = [] - for search_path, src_path in search_paths: - if - - search_paths = [search_path for search_path in search_paths if - _is_package((src_path / root_module_name).resolve()) or - index == 0 and _src_path_is_module - - for src_path in config.src_paths: - root_module_name = name.split(".")[0] - module_path = - if ( - or - or _src_path_is_module(src_path, root_module_name) - ): - return (sections.FIRSTPARTY, f"Found in one of the configured src_paths: {src_path}.") - - - for src_path in config.src_paths: - - for part in - root_module_name = name.split(".")[0] +def _src_path( + name: str, + config: Config, + src_paths: Optional[Iterable[Path]] = None, + prefix: Tuple[str, ...] = (), +) -> Optional[Tuple[str, str]]: + if src_paths is None: + src_paths = config.src_paths + + root_module_name, *nested_module = name.split(".", 1) + new_prefix = prefix + (root_module_name,) + namespace = ".".join(new_prefix) + + for src_path in src_paths: module_path = (src_path / root_module_name).resolve() - if ( + if not prefix and not module_path.is_dir() and src_path.name == root_module_name: + module_path = src_path.resolve() + if nested_module and ( + namespace in config.namespace_packages + or ( + config.auto_identify_namespace_packages + and _is_namespace_package(module_path, config.supported_extensions) + ) + ): + return _src_path(nested_module[0], config, (module_path,), new_prefix) + elif ( _is_module(module_path) or _is_package(module_path) or _src_path_is_module(src_path, root_module_name) @@ -121,6 +110,29 @@ def _is_package(path: Path) -> bool: return exists_case_sensitive(str(path)) and path.is_dir() +def _is_namespace_package(path: Path, src_extensions: FrozenSet[str]) -> bool: + if not _is_package(path): + return False + + init_file = path / "__init__.py" + if not init_file.exists(): + if [filename for filename in path.iterdir() if filename.suffix in src_extensions]: + return False + else: + with init_file.open("rb") as open_init_file: + file_start = open_init_file.read(4096) + if ( + b"__import__('pkg_resources').declare_namespace(__name__)\n" not in file_start + and b'__import__("pkg_resources").declare_namespace(__name__)\n' not in file_start + and b"__path__ = __import__('pkgutil').extend_path(__path__, __name__)" + not in file_start + and b'__path__ = __import__("pkgutil").extend_path(__path__, __name__)' + not in file_start + ): + return False + return True + + def _src_path_is_module(src_path: Path, module_name: str) -> bool: return ( module_name == src_path.name and src_path.is_dir() and exists_case_sensitive(str(src_path)) diff --git a/isort/settings.py b/isort/settings.py index a89f6d69f..e80a989dc 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -200,6 +200,8 @@ class _Config: dedup_headings: bool = False only_sections: bool = False only_modified: bool = False + auto_identify_namespace_packages: bool = True + namespace_packages: FrozenSet[str] = frozenset() def __post_init__(self): py_version = self.py_version From dd22f8ccf22c0059e9b88e2bb2cf2c313828d95b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 7 Oct 2020 03:14:24 -0700 Subject: [PATCH 1140/1439] Mark Fixed #1443 in changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9734f518b..4bb62f3c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ - Fixed #1523: in rare cases isort can ignore direct from import if as import is also on same line. Potentially breaking changes: + - Fixed #1443: Incorrect third vs first party categorization - namespace packages. - Fixed #1486: "Google" profile is not quite Google style. - Fixed "PyCharm" profile to always add 2 lines to be consistent with what PyCharm "Optimize Imports" does. From a9fc7b2caca15c1b5f9a9cf036aae356f19c616a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 7 Oct 2020 03:17:26 -0700 Subject: [PATCH 1141/1439] Add an almost implicit example of namespaces --- .../unit/example_projects/namespaces/almost-implicit/.isort.cfg | 2 ++ .../namespaces/almost-implicit/root/nested/__init__.py | 0 .../namespaces/almost-implicit/root/nested/x.py | 0 tests/unit/example_projects/namespaces/almost-implicit/y.py | 0 4 files changed, 2 insertions(+) create mode 100644 tests/unit/example_projects/namespaces/almost-implicit/.isort.cfg create mode 100644 tests/unit/example_projects/namespaces/almost-implicit/root/nested/__init__.py create mode 100644 tests/unit/example_projects/namespaces/almost-implicit/root/nested/x.py create mode 100644 tests/unit/example_projects/namespaces/almost-implicit/y.py diff --git a/tests/unit/example_projects/namespaces/almost-implicit/.isort.cfg b/tests/unit/example_projects/namespaces/almost-implicit/.isort.cfg new file mode 100644 index 000000000..d3ae4c35a --- /dev/null +++ b/tests/unit/example_projects/namespaces/almost-implicit/.isort.cfg @@ -0,0 +1,2 @@ +[settings] +src_paths=root diff --git a/tests/unit/example_projects/namespaces/almost-implicit/root/nested/__init__.py b/tests/unit/example_projects/namespaces/almost-implicit/root/nested/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/example_projects/namespaces/almost-implicit/root/nested/x.py b/tests/unit/example_projects/namespaces/almost-implicit/root/nested/x.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/example_projects/namespaces/almost-implicit/y.py b/tests/unit/example_projects/namespaces/almost-implicit/y.py new file mode 100644 index 000000000..e69de29bb From 23933cbdb75339c3f6460cd535c1e4ec7e1a2d7e Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 7 Oct 2020 03:28:26 -0700 Subject: [PATCH 1142/1439] Finalize built in namespace support tests --- isort/place.py | 5 ++++- .../namespaces/almost-implicit/y.py | 0 tests/unit/test_place.py | 20 +++++++++---------- 3 files changed, 13 insertions(+), 12 deletions(-) delete mode 100644 tests/unit/example_projects/namespaces/almost-implicit/y.py diff --git a/isort/place.py b/isort/place.py index 8b80cd22e..e9909b599 100644 --- a/isort/place.py +++ b/isort/place.py @@ -116,7 +116,10 @@ def _is_namespace_package(path: Path, src_extensions: FrozenSet[str]) -> bool: init_file = path / "__init__.py" if not init_file.exists(): - if [filename for filename in path.iterdir() if filename.suffix in src_extensions]: + filenames = [ + filename for filename in path.iterdir() if filename.suffix.lstrip(".") in src_extensions + ] + if filenames: return False else: with init_file.open("rb") as open_init_file: diff --git a/tests/unit/example_projects/namespaces/almost-implicit/y.py b/tests/unit/example_projects/namespaces/almost-implicit/y.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/unit/test_place.py b/tests/unit/test_place.py index b2cc28d42..18ac7fccd 100644 --- a/tests/unit/test_place.py +++ b/tests/unit/test_place.py @@ -31,7 +31,7 @@ def test_no_standard_library_placement(): def test_namespace_package_placement(examples_path): namespace_examples = examples_path / "namespaces" - + implicit = namespace_examples / "implicit" pkg_resource = namespace_examples / "pkg_resource" pkgutil = namespace_examples / "pkgutil" @@ -46,13 +46,11 @@ def test_namespace_package_placement(examples_path): assert place.module("root.name", config=namespace_override) == "FIRSTPARTY" no_namespace = namespace_examples / "none" - config = Config(settings_path=no_namespace) - manual_namespace = Config(settings_path=no_namespace, namespace_packages=["root"]) - assert place.module("root.name", config=config) == "FIRSTPARTY" - assert place.module("root.nested", config=config) == "FIRSTPARTY" - assert place.module("root.name", config=manual_namespace) == "THIRDPARTY" - assert place.module("root.nested", config=config) == "FIRSTPARTY" - - - - + almost_implicit = namespace_examples / "almost-implicit" + for lacks_namespace in (no_namespace, almost_implicit): + config = Config(settings_path=lacks_namespace) + manual_namespace = Config(settings_path=lacks_namespace, namespace_packages=["root"]) + assert place.module("root.name", config=config) == "FIRSTPARTY" + assert place.module("root.nested", config=config) == "FIRSTPARTY" + assert place.module("root.name", config=manual_namespace) == "THIRDPARTY" + assert place.module("root.nested", config=config) == "FIRSTPARTY" From 8933c8a5c1855c7ba3745404fe60feb478902a3f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 7 Oct 2020 03:33:20 -0700 Subject: [PATCH 1143/1439] Add missing file --- tests/unit/example_projects/namespaces/almost-implicit/root/y.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/unit/example_projects/namespaces/almost-implicit/root/y.py diff --git a/tests/unit/example_projects/namespaces/almost-implicit/root/y.py b/tests/unit/example_projects/namespaces/almost-implicit/root/y.py new file mode 100644 index 000000000..e69de29bb From 2aab8d1b9bc59afc1227291e9ec8ea6a562593fa Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 7 Oct 2020 03:37:12 -0700 Subject: [PATCH 1144/1439] Address deepsource issue --- isort/place.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/place.py b/isort/place.py index e9909b599..f83252612 100644 --- a/isort/place.py +++ b/isort/place.py @@ -85,7 +85,7 @@ def _src_path( ) ): return _src_path(nested_module[0], config, (module_path,), new_prefix) - elif ( + if ( _is_module(module_path) or _is_package(module_path) or _src_path_is_module(src_path, root_module_name) From 466872adffb8cf9df4140f71c87bf9cc37b03d34 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 7 Oct 2020 03:46:36 -0700 Subject: [PATCH 1145/1439] Try different way to introspect __init__ file --- isort/place.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/isort/place.py b/isort/place.py index f83252612..c85cf83b6 100644 --- a/isort/place.py +++ b/isort/place.py @@ -122,14 +122,14 @@ def _is_namespace_package(path: Path, src_extensions: FrozenSet[str]) -> bool: if filenames: return False else: - with init_file.open("rb") as open_init_file: + with init_file.open() as open_init_file: file_start = open_init_file.read(4096) if ( - b"__import__('pkg_resources').declare_namespace(__name__)\n" not in file_start - and b'__import__("pkg_resources").declare_namespace(__name__)\n' not in file_start - and b"__path__ = __import__('pkgutil').extend_path(__path__, __name__)" + "__import__('pkg_resources').declare_namespace(__name__)" not in file_start + and '__import__("pkg_resources").declare_namespace(__name__)' not in file_start + and "__path__ = __import__('pkgutil').extend_path(__path__, __name__)" not in file_start - and b'__path__ = __import__("pkgutil").extend_path(__path__, __name__)' + and '__path__ = __import__("pkgutil").extend_path(__path__, __name__)' not in file_start ): return False From d6b69e9be396f1f28d468fa0c7e60f10fb094fbb Mon Sep 17 00:00:00 2001 From: James Curtin Date: Wed, 7 Oct 2020 09:06:53 -0400 Subject: [PATCH 1146/1439] Add documentation for Github Actions --- docs/configuration/github_action.md | 63 +++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 docs/configuration/github_action.md diff --git a/docs/configuration/github_action.md b/docs/configuration/github_action.md new file mode 100644 index 000000000..c7a8dc971 --- /dev/null +++ b/docs/configuration/github_action.md @@ -0,0 +1,63 @@ +# Github Action + +isort provides an official [Github Action][github-action-docs] that can be used as part of a CI/CD workflow to ensure a project's imports are properly sorted. +The action can be found on the [Github Actions Marketplace][python-isort]. + +## Usage + +The `python-isort` plugin is designed to be run in combination with the [`checkout`][checkout-action] and [`setup-python`][setup-python] actions. +By default, it will run recursively from the root of the repository being linted and will exit with an error if the code is not properly sorted. + +### Inputs + +#### `isortVersion` + +Optional. Version of `isort` to use. Defaults to latest version of `isort`. + +#### `sortPaths` + +Optional. List of paths to sort, relative to your project root. Defaults to `.` + +#### `configuration` + +Optional. `isort` configuration options to pass to the `isort` CLI. Defaults to `--check-only --diff`. + +#### `requirementsFiles` + +Optional. Paths to python requirements files to install before running isort. +If multiple requirements files are provided, they should be separated by a space. +If custom package installation is required, dependencies should be installed in a separate step before using this action. + +!!! tip + It is important that the project's dependencies be installed before running isort so that third-party libraries are properly sorted. + +### Outputs + +#### `isort-result` + +Output of the `isort` CLI. + +### Example usage + +```yaml +name: Run isort +on: + - push + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: 3.8 + - uses: jamescurtin/isort-action@master + with: + requirementsFiles: "requirements.txt requirements-test.txt" +``` + +[github-action-docs]: https://docs.github.com/en/free-pro-team@latest/actions +[python-isort]: https://github.com/marketplace/actions/python-isort +[checkout-action]: https://github.com/actions/checkout +[setup-python]: https://github.com/actions/setup-python From c8d37de3d11b03652e4f096d75e6b65dfb45b663 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 7 Oct 2020 21:35:01 -0700 Subject: [PATCH 1147/1439] Fixed #1539: In extremely rare cases isort 5.5.4 introduces syntax error by removing closing paren. --- CHANGELOG.md | 5 +++- isort/_version.py | 2 +- isort/core.py | 5 ++-- pyproject.toml | 2 +- tests/unit/test_regressions.py | 49 ++++++++++++++++++++++++++++++++++ 5 files changed, 58 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c5ee4d73..a46e8e92a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,12 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. Find out more about isort's release policy [here](https://pycqa.github.io/isort/docs/major_releases/release_policy/). +### 5.5.5 [Hotfix] October 7, 2020 + - Fixed #1539: isort 5.5.4 introduces syntax error by removing closing paren. + ### 5.5.4 [Hotfix] September 29, 2020 - Fixed #1507: in rare cases isort changes the content of multiline strings after a yield statement. - - Fixed #1505: Support case where known_SECTION points to a section not listed in sections. + - Fixed #1505: Support case where known_SECTION points to a section not listed in sections. ### 5.5.3 [Hotfix] September 20, 2020 - Fixed #1488: in rare cases isort can mangle `yield from` or `raise from` statements. diff --git a/isort/_version.py b/isort/_version.py index e82c92eb4..3a831202b 100644 --- a/isort/_version.py +++ b/isort/_version.py @@ -1 +1 @@ -__version__ = "5.5.4" +__version__ = "5.5.5" diff --git a/isort/core.py b/isort/core.py index 696e5d751..558fc2adc 100644 --- a/isort/core.py +++ b/isort/core.py @@ -153,6 +153,7 @@ def process( in_top_comment = False first_comment_index_end = index - 1 + was_in_quote = bool(in_quote) if (not stripped_line.startswith("#") or in_quote) and '"' in line or "'" in line: char_index = 0 if first_comment_index_start == -1 and ( @@ -178,8 +179,8 @@ def process( break char_index += 1 - not_imports = bool(in_quote) or in_top_comment or isort_off - if not (in_quote or in_top_comment): + not_imports = bool(in_quote) or was_in_quote or in_top_comment or isort_off + if not (in_quote or was_in_quote or in_top_comment): if isort_off: if stripped_line == "# isort: on": isort_off = False diff --git a/pyproject.toml b/pyproject.toml index 75fb08633..e9b1fdc49 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ line-length = 100 [tool.poetry] name = "isort" -version = "5.5.4" +version = "5.5.5" description = "A Python utility / library to sort Python imports." authors = ["Timothy Crosley "] license = "MIT" diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index 78888c409..09d4302d1 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -1121,3 +1121,52 @@ def d(): ''', show_diff=True, ) + + +def test_isort_shouldnt_introduce_syntax_error_issue_1539(): + """isort should NEVER introduce syntax errors. + In 5.5.4 some strings that contained a line starting with from could lead to no empty paren. + See: https://github.com/PyCQA/isort/issues/1539. + """ + assert isort.check_code( + '''"""Foobar + from {}""".format( + "bar", +) +''', + show_diff=True, + ) + assert isort.check_code( + '''"""Foobar + import {}""".format( + "bar", +) +''', + show_diff=True, + ) + assert ( + isort.code( + '''"""Foobar + from {}""" + from a import b, a +''', + ) + == '''"""Foobar + from {}""" + from a import a, b +''' + ) + assert ( + isort.code( + '''"""Foobar + from {}""" + import b + import a +''', + ) + == '''"""Foobar + from {}""" + import a + import b +''' + ) From 4691317a2b6b03e521004d8b824a7f22129dc09f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 7 Oct 2020 21:36:37 -0700 Subject: [PATCH 1148/1439] Emphasis rarity of issue --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a46e8e92a..a512acbb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. Find out more about isort's release policy [here](https://pycqa.github.io/isort/docs/major_releases/release_policy/). ### 5.5.5 [Hotfix] October 7, 2020 - - Fixed #1539: isort 5.5.4 introduces syntax error by removing closing paren. + - Fixed #1539: in extremely rare cases isort 5.5.4 introduces syntax error by removing closing paren. ### 5.5.4 [Hotfix] September 29, 2020 - Fixed #1507: in rare cases isort changes the content of multiline strings after a yield statement. From 092bdb15126710d285f160d84f1c883b3aee9b51 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 7 Oct 2020 23:35:49 -0700 Subject: [PATCH 1149/1439] Implemented #1540: Officially support Python 3.9 stdlib imports by default. --- CHANGELOG.md | 1 + isort/stdlibs/py3.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2869cbab..dcafa5347 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ - Fixed #1523: in rare cases isort can ignore direct from import if as import is also on same line. Potentially breaking changes: + - Implemented #1540: Officially support Python 3.9 stdlib imports by default. - Fixed #1443: Incorrect third vs first party categorization - namespace packages. - Fixed #1486: "Google" profile is not quite Google style. - Fixed "PyCharm" profile to always add 2 lines to be consistent with what PyCharm "Optimize Imports" does. diff --git a/isort/stdlibs/py3.py b/isort/stdlibs/py3.py index 78e0984d5..b7dbcc2cb 100644 --- a/isort/stdlibs/py3.py +++ b/isort/stdlibs/py3.py @@ -1,3 +1,3 @@ -from . import py35, py36, py37, py38 +from . import py35, py36, py37, py38, py39 -stdlib = py35.stdlib | py36.stdlib | py37.stdlib | py38.stdlib +stdlib = py35.stdlib | py36.stdlib | py37.stdlib | py38.stdlib | py39.stdlib From 6930fb277ed135f3c2a0fbd928c4a8d6bf803447 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 7 Oct 2020 23:37:38 -0700 Subject: [PATCH 1150/1439] Finalize 5.6.0 changelog --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dcafa5347..44e5fd899 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. Find out more about isort's release policy [here](https://pycqa.github.io/isort/docs/major_releases/release_policy/). -### 5.6.0 TBD +### 5.6.0 October 7, 2020 - Implemented #1433: Provide helpful feedback in case a custom config file is specified without a configuration. - Implemented #1494: Default to sorting imports within `.pxd` files. - Implemented #1502: Improved float-to-top behavior when there is an existing import section present at top-of-file. @@ -27,6 +27,8 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ Goal Zero: (Tickets related to aspirational goal of achieving 0 regressions for remaining 5.0.0 lifespan): - Implemented #1472: Full testing of stdin CLI Options + - Added additional branch coverage. + - More projects added to integration test suite. ### 5.5.5 [Hotfix] October 7, 2020 - Fixed #1539: in extremely rare cases isort 5.5.4 introduces syntax error by removing closing paren. From 9b260bb079dca3d3d2f42935037f0c5092fb429d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 7 Oct 2020 23:38:58 -0700 Subject: [PATCH 1151/1439] Bump version to 5.6.0 --- isort/_version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/_version.py b/isort/_version.py index 3a831202b..a1e2271ea 100644 --- a/isort/_version.py +++ b/isort/_version.py @@ -1 +1 @@ -__version__ = "5.5.5" +__version__ = "5.6.0" diff --git a/pyproject.toml b/pyproject.toml index 45fd6fc0f..8831e38c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ line-length = 100 [tool.poetry] name = "isort" -version = "5.5.5" +version = "5.6.0" description = "A Python utility / library to sort Python imports." authors = ["Timothy Crosley "] license = "MIT" From f0b7ec9d694821b0a92e7769b1341e7a6ca2c47d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 7 Oct 2020 23:49:24 -0700 Subject: [PATCH 1152/1439] Test against 3.9 --- .github/workflows/integration.yml | 2 +- .github/workflows/test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 283103118..beefbf5a9 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.8] + python-version: [3.9] steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ef93a1eea..bd46ab7b8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.6, 3.7, 3.8] + python-version: [3.6, 3.7, 3.8, 3.9] os: [ubuntu-latest, ubuntu-18.04, macos-latest, windows-latest] steps: From 2837927fb3f31ef11394617b4475fdb0799a5f73 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 7 Oct 2020 23:52:07 -0700 Subject: [PATCH 1153/1439] Revert "Test against 3.9" This reverts commit f0b7ec9d694821b0a92e7769b1341e7a6ca2c47d. --- .github/workflows/integration.yml | 2 +- .github/workflows/test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index beefbf5a9..283103118 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.9] + python-version: [3.8] steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bd46ab7b8..ef93a1eea 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: [3.6, 3.7, 3.8] os: [ubuntu-latest, ubuntu-18.04, macos-latest, windows-latest] steps: From 9ed3c03a81414a2ced94991c4c66f18d2ee422db Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 7 Oct 2020 23:58:24 -0700 Subject: [PATCH 1154/1439] Improve changelog formatting --- CHANGELOG.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44e5fd899..595033f32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,13 +19,13 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ - Fixed #1525: Some warnings can't be disabled with --quiet. - Fixed #1523: in rare cases isort can ignore direct from import if as import is also on same line. - Potentially breaking changes: +#### Potentially breaking changes: - Implemented #1540: Officially support Python 3.9 stdlib imports by default. - Fixed #1443: Incorrect third vs first party categorization - namespace packages. - Fixed #1486: "Google" profile is not quite Google style. - Fixed "PyCharm" profile to always add 2 lines to be consistent with what PyCharm "Optimize Imports" does. - Goal Zero: (Tickets related to aspirational goal of achieving 0 regressions for remaining 5.0.0 lifespan): +#### Goal Zero: (Tickets related to aspirational goal of achieving 0 regressions for remaining 5.0.0 lifespan): - Implemented #1472: Full testing of stdin CLI Options - Added additional branch coverage. - More projects added to integration test suite. @@ -66,12 +66,12 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ - Implemented #1405: Added support for coloring diff output. - Implemented #1434: New multi-line grid mode without parentheses. -Goal Zero (Tickets related to aspirational goal of achieving 0 regressions for remaining 5.0.0 lifespan): +#### Goal Zero (Tickets related to aspirational goal of achieving 0 regressions for remaining 5.0.0 lifespan): - Implemented #1392: Extensive profile testing. - Implemented #1393: Proprety based testing applied to code snippets. - Implemented #1391: Create automated integration test that includes full code base of largest OpenSource isort users. -Potentially breaking changes: +#### Potentially breaking changes: - Fixed #1429: --check doesn't print to stderr as the documentation says. This means if you were looking for `ERROR:` messages for files that contain incorrect imports within stdout you will now need to look in stderr. ### 5.4.2 Aug 14, 2020 @@ -106,7 +106,7 @@ Potentially breaking changes: - Fixed #1366: spurious errors when combining skip with --gitignore. - Fixed #1359: --skip-gitignore does not honor ignored symlink -Internal Development: +#### Internal Development: - Initial hypothesmith powered test to help catch unexpected syntax parsing and output errors (thanks @Zac-HD!) ### 5.2.2 July 30, 2020 @@ -158,7 +158,7 @@ Internal Development: - Fixed #1315: Extra newline before comment with -n + --fss. - Fixed #1192: `-k` or `--keep-direct-and-as-imports` option has been deprecated as it is now always on. -**Formatting changes implied:** +#### Formatting changes implied: - Fixed #1280: rewrite of as imports changes the behavior of the imports. ### 5.0.9 July 11, 2020 From 9c0ff4952bc3a2e8dc605e88815c6e5662b81f8d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 8 Oct 2020 05:11:22 -0700 Subject: [PATCH 1155/1439] Hotfix: Fixed #1546: Unstable (non-idempotent) behavior with certain src trees. --- CHANGELOG.md | 3 +++ isort/_version.py | 2 +- isort/place.py | 5 ++++- isort/settings.py | 14 +++++++++----- pyproject.toml | 2 +- tests/integration/test_projects_using_isort.py | 5 +++++ 6 files changed, 23 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 595033f32..02bb467f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. Find out more about isort's release policy [here](https://pycqa.github.io/isort/docs/major_releases/release_policy/). +### 5.6.1 [Hotfix] October 8, 2020 + - Fixed #1546: Unstable (non-idempotent) behavior with certain src trees. + ### 5.6.0 October 7, 2020 - Implemented #1433: Provide helpful feedback in case a custom config file is specified without a configuration. - Implemented #1494: Default to sorting imports within `.pxd` files. diff --git a/isort/_version.py b/isort/_version.py index a1e2271ea..a23647501 100644 --- a/isort/_version.py +++ b/isort/_version.py @@ -1 +1 @@ -__version__ = "5.6.0" +__version__ = "5.6.1" diff --git a/isort/place.py b/isort/place.py index c85cf83b6..aaf954aa8 100644 --- a/isort/place.py +++ b/isort/place.py @@ -117,7 +117,10 @@ def _is_namespace_package(path: Path, src_extensions: FrozenSet[str]) -> bool: init_file = path / "__init__.py" if not init_file.exists(): filenames = [ - filename for filename in path.iterdir() if filename.suffix.lstrip(".") in src_extensions + filepath + for filepath in path.iterdir() + if filepath.suffix.lstrip(".") in src_extensions + or filepath.name.lower() in ("setup.cfg", "pyproject.toml") ] if filenames: return False diff --git a/isort/settings.py b/isort/settings.py index e80a989dc..158aa36ca 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -182,7 +182,7 @@ class _Config: directory: str = "" profile: str = "" honor_noqa: bool = False - src_paths: FrozenSet[Path] = frozenset() + src_paths: Tuple[Path, ...] = tuple() old_finders: bool = False remove_redundant_aliases: bool = False float_to_top: bool = False @@ -401,11 +401,15 @@ def __init__( path_root = Path(combined_config.get("directory", project_root)).resolve() path_root = path_root if path_root.is_dir() else path_root.parent if "src_paths" not in combined_config: - combined_config["src_paths"] = frozenset((path_root, path_root / "src")) + combined_config["src_paths"] = (path_root / "src", path_root) else: - combined_config["src_paths"] = frozenset( - path_root / path for path in combined_config.get("src_paths", ()) - ) + src_paths: List[Path] = [] + for src_path in combined_config.get("src_paths", ()): + full_path = path_root / src_path + if full_path not in src_paths: + src_paths.append(full_path) + + combined_config["src_paths"] = tuple(src_paths) if "formatter" in combined_config: import pkg_resources diff --git a/pyproject.toml b/pyproject.toml index 8831e38c2..81d45cc27 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ line-length = 100 [tool.poetry] name = "isort" -version = "5.6.0" +version = "5.6.1" description = "A Python utility / library to sort Python imports." authors = ["Timothy Crosley "] license = "MIT" diff --git a/tests/integration/test_projects_using_isort.py b/tests/integration/test_projects_using_isort.py index 95f256b29..d39b942b3 100644 --- a/tests/integration/test_projects_using_isort.py +++ b/tests/integration/test_projects_using_isort.py @@ -140,3 +140,8 @@ def test_pyramid(tmpdir): def test_products_zopetree(tmpdir): git_clone("https://github.com/jugmac00/Products.ZopeTree.git", tmpdir) run_isort([str(tmpdir)]) + + +def test_dobby(tmpdir): + git_clone("https://github.com/rocketDuck/dobby.git", tmpdir) + run_isort([str(tmpdir / "tests"), str(tmpdir / "src")]) From ba43b3cf2c182b3be20964d30b85ef6e7c82ee9f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 8 Oct 2020 05:14:49 -0700 Subject: [PATCH 1156/1439] Fix issue identified by deepsource: unnecessary call to tuple --- isort/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/settings.py b/isort/settings.py index 158aa36ca..96fc9410e 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -182,7 +182,7 @@ class _Config: directory: str = "" profile: str = "" honor_noqa: bool = False - src_paths: Tuple[Path, ...] = tuple() + src_paths: Tuple[Path, ...] = () old_finders: bool = False remove_redundant_aliases: bool = False float_to_top: bool = False From 49878c505ccfd6dceb73c12ead3459c50115a8b7 Mon Sep 17 00:00:00 2001 From: Marco Gorelli Date: Thu, 8 Oct 2020 13:42:38 +0100 Subject: [PATCH 1157/1439] Let pre-commit sort pxd files too In #1494, it looks like support for `.pxd` files was added. However, these are currently ignored when running isort in pre-commit because there is `types: [python]` --- .pre-commit-hooks.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 0027e49df..25b8325ab 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -3,5 +3,6 @@ entry: isort require_serial: true language: python - types: [python] + files: '.pxd$|.py$' + types: [file] args: ['--filter-files'] From 0179b07d680a624922bf75ee02c03551aab16f05 Mon Sep 17 00:00:00 2001 From: Marco Gorelli Date: Thu, 8 Oct 2020 13:44:43 +0100 Subject: [PATCH 1158/1439] remove unnecessary types file --- .pre-commit-hooks.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 25b8325ab..ad64de981 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -4,5 +4,4 @@ require_serial: true language: python files: '.pxd$|.py$' - types: [file] args: ['--filter-files'] From 245471d3dec8453d27a3c3c4895526a9cd49c647 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 8 Oct 2020 23:58:41 -0700 Subject: [PATCH 1159/1439] Fixed #1548: On rare occasions an unecessary empty line can be added when an import is marked as skipped. --- CHANGELOG.md | 3 ++ isort/core.py | 3 +- isort/parse.py | 20 ++++++-- tests/unit/test_regressions.py | 86 ++++++++++++++++++++++++++++++++++ 4 files changed, 106 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02bb467f9..f861830c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. Find out more about isort's release policy [here](https://pycqa.github.io/isort/docs/major_releases/release_policy/). +### 5.6.2 TBD + - Fixed #1548: On rare occasions an unecessary empty line can be added when an import is marked as skipped. + ### 5.6.1 [Hotfix] October 8, 2020 - Fixed #1546: Unstable (non-idempotent) behavior with certain src trees. diff --git a/isort/core.py b/isort/core.py index 3131c2011..292bdc1c2 100644 --- a/isort/core.py +++ b/isort/core.py @@ -85,7 +85,8 @@ def process( isort_off = True if current: if add_imports: - current += line_separator + line_separator.join(add_imports) + add_line_separator = line_separator or "\n" + current += add_line_separator + add_line_separator.join(add_imports) add_imports = [] parsed = parse.file_contents(current, config=config) verbose_output += parsed.verbose_output diff --git a/isort/parse.py b/isort/parse.py index 9a80e97b9..96468f095 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -213,12 +213,22 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte and not lstripped_line.startswith("#") and not lstripped_line.startswith("'''") and not lstripped_line.startswith('"""') - and not lstripped_line.startswith("import") - and not lstripped_line.startswith("from") ): - import_index = index - 1 - while import_index and not in_lines[import_index - 1]: - import_index -= 1 + if not lstripped_line.startswith("import") and not lstripped_line.startswith("from"): + import_index = index - 1 + while import_index and not in_lines[import_index - 1]: + import_index -= 1 + elif "isort:skip" in line or "isort: skip" in line: + commentless = line.split("#", 1)[0] + if ( + "(" in commentless + and not commentless.rstrip().endswith(")") + and import_index < line_count + ): + import_index = index + while import_index < line_count and not commentless.rstrip().endswith(")"): + commentless = in_lines[import_index].split("#", 1)[0] + import_index += 1 line, *end_of_line_comment = line.split("#", 1) if ";" in line: diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index 9f6f92de3..87b533d63 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -1336,3 +1336,89 @@ def test_isort_shouldnt_introduce_syntax_error_issue_1539(): import b ''' ) + + +def test_isort_shouldnt_split_skip_issue_1548(): + """Ensure isort doesn't add a spurious new line if isort: skip is combined with float to top. + See: https://github.com/PyCQA/isort/issues/1548. + """ + assert isort.check_code( + """from tools.dependency_pruning.prune_dependencies import ( # isort:skip + prune_dependencies, +) +""", + show_diff=True, + profile="black", + float_to_top=True, + ) + assert isort.check_code( + """from tools.dependency_pruning.prune_dependencies import ( # isort:skip + prune_dependencies, +) +import a +import b +""", + show_diff=True, + profile="black", + float_to_top=True, + ) + assert isort.check_code( + """from tools.dependency_pruning.prune_dependencies import # isort:skip +import a +import b +""", + show_diff=True, + float_to_top=True, + ) + assert isort.check_code( + """from tools.dependency_pruning.prune_dependencies import ( # isort:skip + a +) +import b +""", + show_diff=True, + profile="black", + float_to_top=True, + ) + assert isort.check_code( + """from tools.dependency_pruning.prune_dependencies import ( # isort:skip + ) +""", + show_diff=True, + profile="black", + float_to_top=True, + ) + assert isort.check_code( + """from tools.dependency_pruning.prune_dependencies import ( # isort:skip +)""", + show_diff=True, + profile="black", + float_to_top=True, + ) + assert ( + isort.code( + """from tools.dependency_pruning.prune_dependencies import ( # isort:skip +) +""", + profile="black", + float_to_top=True, + add_imports=["import os"], + ) + == """from tools.dependency_pruning.prune_dependencies import ( # isort:skip +) +import os +""" + ) + assert ( + isort.code( + """from tools.dependency_pruning.prune_dependencies import ( # isort:skip +)""", + profile="black", + float_to_top=True, + add_imports=["import os"], + ) + == """from tools.dependency_pruning.prune_dependencies import ( # isort:skip +) +import os +""" + ) From b66853ff81b02bada800f9222905e45fa4871b28 Mon Sep 17 00:00:00 2001 From: Timothy Edmund Crosley Date: Fri, 9 Oct 2020 00:12:48 -0700 Subject: [PATCH 1160/1439] Update .pre-commit-hooks.yaml Use types, not extensions --- .pre-commit-hooks.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index ad64de981..6bfb2bba9 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -3,5 +3,5 @@ entry: isort require_serial: true language: python - files: '.pxd$|.py$' + types: [python, cython, pyi] args: ['--filter-files'] From d5afc93770437eb0048718bfe676da9bfb187dc6 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 9 Oct 2020 03:59:48 -0700 Subject: [PATCH 1161/1439] Add zope to integration test suite --- tests/integration/test_projects_using_isort.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/integration/test_projects_using_isort.py b/tests/integration/test_projects_using_isort.py index d39b942b3..34540cbba 100644 --- a/tests/integration/test_projects_using_isort.py +++ b/tests/integration/test_projects_using_isort.py @@ -145,3 +145,8 @@ def test_products_zopetree(tmpdir): def test_dobby(tmpdir): git_clone("https://github.com/rocketDuck/dobby.git", tmpdir) run_isort([str(tmpdir / "tests"), str(tmpdir / "src")]) + + +def test_zope(tmpdir): + git_clone("https://github.com/zopefoundation/Zope.git", tmpdir) + run_isort([str(tmpdir)]) From 8b9e45e18320d395152d3ea49b4d4071ed5312a3 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 10 Oct 2020 02:52:33 -0700 Subject: [PATCH 1162/1439] Fixed #1542: Bug in VERTICAL_PREFIX_FROM_MODULE_IMPORT wrap mode. --- CHANGELOG.md | 1 + isort/wrap_modes.py | 34 +++++++++++++++++++++------------- tests/unit/test_regressions.py | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f861830c3..e774eaad6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ ### 5.6.2 TBD - Fixed #1548: On rare occasions an unecessary empty line can be added when an import is marked as skipped. + - Fixed #1542: Bug in VERTICAL_PREFIX_FROM_MODULE_IMPORT wrap mode. ### 5.6.1 [Hotfix] October 8, 2020 - Fixed #1546: Unstable (non-idempotent) behavior with certain src trees. diff --git a/isort/wrap_modes.py b/isort/wrap_modes.py index ccfc1cd5a..db89d0ecc 100644 --- a/isort/wrap_modes.py +++ b/isort/wrap_modes.py @@ -273,33 +273,41 @@ def vertical_hanging_indent_bracket(**interface): def vertical_prefix_from_module_import(**interface): if not interface["imports"]: return "" + prefix_statement = interface["statement"] - interface["statement"] += interface["imports"].pop(0) - while interface["imports"]: - next_import = interface["imports"].pop(0) - next_statement = isort.comments.add_to_line( - interface["comments"], - interface["statement"] + ", " + next_import, + output_statement = prefix_statement + interface["imports"].pop(0) + comments = interface["comments"] + + statement = output_statement + statement_with_comments = "" + for next_import in interface["imports"]: + statement = statement + ", " + next_import + statement_with_comments = isort.comments.add_to_line( + comments, + statement, removed=interface["remove_comments"], comment_prefix=interface["comment_prefix"], ) if ( - len(next_statement.split(interface["line_separator"])[-1]) + 1 + len(statement_with_comments.split(interface["line_separator"])[-1]) + 1 > interface["line_length"] ): - next_statement = ( + statement = ( isort.comments.add_to_line( interface["comments"], - f"{interface['statement']}", + output_statement, removed=interface["remove_comments"], comment_prefix=interface["comment_prefix"], ) + f"{interface['line_separator']}{prefix_statement}{next_import}" ) - interface["comments"] = [] - interface["statement"] = next_statement - return interface["statement"] - + comments = [] + output_statement = statement + + if comments and statement_with_comments: + output_statement = statement_with_comments + return output_statement + @_wrap_mode def hanging_indent_with_parentheses(**interface): diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index 87b533d63..248285429 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -1422,3 +1422,36 @@ def test_isort_shouldnt_split_skip_issue_1548(): import os """ ) + + +def test_isort_losing_imports_vertical_prefix_from_module_import_wrap_mode_issue_1542(): + """Ensure isort doesnt lose imports when a comment is combined with an import and + wrap mode VERTICAL_PREFIX_FROM_MODULE_IMPORT is used. + See: https://github.com/PyCQA/isort/issues/1542. + """ + assert isort.code( + """ +from xxxxxxxxxxxxxxxx import AAAAAAAAAA, BBBBBBBBBB +from xxxxxxxxxxxxxxxx import CCCCCCCCC, DDDDDDDDD # xxxxxxxxxxxxxxxxxx + +print(CCCCCCCCC) +""", + multi_line_output=9, + ) == """ +from xxxxxxxxxxxxxxxx import AAAAAAAAAA, BBBBBBBBBB # xxxxxxxxxxxxxxxxxx +from xxxxxxxxxxxxxxxx import CCCCCCCCC, DDDDDDDDD + +print(CCCCCCCCC) +""" + + assert isort.check_code( + """ +from xxxxxxxxxxxxxxxx import AAAAAAAAAA, BBBBBBBBBB + +from xxxxxxxxxxxxxxxx import CCCCCCCCC, DDDDDDDDD # xxxxxxxxxxxxxxxxxx isort: skip + +print(CCCCCCCCC) +""", + show_diff = True, + multi_line_output=9, + ) From e478e7c8f8a5e56dcda6f5b87f350445efdaaea0 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 10 Oct 2020 03:12:58 -0700 Subject: [PATCH 1163/1439] Fix formatting --- isort/wrap_modes.py | 10 +++++----- tests/unit/test_regressions.py | 13 ++++++++----- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/isort/wrap_modes.py b/isort/wrap_modes.py index db89d0ecc..02b793067 100644 --- a/isort/wrap_modes.py +++ b/isort/wrap_modes.py @@ -273,11 +273,11 @@ def vertical_hanging_indent_bracket(**interface): def vertical_prefix_from_module_import(**interface): if not interface["imports"]: return "" - + prefix_statement = interface["statement"] output_statement = prefix_statement + interface["imports"].pop(0) comments = interface["comments"] - + statement = output_statement statement_with_comments = "" for next_import in interface["imports"]: @@ -294,7 +294,7 @@ def vertical_prefix_from_module_import(**interface): ): statement = ( isort.comments.add_to_line( - interface["comments"], + comments, output_statement, removed=interface["remove_comments"], comment_prefix=interface["comment_prefix"], @@ -303,11 +303,11 @@ def vertical_prefix_from_module_import(**interface): ) comments = [] output_statement = statement - + if comments and statement_with_comments: output_statement = statement_with_comments return output_statement - + @_wrap_mode def hanging_indent_with_parentheses(**interface): diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index 248285429..873cc2f72 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -1429,20 +1429,23 @@ def test_isort_losing_imports_vertical_prefix_from_module_import_wrap_mode_issue wrap mode VERTICAL_PREFIX_FROM_MODULE_IMPORT is used. See: https://github.com/PyCQA/isort/issues/1542. """ - assert isort.code( - """ + assert ( + isort.code( + """ from xxxxxxxxxxxxxxxx import AAAAAAAAAA, BBBBBBBBBB from xxxxxxxxxxxxxxxx import CCCCCCCCC, DDDDDDDDD # xxxxxxxxxxxxxxxxxx print(CCCCCCCCC) """, - multi_line_output=9, - ) == """ + multi_line_output=9, + ) + == """ from xxxxxxxxxxxxxxxx import AAAAAAAAAA, BBBBBBBBBB # xxxxxxxxxxxxxxxxxx from xxxxxxxxxxxxxxxx import CCCCCCCCC, DDDDDDDDD print(CCCCCCCCC) """ + ) assert isort.check_code( """ @@ -1452,6 +1455,6 @@ def test_isort_losing_imports_vertical_prefix_from_module_import_wrap_mode_issue print(CCCCCCCCC) """, - show_diff = True, + show_diff=True, multi_line_output=9, ) From 9a79067846cff03aca6b7460056aaacffec6a36c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 10 Oct 2020 03:41:32 -0700 Subject: [PATCH 1164/1439] Add test for unseekable pipe errors --- isort/api.py | 3 ++ tests/unit/test_main.py | 62 ++++++++++++++++++++++++++++------------- 2 files changed, 46 insertions(+), 19 deletions(-) diff --git a/isort/api.py b/isort/api.py index a8cecb623..5a2df6af3 100644 --- a/isort/api.py +++ b/isort/api.py @@ -200,6 +200,9 @@ def check_stream( """ config = _config(path=file_path, config=config, **config_kwargs) + if show_diff: + input_stream = StringIO(input_stream.read()) + changed: bool = sort_stream( input_stream=input_stream, output_stream=Empty, diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index caf20e7ed..b7bc7a00e 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -14,6 +14,11 @@ from isort.wrap_modes import WrapModes +class UnseekableTextIOWrapper(TextIOWrapper): + def seek(self, *args, **kwargs): + raise ValueError("underlying stream is not seekable") + + @given( file_name=st.text(), config=st.builds(Config), @@ -343,7 +348,7 @@ def test_isort_command(): def test_isort_with_stdin(capsys): # ensures that isort sorts stdin without any flags - input_content = TextIOWrapper( + input_content = UnseekableTextIOWrapper( BytesIO( b""" import b @@ -362,7 +367,7 @@ def test_isort_with_stdin(capsys): """ ) - input_content_from = TextIOWrapper( + input_content_from = UnseekableTextIOWrapper( BytesIO( b""" import c @@ -385,7 +390,7 @@ def test_isort_with_stdin(capsys): # ensures that isort correctly sorts stdin with --fas flag - input_content = TextIOWrapper( + input_content = UnseekableTextIOWrapper( BytesIO( b""" import sys @@ -411,7 +416,7 @@ def test_isort_with_stdin(capsys): # ensures that isort correctly sorts stdin with --fass flag - input_content = TextIOWrapper( + input_content = UnseekableTextIOWrapper( BytesIO( b""" from a import Path, abc @@ -430,7 +435,7 @@ def test_isort_with_stdin(capsys): # ensures that isort correctly sorts stdin with --ff flag - input_content = TextIOWrapper( + input_content = UnseekableTextIOWrapper( BytesIO( b""" import b @@ -453,7 +458,7 @@ def test_isort_with_stdin(capsys): # ensures that isort correctly sorts stdin with -fss flag - input_content = TextIOWrapper( + input_content = UnseekableTextIOWrapper( BytesIO( b""" import b @@ -472,7 +477,7 @@ def test_isort_with_stdin(capsys): """ ) - input_content = TextIOWrapper( + input_content = UnseekableTextIOWrapper( BytesIO( b""" import a @@ -493,7 +498,7 @@ def test_isort_with_stdin(capsys): # ensures that isort correctly sorts stdin with --ds flag - input_content = TextIOWrapper( + input_content = UnseekableTextIOWrapper( BytesIO( b""" import sys @@ -516,7 +521,7 @@ def test_isort_with_stdin(capsys): # ensures that isort correctly sorts stdin with --cs flag - input_content = TextIOWrapper( + input_content = UnseekableTextIOWrapper( BytesIO( b""" from a import b @@ -536,7 +541,7 @@ def test_isort_with_stdin(capsys): # ensures that isort correctly sorts stdin with --ca flag - input_content = TextIOWrapper( + input_content = UnseekableTextIOWrapper( BytesIO( b""" from a import x as X @@ -556,7 +561,7 @@ def test_isort_with_stdin(capsys): # ensures that isort works consistently with check and ws flags - input_content = TextIOWrapper( + input_content = UnseekableTextIOWrapper( BytesIO( b""" import os @@ -570,10 +575,29 @@ def test_isort_with_stdin(capsys): out, error = capsys.readouterr() assert not error + + # ensures that isort works consistently with check and diff flags + + input_content = UnseekableTextIOWrapper( + BytesIO( + b""" +import b +import a +""" + ) + ) + + with pytest.raises(SystemExit): + main.main(["-", "--check", "--diff"], stdin=input_content) + out, error = capsys.readouterr() + + assert error + assert not "underlying stream is not seekable" in error + assert not "underlying stream is not seekable" in error # ensures that isort correctly sorts stdin with --ls flag - input_content = TextIOWrapper( + input_content = UnseekableTextIOWrapper( BytesIO( b""" import abcdef @@ -594,7 +618,7 @@ def test_isort_with_stdin(capsys): # ensures that isort correctly sorts stdin with --nis flag - input_content = TextIOWrapper( + input_content = UnseekableTextIOWrapper( BytesIO( b""" from z import b, c, a @@ -613,7 +637,7 @@ def test_isort_with_stdin(capsys): # ensures that isort correctly sorts stdin with --sl flag - input_content = TextIOWrapper( + input_content = UnseekableTextIOWrapper( BytesIO( b""" from z import b, c, a @@ -634,7 +658,7 @@ def test_isort_with_stdin(capsys): # ensures that isort correctly sorts stdin with --top flag - input_content = TextIOWrapper( + input_content = UnseekableTextIOWrapper( BytesIO( b""" import os @@ -655,7 +679,7 @@ def test_isort_with_stdin(capsys): # ensure that isort correctly sorts stdin with --os flag - input_content = TextIOWrapper( + input_content = UnseekableTextIOWrapper( BytesIO( b""" import sys @@ -680,7 +704,7 @@ def test_isort_with_stdin(capsys): ) # ensures that isort warns with deprecated flags with stdin - input_content = TextIOWrapper( + input_content = UnseekableTextIOWrapper( BytesIO( b""" import sys @@ -701,7 +725,7 @@ def test_isort_with_stdin(capsys): """ ) - input_content = TextIOWrapper( + input_content = UnseekableTextIOWrapper( BytesIO( b""" import sys @@ -723,7 +747,7 @@ def test_isort_with_stdin(capsys): ) # ensures that only-modified flag works with stdin - input_content = TextIOWrapper( + input_content = UnseekableTextIOWrapper( BytesIO( b""" import a From 320124e8a5637fdcd9850eb2d2aa6d5b7e72be33 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 10 Oct 2020 03:51:18 -0700 Subject: [PATCH 1165/1439] Fixed #1552: Pylama test dependent on source layout. --- CHANGELOG.md | 5 +++++ tests/unit/test_pylama_isort.py | 8 +++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e774eaad6..661ef3a3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ ### 5.6.2 TBD - Fixed #1548: On rare occasions an unecessary empty line can be added when an import is marked as skipped. - Fixed #1542: Bug in VERTICAL_PREFIX_FROM_MODULE_IMPORT wrap mode. + - Fixed #1552: Pylama test dependent on source layout. + +#### Goal Zero: (Tickets related to aspirational goal of achieving 0 regressions for remaining 5.0.0 lifespan): + - Zope added to integration test suite + - Additional testing of CLI (simulate unseekable streams) ### 5.6.1 [Hotfix] October 8, 2020 - Fixed #1546: Unstable (non-idempotent) behavior with certain src trees. diff --git a/tests/unit/test_pylama_isort.py b/tests/unit/test_pylama_isort.py index 1fe19bfa1..035b5e31d 100644 --- a/tests/unit/test_pylama_isort.py +++ b/tests/unit/test_pylama_isort.py @@ -11,14 +11,16 @@ def test_allow(self): assert not self.instance.allow("test_case.c") assert self.instance.allow("test_case.py") - def test_run(self, src_dir, tmpdir): - assert not self.instance.run(os.path.join(src_dir, "api.py")) + def test_run(self, tmpdir): + correct = tmpdir.join("incorrect.py") + correct.write("import a\nimport b\n") + assert not self.instance.run(str(correct)) incorrect = tmpdir.join("incorrect.py") incorrect.write("import b\nimport a\n") assert self.instance.run(str(incorrect)) - def test_skip(self, src_dir, tmpdir): + def test_skip(self, tmpdir): incorrect = tmpdir.join("incorrect.py") incorrect.write("# isort: skip_file\nimport b\nimport a\n") assert not self.instance.run(str(incorrect)) From 618300622a897ee61a614586a58a2095df953d71 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 10 Oct 2020 03:52:05 -0700 Subject: [PATCH 1166/1439] Bump version to 5.6.2 --- isort/_version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/_version.py b/isort/_version.py index a23647501..8d9ad75cd 100644 --- a/isort/_version.py +++ b/isort/_version.py @@ -1 +1 @@ -__version__ = "5.6.1" +__version__ = "5.6.2" diff --git a/pyproject.toml b/pyproject.toml index 81d45cc27..a5ffa0381 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ line-length = 100 [tool.poetry] name = "isort" -version = "5.6.1" +version = "5.6.2" description = "A Python utility / library to sort Python imports." authors = ["Timothy Crosley "] license = "MIT" From 6633e69b0966fc779fd54c24f7333fb8c0166f80 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 10 Oct 2020 03:53:23 -0700 Subject: [PATCH 1167/1439] Add release date --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 661ef3a3d..2b090d416 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. Find out more about isort's release policy [here](https://pycqa.github.io/isort/docs/major_releases/release_policy/). -### 5.6.2 TBD +### 5.6.2 October 10, 2020 - Fixed #1548: On rare occasions an unecessary empty line can be added when an import is marked as skipped. - Fixed #1542: Bug in VERTICAL_PREFIX_FROM_MODULE_IMPORT wrap mode. - Fixed #1552: Pylama test dependent on source layout. From 59dda8d503be642e6a7d38ab666bac4a0d317463 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 10 Oct 2020 03:57:11 -0700 Subject: [PATCH 1168/1439] Formatting --- tests/unit/test_main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index b7bc7a00e..6c5bef76c 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -575,7 +575,7 @@ def test_isort_with_stdin(capsys): out, error = capsys.readouterr() assert not error - + # ensures that isort works consistently with check and diff flags input_content = UnseekableTextIOWrapper( @@ -593,7 +593,7 @@ def test_isort_with_stdin(capsys): assert error assert not "underlying stream is not seekable" in error - assert not "underlying stream is not seekable" in error + assert not "underlying stream is not seekable" in error # ensures that isort correctly sorts stdin with --ls flag From c2412345aa9b75ec732aca57ba60ef4fe4b6c0aa Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 10 Oct 2020 04:06:17 -0700 Subject: [PATCH 1169/1439] Resolve linting errors --- tests/unit/test_main.py | 4 ++-- tests/unit/test_pylama_isort.py | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 6c5bef76c..59e224aed 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -592,8 +592,8 @@ def test_isort_with_stdin(capsys): out, error = capsys.readouterr() assert error - assert not "underlying stream is not seekable" in error - assert not "underlying stream is not seekable" in error + assert "underlying stream is not seekable" not in error + assert "underlying stream is not seekable" not in error # ensures that isort correctly sorts stdin with --ls flag diff --git a/tests/unit/test_pylama_isort.py b/tests/unit/test_pylama_isort.py index 035b5e31d..d3900c3f0 100644 --- a/tests/unit/test_pylama_isort.py +++ b/tests/unit/test_pylama_isort.py @@ -1,5 +1,3 @@ -import os - from isort.pylama_isort import Linter From 93db086913bcfcff4d9de06c3842ef4425bbde8a Mon Sep 17 00:00:00 2001 From: anirudnits Date: Sat, 10 Oct 2020 20:27:02 +0530 Subject: [PATCH 1170/1439] Added combine_straight_import flag in configurations schema --- isort/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/isort/settings.py b/isort/settings.py index 96fc9410e..553475e93 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -200,6 +200,7 @@ class _Config: dedup_headings: bool = False only_sections: bool = False only_modified: bool = False + combine_straight_imports: bool = False auto_identify_namespace_packages: bool = True namespace_packages: FrozenSet[str] = frozenset() From 0caca49f304271ad37f7f99dd70af9fe81da58d6 Mon Sep 17 00:00:00 2001 From: anirudnits Date: Sat, 10 Oct 2020 20:28:54 +0530 Subject: [PATCH 1171/1439] Added combine_straight_imports flag in arg_parser --- isort/main.py | 43 ++++++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/isort/main.py b/isort/main.py index a8e59f0ef..d2cd76b00 100644 --- a/isort/main.py +++ b/isort/main.py @@ -722,6 +722,32 @@ def _build_arg_parser() -> argparse.ArgumentParser: " there are multiple sections with the comment set.", ) + parser.add_argument( + "--only-sections", + "--os", + dest="only_sections", + action="store_true", + help="Causes imports to be sorted only based on their sections like STDLIB,THIRDPARTY etc. " + "Imports are unaltered and keep their relative positions within the different sections.", + ) + + parser.add_argument( + "--only-modified", + "--om", + dest="only_modified", + action="store_true", + help="Suppresses verbose output for non-modified files.", + ) + + parser.add_argument( + "--combine-straight-imports", + "--csi", + dest="combine_straight_imports", + action="store_true", + help="Combines all the bare straight imports of the same section in a single line. " + "Won't work with sections which have 'as' imports", + ) + # deprecated options parser.add_argument( "--recursive", @@ -759,23 +785,6 @@ def _build_arg_parser() -> argparse.ArgumentParser: help=argparse.SUPPRESS, ) - parser.add_argument( - "--only-sections", - "--os", - dest="only_sections", - action="store_true", - help="Causes imports to be sorted only based on their sections like STDLIB,THIRDPARTY etc. " - "Imports are unaltered and keep their relative positions within the different sections.", - ) - - parser.add_argument( - "--only-modified", - "--om", - dest="only_modified", - action="store_true", - help="Suppresses verbose output for non-modified files.", - ) - return parser From 23897bf091e444c8556fcdec6f77db93653c2b3a Mon Sep 17 00:00:00 2001 From: anirudnits Date: Sat, 10 Oct 2020 20:29:52 +0530 Subject: [PATCH 1172/1439] Added code to combine all bare straight imports in a single line when flag is set --- isort/output.py | 84 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 60 insertions(+), 24 deletions(-) diff --git a/isort/output.py b/isort/output.py index d2633ffdd..c6c92d962 100644 --- a/isort/output.py +++ b/isort/output.py @@ -513,35 +513,71 @@ def _with_straight_imports( import_type: str, ) -> List[str]: output: List[str] = [] - for module in straight_modules: - if module in remove_imports: - continue - import_definition = [] - if module in parsed.as_map["straight"]: - if parsed.imports[section]["straight"][module]: - import_definition.append(f"{import_type} {module}") - import_definition.extend( - f"{import_type} {module} as {as_import}" - for as_import in parsed.as_map["straight"][module] + as_imports = any([module in parsed.as_map["straight"] for module in straight_modules]) + + # combine_straight_imports only works for bare imports, 'as' imports not included + if config.combine_straight_imports and not as_imports: + if not straight_modules: + return [] + + above_comments: List[str] = [] + inline_comments: List[str] = [] + + for module in straight_modules: + if module in parsed.categorized_comments["above"]["straight"]: + above_comments.extend(parsed.categorized_comments["above"]["straight"].pop(module)) + + for module in parsed.categorized_comments["straight"]: + inline_comments.extend(parsed.categorized_comments["straight"][module]) + + combined_straight_imports = " ".join(straight_modules) + if inline_comments: + combined_inline_comments = " ".join(inline_comments) + else: + combined_inline_comments = "" + + output.extend(above_comments) + + if combined_inline_comments: + output.append( + f"{import_type} {combined_straight_imports} # {combined_inline_comments}" ) else: - import_definition.append(f"{import_type} {module}") - - comments_above = parsed.categorized_comments["above"]["straight"].pop(module, None) - if comments_above: - output.extend(comments_above) - output.extend( - with_comments( - parsed.categorized_comments["straight"].get(module), - idef, - removed=config.ignore_comments, - comment_prefix=config.comment_prefix, + output.append(f"{import_type} {combined_straight_imports}") + + return output + + else: + for module in straight_modules: + if module in remove_imports: + continue + + import_definition = [] + if module in parsed.as_map["straight"]: + if parsed.imports[section]["straight"][module]: + import_definition.append(f"{import_type} {module}") + import_definition.extend( + f"{import_type} {module} as {as_import}" + for as_import in parsed.as_map["straight"][module] + ) + else: + import_definition.append(f"{import_type} {module}") + + comments_above = parsed.categorized_comments["above"]["straight"].pop(module, None) + if comments_above: + output.extend(comments_above) + output.extend( + with_comments( + parsed.categorized_comments["straight"].get(module), + idef, + removed=config.ignore_comments, + comment_prefix=config.comment_prefix, + ) + for idef in import_definition ) - for idef in import_definition - ) - return output + return output def _output_as_string(lines: List[str], line_separator: str) -> str: From e6ec382eed4432ef2ab43bd5964c83dc45ac615d Mon Sep 17 00:00:00 2001 From: anirudnits Date: Sat, 10 Oct 2020 20:30:40 +0530 Subject: [PATCH 1173/1439] Added tests for combine_straight_imports option --- tests/unit/test_isort.py | 20 ++++++++++++++++++++ tests/unit/test_main.py | 21 +++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/tests/unit/test_isort.py b/tests/unit/test_isort.py index c07d655d7..703f8dab4 100644 --- a/tests/unit/test_isort.py +++ b/tests/unit/test_isort.py @@ -4893,3 +4893,23 @@ def test_only_sections() -> None: test_input = "from foo import b, a, c\n" assert isort.code(test_input, only_sections=True) == test_input + + +def test_combine_straight_imports() -> None: + """ Tests to ensure that combine_straight_imports works correctly """ + + test_input = ( + "import os\n" "import sys\n" "# this is a comment\n" "import math # inline comment\n" + ) + + assert isort.code(test_input, combine_straight_imports=True) == ( + "# this is a comment\n" "import math os sys # inline comment\n" + ) + + # test to ensure that combine_straight_import works with only_sections + + test_input = "import sys\n" "import a\n" "import math\n" "import os\n" "import b\n" + + assert isort.code(test_input, combine_straight_imports=True, only_sections=True) == ( + "import sys math os\n" "\n" "import a b\n" + ) diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 59e224aed..0fa4cd765 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -77,6 +77,8 @@ def test_parse_args(): assert main.parse_args(["--os"]) == {"only_sections": True} assert main.parse_args(["--om"]) == {"only_modified": True} assert main.parse_args(["--only-modified"]) == {"only_modified": True} + assert main.parse_args(["--csi"]) == {"combine_straight_imports": True} + assert main.parse_args(["--combine-straight-imports"]) == {"combine_straight_imports": True} def test_ascii_art(capsys): @@ -762,6 +764,25 @@ def test_isort_with_stdin(capsys): assert "else-type place_module for a returned THIRDPARTY" not in out assert "else-type place_module for b returned THIRDPARTY" not in out + # ensures that combine-straight-imports flag works with stdin + input_content = UnseekableTextIOWrapper( + BytesIO( + b""" +import a +import b +""" + ) + ) + + main.main(["-", "--combine-straight-imports"], stdin=input_content) + out, error = capsys.readouterr() + + assert out == ( + """ +import a b +""" + ) + def test_unsupported_encodings(tmpdir, capsys): tmp_file = tmpdir.join("file.py") From 4c54a63929579da70d2a2713319aa673571ecd7a Mon Sep 17 00:00:00 2001 From: anirudnits Date: Sat, 10 Oct 2020 20:48:33 +0530 Subject: [PATCH 1174/1439] corrected an error with joining imports in one line --- isort/output.py | 2 +- tests/unit/test_isort.py | 6 +++--- tests/unit/test_main.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/isort/output.py b/isort/output.py index c6c92d962..f7ff50ce1 100644 --- a/isort/output.py +++ b/isort/output.py @@ -531,7 +531,7 @@ def _with_straight_imports( for module in parsed.categorized_comments["straight"]: inline_comments.extend(parsed.categorized_comments["straight"][module]) - combined_straight_imports = " ".join(straight_modules) + combined_straight_imports = ", ".join(straight_modules) if inline_comments: combined_inline_comments = " ".join(inline_comments) else: diff --git a/tests/unit/test_isort.py b/tests/unit/test_isort.py index 703f8dab4..01fba9a77 100644 --- a/tests/unit/test_isort.py +++ b/tests/unit/test_isort.py @@ -4903,13 +4903,13 @@ def test_combine_straight_imports() -> None: ) assert isort.code(test_input, combine_straight_imports=True) == ( - "# this is a comment\n" "import math os sys # inline comment\n" + "# this is a comment\n" "import math, os, sys # inline comment\n" ) # test to ensure that combine_straight_import works with only_sections - test_input = "import sys\n" "import a\n" "import math\n" "import os\n" "import b\n" + test_input = "import sys, os\n" "import a\n" "import math\n" "import b\n" assert isort.code(test_input, combine_straight_imports=True, only_sections=True) == ( - "import sys math os\n" "\n" "import a b\n" + "import sys, os, math\n" "\n" "import a, b\n" ) diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 0fa4cd765..b2035f2cc 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -779,7 +779,7 @@ def test_isort_with_stdin(capsys): assert out == ( """ -import a b +import a, b """ ) From b66ed3862e1b882da1c18247356cfa832cfefbe9 Mon Sep 17 00:00:00 2001 From: anirudnits Date: Sat, 10 Oct 2020 21:16:05 +0530 Subject: [PATCH 1175/1439] made some changes to pass deepsource check --- isort/output.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/isort/output.py b/isort/output.py index f7ff50ce1..5a41896d2 100644 --- a/isort/output.py +++ b/isort/output.py @@ -512,15 +512,15 @@ def _with_straight_imports( remove_imports: List[str], import_type: str, ) -> List[str]: + if not straight_modules: + return [] + output: List[str] = [] - as_imports = any([module in parsed.as_map["straight"] for module in straight_modules]) + as_imports = any((module in parsed.as_map["straight"] for module in straight_modules)) # combine_straight_imports only works for bare imports, 'as' imports not included if config.combine_straight_imports and not as_imports: - if not straight_modules: - return [] - above_comments: List[str] = [] inline_comments: List[str] = [] From f77f059e2cf7a0d5fb8876ead8e392d448543dc0 Mon Sep 17 00:00:00 2001 From: anirudnits Date: Sat, 10 Oct 2020 21:23:21 +0530 Subject: [PATCH 1176/1439] made changes to pass deepsource check --- isort/output.py | 57 ++++++++++++++++++++++++------------------------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/isort/output.py b/isort/output.py index 5a41896d2..f4b30304b 100644 --- a/isort/output.py +++ b/isort/output.py @@ -512,15 +512,15 @@ def _with_straight_imports( remove_imports: List[str], import_type: str, ) -> List[str]: - if not straight_modules: - return [] - output: List[str] = [] as_imports = any((module in parsed.as_map["straight"] for module in straight_modules)) # combine_straight_imports only works for bare imports, 'as' imports not included if config.combine_straight_imports and not as_imports: + if not straight_modules: + return [] + above_comments: List[str] = [] inline_comments: List[str] = [] @@ -548,36 +548,35 @@ def _with_straight_imports( return output - else: - for module in straight_modules: - if module in remove_imports: - continue + for module in straight_modules: + if module in remove_imports: + continue - import_definition = [] - if module in parsed.as_map["straight"]: - if parsed.imports[section]["straight"][module]: - import_definition.append(f"{import_type} {module}") - import_definition.extend( - f"{import_type} {module} as {as_import}" - for as_import in parsed.as_map["straight"][module] - ) - else: + import_definition = [] + if module in parsed.as_map["straight"]: + if parsed.imports[section]["straight"][module]: import_definition.append(f"{import_type} {module}") - - comments_above = parsed.categorized_comments["above"]["straight"].pop(module, None) - if comments_above: - output.extend(comments_above) - output.extend( - with_comments( - parsed.categorized_comments["straight"].get(module), - idef, - removed=config.ignore_comments, - comment_prefix=config.comment_prefix, - ) - for idef in import_definition + import_definition.extend( + f"{import_type} {module} as {as_import}" + for as_import in parsed.as_map["straight"][module] ) + else: + import_definition.append(f"{import_type} {module}") + + comments_above = parsed.categorized_comments["above"]["straight"].pop(module, None) + if comments_above: + output.extend(comments_above) + output.extend( + with_comments( + parsed.categorized_comments["straight"].get(module), + idef, + removed=config.ignore_comments, + comment_prefix=config.comment_prefix, + ) + for idef in import_definition + ) - return output + return output def _output_as_string(lines: List[str], line_separator: str) -> str: From d929358766df52590df03b35fe515e6a369c1591 Mon Sep 17 00:00:00 2001 From: Louis Sautier Date: Sat, 10 Oct 2020 19:11:20 +0200 Subject: [PATCH 1177/1439] Prevent installation of the "tests" package Tests should not be listed under packages, otherwise setup.py attempts to install a "tests" package, causing potential clashes with other packages. --- pyproject.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 81d45cc27..5f8fdb0e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,9 @@ classifiers = [ urls = { Changelog = "https://github.com/pycqa/isort/blob/master/CHANGELOG.md" } packages = [ { include = "isort" }, - { include = "tests", format = "sdist" }, +] +include = [ + { path = "tests", format = "sdist" }, ] [tool.poetry.dependencies] From 66d6345171b398c9e025904789d121e78dcad79a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 10 Oct 2020 23:53:16 -0700 Subject: [PATCH 1178/1439] Specify minimum build system --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5f8fdb0e7..158c92fe6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -101,5 +101,5 @@ logo = "art/logo.png" palette = {scheme = "isort"} [build-system] -requires = ["poetry>=0.12"] -build-backend = "poetry.masonry.api" +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" From b662f440ac859fb6b5fcfacea6cb94052a873388 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 11 Oct 2020 00:07:04 -0700 Subject: [PATCH 1179/1439] Add James Curtin (@jamescurtin) to acknowledgements --- docs/contributing/4.-acknowledgements.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index 9f0ccae89..73f2b3f76 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -200,6 +200,7 @@ Code Contributors - Andrew Howe (@howeaj) - Sang-Heon Jeon (@lntuition) - Denis Veselov (@saippuakauppias) +- James Curtin (@jamescurtin) Documenters =================== From 383f323e44da4065a069c4817817e56bcfa7d546 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 11 Oct 2020 00:09:06 -0700 Subject: [PATCH 1180/1439] Update changelog to include source distribution improvement --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b090d416..c8231bd02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. Find out more about isort's release policy [here](https://pycqa.github.io/isort/docs/major_releases/release_policy/). +### 5.6.3 October 11, 2020 + - Improved packaging of test files alongside source distribution (see: https://github.com/PyCQA/isort/pull/1555). + ### 5.6.2 October 10, 2020 - Fixed #1548: On rare occasions an unecessary empty line can be added when an import is marked as skipped. - Fixed #1542: Bug in VERTICAL_PREFIX_FROM_MODULE_IMPORT wrap mode. From 491b3461d572249de71a7b2572af281322d4f40b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 11 Oct 2020 00:09:39 -0700 Subject: [PATCH 1181/1439] Bump version to 5.6.3 --- isort/_version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/_version.py b/isort/_version.py index 8d9ad75cd..06497b9f5 100644 --- a/isort/_version.py +++ b/isort/_version.py @@ -1 +1 @@ -__version__ = "5.6.2" +__version__ = "5.6.3" diff --git a/pyproject.toml b/pyproject.toml index b83a81082..a91cea2ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ line-length = 100 [tool.poetry] name = "isort" -version = "5.6.2" +version = "5.6.3" description = "A Python utility / library to sort Python imports." authors = ["Timothy Crosley "] license = "MIT" From 586cfe1951cd7be8976a8beda0803910ac2cdd3e Mon Sep 17 00:00:00 2001 From: Marco Gorelli Date: Sun, 11 Oct 2020 15:07:23 +0100 Subject: [PATCH 1182/1439] update pre-commit example in documentation --- .pre-commit-hooks.yaml | 2 +- docs/upgrade_guides/5.0.0.md | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 6bfb2bba9..0027e49df 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -3,5 +3,5 @@ entry: isort require_serial: true language: python - types: [python, cython, pyi] + types: [python] args: ['--filter-files'] diff --git a/docs/upgrade_guides/5.0.0.md b/docs/upgrade_guides/5.0.0.md index fb767c2c4..6aeb0f65a 100644 --- a/docs/upgrade_guides/5.0.0.md +++ b/docs/upgrade_guides/5.0.0.md @@ -82,9 +82,17 @@ isort now includes an optimized precommit configuration in the repo itself. To u ``` - repo: https://github.com/pycqa/isort - rev: 5.3.2 + rev: 5.6.3 hooks: - id: isort + name: isort (python) + types: [python] + - id: isort + name: isort (cython) + types: [cython] + - id: isort + name: isort (pyi) + types: [pyi] ``` under the `repos` section of your projects `.pre-commit-config.yaml` config. From 1622543078eae774118fa163efb7bab26d3963b8 Mon Sep 17 00:00:00 2001 From: Marco Gorelli Date: Sun, 11 Oct 2020 15:30:26 +0100 Subject: [PATCH 1183/1439] remove unnecessary types=[python] --- docs/upgrade_guides/5.0.0.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/upgrade_guides/5.0.0.md b/docs/upgrade_guides/5.0.0.md index 6aeb0f65a..c5e806069 100644 --- a/docs/upgrade_guides/5.0.0.md +++ b/docs/upgrade_guides/5.0.0.md @@ -86,7 +86,6 @@ isort now includes an optimized precommit configuration in the repo itself. To u hooks: - id: isort name: isort (python) - types: [python] - id: isort name: isort (cython) types: [cython] From 49bb9babe3d2fb268b95d4170d5fc1dc267c213d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 12 Oct 2020 21:35:06 -0700 Subject: [PATCH 1184/1439] Fixed #1556: Empty line added between imports that should be skipped. --- CHANGELOG.md | 3 +++ isort/parse.py | 34 ++++++++++++++++++++++++++-------- tests/unit/test_regressions.py | 27 +++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b090d416..37d0ebc38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. Find out more about isort's release policy [here](https://pycqa.github.io/isort/docs/major_releases/release_policy/). +### 5.6.3 October TBD, 2020 + - Fixed #1556: Empty line added between imports that should be skipped. + ### 5.6.2 October 10, 2020 - Fixed #1548: On rare occasions an unecessary empty line can be added when an import is marked as skipped. - Fixed #1542: Bug in VERTICAL_PREFIX_FROM_MODULE_IMPORT wrap mode. diff --git a/isort/parse.py b/isort/parse.py index 96468f095..2c152651a 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -218,17 +218,35 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte import_index = index - 1 while import_index and not in_lines[import_index - 1]: import_index -= 1 - elif "isort:skip" in line or "isort: skip" in line: - commentless = line.split("#", 1)[0] + else: + commentless = line.split("#", 1)[0].strip() if ( - "(" in commentless - and not commentless.rstrip().endswith(")") - and import_index < line_count + ("isort:skip" in line + or "isort: skip" in line) + and "(" in commentless + and ")" not in commentless ): import_index = index - while import_index < line_count and not commentless.rstrip().endswith(")"): - commentless = in_lines[import_index].split("#", 1)[0] - import_index += 1 + + starting_line = line + while "isort:skip" in starting_line or "isort: skip" in starting_line: + commentless = starting_line.split("#", 1)[0] + if ( + "(" in commentless + and not commentless.rstrip().endswith(")") + and import_index < line_count + ): + + while import_index < line_count and not commentless.rstrip().endswith(")"): + commentless = in_lines[import_index].split("#", 1)[0] + import_index += 1 + else: + import_index += 1 + + if import_index >= line_count: + break + else: + starting_line = in_lines[import_index] line, *end_of_line_comment = line.split("#", 1) if ";" in line: diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index 873cc2f72..c25ed19c7 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -1424,6 +1424,33 @@ def test_isort_shouldnt_split_skip_issue_1548(): ) +def test_isort_shouldnt_split_skip_issue_1556(): + assert isort.check_code( + """ +from tools.dependency_pruning.prune_dependencies import ( # isort:skip + prune_dependencies, +) +from tools.developer_pruning.prune_developers import ( # isort:skip + prune_developers, +) +""", + show_diff=True, + profile="black", + float_to_top=True, + ) + assert isort.check_code( + """ +from tools.dependency_pruning.prune_dependencies import ( # isort:skip + prune_dependencies, +) +from tools.developer_pruning.prune_developers import x # isort:skip +""", + show_diff=True, + profile="black", + float_to_top=True, + ) + + def test_isort_losing_imports_vertical_prefix_from_module_import_wrap_mode_issue_1542(): """Ensure isort doesnt lose imports when a comment is combined with an import and wrap mode VERTICAL_PREFIX_FROM_MODULE_IMPORT is used. From b4878742ff3f6c9b3e46c329ddbdbdeda8553d9e Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 12 Oct 2020 23:09:17 -0700 Subject: [PATCH 1185/1439] formatting fix --- isort/parse.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/isort/parse.py b/isort/parse.py index 2c152651a..fe3dd157a 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -221,8 +221,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte else: commentless = line.split("#", 1)[0].strip() if ( - ("isort:skip" in line - or "isort: skip" in line) + ("isort:skip" in line or "isort: skip" in line) and "(" in commentless and ")" not in commentless ): @@ -237,7 +236,9 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte and import_index < line_count ): - while import_index < line_count and not commentless.rstrip().endswith(")"): + while import_index < line_count and not commentless.rstrip().endswith( + ")" + ): commentless = in_lines[import_index].split("#", 1)[0] import_index += 1 else: From 6bb47b7acc1554ecb59d2855e9110c447162f674 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 12 Oct 2020 23:21:39 -0700 Subject: [PATCH 1186/1439] Bump version to 5.6.4 --- CHANGELOG.md | 2 +- isort/_version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e57cc61d8..a94901a0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. Find out more about isort's release policy [here](https://pycqa.github.io/isort/docs/major_releases/release_policy/). -### 5.6.4 October TBD, 2020 +### 5.6.4 October 12, 2020 - Fixed #1556: Empty line added between imports that should be skipped. ### 5.6.3 October 11, 2020 diff --git a/isort/_version.py b/isort/_version.py index 06497b9f5..f0b716b28 100644 --- a/isort/_version.py +++ b/isort/_version.py @@ -1 +1 @@ -__version__ = "5.6.3" +__version__ = "5.6.4" diff --git a/pyproject.toml b/pyproject.toml index a91cea2ab..7047b0add 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ line-length = 100 [tool.poetry] name = "isort" -version = "5.6.3" +version = "5.6.4" description = "A Python utility / library to sort Python imports." authors = ["Timothy Crosley "] license = "MIT" From 1682a3385ec91563bc7e0b70ca32b80ff23b0832 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 12 Oct 2020 23:39:19 -0700 Subject: [PATCH 1187/1439] Add Marco Gorelli (@MarcoGorelli) and Louis Sautier (@sbraz) to contributors list --- docs/contributing/4.-acknowledgements.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index 73f2b3f76..9c04e3f8c 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -201,6 +201,8 @@ Code Contributors - Sang-Heon Jeon (@lntuition) - Denis Veselov (@saippuakauppias) - James Curtin (@jamescurtin) +- Marco Gorelli (@MarcoGorelli) +- Louis Sautier (@sbraz) Documenters =================== From d2c1b34b8910b78ccece23f86bf9900b4f38fc02 Mon Sep 17 00:00:00 2001 From: Bhupesh Varshney Date: Tue, 13 Oct 2020 13:50:53 +0000 Subject: [PATCH 1188/1439] add compatibility docs with black --- docs/configuration/compatibility_black.md | 57 +++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 docs/configuration/compatibility_black.md diff --git a/docs/configuration/compatibility_black.md b/docs/configuration/compatibility_black.md new file mode 100644 index 000000000..d731bf23b --- /dev/null +++ b/docs/configuration/compatibility_black.md @@ -0,0 +1,57 @@ +Compatibility with black +======== + +black and isort sometimes don't agree on some rules. Although you can configure isort to behave nicely with black. + + +#Basic compatibility + +Use the profile option while using isort, `isort --profile black`. + +A demo of how this would look like in your _.travis.yml_ + +```yaml +language: python +python: + - "3.6" + - "3.7" + - "3.8" + +install: + - pip install -r requirements-dev.txt + - pip install isort black + - pip install coveralls +script: + - pytest my-package + - isort --profile black my-package + - black --check --diff my-package +after_success: + - coveralls + +``` + +See [built-in profiles](https://pycqa.github.io/isort/docs/configuration/profiles/) for more profiles. + +#Integration with pre-commit + +isort can be easily used with your pre-commit hooks. + +```yaml +- repo: https://github.com/pycqa/isort + rev: 5.6.4 + hooks: + - id: isort + args: ["--profile", "black"] +``` + +#Using a config file (.isort.cfg) + +The easiest way to configure black with isort is to use a config file. + +```ini +[tool.isort] +profile = "black" +multi_line_output = 3 +``` + +Read More about supported [config files](https://pycqa.github.io/isort/docs/configuration/config_files/). \ No newline at end of file From fdfd55ad9027954ecb640a41b5f2fa6490c10237 Mon Sep 17 00:00:00 2001 From: Bhupesh Varshney Date: Tue, 13 Oct 2020 13:56:20 +0000 Subject: [PATCH 1189/1439] fix headings --- docs/configuration/compatibility_black.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/configuration/compatibility_black.md b/docs/configuration/compatibility_black.md index d731bf23b..4afa2459e 100644 --- a/docs/configuration/compatibility_black.md +++ b/docs/configuration/compatibility_black.md @@ -4,7 +4,7 @@ Compatibility with black black and isort sometimes don't agree on some rules. Although you can configure isort to behave nicely with black. -#Basic compatibility +## Basic compatibility Use the profile option while using isort, `isort --profile black`. @@ -32,7 +32,7 @@ after_success: See [built-in profiles](https://pycqa.github.io/isort/docs/configuration/profiles/) for more profiles. -#Integration with pre-commit +## Integration with pre-commit isort can be easily used with your pre-commit hooks. @@ -44,7 +44,7 @@ isort can be easily used with your pre-commit hooks. args: ["--profile", "black"] ``` -#Using a config file (.isort.cfg) +## Using a config file (.isort.cfg) The easiest way to configure black with isort is to use a config file. From 78771148995f3a1d3a12f0177d5ab153dd6de3f0 Mon Sep 17 00:00:00 2001 From: Timur Kushukov Date: Thu, 8 Oct 2020 00:04:50 +0500 Subject: [PATCH 1190/1439] get imports command --- isort/__init__.py | 9 ++++++- isort/api.py | 56 ++++++++++++++++++++++++++++++++++++++++ isort/core.py | 12 +++++++++ tests/unit/test_isort.py | 41 +++++++++++++++++++++++++++++ 4 files changed, 117 insertions(+), 1 deletion(-) diff --git a/isort/__init__.py b/isort/__init__.py index 236255dd8..6f2c2c833 100644 --- a/isort/__init__.py +++ b/isort/__init__.py @@ -2,7 +2,14 @@ from . import settings from ._version import __version__ from .api import check_code_string as check_code -from .api import check_file, check_stream, place_module, place_module_with_reason +from .api import ( + check_file, + check_stream, + get_imports_stream, + get_imports_string, + place_module, + place_module_with_reason, +) from .api import sort_code_string as code from .api import sort_file as file from .api import sort_stream as stream diff --git a/isort/api.py b/isort/api.py index 5a2df6af3..aed041ce4 100644 --- a/isort/api.py +++ b/isort/api.py @@ -366,6 +366,62 @@ def sort_file( return changed +def get_imports_string( + code: str, + extension: Optional[str] = None, + config: Config = DEFAULT_CONFIG, + file_path: Optional[Path] = None, + **config_kwargs, +) -> str: + """Finds all imports within the provided code string, returning a new string with them. + + - **code**: The string of code with imports that need to be sorted. + - **extension**: The file extension that contains imports. Defaults to filename extension or py. + - **config**: The config object to use when sorting imports. + - **file_path**: The disk location where the code string was pulled from. + - ****config_kwargs**: Any config modifications. + """ + input_stream = StringIO(code) + output_stream = StringIO() + config = _config(path=file_path, config=config, **config_kwargs) + get_imports_stream( + input_stream, + output_stream, + extension=extension, + config=config, + file_path=file_path, + ) + output_stream.seek(0) + return output_stream.read() + + +def get_imports_stream( + input_stream: TextIO, + output_stream: TextIO, + extension: Optional[str] = None, + config: Config = DEFAULT_CONFIG, + file_path: Optional[Path] = None, + **config_kwargs, +) -> None: + """Finds all imports within the provided code stream, outputs to the provided output stream. + + - **input_stream**: The stream of code with imports that need to be sorted. + - **output_stream**: The stream where sorted imports should be written to. + - **extension**: The file extension that contains imports. Defaults to filename extension or py. + - **config**: The config object to use when sorting imports. + - **file_path**: The disk location where the code string was pulled from. + - ****config_kwargs**: Any config modifications. + """ + config = _config(path=file_path, config=config, **config_kwargs) + core.process( + input_stream, + output_stream, + extension=extension or (file_path and file_path.suffix.lstrip(".")) or "py", + config=config, + imports_only=True, + ) + + def _config( path: Optional[Path] = None, config: Config = DEFAULT_CONFIG, **config_kwargs ) -> Config: diff --git a/isort/core.py b/isort/core.py index 292bdc1c2..3668ae9e8 100644 --- a/isort/core.py +++ b/isort/core.py @@ -30,6 +30,7 @@ def process( output_stream: TextIO, extension: str = "py", config: Config = DEFAULT_CONFIG, + imports_only: bool = False, ) -> bool: """Parses stream identifying sections of contiguous imports and sorting them @@ -68,6 +69,7 @@ def process( stripped_line: str = "" end_of_file: bool = False verbose_output: List[str] = [] + all_imports: List[str] = [] if config.float_to_top: new_input = "" @@ -331,6 +333,11 @@ def process( parsed_content = parse.file_contents(import_section, config=config) verbose_output += parsed_content.verbose_output + all_imports.extend( + li + for li in parsed_content.in_lines + if li and li not in set(parsed_content.lines_without_imports) + ) sorted_import_section = output.sorted_imports( parsed_content, @@ -395,6 +402,11 @@ def process( for output_str in verbose_output: print(output_str) + if imports_only: + output_stream.seek(0) + output_stream.truncate(0) + output_stream.write(line_separator.join(all_imports) + line_separator) + return made_changes diff --git a/tests/unit/test_isort.py b/tests/unit/test_isort.py index 01fba9a77..f28c5de63 100644 --- a/tests/unit/test_isort.py +++ b/tests/unit/test_isort.py @@ -4913,3 +4913,44 @@ def test_combine_straight_imports() -> None: assert isort.code(test_input, combine_straight_imports=True, only_sections=True) == ( "import sys, os, math\n" "\n" "import a, b\n" ) + + +def test_get_imports_string() -> None: + test_input = ( + "import first_straight\n" + "\n" + "import second_straight\n" + "from first_from import first_from_function_1, first_from_function_2\n" + "import bad_name as good_name\n" + "from parent.some_bad_defs import bad_name_1 as ok_name_1, bad_name_2 as ok_name_2\n" + "\n" + "# isort: list\n" + "__all__ = ['b', 'c', 'a']\n" + "\n" + "def bla():\n" + " import needed_in_bla_2\n" + "\n" + "\n" + " import needed_in_bla\n" + " pass" + "\n" + "def bla_bla():\n" + " import needed_in_bla_bla\n" + "\n" + " #import not_really_an_import\n" + " pass" + "\n" + "import needed_in_end\n" + ) + result = api.get_imports_string(test_input) + assert result == ( + "import first_straight\n" + "import second_straight\n" + "from first_from import first_from_function_1, first_from_function_2\n" + "import bad_name as good_name\n" + "from parent.some_bad_defs import bad_name_1 as ok_name_1, bad_name_2 as ok_name_2\n" + "import needed_in_bla_2\n" + "import needed_in_bla\n" + "import needed_in_bla_bla\n" + "import needed_in_end\n" + ) From f9f5cc9314cc1be1e2f9c2b0270580c1351d8fe1 Mon Sep 17 00:00:00 2001 From: Tamas Szabo Date: Wed, 14 Oct 2020 10:15:34 +0300 Subject: [PATCH 1191/1439] Improves test coverage of settings.py. Branch coverage -> 100%. --- tests/unit/test_settings.py | 55 +++++++++++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/tests/unit/test_settings.py b/tests/unit/test_settings.py index 462b667d0..be8b5020b 100644 --- a/tests/unit/test_settings.py +++ b/tests/unit/test_settings.py @@ -86,12 +86,27 @@ def test_is_supported_filetype_fifo(self, tmpdir): os.mkfifo(fifo_file) assert not self.instance.is_supported_filetype(fifo_file) + def test_src_paths_are_combined_and_deduplicated(self): + src_paths = ["src", "tests"] + src_full_paths = (Path(os.getcwd()) / f for f in src_paths) + assert Config(src_paths=src_paths * 2).src_paths == tuple(src_full_paths) + def test_as_list(): assert settings._as_list([" one "]) == ["one"] assert settings._as_list("one,two") == ["one", "two"] +def _write_simple_settings(tmp_file): + tmp_file.write_text( + """ +[isort] +force_grid_wrap=true +""", + "utf8", + ) + + def test_find_config(tmpdir): tmp_config = tmpdir.join(".isort.cfg") @@ -116,31 +131,46 @@ def test_find_config(tmpdir): # can when it has either a file format, or generic relevant section settings._find_config.cache_clear() settings._get_config_data.cache_clear() - tmp_config.write_text( - """ -[isort] -force_grid_wrap=true -""", - "utf8", - ) + _write_simple_settings(tmp_config) assert settings._find_config(str(tmpdir))[1] +def test_find_config_deep(tmpdir): + # can't find config if it is further up than MAX_CONFIG_SEARCH_DEPTH + dirs = [f"dir{i}" for i in range(settings.MAX_CONFIG_SEARCH_DEPTH + 1)] + tmp_dirs = tmpdir.ensure(*dirs, dirs=True) + tmp_config = tmpdir.join("dir0", ".isort.cfg") + settings._find_config.cache_clear() + settings._get_config_data.cache_clear() + _write_simple_settings(tmp_config) + assert not settings._find_config(str(tmp_dirs))[1] + # but can find config if it is MAX_CONFIG_SEARCH_DEPTH up + one_parent_up = os.path.split(str(tmp_dirs))[0] + assert settings._find_config(one_parent_up)[1] + + def test_get_config_data(tmpdir): test_config = tmpdir.join("test_config.editorconfig") test_config.write_text( """ root = true -[*.py] +[*.{js,py}] indent_style=tab indent_size=tab + +[*.py] force_grid_wrap=false comment_prefix="text" + +[*.{java}] +indent_style = space """, "utf8", ) - loaded_settings = settings._get_config_data(str(test_config), sections=("*.py",)) + loaded_settings = settings._get_config_data( + str(test_config), sections=settings.CONFIG_SECTIONS[".editorconfig"] + ) assert loaded_settings assert loaded_settings["comment_prefix"] == "text" assert loaded_settings["force_grid_wrap"] == 0 @@ -148,6 +178,13 @@ def test_get_config_data(tmpdir): assert str(tmpdir) in loaded_settings["source"] +def test_editorconfig_without_sections(tmpdir): + test_config = tmpdir.join("test_config.editorconfig") + test_config.write_text("\nroot = true\n", "utf8") + loaded_settings = settings._get_config_data(str(test_config), sections=("*.py",)) + assert not loaded_settings + + def test_as_bool(): assert settings._as_bool("TrUe") is True assert settings._as_bool("true") is True From 68bf16a0de7391ff5036ff1fac530f4ea359e489 Mon Sep 17 00:00:00 2001 From: Timur Kushukov Date: Wed, 14 Oct 2020 11:45:33 +0500 Subject: [PATCH 1192/1439] get imports stdout fix --- isort/core.py | 9 ++++++--- tests/unit/test_isort.py | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/isort/core.py b/isort/core.py index 3668ae9e8..c62687e5a 100644 --- a/isort/core.py +++ b/isort/core.py @@ -1,3 +1,4 @@ +import os import textwrap from io import StringIO from itertools import chain @@ -70,6 +71,9 @@ def process( end_of_file: bool = False verbose_output: List[str] = [] all_imports: List[str] = [] + if imports_only: + _output_stream = output_stream + output_stream = open(os.devnull, "wt") if config.float_to_top: new_input = "" @@ -403,9 +407,8 @@ def process( print(output_str) if imports_only: - output_stream.seek(0) - output_stream.truncate(0) - output_stream.write(line_separator.join(all_imports) + line_separator) + result = line_separator.join(all_imports) + line_separator + _output_stream.write(result) return made_changes diff --git a/tests/unit/test_isort.py b/tests/unit/test_isort.py index f28c5de63..cdec1c4e2 100644 --- a/tests/unit/test_isort.py +++ b/tests/unit/test_isort.py @@ -7,6 +7,7 @@ from pathlib import Path import subprocess import sys +from io import StringIO from tempfile import NamedTemporaryFile from typing import Any, Dict, Iterator, List, Set, Tuple @@ -4954,3 +4955,24 @@ def test_get_imports_string() -> None: "import needed_in_bla_bla\n" "import needed_in_end\n" ) + + +def test_get_imports_stdout() -> None: + """Ensure that get_imports_stream can work with nonseekable streams like STDOUT""" + + global_output = [] + + class NonSeekableTestStream(StringIO): + def seek(self, position): + raise OSError("Stream is not seekable") + + def seekable(self): + return False + + def write(self, s): + global_output.append(s) + + test_input = StringIO("import m2\n" "import m1\n" "not_import = 7") + test_output = NonSeekableTestStream() + api.get_imports_stream(test_input, test_output) + assert "".join(global_output) == "import m2\nimport m1\n" From e24c3b0b144f73319f12193155c66ef24a071d8d Mon Sep 17 00:00:00 2001 From: Timur Kushukov Date: Thu, 15 Oct 2020 12:35:06 +0500 Subject: [PATCH 1193/1439] fix devnull linter warning --- isort/core.py | 8 ++++++-- tests/unit/test_isort.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/isort/core.py b/isort/core.py index c62687e5a..badf2a04c 100644 --- a/isort/core.py +++ b/isort/core.py @@ -1,4 +1,3 @@ -import os import textwrap from io import StringIO from itertools import chain @@ -73,7 +72,12 @@ def process( all_imports: List[str] = [] if imports_only: _output_stream = output_stream - output_stream = open(os.devnull, "wt") + + class DevNull(StringIO): + def write(self, *a, **kw): + pass + + output_stream = DevNull() if config.float_to_top: new_input = "" diff --git a/tests/unit/test_isort.py b/tests/unit/test_isort.py index cdec1c4e2..aea2c74b6 100644 --- a/tests/unit/test_isort.py +++ b/tests/unit/test_isort.py @@ -4969,7 +4969,7 @@ def seek(self, position): def seekable(self): return False - def write(self, s): + def write(self, s, *a, **kw): global_output.append(s) test_input = StringIO("import m2\n" "import m1\n" "not_import = 7") From 9bdc453b13d2ae7f4d763ed15955c8471c633857 Mon Sep 17 00:00:00 2001 From: Rohan Khanna Date: Thu, 15 Oct 2020 21:37:05 +0200 Subject: [PATCH 1194/1439] Unnecessary else / elif used after break (deepsource.io PYL-R1723) --- isort/output.py | 2 +- isort/parse.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/isort/output.py b/isort/output.py index f4b30304b..cda5c24d8 100644 --- a/isort/output.py +++ b/isort/output.py @@ -184,7 +184,7 @@ def sorted_imports( continue next_construct = line break - elif in_quote: + if in_quote: next_construct = line break diff --git a/isort/parse.py b/isort/parse.py index fe3dd157a..6a999391a 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -246,8 +246,8 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte if import_index >= line_count: break - else: - starting_line = in_lines[import_index] + + starting_line = in_lines[import_index] line, *end_of_line_comment = line.split("#", 1) if ";" in line: From 579d93c40c736d87f5219c42222df8f02abf3abf Mon Sep 17 00:00:00 2001 From: Timur Kushukov Date: Fri, 16 Oct 2020 21:44:27 +0500 Subject: [PATCH 1195/1439] CLI to get imports --- isort/__init__.py | 1 + isort/api.py | 28 ++++++++++++++++++++++++++++ isort/core.py | 12 +++++++----- isort/main.py | 17 +++++++++++++++++ pyproject.toml | 1 + tests/unit/test_api.py | 7 +++++++ tests/unit/test_main.py | 12 ++++++++++++ 7 files changed, 73 insertions(+), 5 deletions(-) diff --git a/isort/__init__.py b/isort/__init__.py index 6f2c2c833..b800a03de 100644 --- a/isort/__init__.py +++ b/isort/__init__.py @@ -5,6 +5,7 @@ from .api import ( check_file, check_stream, + get_imports_file, get_imports_stream, get_imports_string, place_module, diff --git a/isort/api.py b/isort/api.py index aed041ce4..c6a824124 100644 --- a/isort/api.py +++ b/isort/api.py @@ -422,6 +422,34 @@ def get_imports_stream( ) +def get_imports_file( + filename: Union[str, Path], + output_stream: TextIO, + extension: Optional[str] = None, + config: Config = DEFAULT_CONFIG, + file_path: Optional[Path] = None, + **config_kwargs, +) -> None: + """Finds all imports within the provided file, outputs to the provided output stream. + + - **filename**: The name or Path of the file to check. + - **output_stream**: The stream where sorted imports should be written to. + - **extension**: The file extension that contains imports. Defaults to filename extension or py. + - **config**: The config object to use when sorting imports. + - **file_path**: The disk location where the code string was pulled from. + - ****config_kwargs**: Any config modifications. + """ + with io.File.read(filename) as source_file: + get_imports_stream( + source_file.stream, + output_stream, + extension, + config, + file_path, + **config_kwargs, + ) + + def _config( path: Optional[Path] = None, config: Config = DEFAULT_CONFIG, **config_kwargs ) -> Config: diff --git a/isort/core.py b/isort/core.py index badf2a04c..72b10c7c6 100644 --- a/isort/core.py +++ b/isort/core.py @@ -341,11 +341,13 @@ def write(self, *a, **kw): parsed_content = parse.file_contents(import_section, config=config) verbose_output += parsed_content.verbose_output - all_imports.extend( - li - for li in parsed_content.in_lines - if li and li not in set(parsed_content.lines_without_imports) - ) + if imports_only: + lines_without_imports_set = set(parsed_content.lines_without_imports) + all_imports.extend( + li + for li in parsed_content.in_lines + if li and li not in lines_without_imports_set + ) sorted_import_section = output.sorted_imports( parsed_content, diff --git a/isort/main.py b/isort/main.py index d2cd76b00..5cf96ba73 100644 --- a/isort/main.py +++ b/isort/main.py @@ -826,6 +826,23 @@ def _preconvert(item): raise TypeError("Unserializable object {} of type {}".format(item, type(item))) +def identify_imports_main(argv: Optional[Sequence[str]] = None) -> None: + parser = argparse.ArgumentParser( + description="Get all import definitions from a given file." + "Use `-` as the first argument to represent stdin." + ) + parser.add_argument("file", help="Python source file to get imports from.") + arguments = parser.parse_args(argv) + + file_name = arguments.file + if file_name == "-": + api.get_imports_stream(sys.stdin, sys.stdout) + else: + if os.path.isdir(file_name): + sys.exit("Path must be a file, not a directory") + api.get_imports_file(file_name, sys.stdout) + + def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = None) -> None: arguments = parse_args(argv) if arguments.get("show_version"): diff --git a/pyproject.toml b/pyproject.toml index 7047b0add..840c64916 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,6 +83,7 @@ example_isort_formatting_plugin = "^0.0.2" [tool.poetry.scripts] isort = "isort.main:main" +isort-identify-imports = "isort.main:identify_imports_main" [tool.poetry.plugins."distutils.commands"] isort = "isort.main:ISortCommand" diff --git a/tests/unit/test_api.py b/tests/unit/test_api.py index 3d257e705..fc29c33f5 100644 --- a/tests/unit/test_api.py +++ b/tests/unit/test_api.py @@ -1,5 +1,6 @@ """Tests the isort API module""" import os +import sys from io import StringIO from unittest.mock import MagicMock, patch @@ -81,3 +82,9 @@ def test_diff_stream() -> None: def test_sort_code_string_mixed_newlines(): assert api.sort_code_string("import A\n\r\nimportA\n\n") == "import A\r\n\r\nimportA\r\n\n" + + +def test_get_import_file(imperfect, capsys): + api.get_imports_file(imperfect, sys.stdout) + out, _ = capsys.readouterr() + assert out == imperfect_content diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index b2035f2cc..93ed94e86 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -914,3 +914,15 @@ def test_only_modified_flag(tmpdir, capsys): assert "else-type place_module for os returned STDLIB" in out assert "else-type place_module for math returned STDLIB" not in out assert "else-type place_module for pandas returned THIRDPARTY" not in out + + +def test_identify_imports_main(tmpdir, capsys): + file_content = "import mod2\n" "a = 1\n" "import mod1\n" + file_imports = "import mod2\n" "import mod1\n" + some_file = tmpdir.join("some_file.py") + some_file.write(file_content) + + main.identify_imports_main([str(some_file)]) + + out, error = capsys.readouterr() + assert out == file_imports From e737cb5b4fb385a49c562ff646197cce2e0452b7 Mon Sep 17 00:00:00 2001 From: Timur Kushukov Date: Mon, 19 Oct 2020 19:06:01 +0500 Subject: [PATCH 1196/1439] fix Windows tests --- tests/unit/test_api.py | 2 +- tests/unit/test_main.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_api.py b/tests/unit/test_api.py index fc29c33f5..4ee19bc43 100644 --- a/tests/unit/test_api.py +++ b/tests/unit/test_api.py @@ -87,4 +87,4 @@ def test_sort_code_string_mixed_newlines(): def test_get_import_file(imperfect, capsys): api.get_imports_file(imperfect, sys.stdout) out, _ = capsys.readouterr() - assert out == imperfect_content + assert out == imperfect_content.replace("\n", os.linesep) diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 93ed94e86..70eafb3ad 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -1,4 +1,5 @@ import json +import os import subprocess from datetime import datetime from io import BytesIO, TextIOWrapper @@ -925,4 +926,4 @@ def test_identify_imports_main(tmpdir, capsys): main.identify_imports_main([str(some_file)]) out, error = capsys.readouterr() - assert out == file_imports + assert out == file_imports.replace("\n", os.linesep) From 04e56598de7a38c85982548a25ea6097c9e6403e Mon Sep 17 00:00:00 2001 From: Vasilis Gerakaris Date: Wed, 21 Oct 2020 23:10:38 +0300 Subject: [PATCH 1197/1439] fix deepsource.io warnings "Safe" warnings, regarding style, and ternary operators were fixed. added some variable initialisations to suppress false-positive "possibly unbound" warnings "Unused arguments" warnings were left untouched, as they might be used as kwargs and renaming/removing them might break stuff. --- isort/api.py | 48 ++++++++++++++++++------------------ isort/comments.py | 12 ++++----- isort/core.py | 6 ++--- isort/deprecated/finders.py | 4 +-- isort/literal.py | 2 +- isort/main.py | 40 ++++++++++++++---------------- isort/output.py | 3 +-- isort/parse.py | 16 ++++++------ isort/settings.py | 2 +- isort/setuptools_commands.py | 2 +- isort/sorting.py | 2 +- isort/stdlibs/__init__.py | 3 ++- isort/wrap_modes.py | 14 +++++------ 13 files changed, 75 insertions(+), 79 deletions(-) diff --git a/isort/api.py b/isort/api.py index aed041ce4..c93851306 100644 --- a/isort/api.py +++ b/isort/api.py @@ -216,30 +216,30 @@ def check_stream( if config.verbose and not config.only_modified: printer.success(f"{file_path or ''} Everything Looks Good!") return True - else: - printer.error(f"{file_path or ''} Imports are incorrectly sorted and/or formatted.") - if show_diff: - output_stream = StringIO() - input_stream.seek(0) - file_contents = input_stream.read() - sort_stream( - input_stream=StringIO(file_contents), - output_stream=output_stream, - extension=extension, - config=config, - file_path=file_path, - disregard_skip=disregard_skip, - ) - output_stream.seek(0) - - show_unified_diff( - file_input=file_contents, - file_output=output_stream.read(), - file_path=file_path, - output=None if show_diff is True else cast(TextIO, show_diff), - color_output=config.color_output, - ) - return False + + printer.error(f"{file_path or ''} Imports are incorrectly sorted and/or formatted.") + if show_diff: + output_stream = StringIO() + input_stream.seek(0) + file_contents = input_stream.read() + sort_stream( + input_stream=StringIO(file_contents), + output_stream=output_stream, + extension=extension, + config=config, + file_path=file_path, + disregard_skip=disregard_skip, + ) + output_stream.seek(0) + + show_unified_diff( + file_input=file_contents, + file_output=output_stream.read(), + file_path=file_path, + output=None if show_diff is True else cast(TextIO, show_diff), + color_output=config.color_output, + ) + return False def check_file( diff --git a/isort/comments.py b/isort/comments.py index b865b3281..55c3da674 100644 --- a/isort/comments.py +++ b/isort/comments.py @@ -24,9 +24,9 @@ def add_to_line( if not comments: return original_string - else: - unique_comments: List[str] = [] - for comment in comments: - if comment not in unique_comments: - unique_comments.append(comment) - return f"{parse(original_string)[0]}{comment_prefix} {'; '.join(unique_comments)}" + + unique_comments: List[str] = [] + for comment in comments: + if comment not in unique_comments: + unique_comments.append(comment) + return f"{parse(original_string)[0]}{comment_prefix} {'; '.join(unique_comments)}" diff --git a/isort/core.py b/isort/core.py index badf2a04c..c4ba054af 100644 --- a/isort/core.py +++ b/isort/core.py @@ -70,8 +70,9 @@ def process( end_of_file: bool = False verbose_output: List[str] = [] all_imports: List[str] = [] + + _output_stream = output_stream # Used if imports_only == True if imports_only: - _output_stream = output_stream class DevNull(StringIO): def write(self, *a, **kw): @@ -435,5 +436,4 @@ def _has_changed(before: str, after: str, line_separator: str, ignore_whitespace remove_whitespace(before, line_separator=line_separator).strip() != remove_whitespace(after, line_separator=line_separator).strip() ) - else: - return before.strip() != after.strip() + return before.strip() != after.strip() diff --git a/isort/deprecated/finders.py b/isort/deprecated/finders.py index dbb6fec02..5be8a419f 100644 --- a/isort/deprecated/finders.py +++ b/isort/deprecated/finders.py @@ -188,9 +188,9 @@ def find(self, module_name: str) -> Optional[str]: or (self.virtual_env and self.virtual_env_src in prefix) ): return sections.THIRDPARTY - elif os.path.normcase(prefix) == self.stdlib_lib_prefix: + if os.path.normcase(prefix) == self.stdlib_lib_prefix: return sections.STDLIB - elif self.conda_env and self.conda_env in prefix: + if self.conda_env and self.conda_env in prefix: return sections.THIRDPARTY for src_path in self.config.src_paths: if src_path in path_obj.parents and not self.config.is_skipped(path_obj): diff --git a/isort/literal.py b/isort/literal.py index 01bd05e79..0b1838fe3 100644 --- a/isort/literal.py +++ b/isort/literal.py @@ -41,7 +41,7 @@ def assignment(code: str, sort_type: str, extension: str, config: Config = DEFAU """ if sort_type == "assignments": return assignments(code) - elif sort_type not in type_mapping: + if sort_type not in type_mapping: raise ValueError( "Trying to sort using an undefined sort_type. " f"Defined sort types are {', '.join(type_mapping.keys())}." diff --git a/isort/main.py b/isort/main.py index d2cd76b00..182dd3f61 100644 --- a/isort/main.py +++ b/isort/main.py @@ -81,27 +81,27 @@ def sort_imports( write_to_stdout: bool = False, **kwargs: Any, ) -> Optional[SortAttempt]: + incorrectly_sorted: bool = False + skipped: bool = False try: - incorrectly_sorted: bool = False - skipped: bool = False if check: try: incorrectly_sorted = not api.check_file(file_name, config=config, **kwargs) except FileSkipped: skipped = True return SortAttempt(incorrectly_sorted, skipped, True) - else: - try: - incorrectly_sorted = not api.sort_file( - file_name, - config=config, - ask_to_apply=ask_to_apply, - write_to_stdout=write_to_stdout, - **kwargs, - ) - except FileSkipped: - skipped = True - return SortAttempt(incorrectly_sorted, skipped, True) + + try: + incorrectly_sorted = not api.sort_file( + file_name, + config=config, + ask_to_apply=ask_to_apply, + write_to_stdout=write_to_stdout, + **kwargs, + ) + except FileSkipped: + skipped = True + return SortAttempt(incorrectly_sorted, skipped, True) except (OSError, ValueError) as error: warn(f"Unable to parse file {file_name} due to {error}") return None @@ -816,14 +816,13 @@ def _preconvert(item): """Preconverts objects from native types into JSONifyiable types""" if isinstance(item, (set, frozenset)): return list(item) - elif isinstance(item, WrapModes): + if isinstance(item, WrapModes): return item.name - elif isinstance(item, Path): + if isinstance(item, Path): return str(item) - elif callable(item) and hasattr(item, "__name__"): + if callable(item) and hasattr(item, "__name__"): return item.__name__ - else: - raise TypeError("Unserializable object {} of type {}".format(item, type(item))) + raise TypeError("Unserializable object {} of type {}".format(item, type(item))) def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = None) -> None: @@ -855,8 +854,7 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = print(QUICK_GUIDE) if arguments: sys.exit("Error: arguments passed in without any paths or content.") - else: - return + return if "settings_path" not in arguments: arguments["settings_path"] = ( os.path.abspath(file_names[0] if file_names else ".") or os.getcwd() diff --git a/isort/output.py b/isort/output.py index cda5c24d8..66fc44397 100644 --- a/isort/output.py +++ b/isort/output.py @@ -615,5 +615,4 @@ def _with_star_comments(parsed: parse.ParsedContent, module: str, comments: List star_comment = parsed.categorized_comments["nested"].get(module, {}).pop("*", None) if star_comment: return comments + [star_comment] - else: - return comments + return comments diff --git a/isort/parse.py b/isort/parse.py index 6a999391a..819c5cd85 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -31,10 +31,9 @@ def _infer_line_separator(contents: str) -> str: if "\r\n" in contents: return "\r\n" - elif "\r" in contents: + if "\r" in contents: return "\r" - else: - return "\n" + return "\n" def _normalize_line(raw_line: str) -> Tuple[str, str]: @@ -55,11 +54,11 @@ def import_type(line: str, config: Config = DEFAULT_CONFIG) -> Optional[str]: """If the current line is an import line it will return its type (from or straight)""" if config.honor_noqa and line.lower().rstrip().endswith("noqa"): return None - elif "isort:skip" in line or "isort: skip" in line or "isort: split" in line: + if "isort:skip" in line or "isort: skip" in line or "isort: split" in line: return None - elif line.startswith(("import ", "cimport ")): + if line.startswith(("import ", "cimport ")): return "straight" - elif line.startswith("from "): + if line.startswith("from "): return "from" return None @@ -369,6 +368,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte attach_comments_to: Optional[List[Any]] = None direct_imports = just_imports[1:] straight_import = True + top_level_module = "" if "as" in just_imports and (just_imports.index("as") + 1) < len(just_imports): straight_import = False while "as" in just_imports: @@ -443,7 +443,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte attach_comments_to = categorized_comments["from"].setdefault(import_from, []) if len(out_lines) > max(import_index, 1) - 1: - last = out_lines and out_lines[-1].rstrip() or "" + last = out_lines[-1].rstrip() if out_lines else "" while ( last.startswith("#") and not last.endswith('"""') @@ -489,7 +489,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte if len(out_lines) > max(import_index, +1, 1) - 1: - last = out_lines and out_lines[-1].rstrip() or "" + last = out_lines[-1].rstrip() if out_lines else "" while ( last.startswith("#") and not last.endswith('"""') diff --git a/isort/settings.py b/isort/settings.py index 553475e93..8b93e4236 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -468,7 +468,7 @@ def is_supported_filetype(self, file_name: str): ext = ext.lstrip(".") if ext in self.supported_extensions: return True - elif ext in self.blocked_extensions: + if ext in self.blocked_extensions: return False # Skip editor backup files. diff --git a/isort/setuptools_commands.py b/isort/setuptools_commands.py index 96e41dd0b..6906604ed 100644 --- a/isort/setuptools_commands.py +++ b/isort/setuptools_commands.py @@ -24,7 +24,7 @@ def initialize_options(self) -> None: setattr(self, key, value) def finalize_options(self) -> None: - "Get options from config files." + """Get options from config files.""" self.arguments: Dict[str, Any] = {} # skipcq: PYL-W0201 self.arguments["settings_path"] = os.getcwd() diff --git a/isort/sorting.py b/isort/sorting.py index cab77011b..b614abe79 100644 --- a/isort/sorting.py +++ b/isort/sorting.py @@ -47,7 +47,7 @@ def module_key( or (config.length_sort_straight and straight_import) or str(section_name).lower() in config.length_sort_sections ) - _length_sort_maybe = length_sort and (str(len(module_name)) + ":" + module_name) or module_name + _length_sort_maybe = (str(len(module_name)) + ":" + module_name) if length_sort else module_name return f"{module_name in config.force_to_top and 'A' or 'B'}{prefix}{_length_sort_maybe}" diff --git a/isort/stdlibs/__init__.py b/isort/stdlibs/__init__.py index 9021bc455..ed5aa89dc 100644 --- a/isort/stdlibs/__init__.py +++ b/isort/stdlibs/__init__.py @@ -1 +1,2 @@ -from . import all, py2, py3, py27, py35, py36, py37, py38, py39 +from . import all as _all +from . import py2, py3, py27, py35, py36, py37, py38, py39 diff --git a/isort/wrap_modes.py b/isort/wrap_modes.py index 02b793067..5c2695263 100644 --- a/isort/wrap_modes.py +++ b/isort/wrap_modes.py @@ -250,15 +250,13 @@ def noqa(**interface): <= interface["line_length"] ): return f"{retval}{interface['comment_prefix']} {comment_str}" - elif "NOQA" in interface["comments"]: + if "NOQA" in interface["comments"]: return f"{retval}{interface['comment_prefix']} {comment_str}" - else: - return f"{retval}{interface['comment_prefix']} NOQA {comment_str}" - else: - if len(retval) <= interface["line_length"]: - return retval - else: - return f"{retval}{interface['comment_prefix']} NOQA" + return f"{retval}{interface['comment_prefix']} NOQA {comment_str}" + + if len(retval) <= interface["line_length"]: + return retval + return f"{retval}{interface['comment_prefix']} NOQA" @_wrap_mode From bacd6c7b9f390dbe02e65bdc5a2c1670197f05cf Mon Sep 17 00:00:00 2001 From: jaydesl Date: Sun, 25 Oct 2020 11:42:39 +0000 Subject: [PATCH 1198/1439] Refactor error printer --- isort/main.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/isort/main.py b/isort/main.py index fa93256c3..84569441f 100644 --- a/isort/main.py +++ b/isort/main.py @@ -110,15 +110,23 @@ def sort_imports( warn(f"Encoding not supported for {file_name}") return SortAttempt(incorrectly_sorted, skipped, False) except Exception: - printer = create_terminal_printer(color=config.color_output) - printer.error( - f"Unrecoverable exception thrown when parsing {file_name}! " - "This should NEVER happen.\n" - "If encountered, please open an issue: https://github.com/PyCQA/isort/issues/new" - ) + _print_hard_fail(config, offending_file=file_name) raise +def _print_hard_fail( + config: Config, offending_file: Optional[str] = None, message: Optional[str] = None +) -> None: + """Fail on unrecoverable exception with custom message.""" + message = message or ( + f"Unrecoverable exception thrown when parsing {offending_file or ''}!" + "This should NEVER happen.\n" + "If encountered, please open an issue: https://github.com/PyCQA/isort/issues/new" + ) + printer = create_terminal_printer(color=config.color_output) + printer.error(message) + + def iter_source_code( paths: Iterable[str], config: Config, skipped: List[str], broken: List[str] ) -> Iterator[str]: From 435d2cd3470c24057ea0519140a000a09be4a526 Mon Sep 17 00:00:00 2001 From: jaydesl Date: Sun, 25 Oct 2020 11:43:16 +0000 Subject: [PATCH 1199/1439] Gracefully fail on missing default sections --- isort/main.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/isort/main.py b/isort/main.py index 84569441f..8ac59a709 100644 --- a/isort/main.py +++ b/isort/main.py @@ -14,7 +14,7 @@ from .format import create_terminal_printer from .logo import ASCII_ART from .profiles import profiles -from .settings import VALID_PY_TARGETS, Config, WrapModes +from .settings import DEFAULT_CONFIG, VALID_PY_TARGETS, Config, WrapModes try: from .setuptools_commands import ISortCommand # noqa: F401 @@ -109,6 +109,18 @@ def sort_imports( if config.verbose: warn(f"Encoding not supported for {file_name}") return SortAttempt(incorrectly_sorted, skipped, False) + except KeyError as error: + if error.args[0] not in DEFAULT_CONFIG.sections: + _print_hard_fail(config, offending_file=file_name) + raise + msg = ( + f"Found {error} imports while parsing, but {error} was not included " + "in the `sections` setting of your config. Please add it before continuing\n" + "See https://pycqa.github.io/isort/#custom-sections-and-ordering " + "for more info." + ) + _print_hard_fail(config, message=msg) + sys.exit(os.EX_CONFIG) except Exception: _print_hard_fail(config, offending_file=file_name) raise From 1bb257663caefc347b48ab3c2b2bccb2e0c42e50 Mon Sep 17 00:00:00 2001 From: jaydesl Date: Sun, 25 Oct 2020 11:43:30 +0000 Subject: [PATCH 1200/1439] Improve docs around custom section ordering --- README.md | 2 +- docs/configuration/options.md | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c2c6a8244..f55c15883 100644 --- a/README.md +++ b/README.md @@ -348,7 +348,7 @@ of: FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER ``` -to your preference: +to your preference (if defined, omitting a default section may cause errors): ```ini sections=FUTURE,STDLIB,FIRSTPARTY,THIRDPARTY,LOCALFOLDER diff --git a/docs/configuration/options.md b/docs/configuration/options.md index bf2205243..de70eda48 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -108,7 +108,9 @@ Forces line endings to the specified value. If not set, values will be guessed p ## Sections -**No Description** +Specifies a custom ordering for sections. Any custom defined sections should also be +included in this ordering. Omitting any of the default sections from this tuple may +result in unexpected sorting or an exception being raised. **Type:** Tuple **Default:** `('FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER')` From 94ae8995739a9c8e678527d615d9d9759e0781e9 Mon Sep 17 00:00:00 2001 From: Tonci Kokan Date: Sun, 25 Oct 2020 12:33:29 +0000 Subject: [PATCH 1201/1439] Fix a typo --- isort/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/main.py b/isort/main.py index fa93256c3..ceaf3c8a8 100644 --- a/isort/main.py +++ b/isort/main.py @@ -1003,7 +1003,7 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = print(f"Skipped {num_skipped} files") num_broken += len(broken) - if num_broken and not arguments.get("quite", False): + if num_broken and not arguments.get("quiet", False): if config.verbose: for was_broken in broken: warn(f"{was_broken} was broken path, make sure it exists correctly") From c6499339e8010f42b32f9c31d40dd8bf6fe15319 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 25 Oct 2020 21:08:49 -0700 Subject: [PATCH 1202/1439] Add additional contributors: Timur Kushukov (@timqsh), Bhupesh Varshney (@Bhupesh-V), Rohan Khanna (@rohankhanna) , and Vasilis Gerakaris (@vgerak) --- docs/contributing/4.-acknowledgements.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index 9c04e3f8c..fcae04e81 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -203,6 +203,10 @@ Code Contributors - James Curtin (@jamescurtin) - Marco Gorelli (@MarcoGorelli) - Louis Sautier (@sbraz) +- Timur Kushukov (@timqsh) +- Bhupesh Varshney (@Bhupesh-V) +- Rohan Khanna (@rohankhanna) +- Vasilis Gerakaris (@vgerak) Documenters =================== From 0233e7873d80a6c3fc59ffbeee99536e7a51e5c4 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 25 Oct 2020 21:10:11 -0700 Subject: [PATCH 1203/1439] Update automatically produced docs (profiles & config options --- docs/configuration/options.md | 77 ++++++++++++++++++++++++++++++---- docs/configuration/profiles.md | 2 + 2 files changed, 72 insertions(+), 7 deletions(-) diff --git a/docs/configuration/options.md b/docs/configuration/options.md index bf2205243..12753c31b 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -179,7 +179,7 @@ Force isort to recognize a module as being a local folder. Generally, this is re Force isort to recognize a module as part of Python's standard library. **Type:** Frozenset -**Default:** `('_dummy_thread', '_thread', 'abc', 'aifc', 'argparse', 'array', 'ast', 'asynchat', 'asyncio', 'asyncore', 'atexit', 'audioop', 'base64', 'bdb', 'binascii', 'binhex', 'bisect', 'builtins', 'bz2', 'cProfile', 'calendar', 'cgi', 'cgitb', 'chunk', 'cmath', 'cmd', 'code', 'codecs', 'codeop', 'collections', 'colorsys', 'compileall', 'concurrent', 'configparser', 'contextlib', 'contextvars', 'copy', 'copyreg', 'crypt', 'csv', 'ctypes', 'curses', 'dataclasses', 'datetime', 'dbm', 'decimal', 'difflib', 'dis', 'distutils', 'doctest', 'dummy_threading', 'email', 'encodings', 'ensurepip', 'enum', 'errno', 'faulthandler', 'fcntl', 'filecmp', 'fileinput', 'fnmatch', 'formatter', 'fpectl', 'fractions', 'ftplib', 'functools', 'gc', 'getopt', 'getpass', 'gettext', 'glob', 'grp', 'gzip', 'hashlib', 'heapq', 'hmac', 'html', 'http', 'imaplib', 'imghdr', 'imp', 'importlib', 'inspect', 'io', 'ipaddress', 'itertools', 'json', 'keyword', 'lib2to3', 'linecache', 'locale', 'logging', 'lzma', 'macpath', 'mailbox', 'mailcap', 'marshal', 'math', 'mimetypes', 'mmap', 'modulefinder', 'msilib', 'msvcrt', 'multiprocessing', 'netrc', 'nis', 'nntplib', 'ntpath', 'numbers', 'operator', 'optparse', 'os', 'ossaudiodev', 'parser', 'pathlib', 'pdb', 'pickle', 'pickletools', 'pipes', 'pkgutil', 'platform', 'plistlib', 'poplib', 'posix', 'posixpath', 'pprint', 'profile', 'pstats', 'pty', 'pwd', 'py_compile', 'pyclbr', 'pydoc', 'queue', 'quopri', 'random', 're', 'readline', 'reprlib', 'resource', 'rlcompleter', 'runpy', 'sched', 'secrets', 'select', 'selectors', 'shelve', 'shlex', 'shutil', 'signal', 'site', 'smtpd', 'smtplib', 'sndhdr', 'socket', 'socketserver', 'spwd', 'sqlite3', 'sre', 'sre_compile', 'sre_constants', 'sre_parse', 'ssl', 'stat', 'statistics', 'string', 'stringprep', 'struct', 'subprocess', 'sunau', 'symbol', 'symtable', 'sys', 'sysconfig', 'syslog', 'tabnanny', 'tarfile', 'telnetlib', 'tempfile', 'termios', 'test', 'textwrap', 'threading', 'time', 'timeit', 'tkinter', 'token', 'tokenize', 'trace', 'traceback', 'tracemalloc', 'tty', 'turtle', 'turtledemo', 'types', 'typing', 'unicodedata', 'unittest', 'urllib', 'uu', 'uuid', 'venv', 'warnings', 'wave', 'weakref', 'webbrowser', 'winreg', 'winsound', 'wsgiref', 'xdrlib', 'xml', 'xmlrpc', 'zipapp', 'zipfile', 'zipimport', 'zlib')` +**Default:** `('_dummy_thread', '_thread', 'abc', 'aifc', 'argparse', 'array', 'ast', 'asynchat', 'asyncio', 'asyncore', 'atexit', 'audioop', 'base64', 'bdb', 'binascii', 'binhex', 'bisect', 'builtins', 'bz2', 'cProfile', 'calendar', 'cgi', 'cgitb', 'chunk', 'cmath', 'cmd', 'code', 'codecs', 'codeop', 'collections', 'colorsys', 'compileall', 'concurrent', 'configparser', 'contextlib', 'contextvars', 'copy', 'copyreg', 'crypt', 'csv', 'ctypes', 'curses', 'dataclasses', 'datetime', 'dbm', 'decimal', 'difflib', 'dis', 'distutils', 'doctest', 'dummy_threading', 'email', 'encodings', 'ensurepip', 'enum', 'errno', 'faulthandler', 'fcntl', 'filecmp', 'fileinput', 'fnmatch', 'formatter', 'fpectl', 'fractions', 'ftplib', 'functools', 'gc', 'getopt', 'getpass', 'gettext', 'glob', 'graphlib', 'grp', 'gzip', 'hashlib', 'heapq', 'hmac', 'html', 'http', 'imaplib', 'imghdr', 'imp', 'importlib', 'inspect', 'io', 'ipaddress', 'itertools', 'json', 'keyword', 'lib2to3', 'linecache', 'locale', 'logging', 'lzma', 'macpath', 'mailbox', 'mailcap', 'marshal', 'math', 'mimetypes', 'mmap', 'modulefinder', 'msilib', 'msvcrt', 'multiprocessing', 'netrc', 'nis', 'nntplib', 'ntpath', 'numbers', 'operator', 'optparse', 'os', 'ossaudiodev', 'parser', 'pathlib', 'pdb', 'pickle', 'pickletools', 'pipes', 'pkgutil', 'platform', 'plistlib', 'poplib', 'posix', 'posixpath', 'pprint', 'profile', 'pstats', 'pty', 'pwd', 'py_compile', 'pyclbr', 'pydoc', 'queue', 'quopri', 'random', 're', 'readline', 'reprlib', 'resource', 'rlcompleter', 'runpy', 'sched', 'secrets', 'select', 'selectors', 'shelve', 'shlex', 'shutil', 'signal', 'site', 'smtpd', 'smtplib', 'sndhdr', 'socket', 'socketserver', 'spwd', 'sqlite3', 'sre', 'sre_compile', 'sre_constants', 'sre_parse', 'ssl', 'stat', 'statistics', 'string', 'stringprep', 'struct', 'subprocess', 'sunau', 'symbol', 'symtable', 'sys', 'sysconfig', 'syslog', 'tabnanny', 'tarfile', 'telnetlib', 'tempfile', 'termios', 'test', 'textwrap', 'threading', 'time', 'timeit', 'tkinter', 'token', 'tokenize', 'trace', 'traceback', 'tracemalloc', 'tty', 'turtle', 'turtledemo', 'types', 'typing', 'unicodedata', 'unittest', 'urllib', 'uu', 'uuid', 'venv', 'warnings', 'wave', 'weakref', 'webbrowser', 'winreg', 'winsound', 'wsgiref', 'xdrlib', 'xml', 'xmlrpc', 'zipapp', 'zipfile', 'zipimport', 'zlib', 'zoneinfo')` **Python & Config File Name:** known_standard_library **CLI Flags:** @@ -296,7 +296,7 @@ Sort imports by their string length. ## Length Sort Straight -Sort straight imports by their string length. +Sort straight imports by their string length. Similar to `length_sort` but applies only to straight imports and doesn't affect from imports. **Type:** Bool **Default:** `False` @@ -602,7 +602,7 @@ Force all imports to be sorted as a single section ## Force Grid Wrap -Force number of from imports (defaults to 2 when passed as CLI flag without value) to be grid wrapped regardless of line length. If 0 is passed in (the global default) only line length is considered. +Force number of from imports (defaults to 2 when passed as CLI flag without value)to be grid wrapped regardless of line length. If 0 is passed in (the global default) only line length is considered. **Type:** Int **Default:** `0` @@ -633,6 +633,15 @@ Don't sort straight-style imports (like import sys) before from-style imports (l **Python & Config File Name:** lexicographical **CLI Flags:** **Not Supported** +## Group By Package + +**No Description** + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** group_by_package +**CLI Flags:** **Not Supported** + ## Ignore Whitespace Tells isort to ignore whitespace differences when --check-only is being used. @@ -767,8 +776,8 @@ Tells isort to honor noqa comments to enforce skipping those comments. Add an explicitly defined source path (modules within src paths have their imports automatically categorized as first_party). -**Type:** Frozenset -**Default:** `frozenset()` +**Type:** Tuple +**Default:** `()` **Python & Config File Name:** src_paths **CLI Flags:** @@ -800,7 +809,8 @@ Tells isort to remove redundant aliases from imports, such as `import os as os`. ## Float To Top -Causes all non-indented imports to float to the top of the file having its imports sorted. It can be an excellent shortcut for collecting imports every once in a while when you place them in the middle of a file to avoid context switching. +Causes all non-indented imports to float to the top of the file having its imports sorted (immediately below the top of file comment). +This can be an excellent shortcut for collecting imports every once in a while when you place them in the middle of a file to avoid context switching. *NOTE*: It currently doesn't work with cimports and introduces some extra over-head and a performance penalty. @@ -880,7 +890,7 @@ Tells isort to treat all single line comments as if they are code. Specifies what extensions isort can be ran against. **Type:** Frozenset -**Default:** `('py', 'pyi', 'pyx')` +**Default:** `('pxd', 'py', 'pyi', 'pyx')` **Python & Config File Name:** supported_extensions **CLI Flags:** @@ -949,6 +959,48 @@ Causes imports to be sorted only based on their sections like STDLIB,THIRDPARTY - --only-sections - --os +## Only Modified + +Suppresses verbose output for non-modified files. + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** only_modified +**CLI Flags:** + +- --only-modified +- --om + +## Combine Straight Imports + +Combines all the bare straight imports of the same section in a single line. Won't work with sections which have 'as' imports + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** combine_straight_imports +**CLI Flags:** + +- --combine-straight-imports +- --csi + +## Auto Identify Namespace Packages + +**No Description** + +**Type:** Bool +**Default:** `True` +**Python & Config File Name:** auto_identify_namespace_packages +**CLI Flags:** **Not Supported** + +## Namespace Packages + +**No Description** + +**Type:** Frozenset +**Default:** `frozenset()` +**Python & Config File Name:** namespace_packages +**CLI Flags:** **Not Supported** + ## Check Checks the file for unsorted / unformatted imports and prints them to the command line without modifying the file. @@ -1090,6 +1142,17 @@ See isort's determined config, as well as sources of config options. - --show-config +## Show Files + +See the files isort will be ran against with the current config options. + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** **Not Supported** +**CLI Flags:** + +- --show-files + ## Deprecated Flags ==SUPPRESS== diff --git a/docs/configuration/profiles.md b/docs/configuration/profiles.md index 1b6d6f75e..9de48e0f7 100644 --- a/docs/configuration/profiles.md +++ b/docs/configuration/profiles.md @@ -39,6 +39,8 @@ To use any of the listed profiles, use `isort --profile PROFILE_NAME` from the c - **force_sort_within_sections**: `True` - **lexicographical**: `True` - **single_line_exclusions**: `('typing',)` + - **order_by_type**: `False` + - **group_by_package**: `True` #open_stack From 42ef40994af61e7da4280492afa5843f5bcd9bbe Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 25 Oct 2020 21:11:54 -0700 Subject: [PATCH 1204/1439] Add @tonci-bw to contributors list --- docs/contributing/4.-acknowledgements.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index fcae04e81..19d06a9f2 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -207,6 +207,7 @@ Code Contributors - Bhupesh Varshney (@Bhupesh-V) - Rohan Khanna (@rohankhanna) - Vasilis Gerakaris (@vgerak) +- @tonci-bw Documenters =================== From 13a96af4678bef5d359b90af76d5c2bc1d0c35a8 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 26 Oct 2020 21:01:49 -0700 Subject: [PATCH 1205/1439] Add @jaydesl to contributors list --- docs/contributing/4.-acknowledgements.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index 19d06a9f2..c6b065fde 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -208,6 +208,7 @@ Code Contributors - Rohan Khanna (@rohankhanna) - Vasilis Gerakaris (@vgerak) - @tonci-bw +- @jaydesl Documenters =================== From 35a22053c42797f82c293a59502af815230d2c69 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 27 Oct 2020 23:43:49 -0700 Subject: [PATCH 1206/1439] Fix issue #1582: Provide a flag for not following links --- isort/main.py | 12 ++++++++---- isort/settings.py | 1 + 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/isort/main.py b/isort/main.py index c8715e10d..7f776e0cd 100644 --- a/isort/main.py +++ b/isort/main.py @@ -147,7 +147,7 @@ def iter_source_code( for path in paths: if os.path.isdir(path): - for dirpath, dirnames, filenames in os.walk(path, topdown=True, followlinks=True): + for dirpath, dirnames, filenames in os.walk(path, topdown=True, followlinks=config.follow_links): base_path = Path(dirpath) for dirname in list(dirnames): full_path = base_path / dirname @@ -741,7 +741,6 @@ def _build_arg_parser() -> argparse.ArgumentParser: help="Tells isort to only show an identical custom import heading comment once, even if" " there are multiple sections with the comment set.", ) - parser.add_argument( "--only-sections", "--os", @@ -750,7 +749,6 @@ def _build_arg_parser() -> argparse.ArgumentParser: help="Causes imports to be sorted only based on their sections like STDLIB,THIRDPARTY etc. " "Imports are unaltered and keep their relative positions within the different sections.", ) - parser.add_argument( "--only-modified", "--om", @@ -758,7 +756,6 @@ def _build_arg_parser() -> argparse.ArgumentParser: action="store_true", help="Suppresses verbose output for non-modified files.", ) - parser.add_argument( "--combine-straight-imports", "--csi", @@ -767,6 +764,11 @@ def _build_arg_parser() -> argparse.ArgumentParser: help="Combines all the bare straight imports of the same section in a single line. " "Won't work with sections which have 'as' imports", ) + parser.add_argument( + "--dont-follow-links", + dest="dont_follow_links", + action="store_true" + ) # deprecated options parser.add_argument( @@ -823,6 +825,8 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: if "dont_order_by_type" in arguments: arguments["order_by_type"] = False del arguments["dont_order_by_type"] + if "dont_follow_links" in arguments: + arguments["follow_links"] = False multi_line_output = arguments.get("multi_line_output", None) if multi_line_output: if multi_line_output.isdigit(): diff --git a/isort/settings.py b/isort/settings.py index 8b93e4236..f0bd14b2d 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -203,6 +203,7 @@ class _Config: combine_straight_imports: bool = False auto_identify_namespace_packages: bool = True namespace_packages: FrozenSet[str] = frozenset() + follow_links: bool = True def __post_init__(self): py_version = self.py_version From 669a6c36309ce89581074ab1e3aaa5bf7785aba7 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 27 Oct 2020 23:44:51 -0700 Subject: [PATCH 1207/1439] Add help --- isort/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/isort/main.py b/isort/main.py index 7f776e0cd..b1274ef33 100644 --- a/isort/main.py +++ b/isort/main.py @@ -767,7 +767,8 @@ def _build_arg_parser() -> argparse.ArgumentParser: parser.add_argument( "--dont-follow-links", dest="dont_follow_links", - action="store_true" + action="store_true", + help="Tells isort not to follow symlinks that are encountered when running recursively.", ) # deprecated options From 21b509797ba72631a506d333e86d8effe4584791 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 27 Oct 2020 23:49:30 -0700 Subject: [PATCH 1208/1439] black --- isort/main.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/isort/main.py b/isort/main.py index b1274ef33..0aeed6994 100644 --- a/isort/main.py +++ b/isort/main.py @@ -147,7 +147,9 @@ def iter_source_code( for path in paths: if os.path.isdir(path): - for dirpath, dirnames, filenames in os.walk(path, topdown=True, followlinks=config.follow_links): + for dirpath, dirnames, filenames in os.walk( + path, topdown=True, followlinks=config.follow_links + ): base_path = Path(dirpath) for dirname in list(dirnames): full_path = base_path / dirname From a7bbdc4f564e5267627157044491907038667e4f Mon Sep 17 00:00:00 2001 From: Tamara Khalbashkeeva Date: Thu, 29 Oct 2020 02:02:20 +0200 Subject: [PATCH 1209/1439] Group options in help: general, target, general output, section output, deprecated --- isort/main.py | 739 ++++++++++++++++++++++++++------------------------ 1 file changed, 381 insertions(+), 358 deletions(-) diff --git a/isort/main.py b/isort/main.py index 0aeed6994..58dfecf77 100644 --- a/isort/main.py +++ b/isort/main.py @@ -4,6 +4,7 @@ import json import os import sys +from gettext import gettext as _ from io import TextIOWrapper from pathlib import Path from typing import Any, Dict, Iterable, Iterator, List, Optional, Sequence, Set @@ -186,18 +187,203 @@ def _build_arg_parser() -> argparse.ArgumentParser: "interactive behavior." " " "If you've used isort 4 but are new to isort 5, see the upgrading guide:" - "https://pycqa.github.io/isort/docs/upgrade_guides/5.0.0/." + "https://pycqa.github.io/isort/docs/upgrade_guides/5.0.0/.", + add_help=False, # prevent help option from appearing in "optional arguments" group ) - inline_args_group = parser.add_mutually_exclusive_group() - parser.add_argument( - "--src", - "--src-path", - dest="src_paths", + + general_group = parser.add_argument_group("general options") + target_group = parser.add_argument_group("target options") + output_group = parser.add_argument_group("general output options") + inline_args_group = output_group.add_mutually_exclusive_group() + section_group = parser.add_argument_group("section output options") + deprecated_group = parser.add_argument_group("deprecated options") + + general_group.add_argument( + "-h", + "--help", + action="help", + default=argparse.SUPPRESS, + help=_('show this help message and exit'), + ) + general_group.add_argument( + "-V", + "--version", + action="store_true", + dest="show_version", + help="Displays the currently installed version of isort.", + ) + general_group.add_argument( + "--vn", + "--version-number", + action="version", + version=__version__, + help="Returns just the current version number without the logo", + ) + general_group.add_argument( + "-v", + "--verbose", + action="store_true", + dest="verbose", + help="Shows verbose output, such as when files are skipped or when a check is successful.", + ) + general_group.add_argument( + "--only-modified", + "--om", + dest="only_modified", + action="store_true", + help="Suppresses verbose output for non-modified files.", + ) + general_group.add_argument( + "--dedup-headings", + dest="dedup_headings", + action="store_true", + help="Tells isort to only show an identical custom import heading comment once, even if" + " there are multiple sections with the comment set.", + ) + general_group.add_argument( + "-q", + "--quiet", + action="store_true", + dest="quiet", + help="Shows extra quiet output, only errors are outputted.", + ) + general_group.add_argument( + "-d", + "--stdout", + help="Force resulting output to stdout, instead of in-place.", + dest="write_to_stdout", + action="store_true", + ) + general_group.add_argument( + "--show-config", + dest="show_config", + action="store_true", + help="See isort's determined config, as well as sources of config options.", + ) + general_group.add_argument( + "--show-files", + dest="show_files", + action="store_true", + help="See the files isort will be ran against with the current config options.", + ) + general_group.add_argument( + "--df", + "--diff", + dest="show_diff", + action="store_true", + help="Prints a diff of all the changes isort would make to a file, instead of " + "changing it in place", + ) + general_group.add_argument( + "-c", + "--check-only", + "--check", + action="store_true", + dest="check", + help="Checks the file for unsorted / unformatted imports and prints them to the " + "command line without modifying the file.", + ) + general_group.add_argument( + "--ws", + "--ignore-whitespace", + action="store_true", + dest="ignore_whitespace", + help="Tells isort to ignore whitespace differences when --check-only is being used.", + ) + general_group.add_argument( + "--sp", + "--settings-path", + "--settings-file", + "--settings", + dest="settings_path", + help="Explicitly set the settings path or file instead of auto determining " + "based on file location.", + ) + general_group.add_argument( + "--profile", + dest="profile", + type=str, + help="Base profile type to use for configuration. " + f"Profiles include: {', '.join(profiles.keys())}. As well as any shared profiles.", + ) + general_group.add_argument( + "--old-finders", + "--magic-placement", + dest="old_finders", + action="store_true", + help="Use the old deprecated finder logic that relies on environment introspection magic.", + ) + general_group.add_argument( + "-j", "--jobs", help="Number of files to process in parallel.", dest="jobs", type=int + ) + general_group.add_argument( + "--ac", + "--atomic", + dest="atomic", + action="store_true", + help="Ensures the output doesn't save if the resulting file contains syntax errors.", + ) + general_group.add_argument( + "--interactive", + dest="ask_to_apply", + action="store_true", + help="Tells isort to apply changes interactively.", + ) + + target_group.add_argument( + "files", nargs="*", help="One or more Python source files that need their imports sorted." + ) + target_group.add_argument( + "--filter-files", + dest="filter_files", + action="store_true", + help="Tells isort to filter files even when they are explicitly passed in as " + "part of the CLI command.", + ) + target_group.add_argument( + "-s", + "--skip", + help="Files that sort imports should skip over. If you want to skip multiple " + "files you should specify twice: --skip file1 --skip file2.", + dest="skip", action="append", - help="Add an explicitly defined source path " - "(modules within src paths have their imports automatically categorized as first_party).", ) - parser.add_argument( + target_group.add_argument( + "--sg", + "--skip-glob", + help="Files that sort imports should skip over.", + dest="skip_glob", + action="append", + ) + target_group.add_argument( + "--gitignore", + "--skip-gitignore", + action="store_true", + dest="skip_gitignore", + help="Treat project as a git repository and ignore files listed in .gitignore", + ) + target_group.add_argument( + "--ext", + "--extension", + "--supported-extension", + dest="supported_extensions", + action="append", + help="Specifies what extensions isort can be ran against.", + ) + target_group.add_argument( + "--blocked-extension", + dest="blocked_extensions", + action="append", + help="Specifies what extensions isort can never be ran against.", + ) + target_group.add_argument( + "--dont-follow-links", + dest="dont_follow_links", + action="store_true", + help="Tells isort not to follow symlinks that are encountered when running recursively.", + ) + + output_group.add_argument( "-a", "--add-import", dest="add_imports", @@ -205,58 +391,47 @@ def _build_arg_parser() -> argparse.ArgumentParser: help="Adds the specified import line to all files, " "automatically determining correct placement.", ) - parser.add_argument( + output_group.add_argument( "--append", "--append-only", dest="append_only", action="store_true", - help="Only adds the imports specified in --add-imports if the file" + help="Only adds the imports specified in --add-import if the file" " contains existing imports.", ) - parser.add_argument( - "--ac", - "--atomic", - dest="atomic", - action="store_true", - help="Ensures the output doesn't save if the resulting file contains syntax errors.", - ) - parser.add_argument( + output_group.add_argument( "--af", "--force-adds", dest="force_adds", action="store_true", help="Forces import adds even if the original file is empty.", ) - parser.add_argument( - "-b", - "--builtin", - dest="known_standard_library", - action="append", - help="Force isort to recognize a module as part of Python's standard library.", - ) - parser.add_argument( - "--extra-builtin", - dest="extra_standard_library", + output_group.add_argument( + "--rm", + "--remove-import", + dest="remove_imports", action="append", - help="Extra modules to be included in the list of ones in Python's standard library.", + help="Removes the specified import from all files.", ) - parser.add_argument( - "-c", - "--check-only", - "--check", + output_group.add_argument( + "--float-to-top", + dest="float_to_top", action="store_true", - dest="check", - help="Checks the file for unsorted / unformatted imports and prints them to the " - "command line without modifying the file.", + help="Causes all non-indented imports to float to the top of the file having its imports " + "sorted (immediately below the top of file comment).\n" + "This can be an excellent shortcut for collecting imports every once in a while " + "when you place them in the middle of a file to avoid context switching.\n\n" + "*NOTE*: It currently doesn't work with cimports and introduces some extra over-head " + "and a performance penalty.", ) - parser.add_argument( + output_group.add_argument( "--ca", "--combine-as", dest="combine_as_imports", action="store_true", help="Combines as imports on the same line.", ) - parser.add_argument( + output_group.add_argument( "--cs", "--combine-star", dest="combine_star", @@ -264,69 +439,21 @@ def _build_arg_parser() -> argparse.ArgumentParser: help="Ensures that if a star import is present, " "nothing else is imported from that namespace.", ) - parser.add_argument( - "-d", - "--stdout", - help="Force resulting output to stdout, instead of in-place.", - dest="write_to_stdout", - action="store_true", - ) - parser.add_argument( - "--df", - "--diff", - dest="show_diff", - action="store_true", - help="Prints a diff of all the changes isort would make to a file, instead of " - "changing it in place", - ) - parser.add_argument( - "--ds", - "--no-sections", - help="Put all imports into the same section bucket", - dest="no_sections", - action="store_true", - ) - parser.add_argument( + output_group.add_argument( "-e", "--balanced", dest="balanced_wrapping", action="store_true", help="Balances wrapping to produce the most consistent line length possible", ) - parser.add_argument( - "-f", - "--future", - dest="known_future_library", - action="append", - help="Force isort to recognize a module as part of Python's internal future compatibility " - "libraries. WARNING: this overrides the behavior of __future__ handling and therefore" - " can result in code that can't execute. If you're looking to add dependencies such " - "as six a better option is to create a another section below --future using custom " - "sections. See: https://github.com/PyCQA/isort#custom-sections-and-ordering and the " - "discussion here: https://github.com/PyCQA/isort/issues/1463.", - ) - parser.add_argument( - "--fas", - "--force-alphabetical-sort", - action="store_true", - dest="force_alphabetical_sort", - help="Force all imports to be sorted as a single section", - ) - parser.add_argument( - "--fass", - "--force-alphabetical-sort-within-sections", - action="store_true", - dest="force_alphabetical_sort_within_sections", - help="Force all imports to be sorted alphabetically within a section", - ) - parser.add_argument( + output_group.add_argument( "--ff", "--from-first", dest="from_first", help="Switches the typical ordering preference, " "showing from imports first then straight ones.", ) - parser.add_argument( + output_group.add_argument( "--fgw", "--force-grid-wrap", nargs="?", @@ -337,42 +464,34 @@ def _build_arg_parser() -> argparse.ArgumentParser: "to be grid wrapped regardless of line " "length. If 0 is passed in (the global default) only line length is considered.", ) - parser.add_argument( - "--fss", - "--force-sort-within-sections", - action="store_true", - dest="force_sort_within_sections", - help="Don't sort straight-style imports (like import sys) before from-style imports " - "(like from itertools import groupby). Instead, sort the imports by module, " - "independent of import style.", - ) - parser.add_argument( + output_group.add_argument( "-i", "--indent", help='String to place for indents defaults to " " (4 spaces).', dest="indent", type=str, ) - parser.add_argument( - "-j", "--jobs", help="Number of files to process in parallel.", dest="jobs", type=int + output_group.add_argument( + "--lai", "--lines-after-imports", dest="lines_after_imports", type=int + ) + output_group.add_argument( + "--lbt", "--lines-between-types", dest="lines_between_types", type=int ) - parser.add_argument("--lai", "--lines-after-imports", dest="lines_after_imports", type=int) - parser.add_argument("--lbt", "--lines-between-types", dest="lines_between_types", type=int) - parser.add_argument( + output_group.add_argument( "--le", "--line-ending", dest="line_ending", help="Forces line endings to the specified value. " "If not set, values will be guessed per-file.", ) - parser.add_argument( + output_group.add_argument( "--ls", "--length-sort", help="Sort imports by their string length.", dest="length_sort", action="store_true", ) - parser.add_argument( + output_group.add_argument( "--lss", "--length-sort-straight", help="Sort straight imports by their string length. Similar to `length_sort` " @@ -380,7 +499,7 @@ def _build_arg_parser() -> argparse.ArgumentParser: dest="length_sort_straight", action="store_true", ) - parser.add_argument( + output_group.add_argument( "-m", "--multi-line", dest="multi_line_output", @@ -392,7 +511,7 @@ def _build_arg_parser() -> argparse.ArgumentParser: "8-vertical-hanging-indent-bracket, 9-vertical-prefix-from-module-import, " "10-hanging-indent-with-parentheses).", ) - parser.add_argument( + output_group.add_argument( "-n", "--ensure-newline-before-comments", dest="ensure_newline_before_comments", @@ -407,21 +526,7 @@ def _build_arg_parser() -> argparse.ArgumentParser: help="Leaves `from` imports with multiple imports 'as-is' " "(e.g. `from foo import a, c ,b`).", ) - parser.add_argument( - "--nlb", - "--no-lines-before", - help="Sections which should not be split with previous by empty lines", - dest="no_lines_before", - action="append", - ) - parser.add_argument( - "-o", - "--thirdparty", - dest="known_third_party", - action="append", - help="Force isort to recognize a module as being part of a third party library.", - ) - parser.add_argument( + output_group.add_argument( "--ot", "--order-by-type", dest="order_by_type", @@ -434,7 +539,7 @@ def _build_arg_parser() -> argparse.ArgumentParser: "likely will want to turn it off. From the CLI the `--dont-order-by-type` option will turn " "this off.", ) - parser.add_argument( + output_group.add_argument( "--dt", "--dont-order-by-type", dest="dont_order_by_type", @@ -447,69 +552,13 @@ def _build_arg_parser() -> argparse.ArgumentParser: " or a related coding standard and has many imports this is a good default. You can turn " "this on from the CLI using `--order-by-type`.", ) - parser.add_argument( - "-p", - "--project", - dest="known_first_party", - action="append", - help="Force isort to recognize a module as being part of the current python project.", - ) - parser.add_argument( - "--known-local-folder", - dest="known_local_folder", - action="append", - help="Force isort to recognize a module as being a local folder. " - "Generally, this is reserved for relative imports (from . import module).", - ) - parser.add_argument( - "-q", - "--quiet", - action="store_true", - dest="quiet", - help="Shows extra quiet output, only errors are outputted.", - ) - parser.add_argument( - "--rm", - "--remove-import", - dest="remove_imports", - action="append", - help="Removes the specified import from all files.", - ) - parser.add_argument( + output_group.add_argument( "--rr", "--reverse-relative", dest="reverse_relative", action="store_true", help="Reverse order of relative imports.", ) - parser.add_argument( - "-s", - "--skip", - help="Files that sort imports should skip over. If you want to skip multiple " - "files you should specify twice: --skip file1 --skip file2.", - dest="skip", - action="append", - ) - parser.add_argument( - "--sd", - "--section-default", - dest="default_section", - help="Sets the default section for import options: " + str(sections.DEFAULT), - ) - parser.add_argument( - "--sg", - "--skip-glob", - help="Files that sort imports should skip over.", - dest="skip_glob", - action="append", - ) - parser.add_argument( - "--gitignore", - "--skip-gitignore", - action="store_true", - dest="skip_gitignore", - help="Treat project as a git repository and ignore files listed in .gitignore", - ) inline_args_group.add_argument( "--sl", "--force-single-line-imports", @@ -517,37 +566,21 @@ def _build_arg_parser() -> argparse.ArgumentParser: action="store_true", help="Forces all from imports to appear on their own line", ) - parser.add_argument( + output_group.add_argument( "--nsl", "--single-line-exclusions", help="One or more modules to exclude from the single line rule.", dest="single_line_exclusions", action="append", ) - parser.add_argument( - "--sp", - "--settings-path", - "--settings-file", - "--settings", - dest="settings_path", - help="Explicitly set the settings path or file instead of auto determining " - "based on file location.", - ) - parser.add_argument( - "-t", - "--top", - help="Force specific imports to the top of their appropriate section.", - dest="force_to_top", - action="append", - ) - parser.add_argument( + output_group.add_argument( "--tc", "--trailing-comma", dest="include_trailing_comma", action="store_true", help="Includes a trailing comma on multi line imports that include parentheses.", ) - parser.add_argument( + output_group.add_argument( "--up", "--use-parentheses", dest="use_parentheses", @@ -556,38 +589,7 @@ def _build_arg_parser() -> argparse.ArgumentParser: " **NOTE**: This is separate from wrap modes, and only affects how individual lines that " " are too long get continued, not sections of multiple imports.", ) - parser.add_argument( - "-V", - "--version", - action="store_true", - dest="show_version", - help="Displays the currently installed version of isort.", - ) - parser.add_argument( - "-v", - "--verbose", - action="store_true", - dest="verbose", - 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( + output_group.add_argument( "-l", "-w", "--line-length", @@ -596,7 +598,7 @@ def _build_arg_parser() -> argparse.ArgumentParser: dest="line_length", type=int, ) - parser.add_argument( + output_group.add_argument( "--wl", "--wrap-length", dest="wrap_length", @@ -604,80 +606,13 @@ def _build_arg_parser() -> argparse.ArgumentParser: help="Specifies how long lines that are wrapped should be, if not set line_length is used." "\nNOTE: wrap_length must be LOWER than or equal to line_length.", ) - parser.add_argument( - "--ws", - "--ignore-whitespace", - action="store_true", - dest="ignore_whitespace", - help="Tells isort to ignore whitespace differences when --check-only is being used.", - ) - parser.add_argument( + output_group.add_argument( "--case-sensitive", dest="case_sensitive", action="store_true", help="Tells isort to include casing when sorting module names", ) - parser.add_argument( - "--filter-files", - dest="filter_files", - action="store_true", - help="Tells isort to filter files even when they are explicitly passed in as " - "part of the CLI command.", - ) - parser.add_argument( - "files", nargs="*", help="One or more Python source files that need their imports sorted." - ) - parser.add_argument( - "--py", - "--python-version", - action="store", - dest="py_version", - choices=tuple(VALID_PY_TARGETS) + ("auto",), - help="Tells isort to set the known standard library based on the specified Python " - "version. Default is to assume any Python 3 version could be the target, and use a union " - "of all stdlib modules across versions. If auto is specified, the version of the " - "interpreter used to run isort " - f"(currently: {sys.version_info.major}{sys.version_info.minor}) will be used.", - ) - parser.add_argument( - "--profile", - dest="profile", - type=str, - help="Base profile type to use for configuration. " - f"Profiles include: {', '.join(profiles.keys())}. As well as any shared profiles.", - ) - parser.add_argument( - "--interactive", - dest="ask_to_apply", - action="store_true", - help="Tells isort to apply changes interactively.", - ) - parser.add_argument( - "--old-finders", - "--magic-placement", - dest="old_finders", - action="store_true", - help="Use the old deprecated finder logic that relies on environment introspection magic.", - ) - parser.add_argument( - "--show-config", - dest="show_config", - action="store_true", - help="See isort's determined config, as well as sources of config options.", - ) - parser.add_argument( - "--show-files", - dest="show_files", - action="store_true", - help="See the files isort will be ran against with the current config options.", - ) - parser.add_argument( - "--honor-noqa", - dest="honor_noqa", - action="store_true", - help="Tells isort to honor noqa comments to enforce skipping those comments.", - ) - parser.add_argument( + output_group.add_argument( "--remove-redundant-aliases", dest="remove_redundant_aliases", action="store_true", @@ -687,63 +622,44 @@ def _build_arg_parser() -> argparse.ArgumentParser: " aliases to signify intent and change behaviour." ), ) - parser.add_argument( - "--color", - dest="color_output", - action="store_true", - help="Tells isort to use color in terminal output.", - ) - parser.add_argument( - "--float-to-top", - dest="float_to_top", + output_group.add_argument( + "--honor-noqa", + dest="honor_noqa", action="store_true", - help="Causes all non-indented imports to float to the top of the file having its imports " - "sorted (immediately below the top of file comment).\n" - "This can be an excellent shortcut for collecting imports every once in a while " - "when you place them in the middle of a file to avoid context switching.\n\n" - "*NOTE*: It currently doesn't work with cimports and introduces some extra over-head " - "and a performance penalty.", + help="Tells isort to honor noqa comments to enforce skipping those comments.", ) - parser.add_argument( + output_group.add_argument( "--treat-comment-as-code", dest="treat_comments_as_code", action="append", help="Tells isort to treat the specified single line comment(s) as if they are code.", ) - parser.add_argument( + output_group.add_argument( "--treat-all-comment-as-code", dest="treat_all_comments_as_code", action="store_true", help="Tells isort to treat all single line comments as if they are code.", ) - parser.add_argument( + output_group.add_argument( "--formatter", dest="formatter", type=str, help="Specifies the name of a formatting plugin to use when producing output.", ) - parser.add_argument( - "--ext", - "--extension", - "--supported-extension", - dest="supported_extensions", - action="append", - help="Specifies what extensions isort can be ran against.", - ) - parser.add_argument( - "--blocked-extension", - dest="blocked_extensions", - action="append", - help="Specifies what extensions isort can never be ran against.", - ) - parser.add_argument( - "--dedup-headings", - dest="dedup_headings", + output_group.add_argument( + "--color", + dest="color_output", action="store_true", - help="Tells isort to only show an identical custom import heading comment once, even if" - " there are multiple sections with the comment set.", + help="Tells isort to use color in terminal output.", + ) + + section_group.add_argument( + "--sd", + "--section-default", + dest="default_section", + help="Sets the default section for import options: " + str(sections.DEFAULT), ) - parser.add_argument( + section_group.add_argument( "--only-sections", "--os", dest="only_sections", @@ -751,14 +667,44 @@ def _build_arg_parser() -> argparse.ArgumentParser: help="Causes imports to be sorted only based on their sections like STDLIB,THIRDPARTY etc. " "Imports are unaltered and keep their relative positions within the different sections.", ) - parser.add_argument( - "--only-modified", - "--om", - dest="only_modified", + section_group.add_argument( + "--ds", + "--no-sections", + help="Put all imports into the same section bucket", + dest="no_sections", action="store_true", - help="Suppresses verbose output for non-modified files.", ) - parser.add_argument( + section_group.add_argument( + "--fas", + "--force-alphabetical-sort", + action="store_true", + dest="force_alphabetical_sort", + help="Force all imports to be sorted as a single section", + ) + section_group.add_argument( + "--fss", + "--force-sort-within-sections", + action="store_true", + dest="force_sort_within_sections", + help="Don't sort straight-style imports (like import sys) before from-style imports " + "(like from itertools import groupby). Instead, sort the imports by module, " + "independent of import style.", + ) + section_group.add_argument( + "--fass", + "--force-alphabetical-sort-within-sections", + action="store_true", + dest="force_alphabetical_sort_within_sections", + help="Force all imports to be sorted alphabetically within a section", + ) + section_group.add_argument( + "-t", + "--top", + help="Force specific imports to the top of their appropriate section.", + dest="force_to_top", + action="append", + ) + section_group.add_argument( "--combine-straight-imports", "--csi", dest="combine_straight_imports", @@ -766,42 +712,119 @@ def _build_arg_parser() -> argparse.ArgumentParser: help="Combines all the bare straight imports of the same section in a single line. " "Won't work with sections which have 'as' imports", ) - parser.add_argument( - "--dont-follow-links", - dest="dont_follow_links", - action="store_true", - help="Tells isort not to follow symlinks that are encountered when running recursively.", + section_group.add_argument( + "--nlb", + "--no-lines-before", + help="Sections which should not be split with previous by empty lines", + dest="no_lines_before", + action="append", + ) + section_group.add_argument( + "--src", + "--src-path", + dest="src_paths", + action="append", + help="Add an explicitly defined source path " + "(modules within src paths have their imports automatically categorized as first_party).", + ) + section_group.add_argument( + "-b", + "--builtin", + dest="known_standard_library", + action="append", + help="Force isort to recognize a module as part of Python's standard library.", + ) + section_group.add_argument( + "--extra-builtin", + dest="extra_standard_library", + action="append", + help="Extra modules to be included in the list of ones in Python's standard library.", + ) + section_group.add_argument( + "-f", + "--future", + dest="known_future_library", + action="append", + help="Force isort to recognize a module as part of Python's internal future compatibility " + "libraries. WARNING: this overrides the behavior of __future__ handling and therefore" + " can result in code that can't execute. If you're looking to add dependencies such " + "as six a better option is to create a another section below --future using custom " + "sections. See: https://github.com/PyCQA/isort#custom-sections-and-ordering and the " + "discussion here: https://github.com/PyCQA/isort/issues/1463.", + ) + section_group.add_argument( + "-o", + "--thirdparty", + dest="known_third_party", + action="append", + help="Force isort to recognize a module as being part of a third party library.", + ) + section_group.add_argument( + "-p", + "--project", + dest="known_first_party", + action="append", + help="Force isort to recognize a module as being part of the current python project.", + ) + section_group.add_argument( + "--known-local-folder", + dest="known_local_folder", + action="append", + help="Force isort to recognize a module as being a local folder. " + "Generally, this is reserved for relative imports (from . import module).", + ) + section_group.add_argument( + "--virtual-env", + dest="virtual_env", + help="Virtual environment to use for determining whether a package is third-party", + ) + section_group.add_argument( + "--conda-env", + dest="conda_env", + help="Conda environment to use for determining whether a package is third-party", + ) + section_group.add_argument( + "--py", + "--python-version", + action="store", + dest="py_version", + choices=tuple(VALID_PY_TARGETS) + ("auto",), + help="Tells isort to set the known standard library based on the specified Python " + "version. Default is to assume any Python 3 version could be the target, and use a union " + "of all stdlib modules across versions. If auto is specified, the version of the " + "interpreter used to run isort " + f"(currently: {sys.version_info.major}{sys.version_info.minor}) will be used.", ) # deprecated options - parser.add_argument( + deprecated_group.add_argument( "--recursive", dest="deprecated_flags", action="append_const", const="--recursive", help=argparse.SUPPRESS, ) - parser.add_argument( + deprecated_group.add_argument( "-rc", dest="deprecated_flags", action="append_const", const="-rc", help=argparse.SUPPRESS ) - parser.add_argument( + deprecated_group.add_argument( "--dont-skip", dest="deprecated_flags", action="append_const", const="--dont-skip", help=argparse.SUPPRESS, ) - parser.add_argument( + deprecated_group.add_argument( "-ns", dest="deprecated_flags", action="append_const", const="-ns", help=argparse.SUPPRESS ) - parser.add_argument( + deprecated_group.add_argument( "--apply", dest="deprecated_flags", action="append_const", const="--apply", help=argparse.SUPPRESS, ) - parser.add_argument( + deprecated_group.add_argument( "-k", "--keep-direct-and-as", dest="deprecated_flags", From 04766b35855355d4e02fbb0039efced816bada8f Mon Sep 17 00:00:00 2001 From: Tamara Khalbashkeeva Date: Thu, 29 Oct 2020 02:30:09 +0200 Subject: [PATCH 1210/1439] black --- isort/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/main.py b/isort/main.py index 58dfecf77..a795ffac1 100644 --- a/isort/main.py +++ b/isort/main.py @@ -203,7 +203,7 @@ def _build_arg_parser() -> argparse.ArgumentParser: "--help", action="help", default=argparse.SUPPRESS, - help=_('show this help message and exit'), + help=_("show this help message and exit"), ) general_group.add_argument( "-V", From 22f4161c7f2beab321302e07656bdae883674ba3 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 28 Oct 2020 23:55:04 -0700 Subject: [PATCH 1211/1439] Add Tamara (@infinityxxx) to acknowledgements --- docs/contributing/4.-acknowledgements.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index c6b065fde..a3ec7471f 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -209,6 +209,7 @@ Code Contributors - Vasilis Gerakaris (@vgerak) - @tonci-bw - @jaydesl +- Tamara (@infinityxxx) Documenters =================== From 3c7344f4d6991a93039f74732f526f2ec621af73 Mon Sep 17 00:00:00 2001 From: Samuel Gaist Date: Fri, 30 Oct 2020 14:03:10 +0100 Subject: [PATCH 1212/1439] Profile: follow black behaviour with regard to gitignore Fixes #1585 --- isort/profiles.py | 1 + 1 file changed, 1 insertion(+) diff --git a/isort/profiles.py b/isort/profiles.py index cb8cb5688..523b1ec66 100644 --- a/isort/profiles.py +++ b/isort/profiles.py @@ -8,6 +8,7 @@ "use_parentheses": True, "ensure_newline_before_comments": True, "line_length": 88, + "skip_gitignore": True, } django = { "combine_as_imports": True, From 0ce5bfc0594cb9eb741d7d89484042aa7a9638df Mon Sep 17 00:00:00 2001 From: Samuel Gaist Date: Fri, 30 Oct 2020 15:10:36 +0100 Subject: [PATCH 1213/1439] Implement custom output handling in sort_file This will allow the following to be used: isort_diff = StringIO() isort_output = StringIO() isort.file("x.py", show_diff=isort_diff, output=isort_output) Fixes #1583 --- isort/api.py | 110 +++++++++++++++++---------- tests/unit/test_ticketed_features.py | 25 ++++++ 2 files changed, 94 insertions(+), 41 deletions(-) diff --git a/isort/api.py b/isort/api.py index 611187437..502994b66 100644 --- a/isort/api.py +++ b/isort/api.py @@ -284,6 +284,7 @@ def sort_file( ask_to_apply: bool = False, show_diff: Union[bool, TextIO] = False, write_to_stdout: bool = False, + output: Optional[TextIO] = None, **config_kwargs, ) -> bool: """Sorts and formats any groups of imports imports within the provided file or Path. @@ -298,6 +299,8 @@ def sort_file( - **show_diff**: If `True` the changes that need to be done will be printed to stdout, if a TextIO stream is provided results will be written to it, otherwise no diff will be computed. - **write_to_stdout**: If `True`, write to stdout instead of the input file. + - **output**: If a TextIO is provided, results will be written there rather than replacing + the original file content. - ****config_kwargs**: Any config modifications. """ with io.File.read(filename) as source_file: @@ -315,49 +318,74 @@ def sort_file( extension=extension, ) else: - tmp_file = source_file.path.with_suffix(source_file.path.suffix + ".isorted") - try: - with tmp_file.open( - "w", encoding=source_file.encoding, newline="" - ) as output_stream: - shutil.copymode(filename, tmp_file) - changed = sort_stream( - input_stream=source_file.stream, - output_stream=output_stream, - config=config, - file_path=actual_file_path, - disregard_skip=disregard_skip, - extension=extension, - ) + if output is None: + tmp_file = source_file.path.with_suffix(source_file.path.suffix + ".isorted") + try: + with tmp_file.open( + "w", encoding=source_file.encoding, newline="" + ) as output_stream: + shutil.copymode(filename, tmp_file) + changed = sort_stream( + input_stream=source_file.stream, + output_stream=output_stream, + config=config, + file_path=actual_file_path, + disregard_skip=disregard_skip, + extension=extension, + ) + if changed: + if show_diff or ask_to_apply: + source_file.stream.seek(0) + with tmp_file.open( + encoding=source_file.encoding, newline="" + ) as tmp_out: + show_unified_diff( + file_input=source_file.stream.read(), + file_output=tmp_out.read(), + file_path=actual_file_path, + output=None + if show_diff is True + else cast(TextIO, show_diff), + color_output=config.color_output, + ) + if show_diff or ( + ask_to_apply + and not ask_whether_to_apply_changes_to_file( + str(source_file.path) + ) + ): + return False + source_file.stream.close() + tmp_file.replace(source_file.path) + if not config.quiet: + print(f"Fixing {source_file.path}") + finally: + try: # Python 3.8+: use `missing_ok=True` instead of try except. + tmp_file.unlink() + except FileNotFoundError: + pass # pragma: no cover + else: + changed = sort_stream( + input_stream=source_file.stream, + output_stream=output, + config=config, + file_path=actual_file_path, + disregard_skip=disregard_skip, + extension=extension, + ) if changed: - if show_diff or ask_to_apply: + if show_diff: source_file.stream.seek(0) - with tmp_file.open( - encoding=source_file.encoding, newline="" - ) as tmp_out: - show_unified_diff( - file_input=source_file.stream.read(), - file_output=tmp_out.read(), - file_path=actual_file_path, - output=None if show_diff is True else cast(TextIO, show_diff), - color_output=config.color_output, - ) - if show_diff or ( - ask_to_apply - and not ask_whether_to_apply_changes_to_file( - str(source_file.path) - ) - ): - return False - source_file.stream.close() - tmp_file.replace(source_file.path) - if not config.quiet: - print(f"Fixing {source_file.path}") - finally: - try: # Python 3.8+: use `missing_ok=True` instead of try except. - tmp_file.unlink() - except FileNotFoundError: - pass # pragma: no cover + output.seek(0) + show_unified_diff( + file_input=source_file.stream.read(), + file_output=output.read(), + file_path=actual_file_path, + output=None if show_diff is True else cast(TextIO, show_diff), + color_output=config.color_output, + ) + source_file.stream.close() + except ExistingSyntaxErrors: warn(f"{actual_file_path} unable to sort due to existing syntax errors") except IntroducedSyntaxErrors: # pragma: no cover diff --git a/tests/unit/test_ticketed_features.py b/tests/unit/test_ticketed_features.py index 51b130095..050d1c4c7 100644 --- a/tests/unit/test_ticketed_features.py +++ b/tests/unit/test_ticketed_features.py @@ -848,3 +848,28 @@ def my_function(): pass """ ) + + +def test_api_to_allow_custom_diff_and_output_stream_1583(capsys, tmpdir): + """isort should provide a way from the Python API to process an existing + file and output to a stream the new version of that file, as well as a diff + to a different stream. + See: https://github.com/PyCQA/isort/issues/1583 + """ + + tmp_file = tmpdir.join("file.py") + tmp_file.write("import b\nimport a\n") + + isort_diff = StringIO() + isort_output = StringIO() + + isort.file(tmp_file, show_diff=isort_diff, output=isort_output) + + _, error = capsys.readouterr() + assert not error + + isort_diff.seek(0) + assert "+import a\n import b\n-import a\n" in isort_diff.read() + + isort_output.seek(0) + assert isort_output.read() == "import a\nimport b\n" From a663af8c1f40eb524d8a3cb296ac021822f6cf4d Mon Sep 17 00:00:00 2001 From: Akihiro Nitta Date: Sun, 1 Nov 2020 03:41:43 +0900 Subject: [PATCH 1214/1439] Create branch issue/1579 From 6282d29b4ab041db4a9e824e738dfc96a9ec56d7 Mon Sep 17 00:00:00 2001 From: Akihiro Nitta Date: Sun, 1 Nov 2020 04:00:00 +0900 Subject: [PATCH 1215/1439] Update description of --check --- docs/configuration/options.md | 2 +- isort/main.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/configuration/options.md b/docs/configuration/options.md index 289926106..d32e3f2a6 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -1005,7 +1005,7 @@ Combines all the bare straight imports of the same section in a single line. Won ## Check -Checks the file for unsorted / unformatted imports and prints them to the command line without modifying the file. +Checks the file for unsorted / unformatted imports and prints them to the command line without modifying the file. Returns 0 when nothing would change and returns 1 when the file would be reformatted. **Type:** Bool **Default:** `False` diff --git a/isort/main.py b/isort/main.py index a795ffac1..5dd661f41 100644 --- a/isort/main.py +++ b/isort/main.py @@ -281,7 +281,8 @@ def _build_arg_parser() -> argparse.ArgumentParser: action="store_true", dest="check", help="Checks the file for unsorted / unformatted imports and prints them to the " - "command line without modifying the file.", + "command line without modifying the file. Returns 0 when nothing would change and " + "returns 1 when the file would be reformatted.", ) general_group.add_argument( "--ws", From 5472c810943d7d23c3ce016ceb4d9c24084d8e8c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 31 Oct 2020 23:47:56 -0700 Subject: [PATCH 1216/1439] Fix non-idempotent behavior for combine-straight --- isort/output.py | 5 ++--- tests/integration/test_setting_combinations.py | 7 +++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/isort/output.py b/isort/output.py index 66fc44397..45faa6468 100644 --- a/isort/output.py +++ b/isort/output.py @@ -527,9 +527,8 @@ def _with_straight_imports( for module in straight_modules: if module in parsed.categorized_comments["above"]["straight"]: above_comments.extend(parsed.categorized_comments["above"]["straight"].pop(module)) - - for module in parsed.categorized_comments["straight"]: - inline_comments.extend(parsed.categorized_comments["straight"][module]) + if module in parsed.categorized_comments["straight"]: + inline_comments.extend(parsed.categorized_comments["straight"][module]) combined_straight_imports = ", ".join(straight_modules) if inline_comments: diff --git a/tests/integration/test_setting_combinations.py b/tests/integration/test_setting_combinations.py index 929b877a9..7b57199f0 100644 --- a/tests/integration/test_setting_combinations.py +++ b/tests/integration/test_setting_combinations.py @@ -993,6 +993,13 @@ def _raise(*a): ), disregard_skip=True, ) +@hypothesis.example( + config=isort.Config( + py_version="2", + combine_straight_imports=True, + ), + disregard_skip=True, +) @hypothesis.given( config=st.from_type(isort.Config), disregard_skip=st.booleans(), From 4cc84eec18cbc31d82741a5f8bc7d57f59ee8653 Mon Sep 17 00:00:00 2001 From: Timothy Edmund Crosley Date: Sun, 1 Nov 2020 00:03:34 -0700 Subject: [PATCH 1217/1439] Update test_ticketed_features.py Update test to be OS newline agnostic for diff --- tests/unit/test_ticketed_features.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_ticketed_features.py b/tests/unit/test_ticketed_features.py index 050d1c4c7..58a3efcae 100644 --- a/tests/unit/test_ticketed_features.py +++ b/tests/unit/test_ticketed_features.py @@ -869,7 +869,10 @@ def test_api_to_allow_custom_diff_and_output_stream_1583(capsys, tmpdir): assert not error isort_diff.seek(0) - assert "+import a\n import b\n-import a\n" in isort_diff.read() - + isort_diff_content = isort_diff.read() + assert "+import a" in isort_diff_content + assert " import b" in isort_diff_content + assert "-import a" in isort_diff_content + isort_output.seek(0) assert isort_output.read() == "import a\nimport b\n" From d40ca26754c9eed472131a07b96f5d3ba73dd5bb Mon Sep 17 00:00:00 2001 From: Timothy Edmund Crosley Date: Sun, 1 Nov 2020 00:11:37 -0700 Subject: [PATCH 1218/1439] OS agnostic test changes --- tests/unit/test_ticketed_features.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_ticketed_features.py b/tests/unit/test_ticketed_features.py index 58a3efcae..76f08894c 100644 --- a/tests/unit/test_ticketed_features.py +++ b/tests/unit/test_ticketed_features.py @@ -873,6 +873,6 @@ def test_api_to_allow_custom_diff_and_output_stream_1583(capsys, tmpdir): assert "+import a" in isort_diff_content assert " import b" in isort_diff_content assert "-import a" in isort_diff_content - + isort_output.seek(0) - assert isort_output.read() == "import a\nimport b\n" + assert isort_output.read().splitlines() == ["import a", "import b"] From 3424d8310bb3756834d4251597b02f32d9ccd2c8 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 1 Nov 2020 00:30:58 -0700 Subject: [PATCH 1219/1439] Add - Akihiro Nitta (@akihironitta) - Samuel Gaist (@sgaist) to acknowledgements --- docs/contributing/4.-acknowledgements.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index a3ec7471f..48f7d8cf0 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -210,6 +210,8 @@ Code Contributors - @tonci-bw - @jaydesl - Tamara (@infinityxxx) +- Akihiro Nitta (@akihironitta) +- Samuel Gaist (@sgaist) Documenters =================== From 1dc62fff4718d5463b6633591998c45c5943ba2d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 2 Nov 2020 23:46:57 -0800 Subject: [PATCH 1220/1439] Fix #1575: Leading space is removed from wrong line --- isort/core.py | 10 +++++++--- tests/unit/test_ticketed_features.py | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/isort/core.py b/isort/core.py index 42fbec80b..7408c1f1c 100644 --- a/isort/core.py +++ b/isort/core.py @@ -275,7 +275,7 @@ def write(self, *a, **kw): ): cimport_statement = True - if cimport_statement != cimports or (new_indent != indent and import_section): + if cimport_statement != cimports and import_section: if import_section: next_cimports = cimport_statement next_import_section = import_statement @@ -284,8 +284,12 @@ def write(self, *a, **kw): line = "" else: cimports = cimport_statement - - indent = new_indent + else: + if new_indent != indent: + if import_section: + import_statement = import_statement.lstrip() + else: + indent = new_indent import_section += import_statement else: not_imports = True diff --git a/tests/unit/test_ticketed_features.py b/tests/unit/test_ticketed_features.py index 76f08894c..bf760f8d9 100644 --- a/tests/unit/test_ticketed_features.py +++ b/tests/unit/test_ticketed_features.py @@ -876,3 +876,18 @@ def test_api_to_allow_custom_diff_and_output_stream_1583(capsys, tmpdir): isort_output.seek(0) assert isort_output.read().splitlines() == ["import a", "import b"] + + +def test_autofix_mixed_indent_imports_1575(): + """isort should automatically fix import statements that are sent in + with incorrect mixed indentation. + See: https://github.com/PyCQA/isort/issues/1575 + """ + assert isort.code(""" +import os + import os + """) == """ +import os +""" + + From 69f2a353dd54670f2348374e446358e85f3db545 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 3 Nov 2020 23:57:14 -0800 Subject: [PATCH 1221/1439] Fix new_indent behavior --- isort/core.py | 3 ++- tests/unit/test_ticketed_features.py | 13 ++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/isort/core.py b/isort/core.py index 7408c1f1c..28f1010ef 100644 --- a/isort/core.py +++ b/isort/core.py @@ -246,6 +246,7 @@ def write(self, *a, **kw): ): import_section += line elif stripped_line.startswith(IMPORT_START_IDENTIFIERS): + did_contain_imports = contains_imports contains_imports = True new_indent = line[: -len(line.lstrip())] @@ -286,7 +287,7 @@ def write(self, *a, **kw): cimports = cimport_statement else: if new_indent != indent: - if import_section: + if import_section and did_contain_imports: import_statement = import_statement.lstrip() else: indent = new_indent diff --git a/tests/unit/test_ticketed_features.py b/tests/unit/test_ticketed_features.py index bf760f8d9..06b59f5ff 100644 --- a/tests/unit/test_ticketed_features.py +++ b/tests/unit/test_ticketed_features.py @@ -876,18 +876,21 @@ def test_api_to_allow_custom_diff_and_output_stream_1583(capsys, tmpdir): isort_output.seek(0) assert isort_output.read().splitlines() == ["import a", "import b"] - + def test_autofix_mixed_indent_imports_1575(): """isort should automatically fix import statements that are sent in with incorrect mixed indentation. See: https://github.com/PyCQA/isort/issues/1575 """ - assert isort.code(""" + assert ( + isort.code( + """ import os import os - """) == """ + """ + ) + == """ import os """ - - + ) From cd387678824161bcc50d1437d091737a3a80d02a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 4 Nov 2020 00:57:17 -0800 Subject: [PATCH 1222/1439] Fix handling of mix indented imports --- isort/core.py | 9 +++++++-- tests/unit/test_ticketed_features.py | 29 ++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/isort/core.py b/isort/core.py index 28f1010ef..27e615414 100644 --- a/isort/core.py +++ b/isort/core.py @@ -276,7 +276,12 @@ def write(self, *a, **kw): ): cimport_statement = True - if cimport_statement != cimports and import_section: + if cimport_statement != cimports or ( + new_indent != indent + and import_section + and (not did_contain_imports or len(new_indent) < len(indent)) + ): + indent = new_indent if import_section: next_cimports = cimport_statement next_import_section = import_statement @@ -288,7 +293,7 @@ def write(self, *a, **kw): else: if new_indent != indent: if import_section and did_contain_imports: - import_statement = import_statement.lstrip() + import_statement = indent + import_statement.lstrip() else: indent = new_indent import_section += import_statement diff --git a/tests/unit/test_ticketed_features.py b/tests/unit/test_ticketed_features.py index 06b59f5ff..7871e38c5 100644 --- a/tests/unit/test_ticketed_features.py +++ b/tests/unit/test_ticketed_features.py @@ -892,5 +892,34 @@ def test_autofix_mixed_indent_imports_1575(): ) == """ import os +""" + ) + assert ( + isort.code( + """ +def one(): + import os +import os + """ + ) + == """ +def one(): + import os + +import os +""" + ) + assert ( + isort.code( + """ +import os + import os + import os + import os +import os +""" + ) + == """ +import os """ ) From 8c1158b3f472efe47879e72f96e690ea73b1d065 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 10 Nov 2020 23:49:04 -0800 Subject: [PATCH 1223/1439] Fix issue #1593 & Fix issue #1592: isort doesn't work on Python 3.6.0 --- isort/io.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/isort/io.py b/isort/io.py index 7ff2807d2..a002bc89b 100644 --- a/isort/io.py +++ b/isort/io.py @@ -4,14 +4,16 @@ from contextlib import contextmanager from io import BytesIO, StringIO, TextIOWrapper from pathlib import Path -from typing import Callable, Iterator, NamedTuple, TextIO, Union +from typing import Callable, Iterator, TextIO, Union +from isort._future import dataclass from isort.exceptions import UnsupportedEncoding _ENCODING_PATTERN = re.compile(br"^[ \t\f]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)") -class File(NamedTuple): +@dataclass(frozen=True) +class File: stream: TextIO path: Path encoding: str @@ -26,7 +28,11 @@ def detect_encoding(filename: str, readline: Callable[[], bytes]): @staticmethod def from_contents(contents: str, filename: str) -> "File": encoding = File.detect_encoding(filename, BytesIO(contents.encode("utf-8")).readline) - return File(StringIO(contents), path=Path(filename).resolve(), encoding=encoding) + return File( # type: ignore + stream=StringIO(contents), + path=Path(filename).resolve(), + encoding=encoding + ) @property def extension(self): @@ -55,7 +61,7 @@ def read(filename: Union[str, Path]) -> Iterator["File"]: stream = None try: stream = File._open(file_path) - yield File(stream=stream, path=file_path, encoding=stream.encoding) + yield File(stream=stream, path=file_path, encoding=stream.encoding) # type: ignore finally: if stream is not None: stream.close() From 66ba8c6e43b723e6748e30149cfb640c0e42bd6f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 11 Nov 2020 00:11:00 -0800 Subject: [PATCH 1224/1439] black formatting --- isort/io.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/isort/io.py b/isort/io.py index a002bc89b..2f30be0c7 100644 --- a/isort/io.py +++ b/isort/io.py @@ -29,9 +29,7 @@ def detect_encoding(filename: str, readline: Callable[[], bytes]): def from_contents(contents: str, filename: str) -> "File": encoding = File.detect_encoding(filename, BytesIO(contents.encode("utf-8")).readline) return File( # type: ignore - stream=StringIO(contents), - path=Path(filename).resolve(), - encoding=encoding + stream=StringIO(contents), path=Path(filename).resolve(), encoding=encoding ) @property From b04ff9fb8f4098bc4498d60a05e19c711a0524d9 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 12 Nov 2020 23:52:28 -0800 Subject: [PATCH 1225/1439] Implemented #1596: Provide ways for extension formatting and file paths to be specified when using streaming input from CLI. --- CHANGELOG.md | 3 +++ isort/main.py | 23 ++++++++++++++++ tests/unit/test_main.py | 60 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a94901a0c..524f4ea56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. Find out more about isort's release policy [here](https://pycqa.github.io/isort/docs/major_releases/release_policy/). +### 5.7.0 TBD + - Implemented #1596: Provide ways for extension formatting and file paths to be specified when using streaming input from CLI. + ### 5.6.4 October 12, 2020 - Fixed #1556: Empty line added between imports that should be skipped. diff --git a/isort/main.py b/isort/main.py index 5dd661f41..8e7d0e017 100644 --- a/isort/main.py +++ b/isort/main.py @@ -383,6 +383,11 @@ def _build_arg_parser() -> argparse.ArgumentParser: action="store_true", help="Tells isort not to follow symlinks that are encountered when running recursively.", ) + target_group.add_argument( + "--filename", + dest="filename", + help="Provide the filename associated with a stream.", + ) output_group.add_argument( "-a", @@ -653,6 +658,11 @@ def _build_arg_parser() -> argparse.ArgumentParser: action="store_true", help="Tells isort to use color in terminal output.", ) + output_group.add_argument( + "--ext-format", + dest="ext_format", + help="Tells isort to format the given files according to an extensions formatting rules.", + ) section_group.add_argument( "--sd", @@ -938,6 +948,8 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = write_to_stdout = config_dict.pop("write_to_stdout", False) deprecated_flags = config_dict.pop("deprecated_flags", False) remapped_deprecated_args = config_dict.pop("remapped_deprecated_args", False) + stream_filename = config_dict.pop("filename", None) + ext_format = config_dict.pop("ext_format", None) wrong_sorted_files = False all_attempt_broken = False no_valid_encodings = False @@ -952,6 +964,7 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = print(json.dumps(config.__dict__, indent=4, separators=(",", ": "), default=_preconvert)) return elif file_names == ["-"]: + file_path = Path(stream_filename) if stream_filename else None if show_files: sys.exit("Error: can't show files for streaming input.") @@ -960,6 +973,8 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = input_stream=sys.stdin if stdin is None else stdin, config=config, show_diff=show_diff, + file_path=file_path, + extension=ext_format, ) wrong_sorted_files = incorrectly_sorted @@ -969,8 +984,14 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = output_stream=sys.stdout, config=config, show_diff=show_diff, + file_path=file_path, + extension=ext_format, ) else: + if stream_filename: + printer = create_terminal_printer(color=config.color_output) + printer.error("Filename override is intended only for stream (-) sorting.") + sys.exit(1) skipped: List[str] = [] broken: List[str] = [] @@ -1005,6 +1026,7 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = check=check, ask_to_apply=ask_to_apply, write_to_stdout=write_to_stdout, + extension=ext_format, ), file_names, ) @@ -1018,6 +1040,7 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = ask_to_apply=ask_to_apply, show_diff=show_diff, write_to_stdout=write_to_stdout, + extension=ext_format, ) for file_name in file_names ) diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 70eafb3ad..08f274630 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -348,6 +348,66 @@ def test_isort_command(): assert main.ISortCommand +def test_isort_filename_overrides(tmpdir, capsys): + """Tests isorts available approaches for overriding filename and extension based behavior""" + input_text = """ +import b +import a + +def function(): + pass +""" + + def build_input_content(): + return UnseekableTextIOWrapper(BytesIO(input_text.encode("utf8"))) + + main.main(["-"], stdin=build_input_content()) + out, error = capsys.readouterr() + assert not error + assert out == ( + """ +import a +import b + + +def function(): + pass +""" + ) + + main.main(["-", "--ext-format", "pyi"], stdin=build_input_content()) + out, error = capsys.readouterr() + assert not error + assert out == ( + """ +import a +import b + +def function(): + pass +""" + ) + + tmp_file = tmpdir.join("tmp.pyi") + tmp_file.write_text(input_text, encoding="utf8") + main.main(["-", "--filename", str(tmp_file)], stdin=build_input_content()) + out, error = capsys.readouterr() + assert not error + assert out == ( + """ +import a +import b + +def function(): + pass +""" + ) + + # setting a filename override when file is passed in as non-stream is not supported. + with pytest.raises(SystemExit): + main.main([str(tmp_file), "--filename", str(tmp_file)], stdin=build_input_content()) + + def test_isort_with_stdin(capsys): # ensures that isort sorts stdin without any flags From 7c2cb61e7ce2200a99f6852532e1f40f502d4e2c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 13 Nov 2020 00:02:04 -0800 Subject: [PATCH 1226/1439] Fix integration test to skip unsorted files --- tests/integration/test_projects_using_isort.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/test_projects_using_isort.py b/tests/integration/test_projects_using_isort.py index 34540cbba..2a2abe398 100644 --- a/tests/integration/test_projects_using_isort.py +++ b/tests/integration/test_projects_using_isort.py @@ -61,7 +61,7 @@ def test_habitat_lab(tmpdir): def test_tmuxp(tmpdir): git_clone("https://github.com/tmux-python/tmuxp.git", tmpdir) - run_isort([str(tmpdir)]) + run_isort([str(tmpdir), "--skip", "cli.py", "--skip", "test_workspacebuilder.py"]) def test_websockets(tmpdir): From 41fa2902f62eb8f30561065a8105da4bf4a5372d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 13 Nov 2020 23:15:41 -0800 Subject: [PATCH 1227/1439] Update configuration docs --- docs/configuration/options.md | 65 +++++++++++++++++++++++++++++++---- 1 file changed, 58 insertions(+), 7 deletions(-) diff --git a/docs/configuration/options.md b/docs/configuration/options.md index bf2205243..4a58573d3 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -179,7 +179,7 @@ Force isort to recognize a module as being a local folder. Generally, this is re Force isort to recognize a module as part of Python's standard library. **Type:** Frozenset -**Default:** `('_dummy_thread', '_thread', 'abc', 'aifc', 'argparse', 'array', 'ast', 'asynchat', 'asyncio', 'asyncore', 'atexit', 'audioop', 'base64', 'bdb', 'binascii', 'binhex', 'bisect', 'builtins', 'bz2', 'cProfile', 'calendar', 'cgi', 'cgitb', 'chunk', 'cmath', 'cmd', 'code', 'codecs', 'codeop', 'collections', 'colorsys', 'compileall', 'concurrent', 'configparser', 'contextlib', 'contextvars', 'copy', 'copyreg', 'crypt', 'csv', 'ctypes', 'curses', 'dataclasses', 'datetime', 'dbm', 'decimal', 'difflib', 'dis', 'distutils', 'doctest', 'dummy_threading', 'email', 'encodings', 'ensurepip', 'enum', 'errno', 'faulthandler', 'fcntl', 'filecmp', 'fileinput', 'fnmatch', 'formatter', 'fpectl', 'fractions', 'ftplib', 'functools', 'gc', 'getopt', 'getpass', 'gettext', 'glob', 'grp', 'gzip', 'hashlib', 'heapq', 'hmac', 'html', 'http', 'imaplib', 'imghdr', 'imp', 'importlib', 'inspect', 'io', 'ipaddress', 'itertools', 'json', 'keyword', 'lib2to3', 'linecache', 'locale', 'logging', 'lzma', 'macpath', 'mailbox', 'mailcap', 'marshal', 'math', 'mimetypes', 'mmap', 'modulefinder', 'msilib', 'msvcrt', 'multiprocessing', 'netrc', 'nis', 'nntplib', 'ntpath', 'numbers', 'operator', 'optparse', 'os', 'ossaudiodev', 'parser', 'pathlib', 'pdb', 'pickle', 'pickletools', 'pipes', 'pkgutil', 'platform', 'plistlib', 'poplib', 'posix', 'posixpath', 'pprint', 'profile', 'pstats', 'pty', 'pwd', 'py_compile', 'pyclbr', 'pydoc', 'queue', 'quopri', 'random', 're', 'readline', 'reprlib', 'resource', 'rlcompleter', 'runpy', 'sched', 'secrets', 'select', 'selectors', 'shelve', 'shlex', 'shutil', 'signal', 'site', 'smtpd', 'smtplib', 'sndhdr', 'socket', 'socketserver', 'spwd', 'sqlite3', 'sre', 'sre_compile', 'sre_constants', 'sre_parse', 'ssl', 'stat', 'statistics', 'string', 'stringprep', 'struct', 'subprocess', 'sunau', 'symbol', 'symtable', 'sys', 'sysconfig', 'syslog', 'tabnanny', 'tarfile', 'telnetlib', 'tempfile', 'termios', 'test', 'textwrap', 'threading', 'time', 'timeit', 'tkinter', 'token', 'tokenize', 'trace', 'traceback', 'tracemalloc', 'tty', 'turtle', 'turtledemo', 'types', 'typing', 'unicodedata', 'unittest', 'urllib', 'uu', 'uuid', 'venv', 'warnings', 'wave', 'weakref', 'webbrowser', 'winreg', 'winsound', 'wsgiref', 'xdrlib', 'xml', 'xmlrpc', 'zipapp', 'zipfile', 'zipimport', 'zlib')` +**Default:** `('_dummy_thread', '_thread', 'abc', 'aifc', 'argparse', 'array', 'ast', 'asynchat', 'asyncio', 'asyncore', 'atexit', 'audioop', 'base64', 'bdb', 'binascii', 'binhex', 'bisect', 'builtins', 'bz2', 'cProfile', 'calendar', 'cgi', 'cgitb', 'chunk', 'cmath', 'cmd', 'code', 'codecs', 'codeop', 'collections', 'colorsys', 'compileall', 'concurrent', 'configparser', 'contextlib', 'contextvars', 'copy', 'copyreg', 'crypt', 'csv', 'ctypes', 'curses', 'dataclasses', 'datetime', 'dbm', 'decimal', 'difflib', 'dis', 'distutils', 'doctest', 'dummy_threading', 'email', 'encodings', 'ensurepip', 'enum', 'errno', 'faulthandler', 'fcntl', 'filecmp', 'fileinput', 'fnmatch', 'formatter', 'fpectl', 'fractions', 'ftplib', 'functools', 'gc', 'getopt', 'getpass', 'gettext', 'glob', 'graphlib', 'grp', 'gzip', 'hashlib', 'heapq', 'hmac', 'html', 'http', 'imaplib', 'imghdr', 'imp', 'importlib', 'inspect', 'io', 'ipaddress', 'itertools', 'json', 'keyword', 'lib2to3', 'linecache', 'locale', 'logging', 'lzma', 'macpath', 'mailbox', 'mailcap', 'marshal', 'math', 'mimetypes', 'mmap', 'modulefinder', 'msilib', 'msvcrt', 'multiprocessing', 'netrc', 'nis', 'nntplib', 'ntpath', 'numbers', 'operator', 'optparse', 'os', 'ossaudiodev', 'parser', 'pathlib', 'pdb', 'pickle', 'pickletools', 'pipes', 'pkgutil', 'platform', 'plistlib', 'poplib', 'posix', 'posixpath', 'pprint', 'profile', 'pstats', 'pty', 'pwd', 'py_compile', 'pyclbr', 'pydoc', 'queue', 'quopri', 'random', 're', 'readline', 'reprlib', 'resource', 'rlcompleter', 'runpy', 'sched', 'secrets', 'select', 'selectors', 'shelve', 'shlex', 'shutil', 'signal', 'site', 'smtpd', 'smtplib', 'sndhdr', 'socket', 'socketserver', 'spwd', 'sqlite3', 'sre', 'sre_compile', 'sre_constants', 'sre_parse', 'ssl', 'stat', 'statistics', 'string', 'stringprep', 'struct', 'subprocess', 'sunau', 'symbol', 'symtable', 'sys', 'sysconfig', 'syslog', 'tabnanny', 'tarfile', 'telnetlib', 'tempfile', 'termios', 'test', 'textwrap', 'threading', 'time', 'timeit', 'tkinter', 'token', 'tokenize', 'trace', 'traceback', 'tracemalloc', 'tty', 'turtle', 'turtledemo', 'types', 'typing', 'unicodedata', 'unittest', 'urllib', 'uu', 'uuid', 'venv', 'warnings', 'wave', 'weakref', 'webbrowser', 'winreg', 'winsound', 'wsgiref', 'xdrlib', 'xml', 'xmlrpc', 'zipapp', 'zipfile', 'zipimport', 'zlib', 'zoneinfo')` **Python & Config File Name:** known_standard_library **CLI Flags:** @@ -296,7 +296,7 @@ Sort imports by their string length. ## Length Sort Straight -Sort straight imports by their string length. +Sort straight imports by their string length. Similar to `length_sort` but applies only to straight imports and doesn't affect from imports. **Type:** Bool **Default:** `False` @@ -602,7 +602,7 @@ Force all imports to be sorted as a single section ## Force Grid Wrap -Force number of from imports (defaults to 2 when passed as CLI flag without value) to be grid wrapped regardless of line length. If 0 is passed in (the global default) only line length is considered. +Force number of from imports (defaults to 2 when passed as CLI flag without value)to be grid wrapped regardless of line length. If 0 is passed in (the global default) only line length is considered. **Type:** Int **Default:** `0` @@ -633,6 +633,15 @@ Don't sort straight-style imports (like import sys) before from-style imports (l **Python & Config File Name:** lexicographical **CLI Flags:** **Not Supported** +## Group By Package + +**No Description** + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** group_by_package +**CLI Flags:** **Not Supported** + ## Ignore Whitespace Tells isort to ignore whitespace differences when --check-only is being used. @@ -767,8 +776,8 @@ Tells isort to honor noqa comments to enforce skipping those comments. Add an explicitly defined source path (modules within src paths have their imports automatically categorized as first_party). -**Type:** Frozenset -**Default:** `frozenset()` +**Type:** Tuple +**Default:** `()` **Python & Config File Name:** src_paths **CLI Flags:** @@ -800,7 +809,8 @@ Tells isort to remove redundant aliases from imports, such as `import os as os`. ## Float To Top -Causes all non-indented imports to float to the top of the file having its imports sorted. It can be an excellent shortcut for collecting imports every once in a while when you place them in the middle of a file to avoid context switching. +Causes all non-indented imports to float to the top of the file having its imports sorted (immediately below the top of file comment). +This can be an excellent shortcut for collecting imports every once in a while when you place them in the middle of a file to avoid context switching. *NOTE*: It currently doesn't work with cimports and introduces some extra over-head and a performance penalty. @@ -880,7 +890,7 @@ Tells isort to treat all single line comments as if they are code. Specifies what extensions isort can be ran against. **Type:** Frozenset -**Default:** `('py', 'pyi', 'pyx')` +**Default:** `('pxd', 'py', 'pyi', 'pyx')` **Python & Config File Name:** supported_extensions **CLI Flags:** @@ -949,6 +959,36 @@ Causes imports to be sorted only based on their sections like STDLIB,THIRDPARTY - --only-sections - --os +## Only Modified + +Suppresses verbose output for non-modified files. + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** only_modified +**CLI Flags:** + +- --only-modified +- --om + +## Auto Identify Namespace Packages + +**No Description** + +**Type:** Bool +**Default:** `True` +**Python & Config File Name:** auto_identify_namespace_packages +**CLI Flags:** **Not Supported** + +## Namespace Packages + +**No Description** + +**Type:** Frozenset +**Default:** `frozenset()` +**Python & Config File Name:** namespace_packages +**CLI Flags:** **Not Supported** + ## Check Checks the file for unsorted / unformatted imports and prints them to the command line without modifying the file. @@ -1090,6 +1130,17 @@ See isort's determined config, as well as sources of config options. - --show-config +## Show Files + +See the files isort will be ran against with the current config options. + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** **Not Supported** +**CLI Flags:** + +- --show-files + ## Deprecated Flags ==SUPPRESS== From 35f2f8f50e3e0f992bb44198a2902ce8533ad9a1 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 14 Nov 2020 00:49:19 -0800 Subject: [PATCH 1228/1439] Update config option docs --- docs/configuration/options.md | 148 +++++++++++++++++++++------------- 1 file changed, 94 insertions(+), 54 deletions(-) diff --git a/docs/configuration/options.md b/docs/configuration/options.md index d32e3f2a6..e6d0e8058 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -108,9 +108,7 @@ Forces line endings to the specified value. If not set, values will be guessed p ## Sections -Specifies a custom ordering for sections. Any custom defined sections should also be -included in this ordering. Omitting any of the default sections from this tuple may -result in unexpected sorting or an exception being raised. +**No Description** **Type:** Tuple **Default:** `('FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER')` @@ -343,7 +341,7 @@ Removes the specified import from all files. ## Append Only -Only adds the imports specified in --add-imports if the file contains existing imports. +Only adds the imports specified in --add-import if the file contains existing imports. **Type:** Bool **Default:** `False` @@ -1003,18 +1001,44 @@ Combines all the bare straight imports of the same section in a single line. Won **Python & Config File Name:** namespace_packages **CLI Flags:** **Not Supported** -## Check +## Follow Links -Checks the file for unsorted / unformatted imports and prints them to the command line without modifying the file. Returns 0 when nothing would change and returns 1 when the file would be reformatted. +**No Description** + +**Type:** Bool +**Default:** `True` +**Python & Config File Name:** follow_links +**CLI Flags:** **Not Supported** + +## Show Version + +Displays the currently installed version of isort. **Type:** Bool **Default:** `False` **Python & Config File Name:** **Not Supported** **CLI Flags:** -- -c -- --check-only -- --check +- -V +- --version + +**Examples:** + +### Example cli usage + +`isort --version` + +## Version Number + +Returns just the current version number without the logo + +**Type:** String +**Default:** `==SUPPRESS==` +**Python & Config File Name:** **Not Supported** +**CLI Flags:** + +- --vn +- --version-number ## Write To Stdout @@ -1028,44 +1052,52 @@ Force resulting output to stdout, instead of in-place. - -d - --stdout -## Show Diff +## Show Config -Prints a diff of all the changes isort would make to a file, instead of changing it in place +See isort's determined config, as well as sources of config options. **Type:** Bool **Default:** `False` **Python & Config File Name:** **Not Supported** **CLI Flags:** -- --df -- --diff +- --show-config -## Jobs +## Show Files -Number of files to process in parallel. +See the files isort will be ran against with the current config options. -**Type:** Int -**Default:** `None` +**Type:** Bool +**Default:** `False` **Python & Config File Name:** **Not Supported** **CLI Flags:** -- -j -- --jobs +- --show-files -## Dont Order By Type +## Show Diff -Don't order imports by type, which is determined by case, in addition to alphabetically. +Prints a diff of all the changes isort would make to a file, instead of changing it in place -**NOTE**: type here refers to the implied type from the import name capitalization. - isort does not do type introspection for the imports. These "types" are simply: CONSTANT_VARIABLE, CamelCaseClass, variable_or_function. If your project follows PEP8 or a related coding standard and has many imports this is a good default. You can turn this on from the CLI using `--order-by-type`. +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** **Not Supported** +**CLI Flags:** + +- --df +- --diff + +## Check + +Checks the file for unsorted / unformatted imports and prints them to the command line without modifying the file. Returns 0 when nothing would change and returns 1 when the file would be reformatted. **Type:** Bool **Default:** `False` **Python & Config File Name:** **Not Supported** **CLI Flags:** -- --dt -- --dont-order-by-type +- -c +- --check-only +- --check ## Settings Path @@ -1081,35 +1113,28 @@ Explicitly set the settings path or file instead of auto determining based on fi - --settings-file - --settings -## Show Version +## Jobs -Displays the currently installed version of isort. +Number of files to process in parallel. -**Type:** Bool -**Default:** `False` +**Type:** Int +**Default:** `None` **Python & Config File Name:** **Not Supported** **CLI Flags:** -- -V -- --version - -**Examples:** - -### Example cli usage - -`isort --version` +- -j +- --jobs -## Version Number +## Ask To Apply -Returns just the current version number without the logo +Tells isort to apply changes interactively. -**Type:** String -**Default:** `==SUPPRESS==` +**Type:** Bool +**Default:** `False` **Python & Config File Name:** **Not Supported** **CLI Flags:** -- --vn -- --version-number +- --interactive ## Files @@ -1122,38 +1147,53 @@ One or more Python source files that need their imports sorted. - -## Ask To Apply +## Dont Follow Links -Tells isort to apply changes interactively. +Tells isort not to follow symlinks that are encountered when running recursively. **Type:** Bool **Default:** `False` **Python & Config File Name:** **Not Supported** **CLI Flags:** -- --interactive +- --dont-follow-links -## Show Config +## Filename -See isort's determined config, as well as sources of config options. +Provide the filename associated with a stream. -**Type:** Bool -**Default:** `False` +**Type:** String +**Default:** `None` **Python & Config File Name:** **Not Supported** **CLI Flags:** -- --show-config +- --filename -## Show Files +## Dont Order By Type -See the files isort will be ran against with the current config options. +Don't order imports by type, which is determined by case, in addition to alphabetically. + +**NOTE**: type here refers to the implied type from the import name capitalization. + isort does not do type introspection for the imports. These "types" are simply: CONSTANT_VARIABLE, CamelCaseClass, variable_or_function. If your project follows PEP8 or a related coding standard and has many imports this is a good default. You can turn this on from the CLI using `--order-by-type`. **Type:** Bool **Default:** `False` **Python & Config File Name:** **Not Supported** **CLI Flags:** -- --show-files +- --dt +- --dont-order-by-type + +## Ext Format + +Tells isort to format the given files according to an extensions formatting rules. + +**Type:** String +**Default:** `None` +**Python & Config File Name:** **Not Supported** +**CLI Flags:** + +- --ext-format ## Deprecated Flags From 6644bd6434f4d2c17260ac5cb2205614f15843c1 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 15 Nov 2020 23:59:02 -0800 Subject: [PATCH 1229/1439] Add Implemented #1583 to changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 524f4ea56..99a37c4d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ ### 5.7.0 TBD - Implemented #1596: Provide ways for extension formatting and file paths to be specified when using streaming input from CLI. + - Implemented #1583: Ability to output and diff within a single API call to isort.file. ### 5.6.4 October 12, 2020 - Fixed #1556: Empty line added between imports that should be skipped. From 6ed25c68c738405229faf8664c54cd7f020154e9 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 24 Nov 2020 23:51:14 -0800 Subject: [PATCH 1230/1439] Update changelog --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99a37c4d9..e2b4f92a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,10 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ ### 5.7.0 TBD - Implemented #1596: Provide ways for extension formatting and file paths to be specified when using streaming input from CLI. - - Implemented #1583: Ability to output and diff within a single API call to isort.file. + - Implemented #1583: Ability to output and diff within a single API call to `isort.file`. + - Implemented #1562, #1592 & #1593: Better more useful fatal error messages. + - Implemented #1575: Support for automatically fixing mixed indentation of import sections. + - Implemented #1582: Added a CLI option for skipping symlinks. ### 5.6.4 October 12, 2020 - Fixed #1556: Empty line added between imports that should be skipped. From e9b1bd4e126327bc415b26d1308da5a2cdcdf51e Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 28 Nov 2020 23:39:11 -0800 Subject: [PATCH 1231/1439] Implemented #1603: Support for disabling float_to_top from the command line. --- .isort.cfg | 1 + CHANGELOG.md | 1 + isort/main.py | 12 +++++++++++ tests/unit/test_main.py | 47 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 61 insertions(+) diff --git a/.isort.cfg b/.isort.cfg index 567d1abd6..9ab265a44 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -2,3 +2,4 @@ profile=hug src_paths=isort,test skip=tests/unit/example_crlf_file.py +float_to_top=true diff --git a/CHANGELOG.md b/CHANGELOG.md index e2b4f92a0..6e81a6877 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ - Implemented #1562, #1592 & #1593: Better more useful fatal error messages. - Implemented #1575: Support for automatically fixing mixed indentation of import sections. - Implemented #1582: Added a CLI option for skipping symlinks. + - Implemented #1603: Support for disabling float_to_top from the command line. ### 5.6.4 October 12, 2020 - Fixed #1556: Empty line added between imports that should be skipped. diff --git a/isort/main.py b/isort/main.py index 8e7d0e017..cdf9ba00d 100644 --- a/isort/main.py +++ b/isort/main.py @@ -430,6 +430,12 @@ def _build_arg_parser() -> argparse.ArgumentParser: "*NOTE*: It currently doesn't work with cimports and introduces some extra over-head " "and a performance penalty.", ) + output_group.add_argument( + "--dont-float-to-top", + dest="dont_float_to_top", + action="store_true", + help="Forces --float-to-top setting off. See --float-to-top for more information.", + ) output_group.add_argument( "--ca", "--combine-as", @@ -864,6 +870,12 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: del arguments["dont_order_by_type"] if "dont_follow_links" in arguments: arguments["follow_links"] = False + if "dont_float_to_top" in arguments: + del arguments["dont_float_to_top"] + if arguments.get("float_to_top", False): + sys.exit("Can't set both --float-to-top and --dont-float-to-top.") + else: + arguments["float_to_top"] = False multi_line_output = arguments.get("multi_line_output", None) if multi_line_output: if multi_line_output.isdigit(): diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 08f274630..4af4d58c2 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -408,6 +408,53 @@ def function(): main.main([str(tmp_file), "--filename", str(tmp_file)], stdin=build_input_content()) +def test_isort_float_to_top_overrides(tmpdir, capsys): + """Tests isorts supports overriding float to top from CLI""" + test_input = """ +import b + + +def function(): + pass + + +import a +""" + config_file = tmpdir.join(".isort.cfg") + config_file.write( + """ +[settings] +float_to_top=True +""" + ) + python_file = tmpdir.join("file.py") + python_file.write(test_input) + + main.main([str(python_file)]) + out, error = capsys.readouterr() + assert not error + assert "Fixing" in out + assert python_file.read_text(encoding="utf8") == ( + """ +import a +import b + + +def function(): + pass +""" + ) + + python_file.write(test_input) + main.main([str(python_file), "--dont-float-to-top"]) + _, error = capsys.readouterr() + assert not error + assert python_file.read_text(encoding="utf8") == test_input + + with pytest.raises(SystemExit): + main.main([str(python_file), "--float-to-top", "--dont-float-to-top"]) + + def test_isort_with_stdin(capsys): # ensures that isort sorts stdin without any flags From 0b42e54ff33b2f99d49e1fdef7b1aa281ed0dd29 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 29 Nov 2020 23:39:45 -0800 Subject: [PATCH 1232/1439] Formatting fixes --- .isort.cfg | 2 +- tests/unit/test_main.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.isort.cfg b/.isort.cfg index 9ab265a44..545be9799 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -2,4 +2,4 @@ profile=hug src_paths=isort,test skip=tests/unit/example_crlf_file.py -float_to_top=true + diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 4af4d58c2..ac4a8e4e8 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -444,13 +444,13 @@ def function(): pass """ ) - + python_file.write(test_input) main.main([str(python_file), "--dont-float-to-top"]) _, error = capsys.readouterr() assert not error assert python_file.read_text(encoding="utf8") == test_input - + with pytest.raises(SystemExit): main.main([str(python_file), "--float-to-top", "--dont-float-to-top"]) From 940d74abbb08a1a8d3e86d94d38d655201b590ba Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 30 Nov 2020 21:29:15 -0800 Subject: [PATCH 1233/1439] Remove spaces in blank line --- tests/unit/test_main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index ac4a8e4e8..84ee76531 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -417,7 +417,7 @@ def test_isort_float_to_top_overrides(tmpdir, capsys): def function(): pass - + import a """ config_file = tmpdir.join(".isort.cfg") From dc6020b2ffb6a1cccca8a16108ca7984a4aa7841 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 1 Dec 2020 21:30:27 -0800 Subject: [PATCH 1234/1439] Improve test coverage, add test for dont follow links --- isort/main.py | 1 + tests/unit/test_main.py | 1 + 2 files changed, 2 insertions(+) diff --git a/isort/main.py b/isort/main.py index cdf9ba00d..c16fdbd60 100644 --- a/isort/main.py +++ b/isort/main.py @@ -870,6 +870,7 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: del arguments["dont_order_by_type"] if "dont_follow_links" in arguments: arguments["follow_links"] = False + del arguments["dont_follow_links"] if "dont_float_to_top" in arguments: del arguments["dont_float_to_top"] if arguments.get("float_to_top", False): diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 84ee76531..65f0dae42 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -80,6 +80,7 @@ def test_parse_args(): assert main.parse_args(["--only-modified"]) == {"only_modified": True} assert main.parse_args(["--csi"]) == {"combine_straight_imports": True} assert main.parse_args(["--combine-straight-imports"]) == {"combine_straight_imports": True} + assert main.parse_args(["--dont-follow-links"]) == {"follow_links": False} def test_ascii_art(capsys): From 3d2264b9ca1761b715cb985a4d940201bd257ca3 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 1 Dec 2020 21:47:54 -0800 Subject: [PATCH 1235/1439] Improve code coverage: identify imports stream --- isort/main.py | 6 ++++-- tests/unit/test_main.py | 9 +++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/isort/main.py b/isort/main.py index c16fdbd60..5352f64f8 100644 --- a/isort/main.py +++ b/isort/main.py @@ -899,7 +899,9 @@ def _preconvert(item): raise TypeError("Unserializable object {} of type {}".format(item, type(item))) -def identify_imports_main(argv: Optional[Sequence[str]] = None) -> None: +def identify_imports_main( + argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = None +) -> None: parser = argparse.ArgumentParser( description="Get all import definitions from a given file." "Use `-` as the first argument to represent stdin." @@ -909,7 +911,7 @@ def identify_imports_main(argv: Optional[Sequence[str]] = None) -> None: file_name = arguments.file if file_name == "-": - api.get_imports_stream(sys.stdin, sys.stdout) + api.get_imports_stream(sys.stdin if stdin is None else stdin, sys.stdout) else: if os.path.isdir(file_name): sys.exit("Path must be a file, not a directory") diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 65f0dae42..dd2afddaa 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -20,6 +20,10 @@ def seek(self, *args, **kwargs): raise ValueError("underlying stream is not seekable") +def as_stream(text: str) -> UnseekableTextIOWrapper: + return UnseekableTextIOWrapper(BytesIO(text.encode("utf8"))) + + @given( file_name=st.text(), config=st.builds(Config), @@ -1035,3 +1039,8 @@ def test_identify_imports_main(tmpdir, capsys): out, error = capsys.readouterr() assert out == file_imports.replace("\n", os.linesep) + assert not error + + main.identify_imports_main(["-"], stdin=as_stream(file_content)) + out, error = capsys.readouterr() + assert out == file_imports.replace("\n", os.linesep) From 402df716d4673bac2fdd09e332958df5d4e9c80b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 2 Dec 2020 22:49:53 -0800 Subject: [PATCH 1236/1439] Updates to black_compatability page --- ...bility_black.md => black_compatibility.md} | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) rename docs/configuration/{compatibility_black.md => black_compatibility.md} (50%) diff --git a/docs/configuration/compatibility_black.md b/docs/configuration/black_compatibility.md similarity index 50% rename from docs/configuration/compatibility_black.md rename to docs/configuration/black_compatibility.md index 4afa2459e..ff88ff2dc 100644 --- a/docs/configuration/compatibility_black.md +++ b/docs/configuration/black_compatibility.md @@ -1,12 +1,25 @@ Compatibility with black ======== -black and isort sometimes don't agree on some rules. Although you can configure isort to behave nicely with black. +Compatibility with black is very important to the isort project and comes baked in starting with version 5. +All that's required to use isort alongside black is to set the isort profile to "black". +## Using a config file (such as .isort.cfg) -## Basic compatibility +For projects that officially use both isort and black, we recommend setting the black profile in a config file at the root of your project's repository. +This way independent to how users call isort (pre-commit, CLI, or editor integration) the black profile will automatically be applied. -Use the profile option while using isort, `isort --profile black`. +```ini +[tool.isort] +profile = "black" +multi_line_output = 3 +``` + +Read More about supported [config files](https://pycqa.github.io/isort/docs/configuration/config_files/). + +## CLI + +To use the profile option when calling isort directly from the commandline simply add the --profile black argument: `isort --profile black`. A demo of how this would look like in your _.travis.yml_ @@ -34,7 +47,7 @@ See [built-in profiles](https://pycqa.github.io/isort/docs/configuration/profile ## Integration with pre-commit -isort can be easily used with your pre-commit hooks. +You can also set the profile directly when integrating isort within pre-commit. ```yaml - repo: https://github.com/pycqa/isort @@ -44,14 +57,3 @@ isort can be easily used with your pre-commit hooks. args: ["--profile", "black"] ``` -## Using a config file (.isort.cfg) - -The easiest way to configure black with isort is to use a config file. - -```ini -[tool.isort] -profile = "black" -multi_line_output = 3 -``` - -Read More about supported [config files](https://pycqa.github.io/isort/docs/configuration/config_files/). \ No newline at end of file From 1c45f49fefe7e2d4474cb804690c9f3d477f8234 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 2 Dec 2020 22:50:59 -0800 Subject: [PATCH 1237/1439] Ensure filter-files is in pre-commit example --- docs/configuration/black_compatibility.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration/black_compatibility.md b/docs/configuration/black_compatibility.md index ff88ff2dc..35f6812bf 100644 --- a/docs/configuration/black_compatibility.md +++ b/docs/configuration/black_compatibility.md @@ -54,6 +54,6 @@ You can also set the profile directly when integrating isort within pre-commit. rev: 5.6.4 hooks: - id: isort - args: ["--profile", "black"] + args: ["--profile", "black", "--filter-files"] ``` From 5dc0b4e032f77252ea48f4a0a6728db96a9831b4 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 2 Dec 2020 22:54:24 -0800 Subject: [PATCH 1238/1439] Fix windows test --- tests/unit/test_main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index dd2afddaa..e53a626dc 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -1038,9 +1038,9 @@ def test_identify_imports_main(tmpdir, capsys): main.identify_imports_main([str(some_file)]) out, error = capsys.readouterr() - assert out == file_imports.replace("\n", os.linesep) + assert out == file_imports assert not error main.identify_imports_main(["-"], stdin=as_stream(file_content)) out, error = capsys.readouterr() - assert out == file_imports.replace("\n", os.linesep) + assert out == file_imports From f496eea5b763d6985fc2e3450e67fa179f97b6d4 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 2 Dec 2020 23:05:40 -0800 Subject: [PATCH 1239/1439] Add isot black logo --- art/isort_loves_black.png | Bin 0 -> 61394 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 art/isort_loves_black.png diff --git a/art/isort_loves_black.png b/art/isort_loves_black.png new file mode 100644 index 0000000000000000000000000000000000000000..baeb7baa73cc9705b72cff9d48357b67aade6065 GIT binary patch literal 61394 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 K5F9yNQ0xPkyK{8JV*4~XBI19-Iozo4 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 DzX{47 literal 0 HcmV?d00001 From 94ab14effbe2b3230be57cbaf84b5b9cf467946e Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 2 Dec 2020 23:10:01 -0800 Subject: [PATCH 1240/1439] Update to use new black compatibility image --- docs/configuration/black_compatibility.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/configuration/black_compatibility.md b/docs/configuration/black_compatibility.md index 35f6812bf..c81989a9f 100644 --- a/docs/configuration/black_compatibility.md +++ b/docs/configuration/black_compatibility.md @@ -1,3 +1,5 @@ +![isort loves black](https://raw.githubusercontent.com/pycqa/isort/develop/art/isort_loves_black.png) + Compatibility with black ======== From c61f6ffb23f20268e35d8ccab58ae70a8736b5b4 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 2 Dec 2020 23:13:33 -0800 Subject: [PATCH 1241/1439] Fix test on windows --- tests/unit/test_main.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index e53a626dc..e1a459d1b 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -1,5 +1,4 @@ import json -import os import subprocess from datetime import datetime from io import BytesIO, TextIOWrapper @@ -1038,9 +1037,9 @@ def test_identify_imports_main(tmpdir, capsys): main.identify_imports_main([str(some_file)]) out, error = capsys.readouterr() - assert out == file_imports + assert out.replace("\r\n", "\n") == file_imports assert not error main.identify_imports_main(["-"], stdin=as_stream(file_content)) out, error = capsys.readouterr() - assert out == file_imports + assert out.replace("\r\n", "\n") == file_imports From 7fc229df01c6c54c6a6884182ad95a355758aee6 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 2 Dec 2020 23:16:12 -0800 Subject: [PATCH 1242/1439] Add link to isort black compatibility guide to main readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f55c15883..564442824 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ quickly sort all your imports. It requires Python 3.6+ to run but supports formatting Python 2 code too. [Try isort now from your browser!](https://pycqa.github.io/isort/docs/quick_start/0.-try/) +[Using black? See the isort and black compatiblity guide.](https://pycqa.github.io/isort/docs/configuration/black_compatibility/) ![Example Usage](https://raw.github.com/pycqa/isort/develop/example.gif) From 7593b675996a2cd9ce5cda1e844e55e5718a689e Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 2 Dec 2020 23:22:43 -0800 Subject: [PATCH 1243/1439] Improve formatting --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 564442824..7b4526700 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,8 @@ editors](https://github.com/pycqa/isort/wiki/isort-Plugins) to quickly sort all your imports. It requires Python 3.6+ to run but supports formatting Python 2 code too. -[Try isort now from your browser!](https://pycqa.github.io/isort/docs/quick_start/0.-try/) -[Using black? See the isort and black compatiblity guide.](https://pycqa.github.io/isort/docs/configuration/black_compatibility/) +- [Try isort now from your browser!](https://pycqa.github.io/isort/docs/quick_start/0.-try/) +- [Using black? See the isort and black compatiblity guide.](https://pycqa.github.io/isort/docs/configuration/black_compatibility/) ![Example Usage](https://raw.github.com/pycqa/isort/develop/example.gif) From 05858f873cf3139e5b7c360fb7aef24288daa752 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 3 Dec 2020 23:30:25 -0800 Subject: [PATCH 1244/1439] Refactor test main cases to reuse as_stream --- tests/unit/test_main.py | 130 +++++++++++++--------------------------- 1 file changed, 43 insertions(+), 87 deletions(-) diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index e1a459d1b..2af5d8143 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -363,7 +363,7 @@ def function(): """ def build_input_content(): - return UnseekableTextIOWrapper(BytesIO(input_text.encode("utf8"))) + return as_stream(input_text) main.main(["-"], stdin=build_input_content()) out, error = capsys.readouterr() @@ -462,13 +462,11 @@ def function(): def test_isort_with_stdin(capsys): # ensures that isort sorts stdin without any flags - input_content = UnseekableTextIOWrapper( - BytesIO( - b""" + input_content = as_stream( + """ import b import a """ - ) ) main.main(["-"], stdin=input_content) @@ -481,14 +479,12 @@ def test_isort_with_stdin(capsys): """ ) - input_content_from = UnseekableTextIOWrapper( - BytesIO( - b""" + input_content_from = as_stream( + """ import c import b from a import z, y, x """ - ) ) main.main(["-"], stdin=input_content_from) @@ -504,15 +500,13 @@ def test_isort_with_stdin(capsys): # ensures that isort correctly sorts stdin with --fas flag - input_content = UnseekableTextIOWrapper( - BytesIO( - b""" + input_content = as_stream( + """ import sys import pandas from z import abc from a import xyz """ - ) ) main.main(["-", "--fas"], stdin=input_content) @@ -530,12 +524,10 @@ def test_isort_with_stdin(capsys): # ensures that isort correctly sorts stdin with --fass flag - input_content = UnseekableTextIOWrapper( - BytesIO( - b""" + input_content = as_stream( + """ from a import Path, abc """ - ) ) main.main(["-", "--fass"], stdin=input_content) @@ -549,14 +541,12 @@ def test_isort_with_stdin(capsys): # ensures that isort correctly sorts stdin with --ff flag - input_content = UnseekableTextIOWrapper( - BytesIO( - b""" + input_content = as_stream( + """ import b from c import x from a import y """ - ) ) main.main(["-", "--ff", "FROM_FIRST"], stdin=input_content) @@ -572,13 +562,11 @@ def test_isort_with_stdin(capsys): # ensures that isort correctly sorts stdin with -fss flag - input_content = UnseekableTextIOWrapper( - BytesIO( - b""" + input_content = as_stream( + """ import b from a import a """ - ) ) main.main(["-", "--fss"], stdin=input_content) @@ -591,13 +579,11 @@ def test_isort_with_stdin(capsys): """ ) - input_content = UnseekableTextIOWrapper( - BytesIO( - b""" + input_content = as_stream( + """ import a from b import c """ - ) ) main.main(["-", "--fss"], stdin=input_content) @@ -612,14 +598,12 @@ def test_isort_with_stdin(capsys): # ensures that isort correctly sorts stdin with --ds flag - input_content = UnseekableTextIOWrapper( - BytesIO( - b""" + input_content = as_stream( + """ import sys import pandas import a """ - ) ) main.main(["-", "--ds"], stdin=input_content) @@ -635,13 +619,11 @@ def test_isort_with_stdin(capsys): # ensures that isort correctly sorts stdin with --cs flag - input_content = UnseekableTextIOWrapper( - BytesIO( - b""" + input_content = as_stream( + """ from a import b from a import * """ - ) ) main.main(["-", "--cs"], stdin=input_content) @@ -655,13 +637,11 @@ def test_isort_with_stdin(capsys): # ensures that isort correctly sorts stdin with --ca flag - input_content = UnseekableTextIOWrapper( - BytesIO( - b""" + input_content = as_stream( + """ from a import x as X from a import y as Y """ - ) ) main.main(["-", "--ca"], stdin=input_content) @@ -675,14 +655,12 @@ def test_isort_with_stdin(capsys): # ensures that isort works consistently with check and ws flags - input_content = UnseekableTextIOWrapper( - BytesIO( - b""" + input_content = as_stream( + """ import os import a import b """ - ) ) main.main(["-", "--check-only", "--ws"], stdin=input_content) @@ -692,13 +670,11 @@ def test_isort_with_stdin(capsys): # ensures that isort works consistently with check and diff flags - input_content = UnseekableTextIOWrapper( - BytesIO( - b""" + input_content = as_stream( + """ import b import a """ - ) ) with pytest.raises(SystemExit): @@ -711,13 +687,11 @@ def test_isort_with_stdin(capsys): # ensures that isort correctly sorts stdin with --ls flag - input_content = UnseekableTextIOWrapper( - BytesIO( - b""" + input_content = as_stream( + """ import abcdef import x """ - ) ) main.main(["-", "--ls"], stdin=input_content) @@ -732,12 +706,10 @@ def test_isort_with_stdin(capsys): # ensures that isort correctly sorts stdin with --nis flag - input_content = UnseekableTextIOWrapper( - BytesIO( - b""" + input_content = as_stream( + """ from z import b, c, a """ - ) ) main.main(["-", "--nis"], stdin=input_content) @@ -751,12 +723,10 @@ def test_isort_with_stdin(capsys): # ensures that isort correctly sorts stdin with --sl flag - input_content = UnseekableTextIOWrapper( - BytesIO( - b""" + input_content = as_stream( + """ from z import b, c, a """ - ) ) main.main(["-", "--sl"], stdin=input_content) @@ -772,15 +742,12 @@ def test_isort_with_stdin(capsys): # ensures that isort correctly sorts stdin with --top flag - input_content = UnseekableTextIOWrapper( - BytesIO( - b""" + input_content = as_stream( + """ import os import sys """ - ) ) - main.main(["-", "--top", "sys"], stdin=input_content) out, error = capsys.readouterr() @@ -793,17 +760,14 @@ def test_isort_with_stdin(capsys): # ensure that isort correctly sorts stdin with --os flag - input_content = UnseekableTextIOWrapper( - BytesIO( - b""" + input_content = as_stream( + """ import sys import os import z from a import b, e, c """ - ) ) - main.main(["-", "--os"], stdin=input_content) out, error = capsys.readouterr() @@ -818,13 +782,11 @@ def test_isort_with_stdin(capsys): ) # ensures that isort warns with deprecated flags with stdin - input_content = UnseekableTextIOWrapper( - BytesIO( - b""" + input_content = as_stream( + """ import sys import os """ - ) ) with pytest.warns(UserWarning): @@ -839,13 +801,11 @@ def test_isort_with_stdin(capsys): """ ) - input_content = UnseekableTextIOWrapper( - BytesIO( - b""" + input_content = as_stream( + """ import sys import os """ - ) ) with pytest.warns(UserWarning): @@ -861,13 +821,11 @@ def test_isort_with_stdin(capsys): ) # ensures that only-modified flag works with stdin - input_content = UnseekableTextIOWrapper( - BytesIO( - b""" + input_content = as_stream( + """ import a import b """ - ) ) main.main(["-", "--verbose", "--only-modified"], stdin=input_content) @@ -877,13 +835,11 @@ def test_isort_with_stdin(capsys): assert "else-type place_module for b returned THIRDPARTY" not in out # ensures that combine-straight-imports flag works with stdin - input_content = UnseekableTextIOWrapper( - BytesIO( - b""" + input_content = as_stream( + """ import a import b """ - ) ) main.main(["-", "--combine-straight-imports"], stdin=input_content) From e912fbf541ea1d722aeed7f1f01c4e565ad0196b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 4 Dec 2020 22:48:04 -0800 Subject: [PATCH 1245/1439] Improve coverage of identify imports --- tests/unit/test_main.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 2af5d8143..5fab49ffc 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -999,3 +999,6 @@ def test_identify_imports_main(tmpdir, capsys): main.identify_imports_main(["-"], stdin=as_stream(file_content)) out, error = capsys.readouterr() assert out.replace("\r\n", "\n") == file_imports + + with pytest.raises(SystemExit): + main.identify_imports_main([str(tmpdir)]) From f32cf7724c071f9e84423f07271bfa70178d8000 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 5 Dec 2020 23:53:52 -0800 Subject: [PATCH 1246/1439] Improve test coverage with test for KeyError in main --- poetry.lock | 990 +++++++++++++++++++--------------------- pyproject.toml | 1 + tests/unit/test_main.py | 16 + 3 files changed, 475 insertions(+), 532 deletions(-) diff --git a/poetry.lock b/poetry.lock index d6e942661..21fa4fabc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,98 +1,97 @@ [[package]] -category = "main" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." name = "appdirs" +version = "1.4.4" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "main" optional = false python-versions = "*" -version = "1.4.4" [[package]] -category = "dev" -description = "Disable App Nap on OS X 10.9" -marker = "sys_platform == \"darwin\"" name = "appnope" +version = "0.1.0" +description = "Disable App Nap on OS X 10.9" +category = "dev" optional = false python-versions = "*" -version = "0.1.0" [[package]] -category = "dev" -description = "Better dates & times for Python" name = "arrow" +version = "0.16.0" +description = "Better dates & times for Python" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "0.16.0" [package.dependencies] python-dateutil = ">=2.7.0" [[package]] -category = "dev" -description = "Atomic file writes." -marker = "sys_platform == \"win32\"" name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.4.0" [[package]] -category = "main" -description = "Classes Without Boilerplate" name = "attrs" +version = "20.1.0" +description = "Classes Without Boilerplate" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "20.1.0" [package.extras] -dev = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "sphinx-rtd-theme", "pre-commit"] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "sphinx-rtd-theme", "pre-commit"] docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] -tests = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] [[package]] -category = "dev" -description = "Specifications for callback functions passed in to an API" name = "backcall" +version = "0.2.0" +description = "Specifications for callback functions passed in to an API" +category = "dev" optional = false python-versions = "*" -version = "0.2.0" [[package]] -category = "dev" -description = "Security oriented static analyser for python code." name = "bandit" +version = "1.6.2" +description = "Security oriented static analyser for python code." +category = "dev" optional = false python-versions = "*" -version = "1.6.2" [package.dependencies] +colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} GitPython = ">=1.0.1" PyYAML = ">=3.13" -colorama = ">=0.3.9" six = ">=1.10.0" stevedore = ">=1.20.0" [[package]] -category = "dev" -description = "Ultra-lightweight pure Python package to check if a file is binary or text." name = "binaryornot" +version = "0.4.4" +description = "Ultra-lightweight pure Python package to check if a file is binary or text." +category = "dev" optional = false python-versions = "*" -version = "0.4.4" [package.dependencies] chardet = ">=3.0.2" [[package]] -category = "dev" -description = "The uncompromising code formatter." name = "black" +version = "20.8b1" +description = "The uncompromising code formatter." +category = "dev" optional = false python-versions = ">=3.6" -version = "20.8b1" [package.dependencies] appdirs = "*" click = ">=7.1.2" +dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""} mypy-extensions = ">=0.4.3" pathspec = ">=0.6,<1" regex = ">=2020.1.8" @@ -100,114 +99,106 @@ toml = ">=0.10.1" typed-ast = ">=1.4.0" typing-extensions = ">=3.7.4" -[package.dependencies.dataclasses] -python = "<3.7" -version = ">=0.6" - [package.extras] colorama = ["colorama (>=0.4.3)"] d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] [[package]] -category = "main" -description = "A decorator for caching properties in classes." name = "cached-property" +version = "1.5.1" +description = "A decorator for caching properties in classes." +category = "main" optional = false python-versions = "*" -version = "1.5.1" [[package]] -category = "main" -description = "Lightweight, extensible schema and data validation tool for Python dictionaries." name = "cerberus" +version = "1.3.2" +description = "Lightweight, extensible schema and data validation tool for Python dictionaries." +category = "main" optional = false python-versions = ">=2.7" -version = "1.3.2" - -[package.dependencies] -setuptools = "*" [[package]] -category = "main" -description = "Python package for providing Mozilla's CA Bundle." name = "certifi" +version = "2020.6.20" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" optional = false python-versions = "*" -version = "2020.6.20" [[package]] -category = "main" -description = "Universal encoding detector for Python 2 and 3" name = "chardet" +version = "3.0.4" +description = "Universal encoding detector for Python 2 and 3" +category = "main" optional = false python-versions = "*" -version = "3.0.4" [[package]] -category = "dev" -description = "Composable command line interface toolkit" name = "click" +version = "7.1.2" +description = "Composable command line interface toolkit" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "7.1.2" [[package]] -category = "main" -description = "Cross-platform colored terminal text." name = "colorama" +version = "0.4.3" +description = "Cross-platform colored terminal text." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "0.4.3" [[package]] -category = "dev" -description = "PEP 567 Backport" -marker = "python_version < \"3.7\"" name = "contextvars" +version = "2.4" +description = "PEP 567 Backport" +category = "dev" optional = false python-versions = "*" -version = "2.4" [package.dependencies] immutables = ">=0.9" [[package]] -category = "dev" -description = "A command-line utility that creates projects from project templates, e.g. creating a Python package project from a Python package project template." name = "cookiecutter" +version = "1.7.2" +description = "A command-line utility that creates projects from project templates, e.g. creating a Python package project from a Python package project template." +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "1.7.2" [package.dependencies] -Jinja2 = "<3.0.0" -MarkupSafe = "<2.0.0" binaryornot = ">=0.4.4" click = ">=7.0" +Jinja2 = "<3.0.0" jinja2-time = ">=0.2.0" +MarkupSafe = "<2.0.0" poyo = ">=0.5.0" python-slugify = ">=4.0.0" requests = ">=2.23.0" six = ">=1.10" [[package]] -category = "dev" -description = "Code coverage measurement for Python" name = "coverage" +version = "5.2.1" +description = "Code coverage measurement for Python" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" -version = "5.2.1" [package.extras] toml = ["toml"] [[package]] -category = "dev" -description = "Allows you to maintain all the necessary cruft for packaging and building projects separate from the code you intentionally write. Built on-top of CookieCutter." name = "cruft" +version = "2.3.0" +description = "Allows you to maintain all the necessary cruft for packaging and building projects separate from the code you intentionally write. Built on-top of CookieCutter." +category = "dev" optional = false python-versions = ">=3.6,<4.0" -version = "2.3.0" [package.dependencies] click = ">=7.1.2,<8.0.0" @@ -216,49 +207,48 @@ gitpython = ">=3.0,<4.0" typer = ">=0.3.1,<0.4.0" [package.extras] -examples = ["examples (>=1.0.2,<2.0.0)"] pyproject = ["toml (>=0.10,<0.11)"] +examples = ["examples (>=1.0.2,<2.0.0)"] [[package]] -category = "dev" -description = "A backport of the dataclasses module for Python 3.6" -marker = "python_version < \"3.7\"" name = "dataclasses" +version = "0.6" +description = "A backport of the dataclasses module for Python 3.6" +category = "dev" optional = false python-versions = "*" -version = "0.6" [[package]] -category = "dev" -description = "Decorators for Humans" name = "decorator" +version = "4.4.2" +description = "Decorators for Humans" +category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*" -version = "4.4.2" [[package]] -category = "main" -description = "Distribution utilities" name = "distlib" +version = "0.3.1" +description = "Distribution utilities" +category = "main" optional = false python-versions = "*" -version = "0.3.1" [[package]] -category = "main" -description = "Pythonic argument parser, that will make you smile" name = "docopt" +version = "0.6.2" +description = "Pythonic argument parser, that will make you smile" +category = "main" optional = false python-versions = "*" -version = "0.6.2" [[package]] -category = "dev" -description = "A parser for Python dependency files" name = "dparse" +version = "0.5.1" +description = "A parser for Python dependency files" +category = "dev" optional = false python-versions = ">=3.5" -version = "0.5.1" [package.dependencies] packaging = "*" @@ -269,168 +259,165 @@ toml = "*" pipenv = ["pipenv"] [[package]] -category = "dev" -description = "An example plugin that modifies isort formatting using black." name = "example-isort-formatting-plugin" +version = "0.0.2" +description = "An example plugin that modifies isort formatting using black." +category = "dev" optional = false python-versions = ">=3.6,<4.0" -version = "0.0.2" [package.dependencies] black = ">=20.08b1,<21.0" isort = ">=5.1.4,<6.0.0" [[package]] -category = "dev" -description = "An example shared isort profile" name = "example-shared-isort-profile" +version = "0.0.1" +description = "An example shared isort profile" +category = "dev" optional = false python-versions = ">=3.6,<4.0" -version = "0.0.1" [[package]] -category = "dev" -description = "Tests and Documentation Done by Example." name = "examples" +version = "1.0.2" +description = "Tests and Documentation Done by Example." +category = "dev" optional = false python-versions = ">=3.6,<4.0" -version = "1.0.2" [package.dependencies] pydantic = ">=0.32.2" [[package]] -category = "dev" -description = "An unladen web framework for building APIs and app backends." name = "falcon" +version = "2.0.0" +description = "An unladen web framework for building APIs and app backends." +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.0.0" [[package]] -category = "dev" -description = "the modular source code checker: pep8 pyflakes and co" name = "flake8" +version = "3.8.4" +description = "the modular source code checker: pep8 pyflakes and co" +category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" -version = "3.8.3" [package.dependencies] +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} mccabe = ">=0.6.0,<0.7.0" pycodestyle = ">=2.6.0a1,<2.7.0" pyflakes = ">=2.2.0,<2.3.0" -[package.dependencies.importlib-metadata] -python = "<3.8" -version = "*" - [[package]] -category = "dev" -description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." name = "flake8-bugbear" +version = "19.8.0" +description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." +category = "dev" optional = false python-versions = ">=3.5" -version = "19.8.0" [package.dependencies] attrs = "*" flake8 = ">=3.0.0" [[package]] -category = "dev" -description = "Polyfill package for Flake8 plugins" name = "flake8-polyfill" +version = "1.0.2" +description = "Polyfill package for Flake8 plugins" +category = "dev" optional = false python-versions = "*" -version = "1.0.2" [package.dependencies] flake8 = "*" [[package]] -category = "dev" -description = "Clean single-source support for Python 3 and 2" name = "future" +version = "0.18.2" +description = "Clean single-source support for Python 3 and 2" +category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -version = "0.18.2" [[package]] -category = "dev" -description = "Git Object Database" name = "gitdb" +version = "4.0.5" +description = "Git Object Database" +category = "dev" optional = false python-versions = ">=3.4" -version = "4.0.5" [package.dependencies] smmap = ">=3.0.1,<4" [[package]] -category = "dev" -description = "A mirror package for gitdb" name = "gitdb2" +version = "4.0.2" +description = "A mirror package for gitdb" +category = "dev" optional = false python-versions = "*" -version = "4.0.2" [package.dependencies] gitdb = ">=4.0.1" [[package]] -category = "dev" -description = "Python Git Library" name = "gitpython" +version = "3.1.7" +description = "Python Git Library" +category = "dev" optional = false python-versions = ">=3.4" -version = "3.1.7" [package.dependencies] gitdb = ">=4.0.1,<5" [[package]] -category = "dev" -description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" name = "h11" +version = "0.9.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +category = "dev" optional = false python-versions = "*" -version = "0.9.0" [[package]] -category = "dev" -description = "HTTP/2 State-Machine based protocol implementation" name = "h2" +version = "3.2.0" +description = "HTTP/2 State-Machine based protocol implementation" +category = "dev" optional = false python-versions = "*" -version = "3.2.0" [package.dependencies] hpack = ">=3.0,<4" hyperframe = ">=5.2.0,<6" [[package]] -category = "dev" -description = "Pure-Python HPACK header compression" name = "hpack" +version = "3.0.0" +description = "Pure-Python HPACK header compression" +category = "dev" optional = false python-versions = "*" -version = "3.0.0" [[package]] -category = "dev" -description = "Chromium HSTS Preload list as a Python package and updated daily" name = "hstspreload" +version = "2020.8.25" +description = "Chromium HSTS Preload list as a Python package and updated daily" +category = "dev" optional = false python-versions = ">=3.6" -version = "2020.8.25" [[package]] -category = "dev" -description = "A minimal low-level HTTP client." name = "httpcore" +version = "0.9.1" +description = "A minimal low-level HTTP client." +category = "dev" optional = false python-versions = ">=3.6" -version = "0.9.1" [package.dependencies] h11 = ">=0.8,<0.10" @@ -438,12 +425,12 @@ h2 = ">=3.0.0,<4.0.0" sniffio = ">=1.0.0,<2.0.0" [[package]] -category = "dev" -description = "The next generation HTTP client." name = "httpx" +version = "0.13.3" +description = "The next generation HTTP client." +category = "dev" optional = false python-versions = ">=3.6" -version = "0.13.3" [package.dependencies] certifi = "*" @@ -455,32 +442,32 @@ rfc3986 = ">=1.3,<2" sniffio = "*" [[package]] -category = "dev" -description = "A Python framework that makes developing APIs as simple as possible, but no simpler." name = "hug" +version = "2.6.1" +description = "A Python framework that makes developing APIs as simple as possible, but no simpler." +category = "dev" optional = false python-versions = ">=3.5" -version = "2.6.1" [package.dependencies] falcon = "2.0.0" requests = "*" [[package]] -category = "dev" -description = "HTTP/2 framing layer for Python" name = "hyperframe" +version = "5.2.0" +description = "HTTP/2 framing layer for Python" +category = "dev" optional = false python-versions = "*" -version = "5.2.0" [[package]] -category = "dev" -description = "A library for property-based testing" name = "hypothesis" +version = "5.29.3" +description = "A library for property-based testing" +category = "dev" optional = false python-versions = ">=3.5.2" -version = "5.29.3" [package.dependencies] attrs = ">=19.2.0" @@ -500,12 +487,12 @@ pytest = ["pytest (>=4.3)"] pytz = ["pytz (>=2014.1)"] [[package]] -category = "dev" -description = "Extends Hypothesis to add fully automatic testing of type annotated functions" name = "hypothesis-auto" +version = "1.1.4" +description = "Extends Hypothesis to add fully automatic testing of type annotated functions" +category = "dev" optional = false python-versions = ">=3.6,<4.0" -version = "1.1.4" [package.dependencies] hypothesis = ">=4.36" @@ -515,12 +502,12 @@ pydantic = ">=0.32.2" pytest = ["pytest (>=4.0.0,<5.0.0)"] [[package]] -category = "dev" -description = "Hypothesis strategies for generating Python programs, something like CSmith" name = "hypothesmith" +version = "0.1.4" +description = "Hypothesis strategies for generating Python programs, something like CSmith" +category = "dev" optional = false python-versions = ">=3.6" -version = "0.1.4" [package.dependencies] hypothesis = ">=5.23.7" @@ -528,30 +515,28 @@ lark-parser = ">=0.7.2" libcst = ">=0.3.8" [[package]] -category = "main" -description = "Internationalized Domain Names in Applications (IDNA)" name = "idna" +version = "2.10" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.10" [[package]] -category = "dev" -description = "Immutable Collections" -marker = "python_version < \"3.7\"" name = "immutables" +version = "0.14" +description = "Immutable Collections" +category = "dev" optional = false python-versions = ">=3.5" -version = "0.14" [[package]] -category = "main" -description = "Read metadata from Python packages" -marker = "python_version < \"3.8\"" name = "importlib-metadata" +version = "1.7.0" +description = "Read metadata from Python packages" +category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -version = "1.7.0" [package.dependencies] zipp = ">=0.5" @@ -561,24 +546,23 @@ docs = ["sphinx", "rst.linker"] testing = ["packaging", "pep517", "importlib-resources (>=1.3)"] [[package]] -category = "dev" -description = "IPython: Productive Interactive Computing" name = "ipython" +version = "7.16.1" +description = "IPython: Productive Interactive Computing" +category = "dev" optional = false python-versions = ">=3.6" -version = "7.16.1" [package.dependencies] -appnope = "*" +appnope = {version = "*", markers = "sys_platform == \"darwin\""} backcall = "*" -colorama = "*" +colorama = {version = "*", markers = "sys_platform == \"win32\""} decorator = "*" jedi = ">=0.10" -pexpect = "*" +pexpect = {version = "*", markers = "sys_platform != \"win32\""} pickleshare = "*" prompt-toolkit = ">=2.0.0,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.1.0" pygments = "*" -setuptools = ">=18.5" traitlets = ">=4.2" [package.extras] @@ -593,35 +577,35 @@ qtconsole = ["qtconsole"] test = ["nose (>=0.10.1)", "requests", "testpath", "pygments", "nbformat", "ipykernel", "numpy (>=1.14)"] [[package]] -category = "dev" -description = "Vestigial utilities from IPython" name = "ipython-genutils" +version = "0.2.0" +description = "Vestigial utilities from IPython" +category = "dev" optional = false python-versions = "*" -version = "0.2.0" [[package]] -category = "dev" -description = "An autocompletion tool for Python that can be used for text editors." name = "jedi" +version = "0.17.2" +description = "An autocompletion tool for Python that can be used for text editors." +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "0.17.2" [package.dependencies] parso = ">=0.7.0,<0.8.0" [package.extras] -qa = ["flake8 (3.7.9)"] +qa = ["flake8 (==3.7.9)"] testing = ["Django (<3.1)", "colorama", "docopt", "pytest (>=3.9.0,<5.0.0)"] [[package]] -category = "dev" -description = "A very fast and expressive template engine." name = "jinja2" +version = "2.11.2" +description = "A very fast and expressive template engine." +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "2.11.2" [package.dependencies] MarkupSafe = ">=0.23" @@ -630,99 +614,88 @@ MarkupSafe = ">=0.23" i18n = ["Babel (>=0.8)"] [[package]] -category = "dev" -description = "Jinja2 Extension for Dates and Times" name = "jinja2-time" +version = "0.2.0" +description = "Jinja2 Extension for Dates and Times" +category = "dev" optional = false python-versions = "*" -version = "0.2.0" [package.dependencies] arrow = "*" jinja2 = "*" [[package]] -category = "dev" -description = "Lightweight pipelining: using Python functions as pipeline jobs." -marker = "python_version > \"2.7\"" name = "joblib" +version = "0.16.0" +description = "Lightweight pipelining: using Python functions as pipeline jobs." +category = "dev" optional = false python-versions = ">=3.6" -version = "0.16.0" [[package]] -category = "dev" -description = "a modern parsing library" name = "lark-parser" +version = "0.9.0" +description = "a modern parsing library" +category = "dev" optional = false python-versions = "*" -version = "0.9.0" [package.extras] regex = ["regex"] [[package]] -category = "dev" -description = "A concrete syntax tree with AST-like properties for Python 3.5, 3.6, 3.7 and 3.8 programs." name = "libcst" +version = "0.3.10" +description = "A concrete syntax tree with AST-like properties for Python 3.5, 3.6, 3.7 and 3.8 programs." +category = "dev" optional = false python-versions = ">=3.6" -version = "0.3.10" [package.dependencies] +dataclasses = {version = "*", markers = "python_version < \"3.7\""} pyyaml = ">=5.2" typing-extensions = ">=3.7.4.2" typing-inspect = ">=0.4.0" -[package.dependencies.dataclasses] -python = "<3.7" -version = "*" - [package.extras] dev = ["black", "codecov", "coverage", "hypothesis (>=4.36.0)", "hypothesmith (>=0.0.4)", "isort", "flake8", "jupyter", "nbsphinx", "pyre-check", "sphinx", "sphinx-rtd-theme"] [[package]] -category = "dev" -description = "Python LiveReload is an awesome tool for web developers" name = "livereload" +version = "2.6.3" +description = "Python LiveReload is an awesome tool for web developers" +category = "dev" optional = false python-versions = "*" -version = "2.6.3" [package.dependencies] six = "*" - -[package.dependencies.tornado] -python = ">=2.8" -version = "*" +tornado = {version = "*", markers = "python_version > \"2.7\""} [[package]] -category = "dev" -description = "A Python implementation of Lunr.js" name = "lunr" +version = "0.5.8" +description = "A Python implementation of Lunr.js" +category = "dev" optional = false python-versions = "*" -version = "0.5.8" [package.dependencies] future = ">=0.16.0" +nltk = {version = ">=3.2.5", optional = true, markers = "python_version > \"2.7\" and extra == \"languages\""} six = ">=1.11.0" -[package.dependencies.nltk] -optional = true -python = ">=2.8" -version = ">=3.2.5" - [package.extras] languages = ["nltk (>=3.2.5,<3.5)", "nltk (>=3.2.5)"] [[package]] -category = "dev" -description = "A super-fast templating language that borrows the best ideas from the existing templating languages." name = "mako" +version = "1.1.3" +description = "A super-fast templating language that borrows the best ideas from the existing templating languages." +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.1.3" [package.dependencies] MarkupSafe = ">=0.9.2" @@ -732,98 +705,93 @@ babel = ["babel"] lingua = ["lingua"] [[package]] -category = "dev" -description = "Python implementation of Markdown." name = "markdown" +version = "3.2.2" +description = "Python implementation of Markdown." +category = "dev" optional = false python-versions = ">=3.5" -version = "3.2.2" [package.dependencies] -[package.dependencies.importlib-metadata] -python = "<3.8" -version = "*" +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [package.extras] testing = ["coverage", "pyyaml"] [[package]] -category = "dev" -description = "Safely add untrusted strings to HTML/XML markup." name = "markupsafe" +version = "1.1.1" +description = "Safely add untrusted strings to HTML/XML markup." +category = "dev" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" -version = "1.1.1" [[package]] -category = "dev" -description = "McCabe checker, plugin for flake8" name = "mccabe" +version = "0.6.1" +description = "McCabe checker, plugin for flake8" +category = "dev" optional = false python-versions = "*" -version = "0.6.1" [[package]] -category = "dev" -description = "Project documentation with Markdown." name = "mkdocs" +version = "1.1.2" +description = "Project documentation with Markdown." +category = "dev" optional = false python-versions = ">=3.5" -version = "1.1.2" [package.dependencies] +click = ">=3.3" Jinja2 = ">=2.10.1" +livereload = ">=2.5.1" +lunr = {version = "0.5.8", extras = ["languages"]} Markdown = ">=3.2.1" PyYAML = ">=3.10" -click = ">=3.3" -livereload = ">=2.5.1" tornado = ">=5.0" -[package.dependencies.lunr] -extras = ["languages"] -version = "0.5.8" - [[package]] -category = "dev" -description = "A Material Design theme for MkDocs" name = "mkdocs-material" +version = "5.5.9" +description = "A Material Design theme for MkDocs" +category = "dev" optional = false python-versions = "*" -version = "5.5.9" [package.dependencies] -Pygments = ">=2.4" markdown = ">=3.2" mkdocs = ">=1.1" mkdocs-material-extensions = ">=1.0" +Pygments = ">=2.4" pymdown-extensions = ">=7.0" [[package]] -category = "dev" -description = "Extension pack for Python Markdown." name = "mkdocs-material-extensions" +version = "1.0" +description = "Extension pack for Python Markdown." +category = "dev" optional = false python-versions = ">=3.5" -version = "1.0" [package.dependencies] mkdocs-material = ">=5.0.0" [[package]] -category = "dev" -description = "More routines for operating on iterables, beyond itertools" name = "more-itertools" +version = "8.4.0" +description = "More routines for operating on iterables, beyond itertools" +category = "dev" optional = false python-versions = ">=3.5" -version = "8.4.0" [[package]] -category = "dev" -description = "Optional static typing for Python" name = "mypy" +version = "0.761" +description = "Optional static typing for Python" +category = "dev" optional = false python-versions = ">=3.5" -version = "0.761" [package.dependencies] mypy-extensions = ">=0.4.3,<0.5.0" @@ -834,21 +802,20 @@ typing-extensions = ">=3.7.4" dmypy = ["psutil (>=4.0)"] [[package]] -category = "dev" -description = "Experimental type system extensions for programs checked with the mypy typechecker." name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" optional = false python-versions = "*" -version = "0.4.3" [[package]] -category = "dev" -description = "Natural Language Toolkit" -marker = "python_version > \"2.7\"" name = "nltk" +version = "3.5" +description = "Natural Language Toolkit" +category = "dev" optional = false python-versions = "*" -version = "3.5" [package.dependencies] click = "*" @@ -865,222 +832,204 @@ tgrep = ["pyparsing"] twitter = ["twython"] [[package]] -category = "dev" -description = "NumPy is the fundamental package for array computing with Python." name = "numpy" +version = "1.19.1" +description = "NumPy is the fundamental package for array computing with Python." +category = "dev" optional = false python-versions = ">=3.6" -version = "1.19.1" [[package]] -category = "main" -description = "Ordered Multivalue Dictionary" name = "orderedmultidict" +version = "1.0.1" +description = "Ordered Multivalue Dictionary" +category = "main" optional = false python-versions = "*" -version = "1.0.1" [package.dependencies] six = ">=1.8.0" [[package]] -category = "main" -description = "Core utilities for Python packages" name = "packaging" +version = "20.4" +description = "Core utilities for Python packages" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "20.4" [package.dependencies] pyparsing = ">=2.0.2" six = "*" [[package]] -category = "dev" -description = "A Python Parser" name = "parso" +version = "0.7.1" +description = "A Python Parser" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.7.1" [package.extras] testing = ["docopt", "pytest (>=3.0.7)"] [[package]] -category = "dev" -description = "Utility library for gitignore style pattern matching of file paths." name = "pathspec" +version = "0.8.0" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "0.8.0" [[package]] -category = "dev" -description = "Python Build Reasonableness" name = "pbr" +version = "5.4.5" +description = "Python Build Reasonableness" +category = "dev" optional = false python-versions = "*" -version = "5.4.5" [[package]] -category = "dev" -description = "A simple program and library to auto generate API documentation for Python modules." name = "pdocs" +version = "1.0.2" +description = "A simple program and library to auto generate API documentation for Python modules." +category = "dev" optional = false python-versions = ">=3.6,<4.0" -version = "1.0.2" [package.dependencies] +hug = ">=2.6,<3.0" Mako = ">=1.1,<2.0" Markdown = ">=3.0.0,<4.0.0" -hug = ">=2.6,<3.0" [[package]] -category = "main" -description = "Wrappers to build Python packages using PEP 517 hooks" name = "pep517" +version = "0.8.2" +description = "Wrappers to build Python packages using PEP 517 hooks" +category = "main" optional = false python-versions = "*" -version = "0.8.2" [package.dependencies] +importlib_metadata = {version = "*", markers = "python_version < \"3.8\""} toml = "*" - -[package.dependencies.importlib_metadata] -python = "<3.8" -version = "*" - -[package.dependencies.zipp] -python = "<3.8" -version = "*" +zipp = {version = "*", markers = "python_version < \"3.8\""} [[package]] -category = "dev" -description = "Check PEP-8 naming conventions, plugin for flake8" name = "pep8-naming" +version = "0.8.2" +description = "Check PEP-8 naming conventions, plugin for flake8" +category = "dev" optional = false python-versions = "*" -version = "0.8.2" [package.dependencies] flake8-polyfill = ">=1.0.2,<2" [[package]] -category = "dev" -description = "Pexpect allows easy control of interactive console applications." -marker = "sys_platform != \"win32\"" name = "pexpect" +version = "4.8.0" +description = "Pexpect allows easy control of interactive console applications." +category = "dev" optional = false python-versions = "*" -version = "4.8.0" [package.dependencies] ptyprocess = ">=0.5" [[package]] -category = "dev" -description = "Tiny 'shelve'-like database with concurrency support" name = "pickleshare" +version = "0.7.5" +description = "Tiny 'shelve'-like database with concurrency support" +category = "dev" optional = false python-versions = "*" -version = "0.7.5" [[package]] -category = "main" -description = "An unofficial, importable pip API" name = "pip-api" +version = "0.0.12" +description = "An unofficial, importable pip API" +category = "main" optional = false python-versions = ">=2.7,!=3.0,!=3.1,!=3.2,!=3.3" -version = "0.0.12" - -[package.dependencies] -pip = "*" [[package]] -category = "main" -description = "Compatibility shims for pip versions 8 thru current." name = "pip-shims" +version = "0.5.3" +description = "Compatibility shims for pip versions 8 thru current." +category = "main" optional = false python-versions = "!=3.0,!=3.1,!=3.2,!=3.3,!=3.4,>=2.7" -version = "0.5.3" [package.dependencies] packaging = "*" -pip = "*" -setuptools = "*" six = "*" -wheel = "*" [package.extras] dev = ["pre-commit", "isort", "flake8", "rope", "invoke", "parver", "towncrier", "wheel", "mypy", "flake8-bugbear", "black"] tests = ["pytest-timeout", "pytest (<5.0)", "pytest-xdist", "pytest-cov", "twine", "readme-renderer"] [[package]] -category = "dev" -description = "" name = "pipfile" +version = "0.0.2" +description = "" +category = "dev" optional = false python-versions = "*" -version = "0.0.2" [package.dependencies] toml = "*" [[package]] -category = "main" -description = "Pip requirements.txt generator based on imports in project" name = "pipreqs" +version = "0.4.10" +description = "Pip requirements.txt generator based on imports in project" +category = "main" optional = false python-versions = "*" -version = "0.4.10" [package.dependencies] docopt = "*" yarg = "*" [[package]] -category = "main" -description = "Structured Pipfile and Pipfile.lock models." name = "plette" +version = "0.2.3" +description = "Structured Pipfile and Pipfile.lock models." +category = "main" optional = false python-versions = ">=2.6,!=3.0,!=3.1,!=3.2,!=3.3" -version = "0.2.3" [package.dependencies] +cerberus = {version = "*", optional = true, markers = "extra == \"validation\""} six = "*" tomlkit = "*" -[package.dependencies.cerberus] -optional = true -version = "*" - [package.extras] tests = ["pytest", "pytest-xdist", "pytest-cov"] validation = ["cerberus"] [[package]] -category = "dev" -description = "plugin and hook calling mechanisms for python" name = "pluggy" +version = "0.13.1" +description = "plugin and hook calling mechanisms for python" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.13.1" [package.dependencies] -[package.dependencies.importlib-metadata] -python = "<3.8" -version = ">=0.12" +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} [package.extras] dev = ["pre-commit", "tox"] [[package]] -category = "dev" -description = "Your Project with Great Documentation" name = "portray" +version = "1.4.0" +description = "Your Project with Great Documentation" +category = "dev" optional = false python-versions = ">=3.6,<4.0" -version = "1.4.0" [package.dependencies] GitPython = ">=3.0,<4.0" @@ -1093,101 +1042,98 @@ toml = ">=0.10.0,<0.11.0" yaspin = ">=0.15.0,<0.16.0" [[package]] -category = "dev" -description = "A lightweight YAML Parser for Python. 🐓" name = "poyo" +version = "0.5.0" +description = "A lightweight YAML Parser for Python. 🐓" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.5.0" [[package]] -category = "dev" -description = "Library for building powerful interactive command lines in Python" name = "prompt-toolkit" +version = "3.0.3" +description = "Library for building powerful interactive command lines in Python" +category = "dev" optional = false python-versions = ">=3.6" -version = "3.0.3" [package.dependencies] wcwidth = "*" [[package]] -category = "dev" -description = "Run a subprocess in a pseudo terminal" -marker = "sys_platform != \"win32\"" name = "ptyprocess" +version = "0.6.0" +description = "Run a subprocess in a pseudo terminal" +category = "dev" optional = false python-versions = "*" -version = "0.6.0" [[package]] -category = "dev" -description = "library with cross-python path, ini-parsing, io, code, log facilities" name = "py" +version = "1.9.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.9.0" [[package]] -category = "dev" -description = "Python style guide checker" name = "pycodestyle" +version = "2.6.0" +description = "Python style guide checker" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.6.0" [[package]] -category = "dev" -description = "Data validation and settings management using python 3.6 type hinting" name = "pydantic" +version = "1.6.1" +description = "Data validation and settings management using python 3.6 type hinting" +category = "dev" optional = false python-versions = ">=3.6" -version = "1.6.1" [package.dependencies] -[package.dependencies.dataclasses] -python = "<3.7" -version = ">=0.6" +dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""} [package.extras] dotenv = ["python-dotenv (>=0.10.4)"] email = ["email-validator (>=1.0.3)"] typing_extensions = ["typing-extensions (>=3.7.2)"] -[[package]] -category = "dev" -description = "Python docstring style checker" +[[package]] name = "pydocstyle" +version = "5.1.0" +description = "Python docstring style checker" +category = "dev" optional = false python-versions = ">=3.5" -version = "5.1.0" [package.dependencies] snowballstemmer = "*" [[package]] -category = "dev" -description = "passive checker of Python programs" name = "pyflakes" +version = "2.2.0" +description = "passive checker of Python programs" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.2.0" [[package]] -category = "dev" -description = "Pygments is a syntax highlighting package written in Python." name = "pygments" +version = "2.6.1" +description = "Pygments is a syntax highlighting package written in Python." +category = "dev" optional = false python-versions = ">=3.5" -version = "2.6.1" [[package]] -category = "dev" -description = "pylama -- Code audit tool for python" name = "pylama" +version = "7.7.1" +description = "pylama -- Code audit tool for python" +category = "dev" optional = false python-versions = "*" -version = "7.7.1" [package.dependencies] mccabe = ">=0.5.2" @@ -1196,72 +1142,69 @@ pydocstyle = ">=2.0.0" pyflakes = ">=1.5.0" [[package]] -category = "dev" -description = "Extension pack for Python Markdown." name = "pymdown-extensions" +version = "7.1" +description = "Extension pack for Python Markdown." +category = "dev" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" -version = "7.1" [package.dependencies] Markdown = ">=3.2" [[package]] -category = "main" -description = "Python parsing module" name = "pyparsing" +version = "2.4.7" +description = "Python parsing module" +category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -version = "2.4.7" [[package]] -category = "dev" -description = "pytest: simple powerful testing with Python" name = "pytest" +version = "5.4.3" +description = "pytest: simple powerful testing with Python" +category = "dev" optional = false python-versions = ">=3.5" -version = "5.4.3" [package.dependencies] -atomicwrites = ">=1.0" +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} attrs = ">=17.4.0" -colorama = "*" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} more-itertools = ">=4.0.0" packaging = "*" pluggy = ">=0.12,<1.0" py = ">=1.5.0" wcwidth = "*" -[package.dependencies.importlib-metadata] -python = "<3.8" -version = ">=0.12" - [package.extras] -checkqa-mypy = ["mypy (v0.761)"] +checkqa-mypy = ["mypy (==v0.761)"] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] [[package]] -category = "dev" -description = "Pytest plugin for measuring coverage." name = "pytest-cov" +version = "2.10.1" +description = "Pytest plugin for measuring coverage." +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "2.10.1" [package.dependencies] coverage = ">=4.4" pytest = ">=4.6" [package.extras] -testing = ["fields", "hunter", "process-tests (2.0.2)", "six", "pytest-xdist", "virtualenv"] +testing = ["fields", "hunter", "process-tests (==2.0.2)", "six", "pytest-xdist", "virtualenv"] [[package]] -category = "dev" -description = "Thin-wrapper around the mock package for easier use with py.test" name = "pytest-mock" +version = "1.13.0" +description = "Thin-wrapper around the mock package for easier use with py.test" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.13.0" [package.dependencies] pytest = ">=2.7" @@ -1270,23 +1213,23 @@ pytest = ">=2.7" dev = ["pre-commit", "tox"] [[package]] -category = "main" -description = "Extensions to the standard Python datetime module" name = "python-dateutil" +version = "2.8.1" +description = "Extensions to the standard Python datetime module" +category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -version = "2.8.1" [package.dependencies] six = ">=1.5" [[package]] -category = "dev" -description = "A Python Slugify application that handles Unicode" name = "python-slugify" +version = "4.0.1" +description = "A Python Slugify application that handles Unicode" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "4.0.1" [package.dependencies] text-unidecode = ">=1.3" @@ -1295,28 +1238,28 @@ text-unidecode = ">=1.3" unidecode = ["Unidecode (>=1.1.1)"] [[package]] -category = "dev" -description = "YAML parser and emitter for Python" name = "pyyaml" +version = "5.3.1" +description = "YAML parser and emitter for Python" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "5.3.1" [[package]] -category = "dev" -description = "Alternative regular expression module, to replace re." name = "regex" +version = "2020.7.14" +description = "Alternative regular expression module, to replace re." +category = "dev" optional = false python-versions = "*" -version = "2020.7.14" [[package]] -category = "main" -description = "Python HTTP for Humans." name = "requests" +version = "2.24.0" +description = "Python HTTP for Humans." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "2.24.0" [package.dependencies] certifi = ">=2017.4.17" @@ -1326,15 +1269,15 @@ urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" [package.extras] security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] -socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] [[package]] -category = "main" -description = "A tool for converting between pip-style and pipfile requirements." name = "requirementslib" +version = "1.5.13" +description = "A tool for converting between pip-style and pipfile requirements." +category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -version = "1.5.13" [package.dependencies] appdirs = "*" @@ -1345,170 +1288,159 @@ orderedmultidict = "*" packaging = ">=19.0" pep517 = ">=0.5.0" pip-shims = ">=0.5.2" +plette = {version = "*", extras = ["validation"]} python-dateutil = "*" requests = "*" -setuptools = ">=40.8" six = ">=1.11.0" tomlkit = ">=0.5.3" vistir = ">=0.3.1" -[package.dependencies.plette] -extras = ["validation"] -version = "*" - [package.extras] dev = ["vulture", "flake8", "rope", "isort", "invoke", "twine", "pre-commit", "lxml", "towncrier", "parver", "flake8-bugbear", "black"] tests = ["mock", "pytest", "twine", "readme-renderer", "pytest-xdist", "pytest-cov", "pytest-timeout", "coverage", "hypothesis"] typing = ["typing", "mypy", "mypy-extensions", "mypytools", "pytype", "typed-ast", "monkeytype"] [[package]] -category = "dev" -description = "Validating URI References per RFC 3986" name = "rfc3986" +version = "1.4.0" +description = "Validating URI References per RFC 3986" +category = "dev" optional = false python-versions = "*" -version = "1.4.0" [package.extras] idna2008 = ["idna"] [[package]] -category = "dev" -description = "Checks installed dependencies for known vulnerabilities." name = "safety" +version = "1.9.0" +description = "Checks installed dependencies for known vulnerabilities." +category = "dev" optional = false python-versions = ">=3.5" -version = "1.9.0" [package.dependencies] Click = ">=6.0" dparse = ">=0.5.1" packaging = "*" requests = "*" -setuptools = "*" [[package]] -category = "main" -description = "Python 2 and 3 compatibility utilities" name = "six" +version = "1.15.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -version = "1.15.0" [[package]] -category = "dev" -description = "A pure Python implementation of a sliding window memory map manager" name = "smmap" +version = "3.0.4" +description = "A pure Python implementation of a sliding window memory map manager" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "3.0.4" [[package]] -category = "dev" -description = "A mirror package for smmap" name = "smmap2" +version = "3.0.1" +description = "A mirror package for smmap" +category = "dev" optional = false python-versions = "*" -version = "3.0.1" [package.dependencies] smmap = ">=3.0.1" [[package]] -category = "dev" -description = "Sniff out which async library your code is running under" name = "sniffio" +version = "1.1.0" +description = "Sniff out which async library your code is running under" +category = "dev" optional = false python-versions = ">=3.5" -version = "1.1.0" [package.dependencies] -[package.dependencies.contextvars] -python = "<3.7" -version = ">=2.1" +contextvars = {version = ">=2.1", markers = "python_version < \"3.7\""} [[package]] -category = "dev" -description = "This package provides 26 stemmers for 25 languages generated from Snowball algorithms." name = "snowballstemmer" +version = "2.0.0" +description = "This package provides 26 stemmers for 25 languages generated from Snowball algorithms." +category = "dev" optional = false python-versions = "*" -version = "2.0.0" [[package]] -category = "dev" -description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" name = "sortedcontainers" +version = "2.2.2" +description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +category = "dev" optional = false python-versions = "*" -version = "2.2.2" [[package]] -category = "dev" -description = "Manage dynamic plugins for Python applications" name = "stevedore" +version = "3.2.0" +description = "Manage dynamic plugins for Python applications" +category = "dev" optional = false python-versions = ">=3.6" -version = "3.2.0" [package.dependencies] +importlib-metadata = {version = ">=1.7.0", markers = "python_version < \"3.8\""} pbr = ">=2.0.0,<2.1.0 || >2.1.0" -[package.dependencies.importlib-metadata] -python = "<3.8" -version = ">=1.7.0" - [[package]] -category = "dev" -description = "The most basic Text::Unidecode port" name = "text-unidecode" +version = "1.3" +description = "The most basic Text::Unidecode port" +category = "dev" optional = false python-versions = "*" -version = "1.3" [[package]] -category = "main" -description = "Python Library for Tom's Obvious, Minimal Language" name = "toml" +version = "0.10.1" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "main" optional = false python-versions = "*" -version = "0.10.1" [[package]] -category = "main" -description = "Style preserving TOML library" name = "tomlkit" +version = "0.7.0" +description = "Style preserving TOML library" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "0.7.0" [[package]] -category = "dev" -description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." name = "tornado" +version = "6.0.4" +description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +category = "dev" optional = false python-versions = ">= 3.5" -version = "6.0.4" [[package]] -category = "dev" -description = "Fast, Extensible Progress Meter" -marker = "python_version > \"2.7\"" name = "tqdm" +version = "4.48.2" +description = "Fast, Extensible Progress Meter" +category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*" -version = "4.48.2" [package.extras] dev = ["py-make (>=0.1.0)", "twine", "argopt", "pydoc-markdown"] [[package]] -category = "dev" -description = "Traitlets Python config system" name = "traitlets" +version = "4.3.3" +description = "Traitlets Python config system" +category = "dev" optional = false python-versions = "*" -version = "4.3.3" [package.dependencies] decorator = "*" @@ -1519,70 +1451,70 @@ six = "*" test = ["pytest", "mock"] [[package]] -category = "dev" -description = "a fork of Python 2 and 3 ast modules with type comment support" name = "typed-ast" +version = "1.4.1" +description = "a fork of Python 2 and 3 ast modules with type comment support" +category = "dev" optional = false python-versions = "*" -version = "1.4.1" [[package]] -category = "dev" -description = "Typer, build great CLIs. Easy to code. Based on Python type hints." name = "typer" +version = "0.3.2" +description = "Typer, build great CLIs. Easy to code. Based on Python type hints." +category = "dev" optional = false python-versions = ">=3.6" -version = "0.3.2" [package.dependencies] click = ">=7.1.1,<7.2.0" [package.extras] +test = ["pytest-xdist (>=1.32.0,<2.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "mypy (==0.782)", "black (>=19.10b0,<20.0b0)", "isort (>=5.0.6,<6.0.0)", "shellingham (>=1.3.0,<2.0.0)", "pytest (>=4.4.0,<5.4.0)", "pytest-cov (>=2.10.0,<3.0.0)", "coverage (>=5.2,<6.0)"] all = ["colorama (>=0.4.3,<0.5.0)", "shellingham (>=1.3.0,<2.0.0)"] dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)"] doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=5.4.0,<6.0.0)", "markdown-include (>=0.5.1,<0.6.0)"] -test = ["pytest-xdist (>=1.32.0,<2.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "mypy (0.782)", "black (>=19.10b0,<20.0b0)", "isort (>=5.0.6,<6.0.0)", "shellingham (>=1.3.0,<2.0.0)", "pytest (>=4.4.0,<5.4.0)", "pytest-cov (>=2.10.0,<3.0.0)", "coverage (>=5.2,<6.0)"] [[package]] -category = "dev" -description = "Backported and Experimental Type Hints for Python 3.5+" name = "typing-extensions" +version = "3.7.4.3" +description = "Backported and Experimental Type Hints for Python 3.5+" +category = "dev" optional = false python-versions = "*" -version = "3.7.4.3" [[package]] -category = "dev" -description = "Runtime inspection utilities for typing module." name = "typing-inspect" +version = "0.6.0" +description = "Runtime inspection utilities for typing module." +category = "dev" optional = false python-versions = "*" -version = "0.6.0" [package.dependencies] mypy-extensions = ">=0.3.0" typing-extensions = ">=3.7.4" [[package]] -category = "main" -description = "HTTP library with thread-safe connection pooling, file post, and more." name = "urllib3" +version = "1.25.10" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" -version = "1.25.10" [package.extras] brotli = ["brotlipy (>=0.6.0)"] secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)", "ipaddress"] -socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] -category = "main" -description = "Miscellaneous utilities for dealing with filesystems, paths, projects, subprocesses, and more." name = "vistir" +version = "0.5.2" +description = "Miscellaneous utilities for dealing with filesystems, paths, projects, subprocesses, and more." +category = "main" optional = false python-versions = "!=3.0,!=3.1,!=3.2,!=3.3,>=2.7" -version = "0.5.2" [package.dependencies] colorama = ">=0.3.4,<0.4.2 || >0.4.2" @@ -1596,59 +1528,47 @@ tests = ["hypothesis", "hypothesis-fspaths", "pytest", "pytest-rerunfailures (<9 typing = ["typing", "mypy", "mypy-extensions", "mypytools", "pytype", "typed-ast"] [[package]] -category = "dev" -description = "Find dead code" name = "vulture" +version = "1.6" +description = "Find dead code" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "1.6" [[package]] -category = "dev" -description = "Measures the displayed width of unicode strings in a terminal" name = "wcwidth" -optional = false -python-versions = "*" version = "0.2.5" - -[[package]] -category = "main" -description = "A built-package format for Python" -name = "wheel" +description = "Measures the displayed width of unicode strings in a terminal" +category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -version = "0.35.1" - -[package.extras] -test = ["pytest (>=3.0.0)", "pytest-cov"] +python-versions = "*" [[package]] -category = "main" -description = "A semi hard Cornish cheese, also queries PyPI (PyPI client)" name = "yarg" +version = "0.1.9" +description = "A semi hard Cornish cheese, also queries PyPI (PyPI client)" +category = "main" optional = false python-versions = "*" -version = "0.1.9" [package.dependencies] requests = "*" [[package]] -category = "dev" -description = "Yet Another Terminal Spinner" name = "yaspin" +version = "0.15.0" +description = "Yet Another Terminal Spinner" +category = "dev" optional = false python-versions = "*" -version = "0.15.0" [[package]] -category = "main" -description = "Backport of pathlib-compatible object wrapper for zip files" -marker = "python_version < \"3.8\"" name = "zipp" +version = "3.1.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "main" optional = false python-versions = ">=3.6" -version = "3.1.0" [package.extras] docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] @@ -1660,9 +1580,9 @@ pipfile_deprecated_finder = ["pipreqs", "requirementslib"] requirements_deprecated_finder = ["pipreqs", "pip-api"] [metadata] -content-hash = "b0253934829c50ca3694ad1b04e96761dd3122140ccf34b251e8b1b91dea4ca6" -lock-version = "1.0" +lock-version = "1.1" python-versions = "^3.6" +content-hash = "363f453324e3010e63a4f5281f2fadbf74d958296d7b61d22758d771b137f546" [metadata.files] appdirs = [ @@ -1698,7 +1618,6 @@ binaryornot = [ {file = "binaryornot-0.4.4.tar.gz", hash = "sha256:359501dfc9d40632edc9fac890e19542db1a287bbcfa58175b66658392018061"}, ] black = [ - {file = "black-20.8b1-py3-none-any.whl", hash = "sha256:70b62ef1527c950db59062cda342ea224d772abdf6adc58b86a45421bab20a6b"}, {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, ] cached-property = [ @@ -1819,8 +1738,8 @@ falcon = [ {file = "falcon-2.0.0.tar.gz", hash = "sha256:eea593cf466b9c126ce667f6d30503624ef24459f118c75594a69353b6c3d5fc"}, ] flake8 = [ - {file = "flake8-3.8.3-py2.py3-none-any.whl", hash = "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c"}, - {file = "flake8-3.8.3.tar.gz", hash = "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208"}, + {file = "flake8-3.8.4-py2.py3-none-any.whl", hash = "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839"}, + {file = "flake8-3.8.4.tar.gz", hash = "sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b"}, ] flake8-bugbear = [ {file = "flake8-bugbear-19.8.0.tar.gz", hash = "sha256:d8c466ea79d5020cb20bf9f11cf349026e09517a42264f313d3f6fddb83e0571"}, @@ -2223,6 +2142,8 @@ pyyaml = [ {file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"}, {file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"}, {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"}, + {file = "PyYAML-5.3.1-cp39-cp39-win32.whl", hash = "sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a"}, + {file = "PyYAML-5.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:6034f55dab5fea9e53f436aa68fa3ace2634918e8b5994d82f3621c04ff5ed2e"}, {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, ] regex = [ @@ -2331,19 +2252,28 @@ typed-ast = [ {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, + {file = "typed_ast-1.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:fcf135e17cc74dbfbc05894ebca928ffeb23d9790b3167a674921db19082401f"}, {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, + {file = "typed_ast-1.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:f208eb7aff048f6bea9586e61af041ddf7f9ade7caed625742af423f6bae3298"}, {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, + {file = "typed_ast-1.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:7e4c9d7658aaa1fc80018593abdf8598bf91325af6af5cce4ce7c73bc45ea53d"}, {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, + {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c"}, + {file = "typed_ast-1.4.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072"}, + {file = "typed_ast-1.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91"}, + {file = "typed_ast-1.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:0d8110d78a5736e16e26213114a38ca35cb15b6515d535413b090bd50951556d"}, + {file = "typed_ast-1.4.1-cp39-cp39-win32.whl", hash = "sha256:b52ccf7cfe4ce2a1064b18594381bccf4179c2ecf7f513134ec2f993dd4ab395"}, + {file = "typed_ast-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:3742b32cf1c6ef124d57f95be609c473d7ec4c14d0090e5a5e05a15269fb4d0c"}, {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, ] typer = [ @@ -2376,10 +2306,6 @@ wcwidth = [ {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, ] -wheel = [ - {file = "wheel-0.35.1-py2.py3-none-any.whl", hash = "sha256:497add53525d16c173c2c1c733b8f655510e909ea78cc0e29d374243544b77a2"}, - {file = "wheel-0.35.1.tar.gz", hash = "sha256:99a22d87add3f634ff917310a3d87e499f19e663413a52eb9232c447aa646c9f"}, -] yarg = [ {file = "yarg-0.1.9-py2.py3-none-any.whl", hash = "sha256:4f9cebdc00fac946c9bf2783d634e538a71c7d280a4d806d45fd4dc0ef441492"}, {file = "yarg-0.1.9.tar.gz", hash = "sha256:55695bf4d1e3e7f756496c36a69ba32c40d18f821e38f61d028f6049e5e15911"}, diff --git a/pyproject.toml b/pyproject.toml index 840c64916..122906ec7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,6 +80,7 @@ gitdb2 = "^4.0.2" httpx = "^0.13.3" example_shared_isort_profile = "^0.0.1" example_isort_formatting_plugin = "^0.0.2" +flake8 = "^3.8.4" [tool.poetry.scripts] isort = "isort.main:main" diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 5fab49ffc..bd4c6ffa6 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -135,6 +135,22 @@ def test_show_files(capsys, tmpdir): main.main([str(tmpdir), "--show-files", "--show-config"]) +def test_missing_default_section(tmpdir): + config_file = tmpdir.join(".isort.cfg") + config_file.write( + """ +[settings] +sections=MADEUP +""" + ) + + python_file = tmpdir.join("file.py") + python_file.write("import os") + + with pytest.raises(SystemExit): + main.main([str(python_file)]) + + def test_main(capsys, tmpdir): base_args = [ "-sp", From 0566b4e23a2448c6b71c4f956f2b0006c11ed618 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 6 Dec 2020 22:53:38 -0800 Subject: [PATCH 1247/1439] Improve reporting of known errors in isort, reachieve 100% test coverage --- isort/exceptions.py | 17 +++++++++++++---- isort/main.py | 17 ++++------------- isort/parse.py | 5 +++++ 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/isort/exceptions.py b/isort/exceptions.py index b98454a2c..a73444ba5 100644 --- a/isort/exceptions.py +++ b/isort/exceptions.py @@ -163,9 +163,18 @@ def __init__(self, unsupported_settings: Dict[str, Dict[str, str]]): class UnsupportedEncoding(ISortError): """Raised when isort encounters an encoding error while trying to read a file""" - def __init__( - self, - filename: Union[str, Path], - ): + def __init__(self, filename: Union[str, Path]): super().__init__(f"Unknown or unsupported encoding in {filename}") self.filename = filename + + +class MissingSection(ISortError): + """Raised when isort encounters an import that matches a section that is not defined""" + + def __init__(self, import_module: str, section: str): + super().__init__( + f"Found {import_module} import while parsing, but {section} was not included " + "in the `sections` setting of your config. Please add it before continuing\n" + "See https://pycqa.github.io/isort/#custom-sections-and-ordering " + "for more info." + ) diff --git a/isort/main.py b/isort/main.py index 5352f64f8..ffc887871 100644 --- a/isort/main.py +++ b/isort/main.py @@ -11,11 +11,11 @@ from warnings import warn from . import __version__, api, sections -from .exceptions import FileSkipped, UnsupportedEncoding +from .exceptions import FileSkipped, ISortError, UnsupportedEncoding from .format import create_terminal_printer from .logo import ASCII_ART from .profiles import profiles -from .settings import DEFAULT_CONFIG, VALID_PY_TARGETS, Config, WrapModes +from .settings import VALID_PY_TARGETS, Config, WrapModes try: from .setuptools_commands import ISortCommand # noqa: F401 @@ -110,17 +110,8 @@ def sort_imports( if config.verbose: warn(f"Encoding not supported for {file_name}") return SortAttempt(incorrectly_sorted, skipped, False) - except KeyError as error: - if error.args[0] not in DEFAULT_CONFIG.sections: - _print_hard_fail(config, offending_file=file_name) - raise - msg = ( - f"Found {error} imports while parsing, but {error} was not included " - "in the `sections` setting of your config. Please add it before continuing\n" - "See https://pycqa.github.io/isort/#custom-sections-and-ordering " - "for more info." - ) - _print_hard_fail(config, message=msg) + except ISortError as error: + _print_hard_fail(config, message=str(error)) sys.exit(os.EX_CONFIG) except Exception: _print_hard_fail(config, offending_file=file_name) diff --git a/isort/parse.py b/isort/parse.py index 819c5cd85..d93f559e2 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -8,6 +8,7 @@ from . import place from .comments import parse as parse_comments from .deprecated.finders import FindersManager +from .exceptions import MissingSection from .settings import DEFAULT_CONFIG, Config if TYPE_CHECKING: @@ -524,6 +525,10 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte " Do you need to define a default section?" ) imports.setdefault("", {"straight": OrderedDict(), "from": OrderedDict()}) + + if placed_module and placed_module not in imports: + raise MissingSection(import_module=module, section=placed_module) + straight_import |= imports[placed_module][type_of_import].get( # type: ignore module, False ) From 347a6473a680acb5849427b9528918440a131118 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 7 Dec 2020 22:11:28 -0800 Subject: [PATCH 1248/1439] Exit 1 --- isort/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/main.py b/isort/main.py index ffc887871..a0aedcd17 100644 --- a/isort/main.py +++ b/isort/main.py @@ -112,7 +112,7 @@ def sort_imports( return SortAttempt(incorrectly_sorted, skipped, False) except ISortError as error: _print_hard_fail(config, message=str(error)) - sys.exit(os.EX_CONFIG) + sys.exit(1) except Exception: _print_hard_fail(config, offending_file=file_name) raise From 576f2a5fe310158ad0039bd7e9d4ece82575cf09 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 7 Dec 2020 23:46:43 -0800 Subject: [PATCH 1249/1439] Add dedupe_imports setting --- isort/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/isort/settings.py b/isort/settings.py index f0bd14b2d..b4bc50df3 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -204,6 +204,7 @@ class _Config: auto_identify_namespace_packages: bool = True namespace_packages: FrozenSet[str] = frozenset() follow_links: bool = True + dedupe_imports: bool = True def __post_init__(self): py_version = self.py_version From e1741cd540ef9ad2e7b50217cc86daad6d061ce6 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 7 Dec 2020 23:49:11 -0800 Subject: [PATCH 1250/1439] Add dedupe setting to CLI --- isort/main.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/isort/main.py b/isort/main.py index a0aedcd17..085cfccfa 100644 --- a/isort/main.py +++ b/isort/main.py @@ -660,6 +660,12 @@ def _build_arg_parser() -> argparse.ArgumentParser: dest="ext_format", help="Tells isort to format the given files according to an extensions formatting rules.", ) + output_group.add_argument( + "--dedupe-imports", + dest="dedupe_imports", + help="Tells isort to dedupe duplicated imports that are seen at the root across import blocks." + action="store_true" + ) section_group.add_argument( "--sd", From a0fdec008a685c05f37f91ad4c4e181f5b6922f7 Mon Sep 17 00:00:00 2001 From: Quentin Santos Date: Tue, 8 Dec 2020 18:23:13 +0100 Subject: [PATCH 1251/1439] Fix description of config lookup --- docs/configuration/config_files.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/configuration/config_files.md b/docs/configuration/config_files.md index 7cda9cd35..814b1ec63 100644 --- a/docs/configuration/config_files.md +++ b/docs/configuration/config_files.md @@ -5,6 +5,7 @@ isort supports various standard config formats to allow customizations to be int When applying configurations, isort looks for the closest supported config file, in the order files are listed below. You can manually specify the settings file or path by setting `--settings-path` from the command-line. Otherwise, isort will traverse up to 25 parent directories until it finds a suitable config file. +Note that isort will not leave a git or Mercurial repository (checking for a `.git` or `.hg` directory). As soon as it finds a file, it stops looking. The config file search is done relative to the current directory if `isort .` or a file stream is passed in, or relative to the first path passed in if multiple paths are passed in. isort **never** merges config files together due to the confusion it can cause. From 6c786d7729e208142237c87b7c8d53b5ab50e034 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 8 Dec 2020 23:21:49 -0800 Subject: [PATCH 1252/1439] Add missing comma --- isort/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/main.py b/isort/main.py index 085cfccfa..ea9dc9386 100644 --- a/isort/main.py +++ b/isort/main.py @@ -663,7 +663,7 @@ def _build_arg_parser() -> argparse.ArgumentParser: output_group.add_argument( "--dedupe-imports", dest="dedupe_imports", - help="Tells isort to dedupe duplicated imports that are seen at the root across import blocks." + help="Tells isort to dedupe duplicated imports that are seen at the root across import blocks.", action="store_true" ) From a6c694ba1df58dd6d629800bbd7054d2987a5816 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 8 Dec 2020 23:35:22 -0800 Subject: [PATCH 1253/1439] Remove comment lines from import identification --- isort/core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/isort/core.py b/isort/core.py index 27e615414..d587f3d01 100644 --- a/isort/core.py +++ b/isort/core.py @@ -358,6 +358,7 @@ def write(self, *a, **kw): li for li in parsed_content.in_lines if li and li not in lines_without_imports_set + and not li.lstrip().startswith("#") ) sorted_import_section = output.sorted_imports( From c8afa9a4b1de5e8b2babb19162198dff9a9394e0 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 8 Dec 2020 23:38:46 -0800 Subject: [PATCH 1254/1439] Ensure line stays < 100 characters --- isort/main.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/isort/main.py b/isort/main.py index ea9dc9386..632e171f3 100644 --- a/isort/main.py +++ b/isort/main.py @@ -663,8 +663,9 @@ def _build_arg_parser() -> argparse.ArgumentParser: output_group.add_argument( "--dedupe-imports", dest="dedupe_imports", - help="Tells isort to dedupe duplicated imports that are seen at the root across import blocks.", - action="store_true" + help="Tells isort to dedupe duplicated imports that are seen at the root across " + "import blocks.", + action="store_true", ) section_group.add_argument( From d1ce7d3f0c72cb3a84c512b79e2d33eddb929739 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 8 Dec 2020 23:38:55 -0800 Subject: [PATCH 1255/1439] Ensure line stays < 100 characters --- isort/core.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/isort/core.py b/isort/core.py index d587f3d01..93102e856 100644 --- a/isort/core.py +++ b/isort/core.py @@ -357,7 +357,8 @@ def write(self, *a, **kw): all_imports.extend( li for li in parsed_content.in_lines - if li and li not in lines_without_imports_set + if li + and li not in lines_without_imports_set and not li.lstrip().startswith("#") ) From 8647eb08fb52466620e3392969ab56cf79426a35 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 9 Dec 2020 23:18:08 -0800 Subject: [PATCH 1256/1439] Add test for #1604: allow toggling section header in indented imports --- tests/unit/test_ticketed_features.py | 48 ++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/tests/unit/test_ticketed_features.py b/tests/unit/test_ticketed_features.py index 7871e38c5..03998a474 100644 --- a/tests/unit/test_ticketed_features.py +++ b/tests/unit/test_ticketed_features.py @@ -923,3 +923,51 @@ def one(): import os """ ) + + +def test_indented_import_headings_issue_1604(): + """Test to ensure it is possible to toggle import headings on indented import sections + See: https://github.com/PyCQA/isort/issues/1604 + """ + assert ( + isort.code( + """ +import numpy as np + + +def function(): + import numpy as np +""", + import_heading_thirdparty="External imports", + ) + == """ +# External imports +import numpy as np + + +def function(): + # External imports + import numpy as np +""" + ) + assert ( + isort.code( + """ +import numpy as np + + +def function(): + import numpy as np +""", + import_heading_thirdparty="External imports", + indented_import_headings=False, + ) + == """ +# External imports +import numpy as np + + +def function(): + import numpy as np +""" + ) From 32dee154ce0eef0897de005101bed6d9175c8ee8 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 9 Dec 2020 23:18:27 -0800 Subject: [PATCH 1257/1439] Add setting for #1604: allow toggling section header in indented imports --- isort/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/isort/settings.py b/isort/settings.py index b4bc50df3..8ef6dfa86 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -205,6 +205,7 @@ class _Config: namespace_packages: FrozenSet[str] = frozenset() follow_links: bool = True dedupe_imports: bool = True + indented_import_headings: bool = True def __post_init__(self): py_version = self.py_version From 1f52edd2ebd8e6061edfdb208d5514e5d51a5da0 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 10 Dec 2020 23:19:11 -0800 Subject: [PATCH 1258/1439] Dynamically toggle import_headings as configured in settings --- isort/core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/isort/core.py b/isort/core.py index 93102e856..e53a8b87e 100644 --- a/isort/core.py +++ b/isort/core.py @@ -441,6 +441,7 @@ def _indented_config(config: Config, indent: str): line_length=max(config.line_length - len(indent), 0), wrap_length=max(config.wrap_length - len(indent), 0), lines_after_imports=1, + import_headings=config.import_headings if config.indented_import_headings else {}, ) From a28f6a155b2f52af5f38e5a14ef8c0a6a256ab94 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 10 Dec 2020 23:20:12 -0800 Subject: [PATCH 1259/1439] Resolved #1604: Added to changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e81a6877..476038fe3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ - Implemented #1575: Support for automatically fixing mixed indentation of import sections. - Implemented #1582: Added a CLI option for skipping symlinks. - Implemented #1603: Support for disabling float_to_top from the command line. + - Implemented #1604: Allow toggling section comments on and off for indented import sections. ### 5.6.4 October 12, 2020 - Fixed #1556: Empty line added between imports that should be skipped. From d9c3f88c3852c2ef9e327c216507e218913a32e4 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 11 Dec 2020 23:56:57 -0800 Subject: [PATCH 1260/1439] Mark 5.7.0 for december release --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 476038fe3..ac6d209c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. Find out more about isort's release policy [here](https://pycqa.github.io/isort/docs/major_releases/release_policy/). -### 5.7.0 TBD +### 5.7.0 December TBD - Implemented #1596: Provide ways for extension formatting and file paths to be specified when using streaming input from CLI. - Implemented #1583: Ability to output and diff within a single API call to `isort.file`. - Implemented #1562, #1592 & #1593: Better more useful fatal error messages. From bd170de3c2a680570ab1e83edadf82944909bd25 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 12 Dec 2020 23:52:13 -0800 Subject: [PATCH 1261/1439] Fix datadog integration test --- tests/integration/test_projects_using_isort.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/test_projects_using_isort.py b/tests/integration/test_projects_using_isort.py index 2a2abe398..6818addf5 100644 --- a/tests/integration/test_projects_using_isort.py +++ b/tests/integration/test_projects_using_isort.py @@ -126,7 +126,7 @@ def test_attrs(tmpdir): def test_datadog_integrations_core(tmpdir): git_clone("https://github.com/DataDog/integrations-core.git", tmpdir) - run_isort([str(tmpdir)]) + run_isort([str(tmpdir), '--skip', 'docs']) def test_pyramid(tmpdir): From 6b65b278960be551b972db57bd47607af02be0bd Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 13 Dec 2020 00:37:24 -0800 Subject: [PATCH 1262/1439] linting --- tests/integration/test_projects_using_isort.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/test_projects_using_isort.py b/tests/integration/test_projects_using_isort.py index 6818addf5..2258cbfae 100644 --- a/tests/integration/test_projects_using_isort.py +++ b/tests/integration/test_projects_using_isort.py @@ -126,7 +126,7 @@ def test_attrs(tmpdir): def test_datadog_integrations_core(tmpdir): git_clone("https://github.com/DataDog/integrations-core.git", tmpdir) - run_isort([str(tmpdir), '--skip', 'docs']) + run_isort([str(tmpdir), "--skip", "docs"]) def test_pyramid(tmpdir): From 71b058d6751d26bec36223970494da075df5a1b8 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 13 Dec 2020 18:24:50 -0800 Subject: [PATCH 1263/1439] Initial work to pull out just import identification from parse --- isort/identify.py | 213 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 isort/identify.py diff --git a/isort/identify.py b/isort/identify.py new file mode 100644 index 000000000..98e75b310 --- /dev/null +++ b/isort/identify.py @@ -0,0 +1,213 @@ +"""""" +from collections import OrderedDict, defaultdict +from functools import partial +from itertools import chain +from typing import TYPE_CHECKING, Any, Dict, List, NamedTuple, Optional, Tuple +from warnings import warn + +from . import place +from .comments import parse as parse_comments +from .deprecated.finders import FindersManager +from .exceptions import MissingSection +from .settings import DEFAULT_CONFIG, Config + + +from isort.parse import _infer_line_separator, _normalize_line, _strip_syntax, skip_line + + +def import_type(line: str, config: Config = DEFAULT_CONFIG) -> Optional[str]: + """If the current line is an import line it will return its type (from or straight)""" + if line.startswith(("import ", "cimport ")): + return "straight" + if line.startswith("from "): + return "from" + return None + + +class ImportIdentified(NamedTuple): + from_file: Optional[Path] + line: int + + +def imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG) -> Iterator[ImportIdentified]: + """Parses a python file taking out and categorizing imports.""" + in_quote = "" + + indexed_input = enumerate(input_stream) + for index, line in indexed_input + statement_index = index + (skipping_line, in_quote) = skip_line( + line, in_quote=in_quote, index=index, section_comments=config.section_comments + ) + + if skipping_line: + continue + + line, *end_of_line_comment = line.split("#", 1) + if ";" in line: + statements = [line.strip() for line in line.split(";")] + else: + statements = [line] + if end_of_line_comment: + statements[-1] = f"{statements[-1]}#{end_of_line_comment[0]}" + + for statement in statements: + line, raw_line = _normalize_line(statement) + type_of_import = import_type(line, config) or "" + if not type_of_import: + out_lines.append(raw_line) + continue + + import_string, _ = parse_comments(line) + line_parts = [part for part in _strip_syntax(import_string).strip().split(" ") if part] + + if "(" in line.split("#", 1)[0]: + while not line.split("#")[0].strip().endswith(")"): + try: + index, next_line = next(indexed_input) + except StopIteration: + break + + line, _ = parse_comments(next_line) + stripped_line = _strip_syntax(line).strip() + import_string += line_separator + line + else: + while line.strip().endswith("\\"): + index, next_line = next(indexed_input) + line, _ = parse_comments(next_line) + + # Still need to check for parentheses after an escaped line + if ( + "(" in line.split("#")[0] + and ")" not in line.split("#")[0] + ): + stripped_line = _strip_syntax(line).strip() + import_string += line_separator + line + + while not line.split("#")[0].strip().endswith(")"): + try: + index, next_line = next(indexed_input) + except StopIteration: + break + line, _ = parse_comments(next_line) + stripped_line = _strip_syntax(line).strip() + import_string += line_separator + line + + stripped_line = _strip_syntax(line).strip() + if import_string.strip().endswith( + (" import", " cimport") + ) or line.strip().startswith(("import ", "cimport ")): + import_string += line_separator + line + else: + import_string = import_string.rstrip().rstrip("\\") + " " + line.lstrip() + + if type_of_import == "from": + cimports: bool + import_string = ( + import_string.replace("import(", "import (") + .replace("\\", " ") + .replace("\n", " ") + ) + if " cimport " in import_string: + parts = import_string.split(" cimport ") + cimports = True + + else: + parts = import_string.split(" import ") + cimports = False + + from_import = parts[0].split(" ") + import_string = (" cimport " if cimports else " import ").join( + [from_import[0] + " " + "".join(from_import[1:])] + parts[1:] + ) + + just_imports = [ + item.replace("{|", "{ ").replace("|}", " }") + for item in _strip_syntax(import_string).split() + ] + + direct_imports = just_imports[1:] + straight_import = True + top_level_module = "" + if "as" in just_imports and (just_imports.index("as") + 1) < len(just_imports): + straight_import = False + while "as" in just_imports: + nested_module = None + as_index = just_imports.index("as") + if type_of_import == "from": + nested_module = just_imports[as_index - 1] + top_level_module = just_imports[0] + module = top_level_module + "." + nested_module + as_name = just_imports[as_index + 1] + direct_imports.remove(nested_module) + direct_imports.remove(as_name) + direct_imports.remove("as") + if nested_module == as_name and config.remove_redundant_aliases: + pass + elif as_name not in as_map["from"][module]: + as_map["from"][module].append(as_name) + + full_name = f"{nested_module} as {as_name}" + else: + module = just_imports[as_index - 1] + as_name = just_imports[as_index + 1] + if module == as_name and config.remove_redundant_aliases: + pass + elif as_name not in as_map["straight"][module]: + as_map["straight"][module].append(as_name) + + del just_imports[as_index : as_index + 2] + + if type_of_import == "from": + import_from = just_imports.pop(0) + placed_module = finder(import_from) + if config.verbose and not config.only_modified: + print(f"from-type place_module for {import_from} returned {placed_module}") + + elif config.verbose: + verbose_output.append( + f"from-type place_module for {import_from} returned {placed_module}" + ) + if placed_module == "": + warn( + f"could not place module {import_from} of line {line} --" + " Do you need to define a default section?" + ) + root = imports[placed_module][type_of_import] # type: ignore + + if import_from not in root: + root[import_from] = OrderedDict( + (module, module in direct_imports) for module in just_imports + ) + else: + root[import_from].update( + (module, root[import_from].get(module, False) or module in direct_imports) + for module in just_imports + ) + + else: + + for module in just_imports: + + placed_module = finder(module) + if config.verbose and not config.only_modified: + print(f"else-type place_module for {module} returned {placed_module}") + + elif config.verbose: + verbose_output.append( + f"else-type place_module for {module} returned {placed_module}" + ) + if placed_module == "": + warn( + f"could not place module {module} of line {line} --" + " Do you need to define a default section?" + ) + imports.setdefault("", {"straight": OrderedDict(), "from": OrderedDict()}) + + if placed_module and placed_module not in imports: + raise MissingSection(import_module=module, section=placed_module) + + straight_import |= imports[placed_module][type_of_import].get( # type: ignore + module, False + ) + imports[placed_module][type_of_import][module] = straight_import # type: ignore From 6fe8f0ce4a5c0c8e9a72e335693a5e5ae7c8152e Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 13 Dec 2020 18:26:10 -0800 Subject: [PATCH 1264/1439] Remove pieces of parsing unrelated to import identification --- isort/identify.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/isort/identify.py b/isort/identify.py index 98e75b310..09383aa3a 100644 --- a/isort/identify.py +++ b/isort/identify.py @@ -188,7 +188,6 @@ def imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG) -> Iterator[I else: for module in just_imports: - placed_module = finder(module) if config.verbose and not config.only_modified: print(f"else-type place_module for {module} returned {placed_module}") @@ -203,10 +202,6 @@ def imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG) -> Iterator[I " Do you need to define a default section?" ) imports.setdefault("", {"straight": OrderedDict(), "from": OrderedDict()}) - - if placed_module and placed_module not in imports: - raise MissingSection(import_module=module, section=placed_module) - straight_import |= imports[placed_module][type_of_import].get( # type: ignore module, False ) From 8624101427f08b1ea6a5368040fa62b921ee224b Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Mon, 14 Dec 2020 14:53:12 -0800 Subject: [PATCH 1265/1439] Set pre-commit language_version to python3 For details on how pre-commit chooses a default language version, see: https://pre-commit.com/#overriding-language-version > For each language, they default to using the system installed language On some platforms this could be Python 2. As isort is Python-3-only, this should be enforced in the pre-commit hook. As this isn't currently listed, some projects have been forced to override it. --- .pre-commit-hooks.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 0027e49df..160016181 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -3,5 +3,6 @@ entry: isort require_serial: true language: python + language_version: python3 types: [python] args: ['--filter-files'] From d1d1d9d83b3bd31d0c12b5798fb29564df4f6931 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Mon, 14 Dec 2020 15:07:41 -0800 Subject: [PATCH 1266/1439] Simplify .pre-commit-config.yaml Run isort on all files to avoid listing them. --- .pre-commit-config.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6ff5151ab..586a5edda 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,6 +3,3 @@ repos: rev: 5.5.2 hooks: - id: isort - files: 'isort/.*' - - id: isort - files: 'tests/.*' From 10ef221e74d2ee4bc1ab8a8cfb7261d886e78263 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 14 Dec 2020 23:34:15 -0800 Subject: [PATCH 1267/1439] Support for yielding imports for straight forward case --- isort/identify.py | 28 ++++++---------------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/isort/identify.py b/isort/identify.py index 09383aa3a..3a7633a50 100644 --- a/isort/identify.py +++ b/isort/identify.py @@ -7,7 +7,6 @@ from . import place from .comments import parse as parse_comments -from .deprecated.finders import FindersManager from .exceptions import MissingSection from .settings import DEFAULT_CONFIG, Config @@ -25,9 +24,12 @@ def import_type(line: str, config: Config = DEFAULT_CONFIG) -> Optional[str]: class ImportIdentified(NamedTuple): - from_file: Optional[Path] line: int - + module: str + import_type: str + alias: Optional[str] = None + src: Optional[Path] = None + def imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG) -> Iterator[ImportIdentified]: """Parses a python file taking out and categorizing imports.""" @@ -186,23 +188,5 @@ def imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG) -> Iterator[I ) else: - for module in just_imports: - placed_module = finder(module) - if config.verbose and not config.only_modified: - print(f"else-type place_module for {module} returned {placed_module}") - - elif config.verbose: - verbose_output.append( - f"else-type place_module for {module} returned {placed_module}" - ) - if placed_module == "": - warn( - f"could not place module {module} of line {line} --" - " Do you need to define a default section?" - ) - imports.setdefault("", {"straight": OrderedDict(), "from": OrderedDict()}) - straight_import |= imports[placed_module][type_of_import].get( # type: ignore - module, False - ) - imports[placed_module][type_of_import][module] = straight_import # type: ignore + yield ImportIdentified(index, module, import_type) From 0ace9b3bcb3ff4f3f6b0c56f4ed150adbcabd3be Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 14 Dec 2020 23:44:59 -0800 Subject: [PATCH 1268/1439] Initial code cleanup and linting of identify module --- isort/identify.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/isort/identify.py b/isort/identify.py index 3a7633a50..de07dccf4 100644 --- a/isort/identify.py +++ b/isort/identify.py @@ -1,17 +1,13 @@ """""" -from collections import OrderedDict, defaultdict -from functools import partial -from itertools import chain -from typing import TYPE_CHECKING, Any, Dict, List, NamedTuple, Optional, Tuple +from typing import NamedTuple, Optional from warnings import warn - -from . import place from .comments import parse as parse_comments -from .exceptions import MissingSection from .settings import DEFAULT_CONFIG, Config +from pathlib import Path +from isort.parse import _normalize_line, _strip_syntax, skip_line -from isort.parse import _infer_line_separator, _normalize_line, _strip_syntax, skip_line +from typing import TextIO def import_type(line: str, config: Config = DEFAULT_CONFIG) -> Optional[str]: @@ -36,7 +32,7 @@ def imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG) -> Iterator[I in_quote = "" indexed_input = enumerate(input_stream) - for index, line in indexed_input + for index, line in indexed_input: statement_index = index (skipping_line, in_quote) = skip_line( line, in_quote=in_quote, index=index, section_comments=config.section_comments @@ -129,10 +125,8 @@ def imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG) -> Iterator[I ] direct_imports = just_imports[1:] - straight_import = True top_level_module = "" if "as" in just_imports and (just_imports.index("as") + 1) < len(just_imports): - straight_import = False while "as" in just_imports: nested_module = None as_index = just_imports.index("as") From 4eaec9fee426b06a5ecb008ae6c0c96c2c42c8f7 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 15 Dec 2020 20:49:37 -0800 Subject: [PATCH 1269/1439] Remove parsing aspects not needed for fast identification --- isort/identify.py | 48 ++++++++--------------------------------------- 1 file changed, 8 insertions(+), 40 deletions(-) diff --git a/isort/identify.py b/isort/identify.py index de07dccf4..3da348e64 100644 --- a/isort/identify.py +++ b/isort/identify.py @@ -1,13 +1,12 @@ """""" from typing import NamedTuple, Optional -from warnings import warn from .comments import parse as parse_comments from .settings import DEFAULT_CONFIG, Config from pathlib import Path from isort.parse import _normalize_line, _strip_syntax, skip_line -from typing import TextIO +from typing import TextIO, Iterator def import_type(line: str, config: Config = DEFAULT_CONFIG) -> Optional[str]: @@ -25,7 +24,7 @@ class ImportIdentified(NamedTuple): import_type: str alias: Optional[str] = None src: Optional[Path] = None - + def imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG) -> Iterator[ImportIdentified]: """Parses a python file taking out and categorizing imports.""" @@ -33,7 +32,6 @@ def imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG) -> Iterator[I indexed_input = enumerate(input_stream) for index, line in indexed_input: - statement_index = index (skipping_line, in_quote) = skip_line( line, in_quote=in_quote, index=index, section_comments=config.section_comments ) @@ -53,11 +51,9 @@ def imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG) -> Iterator[I line, raw_line = _normalize_line(statement) type_of_import = import_type(line, config) or "" if not type_of_import: - out_lines.append(raw_line) continue import_string, _ = parse_comments(line) - line_parts = [part for part in _strip_syntax(import_string).strip().split(" ") if part] if "(" in line.split("#", 1)[0]: while not line.split("#")[0].strip().endswith(")"): @@ -67,8 +63,7 @@ def imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG) -> Iterator[I break line, _ = parse_comments(next_line) - stripped_line = _strip_syntax(line).strip() - import_string += line_separator + line + import_string += "\n" + line else: while line.strip().endswith("\\"): index, next_line = next(indexed_input) @@ -79,8 +74,7 @@ def imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG) -> Iterator[I "(" in line.split("#")[0] and ")" not in line.split("#")[0] ): - stripped_line = _strip_syntax(line).strip() - import_string += line_separator + line + import_string += "\n" + line while not line.split("#")[0].strip().endswith(")"): try: @@ -88,14 +82,12 @@ def imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG) -> Iterator[I except StopIteration: break line, _ = parse_comments(next_line) - stripped_line = _strip_syntax(line).strip() - import_string += line_separator + line + import_string += "\n" + line - stripped_line = _strip_syntax(line).strip() if import_string.strip().endswith( (" import", " cimport") ) or line.strip().startswith(("import ", "cimport ")): - import_string += line_separator + line + import_string += "\n" + line else: import_string = import_string.rstrip().rstrip("\\") + " " + line.lstrip() @@ -143,7 +135,6 @@ def imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG) -> Iterator[I elif as_name not in as_map["from"][module]: as_map["from"][module].append(as_name) - full_name = f"{nested_module} as {as_name}" else: module = just_imports[as_index - 1] as_name = just_imports[as_index + 1] @@ -156,31 +147,8 @@ def imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG) -> Iterator[I if type_of_import == "from": import_from = just_imports.pop(0) - placed_module = finder(import_from) - if config.verbose and not config.only_modified: - print(f"from-type place_module for {import_from} returned {placed_module}") - - elif config.verbose: - verbose_output.append( - f"from-type place_module for {import_from} returned {placed_module}" - ) - if placed_module == "": - warn( - f"could not place module {import_from} of line {line} --" - " Do you need to define a default section?" - ) - root = imports[placed_module][type_of_import] # type: ignore - - if import_from not in root: - root[import_from] = OrderedDict( - (module, module in direct_imports) for module in just_imports - ) - else: - root[import_from].update( - (module, root[import_from].get(module, False) or module in direct_imports) - for module in just_imports - ) - + for import_part in just_imports: + yield ImportIdentified(index, f"{import_from}.{import_part}", import_type) else: for module in just_imports: yield ImportIdentified(index, module, import_type) From fffe8bf4174459b335617fb0d4ad72dac2ff5708 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 15 Dec 2020 21:27:07 -0800 Subject: [PATCH 1270/1439] Simplify output signature --- isort/identify.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/isort/identify.py b/isort/identify.py index 3da348e64..745489eb1 100644 --- a/isort/identify.py +++ b/isort/identify.py @@ -21,7 +21,7 @@ def import_type(line: str, config: Config = DEFAULT_CONFIG) -> Optional[str]: class ImportIdentified(NamedTuple): line: int module: str - import_type: str + attribute: str = None alias: Optional[str] = None src: Optional[Path] = None @@ -146,9 +146,9 @@ def imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG) -> Iterator[I del just_imports[as_index : as_index + 2] if type_of_import == "from": - import_from = just_imports.pop(0) - for import_part in just_imports: - yield ImportIdentified(index, f"{import_from}.{import_part}", import_type) + module = just_imports.pop(0) + for attribute in just_imports: + yield ImportIdentified(index, module, attribute) else: for module in just_imports: - yield ImportIdentified(index, module, import_type) + yield ImportIdentified(index, module) From 06d1bddca5b754eb1bcc9f0c3358271f7e8df37e Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 15 Dec 2020 21:48:07 -0800 Subject: [PATCH 1271/1439] Return aliases --- isort/identify.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/isort/identify.py b/isort/identify.py index 745489eb1..74bb0a3e1 100644 --- a/isort/identify.py +++ b/isort/identify.py @@ -24,6 +24,7 @@ class ImportIdentified(NamedTuple): attribute: str = None alias: Optional[str] = None src: Optional[Path] = None + cimport: bool = False def imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG) -> Iterator[ImportIdentified]: @@ -120,20 +121,25 @@ def imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG) -> Iterator[I top_level_module = "" if "as" in just_imports and (just_imports.index("as") + 1) < len(just_imports): while "as" in just_imports: - nested_module = None + attribute = None as_index = just_imports.index("as") if type_of_import == "from": - nested_module = just_imports[as_index - 1] + attribute = just_imports[as_index - 1] top_level_module = just_imports[0] - module = top_level_module + "." + nested_module - as_name = just_imports[as_index + 1] - direct_imports.remove(nested_module) - direct_imports.remove(as_name) + module = top_level_module + "." + attribute + alias = just_imports[as_index + 1] + direct_imports.remove(attribute) + direct_imports.remove(alias) direct_imports.remove("as") - if nested_module == as_name and config.remove_redundant_aliases: + if attribute == alias and config.remove_redundant_aliases: pass - elif as_name not in as_map["from"][module]: - as_map["from"][module].append(as_name) + else: + yield ImportIdentified( + index, + top_level_module, + attribute, + alias=alias + ) else: module = just_imports[as_index - 1] From e65a2b2c663154fcd14cce83691dffd7ecb58b1d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 16 Dec 2020 23:40:46 -0800 Subject: [PATCH 1272/1439] clean up alias import identification --- isort/identify.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/isort/identify.py b/isort/identify.py index 74bb0a3e1..7d2d4a567 100644 --- a/isort/identify.py +++ b/isort/identify.py @@ -143,18 +143,15 @@ def imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG) -> Iterator[I else: module = just_imports[as_index - 1] - as_name = just_imports[as_index + 1] - if module == as_name and config.remove_redundant_aliases: - pass - elif as_name not in as_map["straight"][module]: - as_map["straight"][module].append(as_name) - - del just_imports[as_index : as_index + 2] + alias = just_imports[as_index + 1] + if not (module == alias and config.remove_redundant_aliases): + yield ImportIdentified(index, module, alias) - if type_of_import == "from": - module = just_imports.pop(0) - for attribute in just_imports: - yield ImportIdentified(index, module, attribute) else: - for module in just_imports: - yield ImportIdentified(index, module) + if type_of_import == "from": + module = just_imports.pop(0) + for attribute in just_imports: + yield ImportIdentified(index, module, attribute) + else: + for module in just_imports: + yield ImportIdentified(index, module) From 334577d786f8fc8a839800d46cae3ac21cff7dc1 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 16 Dec 2020 23:53:56 -0800 Subject: [PATCH 1273/1439] Add quick cimport identification --- isort/identify.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/isort/identify.py b/isort/identify.py index 7d2d4a567..fa41309a1 100644 --- a/isort/identify.py +++ b/isort/identify.py @@ -19,7 +19,7 @@ def import_type(line: str, config: Config = DEFAULT_CONFIG) -> Optional[str]: class ImportIdentified(NamedTuple): - line: int + line_number: int module: str attribute: str = None alias: Optional[str] = None @@ -55,6 +55,8 @@ def imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG) -> Iterator[I continue import_string, _ = parse_comments(line) + normalized_import_string = import_string.replace("import(", "import (").replace("\\", " ").replace("\n", " ") + cimports: bool = " cimport " in normalized_import_string or normalized_import_string.startswith("cimport") if "(" in line.split("#", 1)[0]: while not line.split("#")[0].strip().endswith(")"): @@ -93,19 +95,12 @@ def imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG) -> Iterator[I import_string = import_string.rstrip().rstrip("\\") + " " + line.lstrip() if type_of_import == "from": - cimports: bool import_string = ( import_string.replace("import(", "import (") .replace("\\", " ") .replace("\n", " ") ) - if " cimport " in import_string: - parts = import_string.split(" cimport ") - cimports = True - - else: - parts = import_string.split(" import ") - cimports = False + parts = import_string.split(" cimport " if cimports else " import ") from_import = parts[0].split(" ") import_string = (" cimport " if cimports else " import ").join( @@ -138,14 +133,15 @@ def imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG) -> Iterator[I index, top_level_module, attribute, - alias=alias + alias=alias, + cimport=cimports, ) else: module = just_imports[as_index - 1] alias = just_imports[as_index + 1] if not (module == alias and config.remove_redundant_aliases): - yield ImportIdentified(index, module, alias) + yield ImportIdentified(index, module, alias, cimports) else: if type_of_import == "from": @@ -154,4 +150,4 @@ def imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG) -> Iterator[I yield ImportIdentified(index, module, attribute) else: for module in just_imports: - yield ImportIdentified(index, module) + yield ImportIdentified(index, module, cimport=cimports) From 333eaaa720f2c6b51d7744c0f283935324cfab59 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 16 Dec 2020 23:56:24 -0800 Subject: [PATCH 1274/1439] Autoformatting --- isort/identify.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/isort/identify.py b/isort/identify.py index fa41309a1..6672ccf1e 100644 --- a/isort/identify.py +++ b/isort/identify.py @@ -1,12 +1,11 @@ """""" -from typing import NamedTuple, Optional -from .comments import parse as parse_comments -from .settings import DEFAULT_CONFIG, Config from pathlib import Path +from typing import Iterator, NamedTuple, Optional, TextIO from isort.parse import _normalize_line, _strip_syntax, skip_line -from typing import TextIO, Iterator +from .comments import parse as parse_comments +from .settings import DEFAULT_CONFIG, Config def import_type(line: str, config: Config = DEFAULT_CONFIG) -> Optional[str]: @@ -55,8 +54,13 @@ def imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG) -> Iterator[I continue import_string, _ = parse_comments(line) - normalized_import_string = import_string.replace("import(", "import (").replace("\\", " ").replace("\n", " ") - cimports: bool = " cimport " in normalized_import_string or normalized_import_string.startswith("cimport") + normalized_import_string = ( + import_string.replace("import(", "import (").replace("\\", " ").replace("\n", " ") + ) + cimports: bool = ( + " cimport " in normalized_import_string + or normalized_import_string.startswith("cimport") + ) if "(" in line.split("#", 1)[0]: while not line.split("#")[0].strip().endswith(")"): @@ -73,10 +77,7 @@ def imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG) -> Iterator[I line, _ = parse_comments(next_line) # Still need to check for parentheses after an escaped line - if ( - "(" in line.split("#")[0] - and ")" not in line.split("#")[0] - ): + if "(" in line.split("#")[0] and ")" not in line.split("#")[0]: import_string += "\n" + line while not line.split("#")[0].strip().endswith(")"): From 31d35705c9e49ae5146fdfb6cfc36b97f350e602 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 16 Dec 2020 23:57:05 -0800 Subject: [PATCH 1275/1439] Autoformatting --- isort/identify.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/identify.py b/isort/identify.py index 6672ccf1e..3c610c9d2 100644 --- a/isort/identify.py +++ b/isort/identify.py @@ -20,10 +20,10 @@ def import_type(line: str, config: Config = DEFAULT_CONFIG) -> Optional[str]: class ImportIdentified(NamedTuple): line_number: int module: str - attribute: str = None + attribute: Optional[str] = None alias: Optional[str] = None src: Optional[Path] = None - cimport: bool = False + cimport: Optional[bool] = False def imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG) -> Iterator[ImportIdentified]: From 76be45d844cf553040211aa2a254ae8baad9441b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 16 Dec 2020 23:59:00 -0800 Subject: [PATCH 1276/1439] Fix type signatures --- isort/identify.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/identify.py b/isort/identify.py index 3c610c9d2..280e181c2 100644 --- a/isort/identify.py +++ b/isort/identify.py @@ -23,7 +23,7 @@ class ImportIdentified(NamedTuple): attribute: Optional[str] = None alias: Optional[str] = None src: Optional[Path] = None - cimport: Optional[bool] = False + cimport: bool = False def imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG) -> Iterator[ImportIdentified]: @@ -142,7 +142,7 @@ def imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG) -> Iterator[I module = just_imports[as_index - 1] alias = just_imports[as_index + 1] if not (module == alias and config.remove_redundant_aliases): - yield ImportIdentified(index, module, alias, cimports) + yield ImportIdentified(index, module, alias, cimport=cimports) else: if type_of_import == "from": From 9f23d03f575694fd136f013377cc228c395c1ea2 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 17 Dec 2020 21:54:20 -0800 Subject: [PATCH 1277/1439] Refactoring of direct imports --- isort/identify.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/isort/identify.py b/isort/identify.py index 280e181c2..af3bd45da 100644 --- a/isort/identify.py +++ b/isort/identify.py @@ -10,9 +10,9 @@ def import_type(line: str, config: Config = DEFAULT_CONFIG) -> Optional[str]: """If the current line is an import line it will return its type (from or straight)""" - if line.startswith(("import ", "cimport ")): + if line.lstrip().startswith(("import ", "cimport ")): return "straight" - if line.startswith("from "): + if line.lstrip().startswith("from "): return "from" return None @@ -127,6 +127,7 @@ def imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG) -> Iterator[I direct_imports.remove(attribute) direct_imports.remove(alias) direct_imports.remove("as") + just_imports[1:] = direct_imports if attribute == alias and config.remove_redundant_aliases: pass else: From 151844cc7078fd53c7a7b95f8b4d5de850a114ab Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 17 Dec 2020 21:54:54 -0800 Subject: [PATCH 1278/1439] Rename identified import class --- isort/identify.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/isort/identify.py b/isort/identify.py index af3bd45da..22ce4b28d 100644 --- a/isort/identify.py +++ b/isort/identify.py @@ -17,7 +17,7 @@ def import_type(line: str, config: Config = DEFAULT_CONFIG) -> Optional[str]: return None -class ImportIdentified(NamedTuple): +class IdentifiedImport(NamedTuple): line_number: int module: str attribute: Optional[str] = None @@ -26,7 +26,7 @@ class ImportIdentified(NamedTuple): cimport: bool = False -def imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG) -> Iterator[ImportIdentified]: +def imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG) -> Iterator[IdentifiedImport]: """Parses a python file taking out and categorizing imports.""" in_quote = "" @@ -131,7 +131,7 @@ def imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG) -> Iterator[I if attribute == alias and config.remove_redundant_aliases: pass else: - yield ImportIdentified( + yield IdentifiedImport( index, top_level_module, attribute, @@ -143,13 +143,13 @@ def imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG) -> Iterator[I module = just_imports[as_index - 1] alias = just_imports[as_index + 1] if not (module == alias and config.remove_redundant_aliases): - yield ImportIdentified(index, module, alias, cimport=cimports) + yield IdentifiedImport(index, module, alias, cimport=cimports) else: if type_of_import == "from": module = just_imports.pop(0) for attribute in just_imports: - yield ImportIdentified(index, module, attribute) + yield IdentifiedImport(index, module, attribute) else: for module in just_imports: - yield ImportIdentified(index, module, cimport=cimports) + yield IdentifiedImport(index, module, cimport=cimports) From 24593c0d6acfc9008da99bf85600f8e10ba35339 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 17 Dec 2020 21:59:29 -0800 Subject: [PATCH 1279/1439] Add indented classifier to imports --- isort/identify.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/isort/identify.py b/isort/identify.py index 22ce4b28d..1813cb3a0 100644 --- a/isort/identify.py +++ b/isort/identify.py @@ -19,6 +19,7 @@ def import_type(line: str, config: Config = DEFAULT_CONFIG) -> Optional[str]: class IdentifiedImport(NamedTuple): line_number: int + indented: bool module: str attribute: Optional[str] = None alias: Optional[str] = None @@ -40,10 +41,8 @@ def imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG) -> Iterator[I continue line, *end_of_line_comment = line.split("#", 1) - if ";" in line: - statements = [line.strip() for line in line.split(";")] - else: - statements = [line] + indented = line.startswith(" ") or line.startswith("\n") + statements = [line.strip() for line in line.split(";")] if end_of_line_comment: statements[-1] = f"{statements[-1]}#{end_of_line_comment[0]}" @@ -133,6 +132,7 @@ def imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG) -> Iterator[I else: yield IdentifiedImport( index, + indented, top_level_module, attribute, alias=alias, @@ -143,13 +143,13 @@ def imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG) -> Iterator[I module = just_imports[as_index - 1] alias = just_imports[as_index + 1] if not (module == alias and config.remove_redundant_aliases): - yield IdentifiedImport(index, module, alias, cimport=cimports) + yield IdentifiedImport(index, indented, module, alias, cimport=cimports) else: if type_of_import == "from": module = just_imports.pop(0) for attribute in just_imports: - yield IdentifiedImport(index, module, attribute) + yield IdentifiedImport(index, indented, module, attribute) else: for module in just_imports: - yield IdentifiedImport(index, module, cimport=cimports) + yield IdentifiedImport(index, indented, module, cimport=cimports) From 7bd317143c36ca28860daf71eb9a65b86c57d368 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 17 Dec 2020 22:02:24 -0800 Subject: [PATCH 1280/1439] Include file path --- isort/identify.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/isort/identify.py b/isort/identify.py index 1813cb3a0..ebb1fd3e6 100644 --- a/isort/identify.py +++ b/isort/identify.py @@ -23,11 +23,11 @@ class IdentifiedImport(NamedTuple): module: str attribute: Optional[str] = None alias: Optional[str] = None - src: Optional[Path] = None cimport: bool = False + file_path: Optional[Path] = None -def imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG) -> Iterator[IdentifiedImport]: +def imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG, file_path: Optional[Path]=None) -> Iterator[IdentifiedImport]: """Parses a python file taking out and categorizing imports.""" in_quote = "" @@ -137,19 +137,20 @@ def imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG) -> Iterator[I attribute, alias=alias, cimport=cimports, + file_path=file_path, ) else: module = just_imports[as_index - 1] alias = just_imports[as_index + 1] if not (module == alias and config.remove_redundant_aliases): - yield IdentifiedImport(index, indented, module, alias, cimport=cimports) + yield IdentifiedImport(index, indented, module, alias, cimport=cimports, file_path=file_path,) else: if type_of_import == "from": module = just_imports.pop(0) for attribute in just_imports: - yield IdentifiedImport(index, indented, module, attribute) + yield IdentifiedImport(index, indented, module, attribute, file_path=file_path,) else: for module in just_imports: - yield IdentifiedImport(index, indented, module, cimport=cimports) + yield IdentifiedImport(index, indented, module, cimport=cimports, file_path=file_path,) From 53ff355317901406de3544cdb943269f57eb74a1 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 17 Dec 2020 22:02:41 -0800 Subject: [PATCH 1281/1439] Reformatted with black+isort --- isort/identify.py | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/isort/identify.py b/isort/identify.py index ebb1fd3e6..3e0779a06 100644 --- a/isort/identify.py +++ b/isort/identify.py @@ -27,7 +27,9 @@ class IdentifiedImport(NamedTuple): file_path: Optional[Path] = None -def imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG, file_path: Optional[Path]=None) -> Iterator[IdentifiedImport]: +def imports( + input_stream: TextIO, config: Config = DEFAULT_CONFIG, file_path: Optional[Path] = None +) -> Iterator[IdentifiedImport]: """Parses a python file taking out and categorizing imports.""" in_quote = "" @@ -144,13 +146,32 @@ def imports(input_stream: TextIO, config: Config = DEFAULT_CONFIG, file_path: Op module = just_imports[as_index - 1] alias = just_imports[as_index + 1] if not (module == alias and config.remove_redundant_aliases): - yield IdentifiedImport(index, indented, module, alias, cimport=cimports, file_path=file_path,) + yield IdentifiedImport( + index, + indented, + module, + alias, + cimport=cimports, + file_path=file_path, + ) else: if type_of_import == "from": module = just_imports.pop(0) for attribute in just_imports: - yield IdentifiedImport(index, indented, module, attribute, file_path=file_path,) + yield IdentifiedImport( + index, + indented, + module, + attribute, + file_path=file_path, + ) else: for module in just_imports: - yield IdentifiedImport(index, indented, module, cimport=cimports, file_path=file_path,) + yield IdentifiedImport( + index, + indented, + module, + cimport=cimports, + file_path=file_path, + ) From 9cef6bdcf5b85fa477e1b0bd4fc55968bc62c5ac Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 17 Dec 2020 22:07:04 -0800 Subject: [PATCH 1282/1439] Reuse identified import logic --- isort/identify.py | 31 ++++++++++--------------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/isort/identify.py b/isort/identify.py index 3e0779a06..77954aa9b 100644 --- a/isort/identify.py +++ b/isort/identify.py @@ -5,6 +5,7 @@ from isort.parse import _normalize_line, _strip_syntax, skip_line from .comments import parse as parse_comments +from functools import partial from .settings import DEFAULT_CONFIG, Config @@ -43,7 +44,7 @@ def imports( continue line, *end_of_line_comment = line.split("#", 1) - indented = line.startswith(" ") or line.startswith("\n") + identified_import = partial(IdentifiedImport, index, line.startswith(" ") or line.startswith("\n"), file_path=file_path) statements = [line.strip() for line in line.split(";")] if end_of_line_comment: statements[-1] = f"{statements[-1]}#{end_of_line_comment[0]}" @@ -132,46 +133,34 @@ def imports( if attribute == alias and config.remove_redundant_aliases: pass else: - yield IdentifiedImport( - index, - indented, + yield identified_import( top_level_module, attribute, alias=alias, - cimport=cimports, - file_path=file_path, + cimport=cimports ) else: module = just_imports[as_index - 1] alias = just_imports[as_index + 1] if not (module == alias and config.remove_redundant_aliases): - yield IdentifiedImport( - index, - indented, + yield identified_import( module, alias, - cimport=cimports, - file_path=file_path, + cimport=cimports ) else: if type_of_import == "from": module = just_imports.pop(0) for attribute in just_imports: - yield IdentifiedImport( - index, - indented, + yield identified_import( module, - attribute, - file_path=file_path, + attribute ) else: for module in just_imports: - yield IdentifiedImport( - index, - indented, + yield identified_import( module, - cimport=cimports, - file_path=file_path, + cimport=cimports ) From 1e4270aa548fbebd5f3b4a5a8eb513f604f1165b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 17 Dec 2020 22:07:19 -0800 Subject: [PATCH 1283/1439] black+isort formatting --- isort/identify.py | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/isort/identify.py b/isort/identify.py index 77954aa9b..a9281a0d3 100644 --- a/isort/identify.py +++ b/isort/identify.py @@ -1,11 +1,11 @@ """""" +from functools import partial from pathlib import Path from typing import Iterator, NamedTuple, Optional, TextIO from isort.parse import _normalize_line, _strip_syntax, skip_line from .comments import parse as parse_comments -from functools import partial from .settings import DEFAULT_CONFIG, Config @@ -44,7 +44,12 @@ def imports( continue line, *end_of_line_comment = line.split("#", 1) - identified_import = partial(IdentifiedImport, index, line.startswith(" ") or line.startswith("\n"), file_path=file_path) + identified_import = partial( + IdentifiedImport, + index, + line.startswith(" ") or line.startswith("\n"), + file_path=file_path, + ) statements = [line.strip() for line in line.split(";")] if end_of_line_comment: statements[-1] = f"{statements[-1]}#{end_of_line_comment[0]}" @@ -134,33 +139,20 @@ def imports( pass else: yield identified_import( - top_level_module, - attribute, - alias=alias, - cimport=cimports + top_level_module, attribute, alias=alias, cimport=cimports ) else: module = just_imports[as_index - 1] alias = just_imports[as_index + 1] if not (module == alias and config.remove_redundant_aliases): - yield identified_import( - module, - alias, - cimport=cimports - ) + yield identified_import(module, alias, cimport=cimports) else: if type_of_import == "from": module = just_imports.pop(0) for attribute in just_imports: - yield identified_import( - module, - attribute - ) + yield identified_import(module, attribute) else: for module in just_imports: - yield identified_import( - module, - cimport=cimports - ) + yield identified_import(module, cimport=cimports) From 9a706c5caec184d939b621a1cef81269f064cd4e Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 17 Dec 2020 22:15:11 -0800 Subject: [PATCH 1284/1439] Include statement --- isort/identify.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/isort/identify.py b/isort/identify.py index a9281a0d3..94ee56f64 100644 --- a/isort/identify.py +++ b/isort/identify.py @@ -21,6 +21,7 @@ def import_type(line: str, config: Config = DEFAULT_CONFIG) -> Optional[str]: class IdentifiedImport(NamedTuple): line_number: int indented: bool + statement: str module: str attribute: Optional[str] = None alias: Optional[str] = None @@ -44,12 +45,6 @@ def imports( continue line, *end_of_line_comment = line.split("#", 1) - identified_import = partial( - IdentifiedImport, - index, - line.startswith(" ") or line.startswith("\n"), - file_path=file_path, - ) statements = [line.strip() for line in line.split(";")] if end_of_line_comment: statements[-1] = f"{statements[-1]}#{end_of_line_comment[0]}" @@ -68,6 +63,14 @@ def imports( " cimport " in normalized_import_string or normalized_import_string.startswith("cimport") ) + identified_import = partial( + IdentifiedImport, + index, + line.startswith(" ") or line.startswith("\n"), + statement, + cimport=cimports, + file_path=file_path, + ) if "(" in line.split("#", 1)[0]: while not line.split("#")[0].strip().endswith(")"): @@ -139,20 +142,20 @@ def imports( pass else: yield identified_import( - top_level_module, attribute, alias=alias, cimport=cimports + top_level_module, attribute, alias=alias ) else: module = just_imports[as_index - 1] alias = just_imports[as_index + 1] if not (module == alias and config.remove_redundant_aliases): - yield identified_import(module, alias, cimport=cimports) + yield identified_import(module, alias) - else: + if just_imports: if type_of_import == "from": module = just_imports.pop(0) for attribute in just_imports: yield identified_import(module, attribute) else: for module in just_imports: - yield identified_import(module, cimport=cimports) + yield identified_import(module) From 8d8e737a35add11f1f8e95b5bed8f2d2d1ae5ee0 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 18 Dec 2020 00:56:44 -0800 Subject: [PATCH 1285/1439] Simplify import type identification --- isort/identify.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/isort/identify.py b/isort/identify.py index 94ee56f64..83865a140 100644 --- a/isort/identify.py +++ b/isort/identify.py @@ -9,15 +9,6 @@ from .settings import DEFAULT_CONFIG, Config -def import_type(line: str, config: Config = DEFAULT_CONFIG) -> Optional[str]: - """If the current line is an import line it will return its type (from or straight)""" - if line.lstrip().startswith(("import ", "cimport ")): - return "straight" - if line.lstrip().startswith("from "): - return "from" - return None - - class IdentifiedImport(NamedTuple): line_number: int indented: bool @@ -51,8 +42,11 @@ def imports( for statement in statements: line, raw_line = _normalize_line(statement) - type_of_import = import_type(line, config) or "" - if not type_of_import: + if line.lstrip().startswith(("import ", "cimport ")): + type_of_import = "straight" + if line.lstrip().startswith("from "): + type_of_import = "from" + else: continue import_string, _ = parse_comments(line) From 55307009e8bb92a5508e6b56a155adc709af0a96 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 18 Dec 2020 00:58:17 -0800 Subject: [PATCH 1286/1439] Add doc string for identify.py --- isort/identify.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/isort/identify.py b/isort/identify.py index 83865a140..339d6e894 100644 --- a/isort/identify.py +++ b/isort/identify.py @@ -1,4 +1,6 @@ -"""""" +"""Fast stream based import identification. +Eventually this will likely replace parse.py +""" from functools import partial from pathlib import Path from typing import Iterator, NamedTuple, Optional, TextIO From 66fde254430a0499486109cd2192cf5b24fdb328 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 18 Dec 2020 01:07:51 -0800 Subject: [PATCH 1287/1439] Add importidentification str support --- isort/identify.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/isort/identify.py b/isort/identify.py index 339d6e894..56a58d338 100644 --- a/isort/identify.py +++ b/isort/identify.py @@ -21,6 +21,16 @@ class IdentifiedImport(NamedTuple): cimport: bool = False file_path: Optional[Path] = None + def __str__(self): + full_path = ".".join(self.module.split(".") + self.attribute.split(".")) + if self.alias: + full_path = f"{full_path} as {self.alias}" + return ( + f"{self.file_path or ''}:{self.line_number} " + f"{'indented ' if self.indented else ''}" + f"{'cimport' if self.cimport else 'import'} {full_path}" + ) + def imports( input_stream: TextIO, config: Config = DEFAULT_CONFIG, file_path: Optional[Path] = None @@ -137,9 +147,7 @@ def imports( if attribute == alias and config.remove_redundant_aliases: pass else: - yield identified_import( - top_level_module, attribute, alias=alias - ) + yield identified_import(top_level_module, attribute, alias=alias) else: module = just_imports[as_index - 1] From 5e32a4f986a6e1d587de0a7108bc80a13add349c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 18 Dec 2020 01:49:06 -0800 Subject: [PATCH 1288/1439] Improved identify imports stringification --- isort/identify.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/isort/identify.py b/isort/identify.py index 56a58d338..79055c005 100644 --- a/isort/identify.py +++ b/isort/identify.py @@ -22,9 +22,11 @@ class IdentifiedImport(NamedTuple): file_path: Optional[Path] = None def __str__(self): - full_path = ".".join(self.module.split(".") + self.attribute.split(".")) + full_path = self.module + if self.attribute: + full_path += f".{self.attribute}" if self.alias: - full_path = f"{full_path} as {self.alias}" + full_path += " as {self.alias}" return ( f"{self.file_path or ''}:{self.line_number} " f"{'indented ' if self.indented else ''}" From 73827c0389de1c248863197f36670c499ad23075 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 18 Dec 2020 23:51:42 -0800 Subject: [PATCH 1289/1439] Remove statement from initial identified import contract --- isort/identify.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/isort/identify.py b/isort/identify.py index 79055c005..4e4e81ca7 100644 --- a/isort/identify.py +++ b/isort/identify.py @@ -14,7 +14,6 @@ class IdentifiedImport(NamedTuple): line_number: int indented: bool - statement: str module: str attribute: Optional[str] = None alias: Optional[str] = None @@ -75,7 +74,6 @@ def imports( IdentifiedImport, index, line.startswith(" ") or line.startswith("\n"), - statement, cimport=cimports, file_path=file_path, ) From d86e3a38cff78a845293a5ae4e990992eeb80bc3 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 18 Dec 2020 23:59:19 -0800 Subject: [PATCH 1290/1439] Move file identification to standalone module' --- isort/main.py | 44 +++----------------------------------------- 1 file changed, 3 insertions(+), 41 deletions(-) diff --git a/isort/main.py b/isort/main.py index 632e171f3..58071c451 100644 --- a/isort/main.py +++ b/isort/main.py @@ -7,10 +7,10 @@ from gettext import gettext as _ from io import TextIOWrapper from pathlib import Path -from typing import Any, Dict, Iterable, Iterator, List, Optional, Sequence, Set +from typing import Any, Dict, List, Optional, Sequence from warnings import warn -from . import __version__, api, sections +from . import __version__, api, sections, files from .exceptions import FileSkipped, ISortError, UnsupportedEncoding from .format import create_terminal_printer from .logo import ASCII_ART @@ -131,44 +131,6 @@ def _print_hard_fail( printer.error(message) -def iter_source_code( - paths: Iterable[str], config: Config, skipped: List[str], broken: List[str] -) -> Iterator[str]: - """Iterate over all Python source files defined in paths.""" - visited_dirs: Set[Path] = set() - - for path in paths: - if os.path.isdir(path): - for dirpath, dirnames, filenames in os.walk( - path, topdown=True, followlinks=config.follow_links - ): - base_path = Path(dirpath) - for dirname in list(dirnames): - full_path = base_path / dirname - resolved_path = full_path.resolve() - if config.is_skipped(full_path): - skipped.append(dirname) - dirnames.remove(dirname) - else: - if resolved_path in visited_dirs: # pragma: no cover - if not config.quiet: - warn(f"Likely recursive symlink detected to {resolved_path}") - dirnames.remove(dirname) - visited_dirs.add(resolved_path) - - for filename in filenames: - filepath = os.path.join(dirpath, filename) - if config.is_supported_filetype(filepath): - if config.is_skipped(Path(filepath)): - skipped.append(filename) - else: - yield filepath - elif not os.path.exists(path): - broken.append(path) - else: - yield path - - def _build_arg_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( description="Sort Python import definitions alphabetically " @@ -1017,7 +979,7 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = filtered_files.append(file_name) file_names = filtered_files - file_names = iter_source_code(file_names, config, skipped, broken) + file_names = files.find(file_names, config, skipped, broken) if show_files: for file_name in file_names: print(file_name) From a1d004255aa2a9b2c1668af0776d2af22aae10a6 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 18 Dec 2020 23:59:58 -0800 Subject: [PATCH 1291/1439] Add files module --- isort/files.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 isort/files.py diff --git a/isort/files.py b/isort/files.py new file mode 100644 index 000000000..dfb326e55 --- /dev/null +++ b/isort/files.py @@ -0,0 +1,43 @@ +import os +from typing import Iterable,List,Iterator,Set +from isort.settings import Config +from pathlib import Path + + +def find( + paths: Iterable[str], config: Config, skipped: List[str], broken: List[str] +) -> Iterator[str]: + """Fines and provides an iterator for all Python source files defined in paths.""" + visited_dirs: Set[Path] = set() + + for path in paths: + if os.path.isdir(path): + for dirpath, dirnames, filenames in os.walk( + path, topdown=True, followlinks=config.follow_links + ): + base_path = Path(dirpath) + for dirname in list(dirnames): + full_path = base_path / dirname + resolved_path = full_path.resolve() + if config.is_skipped(full_path): + skipped.append(dirname) + dirnames.remove(dirname) + else: + if resolved_path in visited_dirs: # pragma: no cover + if not config.quiet: + warn(f"Likely recursive symlink detected to {resolved_path}") + dirnames.remove(dirname) + visited_dirs.add(resolved_path) + + for filename in filenames: + filepath = os.path.join(dirpath, filename) + if config.is_supported_filetype(filepath): + if config.is_skipped(Path(filepath)): + skipped.append(filename) + else: + yield filepath + elif not os.path.exists(path): + broken.append(path) + else: + yield path + From f106209fa681248f1eb1e8f62a7830a6d0de5bc7 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 19 Dec 2020 00:00:13 -0800 Subject: [PATCH 1292/1439] isort+black --- isort/files.py | 6 +++--- isort/main.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/isort/files.py b/isort/files.py index dfb326e55..224215315 100644 --- a/isort/files.py +++ b/isort/files.py @@ -1,7 +1,8 @@ import os -from typing import Iterable,List,Iterator,Set -from isort.settings import Config from pathlib import Path +from typing import Iterable, Iterator, List, Set + +from isort.settings import Config def find( @@ -40,4 +41,3 @@ def find( broken.append(path) else: yield path - diff --git a/isort/main.py b/isort/main.py index 58071c451..7d1bc3d9b 100644 --- a/isort/main.py +++ b/isort/main.py @@ -10,7 +10,7 @@ from typing import Any, Dict, List, Optional, Sequence from warnings import warn -from . import __version__, api, sections, files +from . import __version__, api, files, sections from .exceptions import FileSkipped, ISortError, UnsupportedEncoding from .format import create_terminal_printer from .logo import ASCII_ART From eb7b9163b18469fd2770af1de28c7a886ad1c03d Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sat, 19 Dec 2020 15:23:14 -0800 Subject: [PATCH 1293/1439] Add pyi and cython file support to .pre-commit-hooks.yaml Since pre-commit 2.9.0 (2020-11-21), the types_or key can be used to match multiple disparate file types. For more upstream details, see: https://github.com/pre-commit/pre-commit/issues/607 Fixes #402 --- .pre-commit-hooks.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 160016181..773b505fd 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -4,5 +4,5 @@ require_serial: true language: python language_version: python3 - types: [python] + types_or: [cython, pyi, python] args: ['--filter-files'] From 52b90b33e3843185df38038cf65311460ee22779 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 19 Dec 2020 21:19:56 -0800 Subject: [PATCH 1294/1439] identify.IdentifiedImport -> identify.Import --- isort/identify.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/isort/identify.py b/isort/identify.py index 4e4e81ca7..696cb3718 100644 --- a/isort/identify.py +++ b/isort/identify.py @@ -11,7 +11,7 @@ from .settings import DEFAULT_CONFIG, Config -class IdentifiedImport(NamedTuple): +class Import(NamedTuple): line_number: int indented: bool module: str @@ -35,7 +35,7 @@ def __str__(self): def imports( input_stream: TextIO, config: Config = DEFAULT_CONFIG, file_path: Optional[Path] = None -) -> Iterator[IdentifiedImport]: +) -> Iterator[Import]: """Parses a python file taking out and categorizing imports.""" in_quote = "" @@ -71,7 +71,7 @@ def imports( or normalized_import_string.startswith("cimport") ) identified_import = partial( - IdentifiedImport, + Import, index, line.startswith(" ") or line.startswith("\n"), cimport=cimports, From f6a18f40527abf9a0e1e16c16c8f1e7019e19bb5 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 19 Dec 2020 22:07:29 -0800 Subject: [PATCH 1295/1439] Add statement method to IdentifiedImport --- isort/api.py | 82 ++++++++++++++++++++--------------------------- isort/identify.py | 8 +++-- 2 files changed, 40 insertions(+), 50 deletions(-) diff --git a/isort/api.py b/isort/api.py index 502994b66..a178758a7 100644 --- a/isort/api.py +++ b/isort/api.py @@ -2,12 +2,12 @@ import sys from io import StringIO from pathlib import Path -from typing import Optional, TextIO, Union, cast +from typing import Optional, TextIO, Union, cast, Iterator from warnings import warn from isort import core -from . import io +from . import io, identify from .exceptions import ( ExistingSyntaxErrors, FileSkipComment, @@ -394,86 +394,74 @@ def sort_file( return changed -def get_imports_string( +def imports_in_code_string( code: str, - extension: Optional[str] = None, config: Config = DEFAULT_CONFIG, - file_path: Optional[Path] = None, + file_path: Opitonal[Path] = None, + unique: bool = False, **config_kwargs, -) -> str: - """Finds all imports within the provided code string, returning a new string with them. +) -> Iterator[identify.Import]: + """Finds and returns all imports within the provided code string. - **code**: The string of code with imports that need to be sorted. - - **extension**: The file extension that contains imports. Defaults to filename extension or py. - **config**: The config object to use when sorting imports. - **file_path**: The disk location where the code string was pulled from. + - **unique**: If True, only the first instance of an import is returned. - ****config_kwargs**: Any config modifications. """ - input_stream = StringIO(code) - output_stream = StringIO() - config = _config(path=file_path, config=config, **config_kwargs) - get_imports_stream( - input_stream, - output_stream, - extension=extension, - config=config, - file_path=file_path, - ) - output_stream.seek(0) - return output_stream.read() + yield from imports_in_stream(input_stream=StringIO(code), config=config, file_path=file_path, unique=unique, **config_kwargs) -def get_imports_stream( +def imports_in_stream( input_stream: TextIO, - output_stream: TextIO, - extension: Optional[str] = None, config: Config = DEFAULT_CONFIG, file_path: Optional[Path] = None, + unique: bool = False, **config_kwargs, -) -> None: - """Finds all imports within the provided code stream, outputs to the provided output stream. +) -> Iterator[identify.Import]: + """Finds and returns all imports within the provided code stream. - **input_stream**: The stream of code with imports that need to be sorted. - - **output_stream**: The stream where sorted imports should be written to. - - **extension**: The file extension that contains imports. Defaults to filename extension or py. - **config**: The config object to use when sorting imports. - **file_path**: The disk location where the code string was pulled from. + - **unique**: If True, only the first instance of an import is returned. - ****config_kwargs**: Any config modifications. """ config = _config(path=file_path, config=config, **config_kwargs) - core.process( - input_stream, - output_stream, - extension=extension or (file_path and file_path.suffix.lstrip(".")) or "py", - config=config, - imports_only=True, - ) + identified_imports = identify.imports(input_stream, config=config, file_path=file_path) + if not unique: + yield from identified_imports + + seen = set() + for identified_import in identified_imports: + key = identified_import.statement() + if key not in seen: + seen.add(key) + yield identified_import -def get_imports_file( +def imports_in_file( filename: Union[str, Path], - output_stream: TextIO, - extension: Optional[str] = None, config: Config = DEFAULT_CONFIG, file_path: Optional[Path] = None, + unique: bool = False, **config_kwargs, -) -> None: - """Finds all imports within the provided file, outputs to the provided output stream. +) -> Iterator[identify.Import]: + """Finds and returns all imports within the provided source file. - - **filename**: The name or Path of the file to check. - - **output_stream**: The stream where sorted imports should be written to. + - **filename**: The name or Path of the file to look for imports in. - **extension**: The file extension that contains imports. Defaults to filename extension or py. - **config**: The config object to use when sorting imports. - **file_path**: The disk location where the code string was pulled from. + - **unique**: If True, only the first instance of an import is returned. - ****config_kwargs**: Any config modifications. """ with io.File.read(filename) as source_file: - get_imports_stream( - source_file.stream, - output_stream, - extension, - config, - file_path, + yield from imports_in_stream( + input_stream=source_file.stream, + config=config, + file_path=file_path, + unique=unique, **config_kwargs, ) diff --git a/isort/identify.py b/isort/identify.py index 696cb3718..558f46937 100644 --- a/isort/identify.py +++ b/isort/identify.py @@ -20,16 +20,18 @@ class Import(NamedTuple): cimport: bool = False file_path: Optional[Path] = None - def __str__(self): + def statement(self) -> str: full_path = self.module if self.attribute: full_path += f".{self.attribute}" if self.alias: full_path += " as {self.alias}" + return f"{'cimport' if self.cimport else 'import'} {full_path}" + + def __str__(self): return ( f"{self.file_path or ''}:{self.line_number} " - f"{'indented ' if self.indented else ''}" - f"{'cimport' if self.cimport else 'import'} {full_path}" + f"{'indented ' if self.indented else ''}{self.statement()}" ) From 39b8f7528792e1126ead4fbe2e65bbb07bc431ea Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 19 Dec 2020 22:08:44 -0800 Subject: [PATCH 1296/1439] Fix typo with Optional --- isort/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/api.py b/isort/api.py index a178758a7..d4b4e739f 100644 --- a/isort/api.py +++ b/isort/api.py @@ -397,7 +397,7 @@ def sort_file( def imports_in_code_string( code: str, config: Config = DEFAULT_CONFIG, - file_path: Opitonal[Path] = None, + file_path: Optional[Path] = None, unique: bool = False, **config_kwargs, ) -> Iterator[identify.Import]: From a4ec137db50720b817186bbc3f43d0345eb77db2 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 19 Dec 2020 22:11:01 -0800 Subject: [PATCH 1297/1439] Export imports_in methods to __init__ --- isort/__init__.py | 6 +++--- isort/api.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/isort/__init__.py b/isort/__init__.py index b800a03de..0277199cc 100644 --- a/isort/__init__.py +++ b/isort/__init__.py @@ -5,9 +5,9 @@ from .api import ( check_file, check_stream, - get_imports_file, - get_imports_stream, - get_imports_string, + imports_in_file, + imports_in_stream, + imports_in_code, place_module, place_module_with_reason, ) diff --git a/isort/api.py b/isort/api.py index d4b4e739f..af5ead881 100644 --- a/isort/api.py +++ b/isort/api.py @@ -394,7 +394,7 @@ def sort_file( return changed -def imports_in_code_string( +def imports_in_code( code: str, config: Config = DEFAULT_CONFIG, file_path: Optional[Path] = None, From b0c92df90f467535908c8c7551acefe0f144ee71 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 19 Dec 2020 22:12:51 -0800 Subject: [PATCH 1298/1439] Missing f for f'string' --- isort/identify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/identify.py b/isort/identify.py index 558f46937..9f6cc9467 100644 --- a/isort/identify.py +++ b/isort/identify.py @@ -25,7 +25,7 @@ def statement(self) -> str: if self.attribute: full_path += f".{self.attribute}" if self.alias: - full_path += " as {self.alias}" + full_path += f" as {self.alias}" return f"{'cimport' if self.cimport else 'import'} {full_path}" def __str__(self): From 74527afdbd6541cd30de5706a23ea0141f975800 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 19 Dec 2020 22:23:20 -0800 Subject: [PATCH 1299/1439] isort+black --- isort/__init__.py | 2 +- isort/api.py | 16 +++++++++++----- isort/files.py | 1 + isort/main.py | 7 +++++-- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/isort/__init__.py b/isort/__init__.py index 0277199cc..dbe2c895e 100644 --- a/isort/__init__.py +++ b/isort/__init__.py @@ -5,9 +5,9 @@ from .api import ( check_file, check_stream, + imports_in_code, imports_in_file, imports_in_stream, - imports_in_code, place_module, place_module_with_reason, ) diff --git a/isort/api.py b/isort/api.py index af5ead881..c1b609248 100644 --- a/isort/api.py +++ b/isort/api.py @@ -2,12 +2,12 @@ import sys from io import StringIO from pathlib import Path -from typing import Optional, TextIO, Union, cast, Iterator +from typing import Iterator, Optional, Set, TextIO, Union, cast from warnings import warn from isort import core -from . import io, identify +from . import identify, io from .exceptions import ( ExistingSyntaxErrors, FileSkipComment, @@ -409,7 +409,13 @@ def imports_in_code( - **unique**: If True, only the first instance of an import is returned. - ****config_kwargs**: Any config modifications. """ - yield from imports_in_stream(input_stream=StringIO(code), config=config, file_path=file_path, unique=unique, **config_kwargs) + yield from imports_in_stream( + input_stream=StringIO(code), + config=config, + file_path=file_path, + unique=unique, + **config_kwargs, + ) def imports_in_stream( @@ -432,7 +438,7 @@ def imports_in_stream( if not unique: yield from identified_imports - seen = set() + seen: Set[str] = set() for identified_import in identified_imports: key = identified_import.statement() if key not in seen: @@ -460,7 +466,7 @@ def imports_in_file( yield from imports_in_stream( input_stream=source_file.stream, config=config, - file_path=file_path, + file_path=file_path or source_file.path, unique=unique, **config_kwargs, ) diff --git a/isort/files.py b/isort/files.py index 224215315..692c2011c 100644 --- a/isort/files.py +++ b/isort/files.py @@ -1,6 +1,7 @@ import os from pathlib import Path from typing import Iterable, Iterator, List, Set +from warnings import warn from isort.settings import Config diff --git a/isort/main.py b/isort/main.py index 7d1bc3d9b..262a9eeb3 100644 --- a/isort/main.py +++ b/isort/main.py @@ -871,11 +871,14 @@ def identify_imports_main( file_name = arguments.file if file_name == "-": - api.get_imports_stream(sys.stdin if stdin is None else stdin, sys.stdout) + identified_imports = api.imports_in_stream(sys.stdin if stdin is None else stdin) else: if os.path.isdir(file_name): sys.exit("Path must be a file, not a directory") - api.get_imports_file(file_name, sys.stdout) + identified_imports = api.imports_in_file(file_name) + + for identified_import in identified_imports: + print(str(identified_import)) def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = None) -> None: From 0c1ed9cd04896baa7926aefd057050cc34f62505 Mon Sep 17 00:00:00 2001 From: Marco Gorelli Date: Sun, 20 Dec 2020 07:37:53 +0000 Subject: [PATCH 1300/1439] Put minimum pre-commit version in hook (seeing as you're now using `types_or` - else the error messages people get may be a bit mysterious) --- .pre-commit-hooks.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 773b505fd..fc6906aae 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -6,3 +6,4 @@ language_version: python3 types_or: [cython, pyi, python] args: ['--filter-files'] + minimum_pre_commit_version: '2.9.0' From aa464877a34fd812fa9f1ba5f6741e7e7f26e0bc Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 20 Dec 2020 17:25:01 -0800 Subject: [PATCH 1301/1439] Fix logic error in type of import identification --- isort/identify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/identify.py b/isort/identify.py index 9f6cc9467..f57f3a3be 100644 --- a/isort/identify.py +++ b/isort/identify.py @@ -59,7 +59,7 @@ def imports( line, raw_line = _normalize_line(statement) if line.lstrip().startswith(("import ", "cimport ")): type_of_import = "straight" - if line.lstrip().startswith("from "): + elif line.lstrip().startswith("from "): type_of_import = "from" else: continue From f6cc79450f2c50923e98bd4e56614dd6c8446de3 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 20 Dec 2020 17:55:26 -0800 Subject: [PATCH 1302/1439] Rename import finding methods to be more intuitive directly from isort (isort.find_imports_in_file > isort.imports_in_file=) --- isort/__init__.py | 6 +++--- isort/api.py | 10 +++++----- isort/main.py | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/isort/__init__.py b/isort/__init__.py index dbe2c895e..afb67e3ae 100644 --- a/isort/__init__.py +++ b/isort/__init__.py @@ -5,9 +5,9 @@ from .api import ( check_file, check_stream, - imports_in_code, - imports_in_file, - imports_in_stream, + find_imports_in_code, + find_imports_in_file, + find_imports_in_stream, place_module, place_module_with_reason, ) diff --git a/isort/api.py b/isort/api.py index c1b609248..583856f2f 100644 --- a/isort/api.py +++ b/isort/api.py @@ -394,7 +394,7 @@ def sort_file( return changed -def imports_in_code( +def find_find_imports_in_code( code: str, config: Config = DEFAULT_CONFIG, file_path: Optional[Path] = None, @@ -409,7 +409,7 @@ def imports_in_code( - **unique**: If True, only the first instance of an import is returned. - ****config_kwargs**: Any config modifications. """ - yield from imports_in_stream( + yield from find_imports_in_stream( input_stream=StringIO(code), config=config, file_path=file_path, @@ -418,7 +418,7 @@ def imports_in_code( ) -def imports_in_stream( +def find_imports_in_stream( input_stream: TextIO, config: Config = DEFAULT_CONFIG, file_path: Optional[Path] = None, @@ -446,7 +446,7 @@ def imports_in_stream( yield identified_import -def imports_in_file( +def find_imports_in_file( filename: Union[str, Path], config: Config = DEFAULT_CONFIG, file_path: Optional[Path] = None, @@ -463,7 +463,7 @@ def imports_in_file( - ****config_kwargs**: Any config modifications. """ with io.File.read(filename) as source_file: - yield from imports_in_stream( + yield from find_imports_in_stream( input_stream=source_file.stream, config=config, file_path=file_path or source_file.path, diff --git a/isort/main.py b/isort/main.py index 262a9eeb3..40f62f6f9 100644 --- a/isort/main.py +++ b/isort/main.py @@ -871,11 +871,11 @@ def identify_imports_main( file_name = arguments.file if file_name == "-": - identified_imports = api.imports_in_stream(sys.stdin if stdin is None else stdin) + identified_imports = api.find_imports_in_stream(sys.stdin if stdin is None else stdin) else: if os.path.isdir(file_name): sys.exit("Path must be a file, not a directory") - identified_imports = api.imports_in_file(file_name) + identified_imports = api.find_imports_in_file(file_name) for identified_import in identified_imports: print(str(identified_import)) From 75d6d9f14637a1bc0a77f4d7582d14e346a09bc8 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 20 Dec 2020 17:58:53 -0800 Subject: [PATCH 1303/1439] Fix double find typo --- isort/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/api.py b/isort/api.py index 583856f2f..ec4152184 100644 --- a/isort/api.py +++ b/isort/api.py @@ -394,7 +394,7 @@ def sort_file( return changed -def find_find_imports_in_code( +def find_imports_in_code( code: str, config: Config = DEFAULT_CONFIG, file_path: Optional[Path] = None, From bf42692e255697767cfd30271bc669587fb9b398 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 20 Dec 2020 18:22:43 -0800 Subject: [PATCH 1304/1439] Fix infinite loop --- isort/identify.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/isort/identify.py b/isort/identify.py index f57f3a3be..6e0b606da 100644 --- a/isort/identify.py +++ b/isort/identify.py @@ -154,6 +154,9 @@ def imports( else: module = just_imports[as_index - 1] alias = just_imports[as_index + 1] + direct_imports.remove(alias) + direct_imports.remove("as") + just_imports[1:] = direct_imports if not (module == alias and config.remove_redundant_aliases): yield identified_import(module, alias) From 10fb87eb4ff0585ae48862af5be0c07543e406b5 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 20 Dec 2020 18:36:53 -0800 Subject: [PATCH 1305/1439] All tests now passingd --- tests/unit/test_api.py | 8 ++--- tests/unit/test_isort.py | 64 ++++++++++++++++++---------------------- tests/unit/test_main.py | 10 ++----- 3 files changed, 34 insertions(+), 48 deletions(-) diff --git a/tests/unit/test_api.py b/tests/unit/test_api.py index 4ee19bc43..20c2fdae6 100644 --- a/tests/unit/test_api.py +++ b/tests/unit/test_api.py @@ -1,6 +1,5 @@ """Tests the isort API module""" import os -import sys from io import StringIO from unittest.mock import MagicMock, patch @@ -84,7 +83,6 @@ def test_sort_code_string_mixed_newlines(): assert api.sort_code_string("import A\n\r\nimportA\n\n") == "import A\r\n\r\nimportA\r\n\n" -def test_get_import_file(imperfect, capsys): - api.get_imports_file(imperfect, sys.stdout) - out, _ = capsys.readouterr() - assert out == imperfect_content.replace("\n", os.linesep) +def test_find_imports_in_file(imperfect): + found_imports = list(api.find_imports_in_file(imperfect)) + assert "b" in [found_import.module for found_import in found_imports] diff --git a/tests/unit/test_isort.py b/tests/unit/test_isort.py index aea2c74b6..aca087092 100644 --- a/tests/unit/test_isort.py +++ b/tests/unit/test_isort.py @@ -14,7 +14,7 @@ import py import pytest import isort -from isort import main, api, sections +from isort import api, sections, files from isort.settings import WrapModes, Config from isort.utils import exists_case_sensitive from isort.exceptions import FileSkipped, ExistingSyntaxErrors @@ -3270,12 +3270,11 @@ def test_safety_skips(tmpdir, enabled: bool) -> None: skipped: List[str] = [] broken: List[str] = [] codes = [str(tmpdir)] - main.iter_source_code(codes, config, skipped, broken) + files.find(codes, config, skipped, broken) # if enabled files within nested unsafe directories should be skipped file_names = { - os.path.relpath(f, str(tmpdir)) - for f in main.iter_source_code([str(tmpdir)], config, skipped, broken) + os.path.relpath(f, str(tmpdir)) for f in files.find([str(tmpdir)], config, skipped, broken) } if enabled: assert file_names == {"victim.py"} @@ -3292,9 +3291,7 @@ def test_safety_skips(tmpdir, enabled: bool) -> None: # directly pointing to files within unsafe directories shouldn't skip them either way file_names = { os.path.relpath(f, str(toxdir)) - for f in main.iter_source_code( - [str(toxdir)], Config(directory=str(toxdir)), skipped, broken - ) + for f in files.find([str(toxdir)], Config(directory=str(toxdir)), skipped, broken) } assert file_names == {"verysafe.py"} @@ -3318,7 +3315,7 @@ def test_skip_glob(tmpdir, skip_glob_assert: Tuple[List[str], int, Set[str]]) -> broken: List[str] = [] file_names = { os.path.relpath(f, str(base_dir)) - for f in main.iter_source_code([str(base_dir)], config, skipped, broken) + for f in files.find([str(base_dir)], config, skipped, broken) } assert len(skipped) == skipped_count assert file_names == file_names_expected @@ -3332,7 +3329,7 @@ def test_broken(tmpdir) -> None: broken: List[str] = [] file_names = { os.path.relpath(f, str(base_dir)) - for f in main.iter_source_code(["not-exist"], config, skipped, broken) + for f in files.find(["not-exist"], config, skipped, broken) } assert len(broken) == 1 assert file_names == set() @@ -4916,7 +4913,7 @@ def test_combine_straight_imports() -> None: ) -def test_get_imports_string() -> None: +def test_find_imports_in_code() -> None: test_input = ( "import first_straight\n" "\n" @@ -4943,24 +4940,25 @@ def test_get_imports_string() -> None: "\n" "import needed_in_end\n" ) - result = api.get_imports_string(test_input) - assert result == ( - "import first_straight\n" - "import second_straight\n" - "from first_from import first_from_function_1, first_from_function_2\n" - "import bad_name as good_name\n" - "from parent.some_bad_defs import bad_name_1 as ok_name_1, bad_name_2 as ok_name_2\n" - "import needed_in_bla_2\n" - "import needed_in_bla\n" - "import needed_in_bla_bla\n" - "import needed_in_end\n" - ) - - -def test_get_imports_stdout() -> None: - """Ensure that get_imports_stream can work with nonseekable streams like STDOUT""" - - global_output = [] + identified_imports = list(map(str, api.find_imports_in_code(test_input))) + assert identified_imports == [ + ":0 import first_straight", + ":2 import second_straight", + ":3 import first_from.first_from_function_1", + ":3 import first_from.first_from_function_2", + ":4 import bad_name.good_name", + ":4 import bad_name", + ":5 import parent.some_bad_defs.bad_name_1 as ok_name_1", + ":5 import parent.some_bad_defs.bad_name_2 as ok_name_2", + ":11 import needed_in_bla_2", + ":14 import needed_in_bla", + ":17 import needed_in_bla_bla", + ":21 import needed_in_end", + ] + + +def test_find_imports_in_stream() -> None: + """Ensure that find_imports_in_stream can work with nonseekable streams like STDOUT""" class NonSeekableTestStream(StringIO): def seek(self, position): @@ -4969,10 +4967,6 @@ def seek(self, position): def seekable(self): return False - def write(self, s, *a, **kw): - global_output.append(s) - - test_input = StringIO("import m2\n" "import m1\n" "not_import = 7") - test_output = NonSeekableTestStream() - api.get_imports_stream(test_input, test_output) - assert "".join(global_output) == "import m2\nimport m1\n" + test_input = NonSeekableTestStream("import m2\n" "import m1\n" "not_import = 7") + identified_imports = list(map(str, api.find_imports_in_stream(test_input))) + assert identified_imports == [":0 import m2", ":1 import m1"] diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index bd4c6ffa6..67adad34b 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -40,12 +40,6 @@ def test_fuzz_sort_imports(file_name, config, check, ask_to_apply, write_to_stdo ) -def test_iter_source_code(tmpdir): - tmp_file = tmpdir.join("file.py") - tmp_file.write("import os, sys\n") - assert tuple(main.iter_source_code((tmp_file,), DEFAULT_CONFIG, [], [])) == (tmp_file,) - - def test_sort_imports(tmpdir): tmp_file = tmpdir.join("file.py") tmp_file.write("import os, sys\n") @@ -1002,9 +996,9 @@ def test_only_modified_flag(tmpdir, capsys): def test_identify_imports_main(tmpdir, capsys): file_content = "import mod2\n" "a = 1\n" "import mod1\n" - file_imports = "import mod2\n" "import mod1\n" some_file = tmpdir.join("some_file.py") some_file.write(file_content) + file_imports = f"{some_file}:0 import mod2\n{some_file}:2 import mod1\n" main.identify_imports_main([str(some_file)]) @@ -1014,7 +1008,7 @@ def test_identify_imports_main(tmpdir, capsys): main.identify_imports_main(["-"], stdin=as_stream(file_content)) out, error = capsys.readouterr() - assert out.replace("\r\n", "\n") == file_imports + assert out.replace("\r\n", "\n") == file_imports.replace(str(some_file), "") with pytest.raises(SystemExit): main.identify_imports_main([str(tmpdir)]) From c5b335500ab326b225626927ebadf5c584f58ac4 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 20 Dec 2020 18:37:20 -0800 Subject: [PATCH 1306/1439] Add test for new files module --- tests/unit/test_files.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 tests/unit/test_files.py diff --git a/tests/unit/test_files.py b/tests/unit/test_files.py new file mode 100644 index 000000000..7ee6acf40 --- /dev/null +++ b/tests/unit/test_files.py @@ -0,0 +1,8 @@ +from isort import files +from isort.settings import DEFAULT_CONFIG + + +def test_find(tmpdir): + tmp_file = tmpdir.join("file.py") + tmp_file.write("import os, sys\n") + assert tuple(files.find((tmp_file,), DEFAULT_CONFIG, [], [])) == (tmp_file,) From bded231bfe679761856f819cf2a7fe29b955378e Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 20 Dec 2020 23:42:08 -0800 Subject: [PATCH 1307/1439] Hide unused variable --- isort/identify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/identify.py b/isort/identify.py index 6e0b606da..3f1a58045 100644 --- a/isort/identify.py +++ b/isort/identify.py @@ -56,7 +56,7 @@ def imports( statements[-1] = f"{statements[-1]}#{end_of_line_comment[0]}" for statement in statements: - line, raw_line = _normalize_line(statement) + line, _raw_line = _normalize_line(statement) if line.lstrip().startswith(("import ", "cimport ")): type_of_import = "straight" elif line.lstrip().startswith("from "): From c4560d657c10b03f6f712026b7335f6c8ee512ee Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 21 Dec 2020 01:05:57 -0800 Subject: [PATCH 1308/1439] identify imports directory support --- isort/main.py | 23 ++++++++++++++++------- tests/unit/test_main.py | 3 +-- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/isort/main.py b/isort/main.py index 40f62f6f9..4d59b2a88 100644 --- a/isort/main.py +++ b/isort/main.py @@ -6,6 +6,7 @@ import sys from gettext import gettext as _ from io import TextIOWrapper +from itertools import chain from pathlib import Path from typing import Any, Dict, List, Optional, Sequence from warnings import warn @@ -15,7 +16,7 @@ from .format import create_terminal_printer from .logo import ASCII_ART from .profiles import profiles -from .settings import VALID_PY_TARGETS, Config, WrapModes +from .settings import DEFAULT_CONFIG, VALID_PY_TARGETS, Config, WrapModes try: from .setuptools_commands import ISortCommand # noqa: F401 @@ -866,16 +867,24 @@ def identify_imports_main( description="Get all import definitions from a given file." "Use `-` as the first argument to represent stdin." ) - parser.add_argument("file", help="Python source file to get imports from.") + parser.add_argument( + "files", nargs="*", help="One or more Python source files that need their imports sorted." + ) arguments = parser.parse_args(argv) - file_name = arguments.file - if file_name == "-": + file_names = arguments.files + if file_names == ["-"]: identified_imports = api.find_imports_in_stream(sys.stdin if stdin is None else stdin) else: - if os.path.isdir(file_name): - sys.exit("Path must be a file, not a directory") - identified_imports = api.find_imports_in_file(file_name) + skipped: List[str] = [] + broken: List[str] = [] + config = DEFAULT_CONFIG + identified_imports = chain( + *( + api.find_imports_in_file(file_name) + for file_name in files.find(file_names, config, skipped, broken) + ) + ) for identified_import in identified_imports: print(str(identified_import)) diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 67adad34b..a13f5badc 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -1010,5 +1010,4 @@ def test_identify_imports_main(tmpdir, capsys): out, error = capsys.readouterr() assert out.replace("\r\n", "\n") == file_imports.replace(str(some_file), "") - with pytest.raises(SystemExit): - main.identify_imports_main([str(tmpdir)]) + main.identify_imports_main([str(tmpdir)]) From d93bbc693a2f61b48e4765c525b01aeeeeff3bb8 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 21 Dec 2020 22:34:34 -0800 Subject: [PATCH 1309/1439] Expose unique option for identifying imports to cli --- isort/main.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/isort/main.py b/isort/main.py index 4d59b2a88..2e84a6645 100644 --- a/isort/main.py +++ b/isort/main.py @@ -870,18 +870,22 @@ def identify_imports_main( parser.add_argument( "files", nargs="*", help="One or more Python source files that need their imports sorted." ) + parser.add_argument( + "--unique", action="store_true", default=False, + help="If true, isort will only identify unique imports." + ) arguments = parser.parse_args(argv) file_names = arguments.files if file_names == ["-"]: - identified_imports = api.find_imports_in_stream(sys.stdin if stdin is None else stdin) + identified_imports = api.find_imports_in_stream(sys.stdin if stdin is None else stdin, unique=arguments.unique) else: skipped: List[str] = [] broken: List[str] = [] config = DEFAULT_CONFIG identified_imports = chain( *( - api.find_imports_in_file(file_name) + api.find_imports_in_file(file_name, unique=arguments.unique) for file_name in files.find(file_names, config, skipped, broken) ) ) From 128e1da3042906a53a239d65568d5abb90844e8c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 21 Dec 2020 22:36:47 -0800 Subject: [PATCH 1310/1439] isort + black --- isort/main.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/isort/main.py b/isort/main.py index 2e84a6645..b63642ba6 100644 --- a/isort/main.py +++ b/isort/main.py @@ -871,14 +871,18 @@ def identify_imports_main( "files", nargs="*", help="One or more Python source files that need their imports sorted." ) parser.add_argument( - "--unique", action="store_true", default=False, - help="If true, isort will only identify unique imports." + "--unique", + action="store_true", + default=False, + help="If true, isort will only identify unique imports.", ) arguments = parser.parse_args(argv) file_names = arguments.files if file_names == ["-"]: - identified_imports = api.find_imports_in_stream(sys.stdin if stdin is None else stdin, unique=arguments.unique) + identified_imports = api.find_imports_in_stream( + sys.stdin if stdin is None else stdin, unique=arguments.unique + ) else: skipped: List[str] = [] broken: List[str] = [] From 54f5bce948bf5f9cc5190efd6e81d533c35d513e Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 22 Dec 2020 22:54:46 -0800 Subject: [PATCH 1311/1439] remove unecesary elif --- isort/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/main.py b/isort/main.py index b63642ba6..889f3b623 100644 --- a/isort/main.py +++ b/isort/main.py @@ -958,7 +958,7 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = if show_config: print(json.dumps(config.__dict__, indent=4, separators=(",", ": "), default=_preconvert)) return - elif file_names == ["-"]: + if file_names == ["-"]: file_path = Path(stream_filename) if stream_filename else None if show_files: sys.exit("Error: can't show files for streaming input.") From 7addd4fc5154fedd90c6c4c32d8f5baf497c1468 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 22 Dec 2020 22:55:41 -0800 Subject: [PATCH 1312/1439] remove unecesary elif~ --- isort/settings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/isort/settings.py b/isort/settings.py index 8ef6dfa86..a1e9067f8 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -387,7 +387,8 @@ def __init__( for section in combined_config.get("sections", ()): if section in SECTION_DEFAULTS: continue - elif not section.lower() in known_other: + + if not section.lower() in known_other: config_keys = ", ".join(known_other.keys()) warn( f"`sections` setting includes {section}, but no known_{section.lower()} " From 1e6db95820f4341712fb7fd9e993597543480a0b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 22 Dec 2020 22:58:24 -0800 Subject: [PATCH 1313/1439] Remove unecesary variable definition --- isort/output.py | 4 +--- isort/settings.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/isort/output.py b/isort/output.py index 45faa6468..1d64785bc 100644 --- a/isort/output.py +++ b/isort/output.py @@ -229,8 +229,6 @@ def _with_from_imports( if not config.no_inline_sort or ( config.force_single_line and module not in config.single_line_exclusions ): - ignore_case = config.force_alphabetical_sort_within_sections - if not config.only_sections: from_imports = sorting.naturally( from_imports, @@ -238,7 +236,7 @@ def _with_from_imports( key, config, True, - ignore_case, + config.force_alphabetical_sort_within_sections, section_name=section, ), ) diff --git a/isort/settings.py b/isort/settings.py index a1e9067f8..03d90335e 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -387,7 +387,7 @@ def __init__( for section in combined_config.get("sections", ()): if section in SECTION_DEFAULTS: continue - + if not section.lower() in known_other: config_keys = ", ".join(known_other.keys()) warn( From 805323439dfd9e208e42c5690e88528f0da547b0 Mon Sep 17 00:00:00 2001 From: anirudnits Date: Wed, 23 Dec 2020 18:41:47 +0530 Subject: [PATCH 1314/1439] Specify the config file example corresponds to pyproject.toml --- docs/configuration/black_compatibility.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/configuration/black_compatibility.md b/docs/configuration/black_compatibility.md index c81989a9f..a57754252 100644 --- a/docs/configuration/black_compatibility.md +++ b/docs/configuration/black_compatibility.md @@ -11,6 +11,8 @@ All that's required to use isort alongside black is to set the isort profile to For projects that officially use both isort and black, we recommend setting the black profile in a config file at the root of your project's repository. This way independent to how users call isort (pre-commit, CLI, or editor integration) the black profile will automatically be applied. +For instance, your _pyproject.toml_ file would look something like + ```ini [tool.isort] profile = "black" From 40d657c252799511c4804a2a75952f6da3a14023 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 23 Dec 2020 22:52:34 -0800 Subject: [PATCH 1315/1439] Call super for colorama printer --- isort/format.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/isort/format.py b/isort/format.py index 46bb15695..22c506f06 100644 --- a/isort/format.py +++ b/isort/format.py @@ -110,7 +110,8 @@ def diff_line(self, line: str) -> None: class ColoramaPrinter(BasicPrinter): def __init__(self, output: Optional[TextIO] = None): - self.output = output or sys.stdout + super().__init__(output=output) + # Note: this constants are instance variables instead ofs class variables # because they refer to colorama which might not be installed. self.ERROR = self.style_text("ERROR", colorama.Fore.RED) From 3f82273dbd513a6fce756da315bb6affd3a8c8ab Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 23 Dec 2020 22:56:20 -0800 Subject: [PATCH 1316/1439] Add ignore line for deepsource rule --- isort/format.py | 2 +- isort/settings.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/format.py b/isort/format.py index 22c506f06..d08a6a513 100644 --- a/isort/format.py +++ b/isort/format.py @@ -111,7 +111,7 @@ def diff_line(self, line: str) -> None: class ColoramaPrinter(BasicPrinter): def __init__(self, output: Optional[TextIO] = None): super().__init__(output=output) - + # Note: this constants are instance variables instead ofs class variables # because they refer to colorama which might not be installed. self.ERROR = self.style_text("ERROR", colorama.Fore.RED) diff --git a/isort/settings.py b/isort/settings.py index 03d90335e..4ea8c0d0b 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -506,7 +506,7 @@ def is_skipped(self, file_path: Path) -> bool: if file_path.name == ".git": # pragma: no cover return True - result = subprocess.run( # nosec + result = subprocess.run( # nosec # skipcq: PYL-W1510 ["git", "-C", str(file_path.parent), "check-ignore", "--quiet", os_path] ) if result.returncode == 0: From 800bfd91f6cde599fca1c45e5566db0379a7a8b8 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 23 Dec 2020 22:57:36 -0800 Subject: [PATCH 1317/1439] Ignore scripts in deepsource config --- .deepsource.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/.deepsource.toml b/.deepsource.toml index cfbbec30a..2cd579f78 100644 --- a/.deepsource.toml +++ b/.deepsource.toml @@ -3,6 +3,7 @@ version = 1 test_patterns = ["tests/**"] exclude_patterns = [ "tests/**", + "scripts/**", "isort/_future/**", "isort/_vendored/**", ] From d37e4142a3ce8c4ecd66c104f8b861975ddf16d2 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 23 Dec 2020 23:01:44 -0800 Subject: [PATCH 1318/1439] Handle stop iteration case --- isort/identify.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/isort/identify.py b/isort/identify.py index 3f1a58045..09f3cabd9 100644 --- a/isort/identify.py +++ b/isort/identify.py @@ -91,7 +91,11 @@ def imports( import_string += "\n" + line else: while line.strip().endswith("\\"): - index, next_line = next(indexed_input) + try: + index, next_line = next(indexed_input) + except StopIteration: + break + line, _ = parse_comments(next_line) # Still need to check for parentheses after an escaped line From aab64a1004613f3dad5b2879d968a8d900f32482 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 23 Dec 2020 23:20:26 -0800 Subject: [PATCH 1319/1439] Remove unecesary blank lines --- isort/hooks.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/isort/hooks.py b/isort/hooks.py index acccede59..dfd7eb3dc 100644 --- a/isort/hooks.py +++ b/isort/hooks.py @@ -12,8 +12,7 @@ def get_output(command: List[str]) -> str: - """ - Run a command and return raw output + """Run a command and return raw output :param str command: the command to run :returns: the stdout output of the command @@ -23,8 +22,7 @@ def get_output(command: List[str]) -> str: def get_lines(command: List[str]) -> List[str]: - """ - Run a command and return lines of output + """Run a command and return lines of output :param str command: the command to run :returns: list of whitespace-stripped lines output by command @@ -36,8 +34,7 @@ def get_lines(command: List[str]) -> List[str]: def git_hook( strict: bool = False, modify: bool = False, lazy: bool = False, settings_file: str = "" ) -> int: - """ - Git pre-commit hook to check staged files for isort errors + """Git pre-commit hook to check staged files for isort errors :param bool strict - if True, return number of errors on exit, causing the hook to fail. If False, return zero so it will From f1c908ac9a79622893e066f8e48154073e04ded2 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 24 Dec 2020 14:41:50 -0800 Subject: [PATCH 1320/1439] Add failing test for issue #1621: Showing that double comma does indeed apear. --- tests/unit/test_regressions.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index c25ed19c7..ef3d84797 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -1485,3 +1485,19 @@ def test_isort_losing_imports_vertical_prefix_from_module_import_wrap_mode_issue show_diff=True, multi_line_output=9, ) + + +def test_isort_adding_second_comma_issue_1621(): + """Ensure isort doesnt add a second comma when very long comment is present + See: https://github.com/PyCQA/isort/issues/1621. + """ + assert isort.code( + """from .test import ( + TestTestTestTestTestTest2 as TestTestTestTestTestTest1 # Some really long comment bla bla bla bla bla +) +""", + profile="black", + ) == """from .test import ( + TestTestTestTestTestTest2 as TestTestTestTestTestTest1, # Some really long comment bla bla bla bla bla +) +""" From 7a84253b9f9f8f6d1d251e3caac1de4698e17b6f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 24 Dec 2020 14:43:00 -0800 Subject: [PATCH 1321/1439] Expand test to capture case where comma is already present --- tests/unit/test_regressions.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index ef3d84797..910525432 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -1491,6 +1491,14 @@ def test_isort_adding_second_comma_issue_1621(): """Ensure isort doesnt add a second comma when very long comment is present See: https://github.com/PyCQA/isort/issues/1621. """ + assert isort.check_code( + """from .test import ( + TestTestTestTestTestTest2 as TestTestTestTestTestTest1, # Some really long comment bla bla bla bla bla +) +""", + profile="black", + show_diff=True, + ) assert isort.code( """from .test import ( TestTestTestTestTestTest2 as TestTestTestTestTestTest1 # Some really long comment bla bla bla bla bla @@ -1501,3 +1509,4 @@ def test_isort_adding_second_comma_issue_1621(): TestTestTestTestTestTest2 as TestTestTestTestTestTest1, # Some really long comment bla bla bla bla bla ) """ + From 8b828fb9b42bc129e10f88b166b69f2271c76a7d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 24 Dec 2020 23:31:06 -0800 Subject: [PATCH 1322/1439] Remove uneeded line --- tests/unit/test_regressions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index 910525432..10bd3bf81 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -1509,4 +1509,3 @@ def test_isort_adding_second_comma_issue_1621(): TestTestTestTestTestTest2 as TestTestTestTestTestTest1, # Some really long comment bla bla bla bla bla ) """ - From 59f635ab33c32925f0faa92835ee3e94fd785ad6 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 25 Dec 2020 00:00:22 -0800 Subject: [PATCH 1323/1439] Fix comma behavior --- isort/wrap.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/isort/wrap.py b/isort/wrap.py index 11542fa07..e993ae0f3 100644 --- a/isort/wrap.py +++ b/isort/wrap.py @@ -77,7 +77,13 @@ def line(content: str, line_separator: str, config: Config = DEFAULT_CONFIG) -> line_parts = re.split(exp, line_without_comment) if comment and not (config.use_parentheses and "noqa" in comment): _comma_maybe = ( - "," if (config.include_trailing_comma and config.use_parentheses) else "" + "," + if ( + config.include_trailing_comma + and config.use_parentheses + and not line_without_comment.rstrip().endswith(",") + ) + else "" ) line_parts[ -1 @@ -92,13 +98,16 @@ def line(content: str, line_separator: str, config: Config = DEFAULT_CONFIG) -> content = next_line.pop() cont_line = _wrap_line( - config.indent + splitter.join(next_line).lstrip(), line_separator, config + config.indent + splitter.join(next_line).lstrip(), + line_separator, + config, ) if config.use_parentheses: if splitter == "as ": output = f"{content}{splitter}{cont_line.lstrip()}" else: _comma = "," if config.include_trailing_comma and not comment else "" + if wrap_mode in ( Modes.VERTICAL_HANGING_INDENT, # type: ignore Modes.VERTICAL_GRID_GROUPED, # type: ignore From ddf0d394653db7c19a7f9a78357caf080c36f11b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 25 Dec 2020 00:00:29 -0800 Subject: [PATCH 1324/1439] Fix test line lengths --- tests/unit/test_regressions.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index 10bd3bf81..89fa09274 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -1493,19 +1493,25 @@ def test_isort_adding_second_comma_issue_1621(): """ assert isort.check_code( """from .test import ( - TestTestTestTestTestTest2 as TestTestTestTestTestTest1, # Some really long comment bla bla bla bla bla + TestTestTestTestTestTest2 as TestTestTestTestTestTest1, """ + """# Some really long comment bla bla bla bla bla ) """, profile="black", show_diff=True, ) - assert isort.code( - """from .test import ( - TestTestTestTestTestTest2 as TestTestTestTestTestTest1 # Some really long comment bla bla bla bla bla + assert ( + isort.code( + """from .test import ( + TestTestTestTestTestTest2 as TestTestTestTestTestTest1 """ + """# Some really long comment bla bla bla bla bla ) """, - profile="black", - ) == """from .test import ( - TestTestTestTestTestTest2 as TestTestTestTestTestTest1, # Some really long comment bla bla bla bla bla + profile="black", + ) + == """from .test import ( + TestTestTestTestTestTest2 as TestTestTestTestTestTest1, """ + """# Some really long comment bla bla bla bla bla ) """ + ) From 18ec2a06146b97022deccd8c90ea8725c7a91f28 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 25 Dec 2020 00:02:18 -0800 Subject: [PATCH 1325/1439] Fixed #1612: In rare circumstances an extra comma is added after import and before comment. marked in changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac6d209c3..7e377fd21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ NOTE: isort follows the [semver](https://semver.org/) versioning standard. Find out more about isort's release policy [here](https://pycqa.github.io/isort/docs/major_releases/release_policy/). ### 5.7.0 December TBD + - Fixed #1612: In rare circumstances an extra comma is added after import and before comment. - Implemented #1596: Provide ways for extension formatting and file paths to be specified when using streaming input from CLI. - Implemented #1583: Ability to output and diff within a single API call to `isort.file`. - Implemented #1562, #1592 & #1593: Better more useful fatal error messages. From c48fd911e4afd8f542f561490b16aeaaaabe9fae Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 26 Dec 2020 23:04:40 -0800 Subject: [PATCH 1326/1439] Expose path based import finding via API --- isort/__init__.py | 1 + isort/api.py | 28 +++++++++++++++++++++++++++- isort/main.py | 13 ++----------- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/isort/__init__.py b/isort/__init__.py index afb67e3ae..03223665d 100644 --- a/isort/__init__.py +++ b/isort/__init__.py @@ -7,6 +7,7 @@ check_stream, find_imports_in_code, find_imports_in_file, + find_imports_in_paths, find_imports_in_stream, place_module, place_module_with_reason, diff --git a/isort/api.py b/isort/api.py index ec4152184..ca5fc301d 100644 --- a/isort/api.py +++ b/isort/api.py @@ -1,13 +1,14 @@ import shutil import sys from io import StringIO +from itertools import chain from pathlib import Path from typing import Iterator, Optional, Set, TextIO, Union, cast from warnings import warn from isort import core -from . import identify, io +from . import files, identify, io from .exceptions import ( ExistingSyntaxErrors, FileSkipComment, @@ -472,6 +473,31 @@ def find_imports_in_file( ) +def find_imports_in_paths( + paths: Iterator[Union[str, Path]], + config: Config = DEFAULT_CONFIG, + file_path: Optional[Path] = None, + unique: bool = False, + **config_kwargs, +) -> Iterator[identify.Import]: + """Finds and returns all imports within the provided source paths. + + - **paths**: A collection of paths to recursively look for imports within. + - **extension**: The file extension that contains imports. Defaults to filename extension or py. + - **config**: The config object to use when sorting imports. + - **file_path**: The disk location where the code string was pulled from. + - **unique**: If True, only the first instance of an import is returned. + - ****config_kwargs**: Any config modifications. + """ + config = _config(path=file_path, config=config, **config_kwargs) + yield from chain( + *( + find_imports_in_file(file_name, unique=unique, config=config) + for file_name in files.find(map(str, paths), config, [], []) + ) + ) + + def _config( path: Optional[Path] = None, config: Config = DEFAULT_CONFIG, **config_kwargs ) -> Config: diff --git a/isort/main.py b/isort/main.py index 889f3b623..ba4f3538b 100644 --- a/isort/main.py +++ b/isort/main.py @@ -6,7 +6,6 @@ import sys from gettext import gettext as _ from io import TextIOWrapper -from itertools import chain from pathlib import Path from typing import Any, Dict, List, Optional, Sequence from warnings import warn @@ -16,7 +15,7 @@ from .format import create_terminal_printer from .logo import ASCII_ART from .profiles import profiles -from .settings import DEFAULT_CONFIG, VALID_PY_TARGETS, Config, WrapModes +from .settings import VALID_PY_TARGETS, Config, WrapModes try: from .setuptools_commands import ISortCommand # noqa: F401 @@ -884,15 +883,7 @@ def identify_imports_main( sys.stdin if stdin is None else stdin, unique=arguments.unique ) else: - skipped: List[str] = [] - broken: List[str] = [] - config = DEFAULT_CONFIG - identified_imports = chain( - *( - api.find_imports_in_file(file_name, unique=arguments.unique) - for file_name in files.find(file_names, config, skipped, broken) - ) - ) + identified_imports = api.find_imports_in_paths(file_names) for identified_import in identified_imports: print(str(identified_import)) From f607723c88ac3fb6983b192ab40e2034442ec60b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 27 Dec 2020 23:51:45 -0800 Subject: [PATCH 1327/1439] Remove no longer needed imports_only functionality in core.py --- isort/core.py | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/isort/core.py b/isort/core.py index e53a8b87e..a37b707e4 100644 --- a/isort/core.py +++ b/isort/core.py @@ -30,7 +30,6 @@ def process( output_stream: TextIO, extension: str = "py", config: Config = DEFAULT_CONFIG, - imports_only: bool = False, ) -> bool: """Parses stream identifying sections of contiguous imports and sorting them @@ -69,16 +68,6 @@ def process( stripped_line: str = "" end_of_file: bool = False verbose_output: List[str] = [] - all_imports: List[str] = [] - - _output_stream = output_stream # Used if imports_only == True - if imports_only: - - class DevNull(StringIO): - def write(self, *a, **kw): - pass - - output_stream = DevNull() if config.float_to_top: new_input = "" @@ -352,15 +341,6 @@ def write(self, *a, **kw): parsed_content = parse.file_contents(import_section, config=config) verbose_output += parsed_content.verbose_output - if imports_only: - lines_without_imports_set = set(parsed_content.lines_without_imports) - all_imports.extend( - li - for li in parsed_content.in_lines - if li - and li not in lines_without_imports_set - and not li.lstrip().startswith("#") - ) sorted_import_section = output.sorted_imports( parsed_content, @@ -425,10 +405,6 @@ def write(self, *a, **kw): for output_str in verbose_output: print(output_str) - if imports_only: - result = line_separator.join(all_imports) + line_separator - _output_stream.write(result) - return made_changes From 41302ffb6f08607596fd5ce1fd176e30ff2ad7e7 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 28 Dec 2020 01:49:56 -0800 Subject: [PATCH 1328/1439] Add testing for unique --- isort/main.py | 2 +- tests/unit/test_main.py | 21 ++++++++++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/isort/main.py b/isort/main.py index ba4f3538b..22164f1af 100644 --- a/isort/main.py +++ b/isort/main.py @@ -883,7 +883,7 @@ def identify_imports_main( sys.stdin if stdin is None else stdin, unique=arguments.unique ) else: - identified_imports = api.find_imports_in_paths(file_names) + identified_imports = api.find_imports_in_paths(file_names, unique=arguments.unique) for identified_import in identified_imports: print(str(identified_import)) diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index a13f5badc..8de3e2bee 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -995,19 +995,30 @@ def test_only_modified_flag(tmpdir, capsys): def test_identify_imports_main(tmpdir, capsys): - file_content = "import mod2\n" "a = 1\n" "import mod1\n" + file_content = "import mod2\n import mod2\n" "a = 1\n" "import mod1\n" some_file = tmpdir.join("some_file.py") some_file.write(file_content) - file_imports = f"{some_file}:0 import mod2\n{some_file}:2 import mod1\n" - - main.identify_imports_main([str(some_file)]) + file_imports = f"{some_file}:0 import mod2\n{some_file}:3 import mod1\n" + file_imports_with_dupes = ( + f"{some_file}:0 import mod2\n{some_file}:1 import mod2\n" f"{some_file}:3 import mod1\n" + ) + main.identify_imports_main([str(some_file), "--unique"]) out, error = capsys.readouterr() assert out.replace("\r\n", "\n") == file_imports assert not error - main.identify_imports_main(["-"], stdin=as_stream(file_content)) + main.identify_imports_main([str(some_file)]) + out, error = capsys.readouterr() + assert out.replace("\r\n", "\n") == file_imports_with_dupes + assert not error + + main.identify_imports_main(["-", "--unique"], stdin=as_stream(file_content)) out, error = capsys.readouterr() assert out.replace("\r\n", "\n") == file_imports.replace(str(some_file), "") + main.identify_imports_main(["-"], stdin=as_stream(file_content)) + out, error = capsys.readouterr() + assert out.replace("\r\n", "\n") == file_imports_with_dupes.replace(str(some_file), "") + main.identify_imports_main([str(tmpdir)]) From e387e4b7e03aa7451873fde1c4b51af8d2c28c59 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 28 Dec 2020 16:47:33 -0800 Subject: [PATCH 1329/1439] Allow unique flag across files --- isort/api.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/isort/api.py b/isort/api.py index ca5fc301d..a8f1ba376 100644 --- a/isort/api.py +++ b/isort/api.py @@ -424,6 +424,7 @@ def find_imports_in_stream( config: Config = DEFAULT_CONFIG, file_path: Optional[Path] = None, unique: bool = False, + _seen: Optional[Set[str]] = None, **config_kwargs, ) -> Iterator[identify.Import]: """Finds and returns all imports within the provided code stream. @@ -432,6 +433,7 @@ def find_imports_in_stream( - **config**: The config object to use when sorting imports. - **file_path**: The disk location where the code string was pulled from. - **unique**: If True, only the first instance of an import is returned. + - **_seen**: An optional set of imports already seen. Generally meant only for internal use. - ****config_kwargs**: Any config modifications. """ config = _config(path=file_path, config=config, **config_kwargs) @@ -439,7 +441,7 @@ def find_imports_in_stream( if not unique: yield from identified_imports - seen: Set[str] = set() + seen: Set[str] = set() if _seen is None else _seen for identified_import in identified_imports: key = identified_import.statement() if key not in seen: @@ -490,9 +492,10 @@ def find_imports_in_paths( - ****config_kwargs**: Any config modifications. """ config = _config(path=file_path, config=config, **config_kwargs) + seen: Set[str] = set() if unique else None yield from chain( *( - find_imports_in_file(file_name, unique=unique, config=config) + find_imports_in_file(file_name, unique=unique, config=config, _seen=seen) for file_name in files.find(map(str, paths), config, [], []) ) ) From e99a4786d60b4f05a854c689df037ff1e3c64d41 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 29 Dec 2020 13:57:37 -0800 Subject: [PATCH 1330/1439] Fix typing error --- isort/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/api.py b/isort/api.py index a8f1ba376..7e597ed9c 100644 --- a/isort/api.py +++ b/isort/api.py @@ -492,7 +492,7 @@ def find_imports_in_paths( - ****config_kwargs**: Any config modifications. """ config = _config(path=file_path, config=config, **config_kwargs) - seen: Set[str] = set() if unique else None + seen: Optional[Set[str]] = set() if unique else None yield from chain( *( find_imports_in_file(file_name, unique=unique, config=config, _seen=seen) From 4762ea4cb594e65d31b7c950d0ab638385be54d4 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 29 Dec 2020 13:57:59 -0800 Subject: [PATCH 1331/1439] Update tests to enforce import identifaction line numbers use 1 base indexing --- tests/unit/test_isort.py | 26 +++++++++++++------------- tests/unit/test_main.py | 4 ++-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/unit/test_isort.py b/tests/unit/test_isort.py index aca087092..9655d9f66 100644 --- a/tests/unit/test_isort.py +++ b/tests/unit/test_isort.py @@ -4942,18 +4942,18 @@ def test_find_imports_in_code() -> None: ) identified_imports = list(map(str, api.find_imports_in_code(test_input))) assert identified_imports == [ - ":0 import first_straight", - ":2 import second_straight", - ":3 import first_from.first_from_function_1", - ":3 import first_from.first_from_function_2", - ":4 import bad_name.good_name", - ":4 import bad_name", - ":5 import parent.some_bad_defs.bad_name_1 as ok_name_1", - ":5 import parent.some_bad_defs.bad_name_2 as ok_name_2", - ":11 import needed_in_bla_2", - ":14 import needed_in_bla", - ":17 import needed_in_bla_bla", - ":21 import needed_in_end", + ":1 import first_straight", + ":3 import second_straight", + ":4 import first_from.first_from_function_1", + ":4 import first_from.first_from_function_2", + ":5 import bad_name.good_name", + ":5 import bad_name", + ":6 import parent.some_bad_defs.bad_name_1 as ok_name_1", + ":6 import parent.some_bad_defs.bad_name_2 as ok_name_2", + ":12 import needed_in_bla_2", + ":15 import needed_in_bla", + ":18 import needed_in_bla_bla", + ":22 import needed_in_end", ] @@ -4969,4 +4969,4 @@ def seekable(self): test_input = NonSeekableTestStream("import m2\n" "import m1\n" "not_import = 7") identified_imports = list(map(str, api.find_imports_in_stream(test_input))) - assert identified_imports == [":0 import m2", ":1 import m1"] + assert identified_imports == [":1 import m2", ":2 import m1"] diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 8de3e2bee..7291cb5fe 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -998,9 +998,9 @@ def test_identify_imports_main(tmpdir, capsys): file_content = "import mod2\n import mod2\n" "a = 1\n" "import mod1\n" some_file = tmpdir.join("some_file.py") some_file.write(file_content) - file_imports = f"{some_file}:0 import mod2\n{some_file}:3 import mod1\n" + file_imports = f"{some_file}:1 import mod2\n{some_file}:4 import mod1\n" file_imports_with_dupes = ( - f"{some_file}:0 import mod2\n{some_file}:1 import mod2\n" f"{some_file}:3 import mod1\n" + f"{some_file}:1 import mod2\n{some_file}:2 import mod2\n" f"{some_file}:4 import mod1\n" ) main.identify_imports_main([str(some_file), "--unique"]) From db0a7c96a13e3210caf2fcda790b03d65e430030 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 29 Dec 2020 13:58:11 -0800 Subject: [PATCH 1332/1439] Update import identification line numbers to use 1 based indexing --- isort/identify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/identify.py b/isort/identify.py index 09f3cabd9..156d866a7 100644 --- a/isort/identify.py +++ b/isort/identify.py @@ -74,7 +74,7 @@ def imports( ) identified_import = partial( Import, - index, + index + 1, # line numbers use 1 based indexing line.startswith(" ") or line.startswith("\n"), cimport=cimports, file_path=file_path, From 1e9b8af7ce5e7245046a95066bac9f6749b52aff Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 30 Dec 2020 00:55:44 -0800 Subject: [PATCH 1333/1439] Add support for multiple ways of identifying an import as unique --- isort/api.py | 47 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/isort/api.py b/isort/api.py index 7e597ed9c..16e92c975 100644 --- a/isort/api.py +++ b/isort/api.py @@ -1,5 +1,6 @@ import shutil import sys +from enum import Enum from io import StringIO from itertools import chain from pathlib import Path @@ -22,6 +23,32 @@ from .settings import DEFAULT_CONFIG, Config +class ImportKey(Enum): + """Defines how to key an individual import, generally for deduping. + + Import keys are defined from less to more specific: + + from x.y import z as a + ______| | | | + | | | | + PACKAGE | | | + ________| | | + | | | + MODULE | | + _________________| | + | | + ATTRIBUTE | + ______________________| + | + ALIAS + """ + + PACKAGE = 1 + MODULE = 2 + ATTRIBUTE = 3 + ALIAS = 4 + + def sort_code_string( code: str, extension: Optional[str] = None, @@ -399,7 +426,7 @@ def find_imports_in_code( code: str, config: Config = DEFAULT_CONFIG, file_path: Optional[Path] = None, - unique: bool = False, + unique: Union[bool, ImportKey] = False, **config_kwargs, ) -> Iterator[identify.Import]: """Finds and returns all imports within the provided code string. @@ -423,7 +450,7 @@ def find_imports_in_stream( input_stream: TextIO, config: Config = DEFAULT_CONFIG, file_path: Optional[Path] = None, - unique: bool = False, + unique: Union[bool, ImportKey] = False, _seen: Optional[Set[str]] = None, **config_kwargs, ) -> Iterator[identify.Import]: @@ -443,8 +470,16 @@ def find_imports_in_stream( seen: Set[str] = set() if _seen is None else _seen for identified_import in identified_imports: - key = identified_import.statement() - if key not in seen: + if unique in (True, ImportKey.ALIAS): + key = identified_import.statement() + elif unique == ImportKey.ATTRIBUTE: + key = f"{identified_import.module}.{identified_import.attribute}" + elif unique == ImportKey.MODULE: + key = identified_import.module + elif unique == ImportKey.PACKAGE: + key = identified_import.module.split(".")[0] + + if key and key not in seen: seen.add(key) yield identified_import @@ -453,7 +488,7 @@ def find_imports_in_file( filename: Union[str, Path], config: Config = DEFAULT_CONFIG, file_path: Optional[Path] = None, - unique: bool = False, + unique: Union[bool, ImportKey] = False, **config_kwargs, ) -> Iterator[identify.Import]: """Finds and returns all imports within the provided source file. @@ -479,7 +514,7 @@ def find_imports_in_paths( paths: Iterator[Union[str, Path]], config: Config = DEFAULT_CONFIG, file_path: Optional[Path] = None, - unique: bool = False, + unique: Union[bool, ImportKey] = False, **config_kwargs, ) -> Iterator[identify.Import]: """Finds and returns all imports within the provided source paths. From 228266772fb4466fd716ad56d6025442f693a077 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 30 Dec 2020 01:33:08 -0800 Subject: [PATCH 1334/1439] Add quick support from CLI to most uniqueness keys --- isort/main.py | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/isort/main.py b/isort/main.py index 22164f1af..09f3c64e4 100644 --- a/isort/main.py +++ b/isort/main.py @@ -869,12 +869,39 @@ def identify_imports_main( parser.add_argument( "files", nargs="*", help="One or more Python source files that need their imports sorted." ) - parser.add_argument( + + uniqueness = parser.add_mutually_exclusive_group() + uniqueness.add_argument( "--unique", action="store_true", default=False, help="If true, isort will only identify unique imports.", ) + uniqueness.add_argument( + "--packages", + dest="unique", + action="store_const", + const=api.ImportKey.PACKAGE, + default=False, + help="If true, isort will only identify the unique top level modules imported.", + ) + uniqueness.add_argument( + "--modules", + dest="unique", + action="store_const", + const=api.ImportKey.MODULE, + default=False, + help="If true, isort will only identify the unique modules imported.", + ) + uniqueness.add_argument( + "--attributes", + dest="unique", + action="store_const", + const=api.ImportKey.ATTRIBUTE, + default=False, + help="If true, isort will only identify the unique attributes imported.", + ) + arguments = parser.parse_args(argv) file_names = arguments.files @@ -886,7 +913,14 @@ def identify_imports_main( identified_imports = api.find_imports_in_paths(file_names, unique=arguments.unique) for identified_import in identified_imports: - print(str(identified_import)) + if arguments.unique == api.ImportKey.PACKAGE: + print(identified_import.module.split(".")[0]) + elif arguments.unique == api.ImportKey.MODULE: + print(identified_import.module) + elif arguments.unique == api.ImportKey.ATTRIBUTE: + print(f"{identified_import.module}.{identified_import.attribute}") + else: + print(str(identified_import)) def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = None) -> None: From b1e676312930029c3974cadb5bdffcdaaf7d02ba Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 30 Dec 2020 01:50:40 -0800 Subject: [PATCH 1335/1439] Add support for quick identification of just the top-level imports, before functions and classes. --- isort/identify.py | 11 +++++++++-- isort/output.py | 3 +-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/isort/identify.py b/isort/identify.py index 156d866a7..46e3df5f4 100644 --- a/isort/identify.py +++ b/isort/identify.py @@ -3,13 +3,15 @@ """ from functools import partial from pathlib import Path -from typing import Iterator, NamedTuple, Optional, TextIO +from typing import Iterator, NamedTuple, Optional, TextIO, Tuple from isort.parse import _normalize_line, _strip_syntax, skip_line from .comments import parse as parse_comments from .settings import DEFAULT_CONFIG, Config +STATEMENT_DECLARATIONS: Tuple[str, ...] = ("def ", "cdef ", "cpdef ", "class ", "@", "async def") + class Import(NamedTuple): line_number: int @@ -36,7 +38,10 @@ def __str__(self): def imports( - input_stream: TextIO, config: Config = DEFAULT_CONFIG, file_path: Optional[Path] = None + input_stream: TextIO, + config: Config = DEFAULT_CONFIG, + file_path: Optional[Path] = None, + top_only: bool = False, ) -> Iterator[Import]: """Parses a python file taking out and categorizing imports.""" in_quote = "" @@ -48,6 +53,8 @@ def imports( ) if skipping_line: + if top_only and not in_quote and line.startswith(STATEMENT_DECLARATIONS): + break continue line, *end_of_line_comment = line.split("#", 1) diff --git a/isort/output.py b/isort/output.py index 1d64785bc..e0855de67 100644 --- a/isort/output.py +++ b/isort/output.py @@ -7,10 +7,9 @@ from . import parse, sorting, wrap from .comments import add_to_line as with_comments +from .identify import STATEMENT_DECLARATIONS from .settings import DEFAULT_CONFIG, Config -STATEMENT_DECLARATIONS: Tuple[str, ...] = ("def ", "cdef ", "cpdef ", "class ", "@", "async def") - def sorted_imports( parsed: parse.ParsedContent, From 95474d38a8cc3edb5ff2eea278b8d8957a2c41be Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 30 Dec 2020 01:51:01 -0800 Subject: [PATCH 1336/1439] Expand quick identification support of just top import section to isort API --- isort/api.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/isort/api.py b/isort/api.py index 16e92c975..a2bc86981 100644 --- a/isort/api.py +++ b/isort/api.py @@ -427,6 +427,7 @@ def find_imports_in_code( config: Config = DEFAULT_CONFIG, file_path: Optional[Path] = None, unique: Union[bool, ImportKey] = False, + top_only: bool = False, **config_kwargs, ) -> Iterator[identify.Import]: """Finds and returns all imports within the provided code string. @@ -435,6 +436,7 @@ def find_imports_in_code( - **config**: The config object to use when sorting imports. - **file_path**: The disk location where the code string was pulled from. - **unique**: If True, only the first instance of an import is returned. + - **top_only**: If True, only return imports that occur before the first function or class. - ****config_kwargs**: Any config modifications. """ yield from find_imports_in_stream( @@ -442,6 +444,7 @@ def find_imports_in_code( config=config, file_path=file_path, unique=unique, + top_only=top_only, **config_kwargs, ) @@ -451,6 +454,7 @@ def find_imports_in_stream( config: Config = DEFAULT_CONFIG, file_path: Optional[Path] = None, unique: Union[bool, ImportKey] = False, + top_only: bool = False, _seen: Optional[Set[str]] = None, **config_kwargs, ) -> Iterator[identify.Import]: @@ -460,6 +464,7 @@ def find_imports_in_stream( - **config**: The config object to use when sorting imports. - **file_path**: The disk location where the code string was pulled from. - **unique**: If True, only the first instance of an import is returned. + - **top_only**: If True, only return imports that occur before the first function or class. - **_seen**: An optional set of imports already seen. Generally meant only for internal use. - ****config_kwargs**: Any config modifications. """ @@ -489,6 +494,7 @@ def find_imports_in_file( config: Config = DEFAULT_CONFIG, file_path: Optional[Path] = None, unique: Union[bool, ImportKey] = False, + top_only: bool = False, **config_kwargs, ) -> Iterator[identify.Import]: """Finds and returns all imports within the provided source file. @@ -498,6 +504,7 @@ def find_imports_in_file( - **config**: The config object to use when sorting imports. - **file_path**: The disk location where the code string was pulled from. - **unique**: If True, only the first instance of an import is returned. + - **top_only**: If True, only return imports that occur before the first function or class. - ****config_kwargs**: Any config modifications. """ with io.File.read(filename) as source_file: @@ -515,6 +522,7 @@ def find_imports_in_paths( config: Config = DEFAULT_CONFIG, file_path: Optional[Path] = None, unique: Union[bool, ImportKey] = False, + top_only: bool = False, **config_kwargs, ) -> Iterator[identify.Import]: """Finds and returns all imports within the provided source paths. @@ -524,6 +532,7 @@ def find_imports_in_paths( - **config**: The config object to use when sorting imports. - **file_path**: The disk location where the code string was pulled from. - **unique**: If True, only the first instance of an import is returned. + - **top_only**: If True, only return imports that occur before the first function or class. - ****config_kwargs**: Any config modifications. """ config = _config(path=file_path, config=config, **config_kwargs) From fc3a1ec9daa1fab9c80ba43c2f902203a1ccd7b3 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 30 Dec 2020 02:04:14 -0800 Subject: [PATCH 1337/1439] Expose top-only identification functionality to CLI --- isort/main.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/isort/main.py b/isort/main.py index 09f3c64e4..c9f339647 100644 --- a/isort/main.py +++ b/isort/main.py @@ -869,6 +869,12 @@ def identify_imports_main( parser.add_argument( "files", nargs="*", help="One or more Python source files that need their imports sorted." ) + parser.add_argument( + "--top-only", + action="store_true", + default=False, + help="Only identify imports that occur in before functions or classes.", + ) uniqueness = parser.add_mutually_exclusive_group() uniqueness.add_argument( @@ -907,10 +913,14 @@ def identify_imports_main( file_names = arguments.files if file_names == ["-"]: identified_imports = api.find_imports_in_stream( - sys.stdin if stdin is None else stdin, unique=arguments.unique + sys.stdin if stdin is None else stdin, + unique=arguments.unique, + top_only=arguments.top_only, ) else: - identified_imports = api.find_imports_in_paths(file_names, unique=arguments.unique) + identified_imports = api.find_imports_in_paths( + file_names, unique=arguments.unique, top_only=arguments.top_only + ) for identified_import in identified_imports: if arguments.unique == api.ImportKey.PACKAGE: From 00f6db125235b1bfb4bd5aecefd1f89256aa043d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 30 Dec 2020 02:13:19 -0800 Subject: [PATCH 1338/1439] Add support even faster identification of imports if only interested in top of file --- isort/api.py | 9 +++++++-- isort/identify.py | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/isort/api.py b/isort/api.py index a2bc86981..6c2011a5f 100644 --- a/isort/api.py +++ b/isort/api.py @@ -469,7 +469,9 @@ def find_imports_in_stream( - ****config_kwargs**: Any config modifications. """ config = _config(path=file_path, config=config, **config_kwargs) - identified_imports = identify.imports(input_stream, config=config, file_path=file_path) + identified_imports = identify.imports( + input_stream, config=config, file_path=file_path, top_only=top_only + ) if not unique: yield from identified_imports @@ -513,6 +515,7 @@ def find_imports_in_file( config=config, file_path=file_path or source_file.path, unique=unique, + top_only=top_only, **config_kwargs, ) @@ -539,7 +542,9 @@ def find_imports_in_paths( seen: Optional[Set[str]] = set() if unique else None yield from chain( *( - find_imports_in_file(file_name, unique=unique, config=config, _seen=seen) + find_imports_in_file( + file_name, unique=unique, config=config, top_only=top_only, _seen=seen + ) for file_name in files.find(map(str, paths), config, [], []) ) ) diff --git a/isort/identify.py b/isort/identify.py index 46e3df5f4..7a9093066 100644 --- a/isort/identify.py +++ b/isort/identify.py @@ -52,9 +52,9 @@ def imports( line, in_quote=in_quote, index=index, section_comments=config.section_comments ) + if top_only and not in_quote and line.startswith(STATEMENT_DECLARATIONS): + break if skipping_line: - if top_only and not in_quote and line.startswith(STATEMENT_DECLARATIONS): - break continue line, *end_of_line_comment = line.split("#", 1) From fe27db934b475985b6120aa8fab046a6777097dc Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 30 Dec 2020 02:15:17 -0800 Subject: [PATCH 1339/1439] Require at least one file or path for import identification CLI --- isort/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/main.py b/isort/main.py index c9f339647..c3190349e 100644 --- a/isort/main.py +++ b/isort/main.py @@ -867,7 +867,7 @@ def identify_imports_main( "Use `-` as the first argument to represent stdin." ) parser.add_argument( - "files", nargs="*", help="One or more Python source files that need their imports sorted." + "files", nargs="+", help="One or more Python source files that need their imports sorted." ) parser.add_argument( "--top-only", From 6e5414cb72ee822d351968626bb9a37e10425857 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 30 Dec 2020 02:54:21 -0800 Subject: [PATCH 1340/1439] Add link following support (and lack thereof) to import identification CLI --- isort/main.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/isort/main.py b/isort/main.py index c3190349e..27866286e 100644 --- a/isort/main.py +++ b/isort/main.py @@ -876,6 +876,14 @@ def identify_imports_main( help="Only identify imports that occur in before functions or classes.", ) + target_group = parser.add_argument_group("target options") + target_group.add_argument( + "--follow-links", + action="store_true", + default=False, + help="Tells isort to follow symlinks that are encountered when running recursively.", + ) + uniqueness = parser.add_mutually_exclusive_group() uniqueness.add_argument( "--unique", @@ -916,10 +924,14 @@ def identify_imports_main( sys.stdin if stdin is None else stdin, unique=arguments.unique, top_only=arguments.top_only, + follow_links=arguments.follow_links, ) else: identified_imports = api.find_imports_in_paths( - file_names, unique=arguments.unique, top_only=arguments.top_only + file_names, + unique=arguments.unique, + top_only=arguments.top_only, + follow_links=arguments.follow_links, ) for identified_import in identified_imports: From 07768d11bbd21ed7760622a7f4644ed0488efa79 Mon Sep 17 00:00:00 2001 From: gofr <32750931+gofr@users.noreply.github.com> Date: Wed, 30 Dec 2020 18:52:39 +0100 Subject: [PATCH 1341/1439] Make force_sort_within_sections respect case force_sort_within_sections only looked at the order_by_type option to determine how to order imports with different case in a section. Whether you order import names by type or not also affected the order of the modules. When force_sort_within_sections is used: * ignore case on the module name if case_sensitive is false, * ignore case on the imported names if order_by_type is false. --- isort/output.py | 1 + isort/sorting.py | 14 ++++++++++++-- tests/unit/test_isort.py | 30 +++++++++++++++++++++++++++++- 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/isort/output.py b/isort/output.py index e0855de67..2e6e6c1eb 100644 --- a/isort/output.py +++ b/isort/output.py @@ -96,6 +96,7 @@ def sorted_imports( new_section_output, key=partial( sorting.section_key, + case_sensitive=config.case_sensitive, order_by_type=config.order_by_type, force_to_top=config.force_to_top, lexicographical=config.lexicographical, diff --git a/isort/sorting.py b/isort/sorting.py index b614abe79..60401f8fe 100644 --- a/isort/sorting.py +++ b/isort/sorting.py @@ -53,6 +53,7 @@ def module_key( def section_key( line: str, + case_sensitive: bool, order_by_type: bool, force_to_top: List[str], lexicographical: bool = False, @@ -76,8 +77,17 @@ def section_key( line = re.sub("^import ", "", line) if line.split(" ")[0] in force_to_top: section = "A" - if not order_by_type: - line = line.lower() + if not case_sensitive or not order_by_type: + split_module = line.split(" import ", 1) + if len(split_module) > 1: + module_name, names = split_module + if not case_sensitive: + module_name = module_name.lower() + if not order_by_type: + names = names.lower() + line = " import ".join([module_name, names]) + elif not case_sensitive: + line = line.lower() return f"{section}{len(line) if length_sort else ''}{line}" diff --git a/tests/unit/test_isort.py b/tests/unit/test_isort.py index 9655d9f66..9d2d410cf 100644 --- a/tests/unit/test_isort.py +++ b/tests/unit/test_isort.py @@ -2358,10 +2358,10 @@ def test_alphabetic_sorting_no_newlines() -> None: def test_sort_within_section() -> None: """Test to ensure its possible to force isort to sort within sections""" test_input = ( - "from Foob import ar\n" "import foo\n" "from foo import bar\n" "from foo.bar import Quux, baz\n" + "from Foob import ar\n" ) test_output = isort.code(test_input, force_sort_within_sections=True) assert test_output == test_input @@ -2381,6 +2381,34 @@ def test_sort_within_section() -> None: ) assert test_output == test_input + test_input = ( + "from Foob import ar\n" + "import foo\n" + "from foo import bar\n" + "from foo.bar import baz\n" + "from foo.bar import Quux\n" + ) + test_output = isort.code( + code=test_input, + case_sensitive=True, + force_sort_within_sections=True, + order_by_type=False, + force_single_line=True, + ) + assert test_output == test_input + + test_input = ( + "from Foob import ar\n" "import foo\n" "from foo import Quux\n" "from foo import baz\n" + ) + test_output = isort.code( + code=test_input, + case_sensitive=True, + force_sort_within_sections=True, + order_by_type=True, + force_single_line=True, + ) + assert test_output == test_input + def test_sorting_with_two_top_comments() -> None: """Test to ensure isort will sort files that contain 2 top comments""" From 721cfd62f487ff7e5ded42a32c3c083bc4d172c1 Mon Sep 17 00:00:00 2001 From: gofr <32750931+gofr@users.noreply.github.com> Date: Wed, 30 Dec 2020 19:18:53 +0100 Subject: [PATCH 1342/1439] Fix Gitter link in Contributing guide doc --- docs/contributing/1.-contributing-guide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contributing/1.-contributing-guide.md b/docs/contributing/1.-contributing-guide.md index e51e852d8..2ba296f8d 100644 --- a/docs/contributing/1.-contributing-guide.md +++ b/docs/contributing/1.-contributing-guide.md @@ -60,7 +60,7 @@ Congrats! You're now ready to make a contribution! Use the following as a guide 1. Check the [issues page](https://github.com/pycqa/isort/issues) on GitHub to see if the task you want to complete is listed there. - If it's listed there, write a comment letting others know you are working on it. - If it's not listed in GitHub issues, go ahead and log a new issue. Then add a comment letting everyone know you have it under control. - - If you're not sure if it's something that is good for the main isort project and want immediate feedback, you can discuss it [here](https://gitter.im/pycqa/isort). + - If you're not sure if it's something that is good for the main isort project and want immediate feedback, you can discuss it [here](https://gitter.im/timothycrosley/isort). 2. Create an issue branch for your local work `git checkout -b issue/$ISSUE-NUMBER`. 3. Do your magic here. 4. Ensure your code matches the [HOPE-8 Coding Standard](https://github.com/hugapi/HOPE/blob/master/all/HOPE-8--Style-Guide-for-Hug-Code.md#hope-8----style-guide-for-hug-code) used by the project. From fd7a2dccbb867d23ae5c98d48261de517b83731d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 30 Dec 2020 13:49:00 -0800 Subject: [PATCH 1343/1439] Config path should never be auto determined for import identification CLI --- isort/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/api.py b/isort/api.py index 6c2011a5f..024b75ac5 100644 --- a/isort/api.py +++ b/isort/api.py @@ -468,7 +468,7 @@ def find_imports_in_stream( - **_seen**: An optional set of imports already seen. Generally meant only for internal use. - ****config_kwargs**: Any config modifications. """ - config = _config(path=file_path, config=config, **config_kwargs) + config = _config(config=config, **config_kwargs) identified_imports = identify.imports( input_stream, config=config, file_path=file_path, top_only=top_only ) @@ -538,7 +538,7 @@ def find_imports_in_paths( - **top_only**: If True, only return imports that occur before the first function or class. - ****config_kwargs**: Any config modifications. """ - config = _config(path=file_path, config=config, **config_kwargs) + config = _config(config=config, **config_kwargs) seen: Optional[Set[str]] = set() if unique else None yield from chain( *( From 570b66e42523e424f217e2e850c55b3d63b0f9c0 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 30 Dec 2020 14:35:49 -0800 Subject: [PATCH 1344/1439] Undo skip gitignore for black profile --- isort/profiles.py | 1 - 1 file changed, 1 deletion(-) diff --git a/isort/profiles.py b/isort/profiles.py index 523b1ec66..cb8cb5688 100644 --- a/isort/profiles.py +++ b/isort/profiles.py @@ -8,7 +8,6 @@ "use_parentheses": True, "ensure_newline_before_comments": True, "line_length": 88, - "skip_gitignore": True, } django = { "combine_as_imports": True, From 5d0f7e1658a6aa7e1f3bb8d54dc9487218307fb5 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 30 Dec 2020 14:42:32 -0800 Subject: [PATCH 1345/1439] Updadte changelog to include fix for #1593 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e377fd21..aa65c4df2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ ### 5.7.0 December TBD - Fixed #1612: In rare circumstances an extra comma is added after import and before comment. + - Fixed #1593: isort encounters bug in Python 3.6.0. - Implemented #1596: Provide ways for extension formatting and file paths to be specified when using streaming input from CLI. - Implemented #1583: Ability to output and diff within a single API call to `isort.file`. - Implemented #1562, #1592 & #1593: Better more useful fatal error messages. From 0b072150304d582ffd67b9343f1dd30a73043625 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 30 Dec 2020 15:05:16 -0800 Subject: [PATCH 1346/1439] Add initial unit testing for identify - with focuses on yield and raise edge cases currently handled by import sorting core --- tests/unit/test_identify.py | 139 ++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 tests/unit/test_identify.py diff --git a/tests/unit/test_identify.py b/tests/unit/test_identify.py new file mode 100644 index 000000000..08a7e7269 --- /dev/null +++ b/tests/unit/test_identify.py @@ -0,0 +1,139 @@ +from io import StringIO +from typing import List + +from isort import identify + + +def imports_in_code(code: str, **kwargs) -> List[identify.Import]: + return list(identify.imports(StringIO(code, **kwargs))) + + +def test_yield_edge_cases(): + assert not imports_in_code( + """ +raise SomeException("Blah") \\ + from exceptionsInfo.popitem()[1] +""" + ) + assert not imports_in_code( + """ +def generator_function(): + yield \\ + from other_function()[1] +""" + ) + assert ( + len( + imports_in_code( + """ +# one + +# two + + +def function(): + # three \\ + import b + import a +""" + ) + ) + == 2 + ) + assert ( + len( + imports_in_code( + """ +# one + +# two + + +def function(): + raise \\ + import b + import a +""" + ) + ) + == 1 + ) + assert not imports_in_code( + """ +def generator_function(): + ( + yield + from other_function()[1] + ) +""" + ) + assert not imports_in_code( + """ +def generator_function(): + ( + ( + (((( + ((((( + (( + ((( + yield + + + + from other_function()[1] + ))))))))))))) + ))) +""" + ) + assert ( + len( + imports_in_code( + """ +def generator_function(): + import os + + yield \\ + from other_function()[1] +""" + ) + ) + == 1 + ) + + assert not imports_in_code( + """ +def generator_function(): + ( + ( + (((( + ((((( + (( + ((( + yield +""" + ) + assert not imports_in_code( + """ +def generator_function(): + ( + ( + (((( + ((((( + (( + ((( + raise ( +""" + ) + assert not imports_in_code( + """ +def generator_function(): + ( + ( + (((( + ((((( + (( + ((( + raise \\ + from \\ +""" + ) From 8b83d56588e9d4d0cf2e37c893de3f20ef78c3f8 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 30 Dec 2020 15:05:27 -0800 Subject: [PATCH 1347/1439] Fix handling of yield and raise statements in import identification --- isort/identify.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/isort/identify.py b/isort/identify.py index 7a9093066..1f8185cbe 100644 --- a/isort/identify.py +++ b/isort/identify.py @@ -57,6 +57,25 @@ def imports( if skipping_line: continue + stripped_line = line.strip().split("#")[0] + if stripped_line.startswith("raise") or stripped_line.startswith("yield"): + if stripped_line == "yield": + while not stripped_line or stripped_line == "yield": + try: + index, next_line = next(indexed_input) + except StopIteration: + break + + stripped_line = next_line.strip().split("#")[0] + while stripped_line.endswith("\\"): + try: + index, next_line = next(indexed_input) + except StopIteration: + break + + stripped_line = next_line.strip().split("#")[0] + continue + line, *end_of_line_comment = line.split("#", 1) statements = [line.strip() for line in line.split(";")] if end_of_line_comment: From 69a89c0b8224895e8d524116e46ac8cd965a181f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 30 Dec 2020 15:31:39 -0800 Subject: [PATCH 1348/1439] Add additional identification test cases --- tests/unit/test_identify.py | 65 +++++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_identify.py b/tests/unit/test_identify.py index 08a7e7269..8515d459e 100644 --- a/tests/unit/test_identify.py +++ b/tests/unit/test_identify.py @@ -5,10 +5,49 @@ def imports_in_code(code: str, **kwargs) -> List[identify.Import]: - return list(identify.imports(StringIO(code, **kwargs))) + return list(identify.imports(StringIO(code), **kwargs)) -def test_yield_edge_cases(): +def test_top_only(): + imports_in_function = """ +import abc + +def xyz(): + import defg +""" + assert len(imports_in_code(imports_in_function)) == 2 + assert len(imports_in_code(imports_in_function, top_only=True)) == 1 + + imports_after_class = """ +import abc + +class MyObject: + pass + +import defg +""" + assert len(imports_in_code(imports_after_class)) == 2 + assert len(imports_in_code(imports_after_class, top_only=True)) == 1 + + +def test_top_doc_string(): + assert ( + len( + imports_in_code( + ''' +#! /bin/bash import x +"""import abc +from y import z +""" +import abc +''' + ) + ) + == 1 + ) + + +def test_yield_and_raise_edge_cases(): assert not imports_in_code( """ raise SomeException("Blah") \\ @@ -137,3 +176,25 @@ def generator_function(): from \\ """ ) + assert ( + len( + imports_in_code( + """ +def generator_function(): + ( + ( + (((( + ((((( + (( + ((( + raise \\ + from \\ + import c + + import abc + import xyz +""" + ) + ) + == 2 + ) From 15502c875c46d0c8761fb3f7c2b6c1ef4b518a49 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 30 Dec 2020 15:38:59 -0800 Subject: [PATCH 1349/1439] Expose ImportKey from main isort import --- isort/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/isort/__init__.py b/isort/__init__.py index 03223665d..fdb1d6e93 100644 --- a/isort/__init__.py +++ b/isort/__init__.py @@ -1,6 +1,7 @@ """Defines the public isort interface""" from . import settings from ._version import __version__ +from .api import ImportKey from .api import check_code_string as check_code from .api import ( check_file, From 0383c3668d289db2d41a43165b564f75bb5e64ff Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 30 Dec 2020 16:26:36 -0800 Subject: [PATCH 1350/1439] Fix indented identification isort --- isort/identify.py | 30 +++++++++++----------- tests/unit/test_identify.py | 50 ++++++++++++++++++++++++++++++++++++- tests/unit/test_isort.py | 9 +++---- tests/unit/test_main.py | 2 +- 4 files changed, 70 insertions(+), 21 deletions(-) diff --git a/isort/identify.py b/isort/identify.py index 1f8185cbe..dbab7f6ae 100644 --- a/isort/identify.py +++ b/isort/identify.py @@ -47,17 +47,17 @@ def imports( in_quote = "" indexed_input = enumerate(input_stream) - for index, line in indexed_input: + for index, raw_line in indexed_input: (skipping_line, in_quote) = skip_line( - line, in_quote=in_quote, index=index, section_comments=config.section_comments + raw_line, in_quote=in_quote, index=index, section_comments=config.section_comments ) - if top_only and not in_quote and line.startswith(STATEMENT_DECLARATIONS): + if top_only and not in_quote and raw_line.startswith(STATEMENT_DECLARATIONS): break if skipping_line: continue - stripped_line = line.strip().split("#")[0] + stripped_line = raw_line.strip().split("#")[0] if stripped_line.startswith("raise") or stripped_line.startswith("yield"): if stripped_line == "yield": while not stripped_line or stripped_line == "yield": @@ -76,16 +76,16 @@ def imports( stripped_line = next_line.strip().split("#")[0] continue - line, *end_of_line_comment = line.split("#", 1) + line, *end_of_line_comment = raw_line.split("#", 1) statements = [line.strip() for line in line.split(";")] if end_of_line_comment: statements[-1] = f"{statements[-1]}#{end_of_line_comment[0]}" for statement in statements: line, _raw_line = _normalize_line(statement) - if line.lstrip().startswith(("import ", "cimport ")): + if line.startswith(("import ", "cimport ")): type_of_import = "straight" - elif line.lstrip().startswith("from "): + elif line.startswith("from "): type_of_import = "from" else: continue @@ -101,7 +101,7 @@ def imports( identified_import = partial( Import, index + 1, # line numbers use 1 based indexing - line.startswith(" ") or line.startswith("\n"), + raw_line.startswith((" ", "\t")), cimport=cimports, file_path=file_path, ) @@ -177,18 +177,20 @@ def imports( direct_imports.remove("as") just_imports[1:] = direct_imports if attribute == alias and config.remove_redundant_aliases: - pass + yield identified_import(top_level_module, attribute) else: yield identified_import(top_level_module, attribute, alias=alias) else: module = just_imports[as_index - 1] alias = just_imports[as_index + 1] - direct_imports.remove(alias) - direct_imports.remove("as") - just_imports[1:] = direct_imports - if not (module == alias and config.remove_redundant_aliases): - yield identified_import(module, alias) + just_imports.remove(alias) + just_imports.remove("as") + just_imports.remove(module) + if module == alias and config.remove_redundant_aliases: + yield identified_import(module) + else: + yield identified_import(module, alias=alias) if just_imports: if type_of_import == "from": diff --git a/tests/unit/test_identify.py b/tests/unit/test_identify.py index 8515d459e..64c9f28a9 100644 --- a/tests/unit/test_identify.py +++ b/tests/unit/test_identify.py @@ -1,7 +1,7 @@ from io import StringIO from typing import List -from isort import identify +from isort import Config, identify def imports_in_code(code: str, **kwargs) -> List[identify.Import]: @@ -198,3 +198,51 @@ def generator_function(): ) == 2 ) + + +def test_complex_examples(): + assert ( + len( + imports_in_code( + """ +import a, b, c; import n + +x = ( + 1, + 2, + 3 +) + +import x +from os \\ + import path +from os ( + import path +) +from os import ( \\""" + ) + ) + == 7 + ) + assert not imports_in_code("from os import \\") + + +def test_aliases(): + assert imports_in_code("import os as os")[0].alias == "os" + assert not imports_in_code( + "import os as os", + config=Config( + remove_redundant_aliases=True, + ), + )[0].alias + + assert imports_in_code("from os import path as path")[0].alias == "path" + assert not imports_in_code( + "from os import path as path", config=Config(remove_redundant_aliases=True) + )[0].alias + + +def test_indented(): + assert not imports_in_code("import os")[0].indented + assert imports_in_code(" import os")[0].indented + assert imports_in_code("\timport os")[0].indented diff --git a/tests/unit/test_isort.py b/tests/unit/test_isort.py index 9655d9f66..125bd0dfa 100644 --- a/tests/unit/test_isort.py +++ b/tests/unit/test_isort.py @@ -4946,13 +4946,12 @@ def test_find_imports_in_code() -> None: ":3 import second_straight", ":4 import first_from.first_from_function_1", ":4 import first_from.first_from_function_2", - ":5 import bad_name.good_name", - ":5 import bad_name", + ":5 import bad_name as good_name", ":6 import parent.some_bad_defs.bad_name_1 as ok_name_1", ":6 import parent.some_bad_defs.bad_name_2 as ok_name_2", - ":12 import needed_in_bla_2", - ":15 import needed_in_bla", - ":18 import needed_in_bla_bla", + ":12 indented import needed_in_bla_2", + ":15 indented import needed_in_bla", + ":18 indented import needed_in_bla_bla", ":22 import needed_in_end", ] diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 7291cb5fe..d1ae50214 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -995,7 +995,7 @@ def test_only_modified_flag(tmpdir, capsys): def test_identify_imports_main(tmpdir, capsys): - file_content = "import mod2\n import mod2\n" "a = 1\n" "import mod1\n" + file_content = "import mod2\nimport mod2\n" "a = 1\n" "import mod1\n" some_file = tmpdir.join("some_file.py") some_file.write(file_content) file_imports = f"{some_file}:1 import mod2\n{some_file}:4 import mod1\n" From 8e70db8d0092c333c112f0685bc9e416caab9d80 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 30 Dec 2020 16:51:48 -0800 Subject: [PATCH 1351/1439] 100% test coverage for new identify module --- isort/identify.py | 18 ++++++++++-------- tests/unit/test_identify.py | 28 +++++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/isort/identify.py b/isort/identify.py index dbab7f6ae..ff0282443 100644 --- a/isort/identify.py +++ b/isort/identify.py @@ -74,7 +74,7 @@ def imports( break stripped_line = next_line.strip().split("#")[0] - continue + continue # pragma: no cover line, *end_of_line_comment = raw_line.split("#", 1) statements = [line.strip() for line in line.split(";")] @@ -88,7 +88,7 @@ def imports( elif line.startswith("from "): type_of_import = "from" else: - continue + continue # pragma: no cover import_string, _ = parse_comments(line) normalized_import_string = ( @@ -135,13 +135,15 @@ def imports( break line, _ = parse_comments(next_line) import_string += "\n" + line - - if import_string.strip().endswith( - (" import", " cimport") - ) or line.strip().startswith(("import ", "cimport ")): - import_string += "\n" + line else: - import_string = import_string.rstrip().rstrip("\\") + " " + line.lstrip() + if import_string.strip().endswith( + (" import", " cimport") + ) or line.strip().startswith(("import ", "cimport ")): + import_string += "\n" + line + else: + import_string = ( + import_string.rstrip().rstrip("\\") + " " + line.lstrip() + ) if type_of_import == "from": import_string = ( diff --git a/tests/unit/test_identify.py b/tests/unit/test_identify.py index 64c9f28a9..c2918b529 100644 --- a/tests/unit/test_identify.py +++ b/tests/unit/test_identify.py @@ -2,6 +2,7 @@ from typing import List from isort import Config, identify +from isort.identify import Import def imports_in_code(code: str, **kwargs) -> List[identify.Import]: @@ -219,12 +220,37 @@ def test_complex_examples(): from os ( import path ) +from os import \\ + path +from os \\ + import ( + path + ) from os import ( \\""" ) ) - == 7 + == 9 ) assert not imports_in_code("from os import \\") + assert ( + imports_in_code( + """ +from os \\ + import ( + system""" + ) + == [ + Import( + line_number=2, + indented=False, + module="os", + attribute="system", + alias=None, + cimport=False, + file_path=None, + ) + ] + ) def test_aliases(): From 3eb14eb975509a34843aa6384a19374854f5979f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 30 Dec 2020 17:06:58 -0800 Subject: [PATCH 1352/1439] 100% test coverage --- tests/unit/test_api.py | 17 ++++++++++++++++- tests/unit/test_main.py | 12 ++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_api.py b/tests/unit/test_api.py index 20c2fdae6..2247d8fc5 100644 --- a/tests/unit/test_api.py +++ b/tests/unit/test_api.py @@ -5,7 +5,7 @@ import pytest -from isort import api +from isort import ImportKey, api from isort.settings import Config imperfect_content = "import b\nimport a\n" @@ -86,3 +86,18 @@ def test_sort_code_string_mixed_newlines(): def test_find_imports_in_file(imperfect): found_imports = list(api.find_imports_in_file(imperfect)) assert "b" in [found_import.module for found_import in found_imports] + + +def test_find_imports_in_code(): + code = """ +from x.y import z as a +from x.y import z as a +from x.y import z +import x.y +import x +""" + assert len(list(api.find_imports_in_code(code))) == 5 + assert len(list(api.find_imports_in_code(code, unique=True))) == 4 + assert len(list(api.find_imports_in_code(code, unique=ImportKey.ATTRIBUTE))) == 3 + assert len(list(api.find_imports_in_code(code, unique=ImportKey.MODULE))) == 2 + assert len(list(api.find_imports_in_code(code, unique=ImportKey.PACKAGE))) == 1 diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index d1ae50214..9a78cbad8 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -1022,3 +1022,15 @@ def test_identify_imports_main(tmpdir, capsys): assert out.replace("\r\n", "\n") == file_imports_with_dupes.replace(str(some_file), "") main.identify_imports_main([str(tmpdir)]) + + main.identify_imports_main(["-", "--packages"], stdin=as_stream(file_content)) + out, error = capsys.readouterr() + len(out.split("\n")) == 2 + + main.identify_imports_main(["-", "--modules"], stdin=as_stream(file_content)) + out, error = capsys.readouterr() + len(out.split("\n")) == 2 + + main.identify_imports_main(["-", "--attributes"], stdin=as_stream(file_content)) + out, error = capsys.readouterr() + len(out.split("\n")) == 2 From 8372d71147334e5a4677197fe1fc1a61c5080435 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 30 Dec 2020 17:07:50 -0800 Subject: [PATCH 1353/1439] Bump to version 5.7.0 --- CHANGELOG.md | 2 +- isort/_version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa65c4df2..dcfeb972d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. Find out more about isort's release policy [here](https://pycqa.github.io/isort/docs/major_releases/release_policy/). -### 5.7.0 December TBD +### 5.7.0 December 30th 2020 - Fixed #1612: In rare circumstances an extra comma is added after import and before comment. - Fixed #1593: isort encounters bug in Python 3.6.0. - Implemented #1596: Provide ways for extension formatting and file paths to be specified when using streaming input from CLI. diff --git a/isort/_version.py b/isort/_version.py index f0b716b28..7f4ce2d4c 100644 --- a/isort/_version.py +++ b/isort/_version.py @@ -1 +1 @@ -__version__ = "5.6.4" +__version__ = "5.7.0" diff --git a/pyproject.toml b/pyproject.toml index 122906ec7..0889cc4f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ line-length = 100 [tool.poetry] name = "isort" -version = "5.6.4" +version = "5.7.0" description = "A Python utility / library to sort Python imports." authors = ["Timothy Crosley "] license = "MIT" From 681b26c766f863a790bd50eb9d2e0a80022f1343 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 30 Dec 2020 17:11:21 -0800 Subject: [PATCH 1354/1439] Add @dwanderson-intel, Quentin Santos (@qsantos), and @gofr to acknowledgements --- docs/contributing/4.-acknowledgements.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index 48f7d8cf0..03ddc1e67 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -212,6 +212,9 @@ Code Contributors - Tamara (@infinityxxx) - Akihiro Nitta (@akihironitta) - Samuel Gaist (@sgaist) +- @dwanderson-intel +- Quentin Santos (@qsantos) +- @gofr Documenters =================== From a8f4ff3e85b7a26cad2c0af499028caa94f5febf Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 30 Dec 2020 17:12:43 -0800 Subject: [PATCH 1355/1439] Regenerate config option docs --- docs/configuration/options.md | 20 ++++++++++++++++++++ isort/main.py | 7 ------- isort/settings.py | 1 - 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/docs/configuration/options.md b/docs/configuration/options.md index e6d0e8058..0b5de2306 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -1010,6 +1010,15 @@ Combines all the bare straight imports of the same section in a single line. Won **Python & Config File Name:** follow_links **CLI Flags:** **Not Supported** +## Indented Import Headings + +**No Description** + +**Type:** Bool +**Default:** `True` +**Python & Config File Name:** indented_import_headings +**CLI Flags:** **Not Supported** + ## Show Version Displays the currently installed version of isort. @@ -1169,6 +1178,17 @@ Provide the filename associated with a stream. - --filename +## Dont Float To Top + +Forces --float-to-top setting off. See --float-to-top for more information. + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** **Not Supported** +**CLI Flags:** + +- --dont-float-to-top + ## Dont Order By Type Don't order imports by type, which is determined by case, in addition to alphabetically. diff --git a/isort/main.py b/isort/main.py index 27866286e..5a7e1b179 100644 --- a/isort/main.py +++ b/isort/main.py @@ -622,13 +622,6 @@ def _build_arg_parser() -> argparse.ArgumentParser: dest="ext_format", help="Tells isort to format the given files according to an extensions formatting rules.", ) - output_group.add_argument( - "--dedupe-imports", - dest="dedupe_imports", - help="Tells isort to dedupe duplicated imports that are seen at the root across " - "import blocks.", - action="store_true", - ) section_group.add_argument( "--sd", diff --git a/isort/settings.py b/isort/settings.py index 4ea8c0d0b..f9c041478 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -204,7 +204,6 @@ class _Config: auto_identify_namespace_packages: bool = True namespace_packages: FrozenSet[str] = frozenset() follow_links: bool = True - dedupe_imports: bool = True indented_import_headings: bool = True def __post_init__(self): From e94a8a94f99fb7022ea3c6eb243c6ce3737fb4a9 Mon Sep 17 00:00:00 2001 From: gofr <32750931+gofr@users.noreply.github.com> Date: Thu, 31 Dec 2020 17:34:30 +0100 Subject: [PATCH 1356/1439] Move fix behind a flag --- isort/main.py | 8 ++++++ isort/output.py | 1 + isort/settings.py | 1 + isort/sorting.py | 9 +++++- tests/unit/test_isort.py | 62 +++++++++++++++++++++++++++++++++++++++- 5 files changed, 79 insertions(+), 2 deletions(-) diff --git a/isort/main.py b/isort/main.py index 27866286e..811bdaf65 100644 --- a/isort/main.py +++ b/isort/main.py @@ -667,6 +667,14 @@ def _build_arg_parser() -> argparse.ArgumentParser: "(like from itertools import groupby). Instead, sort the imports by module, " "independent of import style.", ) + section_group.add_argument( + "--hcss", + "--honor-case-in-force-sorted-sections", + action="store_true", + dest="honor_case_in_force_sorted_sections", + help="Honor `--case-sensitive` when `--force-sort-within-sections` is being used. " + "Without this option set, `--order-by-type` decides module name ordering too.", + ) section_group.add_argument( "--fass", "--force-alphabetical-sort-within-sections", diff --git a/isort/output.py b/isort/output.py index 2e6e6c1eb..54253884b 100644 --- a/isort/output.py +++ b/isort/output.py @@ -97,6 +97,7 @@ def sorted_imports( key=partial( sorting.section_key, case_sensitive=config.case_sensitive, + honor_case_in_force_sorted_sections=config.honor_case_in_force_sorted_sections, order_by_type=config.order_by_type, force_to_top=config.force_to_top, lexicographical=config.lexicographical, diff --git a/isort/settings.py b/isort/settings.py index 4ea8c0d0b..44484e218 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -206,6 +206,7 @@ class _Config: follow_links: bool = True dedupe_imports: bool = True indented_import_headings: bool = True + honor_case_in_force_sorted_sections: bool = False def __post_init__(self): py_version = self.py_version diff --git a/isort/sorting.py b/isort/sorting.py index 60401f8fe..9a4336b8e 100644 --- a/isort/sorting.py +++ b/isort/sorting.py @@ -54,6 +54,7 @@ def module_key( def section_key( line: str, case_sensitive: bool, + honor_case_in_force_sorted_sections: bool, order_by_type: bool, force_to_top: List[str], lexicographical: bool = False, @@ -77,7 +78,11 @@ def section_key( line = re.sub("^import ", "", line) if line.split(" ")[0] in force_to_top: section = "A" - if not case_sensitive or not order_by_type: + # * If honor_case_in_force_sorted_sections is true, and case_sensitive and + # order_by_type are different, only ignore case in part of the line. + # * Otherwise, let order_by_type decide the sorting of the whole line. This + # is only "correct" if case_sensitive and order_by_type have the same value. + if honor_case_in_force_sorted_sections and case_sensitive != order_by_type: split_module = line.split(" import ", 1) if len(split_module) > 1: module_name, names = split_module @@ -88,6 +93,8 @@ def section_key( line = " import ".join([module_name, names]) elif not case_sensitive: line = line.lower() + elif not order_by_type: + line = line.lower() return f"{section}{len(line) if length_sort else ''}{line}" diff --git a/tests/unit/test_isort.py b/tests/unit/test_isort.py index 9d2d410cf..f057cd81d 100644 --- a/tests/unit/test_isort.py +++ b/tests/unit/test_isort.py @@ -2358,10 +2358,10 @@ def test_alphabetic_sorting_no_newlines() -> None: def test_sort_within_section() -> None: """Test to ensure its possible to force isort to sort within sections""" test_input = ( + "from Foob import ar\n" "import foo\n" "from foo import bar\n" "from foo.bar import Quux, baz\n" - "from Foob import ar\n" ) test_output = isort.code(test_input, force_sort_within_sections=True) assert test_output == test_input @@ -2381,6 +2381,64 @@ def test_sort_within_section() -> None: ) assert test_output == test_input + test_input = ( + "import foo\n" + "from foo import bar\n" + "from foo.bar import baz\n" + "from foo.bar import Quux\n" + "from Foob import ar\n" + ) + test_output = isort.code( + code=test_input, + case_sensitive=True, + force_sort_within_sections=True, + order_by_type=False, + force_single_line=True, + ) + assert test_output == test_input + + test_input = ( + "from Foob import ar\n" "import foo\n" "from foo import Quux\n" "from foo import baz\n" + ) + test_output = isort.code( + code=test_input, + case_sensitive=True, + force_sort_within_sections=True, + order_by_type=True, + force_single_line=True, + ) + assert test_output == test_input + + +def test_sort_within_section_case_honored() -> None: + """Ensure isort can do partial case-sensitive sorting in force-sorted sections""" + test_input = ( + "import foo\n" + "from foo import bar\n" + "from foo.bar import Quux, baz\n" + "from Foob import ar\n" + ) + test_output = isort.code( + test_input, force_sort_within_sections=True, honor_case_in_force_sorted_sections=True + ) + assert test_output == test_input + + test_input = ( + "import foo\n" + "from foo import bar\n" + "from foo.bar import baz\n" + "from foo.bar import Quux\n" + "from Foob import ar\n" + ) + test_output = isort.code( + code=test_input, + force_sort_within_sections=True, + honor_case_in_force_sorted_sections=True, + order_by_type=False, + force_single_line=True, + ) + assert test_output == test_input + test_input = ( "from Foob import ar\n" "import foo\n" @@ -2392,6 +2450,7 @@ def test_sort_within_section() -> None: code=test_input, case_sensitive=True, force_sort_within_sections=True, + honor_case_in_force_sorted_sections=True, order_by_type=False, force_single_line=True, ) @@ -2404,6 +2463,7 @@ def test_sort_within_section() -> None: code=test_input, case_sensitive=True, force_sort_within_sections=True, + honor_case_in_force_sorted_sections=True, order_by_type=True, force_single_line=True, ) From 05d8542b0450fecb47d7b5068136e03e72739a89 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 1 Jan 2021 14:24:52 -0800 Subject: [PATCH 1357/1439] Update pytest --- poetry.lock | 726 +++++++++++++++++++++++++++---------------------- pyproject.toml | 2 +- 2 files changed, 407 insertions(+), 321 deletions(-) diff --git a/poetry.lock b/poetry.lock index 21fa4fabc..526dbbe38 100644 --- a/poetry.lock +++ b/poetry.lock @@ -8,15 +8,15 @@ python-versions = "*" [[package]] name = "appnope" -version = "0.1.0" -description = "Disable App Nap on OS X 10.9" +version = "0.1.2" +description = "Disable App Nap on macOS >= 10.9" category = "dev" optional = false python-versions = "*" [[package]] name = "arrow" -version = "0.16.0" +version = "0.17.0" description = "Better dates & times for Python" category = "dev" optional = false @@ -35,16 +35,17 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "attrs" -version = "20.1.0" +version = "20.3.0" description = "Classes Without Boilerplate" category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "sphinx-rtd-theme", "pre-commit"] -docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"] +docs = ["furo", "sphinx", "zope.interface"] tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"] [[package]] name = "backcall" @@ -56,16 +57,16 @@ python-versions = "*" [[package]] name = "bandit" -version = "1.6.2" +version = "1.7.0" description = "Security oriented static analyser for python code." category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.5" [package.dependencies] colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} GitPython = ">=1.0.1" -PyYAML = ">=3.13" +PyYAML = ">=5.3.1" six = ">=1.10.0" stevedore = ">=1.20.0" @@ -105,7 +106,7 @@ d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] [[package]] name = "cached-property" -version = "1.5.1" +version = "1.5.2" description = "A decorator for caching properties in classes." category = "main" optional = false @@ -121,7 +122,7 @@ python-versions = ">=2.7" [[package]] name = "certifi" -version = "2020.6.20" +version = "2020.12.5" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false @@ -145,7 +146,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "colorama" -version = "0.4.3" +version = "0.4.4" description = "Cross-platform colored terminal text." category = "main" optional = false @@ -183,7 +184,7 @@ six = ">=1.10" [[package]] name = "coverage" -version = "5.2.1" +version = "5.3.1" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -194,7 +195,7 @@ toml = ["toml"] [[package]] name = "cruft" -version = "2.3.0" +version = "2.6.0" description = "Allows you to maintain all the necessary cruft for packaging and building projects separate from the code you intentionally write. Built on-top of CookieCutter." category = "dev" optional = false @@ -212,11 +213,11 @@ examples = ["examples (>=1.0.2,<2.0.0)"] [[package]] name = "dataclasses" -version = "0.6" +version = "0.8" description = "A backport of the dataclasses module for Python 3.6" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.6, <3.7" [[package]] name = "decorator" @@ -366,7 +367,7 @@ gitdb = ">=4.0.1" [[package]] name = "gitpython" -version = "3.1.7" +version = "3.1.11" description = "Python Git Library" category = "dev" optional = false @@ -405,8 +406,8 @@ python-versions = "*" [[package]] name = "hstspreload" -version = "2020.8.25" -description = "Chromium HSTS Preload list as a Python package and updated daily" +version = "2020.12.22" +description = "Chromium HSTS Preload list as a Python package" category = "dev" optional = false python-versions = ">=3.6" @@ -463,18 +464,18 @@ python-versions = "*" [[package]] name = "hypothesis" -version = "5.29.3" +version = "5.43.5" description = "A library for property-based testing" category = "dev" optional = false -python-versions = ">=3.5.2" +python-versions = ">=3.6" [package.dependencies] attrs = ">=19.2.0" sortedcontainers = ">=2.1.0,<3.0.0" [package.extras] -all = ["black (>=19.10b0)", "click (>=7.0)", "django (>=2.2)", "dpcontracts (>=0.4)", "lark-parser (>=0.6.5)", "numpy (>=1.9.0)", "pandas (>=0.19)", "pytest (>=4.3)", "python-dateutil (>=1.4)", "pytz (>=2014.1)"] +all = ["black (>=19.10b0)", "click (>=7.0)", "django (>=2.2)", "dpcontracts (>=0.4)", "lark-parser (>=0.6.5)", "numpy (>=1.9.0)", "pandas (>=0.25)", "pytest (>=4.3)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "importlib-resources (>=3.3.0)", "importlib-metadata", "backports.zoneinfo (>=0.2.1)", "tzdata (>=2020.4)"] cli = ["click (>=7.0)", "black (>=19.10b0)"] dateutil = ["python-dateutil (>=1.4)"] django = ["pytz (>=2014.1)", "django (>=2.2)"] @@ -482,9 +483,11 @@ dpcontracts = ["dpcontracts (>=0.4)"] ghostwriter = ["black (>=19.10b0)"] lark = ["lark-parser (>=0.6.5)"] numpy = ["numpy (>=1.9.0)"] -pandas = ["pandas (>=0.19)"] +pandas = ["pandas (>=0.25)"] pytest = ["pytest (>=4.3)"] pytz = ["pytz (>=2014.1)"] +redis = ["redis (>=3.0.0)"] +zoneinfo = ["importlib-resources (>=3.3.0)", "backports.zoneinfo (>=0.2.1)", "tzdata (>=2020.4)"] [[package]] name = "hypothesis-auto" @@ -503,14 +506,14 @@ pytest = ["pytest (>=4.0.0,<5.0.0)"] [[package]] name = "hypothesmith" -version = "0.1.4" +version = "0.1.7" description = "Hypothesis strategies for generating Python programs, something like CSmith" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] -hypothesis = ">=5.23.7" +hypothesis = ">=5.41.0" lark-parser = ">=0.7.2" libcst = ">=0.3.8" @@ -532,18 +535,27 @@ python-versions = ">=3.5" [[package]] name = "importlib-metadata" -version = "1.7.0" +version = "3.3.0" description = "Read metadata from Python packages" category = "main" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = ">=3.6" [package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] -docs = ["sphinx", "rst.linker"] -testing = ["packaging", "pep517", "importlib-resources (>=1.3)"] +docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" [[package]] name = "ipython" @@ -586,18 +598,18 @@ python-versions = "*" [[package]] name = "jedi" -version = "0.17.2" +version = "0.18.0" description = "An autocompletion tool for Python that can be used for text editors." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.6" [package.dependencies] -parso = ">=0.7.0,<0.8.0" +parso = ">=0.8.0,<0.9.0" [package.extras] -qa = ["flake8 (==3.7.9)"] -testing = ["Django (<3.1)", "colorama", "docopt", "pytest (>=3.9.0,<5.0.0)"] +qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] +testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<6.0.0)"] [[package]] name = "jinja2" @@ -627,39 +639,40 @@ jinja2 = "*" [[package]] name = "joblib" -version = "0.16.0" -description = "Lightweight pipelining: using Python functions as pipeline jobs." +version = "1.0.0" +description = "Lightweight pipelining with Python functions" category = "dev" optional = false python-versions = ">=3.6" [[package]] name = "lark-parser" -version = "0.9.0" +version = "0.11.1" description = "a modern parsing library" category = "dev" optional = false python-versions = "*" [package.extras] +nearley = ["js2py"] regex = ["regex"] [[package]] name = "libcst" -version = "0.3.10" +version = "0.3.16" description = "A concrete syntax tree with AST-like properties for Python 3.5, 3.6, 3.7 and 3.8 programs." category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] -dataclasses = {version = "*", markers = "python_version < \"3.7\""} +dataclasses = {version = ">=0.6.0", markers = "python_version < \"3.7\""} pyyaml = ">=5.2" typing-extensions = ">=3.7.4.2" typing-inspect = ">=0.4.0" [package.extras] -dev = ["black", "codecov", "coverage", "hypothesis (>=4.36.0)", "hypothesmith (>=0.0.4)", "isort", "flake8", "jupyter", "nbsphinx", "pyre-check", "sphinx", "sphinx-rtd-theme"] +dev = ["black (==20.8b1)", "codecov (>=2.1.4)", "coverage (>=4.5.4)", "fixit (==0.1.1)", "flake8 (>=3.7.8)", "hypothesis (>=4.36.0)", "hypothesmith (>=0.0.4)", "isort (==5.5.3)", "jupyter (>=1.0.0)", "nbsphinx (>=0.4.2)", "pyre-check (==0.0.41)", "sphinx-rtd-theme (>=0.4.3)", "prompt-toolkit (>=2.0.9)", "tox (>=3.18.1)"] [[package]] name = "livereload" @@ -706,11 +719,11 @@ lingua = ["lingua"] [[package]] name = "markdown" -version = "3.2.2" +version = "3.3.3" description = "Python implementation of Markdown." category = "dev" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" [package.dependencies] importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} @@ -753,7 +766,7 @@ tornado = ">=5.0" [[package]] name = "mkdocs-material" -version = "5.5.9" +version = "5.5.14" description = "A Material Design theme for MkDocs" category = "dev" optional = false @@ -768,7 +781,7 @@ pymdown-extensions = ">=7.0" [[package]] name = "mkdocs-material-extensions" -version = "1.0" +version = "1.0.1" description = "Extension pack for Python Markdown." category = "dev" optional = false @@ -777,14 +790,6 @@ python-versions = ">=3.5" [package.dependencies] mkdocs-material = ">=5.0.0" -[[package]] -name = "more-itertools" -version = "8.4.0" -description = "More routines for operating on iterables, beyond itertools" -category = "dev" -optional = false -python-versions = ">=3.5" - [[package]] name = "mypy" version = "0.761" @@ -833,7 +838,7 @@ twitter = ["twython"] [[package]] name = "numpy" -version = "1.19.1" +version = "1.19.4" description = "NumPy is the fundamental package for array computing with Python." category = "dev" optional = false @@ -852,7 +857,7 @@ six = ">=1.8.0" [[package]] name = "packaging" -version = "20.4" +version = "20.8" description = "Core utilities for Python packages" category = "main" optional = false @@ -860,22 +865,22 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.dependencies] pyparsing = ">=2.0.2" -six = "*" [[package]] name = "parso" -version = "0.7.1" +version = "0.8.1" description = "A Python Parser" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.6" [package.extras] -testing = ["docopt", "pytest (>=3.0.7)"] +qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] +testing = ["docopt", "pytest (<6.0.0)"] [[package]] name = "pathspec" -version = "0.8.0" +version = "0.8.1" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false @@ -883,11 +888,11 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "pbr" -version = "5.4.5" +version = "5.5.1" description = "Python Build Reasonableness" category = "dev" optional = false -python-versions = "*" +python-versions = ">=2.6" [[package]] name = "pdocs" @@ -904,7 +909,7 @@ Markdown = ">=3.0.0,<4.0.0" [[package]] name = "pep517" -version = "0.8.2" +version = "0.9.1" description = "Wrappers to build Python packages using PEP 517 hooks" category = "main" optional = false @@ -1062,7 +1067,7 @@ wcwidth = "*" [[package]] name = "ptyprocess" -version = "0.6.0" +version = "0.7.0" description = "Run a subprocess in a pseudo terminal" category = "dev" optional = false @@ -1070,7 +1075,7 @@ python-versions = "*" [[package]] name = "py" -version = "1.9.0" +version = "1.10.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" category = "dev" optional = false @@ -1086,7 +1091,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pydantic" -version = "1.6.1" +version = "1.7.3" description = "Data validation and settings management using python 3.6 type hinting" category = "dev" optional = false @@ -1102,7 +1107,7 @@ typing_extensions = ["typing-extensions (>=3.7.2)"] [[package]] name = "pydocstyle" -version = "5.1.0" +version = "5.1.1" description = "Python docstring style checker" category = "dev" optional = false @@ -1121,7 +1126,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pygments" -version = "2.6.1" +version = "2.7.3" description = "Pygments is a syntax highlighting package written in Python." category = "dev" optional = false @@ -1162,25 +1167,24 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "pytest" -version = "5.4.3" +version = "6.2.1" description = "pytest: simple powerful testing with Python" category = "dev" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" [package.dependencies] atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} -attrs = ">=17.4.0" +attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} -more-itertools = ">=4.0.0" +iniconfig = "*" packaging = "*" -pluggy = ">=0.12,<1.0" -py = ">=1.5.0" -wcwidth = "*" +pluggy = ">=0.12,<1.0.0a1" +py = ">=1.8.2" +toml = "*" [package.extras] -checkqa-mypy = ["mypy (==v0.761)"] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] [[package]] @@ -1247,7 +1251,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "regex" -version = "2020.7.14" +version = "2020.11.13" description = "Alternative regular expression module, to replace re." category = "dev" optional = false @@ -1255,7 +1259,7 @@ python-versions = "*" [[package]] name = "requests" -version = "2.24.0" +version = "2.25.1" description = "Python HTTP for Humans." category = "main" optional = false @@ -1263,9 +1267,9 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.dependencies] certifi = ">=2017.4.17" -chardet = ">=3.0.2,<4" +chardet = ">=3.0.2,<5" idna = ">=2.5,<3" -urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" +urllib3 = ">=1.21.1,<1.27" [package.extras] security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] @@ -1273,7 +1277,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] [[package]] name = "requirementslib" -version = "1.5.13" +version = "1.5.16" description = "A tool for converting between pip-style and pipfile requirements." category = "main" optional = false @@ -1281,7 +1285,7 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [package.dependencies] appdirs = "*" -attrs = ">=18.2" +attrs = ">=19.2" cached-property = "*" distlib = ">=0.2.8" orderedmultidict = "*" @@ -1313,7 +1317,7 @@ idna2008 = ["idna"] [[package]] name = "safety" -version = "1.9.0" +version = "1.10.0" description = "Checks installed dependencies for known vulnerabilities." category = "dev" optional = false @@ -1354,7 +1358,7 @@ smmap = ">=3.0.1" [[package]] name = "sniffio" -version = "1.1.0" +version = "1.2.0" description = "Sniff out which async library your code is running under" category = "dev" optional = false @@ -1373,7 +1377,7 @@ python-versions = "*" [[package]] name = "sortedcontainers" -version = "2.2.2" +version = "2.3.0" description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" category = "dev" optional = false @@ -1381,7 +1385,7 @@ python-versions = "*" [[package]] name = "stevedore" -version = "3.2.0" +version = "3.3.0" description = "Manage dynamic plugins for Python applications" category = "dev" optional = false @@ -1401,11 +1405,11 @@ python-versions = "*" [[package]] name = "toml" -version = "0.10.1" +version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" category = "main" optional = false -python-versions = "*" +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "tomlkit" @@ -1417,7 +1421,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "tornado" -version = "6.0.4" +version = "6.1" description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." category = "dev" optional = false @@ -1425,14 +1429,15 @@ python-versions = ">= 3.5" [[package]] name = "tqdm" -version = "4.48.2" +version = "4.55.0" description = "Fast, Extensible Progress Meter" category = "dev" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" [package.extras] -dev = ["py-make (>=0.1.0)", "twine", "argopt", "pydoc-markdown"] +dev = ["py-make (>=0.1.0)", "twine", "wheel"] +telegram = ["requests"] [[package]] name = "traitlets" @@ -1452,7 +1457,7 @@ test = ["pytest", "mock"] [[package]] name = "typed-ast" -version = "1.4.1" +version = "1.4.2" description = "a fork of Python 2 and 3 ast modules with type comment support" category = "dev" optional = false @@ -1479,7 +1484,7 @@ doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=5.4.0,<6.0.0)", "markdown- name = "typing-extensions" version = "3.7.4.3" description = "Backported and Experimental Type Hints for Python 3.5+" -category = "dev" +category = "main" optional = false python-versions = "*" @@ -1497,7 +1502,7 @@ typing-extensions = ">=3.7.4" [[package]] name = "urllib3" -version = "1.25.10" +version = "1.26.2" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false @@ -1505,7 +1510,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" [package.extras] brotli = ["brotlipy (>=0.6.0)"] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)", "ipaddress"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] @@ -1564,7 +1569,7 @@ python-versions = "*" [[package]] name = "zipp" -version = "3.1.0" +version = "3.4.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false @@ -1572,7 +1577,7 @@ python-versions = ">=3.6" [package.extras] docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] -testing = ["jaraco.itertools", "func-timeout"] +testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] [extras] colors = ["colorama"] @@ -1582,7 +1587,7 @@ requirements_deprecated_finder = ["pipreqs", "pip-api"] [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "363f453324e3010e63a4f5281f2fadbf74d958296d7b61d22758d771b137f546" +content-hash = "8d92b325ce222b42dfd7e2a8e6e5abd4f213cba6903e6b36edd322b8d9878fdf" [metadata.files] appdirs = [ @@ -1590,28 +1595,28 @@ appdirs = [ {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, ] appnope = [ - {file = "appnope-0.1.0-py2.py3-none-any.whl", hash = "sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0"}, - {file = "appnope-0.1.0.tar.gz", hash = "sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71"}, + {file = "appnope-0.1.2-py2.py3-none-any.whl", hash = "sha256:93aa393e9d6c54c5cd570ccadd8edad61ea0c4b9ea7a01409020c9aa019eb442"}, + {file = "appnope-0.1.2.tar.gz", hash = "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a"}, ] arrow = [ - {file = "arrow-0.16.0-py2.py3-none-any.whl", hash = "sha256:98184d8dd3e5d30b96c2df4596526f7de679ccb467f358b82b0f686436f3a6b8"}, - {file = "arrow-0.16.0.tar.gz", hash = "sha256:92aac856ea5175c804f7ccb96aca4d714d936f1c867ba59d747a8096ec30e90a"}, + {file = "arrow-0.17.0-py2.py3-none-any.whl", hash = "sha256:e098abbd9af3665aea81bdd6c869e93af4feb078e98468dd351c383af187aac5"}, + {file = "arrow-0.17.0.tar.gz", hash = "sha256:ff08d10cda1d36c68657d6ad20d74fbea493d980f8b2d45344e00d6ed2bf6ed4"}, ] atomicwrites = [ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, ] attrs = [ - {file = "attrs-20.1.0-py2.py3-none-any.whl", hash = "sha256:2867b7b9f8326499ab5b0e2d12801fa5c98842d2cbd22b35112ae04bf85b4dff"}, - {file = "attrs-20.1.0.tar.gz", hash = "sha256:0ef97238856430dcf9228e07f316aefc17e8939fc8507e18c6501b761ef1a42a"}, + {file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"}, + {file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"}, ] backcall = [ {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, ] bandit = [ - {file = "bandit-1.6.2-py2.py3-none-any.whl", hash = "sha256:336620e220cf2d3115877685e264477ff9d9abaeb0afe3dc7264f55fa17a3952"}, - {file = "bandit-1.6.2.tar.gz", hash = "sha256:41e75315853507aa145d62a78a2a6c5e3240fe14ee7c601459d0df9418196065"}, + {file = "bandit-1.7.0-py3-none-any.whl", hash = "sha256:216be4d044209fa06cf2a3e51b319769a51be8318140659719aa7a115c35ed07"}, + {file = "bandit-1.7.0.tar.gz", hash = "sha256:8a4c7415254d75df8ff3c3b15cfe9042ecee628a1e40b44c15a98890fbfc2608"}, ] binaryornot = [ {file = "binaryornot-0.4.4-py2.py3-none-any.whl", hash = "sha256:b8b71173c917bddcd2c16070412e369c3ed7f0528926f70cac18a6c97fd563e4"}, @@ -1621,15 +1626,15 @@ black = [ {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, ] cached-property = [ - {file = "cached-property-1.5.1.tar.gz", hash = "sha256:9217a59f14a5682da7c4b8829deadbfc194ac22e9908ccf7c8820234e80a1504"}, - {file = "cached_property-1.5.1-py2.py3-none-any.whl", hash = "sha256:3a026f1a54135677e7da5ce819b0c690f156f37976f3e30c5430740725203d7f"}, + {file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"}, + {file = "cached_property-1.5.2-py2.py3-none-any.whl", hash = "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"}, ] cerberus = [ {file = "Cerberus-1.3.2.tar.gz", hash = "sha256:302e6694f206dd85cb63f13fd5025b31ab6d38c99c50c6d769f8fa0b0f299589"}, ] certifi = [ - {file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"}, - {file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"}, + {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, + {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"}, ] chardet = [ {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, @@ -1640,8 +1645,8 @@ click = [ {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, ] colorama = [ - {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, - {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] contextvars = [ {file = "contextvars-2.4.tar.gz", hash = "sha256:f38c908aaa59c14335eeea12abea5f443646216c4e29380d7bf34d2018e2c39e"}, @@ -1651,48 +1656,63 @@ cookiecutter = [ {file = "cookiecutter-1.7.2.tar.gz", hash = "sha256:efb6b2d4780feda8908a873e38f0e61778c23f6a2ea58215723bcceb5b515dac"}, ] coverage = [ - {file = "coverage-5.2.1-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:40f70f81be4d34f8d491e55936904db5c527b0711b2a46513641a5729783c2e4"}, - {file = "coverage-5.2.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:675192fca634f0df69af3493a48224f211f8db4e84452b08d5fcebb9167adb01"}, - {file = "coverage-5.2.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:2fcc8b58953d74d199a1a4d633df8146f0ac36c4e720b4a1997e9b6327af43a8"}, - {file = "coverage-5.2.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:64c4f340338c68c463f1b56e3f2f0423f7b17ba6c3febae80b81f0e093077f59"}, - {file = "coverage-5.2.1-cp27-cp27m-win32.whl", hash = "sha256:52f185ffd3291196dc1aae506b42e178a592b0b60a8610b108e6ad892cfc1bb3"}, - {file = "coverage-5.2.1-cp27-cp27m-win_amd64.whl", hash = "sha256:30bc103587e0d3df9e52cd9da1dd915265a22fad0b72afe54daf840c984b564f"}, - {file = "coverage-5.2.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:9ea749fd447ce7fb1ac71f7616371f04054d969d412d37611716721931e36efd"}, - {file = "coverage-5.2.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ce7866f29d3025b5b34c2e944e66ebef0d92e4a4f2463f7266daa03a1332a651"}, - {file = "coverage-5.2.1-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:4869ab1c1ed33953bb2433ce7b894a28d724b7aa76c19b11e2878034a4e4680b"}, - {file = "coverage-5.2.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:a3ee9c793ffefe2944d3a2bd928a0e436cd0ac2d9e3723152d6fd5398838ce7d"}, - {file = "coverage-5.2.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:28f42dc5172ebdc32622a2c3f7ead1b836cdbf253569ae5673f499e35db0bac3"}, - {file = "coverage-5.2.1-cp35-cp35m-win32.whl", hash = "sha256:e26c993bd4b220429d4ec8c1468eca445a4064a61c74ca08da7429af9bc53bb0"}, - {file = "coverage-5.2.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4186fc95c9febeab5681bc3248553d5ec8c2999b8424d4fc3a39c9cba5796962"}, - {file = "coverage-5.2.1-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:b360d8fd88d2bad01cb953d81fd2edd4be539df7bfec41e8753fe9f4456a5082"}, - {file = "coverage-5.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:1adb6be0dcef0cf9434619d3b892772fdb48e793300f9d762e480e043bd8e716"}, - {file = "coverage-5.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:098a703d913be6fbd146a8c50cc76513d726b022d170e5e98dc56d958fd592fb"}, - {file = "coverage-5.2.1-cp36-cp36m-win32.whl", hash = "sha256:962c44070c281d86398aeb8f64e1bf37816a4dfc6f4c0f114756b14fc575621d"}, - {file = "coverage-5.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1ed2bdb27b4c9fc87058a1cb751c4df8752002143ed393899edb82b131e0546"}, - {file = "coverage-5.2.1-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:c890728a93fffd0407d7d37c1e6083ff3f9f211c83b4316fae3778417eab9811"}, - {file = "coverage-5.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:538f2fd5eb64366f37c97fdb3077d665fa946d2b6d95447622292f38407f9258"}, - {file = "coverage-5.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:27ca5a2bc04d68f0776f2cdcb8bbd508bbe430a7bf9c02315cd05fb1d86d0034"}, - {file = "coverage-5.2.1-cp37-cp37m-win32.whl", hash = "sha256:aab75d99f3f2874733946a7648ce87a50019eb90baef931698f96b76b6769a46"}, - {file = "coverage-5.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:c2ff24df02a125b7b346c4c9078c8936da06964cc2d276292c357d64378158f8"}, - {file = "coverage-5.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:304fbe451698373dc6653772c72c5d5e883a4aadaf20343592a7abb2e643dae0"}, - {file = "coverage-5.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c96472b8ca5dc135fb0aa62f79b033f02aa434fb03a8b190600a5ae4102df1fd"}, - {file = "coverage-5.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8505e614c983834239f865da2dd336dcf9d72776b951d5dfa5ac36b987726e1b"}, - {file = "coverage-5.2.1-cp38-cp38-win32.whl", hash = "sha256:700997b77cfab016533b3e7dbc03b71d33ee4df1d79f2463a318ca0263fc29dd"}, - {file = "coverage-5.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:46794c815e56f1431c66d81943fa90721bb858375fb36e5903697d5eef88627d"}, - {file = "coverage-5.2.1-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:16042dc7f8e632e0dcd5206a5095ebd18cb1d005f4c89694f7f8aafd96dd43a3"}, - {file = "coverage-5.2.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:c1bbb628ed5192124889b51204de27c575b3ffc05a5a91307e7640eff1d48da4"}, - {file = "coverage-5.2.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4f6428b55d2916a69f8d6453e48a505c07b2245653b0aa9f0dee38785939f5e4"}, - {file = "coverage-5.2.1-cp39-cp39-win32.whl", hash = "sha256:9e536783a5acee79a9b308be97d3952b662748c4037b6a24cbb339dc7ed8eb89"}, - {file = "coverage-5.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:b8f58c7db64d8f27078cbf2a4391af6aa4e4767cc08b37555c4ae064b8558d9b"}, - {file = "coverage-5.2.1.tar.gz", hash = "sha256:a34cb28e0747ea15e82d13e14de606747e9e484fb28d63c999483f5d5188e89b"}, + {file = "coverage-5.3.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:fabeeb121735d47d8eab8671b6b031ce08514c86b7ad8f7d5490a7b6dcd6267d"}, + {file = "coverage-5.3.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:7e4d159021c2029b958b2363abec4a11db0ce8cd43abb0d9ce44284cb97217e7"}, + {file = "coverage-5.3.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:378ac77af41350a8c6b8801a66021b52da8a05fd77e578b7380e876c0ce4f528"}, + {file = "coverage-5.3.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:e448f56cfeae7b1b3b5bcd99bb377cde7c4eb1970a525c770720a352bc4c8044"}, + {file = "coverage-5.3.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:cc44e3545d908ecf3e5773266c487ad1877be718d9dc65fc7eb6e7d14960985b"}, + {file = "coverage-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:08b3ba72bd981531fd557f67beee376d6700fba183b167857038997ba30dd297"}, + {file = "coverage-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:8dacc4073c359f40fcf73aede8428c35f84639baad7e1b46fce5ab7a8a7be4bb"}, + {file = "coverage-5.3.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ee2f1d1c223c3d2c24e3afbb2dd38be3f03b1a8d6a83ee3d9eb8c36a52bee899"}, + {file = "coverage-5.3.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:9a9d4ff06804920388aab69c5ea8a77525cf165356db70131616acd269e19b36"}, + {file = "coverage-5.3.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:782a5c7df9f91979a7a21792e09b34a658058896628217ae6362088b123c8500"}, + {file = "coverage-5.3.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:fda29412a66099af6d6de0baa6bd7c52674de177ec2ad2630ca264142d69c6c7"}, + {file = "coverage-5.3.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:f2c6888eada180814b8583c3e793f3f343a692fc802546eed45f40a001b1169f"}, + {file = "coverage-5.3.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:8f33d1156241c43755137288dea619105477961cfa7e47f48dbf96bc2c30720b"}, + {file = "coverage-5.3.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b239711e774c8eb910e9b1ac719f02f5ae4bf35fa0420f438cdc3a7e4e7dd6ec"}, + {file = "coverage-5.3.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:f54de00baf200b4539a5a092a759f000b5f45fd226d6d25a76b0dff71177a714"}, + {file = "coverage-5.3.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:be0416074d7f253865bb67630cf7210cbc14eb05f4099cc0f82430135aaa7a3b"}, + {file = "coverage-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:c46643970dff9f5c976c6512fd35768c4a3819f01f61169d8cdac3f9290903b7"}, + {file = "coverage-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9a4f66259bdd6964d8cf26142733c81fb562252db74ea367d9beb4f815478e72"}, + {file = "coverage-5.3.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c6e5174f8ca585755988bc278c8bb5d02d9dc2e971591ef4a1baabdf2d99589b"}, + {file = "coverage-5.3.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:3911c2ef96e5ddc748a3c8b4702c61986628bb719b8378bf1e4a6184bbd48fe4"}, + {file = "coverage-5.3.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:c5ec71fd4a43b6d84ddb88c1df94572479d9a26ef3f150cef3dacefecf888105"}, + {file = "coverage-5.3.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f51dbba78d68a44e99d484ca8c8f604f17e957c1ca09c3ebc2c7e3bbd9ba0448"}, + {file = "coverage-5.3.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:a2070c5affdb3a5e751f24208c5c4f3d5f008fa04d28731416e023c93b275277"}, + {file = "coverage-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:535dc1e6e68fad5355f9984d5637c33badbdc987b0c0d303ee95a6c979c9516f"}, + {file = "coverage-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:a4857f7e2bc6921dbd487c5c88b84f5633de3e7d416c4dc0bb70256775551a6c"}, + {file = "coverage-5.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fac3c432851038b3e6afe086f777732bcf7f6ebbfd90951fa04ee53db6d0bcdd"}, + {file = "coverage-5.3.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:cd556c79ad665faeae28020a0ab3bda6cd47d94bec48e36970719b0b86e4dcf4"}, + {file = "coverage-5.3.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:a66ca3bdf21c653e47f726ca57f46ba7fc1f260ad99ba783acc3e58e3ebdb9ff"}, + {file = "coverage-5.3.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:ab110c48bc3d97b4d19af41865e14531f300b482da21783fdaacd159251890e8"}, + {file = "coverage-5.3.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:e52d3d95df81c8f6b2a1685aabffadf2d2d9ad97203a40f8d61e51b70f191e4e"}, + {file = "coverage-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:fa10fee7e32213f5c7b0d6428ea92e3a3fdd6d725590238a3f92c0de1c78b9d2"}, + {file = "coverage-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ce6f3a147b4b1a8b09aae48517ae91139b1b010c5f36423fa2b866a8b23df879"}, + {file = "coverage-5.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:93a280c9eb736a0dcca19296f3c30c720cb41a71b1f9e617f341f0a8e791a69b"}, + {file = "coverage-5.3.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:3102bb2c206700a7d28181dbe04d66b30780cde1d1c02c5f3c165cf3d2489497"}, + {file = "coverage-5.3.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8ffd4b204d7de77b5dd558cdff986a8274796a1e57813ed005b33fd97e29f059"}, + {file = "coverage-5.3.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:a607ae05b6c96057ba86c811d9c43423f35e03874ffb03fbdcd45e0637e8b631"}, + {file = "coverage-5.3.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:3a3c3f8863255f3c31db3889f8055989527173ef6192a283eb6f4db3c579d830"}, + {file = "coverage-5.3.1-cp38-cp38-win32.whl", hash = "sha256:ff1330e8bc996570221b450e2d539134baa9465f5cb98aff0e0f73f34172e0ae"}, + {file = "coverage-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:3498b27d8236057def41de3585f317abae235dd3a11d33e01736ffedb2ef8606"}, + {file = "coverage-5.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ceb499d2b3d1d7b7ba23abe8bf26df5f06ba8c71127f188333dddcf356b4b63f"}, + {file = "coverage-5.3.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:3b14b1da110ea50c8bcbadc3b82c3933974dbeea1832e814aab93ca1163cd4c1"}, + {file = "coverage-5.3.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:76b2775dda7e78680d688daabcb485dc87cf5e3184a0b3e012e1d40e38527cc8"}, + {file = "coverage-5.3.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:cef06fb382557f66d81d804230c11ab292d94b840b3cb7bf4450778377b592f4"}, + {file = "coverage-5.3.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:6f61319e33222591f885c598e3e24f6a4be3533c1d70c19e0dc59e83a71ce27d"}, + {file = "coverage-5.3.1-cp39-cp39-win32.whl", hash = "sha256:cc6f8246e74dd210d7e2b56c76ceaba1cc52b025cd75dbe96eb48791e0250e98"}, + {file = "coverage-5.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:2757fa64e11ec12220968f65d086b7a29b6583d16e9a544c889b22ba98555ef1"}, + {file = "coverage-5.3.1-pp36-none-any.whl", hash = "sha256:723d22d324e7997a651478e9c5a3120a0ecbc9a7e94071f7e1954562a8806cf3"}, + {file = "coverage-5.3.1-pp37-none-any.whl", hash = "sha256:c89b558f8a9a5a6f2cfc923c304d49f0ce629c3bd85cb442ca258ec20366394c"}, + {file = "coverage-5.3.1.tar.gz", hash = "sha256:38f16b1317b8dd82df67ed5daa5f5e7c959e46579840d77a67a4ceb9cef0a50b"}, ] cruft = [ - {file = "cruft-2.3.0-py3-none-any.whl", hash = "sha256:ca973c1ca9e4add9893483dbce02cd8930e105f8940afe0d087a14b70c6068de"}, - {file = "cruft-2.3.0.tar.gz", hash = "sha256:7c0f7682765e76fcf31adf877ea6f74372a0ab9554d8f8d6766e8e0413730e52"}, + {file = "cruft-2.6.0-py3-none-any.whl", hash = "sha256:80e32a2fd8103f3c57c96af1c25d96d748072727a8563c58a65eece56a02b441"}, + {file = "cruft-2.6.0.tar.gz", hash = "sha256:bbeea9fb69812afd74a8140cca5ae7fdab761d4df50b137f2437fab9e72c4580"}, ] dataclasses = [ - {file = "dataclasses-0.6-py3-none-any.whl", hash = "sha256:454a69d788c7fda44efd71e259be79577822f5e3f53f029a22d08004e951dc9f"}, - {file = "dataclasses-0.6.tar.gz", hash = "sha256:6988bd2b895eef432d562370bb707d540f32f7360ab13da45340101bc2307d84"}, + {file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"}, + {file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"}, ] decorator = [ {file = "decorator-4.4.2-py2.py3-none-any.whl", hash = "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760"}, @@ -1761,8 +1781,8 @@ gitdb2 = [ {file = "gitdb2-4.0.2.tar.gz", hash = "sha256:0986cb4003de743f2b3aba4c828edd1ab58ce98e1c4a8acf72ef02760d4beb4e"}, ] gitpython = [ - {file = "GitPython-3.1.7-py3-none-any.whl", hash = "sha256:fa3b92da728a457dd75d62bb5f3eb2816d99a7fe6c67398e260637a40e3fafb5"}, - {file = "GitPython-3.1.7.tar.gz", hash = "sha256:2db287d71a284e22e5c2846042d0602465c7434d910406990d5b74df4afb0858"}, + {file = "GitPython-3.1.11-py3-none-any.whl", hash = "sha256:6eea89b655917b500437e9668e4a12eabdcf00229a0df1762aabd692ef9b746b"}, + {file = "GitPython-3.1.11.tar.gz", hash = "sha256:befa4d101f91bad1b632df4308ec64555db684c360bd7d2130b4807d49ce86b8"}, ] h11 = [ {file = "h11-0.9.0-py2.py3-none-any.whl", hash = "sha256:4bc6d6a1238b7615b266ada57e0618568066f57dd6fa967d1290ec9309b2f2f1"}, @@ -1777,8 +1797,8 @@ hpack = [ {file = "hpack-3.0.0.tar.gz", hash = "sha256:8eec9c1f4bfae3408a3f30500261f7e6a65912dc138526ea054f9ad98892e9d2"}, ] hstspreload = [ - {file = "hstspreload-2020.8.25-py3-none-any.whl", hash = "sha256:c96401eca4669340b423abd711d2d5d03ddf0685461f95e9cfe500d5e9acf3d2"}, - {file = "hstspreload-2020.8.25.tar.gz", hash = "sha256:3129613419c13ea62411ec7375d79840e28004cbb6a585909ddcbeee401bea14"}, + {file = "hstspreload-2020.12.22-py3-none-any.whl", hash = "sha256:f09afa7015257d33ead35137ddfee3604040b7f9eefbe995dacb4d04744f4034"}, + {file = "hstspreload-2020.12.22.tar.gz", hash = "sha256:8bd8cdf180627e6289805efad5399a55bedf2e707fdcbb243b74298b95db48c6"}, ] httpcore = [ {file = "httpcore-0.9.1-py3-none-any.whl", hash = "sha256:9850fe97a166a794d7e920590d5ec49a05488884c9fc8b5dba8561effab0c2a0"}, @@ -1797,16 +1817,16 @@ hyperframe = [ {file = "hyperframe-5.2.0.tar.gz", hash = "sha256:a9f5c17f2cc3c719b917c4f33ed1c61bd1f8dfac4b1bd23b7c80b3400971b41f"}, ] hypothesis = [ - {file = "hypothesis-5.29.3-py3-none-any.whl", hash = "sha256:07b865184494a64cf2e18090ecfb876c97d303973c2f97139a07be361b0c3a28"}, - {file = "hypothesis-5.29.3.tar.gz", hash = "sha256:e6cf92a94a5108d326e45df5a2b256dc0d57f9663d13efdebcadcfbad9accc31"}, + {file = "hypothesis-5.43.5-py3-none-any.whl", hash = "sha256:546db914a7a7be1ccacbd408cf4cec4fa958b96b4015a2216f8187e4f0ec7eaa"}, + {file = "hypothesis-5.43.5.tar.gz", hash = "sha256:9377cd796a5bca3c0ae74ef1c592aa231d3a04cde948467bace9344148ee75cb"}, ] hypothesis-auto = [ {file = "hypothesis-auto-1.1.4.tar.gz", hash = "sha256:5e2c2fb09dc09842512d80630bb792359a1d33d2c0473ad47ee23da0be9e32b1"}, {file = "hypothesis_auto-1.1.4-py3-none-any.whl", hash = "sha256:fea8560c4522c0fd490ed8cc17e420b95dabebb11b9b334c59bf2d768839015f"}, ] hypothesmith = [ - {file = "hypothesmith-0.1.4-py3-none-any.whl", hash = "sha256:bc45f45808078d2bbe6c3806af3b3604bde35624964fcc6b849cecadf254d3a9"}, - {file = "hypothesmith-0.1.4.tar.gz", hash = "sha256:5628fb1a06233c70751105635bc3cee789c82358041b4518c2cab5300e73cd65"}, + {file = "hypothesmith-0.1.7-py3-none-any.whl", hash = "sha256:f37d0b55fec60d31c4fff271e6ae38100fce4c622c584aae0037f08163fea93f"}, + {file = "hypothesmith-0.1.7.tar.gz", hash = "sha256:97802ad8136033e65db748bbb5a7c9193b9e4df85028f074484db993e8548019"}, ] idna = [ {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, @@ -1827,8 +1847,12 @@ immutables = [ {file = "immutables-0.14.tar.gz", hash = "sha256:a0a1cc238b678455145bae291d8426f732f5255537ed6a5b7645949704c70a78"}, ] importlib-metadata = [ - {file = "importlib_metadata-1.7.0-py2.py3-none-any.whl", hash = "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"}, - {file = "importlib_metadata-1.7.0.tar.gz", hash = "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83"}, + {file = "importlib_metadata-3.3.0-py3-none-any.whl", hash = "sha256:bf792d480abbd5eda85794e4afb09dd538393f7d6e6ffef6e9f03d2014cf9450"}, + {file = "importlib_metadata-3.3.0.tar.gz", hash = "sha256:5c5a2720817414a6c41f0a49993908068243ae02c1635a228126519b509c8aed"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] ipython = [ {file = "ipython-7.16.1-py3-none-any.whl", hash = "sha256:2dbcc8c27ca7d3cfe4fcdff7f45b27f9a8d3edfa70ff8024a71c7a8eb5f09d64"}, @@ -1839,8 +1863,8 @@ ipython-genutils = [ {file = "ipython_genutils-0.2.0.tar.gz", hash = "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"}, ] jedi = [ - {file = "jedi-0.17.2-py2.py3-none-any.whl", hash = "sha256:98cc583fa0f2f8304968199b01b6b4b94f469a1f4a74c1560506ca2a211378b5"}, - {file = "jedi-0.17.2.tar.gz", hash = "sha256:86ed7d9b750603e4ba582ea8edc678657fb4007894a12bcf6f4bb97892f31d20"}, + {file = "jedi-0.18.0-py2.py3-none-any.whl", hash = "sha256:18456d83f65f400ab0c2d3319e48520420ef43b23a086fdc05dff34132f0fb93"}, + {file = "jedi-0.18.0.tar.gz", hash = "sha256:92550a404bad8afed881a137ec9a461fed49eca661414be45059329614ed0707"}, ] jinja2 = [ {file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"}, @@ -1851,15 +1875,16 @@ jinja2-time = [ {file = "jinja2_time-0.2.0-py2.py3-none-any.whl", hash = "sha256:d3eab6605e3ec8b7a0863df09cc1d23714908fa61aa6986a845c20ba488b4efa"}, ] joblib = [ - {file = "joblib-0.16.0-py3-none-any.whl", hash = "sha256:d348c5d4ae31496b2aa060d6d9b787864dd204f9480baaa52d18850cb43e9f49"}, - {file = "joblib-0.16.0.tar.gz", hash = "sha256:8f52bf24c64b608bf0b2563e0e47d6fcf516abc8cfafe10cfd98ad66d94f92d6"}, + {file = "joblib-1.0.0-py3-none-any.whl", hash = "sha256:75ead23f13484a2a414874779d69ade40d4fa1abe62b222a23cd50d4bc822f6f"}, + {file = "joblib-1.0.0.tar.gz", hash = "sha256:7ad866067ac1fdec27d51c8678ea760601b70e32ff1881d4dc8e1171f2b64b24"}, ] lark-parser = [ - {file = "lark-parser-0.9.0.tar.gz", hash = "sha256:9e7589365d6b6de1cca40b0eaec31104a3fb96a37a11a9dfd5098e95b50aa6cd"}, + {file = "lark-parser-0.11.1.tar.gz", hash = "sha256:20bdefdf1b6e9bcb38165ea5cc4f27921a99c6f4c35264a3a953fd60335f1f8c"}, + {file = "lark_parser-0.11.1-py2.py3-none-any.whl", hash = "sha256:8b747e1f544dcc2789e3feaddd2a50c6a73bed69d62e9c69760c1e1f7d23495f"}, ] libcst = [ - {file = "libcst-0.3.10-py3-none-any.whl", hash = "sha256:e9395d952a490e6fc160f2bea8df139bdf1fdcb3fe4c01b88893da279eff00de"}, - {file = "libcst-0.3.10.tar.gz", hash = "sha256:b0dccbfc1cff7bfa3214980e1d2d90b4e00b2fed002d4b276a8a411217738df3"}, + {file = "libcst-0.3.16-py3-none-any.whl", hash = "sha256:2c9e40245b8cb49b5219c76b36fe7037effa7594b9e6d5a092be99f8083d2415"}, + {file = "libcst-0.3.16.tar.gz", hash = "sha256:99c200004b6e845642eea7a433844d144994767f9ed50705171720b76d28cf7e"}, ] livereload = [ {file = "livereload-2.6.3.tar.gz", hash = "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869"}, @@ -1873,8 +1898,8 @@ mako = [ {file = "Mako-1.1.3.tar.gz", hash = "sha256:8195c8c1400ceb53496064314c6736719c6f25e7479cd24c77be3d9361cddc27"}, ] markdown = [ - {file = "Markdown-3.2.2-py3-none-any.whl", hash = "sha256:c467cd6233885534bf0fe96e62e3cf46cfc1605112356c4f9981512b8174de59"}, - {file = "Markdown-3.2.2.tar.gz", hash = "sha256:1fafe3f1ecabfb514a5285fca634a53c1b32a81cb0feb154264d55bf2ff22c17"}, + {file = "Markdown-3.3.3-py3-none-any.whl", hash = "sha256:c109c15b7dc20a9ac454c9e6025927d44460b85bd039da028d85e2b6d0bcc328"}, + {file = "Markdown-3.3.3.tar.gz", hash = "sha256:5d9f2b5ca24bc4c7a390d22323ca4bad200368612b5aaa7796babf971d2b2f18"}, ] markupsafe = [ {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, @@ -1920,16 +1945,12 @@ mkdocs = [ {file = "mkdocs-1.1.2.tar.gz", hash = "sha256:f0b61e5402b99d7789efa032c7a74c90a20220a9c81749da06dbfbcbd52ffb39"}, ] mkdocs-material = [ - {file = "mkdocs-material-5.5.9.tar.gz", hash = "sha256:37d60947993b939318945c170c7b3a153646976badf57648fd70befc3b54c830"}, - {file = "mkdocs_material-5.5.9-py2.py3-none-any.whl", hash = "sha256:c8cb3c8c44bf10ed7ac1eb568d93a4346efe03fee2994b6a80e96559421cec49"}, + {file = "mkdocs-material-5.5.14.tar.gz", hash = "sha256:9f3237df1a72f91e0330a5e3b3711cb7aaa0d5705f9585e6ce6fbacaa16e777f"}, + {file = "mkdocs_material-5.5.14-py2.py3-none-any.whl", hash = "sha256:a0b3b3e67606e04d13e777d13f3195402ea09e0c3ce279abc3666cac2c5b3a6d"}, ] mkdocs-material-extensions = [ - {file = "mkdocs-material-extensions-1.0.tar.gz", hash = "sha256:17d7491e189af75700310b7ec33c6c48a22060b8b445001deca040cb60471cde"}, - {file = "mkdocs_material_extensions-1.0-py3-none-any.whl", hash = "sha256:09569c3694b5acc1e8334c9730e52b4bcde65fc9d613cc20e49af131ef1a9ca0"}, -] -more-itertools = [ - {file = "more-itertools-8.4.0.tar.gz", hash = "sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5"}, - {file = "more_itertools-8.4.0-py3-none-any.whl", hash = "sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2"}, + {file = "mkdocs-material-extensions-1.0.1.tar.gz", hash = "sha256:6947fb7f5e4291e3c61405bad3539d81e0b3cd62ae0d66ced018128af509c68f"}, + {file = "mkdocs_material_extensions-1.0.1-py3-none-any.whl", hash = "sha256:d90c807a88348aa6d1805657ec5c0b2d8d609c110e62b9dce4daf7fa981fa338"}, ] mypy = [ {file = "mypy-0.761-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:7f672d02fffcbace4db2b05369142e0506cdcde20cea0e07c7c2171c4fd11dd6"}, @@ -1955,60 +1976,68 @@ nltk = [ {file = "nltk-3.5.zip", hash = "sha256:845365449cd8c5f9731f7cb9f8bd6fd0767553b9d53af9eb1b3abf7700936b35"}, ] numpy = [ - {file = "numpy-1.19.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b1cca51512299841bf69add3b75361779962f9cee7d9ee3bb446d5982e925b69"}, - {file = "numpy-1.19.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c9591886fc9cbe5532d5df85cb8e0cc3b44ba8ce4367bd4cf1b93dc19713da72"}, - {file = "numpy-1.19.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:cf1347450c0b7644ea142712619533553f02ef23f92f781312f6a3553d031fc7"}, - {file = "numpy-1.19.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:ed8a311493cf5480a2ebc597d1e177231984c818a86875126cfd004241a73c3e"}, - {file = "numpy-1.19.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:3673c8b2b29077f1b7b3a848794f8e11f401ba0b71c49fbd26fb40b71788b132"}, - {file = "numpy-1.19.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:56ef7f56470c24bb67fb43dae442e946a6ce172f97c69f8d067ff8550cf782ff"}, - {file = "numpy-1.19.1-cp36-cp36m-win32.whl", hash = "sha256:aaf42a04b472d12515debc621c31cf16c215e332242e7a9f56403d814c744624"}, - {file = "numpy-1.19.1-cp36-cp36m-win_amd64.whl", hash = "sha256:082f8d4dd69b6b688f64f509b91d482362124986d98dc7dc5f5e9f9b9c3bb983"}, - {file = "numpy-1.19.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e4f6d3c53911a9d103d8ec9518190e52a8b945bab021745af4939cfc7c0d4a9e"}, - {file = "numpy-1.19.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:5b6885c12784a27e957294b60f97e8b5b4174c7504665333c5e94fbf41ae5d6a"}, - {file = "numpy-1.19.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:1bc0145999e8cb8aed9d4e65dd8b139adf1919e521177f198529687dbf613065"}, - {file = "numpy-1.19.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:5a936fd51049541d86ccdeef2833cc89a18e4d3808fe58a8abeb802665c5af93"}, - {file = "numpy-1.19.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:ef71a1d4fd4858596ae80ad1ec76404ad29701f8ca7cdcebc50300178db14dfc"}, - {file = "numpy-1.19.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b9792b0ac0130b277536ab8944e7b754c69560dac0415dd4b2dbd16b902c8954"}, - {file = "numpy-1.19.1-cp37-cp37m-win32.whl", hash = "sha256:b12e639378c741add21fbffd16ba5ad25c0a1a17cf2b6fe4288feeb65144f35b"}, - {file = "numpy-1.19.1-cp37-cp37m-win_amd64.whl", hash = "sha256:8343bf67c72e09cfabfab55ad4a43ce3f6bf6e6ced7acf70f45ded9ebb425055"}, - {file = "numpy-1.19.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e45f8e981a0ab47103181773cc0a54e650b2aef8c7b6cd07405d0fa8d869444a"}, - {file = "numpy-1.19.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:667c07063940e934287993366ad5f56766bc009017b4a0fe91dbd07960d0aba7"}, - {file = "numpy-1.19.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:480fdd4dbda4dd6b638d3863da3be82873bba6d32d1fc12ea1b8486ac7b8d129"}, - {file = "numpy-1.19.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:935c27ae2760c21cd7354402546f6be21d3d0c806fffe967f745d5f2de5005a7"}, - {file = "numpy-1.19.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:309cbcfaa103fc9a33ec16d2d62569d541b79f828c382556ff072442226d1968"}, - {file = "numpy-1.19.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:7ed448ff4eaffeb01094959b19cbaf998ecdee9ef9932381420d514e446601cd"}, - {file = "numpy-1.19.1-cp38-cp38-win32.whl", hash = "sha256:de8b4a9b56255797cbddb93281ed92acbc510fb7b15df3f01bd28f46ebc4edae"}, - {file = "numpy-1.19.1-cp38-cp38-win_amd64.whl", hash = "sha256:92feb989b47f83ebef246adabc7ff3b9a59ac30601c3f6819f8913458610bdcc"}, - {file = "numpy-1.19.1-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:e1b1dc0372f530f26a03578ac75d5e51b3868b9b76cd2facba4c9ee0eb252ab1"}, - {file = "numpy-1.19.1.zip", hash = "sha256:b8456987b637232602ceb4d663cb34106f7eb780e247d51a260b84760fd8f491"}, + {file = "numpy-1.19.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e9b30d4bd69498fc0c3fe9db5f62fffbb06b8eb9321f92cc970f2969be5e3949"}, + {file = "numpy-1.19.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:fedbd128668ead37f33917820b704784aff695e0019309ad446a6d0b065b57e4"}, + {file = "numpy-1.19.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8ece138c3a16db8c1ad38f52eb32be6086cc72f403150a79336eb2045723a1ad"}, + {file = "numpy-1.19.4-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:64324f64f90a9e4ef732be0928be853eee378fd6a01be21a0a8469c4f2682c83"}, + {file = "numpy-1.19.4-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:ad6f2ff5b1989a4899bf89800a671d71b1612e5ff40866d1f4d8bcf48d4e5764"}, + {file = "numpy-1.19.4-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d6c7bb82883680e168b55b49c70af29b84b84abb161cbac2800e8fcb6f2109b6"}, + {file = "numpy-1.19.4-cp36-cp36m-win32.whl", hash = "sha256:13d166f77d6dc02c0a73c1101dd87fdf01339febec1030bd810dcd53fff3b0f1"}, + {file = "numpy-1.19.4-cp36-cp36m-win_amd64.whl", hash = "sha256:448ebb1b3bf64c0267d6b09a7cba26b5ae61b6d2dbabff7c91b660c7eccf2bdb"}, + {file = "numpy-1.19.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:27d3f3b9e3406579a8af3a9f262f5339005dd25e0ecf3cf1559ff8a49ed5cbf2"}, + {file = "numpy-1.19.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:16c1b388cc31a9baa06d91a19366fb99ddbe1c7b205293ed072211ee5bac1ed2"}, + {file = "numpy-1.19.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e5b6ed0f0b42317050c88022349d994fe72bfe35f5908617512cd8c8ef9da2a9"}, + {file = "numpy-1.19.4-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:18bed2bcb39e3f758296584337966e68d2d5ba6aab7e038688ad53c8f889f757"}, + {file = "numpy-1.19.4-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:fe45becb4c2f72a0907c1d0246ea6449fe7a9e2293bb0e11c4e9a32bb0930a15"}, + {file = "numpy-1.19.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:6d7593a705d662be5bfe24111af14763016765f43cb6923ed86223f965f52387"}, + {file = "numpy-1.19.4-cp37-cp37m-win32.whl", hash = "sha256:6ae6c680f3ebf1cf7ad1d7748868b39d9f900836df774c453c11c5440bc15b36"}, + {file = "numpy-1.19.4-cp37-cp37m-win_amd64.whl", hash = "sha256:9eeb7d1d04b117ac0d38719915ae169aa6b61fca227b0b7d198d43728f0c879c"}, + {file = "numpy-1.19.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cb1017eec5257e9ac6209ac172058c430e834d5d2bc21961dceeb79d111e5909"}, + {file = "numpy-1.19.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:edb01671b3caae1ca00881686003d16c2209e07b7ef8b7639f1867852b948f7c"}, + {file = "numpy-1.19.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:f29454410db6ef8126c83bd3c968d143304633d45dc57b51252afbd79d700893"}, + {file = "numpy-1.19.4-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:ec149b90019852266fec2341ce1db513b843e496d5a8e8cdb5ced1923a92faab"}, + {file = "numpy-1.19.4-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:1aeef46a13e51931c0b1cf8ae1168b4a55ecd282e6688fdb0a948cc5a1d5afb9"}, + {file = "numpy-1.19.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:08308c38e44cc926bdfce99498b21eec1f848d24c302519e64203a8da99a97db"}, + {file = "numpy-1.19.4-cp38-cp38-win32.whl", hash = "sha256:5734bdc0342aba9dfc6f04920988140fb41234db42381cf7ccba64169f9fe7ac"}, + {file = "numpy-1.19.4-cp38-cp38-win_amd64.whl", hash = "sha256:09c12096d843b90eafd01ea1b3307e78ddd47a55855ad402b157b6c4862197ce"}, + {file = "numpy-1.19.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e452dc66e08a4ce642a961f134814258a082832c78c90351b75c41ad16f79f63"}, + {file = "numpy-1.19.4-cp39-cp39-manylinux1_i686.whl", hash = "sha256:a5d897c14513590a85774180be713f692df6fa8ecf6483e561a6d47309566f37"}, + {file = "numpy-1.19.4-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:a09f98011236a419ee3f49cedc9ef27d7a1651df07810ae430a6b06576e0b414"}, + {file = "numpy-1.19.4-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:50e86c076611212ca62e5a59f518edafe0c0730f7d9195fec718da1a5c2bb1fc"}, + {file = "numpy-1.19.4-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f0d3929fe88ee1c155129ecd82f981b8856c5d97bcb0d5f23e9b4242e79d1de3"}, + {file = "numpy-1.19.4-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:c42c4b73121caf0ed6cd795512c9c09c52a7287b04d105d112068c1736d7c753"}, + {file = "numpy-1.19.4-cp39-cp39-win32.whl", hash = "sha256:8cac8790a6b1ddf88640a9267ee67b1aee7a57dfa2d2dd33999d080bc8ee3a0f"}, + {file = "numpy-1.19.4-cp39-cp39-win_amd64.whl", hash = "sha256:4377e10b874e653fe96985c05feed2225c912e328c8a26541f7fc600fb9c637b"}, + {file = "numpy-1.19.4-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:2a2740aa9733d2e5b2dfb33639d98a64c3b0f24765fed86b0fd2aec07f6a0a08"}, + {file = "numpy-1.19.4.zip", hash = "sha256:141ec3a3300ab89c7f2b0775289954d193cc8edb621ea05f99db9cb181530512"}, ] orderedmultidict = [ {file = "orderedmultidict-1.0.1-py2.py3-none-any.whl", hash = "sha256:43c839a17ee3cdd62234c47deca1a8508a3f2ca1d0678a3bf791c87cf84adbf3"}, {file = "orderedmultidict-1.0.1.tar.gz", hash = "sha256:04070bbb5e87291cc9bfa51df413677faf2141c73c61d2a5f7b26bea3cd882ad"}, ] packaging = [ - {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"}, - {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, + {file = "packaging-20.8-py2.py3-none-any.whl", hash = "sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858"}, + {file = "packaging-20.8.tar.gz", hash = "sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"}, ] parso = [ - {file = "parso-0.7.1-py2.py3-none-any.whl", hash = "sha256:97218d9159b2520ff45eb78028ba8b50d2bc61dcc062a9682666f2dc4bd331ea"}, - {file = "parso-0.7.1.tar.gz", hash = "sha256:caba44724b994a8a5e086460bb212abc5a8bc46951bf4a9a1210745953622eb9"}, + {file = "parso-0.8.1-py2.py3-none-any.whl", hash = "sha256:15b00182f472319383252c18d5913b69269590616c947747bc50bf4ac768f410"}, + {file = "parso-0.8.1.tar.gz", hash = "sha256:8519430ad07087d4c997fda3a7918f7cfa27cb58972a8c89c2a0295a1c940e9e"}, ] pathspec = [ - {file = "pathspec-0.8.0-py2.py3-none-any.whl", hash = "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0"}, - {file = "pathspec-0.8.0.tar.gz", hash = "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"}, + {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, + {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, ] pbr = [ - {file = "pbr-5.4.5-py2.py3-none-any.whl", hash = "sha256:579170e23f8e0c2f24b0de612f71f648eccb79fb1322c814ae6b3c07b5ba23e8"}, - {file = "pbr-5.4.5.tar.gz", hash = "sha256:07f558fece33b05caf857474a366dfcc00562bca13dd8b47b2b3e22d9f9bf55c"}, + {file = "pbr-5.5.1-py2.py3-none-any.whl", hash = "sha256:b236cde0ac9a6aedd5e3c34517b423cd4fd97ef723849da6b0d2231142d89c00"}, + {file = "pbr-5.5.1.tar.gz", hash = "sha256:5fad80b613c402d5b7df7bd84812548b2a61e9977387a80a5fc5c396492b13c9"}, ] pdocs = [ {file = "pdocs-1.0.2-py3-none-any.whl", hash = "sha256:4d5ff87babcd0c46f12b76c887d53225bddb389dee7c6b338dbe281c729fc035"}, {file = "pdocs-1.0.2.tar.gz", hash = "sha256:2e32432bd2736fd678ac1ce4447cd508deb62b5a12f7ba3bf0e3a374063221e2"}, ] pep517 = [ - {file = "pep517-0.8.2-py2.py3-none-any.whl", hash = "sha256:576c480be81f3e1a70a16182c762311eb80d1f8a7b0d11971e5234967d7a342c"}, - {file = "pep517-0.8.2.tar.gz", hash = "sha256:8e6199cf1288d48a0c44057f112acf18aa5ebabbf73faa242f598fbe145ba29e"}, + {file = "pep517-0.9.1-py2.py3-none-any.whl", hash = "sha256:3985b91ebf576883efe5fa501f42a16de2607684f3797ddba7202b71b7d0da51"}, + {file = "pep517-0.9.1.tar.gz", hash = "sha256:aeb78601f2d1aa461960b43add204cc7955667687fbcf9cdb5170f00556f117f"}, ] pep8-naming = [ {file = "pep8-naming-0.8.2.tar.gz", hash = "sha256:01cb1dab2f3ce9045133d08449f1b6b93531dceacb9ef04f67087c11c723cea9"}, @@ -2058,47 +2087,52 @@ prompt-toolkit = [ {file = "prompt_toolkit-3.0.3.tar.gz", hash = "sha256:a402e9bf468b63314e37460b68ba68243d55b2f8c4d0192f85a019af3945050e"}, ] ptyprocess = [ - {file = "ptyprocess-0.6.0-py2.py3-none-any.whl", hash = "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"}, - {file = "ptyprocess-0.6.0.tar.gz", hash = "sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0"}, + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, ] py = [ - {file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"}, - {file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"}, + {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, + {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, ] pycodestyle = [ {file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"}, {file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"}, ] pydantic = [ - {file = "pydantic-1.6.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:418b84654b60e44c0cdd5384294b0e4bc1ebf42d6e873819424f3b78b8690614"}, - {file = "pydantic-1.6.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:4900b8820b687c9a3ed753684337979574df20e6ebe4227381d04b3c3c628f99"}, - {file = "pydantic-1.6.1-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:b49c86aecde15cde33835d5d6360e55f5e0067bb7143a8303bf03b872935c75b"}, - {file = "pydantic-1.6.1-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:2de562a456c4ecdc80cf1a8c3e70c666625f7d02d89a6174ecf63754c734592e"}, - {file = "pydantic-1.6.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f769141ab0abfadf3305d4fcf36660e5cf568a666dd3efab7c3d4782f70946b1"}, - {file = "pydantic-1.6.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2dc946b07cf24bee4737ced0ae77e2ea6bc97489ba5a035b603bd1b40ad81f7e"}, - {file = "pydantic-1.6.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:36dbf6f1be212ab37b5fda07667461a9219c956181aa5570a00edfb0acdfe4a1"}, - {file = "pydantic-1.6.1-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:1783c1d927f9e1366e0e0609ae324039b2479a1a282a98ed6a6836c9ed02002c"}, - {file = "pydantic-1.6.1-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:cf3933c98cb5e808b62fae509f74f209730b180b1e3c3954ee3f7949e083a7df"}, - {file = "pydantic-1.6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f8af9b840a9074e08c0e6dc93101de84ba95df89b267bf7151d74c553d66833b"}, - {file = "pydantic-1.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:40d765fa2d31d5be8e29c1794657ad46f5ee583a565c83cea56630d3ae5878b9"}, - {file = "pydantic-1.6.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:3fa799f3cfff3e5f536cbd389368fc96a44bb30308f258c94ee76b73bd60531d"}, - {file = "pydantic-1.6.1-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:6c3f162ba175678218629f446a947e3356415b6b09122dcb364e58c442c645a7"}, - {file = "pydantic-1.6.1-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:eb75dc1809875d5738df14b6566ccf9fd9c0bcde4f36b72870f318f16b9f5c20"}, - {file = "pydantic-1.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:530d7222a2786a97bc59ee0e0ebbe23728f82974b1f1ad9a11cd966143410633"}, - {file = "pydantic-1.6.1-py36.py37.py38-none-any.whl", hash = "sha256:b5b3489cb303d0f41ad4a7390cf606a5f2c7a94dcba20c051cd1c653694cb14d"}, - {file = "pydantic-1.6.1.tar.gz", hash = "sha256:54122a8ed6b75fe1dd80797f8251ad2063ea348a03b77218d73ea9fe19bd4e73"}, + {file = "pydantic-1.7.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c59ea046aea25be14dc22d69c97bee629e6d48d2b2ecb724d7fe8806bf5f61cd"}, + {file = "pydantic-1.7.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a4143c8d0c456a093387b96e0f5ee941a950992904d88bc816b4f0e72c9a0009"}, + {file = "pydantic-1.7.3-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:d8df4b9090b595511906fa48deda47af04e7d092318bfb291f4d45dfb6bb2127"}, + {file = "pydantic-1.7.3-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:514b473d264671a5c672dfb28bdfe1bf1afd390f6b206aa2ec9fed7fc592c48e"}, + {file = "pydantic-1.7.3-cp36-cp36m-win_amd64.whl", hash = "sha256:dba5c1f0a3aeea5083e75db9660935da90216f8a81b6d68e67f54e135ed5eb23"}, + {file = "pydantic-1.7.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:59e45f3b694b05a69032a0d603c32d453a23f0de80844fb14d55ab0c6c78ff2f"}, + {file = "pydantic-1.7.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:5b24e8a572e4b4c18f614004dda8c9f2c07328cb5b6e314d6e1bbd536cb1a6c1"}, + {file = "pydantic-1.7.3-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:b2b054d095b6431cdda2f852a6d2f0fdec77686b305c57961b4c5dd6d863bf3c"}, + {file = "pydantic-1.7.3-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:025bf13ce27990acc059d0c5be46f416fc9b293f45363b3d19855165fee1874f"}, + {file = "pydantic-1.7.3-cp37-cp37m-win_amd64.whl", hash = "sha256:6e3874aa7e8babd37b40c4504e3a94cc2023696ced5a0500949f3347664ff8e2"}, + {file = "pydantic-1.7.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e682f6442ebe4e50cb5e1cfde7dda6766fb586631c3e5569f6aa1951fd1a76ef"}, + {file = "pydantic-1.7.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:185e18134bec5ef43351149fe34fda4758e53d05bb8ea4d5928f0720997b79ef"}, + {file = "pydantic-1.7.3-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:f5b06f5099e163295b8ff5b1b71132ecf5866cc6e7f586d78d7d3fd6e8084608"}, + {file = "pydantic-1.7.3-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:24ca47365be2a5a3cc3f4a26dcc755bcdc9f0036f55dcedbd55663662ba145ec"}, + {file = "pydantic-1.7.3-cp38-cp38-win_amd64.whl", hash = "sha256:d1fe3f0df8ac0f3a9792666c69a7cd70530f329036426d06b4f899c025aca74e"}, + {file = "pydantic-1.7.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f6864844b039805add62ebe8a8c676286340ba0c6d043ae5dea24114b82a319e"}, + {file = "pydantic-1.7.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:ecb54491f98544c12c66ff3d15e701612fc388161fd455242447083350904730"}, + {file = "pydantic-1.7.3-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:ffd180ebd5dd2a9ac0da4e8b995c9c99e7c74c31f985ba090ee01d681b1c4b95"}, + {file = "pydantic-1.7.3-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:8d72e814c7821125b16f1553124d12faba88e85405b0864328899aceaad7282b"}, + {file = "pydantic-1.7.3-cp39-cp39-win_amd64.whl", hash = "sha256:475f2fa134cf272d6631072554f845d0630907fce053926ff634cc6bc45bf1af"}, + {file = "pydantic-1.7.3-py3-none-any.whl", hash = "sha256:38be427ea01a78206bcaf9a56f835784afcba9e5b88fbdce33bbbfbcd7841229"}, + {file = "pydantic-1.7.3.tar.gz", hash = "sha256:213125b7e9e64713d16d988d10997dabc6a1f73f3991e1ff8e35ebb1409c7dc9"}, ] pydocstyle = [ - {file = "pydocstyle-5.1.0-py3-none-any.whl", hash = "sha256:08374b9d4d2b7164bae50b71bb24eb0d74a56b309029d5d502264092fa7db0c3"}, - {file = "pydocstyle-5.1.0.tar.gz", hash = "sha256:4ca3c7736d36f92bb215dd74ef84ac3d6c146edd795c7afc5154c10f1eb1f65a"}, + {file = "pydocstyle-5.1.1-py3-none-any.whl", hash = "sha256:aca749e190a01726a4fb472dd4ef23b5c9da7b9205c0a7857c06533de13fd678"}, + {file = "pydocstyle-5.1.1.tar.gz", hash = "sha256:19b86fa8617ed916776a11cd8bc0197e5b9856d5433b777f51a3defe13075325"}, ] pyflakes = [ {file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"}, {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"}, ] pygments = [ - {file = "Pygments-2.6.1-py3-none-any.whl", hash = "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324"}, - {file = "Pygments-2.6.1.tar.gz", hash = "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44"}, + {file = "Pygments-2.7.3-py3-none-any.whl", hash = "sha256:f275b6c0909e5dafd2d6269a656aa90fa58ebf4a74f8fcf9053195d226b24a08"}, + {file = "Pygments-2.7.3.tar.gz", hash = "sha256:ccf3acacf3782cbed4a989426012f1c535c9a90d3a7fc3f16d231b9372d2b716"}, ] pylama = [ {file = "pylama-7.7.1-py2.py3-none-any.whl", hash = "sha256:fd61c11872d6256b019ef1235be37b77c922ef37ac9797df6bd489996dddeb15"}, @@ -2113,8 +2147,8 @@ pyparsing = [ {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, ] pytest = [ - {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"}, - {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, + {file = "pytest-6.2.1-py3-none-any.whl", hash = "sha256:1969f797a1a0dbd8ccf0fecc80262312729afea9c17f1d70ebf85c5e76c6f7c8"}, + {file = "pytest-6.2.1.tar.gz", hash = "sha256:66e419b1899bc27346cb2c993e12c5e5e8daba9073c1fbce33b9807abc95c306"}, ] pytest-cov = [ {file = "pytest-cov-2.10.1.tar.gz", hash = "sha256:47bd0ce14056fdd79f93e1713f88fad7bdcc583dcd7783da86ef2f085a0bb88e"}, @@ -2147,43 +2181,63 @@ pyyaml = [ {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, ] regex = [ - {file = "regex-2020.7.14-cp27-cp27m-win32.whl", hash = "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7"}, - {file = "regex-2020.7.14-cp27-cp27m-win_amd64.whl", hash = "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644"}, - {file = "regex-2020.7.14-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc"}, - {file = "regex-2020.7.14-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067"}, - {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd"}, - {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88"}, - {file = "regex-2020.7.14-cp36-cp36m-win32.whl", hash = "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4"}, - {file = "regex-2020.7.14-cp36-cp36m-win_amd64.whl", hash = "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f"}, - {file = "regex-2020.7.14-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162"}, - {file = "regex-2020.7.14-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf"}, - {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7"}, - {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89"}, - {file = "regex-2020.7.14-cp37-cp37m-win32.whl", hash = "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6"}, - {file = "regex-2020.7.14-cp37-cp37m-win_amd64.whl", hash = "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204"}, - {file = "regex-2020.7.14-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99"}, - {file = "regex-2020.7.14-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e"}, - {file = "regex-2020.7.14-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e"}, - {file = "regex-2020.7.14-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a"}, - {file = "regex-2020.7.14-cp38-cp38-win32.whl", hash = "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341"}, - {file = "regex-2020.7.14-cp38-cp38-win_amd64.whl", hash = "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840"}, - {file = "regex-2020.7.14.tar.gz", hash = "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb"}, + {file = "regex-2020.11.13-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85"}, + {file = "regex-2020.11.13-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a63f1a07932c9686d2d416fb295ec2c01ab246e89b4d58e5fa468089cab44b70"}, + {file = "regex-2020.11.13-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:6e4b08c6f8daca7d8f07c8d24e4331ae7953333dbd09c648ed6ebd24db5a10ee"}, + {file = "regex-2020.11.13-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:bba349276b126947b014e50ab3316c027cac1495992f10e5682dc677b3dfa0c5"}, + {file = "regex-2020.11.13-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:56e01daca75eae420bce184edd8bb341c8eebb19dd3bce7266332258f9fb9dd7"}, + {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:6a8ce43923c518c24a2579fda49f093f1397dad5d18346211e46f134fc624e31"}, + {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:1ab79fcb02b930de09c76d024d279686ec5d532eb814fd0ed1e0051eb8bd2daa"}, + {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:9801c4c1d9ae6a70aeb2128e5b4b68c45d4f0af0d1535500884d644fa9b768c6"}, + {file = "regex-2020.11.13-cp36-cp36m-win32.whl", hash = "sha256:49cae022fa13f09be91b2c880e58e14b6da5d10639ed45ca69b85faf039f7a4e"}, + {file = "regex-2020.11.13-cp36-cp36m-win_amd64.whl", hash = "sha256:749078d1eb89484db5f34b4012092ad14b327944ee7f1c4f74d6279a6e4d1884"}, + {file = "regex-2020.11.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b2f4007bff007c96a173e24dcda236e5e83bde4358a557f9ccf5e014439eae4b"}, + {file = "regex-2020.11.13-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:38c8fd190db64f513fe4e1baa59fed086ae71fa45083b6936b52d34df8f86a88"}, + {file = "regex-2020.11.13-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5862975b45d451b6db51c2e654990c1820523a5b07100fc6903e9c86575202a0"}, + {file = "regex-2020.11.13-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:262c6825b309e6485ec2493ffc7e62a13cf13fb2a8b6d212f72bd53ad34118f1"}, + {file = "regex-2020.11.13-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bafb01b4688833e099d79e7efd23f99172f501a15c44f21ea2118681473fdba0"}, + {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:e32f5f3d1b1c663af7f9c4c1e72e6ffe9a78c03a31e149259f531e0fed826512"}, + {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:3bddc701bdd1efa0d5264d2649588cbfda549b2899dc8d50417e47a82e1387ba"}, + {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:02951b7dacb123d8ea6da44fe45ddd084aa6777d4b2454fa0da61d569c6fa538"}, + {file = "regex-2020.11.13-cp37-cp37m-win32.whl", hash = "sha256:0d08e71e70c0237883d0bef12cad5145b84c3705e9c6a588b2a9c7080e5af2a4"}, + {file = "regex-2020.11.13-cp37-cp37m-win_amd64.whl", hash = "sha256:1fa7ee9c2a0e30405e21031d07d7ba8617bc590d391adfc2b7f1e8b99f46f444"}, + {file = "regex-2020.11.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:baf378ba6151f6e272824b86a774326f692bc2ef4cc5ce8d5bc76e38c813a55f"}, + {file = "regex-2020.11.13-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e3faaf10a0d1e8e23a9b51d1900b72e1635c2d5b0e1bea1c18022486a8e2e52d"}, + {file = "regex-2020.11.13-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2a11a3e90bd9901d70a5b31d7dd85114755a581a5da3fc996abfefa48aee78af"}, + {file = "regex-2020.11.13-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1ebb090a426db66dd80df8ca85adc4abfcbad8a7c2e9a5ec7513ede522e0a8f"}, + {file = "regex-2020.11.13-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:b2b1a5ddae3677d89b686e5c625fc5547c6e492bd755b520de5332773a8af06b"}, + {file = "regex-2020.11.13-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:2c99e97d388cd0a8d30f7c514d67887d8021541b875baf09791a3baad48bb4f8"}, + {file = "regex-2020.11.13-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:c084582d4215593f2f1d28b65d2a2f3aceff8342aa85afd7be23a9cad74a0de5"}, + {file = "regex-2020.11.13-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:a3d748383762e56337c39ab35c6ed4deb88df5326f97a38946ddd19028ecce6b"}, + {file = "regex-2020.11.13-cp38-cp38-win32.whl", hash = "sha256:7913bd25f4ab274ba37bc97ad0e21c31004224ccb02765ad984eef43e04acc6c"}, + {file = "regex-2020.11.13-cp38-cp38-win_amd64.whl", hash = "sha256:6c54ce4b5d61a7129bad5c5dc279e222afd00e721bf92f9ef09e4fae28755683"}, + {file = "regex-2020.11.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1862a9d9194fae76a7aaf0150d5f2a8ec1da89e8b55890b1786b8f88a0f619dc"}, + {file = "regex-2020.11.13-cp39-cp39-manylinux1_i686.whl", hash = "sha256:4902e6aa086cbb224241adbc2f06235927d5cdacffb2425c73e6570e8d862364"}, + {file = "regex-2020.11.13-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7a25fcbeae08f96a754b45bdc050e1fb94b95cab046bf56b016c25e9ab127b3e"}, + {file = "regex-2020.11.13-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:d2d8ce12b7c12c87e41123997ebaf1a5767a5be3ec545f64675388970f415e2e"}, + {file = "regex-2020.11.13-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f7d29a6fc4760300f86ae329e3b6ca28ea9c20823df123a2ea8693e967b29917"}, + {file = "regex-2020.11.13-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:717881211f46de3ab130b58ec0908267961fadc06e44f974466d1887f865bd5b"}, + {file = "regex-2020.11.13-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:3128e30d83f2e70b0bed9b2a34e92707d0877e460b402faca908c6667092ada9"}, + {file = "regex-2020.11.13-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:8f6a2229e8ad946e36815f2a03386bb8353d4bde368fdf8ca5f0cb97264d3b5c"}, + {file = "regex-2020.11.13-cp39-cp39-win32.whl", hash = "sha256:f8f295db00ef5f8bae530fc39af0b40486ca6068733fb860b42115052206466f"}, + {file = "regex-2020.11.13-cp39-cp39-win_amd64.whl", hash = "sha256:a15f64ae3a027b64496a71ab1f722355e570c3fac5ba2801cafce846bf5af01d"}, + {file = "regex-2020.11.13.tar.gz", hash = "sha256:83d6b356e116ca119db8e7c6fc2983289d87b27b3fac238cfe5dca529d884562"}, ] requests = [ - {file = "requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"}, - {file = "requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"}, + {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, + {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, ] requirementslib = [ - {file = "requirementslib-1.5.13-py2.py3-none-any.whl", hash = "sha256:cdf8aa652ac52216d156cee2b89c3c9ee53373dded0035184d0b9af569a0f10c"}, - {file = "requirementslib-1.5.13.tar.gz", hash = "sha256:fd98ea873effaede6b3394725a232bcbd3fe3985987e226109a841c85a69e2e3"}, + {file = "requirementslib-1.5.16-py2.py3-none-any.whl", hash = "sha256:50d20f27e4515a2393695b0d886219598302163438ae054253147b2bad9b4a44"}, + {file = "requirementslib-1.5.16.tar.gz", hash = "sha256:9c1e8666ca4512724cdd1739adcc7df19ec7ad2ed21f0e748f9631ad6b54f321"}, ] rfc3986 = [ {file = "rfc3986-1.4.0-py2.py3-none-any.whl", hash = "sha256:af9147e9aceda37c91a05f4deb128d4b4b49d6b199775fd2d2927768abdc8f50"}, {file = "rfc3986-1.4.0.tar.gz", hash = "sha256:112398da31a3344dc25dbf477d8df6cb34f9278a94fee2625d89e4514be8bb9d"}, ] safety = [ - {file = "safety-1.9.0-py2.py3-none-any.whl", hash = "sha256:86c1c4a031fe35bd624fce143fbe642a0234d29f7cbf7a9aa269f244a955b087"}, - {file = "safety-1.9.0.tar.gz", hash = "sha256:23bf20690d4400edc795836b0c983c2b4cbbb922233108ff925b7dd7750f00c9"}, + {file = "safety-1.10.0-py2.py3-none-any.whl", hash = "sha256:69437acf5dd617abd7086ccd0d50e813e67aa969bb9ca90f1847d5fbea047dcc"}, + {file = "safety-1.10.0.tar.gz", hash = "sha256:2ebc71b44666588d7898905d86d575933fcd5fa3c92d301ed12482602b1e928a"}, ] six = [ {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, @@ -2198,83 +2252,115 @@ smmap2 = [ {file = "smmap2-3.0.1.tar.gz", hash = "sha256:44cc8bdaf96442dbb9a8e2e14377d074b3d0eea292eee3c95c8c449b6c92c557"}, ] sniffio = [ - {file = "sniffio-1.1.0-py3-none-any.whl", hash = "sha256:20ed6d5b46f8ae136d00b9dcb807615d83ed82ceea6b2058cecb696765246da5"}, - {file = "sniffio-1.1.0.tar.gz", hash = "sha256:8e3810100f69fe0edd463d02ad407112542a11ffdc29f67db2bf3771afb87a21"}, + {file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"}, + {file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"}, ] snowballstemmer = [ {file = "snowballstemmer-2.0.0-py2.py3-none-any.whl", hash = "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0"}, {file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"}, ] sortedcontainers = [ - {file = "sortedcontainers-2.2.2-py2.py3-none-any.whl", hash = "sha256:c633ebde8580f241f274c1f8994a665c0e54a17724fecd0cae2f079e09c36d3f"}, - {file = "sortedcontainers-2.2.2.tar.gz", hash = "sha256:4e73a757831fc3ca4de2859c422564239a31d8213d09a2a666e375807034d2ba"}, + {file = "sortedcontainers-2.3.0-py2.py3-none-any.whl", hash = "sha256:37257a32add0a3ee490bb170b599e93095eed89a55da91fa9f48753ea12fd73f"}, + {file = "sortedcontainers-2.3.0.tar.gz", hash = "sha256:59cc937650cf60d677c16775597c89a960658a09cf7c1a668f86e1e4464b10a1"}, ] stevedore = [ - {file = "stevedore-3.2.0-py3-none-any.whl", hash = "sha256:c8f4f0ebbc394e52ddf49de8bcc3cf8ad2b4425ebac494106bbc5e3661ac7633"}, - {file = "stevedore-3.2.0.tar.gz", hash = "sha256:38791aa5bed922b0a844513c5f9ed37774b68edc609e5ab8ab8d8fe0ce4315e5"}, + {file = "stevedore-3.3.0-py3-none-any.whl", hash = "sha256:50d7b78fbaf0d04cd62411188fa7eedcb03eb7f4c4b37005615ceebe582aa82a"}, + {file = "stevedore-3.3.0.tar.gz", hash = "sha256:3a5bbd0652bf552748871eaa73a4a8dc2899786bc497a2aa1fcb4dcdb0debeee"}, ] text-unidecode = [ {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, ] toml = [ - {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"}, - {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"}, + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] tomlkit = [ {file = "tomlkit-0.7.0-py2.py3-none-any.whl", hash = "sha256:6babbd33b17d5c9691896b0e68159215a9387ebfa938aa3ac42f4a4beeb2b831"}, {file = "tomlkit-0.7.0.tar.gz", hash = "sha256:ac57f29693fab3e309ea789252fcce3061e19110085aa31af5446ca749325618"}, ] tornado = [ - {file = "tornado-6.0.4-cp35-cp35m-win32.whl", hash = "sha256:5217e601700f24e966ddab689f90b7ea4bd91ff3357c3600fa1045e26d68e55d"}, - {file = "tornado-6.0.4-cp35-cp35m-win_amd64.whl", hash = "sha256:c98232a3ac391f5faea6821b53db8db461157baa788f5d6222a193e9456e1740"}, - {file = "tornado-6.0.4-cp36-cp36m-win32.whl", hash = "sha256:5f6a07e62e799be5d2330e68d808c8ac41d4a259b9cea61da4101b83cb5dc673"}, - {file = "tornado-6.0.4-cp36-cp36m-win_amd64.whl", hash = "sha256:c952975c8ba74f546ae6de2e226ab3cc3cc11ae47baf607459a6728585bb542a"}, - {file = "tornado-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:2c027eb2a393d964b22b5c154d1a23a5f8727db6fda837118a776b29e2b8ebc6"}, - {file = "tornado-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:5618f72e947533832cbc3dec54e1dffc1747a5cb17d1fd91577ed14fa0dc081b"}, - {file = "tornado-6.0.4-cp38-cp38-win32.whl", hash = "sha256:22aed82c2ea340c3771e3babc5ef220272f6fd06b5108a53b4976d0d722bcd52"}, - {file = "tornado-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:c58d56003daf1b616336781b26d184023ea4af13ae143d9dda65e31e534940b9"}, - {file = "tornado-6.0.4.tar.gz", hash = "sha256:0fe2d45ba43b00a41cd73f8be321a44936dc1aba233dee979f17a042b83eb6dc"}, + {file = "tornado-6.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:d371e811d6b156d82aa5f9a4e08b58debf97c302a35714f6f45e35139c332e32"}, + {file = "tornado-6.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:0d321a39c36e5f2c4ff12b4ed58d41390460f798422c4504e09eb5678e09998c"}, + {file = "tornado-6.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9de9e5188a782be6b1ce866e8a51bc76a0fbaa0e16613823fc38e4fc2556ad05"}, + {file = "tornado-6.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:61b32d06ae8a036a6607805e6720ef00a3c98207038444ba7fd3d169cd998910"}, + {file = "tornado-6.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:3e63498f680547ed24d2c71e6497f24bca791aca2fe116dbc2bd0ac7f191691b"}, + {file = "tornado-6.1-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:6c77c9937962577a6a76917845d06af6ab9197702a42e1346d8ae2e76b5e3675"}, + {file = "tornado-6.1-cp35-cp35m-win32.whl", hash = "sha256:6286efab1ed6e74b7028327365cf7346b1d777d63ab30e21a0f4d5b275fc17d5"}, + {file = "tornado-6.1-cp35-cp35m-win_amd64.whl", hash = "sha256:fa2ba70284fa42c2a5ecb35e322e68823288a4251f9ba9cc77be04ae15eada68"}, + {file = "tornado-6.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0a00ff4561e2929a2c37ce706cb8233b7907e0cdc22eab98888aca5dd3775feb"}, + {file = "tornado-6.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:748290bf9112b581c525e6e6d3820621ff020ed95af6f17fedef416b27ed564c"}, + {file = "tornado-6.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:e385b637ac3acaae8022e7e47dfa7b83d3620e432e3ecb9a3f7f58f150e50921"}, + {file = "tornado-6.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:25ad220258349a12ae87ede08a7b04aca51237721f63b1808d39bdb4b2164558"}, + {file = "tornado-6.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:65d98939f1a2e74b58839f8c4dab3b6b3c1ce84972ae712be02845e65391ac7c"}, + {file = "tornado-6.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:e519d64089b0876c7b467274468709dadf11e41d65f63bba207e04217f47c085"}, + {file = "tornado-6.1-cp36-cp36m-win32.whl", hash = "sha256:b87936fd2c317b6ee08a5741ea06b9d11a6074ef4cc42e031bc6403f82a32575"}, + {file = "tornado-6.1-cp36-cp36m-win_amd64.whl", hash = "sha256:cc0ee35043162abbf717b7df924597ade8e5395e7b66d18270116f8745ceb795"}, + {file = "tornado-6.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7250a3fa399f08ec9cb3f7b1b987955d17e044f1ade821b32e5f435130250d7f"}, + {file = "tornado-6.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ed3ad863b1b40cd1d4bd21e7498329ccaece75db5a5bf58cd3c9f130843e7102"}, + {file = "tornado-6.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:dcef026f608f678c118779cd6591c8af6e9b4155c44e0d1bc0c87c036fb8c8c4"}, + {file = "tornado-6.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:70dec29e8ac485dbf57481baee40781c63e381bebea080991893cd297742b8fd"}, + {file = "tornado-6.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:d3f7594930c423fd9f5d1a76bee85a2c36fd8b4b16921cae7e965f22575e9c01"}, + {file = "tornado-6.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:3447475585bae2e77ecb832fc0300c3695516a47d46cefa0528181a34c5b9d3d"}, + {file = "tornado-6.1-cp37-cp37m-win32.whl", hash = "sha256:e7229e60ac41a1202444497ddde70a48d33909e484f96eb0da9baf8dc68541df"}, + {file = "tornado-6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:cb5ec8eead331e3bb4ce8066cf06d2dfef1bfb1b2a73082dfe8a161301b76e37"}, + {file = "tornado-6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:20241b3cb4f425e971cb0a8e4ffc9b0a861530ae3c52f2b0434e6c1b57e9fd95"}, + {file = "tornado-6.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c77da1263aa361938476f04c4b6c8916001b90b2c2fdd92d8d535e1af48fba5a"}, + {file = "tornado-6.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:fba85b6cd9c39be262fcd23865652920832b61583de2a2ca907dbd8e8a8c81e5"}, + {file = "tornado-6.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:1e8225a1070cd8eec59a996c43229fe8f95689cb16e552d130b9793cb570a288"}, + {file = "tornado-6.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d14d30e7f46a0476efb0deb5b61343b1526f73ebb5ed84f23dc794bdb88f9d9f"}, + {file = "tornado-6.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8f959b26f2634a091bb42241c3ed8d3cedb506e7c27b8dd5c7b9f745318ddbb6"}, + {file = "tornado-6.1-cp38-cp38-win32.whl", hash = "sha256:34ca2dac9e4d7afb0bed4677512e36a52f09caa6fded70b4e3e1c89dbd92c326"}, + {file = "tornado-6.1-cp38-cp38-win_amd64.whl", hash = "sha256:6196a5c39286cc37c024cd78834fb9345e464525d8991c21e908cc046d1cc02c"}, + {file = "tornado-6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f0ba29bafd8e7e22920567ce0d232c26d4d47c8b5cf4ed7b562b5db39fa199c5"}, + {file = "tornado-6.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:33892118b165401f291070100d6d09359ca74addda679b60390b09f8ef325ffe"}, + {file = "tornado-6.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7da13da6f985aab7f6f28debab00c67ff9cbacd588e8477034c0652ac141feea"}, + {file = "tornado-6.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:e0791ac58d91ac58f694d8d2957884df8e4e2f6687cdf367ef7eb7497f79eaa2"}, + {file = "tornado-6.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:66324e4e1beede9ac79e60f88de548da58b1f8ab4b2f1354d8375774f997e6c0"}, + {file = "tornado-6.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:a48900ecea1cbb71b8c71c620dee15b62f85f7c14189bdeee54966fbd9a0c5bd"}, + {file = "tornado-6.1-cp39-cp39-win32.whl", hash = "sha256:d3d20ea5782ba63ed13bc2b8c291a053c8d807a8fa927d941bd718468f7b950c"}, + {file = "tornado-6.1-cp39-cp39-win_amd64.whl", hash = "sha256:548430be2740e327b3fe0201abe471f314741efcb0067ec4f2d7dcfb4825f3e4"}, + {file = "tornado-6.1.tar.gz", hash = "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791"}, ] tqdm = [ - {file = "tqdm-4.48.2-py2.py3-none-any.whl", hash = "sha256:1a336d2b829be50e46b84668691e0a2719f26c97c62846298dd5ae2937e4d5cf"}, - {file = "tqdm-4.48.2.tar.gz", hash = "sha256:564d632ea2b9cb52979f7956e093e831c28d441c11751682f84c86fc46e4fd21"}, + {file = "tqdm-4.55.0-py2.py3-none-any.whl", hash = "sha256:0cd81710de29754bf17b6fee07bdb86f956b4fa20d3078f02040f83e64309416"}, + {file = "tqdm-4.55.0.tar.gz", hash = "sha256:f4f80b96e2ceafea69add7bf971b8403b9cba8fb4451c1220f91c79be4ebd208"}, ] traitlets = [ {file = "traitlets-4.3.3-py2.py3-none-any.whl", hash = "sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44"}, {file = "traitlets-4.3.3.tar.gz", hash = "sha256:d023ee369ddd2763310e4c3eae1ff649689440d4ae59d7485eb4cfbbe3e359f7"}, ] typed-ast = [ - {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, - {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, - {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, - {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, - {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, - {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, - {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, - {file = "typed_ast-1.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:fcf135e17cc74dbfbc05894ebca928ffeb23d9790b3167a674921db19082401f"}, - {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, - {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, - {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, - {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, - {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, - {file = "typed_ast-1.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:f208eb7aff048f6bea9586e61af041ddf7f9ade7caed625742af423f6bae3298"}, - {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, - {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, - {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, - {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, - {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, - {file = "typed_ast-1.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:7e4c9d7658aaa1fc80018593abdf8598bf91325af6af5cce4ce7c73bc45ea53d"}, - {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, - {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, - {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, - {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c"}, - {file = "typed_ast-1.4.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072"}, - {file = "typed_ast-1.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91"}, - {file = "typed_ast-1.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:0d8110d78a5736e16e26213114a38ca35cb15b6515d535413b090bd50951556d"}, - {file = "typed_ast-1.4.1-cp39-cp39-win32.whl", hash = "sha256:b52ccf7cfe4ce2a1064b18594381bccf4179c2ecf7f513134ec2f993dd4ab395"}, - {file = "typed_ast-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:3742b32cf1c6ef124d57f95be609c473d7ec4c14d0090e5a5e05a15269fb4d0c"}, - {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, + {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70"}, + {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487"}, + {file = "typed_ast-1.4.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412"}, + {file = "typed_ast-1.4.2-cp35-cp35m-win32.whl", hash = "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400"}, + {file = "typed_ast-1.4.2-cp35-cp35m-win_amd64.whl", hash = "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606"}, + {file = "typed_ast-1.4.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64"}, + {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07"}, + {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc"}, + {file = "typed_ast-1.4.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a"}, + {file = "typed_ast-1.4.2-cp36-cp36m-win32.whl", hash = "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151"}, + {file = "typed_ast-1.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3"}, + {file = "typed_ast-1.4.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41"}, + {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f"}, + {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581"}, + {file = "typed_ast-1.4.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37"}, + {file = "typed_ast-1.4.2-cp37-cp37m-win32.whl", hash = "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd"}, + {file = "typed_ast-1.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496"}, + {file = "typed_ast-1.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc"}, + {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10"}, + {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea"}, + {file = "typed_ast-1.4.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787"}, + {file = "typed_ast-1.4.2-cp38-cp38-win32.whl", hash = "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2"}, + {file = "typed_ast-1.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937"}, + {file = "typed_ast-1.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1"}, + {file = "typed_ast-1.4.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6"}, + {file = "typed_ast-1.4.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166"}, + {file = "typed_ast-1.4.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d"}, + {file = "typed_ast-1.4.2-cp39-cp39-win32.whl", hash = "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b"}, + {file = "typed_ast-1.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440"}, + {file = "typed_ast-1.4.2.tar.gz", hash = "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a"}, ] typer = [ {file = "typer-0.3.2-py3-none-any.whl", hash = "sha256:ba58b920ce851b12a2d790143009fa00ac1d05b3ff3257061ff69dbdfc3d161b"}, @@ -2291,8 +2377,8 @@ typing-inspect = [ {file = "typing_inspect-0.6.0.tar.gz", hash = "sha256:8f1b1dd25908dbfd81d3bebc218011531e7ab614ba6e5bf7826d887c834afab7"}, ] urllib3 = [ - {file = "urllib3-1.25.10-py2.py3-none-any.whl", hash = "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"}, - {file = "urllib3-1.25.10.tar.gz", hash = "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a"}, + {file = "urllib3-1.26.2-py2.py3-none-any.whl", hash = "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473"}, + {file = "urllib3-1.26.2.tar.gz", hash = "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08"}, ] vistir = [ {file = "vistir-0.5.2-py2.py3-none-any.whl", hash = "sha256:a37079cdbd85d31a41cdd18457fe521e15ec08b255811e81aa061fd5f48a20fb"}, @@ -2315,6 +2401,6 @@ yaspin = [ {file = "yaspin-0.15.0.tar.gz", hash = "sha256:5a938bdc7bab353fd8942d0619d56c6b5159a80997dc1c387a479b39e6dc9391"}, ] zipp = [ - {file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"}, - {file = "zipp-3.1.0.tar.gz", hash = "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"}, + {file = "zipp-3.4.0-py3-none-any.whl", hash = "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108"}, + {file = "zipp-3.4.0.tar.gz", hash = "sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb"}, ] diff --git a/pyproject.toml b/pyproject.toml index 0889cc4f2..086dadfd3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,7 +58,7 @@ flake8-bugbear = "^19.8" black = {version = "^20.08b1", allow-prereleases = true} mypy = "^0.761.0" ipython = "^7.7" -pytest = "^5.0" +pytest = "^6.0" pytest-cov = "^2.7" pytest-mock = "^1.10" pep8-naming = "^0.8.2" From 2f9fe8621eb9e29255db64fe77ab0d0aae293f9d Mon Sep 17 00:00:00 2001 From: gofr <32750931+gofr@users.noreply.github.com> Date: Sat, 2 Jan 2021 12:46:10 +0100 Subject: [PATCH 1358/1439] Simplify section_key helper function arguments sorting.section_key() is only used once and all its arguments come from Config. Make it take a single config parameter instead of a bunch of individual settings. --- isort/output.py | 12 +----------- isort/sorting.py | 32 +++++++++++--------------------- 2 files changed, 12 insertions(+), 32 deletions(-) diff --git a/isort/output.py b/isort/output.py index 54253884b..1d51171d3 100644 --- a/isort/output.py +++ b/isort/output.py @@ -94,17 +94,7 @@ def sorted_imports( # only_sections options is not imposed if force_sort_within_sections is True new_section_output = sorting.naturally( new_section_output, - key=partial( - sorting.section_key, - case_sensitive=config.case_sensitive, - honor_case_in_force_sorted_sections=config.honor_case_in_force_sorted_sections, - order_by_type=config.order_by_type, - force_to_top=config.force_to_top, - lexicographical=config.lexicographical, - length_sort=config.length_sort, - reverse_relative=config.reverse_relative, - group_by_package=config.group_by_package, - ), + key=partial(sorting.section_key, config=config), ) # uncollapse comments diff --git a/isort/sorting.py b/isort/sorting.py index 9a4336b8e..2a333f2c5 100644 --- a/isort/sorting.py +++ b/isort/sorting.py @@ -51,52 +51,42 @@ def module_key( return f"{module_name in config.force_to_top and 'A' or 'B'}{prefix}{_length_sort_maybe}" -def section_key( - line: str, - case_sensitive: bool, - honor_case_in_force_sorted_sections: bool, - order_by_type: bool, - force_to_top: List[str], - lexicographical: bool = False, - length_sort: bool = False, - reverse_relative: bool = False, - group_by_package: bool = False, -) -> str: +def section_key(line: str, config: Config) -> str: section = "B" - if reverse_relative and line.startswith("from ."): + if config.reverse_relative and line.startswith("from ."): match = re.match(r"^from (\.+)\s*(.*)", line) if match: # pragma: no cover - regex always matches if line starts with "from ." line = f"from {' '.join(match.groups())}" - if group_by_package and line.strip().startswith("from"): + if config.group_by_package and line.strip().startswith("from"): line = line.split(" import", 1)[0] - if lexicographical: + if config.lexicographical: line = _import_line_intro_re.sub("", _import_line_midline_import_re.sub(".", line)) else: line = re.sub("^from ", "", line) line = re.sub("^import ", "", line) - if line.split(" ")[0] in force_to_top: + if line.split(" ")[0] in config.force_to_top: section = "A" # * If honor_case_in_force_sorted_sections is true, and case_sensitive and # order_by_type are different, only ignore case in part of the line. # * Otherwise, let order_by_type decide the sorting of the whole line. This # is only "correct" if case_sensitive and order_by_type have the same value. - if honor_case_in_force_sorted_sections and case_sensitive != order_by_type: + if config.honor_case_in_force_sorted_sections and config.case_sensitive != config.order_by_type: split_module = line.split(" import ", 1) if len(split_module) > 1: module_name, names = split_module - if not case_sensitive: + if not config.case_sensitive: module_name = module_name.lower() - if not order_by_type: + if not config.order_by_type: names = names.lower() line = " import ".join([module_name, names]) - elif not case_sensitive: + elif not config.case_sensitive: line = line.lower() - elif not order_by_type: + elif not config.order_by_type: line = line.lower() - return f"{section}{len(line) if length_sort else ''}{line}" + return f"{section}{len(line) if config.length_sort else ''}{line}" def naturally(to_sort: Iterable[str], key: Optional[Callable[[str], Any]] = None) -> List[str]: From b27a5dcf039a72ff335fd81e7a313a51aa3b0593 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 10 Jan 2021 23:19:03 -0800 Subject: [PATCH 1359/1439] Add test case for issue #1631 --- tests/unit/test_regressions.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index 89fa09274..a4d98cce3 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -1515,3 +1515,13 @@ def test_isort_adding_second_comma_issue_1621(): ) """ ) + + +def test_isort_shouldnt_duplicate_comments_issue_1631(): + assert isort.check_code( + """ +import a # a comment +import a as b # b comment +""", + show_diff=True, + ) From cc69e6115174b1103b5b3c372e467e8cde0e229d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 11 Jan 2021 22:06:08 -0800 Subject: [PATCH 1360/1439] Update output to take into account difference between as and non as import statements --- isort/parse.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/isort/parse.py b/isort/parse.py index d93f559e2..f4844d506 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -410,9 +410,14 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte f"{top_level_module}.__combined_as__", [] ) else: - attach_comments_to = categorized_comments["straight"].setdefault( - module, [] - ) + if config.remove_redundant_aliases and as_name == module.split(".")[-1]: + attach_comments_to = categorized_comments["straight"].setdefault( + module, [] + ) + else: + attach_comments_to = categorized_comments["straight"].setdefault( + f"{module} as {as_name}" , [] + ) del just_imports[as_index : as_index + 2] if type_of_import == "from": From 433e3b0dcca0d6b9e05910f3c0430ddc46e38a1d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 12 Jan 2021 22:08:40 -0800 Subject: [PATCH 1361/1439] Update output to respect as import comment location --- isort/output.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/isort/output.py b/isort/output.py index 1d51171d3..017ae4d4e 100644 --- a/isort/output.py +++ b/isort/output.py @@ -543,25 +543,25 @@ def _with_straight_imports( import_definition = [] if module in parsed.as_map["straight"]: if parsed.imports[section]["straight"][module]: - import_definition.append(f"{import_type} {module}") + import_definition.append((f"{import_type} {module}", module)) import_definition.extend( - f"{import_type} {module} as {as_import}" + (f"{import_type} {module} as {as_import}", f"{module} as {as_import}") for as_import in parsed.as_map["straight"][module] ) else: - import_definition.append(f"{import_type} {module}") + import_definition.append((f"{import_type} {module}", module)) comments_above = parsed.categorized_comments["above"]["straight"].pop(module, None) if comments_above: output.extend(comments_above) output.extend( with_comments( - parsed.categorized_comments["straight"].get(module), + parsed.categorized_comments["straight"].get(imodule), idef, removed=config.ignore_comments, comment_prefix=config.comment_prefix, ) - for idef in import_definition + for idef, imodule in import_definition ) return output From 804329d7776f3b97b8f248f44d314afb00ce753e Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 12 Jan 2021 22:09:02 -0800 Subject: [PATCH 1362/1439] Update test to use correct spacing for comments --- tests/unit/test_regressions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index a4d98cce3..3d586b2c8 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -1520,8 +1520,8 @@ def test_isort_adding_second_comma_issue_1621(): def test_isort_shouldnt_duplicate_comments_issue_1631(): assert isort.check_code( """ -import a # a comment -import a as b # b comment +import a # a comment +import a as b # b comment """, show_diff=True, ) From c2133b3194e48ad7ba0730648bbbae62b12ec5bd Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 12 Jan 2021 22:21:14 -0800 Subject: [PATCH 1363/1439] Add test for duplicate alias case --- tests/unit/test_regressions.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index 3d586b2c8..7ba1da770 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -1525,3 +1525,15 @@ def test_isort_shouldnt_duplicate_comments_issue_1631(): """, show_diff=True, ) + assert ( + isort.code( + """ +import a # a comment +import a as a # b comment +""", + remove_redundant_aliases=True, + ) + == """ +import a # a comment; b comment +""" + ) From 836302fed1b6bc8720d4248529c989adcc41bbd5 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 12 Jan 2021 22:21:36 -0800 Subject: [PATCH 1364/1439] Clarify it effects straight imports only --- isort/parse.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/isort/parse.py b/isort/parse.py index f4844d506..307015e74 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -410,13 +410,15 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte f"{top_level_module}.__combined_as__", [] ) else: - if config.remove_redundant_aliases and as_name == module.split(".")[-1]: + if type_of_import == "from" or ( + config.remove_redundant_aliases and as_name == module.split(".")[-1] + ): attach_comments_to = categorized_comments["straight"].setdefault( module, [] ) else: attach_comments_to = categorized_comments["straight"].setdefault( - f"{module} as {as_name}" , [] + f"{module} as {as_name}", [] ) del just_imports[as_index : as_index + 2] From 47882c5d3252227783d84ed029691fab0aa6d3cd Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 12 Jan 2021 22:52:52 -0800 Subject: [PATCH 1365/1439] Fix integration test --- tests/integration/test_projects_using_isort.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/integration/test_projects_using_isort.py b/tests/integration/test_projects_using_isort.py index 2258cbfae..17cea29c4 100644 --- a/tests/integration/test_projects_using_isort.py +++ b/tests/integration/test_projects_using_isort.py @@ -61,7 +61,19 @@ def test_habitat_lab(tmpdir): def test_tmuxp(tmpdir): git_clone("https://github.com/tmux-python/tmuxp.git", tmpdir) - run_isort([str(tmpdir), "--skip", "cli.py", "--skip", "test_workspacebuilder.py"]) + run_isort( + [ + str(tmpdir), + "--skip", + "cli.py", + "--skip", + "test_workspacebuilder.py", + "--skip", + "test_cli.py", + "--skip", + "workspacebuilder.py", + ] + ) def test_websockets(tmpdir): From 6230dc3086c8189e8dc873a444d0e26ada068581 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 13 Jan 2021 23:20:10 -0800 Subject: [PATCH 1366/1439] Add Fixed #1631: as import comments can in some cases be duplicated. to changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dcfeb972d..550314067 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. Find out more about isort's release policy [here](https://pycqa.github.io/isort/docs/major_releases/release_policy/). +### 5.8.0 TBD + - Fixed #1631: as import comments can in some cases be duplicated. + ### 5.7.0 December 30th 2020 - Fixed #1612: In rare circumstances an extra comma is added after import and before comment. - Fixed #1593: isort encounters bug in Python 3.6.0. From 2ec47fd8b0e660d12df05b571fe1f23d1d3228c3 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 25 Jan 2021 22:59:40 -0800 Subject: [PATCH 1367/1439] Update project cruft --- .cruft.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.cruft.json b/.cruft.json index dcbd6c8eb..e1aa3cc33 100644 --- a/.cruft.json +++ b/.cruft.json @@ -1,6 +1,6 @@ { "template": "https://github.com/timothycrosley/cookiecutter-python/", - "commit": "ff6836bfaa247c65ff50b39c520ed12d91bf5a20", + "commit": "70bd5343b7321f49ee8d699a94481c5a73d4e380", "context": { "cookiecutter": { "full_name": "Timothy Crosley", From 957321f90835a6d1f1fcceeeffb76f91364bd056 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 30 Jan 2021 22:05:31 -0800 Subject: [PATCH 1368/1439] Add documentation page for pre-commit --- docs/configuration/pre-commit.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 docs/configuration/pre-commit.md diff --git a/docs/configuration/pre-commit.md b/docs/configuration/pre-commit.md new file mode 100644 index 000000000..70eaded47 --- /dev/null +++ b/docs/configuration/pre-commit.md @@ -0,0 +1,32 @@ +Using isort with pre-commit +======== + +isort provides official support for [pre-commit](https://pre-commit.com/). + +### isort pre-commit step + +To use isort's official pre-commit integration add the following config: + +``` + - repo: https://github.com/pycqa/isort + rev: 5.6.3 + hooks: + - id: isort + name: isort (python) + - id: isort + name: isort (cython) + types: [cython] + - id: isort + name: isort (pyi) + types: [pyi] +``` + +under the `repos` section of your projects `.pre-commit-config.yaml` file. + +### seed-isort-config + +Older versions of isort used a lot of magic to determine import placement, that could easily break when running on CI/CD. +To fix this, a utilitiy called `seed-isort-config` was created. Since isort 5 however, the project has drastically improved its placement +logic and ensured a good level of consistency across environments. +If you have a step in your pre-commit config called `seed-isort-config` or similar, it is highly recommend that you remove this. +It is guaranteed to slow things down, and can conflict with isort's own module placement logic. From fdb15cdce7e235ad2181563637c5268f84f0b092 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 30 Jan 2021 22:09:23 -0800 Subject: [PATCH 1369/1439] Add link to pre-commit-docs --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 7b4526700..8d2937523 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ supports formatting Python 2 code too. - [Try isort now from your browser!](https://pycqa.github.io/isort/docs/quick_start/0.-try/) - [Using black? See the isort and black compatiblity guide.](https://pycqa.github.io/isort/docs/configuration/black_compatibility/) +- [isort has official support for pre-commit!](https://pycqa.github.io/isort/docs/configuration/pre-commit/) ![Example Usage](https://raw.github.com/pycqa/isort/develop/example.gif) From 6017f6ef2bd070bac20dce294ab3e2a596b7aff4 Mon Sep 17 00:00:00 2001 From: Pavel Savchenko Date: Wed, 3 Feb 2021 16:05:33 +0000 Subject: [PATCH 1370/1439] Indicate type hint support with a py.typed file fix #1648 --- isort/py.typed | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 isort/py.typed diff --git a/isort/py.typed b/isort/py.typed new file mode 100644 index 000000000..e69de29bb From d10aeefb1bb6355d3ef14cdaa0ee645be9132c5b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 3 Feb 2021 22:33:27 -0800 Subject: [PATCH 1371/1439] Update cruft --- .cruft.json | 2 +- scripts/lint.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.cruft.json b/.cruft.json index e1aa3cc33..6d044f389 100644 --- a/.cruft.json +++ b/.cruft.json @@ -1,6 +1,6 @@ { "template": "https://github.com/timothycrosley/cookiecutter-python/", - "commit": "70bd5343b7321f49ee8d699a94481c5a73d4e380", + "commit": "9be01014cde7ec06bd52415655d52ca30a9e9bcc", "context": { "cookiecutter": { "full_name": "Timothy Crosley", diff --git a/scripts/lint.sh b/scripts/lint.sh index b8eb14da2..992027865 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -7,5 +7,5 @@ poetry run black --target-version py36 --check . poetry run isort --profile hug --check --diff isort/ tests/ poetry run isort --profile hug --check --diff example_isort_formatting_plugin/ poetry run flake8 isort/ tests/ -poetry run safety check +poetry run safety check -i 39462 poetry run bandit -r isort/ -x isort/_vendored From fe4b8c2e915346b7f1b8eb3891df6ddd8b9e7e1c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 3 Feb 2021 23:33:34 -0800 Subject: [PATCH 1372/1439] Add - Pavel Savchenko (@asfaltboy) to acknowledgements --- docs/contributing/4.-acknowledgements.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index 03ddc1e67..bb41e41f4 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -215,6 +215,7 @@ Code Contributors - @dwanderson-intel - Quentin Santos (@qsantos) - @gofr +- Pavel Savchenko (@asfaltboy) Documenters =================== From 1111614a9354f445ebd63e1eb0c34abcbaf2de12 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 3 Feb 2021 23:34:53 -0800 Subject: [PATCH 1373/1439] Update changelog to mention py.typed addition --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 550314067..b169ff6f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ ### 5.8.0 TBD - Fixed #1631: as import comments can in some cases be duplicated. + - Implemented #1648: Export MyPY type hints. ### 5.7.0 December 30th 2020 - Fixed #1612: In rare circumstances an extra comma is added after import and before comment. From 8d6f0b994b0f1917d7c9a195be0b1fc2c17e7bf8 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 3 Feb 2021 23:47:10 -0800 Subject: [PATCH 1374/1439] Fix broken integration test --- tests/integration/test_projects_using_isort.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/integration/test_projects_using_isort.py b/tests/integration/test_projects_using_isort.py index 17cea29c4..5c172f07c 100644 --- a/tests/integration/test_projects_using_isort.py +++ b/tests/integration/test_projects_using_isort.py @@ -113,7 +113,9 @@ def test_poetry(tmpdir): def test_hypothesis(tmpdir): git_clone("https://github.com/HypothesisWorks/hypothesis.git", tmpdir) - run_isort((str(tmpdir), "--skip", "tests")) + run_isort( + (str(tmpdir), "--skip", "tests", "--profile", "black", "--ca", "--project", "hypothesis") + ) def test_pillow(tmpdir): From f89dbc9405e391b02d2bea9dce6eb162bff814b3 Mon Sep 17 00:00:00 2001 From: gofr <32750931+gofr@users.noreply.github.com> Date: Fri, 5 Feb 2021 12:15:51 +0100 Subject: [PATCH 1375/1439] Unit test behavior of vertical grids close to the line length The three vertical grid multi line output modes 4, 5 and 6 use a shared function that makes them all wrap slightly differently when the import lines are right around the line length limit. Add tests to document the behavior and catch regressions. --- tests/unit/test_wrap_modes.py | 46 +++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/unit/test_wrap_modes.py b/tests/unit/test_wrap_modes.py index 29ecfd0df..d43d4956d 100644 --- a/tests/unit/test_wrap_modes.py +++ b/tests/unit/test_wrap_modes.py @@ -1,3 +1,4 @@ +import pytest from hypothesis import given, reject from hypothesis import strategies as st @@ -93,6 +94,51 @@ def test_backslash_grid(): ) +@pytest.mark.parametrize("include_trailing_comma", (False, True)) +@pytest.mark.parametrize("line_length", (18, 19)) +@pytest.mark.parametrize("multi_line_output", (4, 5, 6)) +def test_vertical_grid_size_near_line_length( + multi_line_output: int, + line_length: int, + include_trailing_comma: bool, +): + separator = " " + # Cases where the input should be wrapped: + if ( + # Mode 4 always adds a closing ")", making the imports line 19 chars, + # if include_trailing_comma is True that becomes 20 chars. + (multi_line_output == 4 and line_length < 19 + int(include_trailing_comma)) + # Mode 5 always makes space for a final "," even if include_trailing_comma is False, + # (issue #1634) making the line (seem) 19 chars. + or (multi_line_output == 5 and line_length < 19) + # Mode 6 never makes space for a final "," even if include_trailing_comma is True, + # (issue #1634) making the line (seem) 18 chars, so this doesn't wrap. + ): + separator = "\n " + + test_input = f"from foo import (\n aaaa, bbb,{separator}ccc" + if include_trailing_comma: + test_input += "," + if multi_line_output != 4: + test_input += "\n" + test_input += ")\n" + + try: + assert ( + isort.code( + test_input, + multi_line_output=multi_line_output, + line_length=line_length, + include_trailing_comma=include_trailing_comma, + ) + == test_input + ) + except AssertionError: + if multi_line_output == 4 and include_trailing_comma and line_length == 19: + pytest.xfail("issue #1640") + raise + + # This test code was written by the `hypothesis.extra.ghostwriter` module # and is provided under the Creative Commons Zero public domain dedication. From 5e9ad2b867c8bb061bee2ba844a1b26e7fa0dcab Mon Sep 17 00:00:00 2001 From: gofr <32750931+gofr@users.noreply.github.com> Date: Sat, 9 Jan 2021 19:10:53 +0100 Subject: [PATCH 1376/1439] Respect line_length in vertical grid modes and deprecate mode 6 In the three vertical grid multi line modes (4, 5 and 6), each mode had a bug where the line length would be overcounted or undercounted by one character when used in combination with an unexpected value for the include_trailing_comma setting. This could cause import lines to exceed the line_length or be wrapped at one less than the specified line_length. Count the trailing characters correctly. This also deprecates mode 6, since the only difference between this mode and mode 5 is in how it handles include_trailing_comma. The distinction is no longer relevant, since mode 5 can now handle both possible values of include_trailing_comma. For backwards compatibility, make mode 6 an alias for mode 5. --- README.md | 15 +------- isort/main.py | 2 +- isort/settings.py | 2 + isort/wrap_modes.py | 18 ++++----- tests/unit/test_isort.py | 2 +- tests/unit/test_settings.py | 4 ++ tests/unit/test_wrap_modes.py | 72 ++++++----------------------------- 7 files changed, 31 insertions(+), 84 deletions(-) diff --git a/README.md b/README.md index 8d2937523..960901953 100644 --- a/README.md +++ b/README.md @@ -221,20 +221,9 @@ from third_party import ( ) ``` -**6 - Hanging Grid Grouped, No Trailing Comma** +**6 - Hanging Grid Grouped** -In Mode 5 isort leaves a single extra space to maintain consistency of -output when a comma is added at the end. Mode 6 is the same - except -that no extra space is maintained leading to the possibility of lines -one character longer. You can enforce a trailing comma by using this in -conjunction with `-tc` or `include_trailing_comma: True`. - -```python -from third_party import ( - lib1, lib2, lib3, lib4, - lib5 -) -``` +Same as Mode 5. Deprecated. **7 - NOQA** diff --git a/isort/main.py b/isort/main.py index 39dbf19b4..1a7eb8046 100644 --- a/isort/main.py +++ b/isort/main.py @@ -472,7 +472,7 @@ def _build_arg_parser() -> argparse.ArgumentParser: + [str(mode.value) for mode in WrapModes.__members__.values()], type=str, help="Multi line output (0-grid, 1-vertical, 2-hanging, 3-vert-hanging, 4-vert-grid, " - "5-vert-grid-grouped, 6-vert-grid-grouped-no-comma, 7-noqa, " + "5-vert-grid-grouped, 6-deprecated-alias-for-5, 7-noqa, " "8-vertical-hanging-indent-bracket, 9-vertical-prefix-from-module-import, " "10-hanging-indent-with-parentheses).", ) diff --git a/isort/settings.py b/isort/settings.py index b4a29647b..c2c79cb63 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -234,6 +234,8 @@ def __post_init__(self): self, "known_standard_library", frozenset(getattr(stdlibs, self.py_version).stdlib) ) + if self.multi_line_output == WrapModes.VERTICAL_GRID_GROUPED_NO_COMMA: + object.__setattr__(self, "multi_line_output", WrapModes.VERTICAL_GRID_GROUPED) if self.force_alphabetical_sort: object.__setattr__(self, "force_alphabetical_sort_within_sections", True) object.__setattr__(self, "no_sections", True) diff --git a/isort/wrap_modes.py b/isort/wrap_modes.py index 5c2695263..5ced3713c 100644 --- a/isort/wrap_modes.py +++ b/isort/wrap_modes.py @@ -201,9 +201,11 @@ def _vertical_grid_common(need_trailing_char: bool, **interface): next_import = interface["imports"].pop(0) next_statement = f"{interface['statement']}, {next_import}" current_line_length = len(next_statement.split(interface["line_separator"])[-1]) - if interface["imports"] or need_trailing_char: - # If we have more interface["imports"] we need to account for a comma after this import - # We might also need to account for a closing ) we're going to add. + if interface["imports"] or interface["include_trailing_comma"]: + # We need to account for a comma after this import. + current_line_length += 1 + if not interface["imports"] and need_trailing_char: + # We need to account for a closing ) we're going to add. current_line_length += 1 if current_line_length > interface["line_length"]: next_statement = ( @@ -224,7 +226,7 @@ def vertical_grid(**interface) -> str: @_wrap_mode def vertical_grid_grouped(**interface): return ( - _vertical_grid_common(need_trailing_char=True, **interface) + _vertical_grid_common(need_trailing_char=False, **interface) + interface["line_separator"] + ")" ) @@ -232,11 +234,9 @@ def vertical_grid_grouped(**interface): @_wrap_mode def vertical_grid_grouped_no_comma(**interface): - return ( - _vertical_grid_common(need_trailing_char=False, **interface) - + interface["line_separator"] - + ")" - ) + # This is a deprecated alias for vertical_grid_grouped above. This function + # needs to exist for backwards compatibility but should never get called. + raise NotImplementedError @_wrap_mode diff --git a/tests/unit/test_isort.py b/tests/unit/test_isort.py index 0a9d187a7..e5ab86d31 100644 --- a/tests/unit/test_isort.py +++ b/tests/unit/test_isort.py @@ -481,7 +481,7 @@ def test_output_modes() -> None: test_case = isort.code( code=SINGLE_LINE_LONG_IMPORT, - multi_line_output=WrapModes.VERTICAL_GRID_GROUPED_NO_COMMA, + multi_line_output=WrapModes.VERTICAL_GRID_GROUPED, line_length=40, indent=" ", ) diff --git a/tests/unit/test_settings.py b/tests/unit/test_settings.py index be8b5020b..157a087f4 100644 --- a/tests/unit/test_settings.py +++ b/tests/unit/test_settings.py @@ -6,6 +6,7 @@ from isort import exceptions, settings from isort.settings import Config +from isort.wrap_modes import WrapModes class TestConfig: @@ -91,6 +92,9 @@ def test_src_paths_are_combined_and_deduplicated(self): src_full_paths = (Path(os.getcwd()) / f for f in src_paths) assert Config(src_paths=src_paths * 2).src_paths == tuple(src_full_paths) + def test_deprecated_multi_line_output(self): + assert Config(multi_line_output=6).multi_line_output == WrapModes.VERTICAL_GRID_GROUPED + def test_as_list(): assert settings._as_list([" one "]) == ["one"] diff --git a/tests/unit/test_wrap_modes.py b/tests/unit/test_wrap_modes.py index d43d4956d..c21db9049 100644 --- a/tests/unit/test_wrap_modes.py +++ b/tests/unit/test_wrap_modes.py @@ -96,7 +96,7 @@ def test_backslash_grid(): @pytest.mark.parametrize("include_trailing_comma", (False, True)) @pytest.mark.parametrize("line_length", (18, 19)) -@pytest.mark.parametrize("multi_line_output", (4, 5, 6)) +@pytest.mark.parametrize("multi_line_output", (4, 5)) def test_vertical_grid_size_near_line_length( multi_line_output: int, line_length: int, @@ -108,11 +108,9 @@ def test_vertical_grid_size_near_line_length( # Mode 4 always adds a closing ")", making the imports line 19 chars, # if include_trailing_comma is True that becomes 20 chars. (multi_line_output == 4 and line_length < 19 + int(include_trailing_comma)) - # Mode 5 always makes space for a final "," even if include_trailing_comma is False, - # (issue #1634) making the line (seem) 19 chars. - or (multi_line_output == 5 and line_length < 19) - # Mode 6 never makes space for a final "," even if include_trailing_comma is True, - # (issue #1634) making the line (seem) 18 chars, so this doesn't wrap. + # Modes 5 and 6 only add a comma, if include_trailing_comma is True, + # so their lines are 18 or 19 chars long. + or (multi_line_output != 4 and line_length < 18 + int(include_trailing_comma)) ): separator = "\n " @@ -123,20 +121,15 @@ def test_vertical_grid_size_near_line_length( test_input += "\n" test_input += ")\n" - try: - assert ( - isort.code( - test_input, - multi_line_output=multi_line_output, - line_length=line_length, - include_trailing_comma=include_trailing_comma, - ) - == test_input + assert ( + isort.code( + test_input, + multi_line_output=multi_line_output, + line_length=line_length, + include_trailing_comma=include_trailing_comma, ) - except AssertionError: - if multi_line_output == 4 and include_trailing_comma and line_length == 19: - pytest.xfail("issue #1640") - raise + == test_input + ) # This test code was written by the `hypothesis.extra.ghostwriter` module @@ -471,47 +464,6 @@ def test_fuzz_vertical_grid_grouped( reject() -@given( - statement=st.text(), - imports=st.lists(st.text()), - white_space=st.text(), - indent=st.text(), - line_length=st.integers(), - comments=st.lists(st.text()), - line_separator=st.text(), - comment_prefix=st.text(), - include_trailing_comma=st.booleans(), - remove_comments=st.booleans(), -) -def test_fuzz_vertical_grid_grouped_no_comma( - statement, - imports, - white_space, - indent, - line_length, - comments, - line_separator, - comment_prefix, - include_trailing_comma, - remove_comments, -): - try: - isort.wrap_modes.vertical_grid_grouped_no_comma( - statement=statement, - imports=imports, - white_space=white_space, - indent=indent, - line_length=line_length, - comments=comments, - line_separator=line_separator, - comment_prefix=comment_prefix, - include_trailing_comma=include_trailing_comma, - remove_comments=remove_comments, - ) - except ValueError: - reject() - - @given( statement=st.text(), imports=st.lists(st.text()), From 32f5c2c30dc3dd67f74fa6ebe75aec362b580203 Mon Sep 17 00:00:00 2001 From: gofr <32750931+gofr@users.noreply.github.com> Date: Fri, 5 Feb 2021 16:56:56 +0100 Subject: [PATCH 1377/1439] fixup! Respect line_length in vertical grid modes and deprecate mode 6 --- isort/settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/settings.py b/isort/settings.py index c2c79cb63..584165b99 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -234,8 +234,8 @@ def __post_init__(self): self, "known_standard_library", frozenset(getattr(stdlibs, self.py_version).stdlib) ) - if self.multi_line_output == WrapModes.VERTICAL_GRID_GROUPED_NO_COMMA: - object.__setattr__(self, "multi_line_output", WrapModes.VERTICAL_GRID_GROUPED) + if self.multi_line_output == WrapModes.VERTICAL_GRID_GROUPED_NO_COMMA: # type: ignore + object.__setattr__(self, "multi_line_output", WrapModes.VERTICAL_GRID_GROUPED) # type: ignore if self.force_alphabetical_sort: object.__setattr__(self, "force_alphabetical_sort_within_sections", True) object.__setattr__(self, "no_sections", True) From bd8bd5fb2e7210c06e6a6c87f3c6aa05d48a2409 Mon Sep 17 00:00:00 2001 From: gofr <32750931+gofr@users.noreply.github.com> Date: Fri, 5 Feb 2021 17:10:41 +0100 Subject: [PATCH 1378/1439] squash! fixup! Respect line_length in vertical grid modes and deprecate mode 6 Linting work-around --- isort/settings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/isort/settings.py b/isort/settings.py index 584165b99..62b5f54cd 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -235,7 +235,8 @@ def __post_init__(self): ) if self.multi_line_output == WrapModes.VERTICAL_GRID_GROUPED_NO_COMMA: # type: ignore - object.__setattr__(self, "multi_line_output", WrapModes.VERTICAL_GRID_GROUPED) # type: ignore + vertical_grid_grouped = WrapModes.VERTICAL_GRID_GROUPED # type: ignore + object.__setattr__(self, "multi_line_output", vertical_grid_grouped) if self.force_alphabetical_sort: object.__setattr__(self, "force_alphabetical_sort_within_sections", True) object.__setattr__(self, "no_sections", True) From e7f5e306a400971a6db21892198ebdbffa4249f4 Mon Sep 17 00:00:00 2001 From: gofr <32750931+gofr@users.noreply.github.com> Date: Fri, 5 Feb 2021 17:33:39 +0100 Subject: [PATCH 1379/1439] Don't print skipped/broken files with quiet config setting When using --quiet, no warnings are printed about skipped and broken paths. But you do get warnings when using the "quiet" setting in a config file. Be consistent and don't print the warnings then either. --- isort/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/isort/main.py b/isort/main.py index 39dbf19b4..599913994 100644 --- a/isort/main.py +++ b/isort/main.py @@ -1110,7 +1110,7 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = is_no_attempt = False num_skipped += len(skipped) - if num_skipped and not arguments.get("quiet", False): + if num_skipped and not config.quiet: if config.verbose: for was_skipped in skipped: warn( @@ -1120,7 +1120,7 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = print(f"Skipped {num_skipped} files") num_broken += len(broken) - if num_broken and not arguments.get("quiet", False): + if num_broken and not config.quiet: if config.verbose: for was_broken in broken: warn(f"{was_broken} was broken path, make sure it exists correctly") From 60dec54fc534e9085e3193c2ca9f75d97bcd19ef Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 6 Feb 2021 15:00:30 -0800 Subject: [PATCH 1380/1439] Fix coding error found by deepsource --- isort/settings.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/isort/settings.py b/isort/settings.py index 62b5f54cd..bc0782c55 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -672,11 +672,15 @@ def _get_config_data(file_path: str, sections: Tuple[str]) -> Dict[str, Any]: if section.startswith("*.{") and section.endswith("}"): extension = section[len("*.{") : -1] for config_key in config.keys(): - if config_key.startswith("*.{") and config_key.endswith("}"): - if extension in map( + if ( + config_key.startswith("*.{") + and config_key.endswith("}") + and extension + in map( lambda text: text.strip(), config_key[len("*.{") : -1].split(",") - ): - settings.update(config.items(config_key)) + ) + ): + settings.update(config.items(config_key)) elif config.has_section(section): settings.update(config.items(section)) From 929ee15fe8f2e40480434bcf641c1681c7f2ead6 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 7 Feb 2021 22:55:11 -0800 Subject: [PATCH 1381/1439] Made identified imports .statement() runnable code --- CHANGELOG.md | 1 + isort/identify.py | 10 ++++++---- tests/unit/test_isort.py | 8 ++++---- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b169ff6f0..483a13928 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ ### 5.8.0 TBD - Fixed #1631: as import comments can in some cases be duplicated. - Implemented #1648: Export MyPY type hints. + - Implemented #1641: Identified import statements now return runnable code. ### 5.7.0 December 30th 2020 - Fixed #1612: In rare circumstances an extra comma is added after import and before comment. diff --git a/isort/identify.py b/isort/identify.py index ff0282443..6a4f6d7d8 100644 --- a/isort/identify.py +++ b/isort/identify.py @@ -23,12 +23,14 @@ class Import(NamedTuple): file_path: Optional[Path] = None def statement(self) -> str: - full_path = self.module + import_cmd = "cimport" if self.cimport else "import" if self.attribute: - full_path += f".{self.attribute}" + import_string = f"from {self.module} {import_cmd} {self.attribute}" + else: + import_string = f"{import_cmd} {self.module}" if self.alias: - full_path += f" as {self.alias}" - return f"{'cimport' if self.cimport else 'import'} {full_path}" + import_string += f" as {self.alias}" + return import_string def __str__(self): return ( diff --git a/tests/unit/test_isort.py b/tests/unit/test_isort.py index e5ab86d31..39a396b8b 100644 --- a/tests/unit/test_isort.py +++ b/tests/unit/test_isort.py @@ -5032,11 +5032,11 @@ def test_find_imports_in_code() -> None: assert identified_imports == [ ":1 import first_straight", ":3 import second_straight", - ":4 import first_from.first_from_function_1", - ":4 import first_from.first_from_function_2", + ":4 from first_from import first_from_function_1", + ":4 from first_from import first_from_function_2", ":5 import bad_name as good_name", - ":6 import parent.some_bad_defs.bad_name_1 as ok_name_1", - ":6 import parent.some_bad_defs.bad_name_2 as ok_name_2", + ":6 from parent.some_bad_defs import bad_name_1 as ok_name_1", + ":6 from parent.some_bad_defs import bad_name_2 as ok_name_2", ":12 indented import needed_in_bla_2", ":15 indented import needed_in_bla", ":18 indented import needed_in_bla_bla", From 3fcfdd250e2bd5cbf3d7a91427ac5f2adffd5cbf Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 8 Feb 2021 23:36:20 -0800 Subject: [PATCH 1382/1439] Move multi line output documentation to a dedicated documentation page --- README.md | 119 +----------------------------------------------------- 1 file changed, 1 insertion(+), 118 deletions(-) diff --git a/README.md b/README.md index 960901953..85cc8e990 100644 --- a/README.md +++ b/README.md @@ -165,124 +165,7 @@ notified. You will notice above the \"multi\_line\_output\" setting. This setting defines how from imports wrap when they extend past the line\_length -limit and has 12 possible settings: - -**0 - Grid** - -```python -from third_party import (lib1, lib2, lib3, - lib4, lib5, ...) -``` - -**1 - Vertical** - -```python -from third_party import (lib1, - lib2, - lib3 - lib4, - lib5, - ...) -``` - -**2 - Hanging Indent** - -```python -from third_party import \ - lib1, lib2, lib3, \ - lib4, lib5, lib6 -``` - -**3 - Vertical Hanging Indent** - -```python -from third_party import ( - lib1, - lib2, - lib3, - lib4, -) -``` - -**4 - Hanging Grid** - -```python -from third_party import ( - lib1, lib2, lib3, lib4, - lib5, ...) -``` - -**5 - Hanging Grid Grouped** - -```python -from third_party import ( - lib1, lib2, lib3, lib4, - lib5, ... -) -``` - -**6 - Hanging Grid Grouped** - -Same as Mode 5. Deprecated. - -**7 - NOQA** - -```python -from third_party import lib1, lib2, lib3, ... # NOQA -``` - -Alternatively, you can set `force_single_line` to `True` (`-sl` on the -command line) and every import will appear on its own line: - -```python -from third_party import lib1 -from third_party import lib2 -from third_party import lib3 -... -``` - -**8 - Vertical Hanging Indent Bracket** - -Same as Mode 3 - _Vertical Hanging Indent_ but the closing parentheses -on the last line is indented. - -```python -from third_party import ( - lib1, - lib2, - lib3, - lib4, - ) -``` - -**9 - Vertical Prefix From Module Import** - -Starts a new line with the same `from MODULE import ` prefix when lines are longer than the line length limit. - -```python -from third_party import lib1, lib2, lib3 -from third_party import lib4, lib5, lib6 -``` - -**10 - Hanging Indent With Parentheses** - -Same as Mode 2 - _Hanging Indent_ but uses parentheses instead of backslash -for wrapping long lines. - -```python -from third_party import ( - lib1, lib2, lib3, - lib4, lib5, lib6) -``` - -**11 - Backslash Grid** - -Same as Mode 0 - _Grid_ but uses backslashes instead of parentheses to group imports. - -```python -from third_party import lib1, lib2, lib3, \ - lib4, lib5 -``` +limit and has [12 possible settings](https://pycqa.github.io/isort/docs/configuration/multi_line_output_modes/). ## Indentation From 0c8bdf3c5fc8ebffe95cc07aca3ab164f4629c9a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 9 Feb 2021 23:22:59 -0800 Subject: [PATCH 1383/1439] Add wemake profile --- CHANGELOG.md | 1 + docs/configuration/profiles.md | 8 +++ isort/profiles.py | 7 +++ tests/unit/profiles/test_wemake.py | 80 ++++++++++++++++++++++++++++++ 4 files changed, 96 insertions(+) create mode 100644 tests/unit/profiles/test_wemake.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 483a13928..710eccb1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ - Fixed #1631: as import comments can in some cases be duplicated. - Implemented #1648: Export MyPY type hints. - Implemented #1641: Identified import statements now return runnable code. + - Implemented #1661: Added "wemake" profile. ### 5.7.0 December 30th 2020 - Fixed #1612: In rare circumstances an extra comma is added after import and before comment. diff --git a/docs/configuration/profiles.md b/docs/configuration/profiles.md index 9de48e0f7..073f7d60d 100644 --- a/docs/configuration/profiles.md +++ b/docs/configuration/profiles.md @@ -76,3 +76,11 @@ To use any of the listed profiles, use `isort --profile PROFILE_NAME` from the c - **force_grid_wrap**: `0` - **use_parentheses**: `True` - **line_length**: `100` + +#wemake + + + - **multi_line_output**: `3` + - **include_trailing_comma**: `True` + - **use_parentheses**: `True` + - **line_length**: `80` diff --git a/isort/profiles.py b/isort/profiles.py index cb8cb5688..00ebe418e 100644 --- a/isort/profiles.py +++ b/isort/profiles.py @@ -55,6 +55,12 @@ "use_parentheses": True, "line_length": 100, } +wemake = { + "multi_line_output": 3, + "include_trailing_comma": True, + "use_parentheses": True, + "line_length": 80, +} profiles: Dict[str, Dict[str, Any]] = { "black": black, @@ -65,4 +71,5 @@ "plone": plone, "attrs": attrs, "hug": hug, + "wemake": wemake, } diff --git a/tests/unit/profiles/test_wemake.py b/tests/unit/profiles/test_wemake.py new file mode 100644 index 000000000..5e17a6ad6 --- /dev/null +++ b/tests/unit/profiles/test_wemake.py @@ -0,0 +1,80 @@ +"""A set of test cases for the wemake isort profile. + +Snippets are taken directly from the wemake-python-styleguide project here: +https://github.com/wemake-services/wemake-python-styleguide +""" +from functools import partial + +from ..utils import isort_test + +wemake_isort_test = partial(isort_test, profile="wemake", known_first_party=["wemake_python_styleguide"]) + + +def test_wemake_snippet_one(): + wemake_isort_test(""" +import ast +import tokenize +import traceback +from typing import ClassVar, Iterator, Sequence, Type + +from flake8.options.manager import OptionManager +from typing_extensions import final + +from wemake_python_styleguide import constants, types +from wemake_python_styleguide import version as pkg_version +from wemake_python_styleguide.options.config import Configuration +from wemake_python_styleguide.options.validation import validate_options +from wemake_python_styleguide.presets.types import file_tokens as tokens_preset +from wemake_python_styleguide.presets.types import filename as filename_preset +from wemake_python_styleguide.presets.types import tree as tree_preset +from wemake_python_styleguide.transformations.ast_tree import transform +from wemake_python_styleguide.violations import system +from wemake_python_styleguide.visitors import base + +VisitorClass = Type[base.BaseVisitor] +""" + ) + + +def test_wemake_snippet_two(): + wemake_isort_test(""" +from collections import defaultdict +from typing import ClassVar, DefaultDict, List + +from flake8.formatting.base import BaseFormatter +from flake8.statistics import Statistics +from flake8.style_guide import Violation +from pygments import highlight +from pygments.formatters import TerminalFormatter +from pygments.lexers import PythonLexer +from typing_extensions import Final + +from wemake_python_styleguide.version import pkg_version + +#: That url is generated and hosted by Sphinx. +DOCS_URL_TEMPLATE: Final = ( + 'https://wemake-python-stylegui.de/en/{0}/pages/usage/violations/' +) +""") + + +def test_wemake_snippet_three(): + wemake_isort_test(""" +import ast + +from pep8ext_naming import NamingChecker +from typing_extensions import final + +from wemake_python_styleguide.transformations.ast.bugfixes import ( + fix_async_offset, + fix_line_number, +) +from wemake_python_styleguide.transformations.ast.enhancements import ( + set_if_chain, + set_node_context, +) + + +@final +class _ClassVisitor(ast.NodeVisitor): +""") From 146dfe6e9468a784c1177ae7fdce3235c8805d4f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 9 Feb 2021 23:26:02 -0800 Subject: [PATCH 1384/1439] Fix formatting --- tests/unit/profiles/test_wemake.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/tests/unit/profiles/test_wemake.py b/tests/unit/profiles/test_wemake.py index 5e17a6ad6..ad9530131 100644 --- a/tests/unit/profiles/test_wemake.py +++ b/tests/unit/profiles/test_wemake.py @@ -7,11 +7,14 @@ from ..utils import isort_test -wemake_isort_test = partial(isort_test, profile="wemake", known_first_party=["wemake_python_styleguide"]) +wemake_isort_test = partial( + isort_test, profile="wemake", known_first_party=["wemake_python_styleguide"] +) def test_wemake_snippet_one(): - wemake_isort_test(""" + wemake_isort_test( + """ import ast import tokenize import traceback @@ -37,7 +40,8 @@ def test_wemake_snippet_one(): def test_wemake_snippet_two(): - wemake_isort_test(""" + wemake_isort_test( + """ from collections import defaultdict from typing import ClassVar, DefaultDict, List @@ -55,11 +59,13 @@ def test_wemake_snippet_two(): DOCS_URL_TEMPLATE: Final = ( 'https://wemake-python-stylegui.de/en/{0}/pages/usage/violations/' ) -""") +""" + ) def test_wemake_snippet_three(): - wemake_isort_test(""" + wemake_isort_test( + """ import ast from pep8ext_naming import NamingChecker @@ -77,4 +83,5 @@ def test_wemake_snippet_three(): @final class _ClassVisitor(ast.NodeVisitor): -""") +""" + ) From 05cf7ae995f88c10f0c3ea44a23a1f47d0412ef2 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 9 Feb 2021 23:27:35 -0800 Subject: [PATCH 1385/1439] Add multi line output modes documentation dedicated file --- docs/configuration/multi_line_output_modes.md | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 docs/configuration/multi_line_output_modes.md diff --git a/docs/configuration/multi_line_output_modes.md b/docs/configuration/multi_line_output_modes.md new file mode 100644 index 000000000..aed2897e9 --- /dev/null +++ b/docs/configuration/multi_line_output_modes.md @@ -0,0 +1,121 @@ +# Multi Line Output Modes + +This [config option](https://pycqa.github.io/isort/docs/configuration/options/#multi-line-output) defines how from imports wrap when they extend past the line\_length +limit and has 12 possible settings: + +## 0 - Grid + +```python +from third_party import (lib1, lib2, lib3, + lib4, lib5, ...) +``` + +## 1 - Vertical + +```python +from third_party import (lib1, + lib2, + lib3 + lib4, + lib5, + ...) +``` + +## 2 - Hanging Indent + +```python +from third_party import \ + lib1, lib2, lib3, \ + lib4, lib5, lib6 +``` + +## 3 - Vertical Hanging Indent + +```python +from third_party import ( + lib1, + lib2, + lib3, + lib4, +) +``` + +## 4 - Hanging Grid + +```python +from third_party import ( + lib1, lib2, lib3, lib4, + lib5, ...) +``` + +## 5 - Hanging Grid Grouped + +```python +from third_party import ( + lib1, lib2, lib3, lib4, + lib5, ... +) +``` + +## 6 - Hanging Grid Grouped + +Same as Mode 5. Deprecated. + +## 7 - NOQA + +```python +from third_party import lib1, lib2, lib3, ... # NOQA +``` + +Alternatively, you can set `force_single_line` to `True` (`-sl` on the +command line) and every import will appear on its own line: + +```python +from third_party import lib1 +from third_party import lib2 +from third_party import lib3 +... +``` + +## 8 - Vertical Hanging Indent Bracket + +Same as Mode 3 - _Vertical Hanging Indent_ but the closing parentheses +on the last line is indented. + +```python +from third_party import ( + lib1, + lib2, + lib3, + lib4, + ) +``` + +## 9 - Vertical Prefix From Module Import + +Starts a new line with the same `from MODULE import ` prefix when lines are longer than the line length limit. + +```python +from third_party import lib1, lib2, lib3 +from third_party import lib4, lib5, lib6 +``` + +## 10 - Hanging Indent With Parentheses + +Same as Mode 2 - _Hanging Indent_ but uses parentheses instead of backslash +for wrapping long lines. + +```python +from third_party import ( + lib1, lib2, lib3, + lib4, lib5, lib6) +``` + +## 11 - Backslash Grid + +Same as Mode 0 - _Grid_ but uses backslashes instead of parentheses to group imports. + +```python +from third_party import lib1, lib2, lib3, \ + lib4, lib5 +``` From 22d2cc776d19f188db7a223aa75507ba676b1f22 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 9 Feb 2021 23:33:24 -0800 Subject: [PATCH 1386/1439] Make last snippet valid code --- tests/unit/profiles/test_wemake.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/profiles/test_wemake.py b/tests/unit/profiles/test_wemake.py index ad9530131..2d1fb22df 100644 --- a/tests/unit/profiles/test_wemake.py +++ b/tests/unit/profiles/test_wemake.py @@ -82,6 +82,6 @@ def test_wemake_snippet_three(): @final -class _ClassVisitor(ast.NodeVisitor): +class _ClassVisitor(ast.NodeVisitor): ... """ ) From 0d1b8ef6e353a37721814fbedfe7cb78ec57178f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 10 Feb 2021 21:26:12 -0800 Subject: [PATCH 1387/1439] Move custom sections and ordering to a dedicated documentation page --- README.md | 128 +---------------- .../custom_sections_and_ordering.md | 131 ++++++++++++++++++ 2 files changed, 134 insertions(+), 125 deletions(-) create mode 100644 docs/configuration/custom_sections_and_ordering.md diff --git a/README.md b/README.md index 85cc8e990..c42741ef2 100644 --- a/README.md +++ b/README.md @@ -215,132 +215,10 @@ the `-e` option into the command line utility. ## Custom Sections and Ordering -You can change the section order with `sections` option from the default -of: +isort provides configuration options to change almost every aspect of how +imports are organized, ordered, or grouped together in sections. -```ini -FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER -``` - -to your preference (if defined, omitting a default section may cause errors): - -```ini -sections=FUTURE,STDLIB,FIRSTPARTY,THIRDPARTY,LOCALFOLDER -``` - -You also can define your own sections and their order. - -Example: - -```ini -known_django=django -known_pandas=pandas,numpy -sections=FUTURE,STDLIB,DJANGO,THIRDPARTY,PANDAS,FIRSTPARTY,LOCALFOLDER -``` - -would create two new sections with the specified known modules. - -The `no_lines_before` option will prevent the listed sections from being -split from the previous section by an empty line. - -Example: - -```ini -sections=FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER -no_lines_before=LOCALFOLDER -``` - -would produce a section with both FIRSTPARTY and LOCALFOLDER modules -combined. - -**IMPORTANT NOTE**: It is very important to know when setting `known` sections that the naming -does not directly map for historical reasons. For custom settings, the only difference is -capitalization (`known_custom=custom` VS `sections=CUSTOM,...`) for all others reference the -following mapping: - - - `known_standard_library` : `STANDARD_LIBRARY` - - `extra_standard_library` : `STANDARD_LIBRARY` # Like known standard library but appends instead of replacing - - `known_future_library` : `FUTURE` - - `known_first_party`: `FIRSTPARTY` - - `known_third_party`: `THIRDPARTY` - - `known_local_folder`: `LOCALFOLDER` - -This will likely be changed in isort 6.0.0+ in a backwards compatible way. - -## Auto-comment import sections - -Some projects prefer to have import sections uniquely titled to aid in -identifying the sections quickly when visually scanning. isort can -automate this as well. To do this simply set the -`import_heading_{section_name}` setting for each section you wish to -have auto commented - to the desired comment. - -For Example: - -```ini -import_heading_stdlib=Standard Library -import_heading_firstparty=My Stuff -``` - -Would lead to output looking like the following: - -```python -# Standard Library -import os -import sys - -import django.settings - -# My Stuff -import myproject.test -``` - -## Ordering by import length - -isort also makes it easy to sort your imports by length, simply by -setting the `length_sort` option to `True`. This will result in the -following output style: - -```python -from evn.util import ( - Pool, - Dict, - Options, - Constant, - DecayDict, - UnexpectedCodePath, -) -``` - -It is also possible to opt-in to sorting imports by length for only -specific sections by using `length_sort_` followed by the section name -as a configuration item, e.g.: - - length_sort_stdlib=1 - -## Controlling how isort sections `from` imports - -By default isort places straight (`import y`) imports above from imports (`from x import y`): - -```python -import b -from a import a # This will always appear below because it is a from import. -``` - -However, if you prefer to keep strict alphabetical sorting you can set [force sort within sections](https://pycqa.github.io/isort/docs/configuration/options/#force-sort-within-sections) to true. Resulting in: - - -```python -from a import a # This will now appear at top because a appears in the alphabet before b -import b -``` - -You can even tell isort to always place from imports on top, instead of the default of placing them on bottom, using [from first](https://pycqa.github.io/isort/docs/configuration/options/#from-first). - -```python -from b import b # If from first is set to True, all from imports will be placed before non-from imports. -import a -``` +[Click here](https://pycqa.github.io/isort/docs/configuration/custom_sections_and_ordering/) for an overview of all these options. ## Skip processing of imports (outside of configuration) diff --git a/docs/configuration/custom_sections_and_ordering.md b/docs/configuration/custom_sections_and_ordering.md new file mode 100644 index 000000000..df145e889 --- /dev/null +++ b/docs/configuration/custom_sections_and_ordering.md @@ -0,0 +1,131 @@ +# Custom Sections and Ordering + +isort provides lots of features to enable configuring how it sections imports +and how it sorts imports within those sections. +You can change the section order with `sections` option from the default +of: + +```ini +FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER +``` + +to your preference (if defined, omitting a default section may cause errors): + +```ini +sections=FUTURE,STDLIB,FIRSTPARTY,THIRDPARTY,LOCALFOLDER +``` + +You also can define your own sections and their order. + +Example: + +```ini +known_django=django +known_pandas=pandas,numpy +sections=FUTURE,STDLIB,DJANGO,THIRDPARTY,PANDAS,FIRSTPARTY,LOCALFOLDER +``` + +would create two new sections with the specified known modules. + +The `no_lines_before` option will prevent the listed sections from being +split from the previous section by an empty line. + +Example: + +```ini +sections=FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER +no_lines_before=LOCALFOLDER +``` + +would produce a section with both FIRSTPARTY and LOCALFOLDER modules +combined. + +**IMPORTANT NOTE**: It is very important to know when setting `known` sections that the naming +does not directly map for historical reasons. For custom settings, the only difference is +capitalization (`known_custom=custom` VS `sections=CUSTOM,...`) for all others reference the +following mapping: + + - `known_standard_library` : `STANDARD_LIBRARY` + - `extra_standard_library` : `STANDARD_LIBRARY` # Like known standard library but appends instead of replacing + - `known_future_library` : `FUTURE` + - `known_first_party`: `FIRSTPARTY` + - `known_third_party`: `THIRDPARTY` + - `known_local_folder`: `LOCALFOLDER` + +This will likely be changed in isort 6.0.0+ in a backwards compatible way. + + +## Auto-comment import sections + +Some projects prefer to have import sections uniquely titled to aid in +identifying the sections quickly when visually scanning. isort can +automate this as well. To do this simply set the +`import_heading_{section_name}` setting for each section you wish to +have auto commented - to the desired comment. + +For Example: + +```ini +import_heading_stdlib=Standard Library +import_heading_firstparty=My Stuff +``` + +Would lead to output looking like the following: + +```python +# Standard Library +import os +import sys + +import django.settings + +# My Stuff +import myproject.test +``` + +## Ordering by import length + +isort also makes it easy to sort your imports by length, simply by +setting the `length_sort` option to `True`. This will result in the +following output style: + +```python +from evn.util import ( + Pool, + Dict, + Options, + Constant, + DecayDict, + UnexpectedCodePath, +) +``` + +It is also possible to opt-in to sorting imports by length for only +specific sections by using `length_sort_` followed by the section name +as a configuration item, e.g.: + + length_sort_stdlib=1 + +## Controlling how isort sections `from` imports + +By default isort places straight (`import y`) imports above from imports (`from x import y`): + +```python +import b +from a import a # This will always appear below because it is a from import. +``` + +However, if you prefer to keep strict alphabetical sorting you can set [force sort within sections](https://pycqa.github.io/isort/docs/configuration/options/#force-sort-within-sections) to true. Resulting in: + + +```python +from a import a # This will now appear at top because a appears in the alphabet before b +import b +``` + +You can even tell isort to always place from imports on top, instead of the default of placing them on bottom, using [from first](https://pycqa.github.io/isort/docs/configuration/options/#from-first). + +```python +from b import b # If from first is set to True, all from imports will be placed before non-from imports. +import a +``` From 5f8c5fae9c638bf01e0a8fc61115786a39315e47 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 10 Feb 2021 21:33:51 -0800 Subject: [PATCH 1388/1439] Move githook documentation to dedicated page --- README.md | 30 +----------------------------- docs/configuration/git_hook.md | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 29 deletions(-) create mode 100644 docs/configuration/git_hook.md diff --git a/README.md b/README.md index c42741ef2..6c0f403ba 100644 --- a/README.md +++ b/README.md @@ -310,35 +310,7 @@ Git hook isort provides a hook function that can be integrated into your Git pre-commit script to check Python code before committing. -To cause the commit to fail if there are isort errors (strict mode), -include the following in `.git/hooks/pre-commit`: - -```python -#!/usr/bin/env python -import sys -from isort.hooks import git_hook - -sys.exit(git_hook(strict=True, modify=True, lazy=True, settings_file="")) -``` - -If you just want to display warnings, but allow the commit to happen -anyway, call `git_hook` without the strict parameter. If you want to -display warnings, but not also fix the code, call `git_hook` without the -modify parameter. -The `lazy` argument is to support users who are "lazy" to add files -individually to the index and tend to use `git commit -a` instead. -Set it to `True` to ensure all tracked files are properly isorted, -leave it out or set it to `False` to check only files added to your -index. - -If you want to use a specific configuration file for the hook, you can pass its -path to settings_file. If no path is specifically requested, `git_hook` will -search for the configuration file starting at the directory containing the first -staged file, as per `git diff-index` ordering, and going upward in the directory -structure until a valid configuration file is found or -[`MAX_CONFIG_SEARCH_DEPTH`](src/config.py:35) directories are checked. -The settings_file parameter is used to support users who keep their configuration -file in a directory that might not be a parent of all the other files. +[More info here.](https://pycqa.github.io/isort/docs/configuration/git_hook/) ## Setuptools integration diff --git a/docs/configuration/git_hook.md b/docs/configuration/git_hook.md new file mode 100644 index 000000000..6fb514421 --- /dev/null +++ b/docs/configuration/git_hook.md @@ -0,0 +1,34 @@ +# Git Hook + +isort provides a hook function that can be integrated into your Git +pre-commit script to check Python code before committing. + +To cause the commit to fail if there are isort errors (strict mode), +include the following in `.git/hooks/pre-commit`: + +```python +#!/usr/bin/env python +import sys +from isort.hooks import git_hook + +sys.exit(git_hook(strict=True, modify=True, lazy=True, settings_file="")) +``` + +If you just want to display warnings, but allow the commit to happen +anyway, call `git_hook` without the strict parameter. If you want to +display warnings, but not also fix the code, call `git_hook` without the +modify parameter. +The `lazy` argument is to support users who are "lazy" to add files +individually to the index and tend to use `git commit -a` instead. +Set it to `True` to ensure all tracked files are properly isorted, +leave it out or set it to `False` to check only files added to your +index. + +If you want to use a specific configuration file for the hook, you can pass its +path to settings_file. If no path is specifically requested, `git_hook` will +search for the configuration file starting at the directory containing the first +staged file, as per `git diff-index` ordering, and going upward in the directory +structure until a valid configuration file is found or +[`MAX_CONFIG_SEARCH_DEPTH`](src/config.py:35) directories are checked. +The settings_file parameter is used to support users who keep their configuration +file in a directory that might not be a parent of all the other files. From 8a9ad28563403edec26d1eced173e4e679cc6439 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 10 Feb 2021 21:40:28 -0800 Subject: [PATCH 1389/1439] Move add or remove imports to dedicated documentation page --- README.md | 29 +++------------------ docs/configuration/add_or_remove_imports.md | 28 ++++++++++++++++++++ 2 files changed, 31 insertions(+), 26 deletions(-) create mode 100644 docs/configuration/add_or_remove_imports.md diff --git a/README.md b/README.md index 6c0f403ba..dd25c66ac 100644 --- a/README.md +++ b/README.md @@ -251,34 +251,11 @@ import b import a ``` -## Adding an import to multiple files +## Adding or removing an import from multiple files -isort makes it easy to add an import statement across multiple files, -while being assured it's correctly placed. +isort can be ran or configured to add / remove imports automatically. -To add an import to all files: - -```bash -isort -a "from __future__ import print_function" *.py -``` - -To add an import only to files that already have imports: - -```bash -isort -a "from __future__ import print_function" --append-only *.py -``` - - -## Removing an import from multiple files - -isort also makes it easy to remove an import from multiple files, -without having to be concerned with how it was originally formatted. - -From the command line: - -```bash -isort --rm "os.system" *.py -``` +[See a complete guide here.]((https://pycqa.github.io/isort/docs/configuration/add_or_remove_imports/) ## Using isort to verify code diff --git a/docs/configuration/add_or_remove_imports.md b/docs/configuration/add_or_remove_imports.md new file mode 100644 index 000000000..d43286e95 --- /dev/null +++ b/docs/configuration/add_or_remove_imports.md @@ -0,0 +1,28 @@ + +## Adding an import to multiple files +isort makes it easy to add an import statement across multiple files, +while being assured it's correctly placed. + +To add an import to all files: + +```bash +isort -a "from __future__ import print_function" *.py +``` + +To add an import only to files that already have imports: + +```bash +isort -a "from __future__ import print_function" --append-only *.py +``` + + +## Removing an import from multiple files + +isort also makes it easy to remove an import from multiple files, +without having to be concerned with how it was originally formatted. + +From the command line: + +```bash +isort --rm "os.system" *.py +``` From 2d13d5ad07ecff5a15e905a50cdbe5413921ebf9 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 10 Feb 2021 21:42:26 -0800 Subject: [PATCH 1390/1439] Move setuptools integration to dedicated documentation page --- README.md | 26 ++----------------- docs/configuration/setuptools_integration.md | 27 ++++++++++++++++++++ 2 files changed, 29 insertions(+), 24 deletions(-) create mode 100644 docs/configuration/setuptools_integration.md diff --git a/README.md b/README.md index dd25c66ac..54b6d324f 100644 --- a/README.md +++ b/README.md @@ -281,8 +281,7 @@ this one by \@acdha: This can help to ensure a certain level of code quality throughout a project. -Git hook --------- +## Git hook isort provides a hook function that can be integrated into your Git pre-commit script to check Python code before committing. @@ -294,28 +293,7 @@ pre-commit script to check Python code before committing. Upon installation, isort enables a `setuptools` command that checks Python files declared by your project. -Running `python setup.py isort` on the command line will check the files -listed in your `py_modules` and `packages`. If any warning is found, the -command will exit with an error code: - -```bash -$ python setup.py isort -``` - -Also, to allow users to be able to use the command without having to -install isort themselves, add isort to the setup\_requires of your -`setup()` like so: - -```python -setup( - name="project", - packages=["project"], - - setup_requires=[ - "isort" - ] -) -``` +[More info here.](https://pycqa.github.io/isort/docs/configuration/setuptools_integration/) ## Spread the word diff --git a/docs/configuration/setuptools_integration.md b/docs/configuration/setuptools_integration.md new file mode 100644 index 000000000..ca2dbe568 --- /dev/null +++ b/docs/configuration/setuptools_integration.md @@ -0,0 +1,27 @@ +# Setuptools integration + +Upon installation, isort enables a `setuptools` command that checks +Python files declared by your project. + +Running `python setup.py isort` on the command line will check the files +listed in your `py_modules` and `packages`. If any warning is found, the +command will exit with an error code: + +```bash +$ python setup.py isort +``` + +Also, to allow users to be able to use the command without having to +install isort themselves, add isort to the setup\_requires of your +`setup()` like so: + +```python +setup( + name="project", + packages=["project"], + + setup_requires=[ + "isort" + ] +) +``` From 783f3e85545684d970d584a8690c0c18d4b355bf Mon Sep 17 00:00:00 2001 From: gofr <32750931+gofr@users.noreply.github.com> Date: Thu, 11 Feb 2021 17:42:11 +0100 Subject: [PATCH 1391/1439] Sort relative imports correctly with force_sort_within_sections Relative import sort order when using force_sort_within_sections was inconsistent with the order without that setting. Change the force_sort_within_sections sort order to match. This fixes the relative import ordering issues noted in #1659. --- isort/sorting.py | 6 ++---- tests/unit/test_isort.py | 33 +++++++++++++++++++++++++++++++++ tests/unit/test_regressions.py | 8 ++++---- 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/isort/sorting.py b/isort/sorting.py index 2a333f2c5..3c61955f8 100644 --- a/isort/sorting.py +++ b/isort/sorting.py @@ -54,10 +54,6 @@ def module_key( def section_key(line: str, config: Config) -> str: section = "B" - if config.reverse_relative and line.startswith("from ."): - match = re.match(r"^from (\.+)\s*(.*)", line) - if match: # pragma: no cover - regex always matches if line starts with "from ." - line = f"from {' '.join(match.groups())}" if config.group_by_package and line.strip().startswith("from"): line = line.split(" import", 1)[0] @@ -66,6 +62,8 @@ def section_key(line: str, config: Config) -> str: else: line = re.sub("^from ", "", line) line = re.sub("^import ", "", line) + sep = " " if config.reverse_relative else "_" + line = re.sub(r"^(\.+)", fr"\1{sep}", line) if line.split(" ")[0] in config.force_to_top: section = "A" # * If honor_case_in_force_sorted_sections is true, and case_sensitive and diff --git a/tests/unit/test_isort.py b/tests/unit/test_isort.py index 39a396b8b..6ba716d91 100644 --- a/tests/unit/test_isort.py +++ b/tests/unit/test_isort.py @@ -2914,6 +2914,39 @@ def test_sort_within_sections_with_force_to_top_issue_473() -> None: ) +def test_force_sort_within_sections_with_relative_imports_issue_1659() -> None: + """Ensure relative imports are sorted within sections""" + assert isort.check_code( + """from .. import a +from ..alpha.beta import b +from ..omega import c +import . +from . import foo +import .apple as bar +from .mango import baz +""", + show_diff=True, + force_sort_within_sections=True, + ) + + +def test_force_sort_within_sections_with_reverse_relative_imports_issue_1659() -> None: + """Ensure reverse ordered relative imports are sorted within sections""" + assert isort.check_code( + """import . +from . import foo +import .apple as bar +from .mango import baz +from .. import a +from ..alpha.beta import b +from ..omega import c +""", + show_diff=True, + force_sort_within_sections=True, + reverse_relative=True, + ) + + def test_correct_number_of_new_lines_with_comment_issue_435() -> None: """Test to ensure that injecting a comment in-between imports doesn't mess up the new line spacing diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index 7ba1da770..eb571c218 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -331,18 +331,18 @@ def test_isort_shouldnt_add_extra_new_line_when_fass_and_n_issue_1315(): assert ( isort.code( """ -from . import foo -# Comment canary from .. import foo +# Comment canary +from . import foo """, ensure_newline_before_comments=True, force_sort_within_sections=True, ) == """ -from . import foo +from .. import foo # Comment canary -from .. import foo +from . import foo """ ) From d8394e74692a078c479dedd3a303079ff89c3c87 Mon Sep 17 00:00:00 2001 From: hirosassa Date: Sat, 13 Feb 2021 05:08:05 +0900 Subject: [PATCH 1392/1439] fix typo on README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 54b6d324f..d9c86ff67 100644 --- a/README.md +++ b/README.md @@ -255,7 +255,7 @@ import a isort can be ran or configured to add / remove imports automatically. -[See a complete guide here.]((https://pycqa.github.io/isort/docs/configuration/add_or_remove_imports/) +[See a complete guide here.](https://pycqa.github.io/isort/docs/configuration/add_or_remove_imports/) ## Using isort to verify code From 6e74fe8c91a8b5e989e402838415a6879c250d65 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 12 Feb 2021 22:47:19 -0800 Subject: [PATCH 1393/1439] Add - @hirosassa to acknowledgements --- docs/contributing/4.-acknowledgements.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index bb41e41f4..b1fc86c9e 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -239,6 +239,7 @@ Documenters - Abtin (@abtinmo) - @scottwedge - Hasan Ramezani (@hramezani) +- @hirosassa -------------------------------------------- From f89e80277868f135ed1da70f757c41efc14e530c Mon Sep 17 00:00:00 2001 From: gofr <32750931+gofr@users.noreply.github.com> Date: Sun, 14 Feb 2021 12:39:01 +0100 Subject: [PATCH 1394/1439] Add new option for sorting relative imports in force-sorted sections Add --sort-relative-in-force-sorted-sections to make sorting of --force-sort-within-sections consistent with the way imports are sorted without force-sorted sections. Add tests for both the old and new behaviors. --- isort/main.py | 8 ++++++++ isort/settings.py | 1 + isort/sorting.py | 13 +++++++++++-- tests/unit/test_isort.py | 39 +++++++++++++++++++++++++++++++++++++-- 4 files changed, 57 insertions(+), 4 deletions(-) diff --git a/isort/main.py b/isort/main.py index 6824bae73..14fcd487c 100644 --- a/isort/main.py +++ b/isort/main.py @@ -668,6 +668,14 @@ def _build_arg_parser() -> argparse.ArgumentParser: help="Honor `--case-sensitive` when `--force-sort-within-sections` is being used. " "Without this option set, `--order-by-type` decides module name ordering too.", ) + section_group.add_argument( + "--srss", + "--sort-relative-in-force-sorted-sections", + action="store_true", + dest="sort_relative_in_force_sorted_sections", + help="When using `--force-sort-within-sections`, sort relative imports the same " + "way as they are sorted when not using that setting.", + ) section_group.add_argument( "--fass", "--force-alphabetical-sort-within-sections", diff --git a/isort/settings.py b/isort/settings.py index bc0782c55..dd6536128 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -206,6 +206,7 @@ class _Config: follow_links: bool = True indented_import_headings: bool = True honor_case_in_force_sorted_sections: bool = False + sort_relative_in_force_sorted_sections: bool = False def __post_init__(self): py_version = self.py_version diff --git a/isort/sorting.py b/isort/sorting.py index 3c61955f8..2cfe60bf5 100644 --- a/isort/sorting.py +++ b/isort/sorting.py @@ -54,6 +54,14 @@ def module_key( def section_key(line: str, config: Config) -> str: section = "B" + if ( + not config.sort_relative_in_force_sorted_sections + and config.reverse_relative + and line.startswith("from .") + ): + match = re.match(r"^from (\.+)\s*(.*)", line) + if match: # pragma: no cover - regex always matches if line starts with "from ." + line = f"from {' '.join(match.groups())}" if config.group_by_package and line.strip().startswith("from"): line = line.split(" import", 1)[0] @@ -62,8 +70,9 @@ def section_key(line: str, config: Config) -> str: else: line = re.sub("^from ", "", line) line = re.sub("^import ", "", line) - sep = " " if config.reverse_relative else "_" - line = re.sub(r"^(\.+)", fr"\1{sep}", line) + if config.sort_relative_in_force_sorted_sections: + sep = " " if config.reverse_relative else "_" + line = re.sub(r"^(\.+)", fr"\1{sep}", line) if line.split(" ")[0] in config.force_to_top: section = "A" # * If honor_case_in_force_sorted_sections is true, and case_sensitive and diff --git a/tests/unit/test_isort.py b/tests/unit/test_isort.py index 6ba716d91..19452db6d 100644 --- a/tests/unit/test_isort.py +++ b/tests/unit/test_isort.py @@ -2914,7 +2914,40 @@ def test_sort_within_sections_with_force_to_top_issue_473() -> None: ) -def test_force_sort_within_sections_with_relative_imports_issue_1659() -> None: +def test_force_sort_within_sections_with_relative_imports() -> None: + """Test sorting of relative imports with force_sort_within_sections=True""" + assert isort.check_code( + """import . +from . import foo +from .. import a +from ..alpha.beta import b +from ..omega import c +import .apple as bar +from .mango import baz +""", + show_diff=True, + force_sort_within_sections=True, + ) + + +def test_force_sort_within_sections_with_reverse_relative_imports() -> None: + """Test reverse sorting of relative imports with force_sort_within_sections=True""" + assert isort.check_code( + """import . +from . import foo +from .mango import baz +from ..alpha.beta import b +from .. import a +from ..omega import c +import .apple as bar +""", + show_diff=True, + force_sort_within_sections=True, + reverse_relative=True, + ) + + +def test_sort_relative_in_force_sorted_sections_issue_1659() -> None: """Ensure relative imports are sorted within sections""" assert isort.check_code( """from .. import a @@ -2927,10 +2960,11 @@ def test_force_sort_within_sections_with_relative_imports_issue_1659() -> None: """, show_diff=True, force_sort_within_sections=True, + sort_relative_in_force_sorted_sections=True, ) -def test_force_sort_within_sections_with_reverse_relative_imports_issue_1659() -> None: +def test_reverse_sort_relative_in_force_sorted_sections_issue_1659() -> None: """Ensure reverse ordered relative imports are sorted within sections""" assert isort.check_code( """import . @@ -2943,6 +2977,7 @@ def test_force_sort_within_sections_with_reverse_relative_imports_issue_1659() - """, show_diff=True, force_sort_within_sections=True, + sort_relative_in_force_sorted_sections=True, reverse_relative=True, ) From c26296fee8dc386e7673eaeb7acf0efff0013a8d Mon Sep 17 00:00:00 2001 From: gofr <32750931+gofr@users.noreply.github.com> Date: Sun, 14 Feb 2021 13:05:00 +0100 Subject: [PATCH 1395/1439] squash! Add new option for sorting relative imports in force-sorted sections Restore unrelated test to original --- tests/unit/test_regressions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index eb571c218..7ba1da770 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -331,18 +331,18 @@ def test_isort_shouldnt_add_extra_new_line_when_fass_and_n_issue_1315(): assert ( isort.code( """ -from .. import foo -# Comment canary from . import foo +# Comment canary +from .. import foo """, ensure_newline_before_comments=True, force_sort_within_sections=True, ) == """ -from .. import foo +from . import foo # Comment canary -from . import foo +from .. import foo """ ) From 0e892a85ab76968dca754487f2f6370dbd11caff Mon Sep 17 00:00:00 2001 From: David Poznik Date: Tue, 16 Feb 2021 15:55:45 -0800 Subject: [PATCH 1396/1439] Add clarifying note on globstar to README.md --- README.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d9c86ff67..fa6e26ea3 100644 --- a/README.md +++ b/README.md @@ -104,37 +104,40 @@ pip install isort[requirements_deprecated_finder,pipfile_deprecated_finder] **From the command line**: +To run on specific files: + ```bash isort mypythonfile.py mypythonfile2.py ``` -or recursively: +To apply recursively: ```bash isort . ``` -*which is equivalent to:* +If [globstar](https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html) +is enabled, `isort .` is equivalent to: ```bash isort **/*.py ``` -or to see the proposed changes without applying them: +To view proposed changes without applying them: ```bash isort mypythonfile.py --diff ``` Finally, to atomically run isort against a project, only applying -changes if they don't introduce syntax errors do: +changes if they don't introduce syntax errors: ```bash isort --atomic . ``` -(Note: this is disabled by default as it keeps isort from being able to -run against code written using a different version of Python) +(Note: this is disabled by default, as it prevents isort from +running against code written using a different version of Python.) **From within Python**: From c1ebff5d8603985f3819ac97ade756b6510d1ebe Mon Sep 17 00:00:00 2001 From: Marco Lam Date: Wed, 17 Feb 2021 11:03:18 +0800 Subject: [PATCH 1397/1439] Fix not replacing the source file if only literals are changed but not the imports --- isort/core.py | 50 +++++++++++++++++----------- tests/unit/test_ticketed_features.py | 39 +++++++++++++++++++++- 2 files changed, 68 insertions(+), 21 deletions(-) diff --git a/isort/core.py b/isort/core.py index a37b707e4..ae32a1dd0 100644 --- a/isort/core.py +++ b/isort/core.py @@ -125,17 +125,22 @@ def process( line_separator = "\n" if code_sorting and code_sorting_section: - output_stream.write( - textwrap.indent( - isort.literal.assignment( - code_sorting_section, - str(code_sorting), - extension, - config=_indented_config(config, indent), - ), - code_sorting_indent, - ) + sorted_code = textwrap.indent( + isort.literal.assignment( + code_sorting_section, + str(code_sorting), + extension, + config=_indented_config(config, indent), + ), + code_sorting_indent, ) + made_changes = made_changes or _has_changed( + before=code_sorting_section, + after=sorted_code, + line_separator=line_separator, + ignore_whitespace=config.ignore_whitespace, + ) + output_stream.write(sorted_code) else: stripped_line = line.strip() if stripped_line and not line_separator: @@ -198,17 +203,22 @@ def process( not_imports = True elif code_sorting: if not stripped_line: - output_stream.write( - textwrap.indent( - isort.literal.assignment( - code_sorting_section, - str(code_sorting), - extension, - config=_indented_config(config, indent), - ), - code_sorting_indent, - ) + sorted_code = textwrap.indent( + isort.literal.assignment( + code_sorting_section, + str(code_sorting), + extension, + config=_indented_config(config, indent), + ), + code_sorting_indent, + ) + made_changes = made_changes or _has_changed( + before=code_sorting_section, + after=sorted_code, + line_separator=line_separator, + ignore_whitespace=config.ignore_whitespace, ) + output_stream.write(sorted_code) not_imports = True code_sorting = False code_sorting_section = "" diff --git a/tests/unit/test_ticketed_features.py b/tests/unit/test_ticketed_features.py index 03998a474..130ef67c7 100644 --- a/tests/unit/test_ticketed_features.py +++ b/tests/unit/test_ticketed_features.py @@ -7,7 +7,7 @@ import pytest import isort -from isort import Config, exceptions +from isort import Config, api, exceptions def test_semicolon_ignored_for_dynamic_lines_after_import_issue_1178(): @@ -483,6 +483,43 @@ def method(): # isort: dict y = {"b": "c", "z": "z"}""" ) + assert api.sort_stream( + input_stream=StringIO( + """ +import a +import x + +# isort: list +__all__ = ["b", "a", "b"] + +# isort: unique-list +__all__ = ["b", "a", "b"] + +# isort: tuple +__all__ = ("b", "a", "b") + +# isort: unique-tuple +__all__ = ("b", "a", "b") + +# isort: set +__all__ = {"b", "a", "b"} + + +def method(): + # isort: list + x = ["b", "a"] + + +# isort: assignments +d = 1 +b = 2 +a = 3 + +# isort: dict +y = {"z": "z", "b": "b", "b": "c"}""", + ), + output_stream=StringIO(), + ) def test_isort_allows_setting_import_types_issue_1181(): From 5f15873c0b77be6f71b56324becd9832b6dae800 Mon Sep 17 00:00:00 2001 From: Marco Lam Date: Wed, 17 Feb 2021 13:13:24 +0800 Subject: [PATCH 1398/1439] Merge if statments --- isort/core.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/isort/core.py b/isort/core.py index ae32a1dd0..598789984 100644 --- a/isort/core.py +++ b/isort/core.py @@ -159,10 +159,11 @@ def process( and stripped_line not in config.section_comments ): in_top_comment = True - elif in_top_comment: - if not line.startswith("#") or stripped_line in config.section_comments: - in_top_comment = False - first_comment_index_end = index - 1 + elif in_top_comment and ( + not line.startswith("#") or stripped_line in config.section_comments + ): + in_top_comment = False + first_comment_index_end = index - 1 was_in_quote = bool(in_quote) if (not stripped_line.startswith("#") or in_quote) and '"' in line or "'" in line: From c025ad91b70fcf59fe494e0576539a28aafc0875 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 16 Feb 2021 22:09:10 -0800 Subject: [PATCH 1399/1439] David Poznik (@dpoznik) to contributors --- docs/contributing/4.-acknowledgements.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index b1fc86c9e..15fff2a12 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -240,6 +240,7 @@ Documenters - @scottwedge - Hasan Ramezani (@hramezani) - @hirosassa +- David Poznik (@dpoznik) -------------------------------------------- From 25d1095de4d5ed139831cffa7ded533dbc2f861b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 17 Feb 2021 23:56:32 -0800 Subject: [PATCH 1400/1439] Add test case for issue #1670 --- tests/unit/test_regressions.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index 7ba1da770..749bf6307 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -1537,3 +1537,34 @@ def test_isort_shouldnt_duplicate_comments_issue_1631(): import a # a comment; b comment """ ) + + +def test_isort_shouldnt_add_extra_new_lines_with_import_heading_issue_1670(): + snippet = """#!/usr/bin/python3 -ttu +# Standard Library +import argparse +import datetime + +import attr +import requests + + +def foo() -> int: + print("Hello world") + return 0 + + +def spam(): + + + # Standard Library + import collections + import logging +""" + assert ( + isort.code( + snippet, + import_heading_stdlib="Standard Library", + ) + == snippet + ) From f903034a584c5ef3c67cae5aa0aef1b7cc2db194 Mon Sep 17 00:00:00 2001 From: dongfangtianyu <7629022+dongfangtianyu@users.noreply.github.com> Date: Wed, 24 Feb 2021 13:41:30 +0800 Subject: [PATCH 1401/1439] fix :Failed to pull configuration information from pyproject.toml --- isort/settings.py | 2 +- tests/unit/test_settings.py | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/isort/settings.py b/isort/settings.py index bc0782c55..1ea12179e 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -647,7 +647,7 @@ def _find_config(path: str) -> Tuple[str, Dict[str, Any]]: def _get_config_data(file_path: str, sections: Tuple[str]) -> Dict[str, Any]: settings: Dict[str, Any] = {} - with open(file_path) as config_file: + with open(file_path, encoding="utf-8") as config_file: if file_path.endswith(".toml"): config = toml.load(config_file) for section in sections: diff --git a/tests/unit/test_settings.py b/tests/unit/test_settings.py index 157a087f4..6ef8f70ec 100644 --- a/tests/unit/test_settings.py +++ b/tests/unit/test_settings.py @@ -189,6 +189,31 @@ def test_editorconfig_without_sections(tmpdir): assert not loaded_settings +def test_get_config_data_with_toml_and_utf8(tmpdir): + test_config = tmpdir.join("pyproject.toml") + # Exception: UnicodeDecodeError: 'gbk' codec can't decode byte 0x84 in position 57 + test_config.write_text( + """ +[tool.poetry] + +description = "基于FastAPI + Mysql的 TodoList" # Exception: UnicodeDecodeError +name = "TodoList" +version = "0.1.0" + +[tool.isort] + +multi_line_output = 3 + +""", + "utf8", + ) + loaded_settings = settings._get_config_data( + str(test_config), sections=settings.CONFIG_SECTIONS["pyproject.toml"] + ) + assert loaded_settings + assert str(tmpdir) in loaded_settings["source"] + + def test_as_bool(): assert settings._as_bool("TrUe") is True assert settings._as_bool("true") is True From fb6e0a9da6d2010805916682adc2df4b00b5cc0b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 26 Feb 2021 21:14:25 -0800 Subject: [PATCH 1402/1439] Resolved issue #1669: Parallel now defaults to number of CPU cores if no value is provided --- CHANGELOG.md | 1 + isort/main.py | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 710eccb1f..72b8caeb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ - Implemented #1648: Export MyPY type hints. - Implemented #1641: Identified import statements now return runnable code. - Implemented #1661: Added "wemake" profile. + - implemented #1669: Parallel (`-j`) now defaults to number of CPU cores if no value is provided. ### 5.7.0 December 30th 2020 - Fixed #1612: In rare circumstances an extra comma is added after import and before comment. diff --git a/isort/main.py b/isort/main.py index 6824bae73..c6f6737b5 100644 --- a/isort/main.py +++ b/isort/main.py @@ -2,6 +2,7 @@ import argparse import functools import json +import multiprocessing import os import sys from gettext import gettext as _ @@ -268,7 +269,13 @@ def _build_arg_parser() -> argparse.ArgumentParser: help="Use the old deprecated finder logic that relies on environment introspection magic.", ) general_group.add_argument( - "-j", "--jobs", help="Number of files to process in parallel.", dest="jobs", type=int + "-j", + "--jobs", + help="Number of files to process in parallel.", + dest="jobs", + type=int, + nargs="?", + const=multiprocessing.cpu_count(), ) general_group.add_argument( "--ac", @@ -844,6 +851,7 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]: arguments["multi_line_output"] = WrapModes(int(multi_line_output)) else: arguments["multi_line_output"] = WrapModes[multi_line_output] + return arguments From 3df2ed45ea05a9817f0a099ee436561b28092934 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 27 Feb 2021 03:15:45 -0800 Subject: [PATCH 1403/1439] Fix errors found by deepsource --- isort/main.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/isort/main.py b/isort/main.py index c6f6737b5..11da105b8 100644 --- a/isort/main.py +++ b/isort/main.py @@ -2,7 +2,6 @@ import argparse import functools import json -import multiprocessing import os import sys from gettext import gettext as _ @@ -275,7 +274,7 @@ def _build_arg_parser() -> argparse.ArgumentParser: dest="jobs", type=int, nargs="?", - const=multiprocessing.cpu_count(), + const=-1, ) general_group.add_argument( "--ac", @@ -993,7 +992,7 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = config_dict = arguments.copy() ask_to_apply = config_dict.pop("ask_to_apply", False) - jobs = config_dict.pop("jobs", ()) + jobs = config_dict.pop("jobs", None) check = config_dict.pop("check", False) show_diff = config_dict.pop("show_diff", False) write_to_stdout = config_dict.pop("write_to_stdout", False) @@ -1069,7 +1068,7 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = if jobs: import multiprocessing - executor = multiprocessing.Pool(jobs) + executor = multiprocessing.Pool(jobs if jobs > 0 else multiprocessing.cpu_count()) attempt_iterator = executor.imap( functools.partial( sort_imports, From f6b3fef93e2e382242ab99421ed3de240d9334ad Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 28 Feb 2021 23:59:41 -0800 Subject: [PATCH 1404/1439] Fix no / safety --- isort/main.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/isort/main.py b/isort/main.py index 11da105b8..648efd012 100644 --- a/isort/main.py +++ b/isort/main.py @@ -347,6 +347,12 @@ def _build_arg_parser() -> argparse.ArgumentParser: dest="filename", help="Provide the filename associated with a stream.", ) + target_group.add_argument( + "--no-preserve-root", + action="store_true", + default=False, + help="Tells isort not to treat / specially, allowing it to be ran on the root dir.", + ) output_group.add_argument( "-a", @@ -1000,6 +1006,7 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = remapped_deprecated_args = config_dict.pop("remapped_deprecated_args", False) stream_filename = config_dict.pop("filename", None) ext_format = config_dict.pop("ext_format", None) + no_preserve_root = config_dict.pop("no_preserve_root", None) wrong_sorted_files = False all_attempt_broken = False no_valid_encodings = False @@ -1037,11 +1044,18 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = file_path=file_path, extension=ext_format, ) + elif "/" in file_names and not no_preserve_root: + printer = create_terminal_printer(color=config.color_output) + printer.error("it is dangerous to operate recursively on '/'") + printer.error("use --no-preserve-root to override this failsafe") + sys.exit(1) else: if stream_filename: printer = create_terminal_printer(color=config.color_output) printer.error("Filename override is intended only for stream (-) sorting.") sys.exit(1) + if file_names == ["/"]: + input("You've requested to run isort against your entire computer (/) are you sure? ") skipped: List[str] = [] broken: List[str] = [] From 8fb0fca0bd192c2aa79c869d115445ccca8d0363 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 1 Mar 2021 00:00:50 -0800 Subject: [PATCH 1405/1439] Fixed #1668 --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72b8caeb2..6b325025a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ - Implemented #1648: Export MyPY type hints. - Implemented #1641: Identified import statements now return runnable code. - Implemented #1661: Added "wemake" profile. - - implemented #1669: Parallel (`-j`) now defaults to number of CPU cores if no value is provided. + - Implemented #1669: Parallel (`-j`) now defaults to number of CPU cores if no value is provided. + - Implemented #1668: Added a safeguard against accidental usage against /. ### 5.7.0 December 30th 2020 - Fixed #1612: In rare circumstances an extra comma is added after import and before comment. From 747a08c280028fea66ff6ab8463e7fb05527e8d8 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 1 Mar 2021 00:05:45 -0800 Subject: [PATCH 1406/1439] Update dependencies --- poetry.lock | 433 +++++++++++++++++++++++++++++----------------------- 1 file changed, 240 insertions(+), 193 deletions(-) diff --git a/poetry.lock b/poetry.lock index 526dbbe38..03bcbfd59 100644 --- a/poetry.lock +++ b/poetry.lock @@ -16,14 +16,15 @@ python-versions = "*" [[package]] name = "arrow" -version = "0.17.0" +version = "1.0.2" description = "Better dates & times for Python" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.6" [package.dependencies] python-dateutil = ">=2.7.0" +typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "atomicwrites" @@ -184,7 +185,7 @@ six = ">=1.10" [[package]] name = "coverage" -version = "5.3.1" +version = "5.5" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -195,7 +196,7 @@ toml = ["toml"] [[package]] name = "cruft" -version = "2.6.0" +version = "2.7.0" description = "Allows you to maintain all the necessary cruft for packaging and building projects separate from the code you intentionally write. Built on-top of CookieCutter." category = "dev" optional = false @@ -213,7 +214,7 @@ examples = ["examples (>=1.0.2,<2.0.0)"] [[package]] name = "dataclasses" -version = "0.8" +version = "0.7" description = "A backport of the dataclasses module for Python 3.6" category = "dev" optional = false @@ -243,6 +244,14 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "docstring-parser" +version = "0.7.3" +description = "" +category = "dev" +optional = false +python-versions = "~=3.5" + [[package]] name = "dparse" version = "0.5.1" @@ -367,7 +376,7 @@ gitdb = ">=4.0.1" [[package]] name = "gitpython" -version = "3.1.11" +version = "3.1.14" description = "Python Git Library" category = "dev" optional = false @@ -464,7 +473,7 @@ python-versions = "*" [[package]] name = "hypothesis" -version = "5.43.5" +version = "6.3.4" description = "A library for property-based testing" category = "dev" optional = false @@ -475,8 +484,9 @@ attrs = ">=19.2.0" sortedcontainers = ">=2.1.0,<3.0.0" [package.extras] -all = ["black (>=19.10b0)", "click (>=7.0)", "django (>=2.2)", "dpcontracts (>=0.4)", "lark-parser (>=0.6.5)", "numpy (>=1.9.0)", "pandas (>=0.25)", "pytest (>=4.3)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "importlib-resources (>=3.3.0)", "importlib-metadata", "backports.zoneinfo (>=0.2.1)", "tzdata (>=2020.4)"] +all = ["black (>=19.10b0)", "click (>=7.0)", "django (>=2.2)", "dpcontracts (>=0.4)", "lark-parser (>=0.6.5)", "libcst (>=0.3.16)", "numpy (>=1.9.0)", "pandas (>=0.25)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "importlib-resources (>=3.3.0)", "importlib-metadata", "backports.zoneinfo (>=0.2.1)", "tzdata (>=2020.4)"] cli = ["click (>=7.0)", "black (>=19.10b0)"] +codemods = ["libcst (>=0.3.16)"] dateutil = ["python-dateutil (>=1.4)"] django = ["pytz (>=2014.1)", "django (>=2.2)"] dpcontracts = ["dpcontracts (>=0.4)"] @@ -484,7 +494,7 @@ ghostwriter = ["black (>=19.10b0)"] lark = ["lark-parser (>=0.6.5)"] numpy = ["numpy (>=1.9.0)"] pandas = ["pandas (>=0.25)"] -pytest = ["pytest (>=4.3)"] +pytest = ["pytest (>=4.6)"] pytz = ["pytz (>=2014.1)"] redis = ["redis (>=3.0.0)"] zoneinfo = ["importlib-resources (>=3.3.0)", "backports.zoneinfo (>=0.2.1)", "tzdata (>=2020.4)"] @@ -506,7 +516,7 @@ pytest = ["pytest (>=4.0.0,<5.0.0)"] [[package]] name = "hypothesmith" -version = "0.1.7" +version = "0.1.8" description = "Hypothesis strategies for generating Python programs, something like CSmith" category = "dev" optional = false @@ -527,15 +537,18 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "immutables" -version = "0.14" +version = "0.15" description = "Immutable Collections" category = "dev" optional = false python-versions = ">=3.5" +[package.extras] +test = ["flake8 (>=3.8.4,<3.9.0)", "pycodestyle (>=2.6.0,<2.7.0)"] + [[package]] name = "importlib-metadata" -version = "3.3.0" +version = "3.7.0" description = "Read metadata from Python packages" category = "main" optional = false @@ -546,8 +559,8 @@ typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] [[package]] name = "iniconfig" @@ -613,7 +626,7 @@ testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<6.0.0)"] [[package]] name = "jinja2" -version = "2.11.2" +version = "2.11.3" description = "A very fast and expressive template engine." category = "dev" optional = false @@ -639,7 +652,7 @@ jinja2 = "*" [[package]] name = "joblib" -version = "1.0.0" +version = "1.0.1" description = "Lightweight pipelining with Python functions" category = "dev" optional = false @@ -647,7 +660,7 @@ python-versions = ">=3.6" [[package]] name = "lark-parser" -version = "0.11.1" +version = "0.11.2" description = "a modern parsing library" category = "dev" optional = false @@ -659,7 +672,7 @@ regex = ["regex"] [[package]] name = "libcst" -version = "0.3.16" +version = "0.3.17" description = "A concrete syntax tree with AST-like properties for Python 3.5, 3.6, 3.7 and 3.8 programs." category = "dev" optional = false @@ -704,7 +717,7 @@ languages = ["nltk (>=3.2.5,<3.5)", "nltk (>=3.2.5)"] [[package]] name = "mako" -version = "1.1.3" +version = "1.1.4" description = "A super-fast templating language that borrows the best ideas from the existing templating languages." category = "dev" optional = false @@ -719,7 +732,7 @@ lingua = ["lingua"] [[package]] name = "markdown" -version = "3.3.3" +version = "3.3.4" description = "Python implementation of Markdown." category = "dev" optional = false @@ -838,7 +851,7 @@ twitter = ["twython"] [[package]] name = "numpy" -version = "1.19.4" +version = "1.19.5" description = "NumPy is the fundamental package for array computing with Python." category = "dev" optional = false @@ -857,7 +870,7 @@ six = ">=1.8.0" [[package]] name = "packaging" -version = "20.8" +version = "20.9" description = "Core utilities for Python packages" category = "main" optional = false @@ -896,13 +909,15 @@ python-versions = ">=2.6" [[package]] name = "pdocs" -version = "1.0.2" +version = "1.1.1" description = "A simple program and library to auto generate API documentation for Python modules." category = "dev" optional = false python-versions = ">=3.6,<4.0" [package.dependencies] +dataclasses = {version = ">=0.7,<0.8", markers = "python_version >= \"3.6\" and python_version < \"3.7\""} +docstring_parser = ">=0.7.2,<0.8.0" hug = ">=2.6,<3.0" Mako = ">=1.1,<2.0" Markdown = ">=3.0.0,<4.0.0" @@ -1030,7 +1045,7 @@ dev = ["pre-commit", "tox"] [[package]] name = "portray" -version = "1.4.0" +version = "1.5.2" description = "Your Project with Great Documentation" category = "dev" optional = false @@ -1039,9 +1054,10 @@ python-versions = ">=3.6,<4.0" [package.dependencies] GitPython = ">=3.0,<4.0" hug = ">=2.6,<3.0" +livereload = ">=2.6.3,<3.0.0" mkdocs = ">=1.0,<2.0" mkdocs-material = ">=5.0,<6.0" -pdocs = ">=1.0.2,<2.0.0" +pdocs = ">=1.1.1,<2.0.0" pymdown-extensions = ">=7.0,<8.0" toml = ">=0.10.0,<0.11.0" yaspin = ">=0.15.0,<0.16.0" @@ -1126,7 +1142,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pygments" -version = "2.7.3" +version = "2.8.0" description = "Pygments is a syntax highlighting package written in Python." category = "dev" optional = false @@ -1167,7 +1183,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "pytest" -version = "6.2.1" +version = "6.2.2" description = "pytest: simple powerful testing with Python" category = "dev" optional = false @@ -1189,14 +1205,14 @@ testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xm [[package]] name = "pytest-cov" -version = "2.10.1" +version = "2.11.1" description = "Pytest plugin for measuring coverage." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.dependencies] -coverage = ">=4.4" +coverage = ">=5.2.1" pytest = ">=4.6" [package.extras] @@ -1243,11 +1259,11 @@ unidecode = ["Unidecode (>=1.1.1)"] [[package]] name = "pyyaml" -version = "5.3.1" +version = "5.4.1" description = "YAML parser and emitter for Python" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [[package]] name = "regex" @@ -1317,7 +1333,7 @@ idna2008 = ["idna"] [[package]] name = "safety" -version = "1.10.0" +version = "1.10.3" description = "Checks installed dependencies for known vulnerabilities." category = "dev" optional = false @@ -1339,7 +1355,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "smmap" -version = "3.0.4" +version = "3.0.5" description = "A pure Python implementation of a sliding window memory map manager" category = "dev" optional = false @@ -1369,8 +1385,8 @@ contextvars = {version = ">=2.1", markers = "python_version < \"3.7\""} [[package]] name = "snowballstemmer" -version = "2.0.0" -description = "This package provides 26 stemmers for 25 languages generated from Snowball algorithms." +version = "2.1.0" +description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." category = "dev" optional = false python-versions = "*" @@ -1429,7 +1445,7 @@ python-versions = ">= 3.5" [[package]] name = "tqdm" -version = "4.55.0" +version = "4.58.0" description = "Fast, Extensible Progress Meter" category = "dev" optional = false @@ -1502,7 +1518,7 @@ typing-extensions = ">=3.7.4" [[package]] name = "urllib3" -version = "1.26.2" +version = "1.26.3" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false @@ -1599,8 +1615,8 @@ appnope = [ {file = "appnope-0.1.2.tar.gz", hash = "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a"}, ] arrow = [ - {file = "arrow-0.17.0-py2.py3-none-any.whl", hash = "sha256:e098abbd9af3665aea81bdd6c869e93af4feb078e98468dd351c383af187aac5"}, - {file = "arrow-0.17.0.tar.gz", hash = "sha256:ff08d10cda1d36c68657d6ad20d74fbea493d980f8b2d45344e00d6ed2bf6ed4"}, + {file = "arrow-1.0.2-py3-none-any.whl", hash = "sha256:cb1b7bc3a07eb1c1e98ccc740627460c9891636642bcf03c4097b71d8bc5ca1d"}, + {file = "arrow-1.0.2.tar.gz", hash = "sha256:5df8e632e9158c48f42f68a742068bcfc1c0181cbe7543e4cda6089bb287a305"}, ] atomicwrites = [ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, @@ -1656,63 +1672,63 @@ cookiecutter = [ {file = "cookiecutter-1.7.2.tar.gz", hash = "sha256:efb6b2d4780feda8908a873e38f0e61778c23f6a2ea58215723bcceb5b515dac"}, ] coverage = [ - {file = "coverage-5.3.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:fabeeb121735d47d8eab8671b6b031ce08514c86b7ad8f7d5490a7b6dcd6267d"}, - {file = "coverage-5.3.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:7e4d159021c2029b958b2363abec4a11db0ce8cd43abb0d9ce44284cb97217e7"}, - {file = "coverage-5.3.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:378ac77af41350a8c6b8801a66021b52da8a05fd77e578b7380e876c0ce4f528"}, - {file = "coverage-5.3.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:e448f56cfeae7b1b3b5bcd99bb377cde7c4eb1970a525c770720a352bc4c8044"}, - {file = "coverage-5.3.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:cc44e3545d908ecf3e5773266c487ad1877be718d9dc65fc7eb6e7d14960985b"}, - {file = "coverage-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:08b3ba72bd981531fd557f67beee376d6700fba183b167857038997ba30dd297"}, - {file = "coverage-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:8dacc4073c359f40fcf73aede8428c35f84639baad7e1b46fce5ab7a8a7be4bb"}, - {file = "coverage-5.3.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ee2f1d1c223c3d2c24e3afbb2dd38be3f03b1a8d6a83ee3d9eb8c36a52bee899"}, - {file = "coverage-5.3.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:9a9d4ff06804920388aab69c5ea8a77525cf165356db70131616acd269e19b36"}, - {file = "coverage-5.3.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:782a5c7df9f91979a7a21792e09b34a658058896628217ae6362088b123c8500"}, - {file = "coverage-5.3.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:fda29412a66099af6d6de0baa6bd7c52674de177ec2ad2630ca264142d69c6c7"}, - {file = "coverage-5.3.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:f2c6888eada180814b8583c3e793f3f343a692fc802546eed45f40a001b1169f"}, - {file = "coverage-5.3.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:8f33d1156241c43755137288dea619105477961cfa7e47f48dbf96bc2c30720b"}, - {file = "coverage-5.3.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b239711e774c8eb910e9b1ac719f02f5ae4bf35fa0420f438cdc3a7e4e7dd6ec"}, - {file = "coverage-5.3.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:f54de00baf200b4539a5a092a759f000b5f45fd226d6d25a76b0dff71177a714"}, - {file = "coverage-5.3.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:be0416074d7f253865bb67630cf7210cbc14eb05f4099cc0f82430135aaa7a3b"}, - {file = "coverage-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:c46643970dff9f5c976c6512fd35768c4a3819f01f61169d8cdac3f9290903b7"}, - {file = "coverage-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9a4f66259bdd6964d8cf26142733c81fb562252db74ea367d9beb4f815478e72"}, - {file = "coverage-5.3.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c6e5174f8ca585755988bc278c8bb5d02d9dc2e971591ef4a1baabdf2d99589b"}, - {file = "coverage-5.3.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:3911c2ef96e5ddc748a3c8b4702c61986628bb719b8378bf1e4a6184bbd48fe4"}, - {file = "coverage-5.3.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:c5ec71fd4a43b6d84ddb88c1df94572479d9a26ef3f150cef3dacefecf888105"}, - {file = "coverage-5.3.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f51dbba78d68a44e99d484ca8c8f604f17e957c1ca09c3ebc2c7e3bbd9ba0448"}, - {file = "coverage-5.3.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:a2070c5affdb3a5e751f24208c5c4f3d5f008fa04d28731416e023c93b275277"}, - {file = "coverage-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:535dc1e6e68fad5355f9984d5637c33badbdc987b0c0d303ee95a6c979c9516f"}, - {file = "coverage-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:a4857f7e2bc6921dbd487c5c88b84f5633de3e7d416c4dc0bb70256775551a6c"}, - {file = "coverage-5.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fac3c432851038b3e6afe086f777732bcf7f6ebbfd90951fa04ee53db6d0bcdd"}, - {file = "coverage-5.3.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:cd556c79ad665faeae28020a0ab3bda6cd47d94bec48e36970719b0b86e4dcf4"}, - {file = "coverage-5.3.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:a66ca3bdf21c653e47f726ca57f46ba7fc1f260ad99ba783acc3e58e3ebdb9ff"}, - {file = "coverage-5.3.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:ab110c48bc3d97b4d19af41865e14531f300b482da21783fdaacd159251890e8"}, - {file = "coverage-5.3.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:e52d3d95df81c8f6b2a1685aabffadf2d2d9ad97203a40f8d61e51b70f191e4e"}, - {file = "coverage-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:fa10fee7e32213f5c7b0d6428ea92e3a3fdd6d725590238a3f92c0de1c78b9d2"}, - {file = "coverage-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ce6f3a147b4b1a8b09aae48517ae91139b1b010c5f36423fa2b866a8b23df879"}, - {file = "coverage-5.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:93a280c9eb736a0dcca19296f3c30c720cb41a71b1f9e617f341f0a8e791a69b"}, - {file = "coverage-5.3.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:3102bb2c206700a7d28181dbe04d66b30780cde1d1c02c5f3c165cf3d2489497"}, - {file = "coverage-5.3.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8ffd4b204d7de77b5dd558cdff986a8274796a1e57813ed005b33fd97e29f059"}, - {file = "coverage-5.3.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:a607ae05b6c96057ba86c811d9c43423f35e03874ffb03fbdcd45e0637e8b631"}, - {file = "coverage-5.3.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:3a3c3f8863255f3c31db3889f8055989527173ef6192a283eb6f4db3c579d830"}, - {file = "coverage-5.3.1-cp38-cp38-win32.whl", hash = "sha256:ff1330e8bc996570221b450e2d539134baa9465f5cb98aff0e0f73f34172e0ae"}, - {file = "coverage-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:3498b27d8236057def41de3585f317abae235dd3a11d33e01736ffedb2ef8606"}, - {file = "coverage-5.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ceb499d2b3d1d7b7ba23abe8bf26df5f06ba8c71127f188333dddcf356b4b63f"}, - {file = "coverage-5.3.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:3b14b1da110ea50c8bcbadc3b82c3933974dbeea1832e814aab93ca1163cd4c1"}, - {file = "coverage-5.3.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:76b2775dda7e78680d688daabcb485dc87cf5e3184a0b3e012e1d40e38527cc8"}, - {file = "coverage-5.3.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:cef06fb382557f66d81d804230c11ab292d94b840b3cb7bf4450778377b592f4"}, - {file = "coverage-5.3.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:6f61319e33222591f885c598e3e24f6a4be3533c1d70c19e0dc59e83a71ce27d"}, - {file = "coverage-5.3.1-cp39-cp39-win32.whl", hash = "sha256:cc6f8246e74dd210d7e2b56c76ceaba1cc52b025cd75dbe96eb48791e0250e98"}, - {file = "coverage-5.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:2757fa64e11ec12220968f65d086b7a29b6583d16e9a544c889b22ba98555ef1"}, - {file = "coverage-5.3.1-pp36-none-any.whl", hash = "sha256:723d22d324e7997a651478e9c5a3120a0ecbc9a7e94071f7e1954562a8806cf3"}, - {file = "coverage-5.3.1-pp37-none-any.whl", hash = "sha256:c89b558f8a9a5a6f2cfc923c304d49f0ce629c3bd85cb442ca258ec20366394c"}, - {file = "coverage-5.3.1.tar.gz", hash = "sha256:38f16b1317b8dd82df67ed5daa5f5e7c959e46579840d77a67a4ceb9cef0a50b"}, + {file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"}, + {file = "coverage-5.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b"}, + {file = "coverage-5.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669"}, + {file = "coverage-5.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90"}, + {file = "coverage-5.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c"}, + {file = "coverage-5.5-cp27-cp27m-win32.whl", hash = "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a"}, + {file = "coverage-5.5-cp27-cp27m-win_amd64.whl", hash = "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81"}, + {file = "coverage-5.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb"}, + {file = "coverage-5.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160"}, + {file = "coverage-5.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"}, + {file = "coverage-5.5-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701"}, + {file = "coverage-5.5-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793"}, + {file = "coverage-5.5-cp35-cp35m-win32.whl", hash = "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e"}, + {file = "coverage-5.5-cp35-cp35m-win_amd64.whl", hash = "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3"}, + {file = "coverage-5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066"}, + {file = "coverage-5.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a"}, + {file = "coverage-5.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465"}, + {file = "coverage-5.5-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb"}, + {file = "coverage-5.5-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821"}, + {file = "coverage-5.5-cp36-cp36m-win32.whl", hash = "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45"}, + {file = "coverage-5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184"}, + {file = "coverage-5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a"}, + {file = "coverage-5.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53"}, + {file = "coverage-5.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d"}, + {file = "coverage-5.5-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638"}, + {file = "coverage-5.5-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3"}, + {file = "coverage-5.5-cp37-cp37m-win32.whl", hash = "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a"}, + {file = "coverage-5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a"}, + {file = "coverage-5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6"}, + {file = "coverage-5.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2"}, + {file = "coverage-5.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759"}, + {file = "coverage-5.5-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873"}, + {file = "coverage-5.5-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a"}, + {file = "coverage-5.5-cp38-cp38-win32.whl", hash = "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6"}, + {file = "coverage-5.5-cp38-cp38-win_amd64.whl", hash = "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502"}, + {file = "coverage-5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b"}, + {file = "coverage-5.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529"}, + {file = "coverage-5.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b"}, + {file = "coverage-5.5-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff"}, + {file = "coverage-5.5-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b"}, + {file = "coverage-5.5-cp39-cp39-win32.whl", hash = "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6"}, + {file = "coverage-5.5-cp39-cp39-win_amd64.whl", hash = "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03"}, + {file = "coverage-5.5-pp36-none-any.whl", hash = "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079"}, + {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, + {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, ] cruft = [ - {file = "cruft-2.6.0-py3-none-any.whl", hash = "sha256:80e32a2fd8103f3c57c96af1c25d96d748072727a8563c58a65eece56a02b441"}, - {file = "cruft-2.6.0.tar.gz", hash = "sha256:bbeea9fb69812afd74a8140cca5ae7fdab761d4df50b137f2437fab9e72c4580"}, + {file = "cruft-2.7.0-py3-none-any.whl", hash = "sha256:f4b8d93276a07fedd4b6bc7880c9f7ec72ce35fca61effe049bd1e951c601776"}, + {file = "cruft-2.7.0.tar.gz", hash = "sha256:a47b84387f3133181da9f82f4711246147363eff78f13bd2b031400a33ee24d1"}, ] dataclasses = [ - {file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"}, - {file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"}, + {file = "dataclasses-0.7-py3-none-any.whl", hash = "sha256:3459118f7ede7c8bea0fe795bff7c6c2ce287d01dd226202f7c9ebc0610a7836"}, + {file = "dataclasses-0.7.tar.gz", hash = "sha256:494a6dcae3b8bcf80848eea2ef64c0cc5cd307ffc263e17cdf42f3e5420808e6"}, ] decorator = [ {file = "decorator-4.4.2-py2.py3-none-any.whl", hash = "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760"}, @@ -1725,6 +1741,9 @@ distlib = [ docopt = [ {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, ] +docstring-parser = [ + {file = "docstring_parser-0.7.3.tar.gz", hash = "sha256:cde5fbf8b846433dfbde1e0f96b7f909336a634d5df34a38cb75050c7346734a"}, +] dparse = [ {file = "dparse-0.5.1-py3-none-any.whl", hash = "sha256:e953a25e44ebb60a5c6efc2add4420c177f1d8404509da88da9729202f306994"}, {file = "dparse-0.5.1.tar.gz", hash = "sha256:a1b5f169102e1c894f9a7d5ccf6f9402a836a5d24be80a986c7ce9eaed78f367"}, @@ -1781,8 +1800,8 @@ gitdb2 = [ {file = "gitdb2-4.0.2.tar.gz", hash = "sha256:0986cb4003de743f2b3aba4c828edd1ab58ce98e1c4a8acf72ef02760d4beb4e"}, ] gitpython = [ - {file = "GitPython-3.1.11-py3-none-any.whl", hash = "sha256:6eea89b655917b500437e9668e4a12eabdcf00229a0df1762aabd692ef9b746b"}, - {file = "GitPython-3.1.11.tar.gz", hash = "sha256:befa4d101f91bad1b632df4308ec64555db684c360bd7d2130b4807d49ce86b8"}, + {file = "GitPython-3.1.14-py3-none-any.whl", hash = "sha256:3283ae2fba31c913d857e12e5ba5f9a7772bbc064ae2bb09efafa71b0dd4939b"}, + {file = "GitPython-3.1.14.tar.gz", hash = "sha256:be27633e7509e58391f10207cd32b2a6cf5b908f92d9cd30da2e514e1137af61"}, ] h11 = [ {file = "h11-0.9.0-py2.py3-none-any.whl", hash = "sha256:4bc6d6a1238b7615b266ada57e0618568066f57dd6fa967d1290ec9309b2f2f1"}, @@ -1817,38 +1836,41 @@ hyperframe = [ {file = "hyperframe-5.2.0.tar.gz", hash = "sha256:a9f5c17f2cc3c719b917c4f33ed1c61bd1f8dfac4b1bd23b7c80b3400971b41f"}, ] hypothesis = [ - {file = "hypothesis-5.43.5-py3-none-any.whl", hash = "sha256:546db914a7a7be1ccacbd408cf4cec4fa958b96b4015a2216f8187e4f0ec7eaa"}, - {file = "hypothesis-5.43.5.tar.gz", hash = "sha256:9377cd796a5bca3c0ae74ef1c592aa231d3a04cde948467bace9344148ee75cb"}, + {file = "hypothesis-6.3.4-py3-none-any.whl", hash = "sha256:69f7d32270f52a14f853cb3673eae65fc2f057601f6e7b5502e5c905985d8c69"}, + {file = "hypothesis-6.3.4.tar.gz", hash = "sha256:26771ce7f21c2ed08e93f62bdd63755dab051c163cc9d54544269b5b8b550eac"}, ] hypothesis-auto = [ {file = "hypothesis-auto-1.1.4.tar.gz", hash = "sha256:5e2c2fb09dc09842512d80630bb792359a1d33d2c0473ad47ee23da0be9e32b1"}, {file = "hypothesis_auto-1.1.4-py3-none-any.whl", hash = "sha256:fea8560c4522c0fd490ed8cc17e420b95dabebb11b9b334c59bf2d768839015f"}, ] hypothesmith = [ - {file = "hypothesmith-0.1.7-py3-none-any.whl", hash = "sha256:f37d0b55fec60d31c4fff271e6ae38100fce4c622c584aae0037f08163fea93f"}, - {file = "hypothesmith-0.1.7.tar.gz", hash = "sha256:97802ad8136033e65db748bbb5a7c9193b9e4df85028f074484db993e8548019"}, + {file = "hypothesmith-0.1.8-py3-none-any.whl", hash = "sha256:6248c3d0e0dc934e5352e3f7d79290560ab5861847ca6701e410f9a287461216"}, + {file = "hypothesmith-0.1.8.tar.gz", hash = "sha256:f9ff047b15c4ed312ce3da57ea27570f86d6b53ce12af9f25e59e6576a00410a"}, ] idna = [ {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, ] immutables = [ - {file = "immutables-0.14-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:860666fab142401a5535bf65cbd607b46bc5ed25b9d1eb053ca8ed9a1a1a80d6"}, - {file = "immutables-0.14-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:ce01788878827c3f0331c254a4ad8d9721489a5e65cc43e19c80040b46e0d297"}, - {file = "immutables-0.14-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:8797eed4042f4626b0bc04d9cf134208918eb0c937a8193a2c66df5041e62d2e"}, - {file = "immutables-0.14-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:33ce2f977da7b5e0dddd93744862404bdb316ffe5853ec853e53141508fa2e6a"}, - {file = "immutables-0.14-cp36-cp36m-win_amd64.whl", hash = "sha256:6c8eace4d98988c72bcb37c05e79aae756832738305ae9497670482a82db08bc"}, - {file = "immutables-0.14-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:ab6c18b7b2b2abc83e0edc57b0a38bf0915b271582a1eb8c7bed1c20398f8040"}, - {file = "immutables-0.14-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c099212fd6504513a50e7369fe281007c820cf9d7bb22a336486c63d77d6f0b2"}, - {file = "immutables-0.14-cp37-cp37m-win_amd64.whl", hash = "sha256:714aedbdeba4439d91cb5e5735cb10631fc47a7a69ea9cc8ecbac90322d50a4a"}, - {file = "immutables-0.14-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:1c11050c49e193a1ec9dda1747285333f6ba6a30bbeb2929000b9b1192097ec0"}, - {file = "immutables-0.14-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:c453e12b95e1d6bb4909e8743f88b7f5c0c97b86a8bc0d73507091cb644e3c1e"}, - {file = "immutables-0.14-cp38-cp38-win_amd64.whl", hash = "sha256:ef9da20ec0f1c5853b5c8f8e3d9e1e15b8d98c259de4b7515d789a606af8745e"}, - {file = "immutables-0.14.tar.gz", hash = "sha256:a0a1cc238b678455145bae291d8426f732f5255537ed6a5b7645949704c70a78"}, + {file = "immutables-0.15-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:6728f4392e3e8e64b593a5a0cd910a1278f07f879795517e09f308daed138631"}, + {file = "immutables-0.15-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f0836cd3bdc37c8a77b192bbe5f41dbcc3ce654db048ebbba89bdfe6db7a1c7a"}, + {file = "immutables-0.15-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:8703d8abfd8687932f2a05f38e7de270c3a6ca3bd1c1efb3c938656b3f2f985a"}, + {file = "immutables-0.15-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:b8ad986f9b532c026f19585289384b0769188fcb68b37c7f0bd0df9092a6ca54"}, + {file = "immutables-0.15-cp36-cp36m-win_amd64.whl", hash = "sha256:6f117d9206165b9dab8fd81c5129db757d1a044953f438654236ed9a7a4224ae"}, + {file = "immutables-0.15-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:b75ade826920c4e490b1bb14cf967ac14e61eb7c5562161c5d7337d61962c226"}, + {file = "immutables-0.15-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:b7e13c061785e34f73c4f659861f1b3e4a5fd918e4395c84b21c4e3d449ebe27"}, + {file = "immutables-0.15-cp37-cp37m-win_amd64.whl", hash = "sha256:3035849accee4f4e510ed7c94366a40e0f5fef9069fbe04a35f4787b13610a4a"}, + {file = "immutables-0.15-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:b04fa69174e0c8f815f9c55f2a43fc9e5a68452fab459a08e904a74e8471639f"}, + {file = "immutables-0.15-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:141c2e9ea515a3a815007a429f0b47a578ebeb42c831edaec882a245a35fffca"}, + {file = "immutables-0.15-cp38-cp38-win_amd64.whl", hash = "sha256:cbe8c64640637faa5535d539421b293327f119c31507c33ca880bd4f16035eb6"}, + {file = "immutables-0.15-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:a0a4e4417d5ef4812d7f99470cd39347b58cb927365dd2b8da9161040d260db0"}, + {file = "immutables-0.15-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:3b15c08c71c59e5b7c2470ef949d49ff9f4263bb77f488422eaa157da84d6999"}, + {file = "immutables-0.15-cp39-cp39-win_amd64.whl", hash = "sha256:2283a93c151566e6830aee0e5bee55fc273455503b43aa004356b50f9182092b"}, + {file = "immutables-0.15.tar.gz", hash = "sha256:3713ab1ebbb6946b7ce1387bb9d1d7f5e09c45add58c2a2ee65f963c171e746b"}, ] importlib-metadata = [ - {file = "importlib_metadata-3.3.0-py3-none-any.whl", hash = "sha256:bf792d480abbd5eda85794e4afb09dd538393f7d6e6ffef6e9f03d2014cf9450"}, - {file = "importlib_metadata-3.3.0.tar.gz", hash = "sha256:5c5a2720817414a6c41f0a49993908068243ae02c1635a228126519b509c8aed"}, + {file = "importlib_metadata-3.7.0-py3-none-any.whl", hash = "sha256:c6af5dbf1126cd959c4a8d8efd61d4d3c83bddb0459a17e554284a077574b614"}, + {file = "importlib_metadata-3.7.0.tar.gz", hash = "sha256:24499ffde1b80be08284100393955842be4a59c7c16bbf2738aad0e464a8e0aa"}, ] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, @@ -1867,24 +1889,23 @@ jedi = [ {file = "jedi-0.18.0.tar.gz", hash = "sha256:92550a404bad8afed881a137ec9a461fed49eca661414be45059329614ed0707"}, ] jinja2 = [ - {file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"}, - {file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"}, + {file = "Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419"}, + {file = "Jinja2-2.11.3.tar.gz", hash = "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"}, ] jinja2-time = [ {file = "jinja2-time-0.2.0.tar.gz", hash = "sha256:d14eaa4d315e7688daa4969f616f226614350c48730bfa1692d2caebd8c90d40"}, {file = "jinja2_time-0.2.0-py2.py3-none-any.whl", hash = "sha256:d3eab6605e3ec8b7a0863df09cc1d23714908fa61aa6986a845c20ba488b4efa"}, ] joblib = [ - {file = "joblib-1.0.0-py3-none-any.whl", hash = "sha256:75ead23f13484a2a414874779d69ade40d4fa1abe62b222a23cd50d4bc822f6f"}, - {file = "joblib-1.0.0.tar.gz", hash = "sha256:7ad866067ac1fdec27d51c8678ea760601b70e32ff1881d4dc8e1171f2b64b24"}, + {file = "joblib-1.0.1-py3-none-any.whl", hash = "sha256:feeb1ec69c4d45129954f1b7034954241eedfd6ba39b5e9e4b6883be3332d5e5"}, + {file = "joblib-1.0.1.tar.gz", hash = "sha256:9c17567692206d2f3fb9ecf5e991084254fe631665c450b443761c4186a613f7"}, ] lark-parser = [ - {file = "lark-parser-0.11.1.tar.gz", hash = "sha256:20bdefdf1b6e9bcb38165ea5cc4f27921a99c6f4c35264a3a953fd60335f1f8c"}, - {file = "lark_parser-0.11.1-py2.py3-none-any.whl", hash = "sha256:8b747e1f544dcc2789e3feaddd2a50c6a73bed69d62e9c69760c1e1f7d23495f"}, + {file = "lark-parser-0.11.2.tar.gz", hash = "sha256:ef610461ebf2b243502f337d9d49879e39f9add846a4749e88c8dcdc1378bb6b"}, ] libcst = [ - {file = "libcst-0.3.16-py3-none-any.whl", hash = "sha256:2c9e40245b8cb49b5219c76b36fe7037effa7594b9e6d5a092be99f8083d2415"}, - {file = "libcst-0.3.16.tar.gz", hash = "sha256:99c200004b6e845642eea7a433844d144994767f9ed50705171720b76d28cf7e"}, + {file = "libcst-0.3.17-py3-none-any.whl", hash = "sha256:4638e4e8f166f4c74df399222d347ce3e1d316e206b550d8a6254d51b4cf7275"}, + {file = "libcst-0.3.17.tar.gz", hash = "sha256:2766671c107263daa3fc34e39d55134a6fe253701564d7670586f30eee2c201c"}, ] livereload = [ {file = "livereload-2.6.3.tar.gz", hash = "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869"}, @@ -1894,12 +1915,11 @@ lunr = [ {file = "lunr-0.5.8.tar.gz", hash = "sha256:c4fb063b98eff775dd638b3df380008ae85e6cb1d1a24d1cd81a10ef6391c26e"}, ] mako = [ - {file = "Mako-1.1.3-py2.py3-none-any.whl", hash = "sha256:93729a258e4ff0747c876bd9e20df1b9758028946e976324ccd2d68245c7b6a9"}, - {file = "Mako-1.1.3.tar.gz", hash = "sha256:8195c8c1400ceb53496064314c6736719c6f25e7479cd24c77be3d9361cddc27"}, + {file = "Mako-1.1.4.tar.gz", hash = "sha256:17831f0b7087c313c0ffae2bcbbd3c1d5ba9eeac9c38f2eb7b50e8c99fe9d5ab"}, ] markdown = [ - {file = "Markdown-3.3.3-py3-none-any.whl", hash = "sha256:c109c15b7dc20a9ac454c9e6025927d44460b85bd039da028d85e2b6d0bcc328"}, - {file = "Markdown-3.3.3.tar.gz", hash = "sha256:5d9f2b5ca24bc4c7a390d22323ca4bad200368612b5aaa7796babf971d2b2f18"}, + {file = "Markdown-3.3.4-py3-none-any.whl", hash = "sha256:96c3ba1261de2f7547b46a00ea8463832c921d3f9d6aba3f255a6f71386db20c"}, + {file = "Markdown-3.3.4.tar.gz", hash = "sha256:31b5b491868dcc87d6c24b7e3d19a0d730d59d3e46f4eea6430a321bed387a49"}, ] markupsafe = [ {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, @@ -1920,20 +1940,39 @@ markupsafe = [ {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b"}, {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-win32.whl", hash = "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8"}, {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, ] mccabe = [ @@ -1976,48 +2015,48 @@ nltk = [ {file = "nltk-3.5.zip", hash = "sha256:845365449cd8c5f9731f7cb9f8bd6fd0767553b9d53af9eb1b3abf7700936b35"}, ] numpy = [ - {file = "numpy-1.19.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e9b30d4bd69498fc0c3fe9db5f62fffbb06b8eb9321f92cc970f2969be5e3949"}, - {file = "numpy-1.19.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:fedbd128668ead37f33917820b704784aff695e0019309ad446a6d0b065b57e4"}, - {file = "numpy-1.19.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8ece138c3a16db8c1ad38f52eb32be6086cc72f403150a79336eb2045723a1ad"}, - {file = "numpy-1.19.4-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:64324f64f90a9e4ef732be0928be853eee378fd6a01be21a0a8469c4f2682c83"}, - {file = "numpy-1.19.4-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:ad6f2ff5b1989a4899bf89800a671d71b1612e5ff40866d1f4d8bcf48d4e5764"}, - {file = "numpy-1.19.4-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d6c7bb82883680e168b55b49c70af29b84b84abb161cbac2800e8fcb6f2109b6"}, - {file = "numpy-1.19.4-cp36-cp36m-win32.whl", hash = "sha256:13d166f77d6dc02c0a73c1101dd87fdf01339febec1030bd810dcd53fff3b0f1"}, - {file = "numpy-1.19.4-cp36-cp36m-win_amd64.whl", hash = "sha256:448ebb1b3bf64c0267d6b09a7cba26b5ae61b6d2dbabff7c91b660c7eccf2bdb"}, - {file = "numpy-1.19.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:27d3f3b9e3406579a8af3a9f262f5339005dd25e0ecf3cf1559ff8a49ed5cbf2"}, - {file = "numpy-1.19.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:16c1b388cc31a9baa06d91a19366fb99ddbe1c7b205293ed072211ee5bac1ed2"}, - {file = "numpy-1.19.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e5b6ed0f0b42317050c88022349d994fe72bfe35f5908617512cd8c8ef9da2a9"}, - {file = "numpy-1.19.4-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:18bed2bcb39e3f758296584337966e68d2d5ba6aab7e038688ad53c8f889f757"}, - {file = "numpy-1.19.4-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:fe45becb4c2f72a0907c1d0246ea6449fe7a9e2293bb0e11c4e9a32bb0930a15"}, - {file = "numpy-1.19.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:6d7593a705d662be5bfe24111af14763016765f43cb6923ed86223f965f52387"}, - {file = "numpy-1.19.4-cp37-cp37m-win32.whl", hash = "sha256:6ae6c680f3ebf1cf7ad1d7748868b39d9f900836df774c453c11c5440bc15b36"}, - {file = "numpy-1.19.4-cp37-cp37m-win_amd64.whl", hash = "sha256:9eeb7d1d04b117ac0d38719915ae169aa6b61fca227b0b7d198d43728f0c879c"}, - {file = "numpy-1.19.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cb1017eec5257e9ac6209ac172058c430e834d5d2bc21961dceeb79d111e5909"}, - {file = "numpy-1.19.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:edb01671b3caae1ca00881686003d16c2209e07b7ef8b7639f1867852b948f7c"}, - {file = "numpy-1.19.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:f29454410db6ef8126c83bd3c968d143304633d45dc57b51252afbd79d700893"}, - {file = "numpy-1.19.4-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:ec149b90019852266fec2341ce1db513b843e496d5a8e8cdb5ced1923a92faab"}, - {file = "numpy-1.19.4-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:1aeef46a13e51931c0b1cf8ae1168b4a55ecd282e6688fdb0a948cc5a1d5afb9"}, - {file = "numpy-1.19.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:08308c38e44cc926bdfce99498b21eec1f848d24c302519e64203a8da99a97db"}, - {file = "numpy-1.19.4-cp38-cp38-win32.whl", hash = "sha256:5734bdc0342aba9dfc6f04920988140fb41234db42381cf7ccba64169f9fe7ac"}, - {file = "numpy-1.19.4-cp38-cp38-win_amd64.whl", hash = "sha256:09c12096d843b90eafd01ea1b3307e78ddd47a55855ad402b157b6c4862197ce"}, - {file = "numpy-1.19.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e452dc66e08a4ce642a961f134814258a082832c78c90351b75c41ad16f79f63"}, - {file = "numpy-1.19.4-cp39-cp39-manylinux1_i686.whl", hash = "sha256:a5d897c14513590a85774180be713f692df6fa8ecf6483e561a6d47309566f37"}, - {file = "numpy-1.19.4-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:a09f98011236a419ee3f49cedc9ef27d7a1651df07810ae430a6b06576e0b414"}, - {file = "numpy-1.19.4-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:50e86c076611212ca62e5a59f518edafe0c0730f7d9195fec718da1a5c2bb1fc"}, - {file = "numpy-1.19.4-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f0d3929fe88ee1c155129ecd82f981b8856c5d97bcb0d5f23e9b4242e79d1de3"}, - {file = "numpy-1.19.4-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:c42c4b73121caf0ed6cd795512c9c09c52a7287b04d105d112068c1736d7c753"}, - {file = "numpy-1.19.4-cp39-cp39-win32.whl", hash = "sha256:8cac8790a6b1ddf88640a9267ee67b1aee7a57dfa2d2dd33999d080bc8ee3a0f"}, - {file = "numpy-1.19.4-cp39-cp39-win_amd64.whl", hash = "sha256:4377e10b874e653fe96985c05feed2225c912e328c8a26541f7fc600fb9c637b"}, - {file = "numpy-1.19.4-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:2a2740aa9733d2e5b2dfb33639d98a64c3b0f24765fed86b0fd2aec07f6a0a08"}, - {file = "numpy-1.19.4.zip", hash = "sha256:141ec3a3300ab89c7f2b0775289954d193cc8edb621ea05f99db9cb181530512"}, + {file = "numpy-1.19.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cc6bd4fd593cb261332568485e20a0712883cf631f6f5e8e86a52caa8b2b50ff"}, + {file = "numpy-1.19.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:aeb9ed923be74e659984e321f609b9ba54a48354bfd168d21a2b072ed1e833ea"}, + {file = "numpy-1.19.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8b5e972b43c8fc27d56550b4120fe6257fdc15f9301914380b27f74856299fea"}, + {file = "numpy-1.19.5-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:43d4c81d5ffdff6bae58d66a3cd7f54a7acd9a0e7b18d97abb255defc09e3140"}, + {file = "numpy-1.19.5-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:a4646724fba402aa7504cd48b4b50e783296b5e10a524c7a6da62e4a8ac9698d"}, + {file = "numpy-1.19.5-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:2e55195bc1c6b705bfd8ad6f288b38b11b1af32f3c8289d6c50d47f950c12e76"}, + {file = "numpy-1.19.5-cp36-cp36m-win32.whl", hash = "sha256:39b70c19ec771805081578cc936bbe95336798b7edf4732ed102e7a43ec5c07a"}, + {file = "numpy-1.19.5-cp36-cp36m-win_amd64.whl", hash = "sha256:dbd18bcf4889b720ba13a27ec2f2aac1981bd41203b3a3b27ba7a33f88ae4827"}, + {file = "numpy-1.19.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:603aa0706be710eea8884af807b1b3bc9fb2e49b9f4da439e76000f3b3c6ff0f"}, + {file = "numpy-1.19.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:cae865b1cae1ec2663d8ea56ef6ff185bad091a5e33ebbadd98de2cfa3fa668f"}, + {file = "numpy-1.19.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:36674959eed6957e61f11c912f71e78857a8d0604171dfd9ce9ad5cbf41c511c"}, + {file = "numpy-1.19.5-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:06fab248a088e439402141ea04f0fffb203723148f6ee791e9c75b3e9e82f080"}, + {file = "numpy-1.19.5-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6149a185cece5ee78d1d196938b2a8f9d09f5a5ebfbba66969302a778d5ddd1d"}, + {file = "numpy-1.19.5-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:50a4a0ad0111cc1b71fa32dedd05fa239f7fb5a43a40663269bb5dc7877cfd28"}, + {file = "numpy-1.19.5-cp37-cp37m-win32.whl", hash = "sha256:d051ec1c64b85ecc69531e1137bb9751c6830772ee5c1c426dbcfe98ef5788d7"}, + {file = "numpy-1.19.5-cp37-cp37m-win_amd64.whl", hash = "sha256:a12ff4c8ddfee61f90a1633a4c4afd3f7bcb32b11c52026c92a12e1325922d0d"}, + {file = "numpy-1.19.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cf2402002d3d9f91c8b01e66fbb436a4ed01c6498fffed0e4c7566da1d40ee1e"}, + {file = "numpy-1.19.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1ded4fce9cfaaf24e7a0ab51b7a87be9038ea1ace7f34b841fe3b6894c721d1c"}, + {file = "numpy-1.19.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:012426a41bc9ab63bb158635aecccc7610e3eff5d31d1eb43bc099debc979d94"}, + {file = "numpy-1.19.5-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:759e4095edc3c1b3ac031f34d9459fa781777a93ccc633a472a5468587a190ff"}, + {file = "numpy-1.19.5-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:a9d17f2be3b427fbb2bce61e596cf555d6f8a56c222bd2ca148baeeb5e5c783c"}, + {file = "numpy-1.19.5-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:99abf4f353c3d1a0c7a5f27699482c987cf663b1eac20db59b8c7b061eabd7fc"}, + {file = "numpy-1.19.5-cp38-cp38-win32.whl", hash = "sha256:384ec0463d1c2671170901994aeb6dce126de0a95ccc3976c43b0038a37329c2"}, + {file = "numpy-1.19.5-cp38-cp38-win_amd64.whl", hash = "sha256:811daee36a58dc79cf3d8bdd4a490e4277d0e4b7d103a001a4e73ddb48e7e6aa"}, + {file = "numpy-1.19.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c843b3f50d1ab7361ca4f0b3639bf691569493a56808a0b0c54a051d260b7dbd"}, + {file = "numpy-1.19.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d6631f2e867676b13026e2846180e2c13c1e11289d67da08d71cacb2cd93d4aa"}, + {file = "numpy-1.19.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7fb43004bce0ca31d8f13a6eb5e943fa73371381e53f7074ed21a4cb786c32f8"}, + {file = "numpy-1.19.5-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2ea52bd92ab9f768cc64a4c3ef8f4b2580a17af0a5436f6126b08efbd1838371"}, + {file = "numpy-1.19.5-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:400580cbd3cff6ffa6293df2278c75aef2d58d8d93d3c5614cd67981dae68ceb"}, + {file = "numpy-1.19.5-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:df609c82f18c5b9f6cb97271f03315ff0dbe481a2a02e56aeb1b1a985ce38e60"}, + {file = "numpy-1.19.5-cp39-cp39-win32.whl", hash = "sha256:ab83f24d5c52d60dbc8cd0528759532736b56db58adaa7b5f1f76ad551416a1e"}, + {file = "numpy-1.19.5-cp39-cp39-win_amd64.whl", hash = "sha256:0eef32ca3132a48e43f6a0f5a82cb508f22ce5a3d6f67a8329c81c8e226d3f6e"}, + {file = "numpy-1.19.5-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:a0d53e51a6cb6f0d9082decb7a4cb6dfb33055308c4c44f53103c073f649af73"}, + {file = "numpy-1.19.5.zip", hash = "sha256:a76f502430dd98d7546e1ea2250a7360c065a5fdea52b2dffe8ae7180909b6f4"}, ] orderedmultidict = [ {file = "orderedmultidict-1.0.1-py2.py3-none-any.whl", hash = "sha256:43c839a17ee3cdd62234c47deca1a8508a3f2ca1d0678a3bf791c87cf84adbf3"}, {file = "orderedmultidict-1.0.1.tar.gz", hash = "sha256:04070bbb5e87291cc9bfa51df413677faf2141c73c61d2a5f7b26bea3cd882ad"}, ] packaging = [ - {file = "packaging-20.8-py2.py3-none-any.whl", hash = "sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858"}, - {file = "packaging-20.8.tar.gz", hash = "sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"}, + {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, + {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, ] parso = [ {file = "parso-0.8.1-py2.py3-none-any.whl", hash = "sha256:15b00182f472319383252c18d5913b69269590616c947747bc50bf4ac768f410"}, @@ -2032,8 +2071,8 @@ pbr = [ {file = "pbr-5.5.1.tar.gz", hash = "sha256:5fad80b613c402d5b7df7bd84812548b2a61e9977387a80a5fc5c396492b13c9"}, ] pdocs = [ - {file = "pdocs-1.0.2-py3-none-any.whl", hash = "sha256:4d5ff87babcd0c46f12b76c887d53225bddb389dee7c6b338dbe281c729fc035"}, - {file = "pdocs-1.0.2.tar.gz", hash = "sha256:2e32432bd2736fd678ac1ce4447cd508deb62b5a12f7ba3bf0e3a374063221e2"}, + {file = "pdocs-1.1.1-py3-none-any.whl", hash = "sha256:4f5116cf5ce0fa9f13171cd74db224636d4d71370115eefce22d8945526fcfc0"}, + {file = "pdocs-1.1.1.tar.gz", hash = "sha256:f148034970220c9e05d2e04d8eb3fcec3575cf480af0966123ef9d6621b46e4f"}, ] pep517 = [ {file = "pep517-0.9.1-py2.py3-none-any.whl", hash = "sha256:3985b91ebf576883efe5fa501f42a16de2607684f3797ddba7202b71b7d0da51"}, @@ -2075,8 +2114,8 @@ pluggy = [ {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, ] portray = [ - {file = "portray-1.4.0-py3-none-any.whl", hash = "sha256:a6a06042a6b7fcb876b1e6cdcaee5adaeb6751388cb292fc05b2f31b1a4c3fb2"}, - {file = "portray-1.4.0.tar.gz", hash = "sha256:ea2271c5e3fbe956070a6f8b1aee6dc3d6a66c18c11907e878db8faa6fd2c449"}, + {file = "portray-1.5.2-py3-none-any.whl", hash = "sha256:40c6dfbff392f16a5c56c93aa79356b4ee25aa2d00579d61b560445ec4d3dc47"}, + {file = "portray-1.5.2.tar.gz", hash = "sha256:c8a3d489cfc95df6922868971996bb23584d37e46aa55e7ab9b6d29aa49f109d"}, ] poyo = [ {file = "poyo-0.5.0-py2.py3-none-any.whl", hash = "sha256:3e2ca8e33fdc3c411cd101ca395668395dd5dc7ac775b8e809e3def9f9fe041a"}, @@ -2131,8 +2170,8 @@ pyflakes = [ {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"}, ] pygments = [ - {file = "Pygments-2.7.3-py3-none-any.whl", hash = "sha256:f275b6c0909e5dafd2d6269a656aa90fa58ebf4a74f8fcf9053195d226b24a08"}, - {file = "Pygments-2.7.3.tar.gz", hash = "sha256:ccf3acacf3782cbed4a989426012f1c535c9a90d3a7fc3f16d231b9372d2b716"}, + {file = "Pygments-2.8.0-py3-none-any.whl", hash = "sha256:b21b072d0ccdf29297a82a2363359d99623597b8a265b8081760e4d0f7153c88"}, + {file = "Pygments-2.8.0.tar.gz", hash = "sha256:37a13ba168a02ac54cc5891a42b1caec333e59b66addb7fa633ea8a6d73445c0"}, ] pylama = [ {file = "pylama-7.7.1-py2.py3-none-any.whl", hash = "sha256:fd61c11872d6256b019ef1235be37b77c922ef37ac9797df6bd489996dddeb15"}, @@ -2147,12 +2186,12 @@ pyparsing = [ {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, ] pytest = [ - {file = "pytest-6.2.1-py3-none-any.whl", hash = "sha256:1969f797a1a0dbd8ccf0fecc80262312729afea9c17f1d70ebf85c5e76c6f7c8"}, - {file = "pytest-6.2.1.tar.gz", hash = "sha256:66e419b1899bc27346cb2c993e12c5e5e8daba9073c1fbce33b9807abc95c306"}, + {file = "pytest-6.2.2-py3-none-any.whl", hash = "sha256:b574b57423e818210672e07ca1fa90aaf194a4f63f3ab909a2c67ebb22913839"}, + {file = "pytest-6.2.2.tar.gz", hash = "sha256:9d1edf9e7d0b84d72ea3dbcdfd22b35fb543a5e8f2a60092dd578936bf63d7f9"}, ] pytest-cov = [ - {file = "pytest-cov-2.10.1.tar.gz", hash = "sha256:47bd0ce14056fdd79f93e1713f88fad7bdcc583dcd7783da86ef2f085a0bb88e"}, - {file = "pytest_cov-2.10.1-py2.py3-none-any.whl", hash = "sha256:45ec2d5182f89a81fc3eb29e3d1ed3113b9e9a873bcddb2a71faaab066110191"}, + {file = "pytest-cov-2.11.1.tar.gz", hash = "sha256:359952d9d39b9f822d9d29324483e7ba04a3a17dd7d05aa6beb7ea01e359e5f7"}, + {file = "pytest_cov-2.11.1-py2.py3-none-any.whl", hash = "sha256:bdb9fdb0b85a7cc825269a4c56b48ccaa5c7e365054b6038772c32ddcdc969da"}, ] pytest-mock = [ {file = "pytest-mock-1.13.0.tar.gz", hash = "sha256:e24a911ec96773022ebcc7030059b57cd3480b56d4f5d19b7c370ec635e6aed5"}, @@ -2166,19 +2205,27 @@ python-slugify = [ {file = "python-slugify-4.0.1.tar.gz", hash = "sha256:69a517766e00c1268e5bbfc0d010a0a8508de0b18d30ad5a1ff357f8ae724270"}, ] pyyaml = [ - {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, - {file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"}, - {file = "PyYAML-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2"}, - {file = "PyYAML-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c"}, - {file = "PyYAML-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2"}, - {file = "PyYAML-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648"}, - {file = "PyYAML-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"}, - {file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"}, - {file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"}, - {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"}, - {file = "PyYAML-5.3.1-cp39-cp39-win32.whl", hash = "sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a"}, - {file = "PyYAML-5.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:6034f55dab5fea9e53f436aa68fa3ace2634918e8b5994d82f3621c04ff5ed2e"}, - {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, + {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, + {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, + {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"}, + {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"}, + {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"}, + {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"}, + {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"}, + {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"}, + {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"}, + {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"}, + {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"}, + {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"}, + {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"}, + {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"}, + {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"}, + {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"}, + {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"}, + {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"}, + {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"}, + {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, + {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, ] regex = [ {file = "regex-2020.11.13-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85"}, @@ -2236,16 +2283,16 @@ rfc3986 = [ {file = "rfc3986-1.4.0.tar.gz", hash = "sha256:112398da31a3344dc25dbf477d8df6cb34f9278a94fee2625d89e4514be8bb9d"}, ] safety = [ - {file = "safety-1.10.0-py2.py3-none-any.whl", hash = "sha256:69437acf5dd617abd7086ccd0d50e813e67aa969bb9ca90f1847d5fbea047dcc"}, - {file = "safety-1.10.0.tar.gz", hash = "sha256:2ebc71b44666588d7898905d86d575933fcd5fa3c92d301ed12482602b1e928a"}, + {file = "safety-1.10.3-py2.py3-none-any.whl", hash = "sha256:5f802ad5df5614f9622d8d71fedec2757099705c2356f862847c58c6dfe13e84"}, + {file = "safety-1.10.3.tar.gz", hash = "sha256:30e394d02a20ac49b7f65292d19d38fa927a8f9582cdfd3ad1adbbc66c641ad5"}, ] six = [ {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, ] smmap = [ - {file = "smmap-3.0.4-py2.py3-none-any.whl", hash = "sha256:54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4"}, - {file = "smmap-3.0.4.tar.gz", hash = "sha256:9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24"}, + {file = "smmap-3.0.5-py2.py3-none-any.whl", hash = "sha256:7bfcf367828031dc893530a29cb35eb8c8f2d7c8f2d0989354d75d24c8573714"}, + {file = "smmap-3.0.5.tar.gz", hash = "sha256:84c2751ef3072d4f6b2785ec7ee40244c6f45eb934d9e543e2c51f1bd3d54c50"}, ] smmap2 = [ {file = "smmap2-3.0.1-py3-none-any.whl", hash = "sha256:0cb6ea470b1ad9a65a02ca7f4c7ae601861f7dd24a43812ca51cfca2892bb524"}, @@ -2256,8 +2303,8 @@ sniffio = [ {file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"}, ] snowballstemmer = [ - {file = "snowballstemmer-2.0.0-py2.py3-none-any.whl", hash = "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0"}, - {file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"}, + {file = "snowballstemmer-2.1.0-py2.py3-none-any.whl", hash = "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2"}, + {file = "snowballstemmer-2.1.0.tar.gz", hash = "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"}, ] sortedcontainers = [ {file = "sortedcontainers-2.3.0-py2.py3-none-any.whl", hash = "sha256:37257a32add0a3ee490bb170b599e93095eed89a55da91fa9f48753ea12fd73f"}, @@ -2323,8 +2370,8 @@ tornado = [ {file = "tornado-6.1.tar.gz", hash = "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791"}, ] tqdm = [ - {file = "tqdm-4.55.0-py2.py3-none-any.whl", hash = "sha256:0cd81710de29754bf17b6fee07bdb86f956b4fa20d3078f02040f83e64309416"}, - {file = "tqdm-4.55.0.tar.gz", hash = "sha256:f4f80b96e2ceafea69add7bf971b8403b9cba8fb4451c1220f91c79be4ebd208"}, + {file = "tqdm-4.58.0-py2.py3-none-any.whl", hash = "sha256:2c44efa73b8914dba7807aefd09653ac63c22b5b4ea34f7a80973f418f1a3089"}, + {file = "tqdm-4.58.0.tar.gz", hash = "sha256:c23ac707e8e8aabb825e4d91f8e17247f9cc14b0d64dd9e97be0781e9e525bba"}, ] traitlets = [ {file = "traitlets-4.3.3-py2.py3-none-any.whl", hash = "sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44"}, @@ -2377,8 +2424,8 @@ typing-inspect = [ {file = "typing_inspect-0.6.0.tar.gz", hash = "sha256:8f1b1dd25908dbfd81d3bebc218011531e7ab614ba6e5bf7826d887c834afab7"}, ] urllib3 = [ - {file = "urllib3-1.26.2-py2.py3-none-any.whl", hash = "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473"}, - {file = "urllib3-1.26.2.tar.gz", hash = "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08"}, + {file = "urllib3-1.26.3-py2.py3-none-any.whl", hash = "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80"}, + {file = "urllib3-1.26.3.tar.gz", hash = "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73"}, ] vistir = [ {file = "vistir-0.5.2-py2.py3-none-any.whl", hash = "sha256:a37079cdbd85d31a41cdd18457fe521e15ec08b255811e81aa061fd5f48a20fb"}, From 3de261f784dfea5729a06c6d8a4dddf82a715caa Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 1 Mar 2021 00:12:35 -0800 Subject: [PATCH 1407/1439] Add test for allowing root, switch to allow root command --- isort/main.py | 12 +++++------- tests/unit/test_main.py | 5 +++++ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/isort/main.py b/isort/main.py index 648efd012..0dedcd408 100644 --- a/isort/main.py +++ b/isort/main.py @@ -348,10 +348,10 @@ def _build_arg_parser() -> argparse.ArgumentParser: help="Provide the filename associated with a stream.", ) target_group.add_argument( - "--no-preserve-root", + "--allow-root", action="store_true", default=False, - help="Tells isort not to treat / specially, allowing it to be ran on the root dir.", + help="Tells isort not to treat / specially, allowing it to be ran against the root dir.", ) output_group.add_argument( @@ -1006,7 +1006,7 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = remapped_deprecated_args = config_dict.pop("remapped_deprecated_args", False) stream_filename = config_dict.pop("filename", None) ext_format = config_dict.pop("ext_format", None) - no_preserve_root = config_dict.pop("no_preserve_root", None) + allow_root = config_dict.pop("allow_root", None) wrong_sorted_files = False all_attempt_broken = False no_valid_encodings = False @@ -1044,18 +1044,16 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = file_path=file_path, extension=ext_format, ) - elif "/" in file_names and not no_preserve_root: + elif "/" in file_names and not allow_root: printer = create_terminal_printer(color=config.color_output) printer.error("it is dangerous to operate recursively on '/'") - printer.error("use --no-preserve-root to override this failsafe") + printer.error("use --allow-root to override this failsafe") sys.exit(1) else: if stream_filename: printer = create_terminal_printer(color=config.color_output) printer.error("Filename override is intended only for stream (-) sorting.") sys.exit(1) - if file_names == ["/"]: - input("You've requested to run isort against your entire computer (/) are you sure? ") skipped: List[str] = [] broken: List[str] = [] diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 9a78cbad8..133a94883 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -145,6 +145,11 @@ def test_missing_default_section(tmpdir): main.main([str(python_file)]) +def test_ran_against_root(): + with pytest.raises(SystemExit): + main.main(["/"]) + + def test_main(capsys, tmpdir): base_args = [ "-sp", From 4acbb6da9480ed35a22cd097949eb01c8c13fd4a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 2 Mar 2021 23:03:50 -0800 Subject: [PATCH 1408/1439] Create failing test for issue #1667 --- tests/unit/test_regressions.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index 749bf6307..a823d398e 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -1568,3 +1568,20 @@ def spam(): ) == snippet ) + + +def test_isort_shouldnt_add_extra_line_float_to_top_issue_1667(): + assert isort.check_code( + """ + import sys + +sys.path.insert(1, 'path/containing/something_else/..') + +import something_else # isort:skip + +# Some constant +SOME_CONSTANT = 4 +""", + show_diff=True, + float_to_top=True, + ) From dd1059e91e426ab87aee2d0ac8cfb4c6a727714f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 3 Mar 2021 23:19:13 -0800 Subject: [PATCH 1409/1439] Fix indentation --- tests/unit/test_regressions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index a823d398e..7d311f3c2 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -1573,7 +1573,7 @@ def spam(): def test_isort_shouldnt_add_extra_line_float_to_top_issue_1667(): assert isort.check_code( """ - import sys +import sys sys.path.insert(1, 'path/containing/something_else/..') From 7508a0afac727b3ccda9e038b354de0c913ba4cb Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 3 Mar 2021 23:35:19 -0800 Subject: [PATCH 1410/1439] Don't add newlines if there are is no output --- isort/output.py | 77 +++++++++++++++++++++++++------------------------ 1 file changed, 40 insertions(+), 37 deletions(-) diff --git a/isort/output.py b/isort/output.py index 017ae4d4e..958a81336 100644 --- a/isort/output.py +++ b/isort/output.py @@ -148,43 +148,46 @@ def sorted_imports( output_at = parsed.import_index formatted_output[output_at:0] = output - imports_tail = output_at + len(output) - while [ - character.strip() for character in formatted_output[imports_tail : imports_tail + 1] - ] == [""]: - formatted_output.pop(imports_tail) - - if len(formatted_output) > imports_tail: - next_construct = "" - tail = formatted_output[imports_tail:] - - for index, line in enumerate(tail): - should_skip, in_quote, *_ = parse.skip_line( - line, - in_quote="", - index=len(formatted_output), - section_comments=config.section_comments, - needs_import=False, - ) - if not should_skip and line.strip(): - if ( - line.strip().startswith("#") - and len(tail) > (index + 1) - and tail[index + 1].strip() - ): - continue - next_construct = line - break - if in_quote: - next_construct = line - break - - if config.lines_after_imports != -1: - formatted_output[imports_tail:0] = ["" for line in range(config.lines_after_imports)] - elif extension != "pyi" and next_construct.startswith(STATEMENT_DECLARATIONS): - formatted_output[imports_tail:0] = ["", ""] - else: - formatted_output[imports_tail:0] = [""] + if output: + imports_tail = output_at + len(output) + while [ + character.strip() for character in formatted_output[imports_tail : imports_tail + 1] + ] == [""]: + formatted_output.pop(imports_tail) + + if len(formatted_output) > imports_tail: + next_construct = "" + tail = formatted_output[imports_tail:] + + for index, line in enumerate(tail): + should_skip, in_quote, *_ = parse.skip_line( + line, + in_quote="", + index=len(formatted_output), + section_comments=config.section_comments, + needs_import=False, + ) + if not should_skip and line.strip(): + if ( + line.strip().startswith("#") + and len(tail) > (index + 1) + and tail[index + 1].strip() + ): + continue + next_construct = line + break + if in_quote: + next_construct = line + break + + if config.lines_after_imports != -1: + formatted_output[imports_tail:0] = [ + "" for line in range(config.lines_after_imports) + ] + elif extension != "pyi" and next_construct.startswith(STATEMENT_DECLARATIONS): + formatted_output[imports_tail:0] = ["", ""] + else: + formatted_output[imports_tail:0] = [""] if parsed.place_imports: new_out_lines = [] From f62e6be92d0055750011b14a166c1fb49e381cad Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 3 Mar 2021 23:36:27 -0800 Subject: [PATCH 1411/1439] Fixed #1667: Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b325025a..dd6f8163a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ ### 5.8.0 TBD - Fixed #1631: as import comments can in some cases be duplicated. + - Fixed #1667: extra newline added with float-to-top, after skip, in some cases. - Implemented #1648: Export MyPY type hints. - Implemented #1641: Identified import statements now return runnable code. - Implemented #1661: Added "wemake" profile. From aed861e07711996317f049be2e7ea5fc8fc31c76 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 3 Mar 2021 23:42:12 -0800 Subject: [PATCH 1412/1439] Merge if statements per deepsource --- isort/output.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/isort/output.py b/isort/output.py index 958a81336..6b3401089 100644 --- a/isort/output.py +++ b/isort/output.py @@ -220,20 +220,20 @@ def _with_from_imports( import_start = f"from {module} {import_type} " from_imports = list(parsed.imports[section]["from"][module]) - if not config.no_inline_sort or ( - config.force_single_line and module not in config.single_line_exclusions - ): - if not config.only_sections: - from_imports = sorting.naturally( - from_imports, - key=lambda key: sorting.module_key( - key, - config, - True, - config.force_alphabetical_sort_within_sections, - section_name=section, - ), - ) + if ( + not config.no_inline_sort + or (config.force_single_line and module not in config.single_line_exclusions) + ) and not config.only_sections: + from_imports = sorting.naturally( + from_imports, + key=lambda key: sorting.module_key( + key, + config, + True, + config.force_alphabetical_sort_within_sections, + section_name=section, + ), + ) if remove_imports: from_imports = [ line for line in from_imports if f"{module}.{line}" not in remove_imports From e09bbbcf07bf600c7d67cf794868e629681e3cca Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 4 Mar 2021 20:05:19 -0800 Subject: [PATCH 1413/1439] Implemented #1638 / #1644: Provide a flag to ensure same file handle is used after sorting. --- CHANGELOG.md | 1 + isort/api.py | 5 ++++- isort/main.py | 8 ++++++++ isort/settings.py | 1 + tests/unit/test_api.py | 5 +++++ tests/unit/test_main.py | 1 + 6 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd6f8163a..03d415f50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ - Implemented #1661: Added "wemake" profile. - Implemented #1669: Parallel (`-j`) now defaults to number of CPU cores if no value is provided. - Implemented #1668: Added a safeguard against accidental usage against /. + - Implemented #1638 / #1644: Provide a flag `--overwrite-in-place` to ensure same file handle is used after sorting. ### 5.7.0 December 30th 2020 - Fixed #1612: In rare circumstances an extra comma is added after import and before comment. diff --git a/isort/api.py b/isort/api.py index 024b75ac5..0cf70845f 100644 --- a/isort/api.py +++ b/isort/api.py @@ -384,7 +384,10 @@ def sort_file( ): return False source_file.stream.close() - tmp_file.replace(source_file.path) + if config.overwrite_in_place: + source_file.path.write_bytes(tmp_file.read_bytes()) + else: + tmp_file.replace(source_file.path) if not config.quiet: print(f"Fixing {source_file.path}") finally: diff --git a/isort/main.py b/isort/main.py index 0dedcd408..bdf6a89dd 100644 --- a/isort/main.py +++ b/isort/main.py @@ -207,6 +207,14 @@ def _build_arg_parser() -> argparse.ArgumentParser: dest="write_to_stdout", action="store_true", ) + general_group.add_argument( + "--overwrite-in-place", + help="Tells isort to overwrite in place using the same file handle." + "Comes at a performance and memory usage penalty over it's standard " + "approach but ensures all file flags and modes stay unchanged.", + dest="overwrite_in_place", + action="store_true", + ) general_group.add_argument( "--show-config", dest="show_config", diff --git a/isort/settings.py b/isort/settings.py index 1ea12179e..c7effa5d5 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -206,6 +206,7 @@ class _Config: follow_links: bool = True indented_import_headings: bool = True honor_case_in_force_sorted_sections: bool = False + overwrite_in_place: bool = False def __post_init__(self): py_version = self.py_version diff --git a/tests/unit/test_api.py b/tests/unit/test_api.py index 2247d8fc5..bffa7fca2 100644 --- a/tests/unit/test_api.py +++ b/tests/unit/test_api.py @@ -34,6 +34,11 @@ def test_sort_file(imperfect) -> None: assert imperfect.read() == fixed_content +def test_sort_file_in_place(imperfect) -> None: + assert api.sort_file(imperfect, overwrite_in_place=True) + assert imperfect.read() == fixed_content + + def test_sort_file_to_stdout(capsys, imperfect) -> None: assert api.sort_file(imperfect, write_to_stdout=True) out, _ = capsys.readouterr() diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 133a94883..eb1970901 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -78,6 +78,7 @@ def test_parse_args(): assert main.parse_args(["--csi"]) == {"combine_straight_imports": True} assert main.parse_args(["--combine-straight-imports"]) == {"combine_straight_imports": True} assert main.parse_args(["--dont-follow-links"]) == {"follow_links": False} + assert main.parse_args(["--overwrite-in-place"]) == {"overwrite_in_place": True} def test_ascii_art(capsys): From 6b0b85f6bd2f18d8e507dc530e8730a381586f72 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 5 Mar 2021 23:54:47 -0800 Subject: [PATCH 1414/1439] Merge combinabele if statements as recommended by deepsource --- isort/api.py | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/isort/api.py b/isort/api.py index 0cf70845f..a861da122 100644 --- a/isort/api.py +++ b/isort/api.py @@ -165,9 +165,8 @@ def sort_stream( config = _config(path=file_path, config=config, **config_kwargs) content_source = str(file_path or "Passed in content") - if not disregard_skip: - if file_path and config.is_skipped(file_path): - raise FileSkipSetting(content_source) + if not disregard_skip and file_path and config.is_skipped(file_path): + raise FileSkipSetting(content_source) _internal_output = output_stream @@ -404,17 +403,16 @@ def sort_file( disregard_skip=disregard_skip, extension=extension, ) - if changed: - if show_diff: - source_file.stream.seek(0) - output.seek(0) - show_unified_diff( - file_input=source_file.stream.read(), - file_output=output.read(), - file_path=actual_file_path, - output=None if show_diff is True else cast(TextIO, show_diff), - color_output=config.color_output, - ) + if changed and show_diff: + source_file.stream.seek(0) + output.seek(0) + show_unified_diff( + file_input=source_file.stream.read(), + file_output=output.read(), + file_path=actual_file_path, + output=None if show_diff is True else cast(TextIO, show_diff), + color_output=config.color_output, + ) source_file.stream.close() except ExistingSyntaxErrors: @@ -556,13 +554,12 @@ def find_imports_in_paths( def _config( path: Optional[Path] = None, config: Config = DEFAULT_CONFIG, **config_kwargs ) -> Config: - if path: - if ( - config is DEFAULT_CONFIG - and "settings_path" not in config_kwargs - and "settings_file" not in config_kwargs - ): - config_kwargs["settings_path"] = path + if path and ( + config is DEFAULT_CONFIG + and "settings_path" not in config_kwargs + and "settings_file" not in config_kwargs + ): + config_kwargs["settings_path"] = path if config_kwargs: if config is not DEFAULT_CONFIG: From fce310f4157b3d185d9c8076cb191a7de1506daf Mon Sep 17 00:00:00 2001 From: gofr <32750931+gofr@users.noreply.github.com> Date: Thu, 11 Mar 2021 17:37:40 +0100 Subject: [PATCH 1415/1439] Clarify documentation for --skip option The --skip option only handles full file paths or names of individual path components. For example, "foo/bar/baz.py" can be used to skip just that file. And "foo" would skip any files with that name and any files nested in directories with that name. On the other hand, "foo/bar" does *not* skip everything in the "foo/bar" directory. --skip-glob can be used to achieve this. Attempt to clarify the documentation to say that. --- isort/main.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/isort/main.py b/isort/main.py index 66df2be51..f759429ee 100644 --- a/isort/main.py +++ b/isort/main.py @@ -312,7 +312,9 @@ def _build_arg_parser() -> argparse.ArgumentParser: "-s", "--skip", help="Files that sort imports should skip over. If you want to skip multiple " - "files you should specify twice: --skip file1 --skip file2.", + "files you should specify twice: --skip file1 --skip file2. Values can be ", + "file names, directory names or file paths. To skip all files in a nested path " + "use --skip-glob." dest="skip", action="append", ) From 6d885b5e3770e2e2bfb69522a97b4800c6bf3558 Mon Sep 17 00:00:00 2001 From: gofr <32750931+gofr@users.noreply.github.com> Date: Thu, 11 Mar 2021 17:50:52 +0100 Subject: [PATCH 1416/1439] fixup! Clarify documentation for --skip option --- isort/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/main.py b/isort/main.py index f759429ee..32fd4604b 100644 --- a/isort/main.py +++ b/isort/main.py @@ -314,7 +314,7 @@ def _build_arg_parser() -> argparse.ArgumentParser: help="Files that sort imports should skip over. If you want to skip multiple " "files you should specify twice: --skip file1 --skip file2. Values can be ", "file names, directory names or file paths. To skip all files in a nested path " - "use --skip-glob." + "use --skip-glob.", dest="skip", action="append", ) From 6fb82c3c802d6e8d25783987eb93fe9c5b9dddba Mon Sep 17 00:00:00 2001 From: gofr <32750931+gofr@users.noreply.github.com> Date: Thu, 11 Mar 2021 17:59:08 +0100 Subject: [PATCH 1417/1439] fixup! fixup! Clarify documentation for --skip option --- isort/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/main.py b/isort/main.py index 32fd4604b..38d9a4bbb 100644 --- a/isort/main.py +++ b/isort/main.py @@ -312,7 +312,7 @@ def _build_arg_parser() -> argparse.ArgumentParser: "-s", "--skip", help="Files that sort imports should skip over. If you want to skip multiple " - "files you should specify twice: --skip file1 --skip file2. Values can be ", + "files you should specify twice: --skip file1 --skip file2. Values can be " "file names, directory names or file paths. To skip all files in a nested path " "use --skip-glob.", dest="skip", From c4216b10473c2f9073052a32a43e93048fee96cc Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 11 Mar 2021 22:03:38 -0800 Subject: [PATCH 1418/1439] Update config option docs --- docs/configuration/options.md | 63 +++++++++++++++++++++++++++-------- 1 file changed, 49 insertions(+), 14 deletions(-) diff --git a/docs/configuration/options.md b/docs/configuration/options.md index a44e2b30d..5ecb6c5ea 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -33,7 +33,7 @@ Force specific imports to the top of their appropriate section. ## Skip -Files that sort imports should skip over. If you want to skip multiple files you should specify twice: --skip file1 --skip file2. +Files that sort imports should skip over. If you want to skip multiple files you should specify twice: --skip file1 --skip file2. Values can be file names, directory names or file paths. To skip all files in a nested path use --skip-glob. **Type:** Frozenset **Default:** `('.bzr', '.direnv', '.eggs', '.git', '.hg', '.mypy_cache', '.nox', '.pants.d', '.svn', '.tox', '.venv', '_build', 'buck-out', 'build', 'dist', 'node_modules', 'venv')` @@ -226,7 +226,7 @@ known_airflow = ['airflow'] ## Multi Line Output -Multi line output (0-grid, 1-vertical, 2-hanging, 3-vert-hanging, 4-vert-grid, 5-vert-grid-grouped, 6-vert-grid-grouped-no-comma, 7-noqa, 8-vertical-hanging-indent-bracket, 9-vertical-prefix-from-module-import, 10-hanging-indent-with-parentheses). +Multi line output (0-grid, 1-vertical, 2-hanging, 3-vert-hanging, 4-vert-grid, 5-vert-grid-grouped, 6-deprecated-alias-for-5, 7-noqa, 8-vertical-hanging-indent-bracket, 9-vertical-prefix-from-module-import, 10-hanging-indent-with-parentheses). **Type:** Wrapmodes **Default:** `WrapModes.GRID` @@ -752,7 +752,7 @@ Inserts a blank line before a comment following an import. ## Profile -Base profile type to use for configuration. Profiles include: black, django, pycharm, google, open_stack, plone, attrs, hug. As well as any shared profiles. +Base profile type to use for configuration. Profiles include: black, django, pycharm, google, open_stack, plone, attrs, hug, wemake. As well as any shared profiles. **Type:** String **Default:** `` @@ -1019,6 +1019,41 @@ Combines all the bare straight imports of the same section in a single line. Won **Python & Config File Name:** indented_import_headings **CLI Flags:** **Not Supported** +## Honor Case In Force Sorted Sections + +Honor `--case-sensitive` when `--force-sort-within-sections` is being used. Without this option set, `--order-by-type` decides module name ordering too. + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** honor_case_in_force_sorted_sections +**CLI Flags:** + +- --hcss +- --honor-case-in-force-sorted-sections + +## Sort Relative In Force Sorted Sections + +When using `--force-sort-within-sections`, sort relative imports the same way as they are sorted when not using that setting. + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** sort_relative_in_force_sorted_sections +**CLI Flags:** + +- --srss +- --sort-relative-in-force-sorted-sections + +## Overwrite In Place + +Tells isort to overwrite in place using the same file handle.Comes at a performance and memory usage penalty over it's standard approach but ensures all file flags and modes stay unchanged. + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** overwrite_in_place +**CLI Flags:** + +- --overwrite-in-place + ## Show Version Displays the currently installed version of isort. @@ -1178,6 +1213,17 @@ Provide the filename associated with a stream. - --filename +## Allow Root + +Tells isort not to treat / specially, allowing it to be ran against the root dir. + +**Type:** Bool +**Default:** `False` +**Python & Config File Name:** **Not Supported** +**CLI Flags:** + +- --allow-root + ## Dont Float To Top Forces --float-to-top setting off. See --float-to-top for more information. @@ -1215,17 +1261,6 @@ Tells isort to format the given files according to an extensions formatting rule - --ext-format -## Show Files - -See the files isort will be ran against with the current config options. - -**Type:** Bool -**Default:** `False` -**Python & Config File Name:** **Not Supported** -**CLI Flags:** - -- --show-files - ## Deprecated Flags ==SUPPRESS== From 7d0e94caf4250b12148b28bf5a09540bf00798f1 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 11 Mar 2021 22:05:22 -0800 Subject: [PATCH 1419/1439] Add - @dongfangtianyu to acknowledgements file --- docs/contributing/4.-acknowledgements.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/contributing/4.-acknowledgements.md b/docs/contributing/4.-acknowledgements.md index 15fff2a12..09e000a7c 100644 --- a/docs/contributing/4.-acknowledgements.md +++ b/docs/contributing/4.-acknowledgements.md @@ -216,6 +216,7 @@ Code Contributors - Quentin Santos (@qsantos) - @gofr - Pavel Savchenko (@asfaltboy) +- @dongfangtianyu Documenters =================== From 31e4fde04d00627977a27b8e083d562b61e2e608 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 12 Mar 2021 23:50:07 -0800 Subject: [PATCH 1420/1439] Added changelog entry for #1685 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03d415f50..4ce9f0224 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ - Implemented #1669: Parallel (`-j`) now defaults to number of CPU cores if no value is provided. - Implemented #1668: Added a safeguard against accidental usage against /. - Implemented #1638 / #1644: Provide a flag `--overwrite-in-place` to ensure same file handle is used after sorting. + - Documented #1685: Skip doesn't support plain directory names, but skip_glob does. ### 5.7.0 December 30th 2020 - Fixed #1612: In rare circumstances an extra comma is added after import and before comment. From d09acd0974847314278e18c92db13950555e0ae9 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 13 Mar 2021 18:41:15 -0800 Subject: [PATCH 1421/1439] Resolve #1684: Add support for --extend-skip and --extend-skip-glob --- CHANGELOG.md | 1 + isort/main.py | 20 ++++++++++++++++++-- isort/settings.py | 28 +++++++++++++++++++++++++--- tests/unit/test_main.py | 2 +- 4 files changed, 45 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ce9f0224..b0107444b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ - Implemented #1669: Parallel (`-j`) now defaults to number of CPU cores if no value is provided. - Implemented #1668: Added a safeguard against accidental usage against /. - Implemented #1638 / #1644: Provide a flag `--overwrite-in-place` to ensure same file handle is used after sorting. + - Implemented #1684: Added support for extending skips with `--extend-skip` and `--extend-skip-glob`. - Documented #1685: Skip doesn't support plain directory names, but skip_glob does. ### 5.7.0 December 30th 2020 diff --git a/isort/main.py b/isort/main.py index 38d9a4bbb..5ed22cc26 100644 --- a/isort/main.py +++ b/isort/main.py @@ -311,17 +311,33 @@ def _build_arg_parser() -> argparse.ArgumentParser: target_group.add_argument( "-s", "--skip", - help="Files that sort imports should skip over. If you want to skip multiple " + help="Files that isort should skip over. If you want to skip multiple " "files you should specify twice: --skip file1 --skip file2. Values can be " "file names, directory names or file paths. To skip all files in a nested path " "use --skip-glob.", dest="skip", action="append", ) + target_group.add_argument( + "--extend-skip", + help="Extends --skip to add additional files that isort should skip over. " + "If you want to skip multiple " + "files you should specify twice: --skip file1 --skip file2. Values can be " + "file names, directory names or file paths. To skip all files in a nested path " + "use --skip-glob.", + dest="extend_skip", + action="append", + ) target_group.add_argument( "--sg", "--skip-glob", - help="Files that sort imports should skip over.", + help="Files that isort should skip over.", + dest="skip_glob", + action="append", + ) + target_group.add_argument( + "--extend-skip-glob", + help="Additional files that isort should skip over (extending --skip-glob).", dest="skip_glob", action="append", ) diff --git a/isort/settings.py b/isort/settings.py index e52937721..a62c27bf6 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -121,7 +121,9 @@ class _Config: py_version: str = "3" force_to_top: FrozenSet[str] = frozenset() skip: FrozenSet[str] = DEFAULT_SKIP + extend_skip: FrozenSet[str] = frozenset() skip_glob: FrozenSet[str] = frozenset() + extend_skip_glob: FrozenSet[str] = frozenset() skip_gitignore: bool = False line_length: int = 79 wrap_length: int = 0 @@ -267,6 +269,8 @@ def __init__( ): self._known_patterns: Optional[List[Tuple[Pattern[str], str]]] = None self._section_comments: Optional[Tuple[str, ...]] = None + self._skips: Optional[FrozenSet[str]] = None + self._skip_globs: Optional[FrozenSet[str]] = None if config: config_vars = vars(config).copy() @@ -274,6 +278,8 @@ def __init__( config_vars["py_version"] = config_vars["py_version"].replace("py", "") config_vars.pop("_known_patterns") config_vars.pop("_section_comments") + config_vars.pop("_skips") + config_vars.pop("_skip_globs") super().__init__(**config_vars) # type: ignore return @@ -521,7 +527,7 @@ def is_skipped(self, file_path: Path) -> bool: if normalized_path[1:2] == ":": normalized_path = normalized_path[2:] - for skip_path in self.skip: + for skip_path in self.skips: if posixpath.abspath(normalized_path) == posixpath.abspath( skip_path.replace("\\", "/") ): @@ -529,11 +535,11 @@ def is_skipped(self, file_path: Path) -> bool: position = os.path.split(file_name) while position[1]: - if position[1] in self.skip: + if position[1] in self.skips: return True position = os.path.split(position[0]) - for glob in self.skip_glob: + for glob in self.skip_globs: if fnmatch.fnmatch(file_name, glob) or fnmatch.fnmatch("/" + file_name, glob): return True @@ -574,6 +580,22 @@ def section_comments(self) -> Tuple[str, ...]: self._section_comments = tuple(f"# {heading}" for heading in self.import_headings.values()) return self._section_comments + @property + def skips(self) -> FrozenSet[str]: + if self._skips is not None: + return self._skips + + self._skips = self.skip.union(self.extend_skip) + return self._skips + + @property + def skip_globs(self) -> FrozenSet[str]: + if self._skip_globs is not None: + return self._skip_globs + + self._skip_globs = self.skip_glob.union(self.extend_skip_glob) + return self._skip_globs + def _parse_known_pattern(self, pattern: str) -> List[str]: """Expand pattern if identified as a directory and return found sub packages""" if pattern.endswith(os.path.sep): diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index eb1970901..86f62db3b 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -326,7 +326,7 @@ def test_main(capsys, tmpdir): import b """ ) - main.main([str(tmpdir), "--skip", "skip.py", "--check"]) + main.main([str(tmpdir), "--extend-skip", "skip.py", "--check"]) # without filter options passed in should successfully sort files main.main([str(python_file), str(should_skip), "--verbose", "--atomic"]) From 3945b82808023f76227a29e2a5b5f451ccb4ad3f Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sun, 14 Mar 2021 20:42:49 +0100 Subject: [PATCH 1422/1439] GitHub Actions: Upgrade actions/cache https://github.com/actions/cache/releases --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 06780574e..857fb209f 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -13,7 +13,7 @@ jobs: - uses: actions/checkout@v2 - name: pip cache - uses: actions/cache@v1 + uses: actions/cache@v2 with: path: ~/.cache/pip key: lint-pip-${{ hashFiles('**/pyproject.toml') }} From 215ee2097e25202e699f6ecd8ba1056ff08b3049 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sun, 14 Mar 2021 20:43:35 +0100 Subject: [PATCH 1423/1439] setup-python --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 857fb209f..af9158bc9 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -21,7 +21,7 @@ jobs: lint-pip- - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} From 4299df9a8025846bef685324cb9c2fbbfd4ff999 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sun, 14 Mar 2021 20:46:35 +0100 Subject: [PATCH 1424/1439] Update test.yml --- .github/workflows/test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ef93a1eea..1238f7ff7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: Ubuntu cache - uses: actions/cache@v1 + uses: actions/cache@v2 if: startsWith(matrix.os, 'ubuntu') with: path: ~/.cache/pip @@ -24,7 +24,7 @@ jobs: ${{ matrix.os }}-${{ matrix.python-version }}- - name: macOS cache - uses: actions/cache@v1 + uses: actions/cache@v2 if: startsWith(matrix.os, 'macOS') with: path: ~/Library/Caches/pip @@ -34,7 +34,7 @@ jobs: ${{ matrix.os }}-${{ matrix.python-version }}- - name: Windows cache - uses: actions/cache@v1 + uses: actions/cache@v2 if: startsWith(matrix.os, 'windows') with: path: c:\users\runneradmin\appdata\local\pip\cache @@ -44,7 +44,7 @@ jobs: ${{ matrix.os }}-${{ matrix.python-version }}- - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} From 748cbe461fb949ee833a934b65d3e413e850b40d Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sun, 14 Mar 2021 20:46:58 +0100 Subject: [PATCH 1425/1439] Update integration.yml --- .github/workflows/integration.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 283103118..3f66a7ef2 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -13,7 +13,7 @@ jobs: - uses: actions/checkout@v2 - name: pip cache - uses: actions/cache@v1 + uses: actions/cache@v2 with: path: ~/.cache/pip key: integration-pip-${{ hashFiles('**/pyproject.toml') }} @@ -21,7 +21,7 @@ jobs: integration-pip- - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} From ec3ac4dc2f2b77937321015393fac09173711c00 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 15 Mar 2021 22:40:49 -0700 Subject: [PATCH 1426/1439] Add entry to changelog for issue #1688 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0107444b..53271c104 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ - Implemented #1668: Added a safeguard against accidental usage against /. - Implemented #1638 / #1644: Provide a flag `--overwrite-in-place` to ensure same file handle is used after sorting. - Implemented #1684: Added support for extending skips with `--extend-skip` and `--extend-skip-glob`. + - Implemented #1688: Auto identification and skipping of some invalid import statements. - Documented #1685: Skip doesn't support plain directory names, but skip_glob does. ### 5.7.0 December 30th 2020 From 2bce0f2c197a6c7b70c7ae2a6c9c0cbded2a6600 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 15 Mar 2021 23:04:37 -0700 Subject: [PATCH 1427/1439] Add test case to ensure incorrectly formatted from statements dont disapear --- tests/unit/test_ticketed_features.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/unit/test_ticketed_features.py b/tests/unit/test_ticketed_features.py index 130ef67c7..dddb31bc2 100644 --- a/tests/unit/test_ticketed_features.py +++ b/tests/unit/test_ticketed_features.py @@ -1008,3 +1008,24 @@ def function(): import numpy as np """ ) + + +def test_isort_auto_detects_and_ignores_invalid_from_imports_issue_1688(): + """isort should automatically detect and ignore incorrectly written from import statements + see: https://github.com/PyCQA/isort/issues/1688 + """ + assert ( + isort.code( + """ +from package1 import alright +from package2 imprt and_its_gone +from package3 import also_ok +""" + ) + == """ +from package1 import alright + +from package2 imprt and_its_gone +from package3 import also_ok +""" + ) From 9c18cbb630749925b71f9904e4581a8657c33d3a Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 15 Mar 2021 23:04:57 -0700 Subject: [PATCH 1428/1439] Fix issue #1688: from statements removed when invalid --- isort/core.py | 67 ++++++++++++++++++++++++++++---------------------- isort/parse.py | 8 ++++++ 2 files changed, 45 insertions(+), 30 deletions(-) diff --git a/isort/core.py b/isort/core.py index 598789984..d763ad469 100644 --- a/isort/core.py +++ b/isort/core.py @@ -246,9 +246,6 @@ def process( ): import_section += line elif stripped_line.startswith(IMPORT_START_IDENTIFIERS): - did_contain_imports = contains_imports - contains_imports = True - new_indent = line[: -len(line.lstrip())] import_statement = line stripped_line = line.strip().split("#")[0] @@ -266,37 +263,47 @@ def process( stripped_line = line.strip().split("#")[0] import_statement += line - cimport_statement: bool = False if ( - import_statement.lstrip().startswith(CIMPORT_IDENTIFIERS) - or " cimport " in import_statement - or " cimport*" in import_statement - or " cimport(" in import_statement - or ".cimport" in import_statement - ): - cimport_statement = True - - if cimport_statement != cimports or ( - new_indent != indent - and import_section - and (not did_contain_imports or len(new_indent) < len(indent)) + import_statement.lstrip().startswith("from") + and "import" not in import_statement ): - indent = new_indent - if import_section: - next_cimports = cimport_statement - next_import_section = import_statement - import_statement = "" - not_imports = True - line = "" - else: - cimports = cimport_statement + line = import_statement + not_imports = True else: - if new_indent != indent: - if import_section and did_contain_imports: - import_statement = indent + import_statement.lstrip() + did_contain_imports = contains_imports + contains_imports = True + + cimport_statement: bool = False + if ( + import_statement.lstrip().startswith(CIMPORT_IDENTIFIERS) + or " cimport " in import_statement + or " cimport*" in import_statement + or " cimport(" in import_statement + or ".cimport" in import_statement + ): + cimport_statement = True + + if cimport_statement != cimports or ( + new_indent != indent + and import_section + and (not did_contain_imports or len(new_indent) < len(indent)) + ): + indent = new_indent + if import_section: + next_cimports = cimport_statement + next_import_section = import_statement + import_statement = "" + not_imports = True + line = "" else: - indent = new_indent - import_section += import_statement + cimports = cimport_statement + else: + if new_indent != indent: + if import_section and did_contain_imports: + import_statement = indent + import_statement.lstrip() + else: + indent = new_indent + import_section += import_statement else: not_imports = True diff --git a/isort/parse.py b/isort/parse.py index 307015e74..714e39abd 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -260,6 +260,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte for statement in statements: line, raw_line = _normalize_line(statement) type_of_import = import_type(line, config) or "" + raw_lines = [raw_line] if not type_of_import: out_lines.append(raw_line) continue @@ -288,6 +289,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte ): nested_comments[stripped_line] = comments[-1] import_string += line_separator + line + raw_lines.append(line) else: while line.strip().endswith("\\"): line, new_comment = parse_comments(in_lines[index]) @@ -310,6 +312,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte ): nested_comments[stripped_line] = comments[-1] import_string += line_separator + line + raw_lines.append(line) while not line.split("#")[0].strip().endswith(")") and index < line_count: line, new_comment = parse_comments(in_lines[index]) @@ -325,6 +328,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte ): nested_comments[stripped_line] = comments[-1] import_string += line_separator + line + raw_lines.append(line) stripped_line = _strip_syntax(line).strip() if ( @@ -348,6 +352,10 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte .replace("\\", " ") .replace("\n", " ") ) + if "import " not in import_string: + out_lines.extend(raw_lines) + continue + if " cimport " in import_string: parts = import_string.split(" cimport ") cimports = True From d4907b936e03a313c2b0305f63fe57a6c333cb52 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 16 Mar 2021 21:03:38 -0700 Subject: [PATCH 1429/1439] Add config option for reversing sort order --- isort/main.py | 6 ++++++ isort/settings.py | 1 + 2 files changed, 7 insertions(+) diff --git a/isort/main.py b/isort/main.py index 5ed22cc26..d16626f5f 100644 --- a/isort/main.py +++ b/isort/main.py @@ -562,6 +562,12 @@ def _build_arg_parser() -> argparse.ArgumentParser: action="store_true", help="Reverse order of relative imports.", ) + output_group.add_argument( + "--reverse-sort", + dest="reverse_sort", + action="store_true", + help="Reverses the ordering of imports.", + ) inline_args_group.add_argument( "--sl", "--force-single-line-imports", diff --git a/isort/settings.py b/isort/settings.py index a62c27bf6..dc3754ef8 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -210,6 +210,7 @@ class _Config: honor_case_in_force_sorted_sections: bool = False sort_relative_in_force_sorted_sections: bool = False overwrite_in_place: bool = False + reverse_sort: bool = False def __post_init__(self): py_version = self.py_version From 080501975cb6cf200d4fff6ae17e08eb82c8f65f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 16 Mar 2021 21:04:38 -0700 Subject: [PATCH 1430/1439] Add to changelog Implemented #1645: Ability to reverse the import sorting order. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53271c104..72bebd8c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ - Implemented #1638 / #1644: Provide a flag `--overwrite-in-place` to ensure same file handle is used after sorting. - Implemented #1684: Added support for extending skips with `--extend-skip` and `--extend-skip-glob`. - Implemented #1688: Auto identification and skipping of some invalid import statements. + - Implemented #1645: Ability to reverse the import sorting order. - Documented #1685: Skip doesn't support plain directory names, but skip_glob does. ### 5.7.0 December 30th 2020 From 7802b4f88e92a4a9534b9044527cd1f37c965755 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 17 Mar 2021 00:31:46 -0700 Subject: [PATCH 1431/1439] Add test case for issue #1645: Allowing sort order to be reversed --- tests/unit/test_ticketed_features.py | 30 ++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/unit/test_ticketed_features.py b/tests/unit/test_ticketed_features.py index dddb31bc2..6bed59a66 100644 --- a/tests/unit/test_ticketed_features.py +++ b/tests/unit/test_ticketed_features.py @@ -1029,3 +1029,33 @@ def test_isort_auto_detects_and_ignores_invalid_from_imports_issue_1688(): from package3 import also_ok """ ) + + +def test_isort_allows_reversing_sort_order_issue_1645(): + """isort allows reversing the sort order for those who prefer Z or longer imports first. + see: https://github.com/PyCQA/isort/issues/1688 + """ + assert ( + isort.code( + """ +from xxx import ( + g, + hi, + def, + abcd, +) +""", + profile="black", + reverse_sort=True, + length_sort=True, + line_length=20, + ) + == """ +from xxx import ( + abcd, + def, + hi, + g, +) +""" + ) From cb896cd47962034f681171d3eced45a82f0ddf0b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 17 Mar 2021 00:32:36 -0700 Subject: [PATCH 1432/1439] Add support for reversing import sort --- isort/output.py | 6 +++++- isort/sorting.py | 6 ++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/isort/output.py b/isort/output.py index 6b3401089..ee7406ca8 100644 --- a/isort/output.py +++ b/isort/output.py @@ -54,12 +54,15 @@ def sorted_imports( key=lambda key: sorting.module_key( key, config, section_name=section, straight_import=True ), + reverse=config.reverse_sort, ) from_modules = parsed.imports[section]["from"] if not config.only_sections: from_modules = sorting.naturally( - from_modules, key=lambda key: sorting.module_key(key, config, section_name=section) + from_modules, + key=lambda key: sorting.module_key(key, config, section_name=section), + reverse=config.reverse_sort, ) straight_imports = _with_straight_imports( @@ -233,6 +236,7 @@ def _with_from_imports( config.force_alphabetical_sort_within_sections, section_name=section, ), + reverse=config.reverse_sort, ) if remove_imports: from_imports = [ diff --git a/isort/sorting.py b/isort/sorting.py index 2cfe60bf5..e8c355bab 100644 --- a/isort/sorting.py +++ b/isort/sorting.py @@ -96,7 +96,9 @@ def section_key(line: str, config: Config) -> str: return f"{section}{len(line) if config.length_sort else ''}{line}" -def naturally(to_sort: Iterable[str], key: Optional[Callable[[str], Any]] = None) -> List[str]: +def naturally( + to_sort: Iterable[str], key: Optional[Callable[[str], Any]] = None, reverse: bool = False +) -> List[str]: """Returns a naturally sorted list""" if key is None: key_callback = _natural_keys @@ -105,7 +107,7 @@ def naturally(to_sort: Iterable[str], key: Optional[Callable[[str], Any]] = None def key_callback(text: str) -> List[Any]: return _natural_keys(key(text)) # type: ignore - return sorted(to_sort, key=key_callback) + return sorted(to_sort, key=key_callback, reverse=reverse) def _atoi(text: str) -> Any: From 33b4cc3abb700cd89a52ef5fd27080bf32c991a4 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 17 Mar 2021 22:22:22 -0700 Subject: [PATCH 1433/1439] Resolved #1504: Added ability to push star imports to the top to avoid overriding explicitly defined imports. --- CHANGELOG.md | 1 + isort/main.py | 6 ++++++ isort/output.py | 10 ++++++++++ isort/settings.py | 1 + tests/unit/test_ticketed_features.py | 23 +++++++++++++++++++++++ 5 files changed, 41 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72bebd8c1..2de5058ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ - Implemented #1684: Added support for extending skips with `--extend-skip` and `--extend-skip-glob`. - Implemented #1688: Auto identification and skipping of some invalid import statements. - Implemented #1645: Ability to reverse the import sorting order. + - Implemented #1504: Ability to push star imports to the top to avoid overriding explicitly defined imports. - Documented #1685: Skip doesn't support plain directory names, but skip_glob does. ### 5.7.0 December 30th 2020 diff --git a/isort/main.py b/isort/main.py index d16626f5f..b6954d249 100644 --- a/isort/main.py +++ b/isort/main.py @@ -666,6 +666,12 @@ def _build_arg_parser() -> argparse.ArgumentParser: dest="ext_format", help="Tells isort to format the given files according to an extensions formatting rules.", ) + output_group.add_argument( + "--star-first", + help="Forces star imports above others to avoid overriding directly imported variables.", + dest="star_first", + action="store_true", + ) section_group.add_argument( "--sd", diff --git a/isort/output.py b/isort/output.py index ee7406ca8..01abbe27d 100644 --- a/isort/output.py +++ b/isort/output.py @@ -65,6 +65,16 @@ def sorted_imports( reverse=config.reverse_sort, ) + if config.star_first: + star_modules = [] + other_modules = [] + for module in from_modules: + if "*" in parsed.imports[section]["from"][module]: + star_modules.append(module) + else: + other_modules.append(module) + from_modules = star_modules + other_modules + straight_imports = _with_straight_imports( parsed, config, straight_modules, section, remove_imports, import_type ) diff --git a/isort/settings.py b/isort/settings.py index dc3754ef8..c2cd9624a 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -211,6 +211,7 @@ class _Config: sort_relative_in_force_sorted_sections: bool = False overwrite_in_place: bool = False reverse_sort: bool = False + star_first: bool = False def __post_init__(self): py_version = self.py_version diff --git a/tests/unit/test_ticketed_features.py b/tests/unit/test_ticketed_features.py index 6bed59a66..a0e5e1640 100644 --- a/tests/unit/test_ticketed_features.py +++ b/tests/unit/test_ticketed_features.py @@ -1059,3 +1059,26 @@ def test_isort_allows_reversing_sort_order_issue_1645(): ) """ ) + + +def test_isort_can_push_star_imports_above_others_issue_1504(): + """isort should provide a way to push star imports above other imports to avoid explicit + imports from being overwritten. + see: https://github.com/PyCQA/isort/issues/1504 + """ + assert ( + ( + isort.code( + """ +from ._bar import Any, All, Not +from ._foo import a, * +""", + star_first=True, + ) + ) + == """ +from ._foo import * +from ._foo import a +from ._bar import All, Any, Not +""" + ) From 5a0ebba5174d67210b6b23f8f644f1b0bc03de73 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 18 Mar 2021 23:23:02 -0700 Subject: [PATCH 1434/1439] Fixed #1594: incorrect placement of noqa comments with multiple from imports. --- CHANGELOG.md | 3 ++- isort/output.py | 12 ++++++++---- tests/unit/test_regressions.py | 22 ++++++++++++++++++++++ 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2de5058ad..4d0a99b2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ ### 5.8.0 TBD - Fixed #1631: as import comments can in some cases be duplicated. - Fixed #1667: extra newline added with float-to-top, after skip, in some cases. + - Fixed #1594: incorrect placement of noqa comments with multiple from imports. - Implemented #1648: Export MyPY type hints. - Implemented #1641: Identified import statements now return runnable code. - Implemented #1661: Added "wemake" profile. @@ -16,7 +17,7 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ - Implemented #1684: Added support for extending skips with `--extend-skip` and `--extend-skip-glob`. - Implemented #1688: Auto identification and skipping of some invalid import statements. - Implemented #1645: Ability to reverse the import sorting order. - - Implemented #1504: Ability to push star imports to the top to avoid overriding explicitly defined imports. + - Implemented #1504: Added ability to push star imports to the top to avoid overriding explicitly defined imports. - Documented #1685: Skip doesn't support plain directory names, but skip_glob does. ### 5.7.0 December 30th 2020 diff --git a/isort/output.py b/isort/output.py index 01abbe27d..13f72a3d5 100644 --- a/isort/output.py +++ b/isort/output.py @@ -429,18 +429,22 @@ def _with_from_imports( parsed.categorized_comments["nested"].get(module, {}).pop(from_import, None) ) if comment: + from_imports.remove(from_import) + if from_imports: + use_comments = [] + else: + use_comments = comments + comments = None single_import_line = with_comments( - comments, + use_comments, import_start + from_import, removed=config.ignore_comments, comment_prefix=config.comment_prefix, ) single_import_line += ( - f"{comments and ';' or config.comment_prefix} " f"{comment}" + f"{use_comments and ';' or config.comment_prefix} " f"{comment}" ) output.append(wrap.line(single_import_line, parsed.line_separator, config)) - from_imports.remove(from_import) - comments = None from_import_section = [] while from_imports and ( diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index 7d311f3c2..9f1755254 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -1585,3 +1585,25 @@ def test_isort_shouldnt_add_extra_line_float_to_top_issue_1667(): show_diff=True, float_to_top=True, ) + + +def test_isort_shouldnt_move_noqa_comment_issue_1594(): + assert ( + isort.code( + """ +from .test import TestTestTestTestTestTest1 # noqa: F401 +from .test import TestTestTestTestTestTest2, TestTestTestTestTestTest3, """ + """TestTestTestTestTestTest4, TestTestTestTestTestTest5 # noqa: F401 +""", + profile="black", + ) + == """ +from .test import TestTestTestTestTestTest1 # noqa: F401 +from .test import ( # noqa: F401 + TestTestTestTestTestTest2, + TestTestTestTestTestTest3, + TestTestTestTestTestTest4, + TestTestTestTestTestTest5, +) +""" + ) From a1f2782e4d825a54b2eec698b3520e30526a343f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 19 Mar 2021 23:33:56 -0700 Subject: [PATCH 1435/1439] Create failing test for issue #1566 --- tests/unit/test_regressions.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index 9f1755254..4cd8a3fac 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -1607,3 +1607,15 @@ def test_isort_shouldnt_move_noqa_comment_issue_1594(): ) """ ) + + +def test_isort_correctly_handles_unix_vs_linux_newlines_issue_1566(): + IMPORT_STATEMENT = ( + "from impacket.smb3structs import (\n" + "SMB2_CREATE, SMB2_FLAGS_DFS_OPERATIONS, SMB2_IL_IMPERSONATION, " + "SMB2_OPLOCK_LEVEL_NONE, SMB2Create," + "\nSMB2Create_Response, SMB2Packet)\n" + ) + assert isort.code(IMPORT_STATEMENT, line_length=120) == isort.code( + IMPORT_STATEMENT.replace("\n", "\r\n"), line_length=120 + ) From 844c85c36108937ec1962a94d54dba634d568d00 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Fri, 19 Mar 2021 23:34:38 -0700 Subject: [PATCH 1436/1439] Add changelog entry for issue #1566 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d0a99b2c..4a805715c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ - Fixed #1631: as import comments can in some cases be duplicated. - Fixed #1667: extra newline added with float-to-top, after skip, in some cases. - Fixed #1594: incorrect placement of noqa comments with multiple from imports. + - Fixed #1566: in some cases different length limits for dos based line endings. - Implemented #1648: Export MyPY type hints. - Implemented #1641: Identified import statements now return runnable code. - Implemented #1661: Added "wemake" profile. From 0ee873365c722d5eea95e0e838b771f4b06a61e1 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 20 Mar 2021 00:06:02 -0700 Subject: [PATCH 1437/1439] Fix test to ensure output is the same --- tests/unit/test_regressions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index 4cd8a3fac..26517598c 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -1610,12 +1610,12 @@ def test_isort_shouldnt_move_noqa_comment_issue_1594(): def test_isort_correctly_handles_unix_vs_linux_newlines_issue_1566(): - IMPORT_STATEMENT = ( + import_statement = ( "from impacket.smb3structs import (\n" "SMB2_CREATE, SMB2_FLAGS_DFS_OPERATIONS, SMB2_IL_IMPERSONATION, " "SMB2_OPLOCK_LEVEL_NONE, SMB2Create," "\nSMB2Create_Response, SMB2Packet)\n" ) - assert isort.code(IMPORT_STATEMENT, line_length=120) == isort.code( - IMPORT_STATEMENT.replace("\n", "\r\n"), line_length=120 - ) + assert isort.code(import_statement, line_length=120) == isort.code( + import_statement.replace("\n", "\r\n"), line_length=120 + ).replace("\r\n", "\n") From 404d809a692d68db0f36016b91171befec48be81 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 20 Mar 2021 00:06:38 -0700 Subject: [PATCH 1438/1439] Fix issue #1566: Fixed single location parsed line separator isn't used --- isort/output.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/isort/output.py b/isort/output.py index 13f72a3d5..61c2e30ec 100644 --- a/isort/output.py +++ b/isort/output.py @@ -504,7 +504,13 @@ def _with_from_imports( config=config, multi_line_output=wrap.Modes.VERTICAL_GRID, # type: ignore ) - if max(len(x) for x in import_statement.split("\n")) > config.line_length: + if ( + max( + len(import_line) + for import_line in import_statement.split(parsed.line_separator) + ) + > config.line_length + ): import_statement = other_import_statement if not do_multiline_reformat and len(import_statement) > config.line_length: import_statement = wrap.line(import_statement, parsed.line_separator, config) From a6222a8a125ec719724e628a5d3d0d5c60923281 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 20 Mar 2021 22:25:02 -0700 Subject: [PATCH 1439/1439] Bump version to 5.8.0 --- CHANGELOG.md | 2 +- isort/_version.py | 2 +- isort/settings.py | 1 + pyproject.toml | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a805715c..aaeae67a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ Changelog NOTE: isort follows the [semver](https://semver.org/) versioning standard. Find out more about isort's release policy [here](https://pycqa.github.io/isort/docs/major_releases/release_policy/). -### 5.8.0 TBD +### 5.8.0 March 20th 2021 - Fixed #1631: as import comments can in some cases be duplicated. - Fixed #1667: extra newline added with float-to-top, after skip, in some cases. - Fixed #1594: incorrect placement of noqa comments with multiple from imports. diff --git a/isort/_version.py b/isort/_version.py index 7f4ce2d4c..e0b752771 100644 --- a/isort/_version.py +++ b/isort/_version.py @@ -1 +1 @@ -__version__ = "5.7.0" +__version__ = "5.8.0" diff --git a/isort/settings.py b/isort/settings.py index c2cd9624a..d1fba4c20 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -212,6 +212,7 @@ class _Config: overwrite_in_place: bool = False reverse_sort: bool = False star_first: bool = False + import_dependencies = Dict[str, str] def __post_init__(self): py_version = self.py_version diff --git a/pyproject.toml b/pyproject.toml index 086dadfd3..b4813b780 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ line-length = 100 [tool.poetry] name = "isort" -version = "5.7.0" +version = "5.8.0" description = "A Python utility / library to sort Python imports." authors = ["Timothy Crosley "] license = "MIT"