Repository: timothycrosley/isort
Pull request #1054 (2019-11-09 08:28:13)

Feature/profile support


Patch:
diff --git a/isort/exceptions.py b/isort/exceptions.py
index 4c512fd3..b62b2c5d 100644
--- a/isort/exceptions.py
+++ b/isort/exceptions.py
@@ -1,7 +1,7 @@
 """All isort specific exception classes should be defined here"""
 from pathlib import Path
 
-from .settings import FILE_SKIP_COMMENT
+from .profiles import profiles
 
 
 class ISortError(Exception):
@@ -54,8 +54,7 @@ class FileSkipComment(FileSkipped):
 
     def __init__(self, file_path: str):
         super().__init__(
-            f"{file_path} contains an {FILE_SKIP_COMMENT} comment and was skipped.",
-            file_path=file_path,
+            f"{file_path} contains an file skip comment and was skipped.", file_path=file_path
         )
 
 
@@ -68,3 +67,13 @@ class FileSkipSetting(FileSkipped):
             " or matches a glob in 'skip_glob' setting",
             file_path=file_path,
         )
+
+
+class ProfileDoesNotExist(ISortError):
+    """Raised when a profile is set by the user that doesn't exist"""
+
+    def __init__(self, profile: str):
+        super().__init__(
+            f"Specified profile of {profile} does not exist. "
+            f"Available profiles: {','.join(profiles)}."
+        )
diff --git a/isort/main.py b/isort/main.py
index 2d47f79b..a8e17193 100644
--- a/isort/main.py
+++ b/isort/main.py
@@ -11,11 +11,10 @@ from warnings import warn
 
 import setuptools
 
-from isort import SortImports, __version__
-from isort.logo import ASCII_ART
-from isort.settings import DEFAULT_CONFIG, VALID_PY_TARGETS, Config, WrapModes
-
-from . import sections
+from . import SortImports, __version__, sections
+from .logo import ASCII_ART
+from .profiles import profiles
+from .settings import DEFAULT_CONFIG, VALID_PY_TARGETS, Config, WrapModes
 
 shebang_re = re.compile(br"^#!.*\bpython[23w]?\b")
 
@@ -524,6 +523,13 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]:
         "interpreter used to run isort "
         f"(currently: {sys.version_info.major}{sys.version_info.minor}) will be used.",
     )
+    parser.add_argument(
+        "--profile",
+        dest="profile",
+        choices=list(profiles.keys()),
+        type=str,
+        help="Base profile type to use for configuration.",
+    )
 
     arguments = {key: value for key, value in vars(parser.parse_args(argv)).items() if value}
     if "dont_order_by_type" in arguments:
diff --git a/isort/output.py b/isort/output.py
index fe58466b..846dfc4b 100644
--- a/isort/output.py
+++ b/isort/output.py
@@ -93,6 +93,7 @@ def sorted_imports(
                     sorting.section_key,
                     order_by_type=config.order_by_type,
                     force_to_top=config.force_to_top,
+                    lexicographical=config.lexicographical,
                 ),
             )
 
diff --git a/isort/profiles.py b/isort/profiles.py
new file mode 100644
index 00000000..669c8277
--- /dev/null
+++ b/isort/profiles.py
@@ -0,0 +1,56 @@
+"""Common profiles are defined here to be easily used within a project using --profile {name}"""
+from typing import Any, Dict
+
+black = {
+    "multi_line_output": 3,
+    "include_trailing_comma": True,
+    "force_grid_wrap": 0,
+    "use_parentheses": True,
+}
+django = {
+    "combine_as_imports": True,
+    "include_trailing_comma": True,
+    "multi_line_output": 5,
+    "line_length": 79,
+}
+pycharm = {"multi_line_output": 3, "force_grid_wrap": 2}
+google = {"force_single_line": True, "force_sort_within_sections": True, "lexicographical": True}
+open_stack = {
+    "force_single_line": True,
+    "force_sort_within_sections": True,
+    "lexicographical": True,
+}
+plone = {
+    "force_alphabetical_sort": True,
+    "force_single_line": True,
+    "ines_after_imports": 2,
+    "line_length": 200,
+}
+attrs = {
+    "atomic": True,
+    "force_grid_wrap": 0,
+    "include_trailing_comma": True,
+    "lines_after_imports": 2,
+    "lines_between_types": 1,
+    "multi_line_output": 3,
+    "not_skip": "__init__.py",
+    "use_parentheses": True,
+}
+hug = {
+    "multi_line_output": 3,
+    "include_trailing_comma": True,
+    "force_grid_wrap": 0,
+    "use_parentheses": True,
+    "line_length": 100,
+}
+
+profiles: Dict[str, Dict[str, Any]] = {
+    "black": black,
+    "django": django,
+    "pycharm": pycharm,
+    "google": google,
+    "open_stack": open_stack,
+    "plone": plone,
+    "attrs": attrs,
+    "hug": hug,
+}
diff --git a/isort/settings.py b/isort/settings.py
index b802b2ae..607c67b6 100644
--- a/isort/settings.py
+++ b/isort/settings.py
@@ -34,6 +34,8 @@ from warnings import warn
 
 from . import stdlibs
 from ._future import dataclass, field
