diff options
-rw-r--r-- | isort/api.py | 48 | ||||
-rw-r--r-- | isort/main.py | 4 | ||||
-rw-r--r-- | isort/output.py | 33 | ||||
-rw-r--r-- | isort/parse.py | 34 | ||||
-rw-r--r-- | isort/settings.py | 1 | ||||
-rw-r--r-- | tests/test_isort.py | 359 |
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 |