Skip to content

Commit

Permalink
Implemented #1732: Support for custom sort functions.
Browse files Browse the repository at this point in the history
  • Loading branch information
timothycrosley committed Jun 21, 2021
1 parent 37dcd46 commit a1a3aff
Show file tree
Hide file tree
Showing 16 changed files with 236 additions and 72 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -9,6 +9,7 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/
- Implemented #1705: More intuitive handling of isort:skip_file comments on streams.
- Implemented #1737: Support for using action comments to avoid adding imports to individual files.
- Implemented #1750: Ability to customize output format lines.
- Implemented #1732: Support for custom sort functions.
- Fixed (https://github.com/PyCQA/isort/pull/1695): added imports being added to doc string in some cases.
- Fixed (https://github.com/PyCQA/isort/pull/1714): in rare cases line continuation combined with tabs can output invalid code.
- Fixed (https://github.com/PyCQA/isort/pull/1726): isort ignores reverse_sort when force_sort_within_sections is true.
Expand Down
6 changes: 6 additions & 0 deletions example_isort_sorting_plugin/example_isort_sorting_plugin.py
@@ -0,0 +1,6 @@
from natsort import natsorted


def natural_plus(*args, **kwargs) -> str:
"""An even more natural sorting order for isort using natsort."""
return natsorted(*args, **kwargs)
22 changes: 22 additions & 0 deletions example_isort_sorting_plugin/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions example_isort_sorting_plugin/pyproject.toml
@@ -0,0 +1,19 @@
[tool.poetry]
name = "example_isort_sorting_plugin"
version = "0.0.2"
description = "An example plugin that modifies isorts sorting order to provide an even more natural sort by utilizing natsort."
authors = ["Timothy Crosley <timothy.crosley@gmail.com>"]
license = "MIT"

[tool.poetry.plugins."isort.sort_function"]
natural_plus = "example_isort_sorting_plugin:natural_plus"

[tool.poetry.dependencies]
python = "^3.6"
natsort = "^7.1.1"

[tool.poetry.dev-dependencies]

[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"
5 changes: 3 additions & 2 deletions example_shared_isort_profile/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 13 additions & 1 deletion isort/exceptions.py
@@ -1,6 +1,6 @@
"""All isort specific exception classes should be defined here"""
from pathlib import Path
from typing import Any, Dict, Union
from typing import Any, Dict, List, Union

from .profiles import profiles

Expand Down Expand Up @@ -82,6 +82,18 @@ def __init__(self, profile: str):
self.profile = profile


class SortingFunctionDoesNotExist(ISortError):
"""Raised when the specified sorting function isn't available"""

def __init__(self, sort_order: str, available_sort_orders: List[str]):
super().__init__(
f"Specified sort_order of {sort_order} does not exist. "
f"Available sort_orders: {','.join(available_sort_orders)}."
)
self.sort_order = sort_order
self.available_sort_orders = available_sort_orders


class FormattingPluginDoesNotExist(ISortError):
"""Raised when a formatting plugin is set by the user that doesn't exist"""

Expand Down
4 changes: 2 additions & 2 deletions isort/main.py
Expand Up @@ -585,8 +585,8 @@ def _build_arg_parser() -> argparse.ArgumentParser:
output_group.add_argument(
"--sort-order",
dest="sort_order",
choices=["default", "pythonic"],
help="Use natural language or pythonic sorting.",
help="Specify sorting function. Can be built in (natural[default] = force numbers "
"to be sequential, native = Python's built-in sorted function) or an installable plugin.",
)
inline_args_group.add_argument(
"--sl",
Expand Down
30 changes: 28 additions & 2 deletions isort/settings.py
Expand Up @@ -30,13 +30,14 @@
)
from warnings import warn

from . import stdlibs
from . import sorting, stdlibs
from ._future import dataclass, field
from ._vendored import toml # type: ignore
from .exceptions import (
FormattingPluginDoesNotExist,
InvalidSettingsPath,
ProfileDoesNotExist,
SortingFunctionDoesNotExist,
UnsupportedSettings,
)
from .profiles import profiles
Expand Down Expand Up @@ -232,7 +233,7 @@ class _Config:
git_ignore: Dict[Path, Set[Path]] = field(default_factory=dict)
format_error: str = "{error}: {message}"
format_success: str = "{success}: {message}"
sort_order: str = "default"
sort_order: str = "natural"

def __post_init__(self) -> None:
py_version = self.py_version
Expand Down Expand Up @@ -294,6 +295,7 @@ def __init__(
self._section_comments: Optional[Tuple[str, ...]] = None
self._skips: Optional[FrozenSet[str]] = None
self._skip_globs: Optional[FrozenSet[str]] = None
self._sorting_function: Optional[Callable[..., List[str]]] = None

if config:
config_vars = vars(config).copy()
Expand All @@ -303,6 +305,7 @@ def __init__(
config_vars.pop("_section_comments")
config_vars.pop("_skips")
config_vars.pop("_skip_globs")
config_vars.pop("_sorting_function")
super().__init__(**config_vars) # type: ignore
return

Expand Down Expand Up @@ -651,6 +654,29 @@ def skip_globs(self) -> FrozenSet[str]:
self._skip_globs = self.skip_glob.union(self.extend_skip_glob)
return self._skip_globs

@property
def sorting_function(self) -> Callable[..., List[str]]:
if self._sorting_function is not None:
return self._sorting_function

if self.sort_order == "natural":
self._sorting_function = sorting.naturally
elif self.sort_order == "native":
self._sorting_function = sorted
else:
available_sort_orders = ["natural", "native"]
import pkg_resources

for sort_plugin in pkg_resources.iter_entry_points("isort.sort_function"):
available_sort_orders.append(sort_plugin.name)
if sort_plugin.name == self.sort_order:
self._sorting_function = sort_plugin.load()
break
else:
raise SortingFunctionDoesNotExist(self.sort_order, available_sort_orders)

return self._sorting_function

def _parse_known_pattern(self, pattern: str) -> List[str]:
"""Expand pattern if identified as a directory and return found sub packages"""
if pattern.endswith(os.path.sep):
Expand Down
12 changes: 6 additions & 6 deletions isort/sorting.py
@@ -1,7 +1,10 @@
import re
from typing import Any, Callable, Iterable, List, Optional
from typing import TYPE_CHECKING, Any, Callable, Iterable, List, Optional

from .settings import Config
if TYPE_CHECKING:
from .settings import Config
else:
Config = Any

_import_line_intro_re = re.compile("^(?:from|import) ")
_import_line_midline_import_re = re.compile(" import ")
Expand Down Expand Up @@ -102,10 +105,7 @@ def sort(
key: Optional[Callable[[str], Any]] = None,
reverse: bool = False,
) -> List[str]:
sorting_func: Callable[..., List[str]] = naturally
if config.sort_order == "pythonic":
sorting_func = sorted
return sorting_func(to_sort, key=key, reverse=reverse)
return config.sorting_function(to_sort, key=key, reverse=reverse)


def naturally(
Expand Down

0 comments on commit a1a3aff

Please sign in to comment.