diff --git a/docs/configuration/options.md b/docs/configuration/options.md index fdf68752..262f131b 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -1039,6 +1039,59 @@ If `True` isort will automatically create section groups by the top-level packag **Python & Config File Name:** group_by_package **CLI Flags:** **Not Supported** +## Separate Packages + +Separate packages within the listed sections with newlines. + +**Type:** List of Strings +**Default:** `frozenset()` +**Config default:** `[]` +**Python & Config File Name:** separate_packages +**CLI Flags:** **Not Supported** + +**Examples:** + +### Example `.isort.cfg` + +``` +[settings] +separate_packages=THIRDPARTY +``` + +### Example `pyproject.toml` + +``` +[tool.isort] +separate_packages = ["THIRDPARTY"] +``` + +### Example before: +```python +import os +import sys + +from django.db.models.signals import m2m_changed +from django.utils import functional +from django_filters import BooleanFilter +from junitparser import JUnitXml +from loguru import logger +``` + +### Example after: +```python +import os +import sys + +from django.db.models.signals import m2m_changed +from django.utils import functional + +from django_filters import BooleanFilter + +from junitparser import JUnitXml + +from loguru import logger +``` + ## Ignore Whitespace Tells isort to ignore whitespace differences when --check-only is being used. diff --git a/isort/output.py b/isort/output.py index 65a6ff7e..a826ba04 100644 --- a/isort/output.py +++ b/isort/output.py @@ -8,6 +8,7 @@ from . import parse, sorting, wrap from .comments import add_to_line as with_comments from .identify import STATEMENT_DECLARATIONS +from .place import module_with_reason from .settings import DEFAULT_CONFIG, Config @@ -149,6 +150,9 @@ def sorted_imports( section_output.append("") # Empty line for black compatibility section_output.append(section_comment_end) + if section in config.separate_packages: + section_output = _separate_packages(section_output, config) + if pending_lines_before or not no_lines_before: output += [""] * config.lines_between_sections @@ -674,3 +678,37 @@ def _with_star_comments(parsed: parse.ParsedContent, module: str, comments: List if star_comment: return [*comments, star_comment] return comments + + +def _separate_packages(section_output: List[str], config: Config) -> List[str]: + group_keys: Set[str] = set() + comments_above: List[str] = [] + processed_section_output: List[str] = [] + + for section_line in section_output: + if section_line.startswith("#"): + comments_above.append(section_line) + continue + + package_name: str = section_line.split(" ")[1] + _, reason = module_with_reason(package_name, config) + + if "Matched configured known pattern" in reason: + package_depth = len(reason.split(".")) - 1 # minus 1 for re.compile + key = ".".join(package_name.split(".")[: package_depth + 1]) + else: + key = package_name.split(".")[0] + + if key not in group_keys: + if group_keys: + processed_section_output.append("") + + group_keys.add(key) + + if comments_above: + processed_section_output.extend(comments_above) + comments_above = [] + + processed_section_output.append(section_line) + + return processed_section_output diff --git a/isort/settings.py b/isort/settings.py index b35792d8..a3687d58 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -201,6 +201,7 @@ class _Config: force_sort_within_sections: bool = False lexicographical: bool = False group_by_package: bool = False + separate_packages: FrozenSet[str] = frozenset() ignore_whitespace: bool = False no_lines_before: FrozenSet[str] = frozenset() no_inline_sort: bool = False diff --git a/tests/unit/test_ticketed_features.py b/tests/unit/test_ticketed_features.py index d2fe5639..dc86b7c2 100644 --- a/tests/unit/test_ticketed_features.py +++ b/tests/unit/test_ticketed_features.py @@ -1073,3 +1073,111 @@ def use_libc_math(): """, show_diff=True, ) + + +def test_sort_separate_packages_issue_2104(): + """ + Test to ensure that packages within a section can be separated by blank lines. + See: https://github.com/PyCQA/isort/issues/2104 + """ + + # Base case as described in issue + assert ( + isort.code( + """ +import os +import sys + +from django.db.models.signals import m2m_changed +from django.utils import functional +from django_filters import BooleanFilter +from junitparser import JUnitXml +from junitparser import TestSuite +from loguru import logger +""", + force_single_line=True, + separate_packages=["THIRDPARTY"], + ) + == """ +import os +import sys + +from django.db.models.signals import m2m_changed +from django.utils import functional + +from django_filters import BooleanFilter + +from junitparser import JUnitXml +from junitparser import TestSuite + +from loguru import logger +""" + ) + + # Check that multiline comments aren't broken up + assert ( + isort.code( + """ +from junitparser import TestSuite +# Some multiline +# comment +from loguru import logger +""", + force_single_line=True, + separate_packages=["THIRDPARTY"], + ) + == """ +from junitparser import TestSuite + +# Some multiline +# comment +from loguru import logger +""" + ) + + # Check it works for custom sections + assert ( + isort.code( + """ +import os +from package2 import bar +from package1 import foo + """, + force_single_line=True, + known_MYPACKAGES=["package1", "package2"], + sections=["STDLIB", "MYPACKAGES"], + separate_packages=["MYPACKAGES"], + ) + == """ +import os + +from package1 import foo + +from package2 import bar +""" + ) + + # Check it works for packages with deeper nesting + assert ( + isort.code( + """ +import os +from package2 import bar +from package1.a.b import foo +from package1.a.c import baz + """, + force_single_line=True, + known_MYPACKAGES=["package1.a", "package2"], + sections=["STDLIB", "MYPACKAGES"], + separate_packages=["MYPACKAGES"], + ) + == """ +import os + +from package1.a.b import foo + +from package1.a.c import baz + +from package2 import bar +""" + )