From 49878c505ccfd6dceb73c12ead3459c50115a8b7 Mon Sep 17 00:00:00 2001 From: Marco Gorelli Date: Thu, 8 Oct 2020 13:42:38 +0100 Subject: [PATCH 001/179] 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 002/179] 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 b66853ff81b02bada800f9222905e45fa4871b28 Mon Sep 17 00:00:00 2001 From: Timothy Edmund Crosley Date: Fri, 9 Oct 2020 00:12:48 -0700 Subject: [PATCH 003/179] 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 93db086913bcfcff4d9de06c3842ef4425bbde8a Mon Sep 17 00:00:00 2001 From: anirudnits Date: Sat, 10 Oct 2020 20:27:02 +0530 Subject: [PATCH 004/179] 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 005/179] 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 006/179] 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 007/179] 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 008/179] 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 009/179] 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 010/179] 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 586cfe1951cd7be8976a8beda0803910ac2cdd3e Mon Sep 17 00:00:00 2001 From: Marco Gorelli Date: Sun, 11 Oct 2020 15:07:23 +0100 Subject: [PATCH 011/179] 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 012/179] 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 1682a3385ec91563bc7e0b70ca32b80ff23b0832 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Mon, 12 Oct 2020 23:39:19 -0700 Subject: [PATCH 013/179] 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 014/179] 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 015/179] 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 016/179] 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 017/179] 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 018/179] 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 019/179] 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 020/179] 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 021/179] 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 022/179] 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 023/179] 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 024/179] 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 025/179] 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 026/179] 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 027/179] 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 028/179] 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 029/179] 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 030/179] 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 031/179] 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 032/179] 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 033/179] 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 034/179] 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 035/179] 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 036/179] 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 037/179] 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 038/179] 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 039/179] 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 040/179] 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 041/179] 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 042/179] 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 043/179] 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 044/179] 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 045/179] 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 046/179] 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 047/179] 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 048/179] 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 049/179] 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 050/179] 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 051/179] 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 052/179] 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 35f2f8f50e3e0f992bb44198a2902ce8533ad9a1 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sat, 14 Nov 2020 00:49:19 -0800 Subject: [PATCH 053/179] 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 054/179] 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 055/179] 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 056/179] 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 057/179] 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 058/179] 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 059/179] 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 060/179] 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 061/179] 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 062/179] 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 063/179] 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 064/179] 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 065/179] 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 066/179] 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 067/179] 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 068/179] 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 069/179] 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 070/179] 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 071/179] 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 072/179] 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 073/179] 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 074/179] 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 075/179] 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 076/179] 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 077/179] 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 078/179] 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 079/179] 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 080/179] 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 081/179] 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 082/179] 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 083/179] 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 084/179] 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 085/179] 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 086/179] 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 087/179] 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 088/179] 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 089/179] 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 090/179] 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 091/179] 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 092/179] 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 093/179] 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 094/179] 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 095/179] 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 096/179] 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 097/179] 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 098/179] 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 099/179] 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 100/179] 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 101/179] 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 102/179] 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 103/179] 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 104/179] 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 105/179] 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 106/179] 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 107/179] 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 108/179] 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 109/179] 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 110/179] 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 111/179] 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 112/179] 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 113/179] 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 114/179] 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 115/179] 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 116/179] 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 117/179] 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 118/179] 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 119/179] 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 120/179] 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 121/179] 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 122/179] 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 123/179] 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 124/179] 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 125/179] 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 126/179] 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 127/179] 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 128/179] 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 129/179] 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 130/179] 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 131/179] 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 132/179] 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 133/179] 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 134/179] 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 135/179] 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 136/179] 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 137/179] 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 138/179] 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 139/179] 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 140/179] 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 141/179] 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 142/179] 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 143/179] 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 144/179] 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 145/179] 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 146/179] 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 147/179] 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 148/179] 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 149/179] 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 150/179] 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 151/179] 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 152/179] 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 153/179] 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 154/179] 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 155/179] 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 156/179] 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 157/179] 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 158/179] 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 159/179] 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 160/179] 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 161/179] 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 162/179] 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 163/179] 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 164/179] 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 165/179] 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 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 166/179] 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 167/179] 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 168/179] 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 169/179] 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 170/179] 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 171/179] 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 172/179] 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 173/179] 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 174/179] 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 175/179] 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 176/179] 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 177/179] 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 178/179] 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 179/179] 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):