Repository: timothycrosley/isort
Pull request #1074 (2019-12-28 10:19:47)

Feature/isort skip support


Patch:
diff --git a/isort/api.py b/isort/api.py
index c53fe98a..8d435b21 100644
--- a/isort/api.py
+++ b/isort/api.py
@@ -15,7 +15,7 @@ from .exceptions import (
 )
 from .format import format_natural, remove_whitespace, show_unified_diff
 from .io import File
-from .settings import DEFAULT_CONFIG, FILE_SKIP_COMMENT, Config
+from .settings import DEFAULT_CONFIG, FILE_SKIP_COMMENTS, Config
 
 IMPORT_START_IDENTIFIERS = ("from ", "from.import", "import ", "import*")
 COMMENT_INDICATORS = ('"""', "'''", "'", '"', "#")
@@ -54,12 +54,13 @@ def sorted_imports(
     config = _config(config=config, **config_kwargs)
     content_source = str(file_path or "Passed in content")
     if not disregard_skip:
-        if FILE_SKIP_COMMENT in file_contents:
-            raise FileSkipComment(content_source)
-
-        elif file_path and config.is_skipped(file_path):
+        if file_path and config.is_skipped(file_path):
             raise FileSkipSetting(content_source)
 
+        for file_skip_comment in FILE_SKIP_COMMENTS:
+            if file_skip_comment in file_contents:
+                raise FileSkipComment(content_source)
+
     if config.atomic:
         try:
             compile(file_contents, content_source, "exec", 0, 1)
@@ -156,6 +157,7 @@ def sort_imports(
     first_import_section: bool = True
     section_comments = [f"# {heading}" for heading in config.import_headings.values()]
     indent: str = ""
+    isort_off: bool = False
 
     for index, line in enumerate(chain(input_stream, (None,))):
         if line is None:
@@ -207,10 +209,18 @@ def sort_imports(
                         break
                     char_index += 1
 
-            not_imports = bool(in_quote) or in_top_comment
+            not_imports = bool(in_quote) or in_top_comment or isort_off
             if not (in_quote or in_top_comment):
                 stripped_line = line.strip()
-                if not stripped_line or stripped_line.startswith("#"):
+                if isort_off:
+                    if stripped_line == "# isort: on":
+                        isort_off = False
+                elif stripped_line == "# isort: off":
+                    not_imports = True
+                    isort_off = True
+                elif stripped_line == "# isort: split":
+                    not_imports = True
+                elif not stripped_line or stripped_line.startswith("#"):
                     import_section += line
                 elif stripped_line.startswith(IMPORT_START_IDENTIFIERS):
                     contains_imports = True
diff --git a/isort/parse.py b/isort/parse.py
index a87461ff..6089c08b 100644
--- a/isort/parse.py
+++ b/isort/parse.py
@@ -54,7 +54,7 @@ def _normalize_line(raw_line: str) -> Tuple[str, str]:
 
 def import_type(line: str) -> Optional[str]:
     """If the current line is an import line it will return its type (from or straight)"""
-    if "isort:skip" in line or "NOQA" in line:
+    if "isort:skip" in line or "isort: skip" in line or "NOQA" in line:
         return None
     elif line.startswith("import "):
         return "straight"
@@ -174,6 +174,10 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte
             section = line.split("isort:imports-")[-1].split()[0].upper()
             place_imports[section] = []
             import_placements[line] = section
+        elif "isort: imports-" in line and line.startswith("#"):
+            section = line.split("isort: imports-")[-1].split()[0].upper()
+            place_imports[section] = []
+            import_placements[line] = section
 
         if skipping_line:
             out_lines.append(line)
@@ -325,6 +329,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte
                         and not last.endswith('"""')
                         and not last.endswith("'''")
                         and "isort:imports-" not in last
+                        and "isort: imports-" not in last
                     ):
                         categorized_comments["above"]["from"].setdefault(import_from, []).insert(
                             0, out_lines.pop(-1)
@@ -361,6 +366,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte
                             and not last.endswith('"""')
                             and not last.endswith("'''")
                             and "isort:imports-" not in last
+                            and "isort: imports-" not in last
                         ):
                             categorized_comments["above"]["straight"].setdefault(module, []).insert(
                                 0, out_lines.pop(-1)
diff --git a/isort/settings.py b/isort/settings.py
index 1bdc35cb..ed5e4a51 100644
--- a/isort/settings.py
+++ b/isort/settings.py
@@ -55,7 +55,10 @@ try:
 except ImportError:
     appdirs = None
 
-FILE_SKIP_COMMENT: str = ("isort:" + "skip_file")  # Concatenated to avoid this file being skipped
+FILE_SKIP_COMMENTS: Tuple[str, ...] = (
+    "isort:" + "skip_file",
+    "isort: " + "skip_file",
+)  # Concatenated to avoid this file being skipped
 MAX_CONFIG_SEARCH_DEPTH: int = 25  # The number of parent directories to for a config file within
 STOP_CONFIG_SEARCH_ON_DIRS: Tuple[str, ...] = (".git", ".hg")
 VALID_PY_TARGETS: Tuple[str, ...] = tuple(


Test patch:
diff --git a/tests/test_isort.py b/tests/test_isort.py
index 40b22bb2..e5072b0f 100644
--- a/tests/test_isort.py
+++ b/tests/test_isort.py
@@ -641,7 +641,7 @@ def test_skip() -> None:
         "import myproject\n"
         "import django\n"
         "print('hey')\n"
-        "import sys  # isort:skip this import needs to be placed here\n\n\n\n\n\n\n"
+        "import sys  # isort: skip this import needs to be placed here\n\n\n\n\n\n\n"
     )
 
     test_output = SortImports(file_contents=test_input, known_third_party=["django"]).output
@@ -651,7 +651,7 @@ def test_skip() -> None:
         "import myproject\n"
         "\n"
         "print('hey')\n"
-        "import sys  # isort:skip this import needs to be placed here\n"
+        "import sys  # isort: skip this import needs to be placed here\n"
     )
 
 
@@ -668,7 +668,7 @@ def test_skip_with_file_name() -> None:
 
 def test_skip_within_file() -> None:
     """Ensure skipping a whole file works."""
-    test_input = "# isort:skip_file\nimport django\nimport myproject\n"
+    test_input = "# isort: skip_file\nimport django\nimport myproject\n"
     sort_imports = SortImports(file_contents=test_input, known_third_party=["django"])
     assert sort_imports.skipped
     assert sort_imports.output == ""
@@ -1624,19 +1624,19 @@ def test_place_comments() -> None:
         "import myproject.test\n"
         "import django.settings\n"
         "\n"
-        "# isort:imports-thirdparty\n"
-        "# isort:imports-firstparty\n"
-        "# isort:imports-stdlib\n"
+        "# isort: imports-thirdparty\n"
+        "# isort: imports-firstparty\n"
+        "# isort: imports-stdlib\n"
         "\n"
     )
     expected_output = (
-        "\n# isort:imports-thirdparty\n"
+        "\n# isort: imports-thirdparty\n"
         "import django.settings\n"
         "\n"
-        "# isort:imports-firstparty\n"
+        "# isort: imports-firstparty\n"
         "import myproject.test\n"
         "\n"
-        "# isort:imports-stdlib\n"
+        "# isort: imports-stdlib\n"
         "import os\n"
         "import sys\n"
     )
@@ -4186,3 +4186,30 @@ def test_isort_nested_imports() -> None:
         return True
     """
     )
+
+
+def test_isort_off() -> None:
+    """Test that isort can be turned on and off at will using comments"""
+    test_input = """import os
+
+# isort: off
+import sys
+import os
+# isort: on
+
+from . import local
+"""
+    assert SortImports(file_contents=test_input).output == test_input
+
+
+def test_isort_split() -> None:
+    """Test the ability to split isort import sections"""
+    test_input = """import os
+import sys
+
+# isort: split
+
+import os
+import sys
+"""
+    assert SortImports(file_contents=test_input).output == test_input