Skip to content

Commit

Permalink
Merge pull request #2191 from bp72/issue/2152
Browse files Browse the repository at this point in the history
Added a fix for normalizing imports from more than one level of parent modules (issue/2152)
  • Loading branch information
staticdev committed Dec 10, 2023
2 parents 9f7e0e5 + a8fc20c commit 4be9850
Show file tree
Hide file tree
Showing 3 changed files with 46 additions and 18 deletions.
6 changes: 3 additions & 3 deletions isort/identify.py
Expand Up @@ -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
Expand Down Expand Up @@ -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 "):
Expand Down Expand Up @@ -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:]
Expand Down
29 changes: 15 additions & 14 deletions 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
Expand Down Expand Up @@ -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]:
Expand All @@ -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 ["\\", "(", ")", ","]:
Expand Down Expand Up @@ -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:
Expand All @@ -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]

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
29 changes: 28 additions & 1 deletion tests/unit/test_parse.py
@@ -1,3 +1,4 @@
import pytest
from hypothesis import given
from hypothesis import strategies as st

Expand Down Expand Up @@ -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))
Expand All @@ -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

0 comments on commit 4be9850

Please sign in to comment.