+from .exceptions import ProfileDoesNotExist
+from .profiles import profiles
 from .sections import DEFAULT as SECTION_DEFAULTS
 from .utils import difference, union
 from .wrap_modes import WrapModes
@@ -156,6 +158,7 @@ class _Config:
     force_alphabetical_sort: bool = False
     force_grid_wrap: int = 0
     force_sort_within_sections: bool = False
+    lexicographical: bool = False
     ignore_whitespace: bool = False
     no_lines_before: FrozenSet[str] = frozenset()
     no_inline_sort: bool = False
@@ -166,6 +169,7 @@ class _Config:
     conda_env: str = ""
     ensure_newline_before_comments: bool = False
     directory: str = ""
+    profile: str = ""
 
     def __post_init__(self):
         py_version = self.py_version
@@ -219,13 +223,23 @@ class Config(_Config):
         else:
             config_settings = {}
 
+        profile_name = config_overrides.get("profile", config_settings.get("profile", ""))
+        profile: Dict[str, Any] = {}
+        if profile_name:
+            if profile_name not in profiles:
+                raise ProfileDoesNotExist(profile_name)
+
+            profile = profiles[profile_name].copy()
+            profile["source"] = f"{profile_name} profile"
+            sources.append(profile)
+
         if config_settings:
             sources.append(config_settings)
         if config_overrides:
             config_overrides["source"] = "runtime"
             sources.append(config_overrides)
 
-        combined_config = {**config_settings, **config_overrides}
+        combined_config = {**profile, **config_settings, **config_overrides}
         if "indent" in combined_config:
             indent = str(combined_config["indent"])
             if indent.isdigit():
diff --git a/isort/sorting.py b/isort/sorting.py
index b45bbf51..e3aa277a 100644
--- a/isort/sorting.py
+++ b/isort/sorting.py
@@ -40,10 +40,16 @@ def module_key(
     return f"{module_name in config.force_to_top and 'A' or 'B'}{prefix}{_length_sort_maybe}"
 
 
-def section_key(line: str, order_by_type: bool, force_to_top: List[str]) -> str:
+def section_key(
+    line: str, order_by_type: bool, force_to_top: List[str], lexicographical: bool = False
+) -> str:
     section = "B"
 
-    line = _import_line_intro_re.sub("", _import_line_midline_import_re.sub(".", line))
+    if lexicographical:
+        line = _import_line_intro_re.sub("", _import_line_midline_import_re.sub(".", line))
+    else:
+        line = re.sub("^from ", "", line)
+        line = re.sub("^import ", "", line)
     if line.split(" ")[0] in force_to_top:
         section = "A"
     if not order_by_type:


Test patch:
diff --git a/tests/test_isort.py b/tests/test_isort.py
index 98b88040..b4d5b5d3 100644
--- a/tests/test_isort.py
+++ b/tests/test_isort.py
@@ -994,6 +994,20 @@ def test_force_single_line_imports_and_sort_within_sections() -> None:
         force_single_line=True,
         force_sort_within_sections=True,
     ).output
+    assert test_output == (
+        "from third_party import lib_a\n"
+        "from third_party import lib_b\n"
+        "from third_party import lib_d\n"
+        "from third_party.lib_c import lib1\n"
+    )
+    test_output = SortImports(
+        file_contents=test_input,
+        multi_line_output=WrapModes.GRID,
+        line_length=40,
+        force_single_line=True,
+        force_sort_within_sections=True,
+        lexicographical=True,
+    ).output
     assert test_output == (
         "from third_party import lib_a\n"
         "from third_party import lib_b\n"