diff --git a/isort/identify.py b/isort/identify.py index e45fcb39..8223e256 100644 --- a/isort/identify.py +++ b/isort/identify.py @@ -5,7 +5,7 @@ from pathlib import Path from typing import Iterator, NamedTuple, Optional, TextIO, Tuple -from isort.parse import _normalize_line, _strip_syntax, skip_line +from isort.parse import normalize_line, skip_line, strip_syntax from .comments import parse as parse_comments from .settings import DEFAULT_CONFIG, Config @@ -84,7 +84,7 @@ def imports( statements[-1] = f"{statements[-1]}#{end_of_line_comment[0]}" for statement in statements: - line, _raw_line = _normalize_line(statement) + line, _raw_line = normalize_line(statement) if line.startswith(("import ", "cimport ")): type_of_import = "straight" elif line.startswith("from "): @@ -162,7 +162,7 @@ def imports( just_imports = [ item.replace("{|", "{ ").replace("|}", " }") - for item in _strip_syntax(import_string).split() + for item in strip_syntax(import_string).split() ] direct_imports = just_imports[1:] diff --git a/isort/parse.py b/isort/parse.py index 614afabd..7dfd7cef 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -1,4 +1,5 @@ """Defines parsing functions used by isort for parsing import definitions""" +import re from collections import OrderedDict, defaultdict from functools import partial from itertools import chain @@ -36,18 +37,18 @@ def _infer_line_separator(contents: str) -> str: return "\n" -def _normalize_line(raw_line: str) -> Tuple[str, str]: +def normalize_line(raw_line: str) -> Tuple[str, str]: """Normalizes import related statements in the provided line. Returns (normalized_line: str, raw_line: str) """ - line = raw_line.replace("from.import ", "from . import ") - line = line.replace("from.cimport ", "from . cimport ") + line = re.sub(r"from(\.+)cimport ", r"from \g<1> cimport ", raw_line) + line = re.sub(r"from(\.+)import ", r"from \g<1> import ", line) line = line.replace("import*", "import *") - line = line.replace(" .import ", " . import ") - line = line.replace(" .cimport ", " . cimport ") + line = re.sub(r" (\.+)import ", r" \g<1> import ", line) + line = re.sub(r" (\.+)cimport ", r" \g<1> cimport ", line) line = line.replace("\t", " ") - return (line, raw_line) + return line, raw_line def import_type(line: str, config: Config = DEFAULT_CONFIG) -> Optional[str]: @@ -63,7 +64,7 @@ def import_type(line: str, config: Config = DEFAULT_CONFIG) -> Optional[str]: return None -def _strip_syntax(import_string: str) -> 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 ["\\", "(", ")", ","]: @@ -263,7 +264,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte statements[-1] = f"{statements[-1]}#{end_of_line_comment[0]}" for statement in statements: - line, raw_line = _normalize_line(statement) + line, raw_line = normalize_line(statement) type_of_import = import_type(line, config) or "" raw_lines = [raw_line] if not type_of_import: @@ -275,7 +276,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte nested_comments = {} import_string, comment = parse_comments(line) comments = [comment] if comment else [] - line_parts = [part for part in _strip_syntax(import_string).strip().split(" ") if part] + line_parts = [part for part in strip_syntax(import_string).strip().split(" ") if part] if type_of_import == "from" and len(line_parts) == 2 and comments: nested_comments[line_parts[-1]] = comments[0] @@ -285,7 +286,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte index += 1 if new_comment: comments.append(new_comment) - stripped_line = _strip_syntax(line).strip() + stripped_line = strip_syntax(line).strip() if ( type_of_import == "from" and stripped_line @@ -309,7 +310,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte and ")" not in line.split("#")[0] and index < line_count ): - stripped_line = _strip_syntax(line).strip() + stripped_line = strip_syntax(line).strip() if ( type_of_import == "from" and stripped_line @@ -325,7 +326,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte index += 1 if new_comment: comments.append(new_comment) - stripped_line = _strip_syntax(line).strip() + stripped_line = strip_syntax(line).strip() if ( type_of_import == "from" and stripped_line @@ -336,7 +337,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte import_string += line_separator + line raw_lines.append(line) - stripped_line = _strip_syntax(line).strip() + stripped_line = strip_syntax(line).strip() if ( type_of_import == "from" and stripped_line @@ -377,7 +378,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte just_imports = [ item.replace("{|", "{ ").replace("|}", " }") - for item in _strip_syntax(import_string).split() + for item in strip_syntax(import_string).split() ] attach_comments_to: Optional[List[Any]] = None diff --git a/tests/unit/test_parse.py b/tests/unit/test_parse.py index 7d9e6606..e7d74aa5 100644 --- a/tests/unit/test_parse.py +++ b/tests/unit/test_parse.py @@ -1,3 +1,4 @@ +import pytest from hypothesis import given from hypothesis import strategies as st @@ -58,7 +59,7 @@ def test_fuzz__infer_line_separator(contents): @given(import_string=st.text()) def test_fuzz__strip_syntax(import_string): - parse._strip_syntax(import_string=import_string) + parse.strip_syntax(import_string=import_string) @given(line=st.text(), config=st.builds(Config)) @@ -81,3 +82,29 @@ def test_fuzz_skip_line(line, in_quote, index, section_comments, needs_import): section_comments=section_comments, needs_import=needs_import, ) + + +@pytest.mark.parametrize( + "raw_line, expected", + ( + ("from . cimport a", "from . cimport a"), + ("from.cimport a", "from . cimport a"), + ("from..cimport a", "from .. cimport a"), + ("from . import a", "from . import a"), + ("from.import a", "from . import a"), + ("from..import a", "from .. import a"), + ("import *", "import *"), + ("import*", "import *"), + ("from . import a", "from . import a"), + ("from .import a", "from . import a"), + ("from ..import a", "from .. import a"), + ("from . cimport a", "from . cimport a"), + ("from .cimport a", "from . cimport a"), + ("from ..cimport a", "from .. cimport a"), + ("from\t.\timport a", "from . import a"), + ), +) +def test_normalize_line(raw_line, expected): + line, returned_raw_line = parse.normalize_line(raw_line) + assert line == expected + assert returned_raw_line == raw_line