Skip to content
53 changes: 53 additions & 0 deletions docs/configuration/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you mind adding an example here to make it more clear?


**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.
Expand Down
38 changes: 38 additions & 0 deletions isort/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,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


Expand Down Expand Up @@ -150,6 +151,9 @@ def sorted_imports(
section_output.append("") # Empty line for black compatibility
section_output.append(section_comment_end)

if section in config.separate_packages:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for this @alex-liang3. I know the complexy of this method was already high, but this pirce of code you added here is a perfect candidate for an extract method. With that we could start the needed refactor here to reduce the complexity. Could you please do this refactor?

section_output = _separate_packages(section_output, config)

if pending_lines_before or not no_lines_before:
output += [""] * config.lines_between_sections

Expand Down Expand Up @@ -685,3 +689,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
1 change: 1 addition & 0 deletions isort/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,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
Expand Down
108 changes: 108 additions & 0 deletions tests/unit/test_ticketed_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
"""
)