summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTimothy Edmund Crosley <timothy.crosley@gmail.com>2020-01-05 01:35:11 -0800
committerGitHub <noreply@github.com>2020-01-05 01:35:11 -0800
commit1aa3f06b4ee9d9a7278a003c8850ca3f2cae230a (patch)
tree5abb2a4514c2370d6ef6b5e6fd404f0a2df8da83
parent35bc8f34d2f6a584f9abeeadc84933e2e66617f6 (diff)
parentc3668df374e91a54acaf0c9786ac467b19eb3f12 (diff)
downloadisort-1aa3f06b4ee9d9a7278a003c8850ca3f2cae230a.tar.gz
Merge pull request #1078 from timothycrosley/feature/fix-issue-252
Feature/fix issue 252
-rw-r--r--isort/api.py48
-rw-r--r--isort/main.py4
-rw-r--r--isort/output.py33
-rw-r--r--isort/parse.py34
-rw-r--r--isort/settings.py1
-rw-r--r--tests/test_isort.py359
6 files changed, 454 insertions, 25 deletions
diff --git a/isort/api.py b/isort/api.py
index 0aae7711..99d717eb 100644
--- a/isort/api.py
+++ b/isort/api.py
@@ -17,7 +17,8 @@ from .format import format_natural, remove_whitespace, show_unified_diff
from .io import File
from .settings import DEFAULT_CONFIG, FILE_SKIP_COMMENTS, Config
-IMPORT_START_IDENTIFIERS = ("from ", "from.import", "import ", "import*")
+CIMPORT_IDENTIFIERS = ("cimport ", "cimport*", "from.cimport")
+IMPORT_START_IDENTIFIERS = ("from ", "from.import", "import ", "import*") + CIMPORT_IDENTIFIERS
COMMENT_INDICATORS = ('"""', "'''", "'", '"', "#")
@@ -149,6 +150,7 @@ def sort_imports(
line_separator: str = config.line_ending
add_imports: List[str] = [format_natural(addition) for addition in config.add_imports]
import_section: str = ""
+ next_import_section: str = ""
in_quote: str = ""
first_comment_index_start: int = -1
first_comment_index_end: int = -1
@@ -158,6 +160,7 @@ def sort_imports(
section_comments = [f"# {heading}" for heading in config.import_headings.values()]
indent: str = ""
isort_off: bool = False
+ cimports: bool = False
for index, line in enumerate(chain(input_stream, (None,))):
if line is None:
@@ -226,7 +229,7 @@ def sort_imports(
contains_imports = True
indent = line[: -len(line.lstrip())]
- import_section += line
+ import_statement = line
while stripped_line.endswith("\\") or (
"(" in stripped_line and ")" not in stripped_line
):
@@ -234,12 +237,33 @@ def sort_imports(
while stripped_line and stripped_line.endswith("\\"):
line = input_stream.readline()
stripped_line = line.strip().split("#")[0]
- import_section += line
+ import_statement += line
else:
while ")" not in stripped_line:
line = input_stream.readline()
stripped_line = line.strip().split("#")[0]
- import_section += line
+ import_statement += line
+
+ cimport_statement: bool = False
+ if (
+ import_statement.lstrip().startswith(CIMPORT_IDENTIFIERS)
+ or " cimport " in import_statement
+ or " cimport*" in import_statement
+ or " cimport(" in import_statement
+ or ".cimport" in import_statement
+ ):
+ cimport_statement = True
+
+ if cimport_statement != cimports:
+ if import_section:
+ next_import_section = import_statement
+ import_statement = ""
+ not_imports = True
+ line = ""
+ else:
+ cimports = cimport_statement
+
+ import_section += import_statement
else:
not_imports = True
@@ -255,6 +279,10 @@ def sort_imports(
contains_imports = True
add_imports = []
+ if next_import_section and not import_section:
+ import_section = next_import_section
+ next_import_section = ""
+
if import_section:
if add_imports and not indent:
import_section += line_separator.join(add_imports) + line_separator
@@ -277,7 +305,10 @@ def sort_imports(
line.lstrip() for line in import_section.split(line_separator)
)
sorted_import_section = output.sorted_imports(
- parse.file_contents(import_section, config=config), config, extension
+ parse.file_contents(import_section, config=config),
+ config,
+ extension,
+ import_type="cimport" if cimports else "import",
)
if indent:
sorted_import_section = (
@@ -285,13 +316,18 @@ def sort_imports(
)
output_stream.write(sorted_import_section)
+ if not line and next_import_section:
+ output_stream.write(line_separator)
if indent:
output_stream.write(line)
indent = ""
contains_imports = False
- import_section = ""
+ if next_import_section:
+ cimports = not cimports
+ import_section = next_import_section
+ next_import_section = ""
else:
output_stream.write(line)
not_imports = False
diff --git a/isort/main.py b/isort/main.py
index 4447545e..2c30cfa0 100644
--- a/isort/main.py
+++ b/isort/main.py
@@ -14,7 +14,7 @@ import setuptools
from . import SortImports, __version__, sections
from .logo import ASCII_ART
from .profiles import profiles
-from .settings import DEFAULT_CONFIG, VALID_PY_TARGETS, Config, WrapModes
+from .settings import DEFAULT_CONFIG, SUPPORTED_EXTENSIONS, VALID_PY_TARGETS, Config, WrapModes
shebang_re = re.compile(br"^#!.*\bpython[23w]?\b")
QUICK_GUIDE = f"""
@@ -35,7 +35,7 @@ Visit https://timothycrosley.github.io/isort/ for complete information about how
def is_python_file(path: str) -> bool:
_root, ext = os.path.splitext(path)
- if ext in (".py", ".pyi"):
+ if ext in SUPPORTED_EXTENSIONS:
return True
if ext in (".pex",):
return False
diff --git a/isort/output.py b/isort/output.py
index f2d287a7..5f2659f7 100644
--- a/isort/output.py
+++ b/isort/output.py
@@ -11,7 +11,10 @@ from .settings import DEFAULT_CONFIG, Config
def sorted_imports(
- parsed: parse.ParsedContent, config: Config = DEFAULT_CONFIG, extension: str = "py"
+ parsed: parse.ParsedContent,
+ config: Config = DEFAULT_CONFIG,
+ extension: str = "py",
+ import_type: str = "import",
) -> str:
"""Adds the imports back to the file.
@@ -61,15 +64,28 @@ def sorted_imports(
section_output,
sort_ignore_case,
remove_imports,
+ import_type,
)
if config.lines_between_types and from_modules and straight_modules:
section_output.extend([""] * config.lines_between_types)
section_output = _with_straight_imports(
- parsed, config, straight_modules, section, section_output, remove_imports
+ parsed,
+ config,
+ straight_modules,
+ section,
+ section_output,
+ remove_imports,
+ import_type,
)
else:
section_output = _with_straight_imports(
- parsed, config, straight_modules, section, section_output, remove_imports
+ parsed,
+ config,
+ straight_modules,
+ section,
+ section_output,
+ remove_imports,
+ import_type,
)
if config.lines_between_types and from_modules and straight_modules:
section_output.extend([""] * config.lines_between_types)
@@ -81,6 +97,7 @@ def sorted_imports(
section_output,
sort_ignore_case,
remove_imports,
+ import_type,
)
if config.force_sort_within_sections:
@@ -216,13 +233,14 @@ def _with_from_imports(
section_output: List[str],
ignore_case: bool,
remove_imports: List[str],
+ import_type: str,
) -> List[str]:
new_section_output = section_output.copy()
for module in from_modules:
if module in remove_imports:
continue
- import_start = f"from {module} import "
+ import_start = f"from {module} {import_type} "
from_imports = list(parsed.imports[section]["from"][module])
if not config.no_inline_sort or config.force_single_line:
from_imports = sorting.naturally(
@@ -475,6 +493,7 @@ def _with_straight_imports(
section: str,
section_output: List[str],
remove_imports: List[str],
+ import_type: str,
) -> List[str]:
new_section_output = section_output.copy()
for module in straight_modules:
@@ -484,12 +503,12 @@ def _with_straight_imports(
import_definition = []
if module in parsed.as_map:
if config.keep_direct_and_as_imports and parsed.imports[section]["straight"][module]:
- import_definition.append(f"import {module}")
+ import_definition.append(f"{import_type} {module}")
import_definition.extend(
- f"import {module} as {as_import}" for as_import in parsed.as_map[module]
+ f"{import_type} {module} as {as_import}" for as_import in parsed.as_map[module]
)
else:
- import_definition.append(f"import {module}")
+ import_definition.append(f"{import_type} {module}")
comments_above = parsed.categorized_comments["above"]["straight"].pop(module, None)
if comments_above:
diff --git a/isort/parse.py b/isort/parse.py
index 6089c08b..92bb46b5 100644
--- a/isort/parse.py
+++ b/isort/parse.py
@@ -11,8 +11,6 @@ from isort.settings import DEFAULT_CONFIG, Config
from .comments import parse as parse_comments
from .finders import FindersManager
-IMPORT_START_IDENTIFIERS = ("from ", "from.import", "import ", "import*")
-
if TYPE_CHECKING:
from mypy_extensions import TypedDict
@@ -46,8 +44,10 @@ def _normalize_line(raw_line: str) -> Tuple[str, str]:
Returns (normalized_line: str, raw_line: str)
"""
line = raw_line.replace("from.import ", "from . import ")
+ line = line.replace("from.cimport ", "from . cimport ")
line = line.replace("import*", "import *")
line = line.replace(" .import ", " . import ")
+ line = line.replace(" .cimport ", " . cimport ")
line = line.replace("\t", " ")
return (line, raw_line)
@@ -56,7 +56,7 @@ 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 "isort: skip" in line or "NOQA" in line:
return None
- elif line.startswith("import "):
+ elif line.startswith(("import ", "cimport ")):
return "straight"
elif line.startswith("from "):
return "from"
@@ -65,14 +65,16 @@ def import_type(line: str) -> Optional[str]:
def _strip_syntax(import_string: str) -> str:
import_string = import_string.replace("_import", "[[i]]")
+ import_string = import_string.replace("_cimport", "[[ci]]")
for remove_syntax in ["\\", "(", ")", ","]:
import_string = import_string.replace(remove_syntax, " ")
import_list = import_string.split()
- for key in ("from", "import"):
+ for key in ("from", "import", "cimport"):
if key in import_list:
import_list.remove(key)
import_string = " ".join(import_list)
import_string = import_string.replace("[[i]]", "_import")
+ import_string = import_string.replace("[[ci]]", "_cimport")
return import_string.replace("{ ", "{|").replace(" }", "|}")
@@ -108,7 +110,11 @@ def skip_line(
if ";" in line:
for part in (part.strip() for part in line.split(";")):
- if part and not part.startswith("from ") and not part.startswith("import "):
+ if (
+ part
+ and not part.startswith("from ")
+ and not part.startswith(("import ", "cimport "))
+ ):
skip_line = True
return (bool(skip_line or in_quote), in_quote)
@@ -267,18 +273,26 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte
and new_comment
):
nested_comments[stripped_line] = comments[-1]
- if import_string.strip().endswith(" import") or line.strip().startswith(
- "import "
- ):
+ if import_string.strip().endswith(
+ (" import", " cimport")
+ ) or line.strip().startswith(("import ", "cimport ")):
import_string += line_separator + line
else:
import_string = import_string.rstrip().rstrip("\\") + " " + line.lstrip()
if type_of_import == "from":
+ cimports: bool
import_string = import_string.replace("import(", "import (")
- parts = import_string.split(" import ")
+ if " cimport " in import_string:
+ parts = import_string.split(" cimport ")
+ cimports = True
+
+ else:
+ parts = import_string.split(" import ")
+ cimports = False
+
from_import = parts[0].split(" ")
- import_string = " import ".join(
+ import_string = (" cimport " if cimports else " import ").join(
[from_import[0] + " " + "".join(from_import[1:])] + parts[1:]
)
diff --git a/isort/settings.py b/isort/settings.py
index ed5e4a51..387d10e4 100644
--- a/isort/settings.py
+++ b/isort/settings.py
@@ -55,6 +55,7 @@ try:
except ImportError:
appdirs = None
+SUPPORTED_EXTENSIONS = (".py", ".pyi", ".pyx")
FILE_SKIP_COMMENTS: Tuple[str, ...] = (
"isort:" + "skip_file",
"isort: " + "skip_file",
diff --git a/tests/test_isort.py b/tests/test_isort.py
index 451af9e2..34fa7bc6 100644
--- a/tests/test_isort.py
+++ b/tests/test_isort.py
@@ -4238,3 +4238,362 @@ import os
import sys
'''
)
+
+
+def test_cimport_support():
+ """Test to ensure cimports (Cython style imports) work"""
+ test_input = """
+import os
+import sys
+import cython
+import platform
+import traceback
+import time
+import types
+import re
+import copy
+import inspect # used by JavascriptBindings.__SetObjectMethods()
+import urllib
+import json
+import datetime
+import random
+
+if sys.version_info.major == 2:
+ import urlparse
+else:
+ from urllib import parse as urlparse
+
+if sys.version_info.major == 2:
+ from urllib import pathname2url as urllib_pathname2url
+else:
+ from urllib.request import pathname2url as urllib_pathname2url
+
+from cpython.version cimport PY_MAJOR_VERSION
+import weakref
+
+# We should allow multiple string types: str, unicode, bytes.
+# PyToCefString() can handle them all.
+# Important:
+# If you set it to basestring, Cython will accept exactly(!)
+# str/unicode in Py2 and str in Py3. This won't work in Py3
+# as we might want to pass bytes as well. Also it will
+# reject string subtypes, so using it in publi API functions
+# would be a bad idea.
+ctypedef object py_string
+
+# You can't use "void" along with cpdef function returning None, it is planned to be
+# added to Cython in the future, creating this virtual type temporarily. If you
+# change it later to "void" then don't forget to add "except *".
+ctypedef object py_void
+ctypedef long WindowHandle
+
+from cpython cimport PyLong_FromVoidPtr
+
+from cpython cimport bool as py_bool
+from libcpp cimport bool as cpp_bool
+
+from libcpp.map cimport map as cpp_map
+from multimap cimport multimap as cpp_multimap
+from libcpp.pair cimport pair as cpp_pair
+from libcpp.vector cimport vector as cpp_vector
+
+from libcpp.string cimport string as cpp_string
+from wstring cimport wstring as cpp_wstring
+
+from libc.string cimport strlen
+from libc.string cimport memcpy
+
+# preincrement and dereference must be "as" otherwise not seen.
+from cython.operator cimport preincrement as preinc, dereference as deref
+
+# from cython.operator cimport address as addr # Address of an c++ object?
+
+from libc.stdlib cimport calloc, malloc, free
+from libc.stdlib cimport atoi
+
+# When pyx file cimports * from a pxd file and that pxd cimports * from another pxd
+# then these names will be visible in pyx file.
+
+# Circular imports are allowed in form "cimport ...", but won't work if you do
+# "from ... cimport *", this is important to know in pxd files.
+
+from libc.stdint cimport uint64_t
+from libc.stdint cimport uintptr_t
+
+cimport ctime
+
+IF UNAME_SYSNAME == "Windows":
+ from windows cimport *
+ from dpi_aware_win cimport *
+ELIF UNAME_SYSNAME == "Linux":
+ from linux cimport *
+ELIF UNAME_SYSNAME == "Darwin":
+ from mac cimport *
+
+from cpp_utils cimport *
+from task cimport *
+
+from cef_string cimport *
+cdef extern from *:
+ ctypedef CefString ConstCefString "const CefString"
+
+from cef_types_wrappers cimport *
+from cef_task cimport *
+from cef_runnable cimport *
+
+from cef_platform cimport *
+
+from cef_ptr cimport *
+from cef_app cimport *
+from cef_browser cimport *
+cimport cef_browser_static
+from cef_client cimport *
+from client_handler cimport *
+from cef_frame cimport *
+
+# cannot cimport *, that would cause name conflicts with constants.
+cimport cef_types
+ctypedef cef_types.cef_paint_element_type_t PaintElementType
+ctypedef cef_types.cef_jsdialog_type_t JSDialogType
+from cef_types cimport CefKeyEvent
+from cef_types cimport CefMouseEvent
+from cef_types cimport CefScreenInfo
+
+# cannot cimport *, name conflicts
+IF UNAME_SYSNAME == "Windows":
+ cimport cef_types_win
+ELIF UNAME_SYSNAME == "Darwin":
+ cimport cef_types_mac
+ELIF UNAME_SYSNAME == "Linux":
+ cimport cef_types_linux
+
+from cef_time cimport *
+from cef_drag cimport *
+
+IF CEF_VERSION == 1:
+ from cef_v8 cimport *
+ cimport cef_v8_static
+ cimport cef_v8_stack_trace
+ from v8function_handler cimport *
+ from cef_request_cef1 cimport *
+ from cef_web_urlrequest_cef1 cimport *
+ cimport cef_web_urlrequest_static_cef1
+ from web_request_client_cef1 cimport *
+ from cef_stream cimport *
+ cimport cef_stream_static
+ from cef_response_cef1 cimport *
+ from cef_stream cimport *
+ from cef_content_filter cimport *
+ from content_filter_handler cimport *
+ from cef_download_handler cimport *
+ from download_handler cimport *
+ from cef_cookie_cef1 cimport *
+ cimport cef_cookie_manager_namespace
+ from cookie_visitor cimport *
+ from cef_render_handler cimport *
+ from cef_drag_data cimport *
+
+IF UNAME_SYSNAME == "Windows":
+ IF CEF_VERSION == 1:
+ from http_authentication cimport *
+
+IF CEF_VERSION == 3:
+ from cef_values cimport *
+ from cefpython_app cimport *
+ from cef_process_message cimport *
+ from cef_web_plugin_cef3 cimport *
+ from cef_request_handler_cef3 cimport *
+ from cef_request_cef3 cimport *
+ from cef_cookie_cef3 cimport *
+ from cef_string_visitor cimport *
+ cimport cef_cookie_manager_namespace
+ from cookie_visitor cimport *
+ from string_visitor cimport *
+ from cef_callback_cef3 cimport *
+ from cef_response_cef3 cimport *
+ from cef_resource_handler_cef3 cimport *
+ from resource_handler_cef3 cimport *
+ from cef_urlrequest_cef3 cimport *
+ from web_request_client_cef3 cimport *
+ from cef_command_line cimport *
+ from cef_request_context cimport *
+ from cef_request_context_handler cimport *
+ from request_context_handler cimport *
+ from cef_jsdialog_handler cimport *
+"""
+ expected_output = """
+import copy
+import datetime
+import inspect # used by JavascriptBindings.__SetObjectMethods()
+import json
+import os
+import platform
+import random
+import re
+import sys
+import time
+import traceback
+import types
+import urllib
+
+import cython
+
+if sys.version_info.major == 2:
+ import urlparse
+
+else:
+ from urllib import parse as urlparse
+
+if sys.version_info.major == 2:
+ from urllib import pathname2url as urllib_pathname2url
+
+else:
+ from urllib.request import pathname2url as urllib_pathname2url
+
+from cpython.version cimport PY_MAJOR_VERSION
+
+import weakref
+
+# We should allow multiple string types: str, unicode, bytes.
+# PyToCefString() can handle them all.
+# Important:
+# If you set it to basestring, Cython will accept exactly(!)
+# str/unicode in Py2 and str in Py3. This won't work in Py3
+# as we might want to pass bytes as well. Also it will
+# reject string subtypes, so using it in publi API functions
+# would be a bad idea.
+ctypedef object py_string
+
+# You can't use "void" along with cpdef function returning None, it is planned to be
+# added to Cython in the future, creating this virtual type temporarily. If you
+# change it later to "void" then don't forget to add "except *".
+ctypedef object py_void
+ctypedef long WindowHandle
+
+cimport ctime
+from cpython cimport PyLong_FromVoidPtr
+from cpython cimport bool as py_bool
+# preincrement and dereference must be "as" otherwise not seen.
+from cython.operator cimport dereference as deref
+from cython.operator cimport preincrement as preinc
+from libc.stdint cimport uint64_t, uintptr_t
+from libc.stdlib cimport atoi, calloc, free, malloc
+from libc.string cimport memcpy, strlen
+from libcpp cimport bool as cpp_bool
+from libcpp.map cimport map as cpp_map
+from libcpp.pair cimport pair as cpp_pair
+from libcpp.string cimport string as cpp_string
+from libcpp.vector cimport vector as cpp_vector
+from multimap cimport multimap as cpp_multimap
+from wstring cimport wstring as cpp_wstring
+
+# from cython.operator cimport address as addr # Address of an c++ object?
+
+
+# When pyx file cimports * from a pxd file and that pxd cimports * from another pxd
+# then these names will be visible in pyx file.
+
+# Circular imports are allowed in form "cimport ...", but won't work if you do
+# "from ... cimport *", this is important to know in pxd files.
+
+
+
+IF UNAME_SYSNAME == "Windows":
+ from dpi_aware_win cimport *
+ from windows cimport *
+
+ELIF UNAME_SYSNAME == "Linux":
+ from linux cimport *
+
+ELIF UNAME_SYSNAME == "Darwin":
+ from mac cimport *
+
+from cef_string cimport *
+from cpp_utils cimport *
+from task cimport *
+
+cdef extern from *:
+ ctypedef CefString ConstCefString "const CefString"
+
+cimport cef_browser_static
+# cannot cimport *, that would cause name conflicts with constants.
+cimport cef_types
+from cef_app cimport *
+from cef_browser cimport *
+from cef_client cimport *
+from cef_frame cimport *
+from cef_platform cimport *
+from cef_ptr cimport *
+from cef_runnable cimport *
+from cef_task cimport *
+from cef_types_wrappers cimport *
+from client_handler cimport *
+
+ctypedef cef_types.cef_paint_element_type_t PaintElementType
+ctypedef cef_types.cef_jsdialog_type_t JSDialogType
+from cef_types cimport CefKeyEvent, CefMouseEvent, CefScreenInfo
+
+# cannot cimport *, name conflicts
+IF UNAME_SYSNAME == "Windows":
+ cimport cef_types_win
+
+ELIF UNAME_SYSNAME == "Darwin":
+ cimport cef_types_mac
+
+ELIF UNAME_SYSNAME == "Linux":
+ cimport cef_types_linux
+
+from cef_drag cimport *
+from cef_time cimport *
+
+IF CEF_VERSION == 1:
+ cimport cef_cookie_manager_namespace
+ cimport cef_stream_static
+ cimport cef_v8_stack_trace
+ cimport cef_v8_static
+ cimport cef_web_urlrequest_static_cef1
+ from cef_content_filter cimport *
+ from cef_cookie_cef1 cimport *
+ from cef_download_handler cimport *
+ from cef_drag_data cimport *
+ from cef_render_handler cimport *
+ from cef_request_cef1 cimport *
+ from cef_response_cef1 cimport *
+ from cef_stream cimport *
+ from cef_v8 cimport *
+ from cef_web_urlrequest_cef1 cimport *
+ from content_filter_handler cimport *
+ from cookie_visitor cimport *
+ from download_handler cimport *
+ from v8function_handler cimport *
+ from web_request_client_cef1 cimport *
+
+IF UNAME_SYSNAME == "Windows":
+ IF CEF_VERSION == 1:
+ from http_authentication cimport *
+
+IF CEF_VERSION == 3:
+ cimport cef_cookie_manager_namespace
+ from cef_callback_cef3 cimport *
+ from cef_command_line cimport *
+ from cef_cookie_cef3 cimport *
+ from cef_jsdialog_handler cimport *
+ from cef_process_message cimport *
+ from cef_request_cef3 cimport *
+ from cef_request_context cimport *
+ from cef_request_context_handler cimport *
+ from cef_request_handler_cef3 cimport *
+ from cef_resource_handler_cef3 cimport *
+ from cef_response_cef3 cimport *
+ from cef_string_visitor cimport *
+ from cef_urlrequest_cef3 cimport *
+ from cef_values cimport *
+ from cef_web_plugin_cef3 cimport *
+ from cefpython_app cimport *
+ from cookie_visitor cimport *
+ from request_context_handler cimport *
+ from resource_handler_cef3 cimport *
+ from string_visitor cimport *
+ from web_request_client_cef3 cimport *
+"""
+ SortImports(file_contents=test_input).output == expected_output