Skip to content

Commit 4be9850

Browse files
authored
Merge pull request #2191 from bp72/issue/2152
Added a fix for normalizing imports from more than one level of parent modules (issue/2152)
2 parents 9f7e0e5 + a8fc20c commit 4be9850

File tree

3 files changed

+46
-18
lines changed

3 files changed

+46
-18
lines changed

isort/identify.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from pathlib import Path
66
from typing import Iterator, NamedTuple, Optional, TextIO, Tuple
77

8-
from isort.parse import _normalize_line, _strip_syntax, skip_line
8+
from isort.parse import normalize_line, skip_line, strip_syntax
99

1010
from .comments import parse as parse_comments
1111
from .settings import DEFAULT_CONFIG, Config
@@ -84,7 +84,7 @@ def imports(
8484
statements[-1] = f"{statements[-1]}#{end_of_line_comment[0]}"
8585

8686
for statement in statements:
87-
line, _raw_line = _normalize_line(statement)
87+
line, _raw_line = normalize_line(statement)
8888
if line.startswith(("import ", "cimport ")):
8989
type_of_import = "straight"
9090
elif line.startswith("from "):
@@ -162,7 +162,7 @@ def imports(
162162

163163
just_imports = [
164164
item.replace("{|", "{ ").replace("|}", " }")
165-
for item in _strip_syntax(import_string).split()
165+
for item in strip_syntax(import_string).split()
166166
]
167167

168168
direct_imports = just_imports[1:]

isort/parse.py

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Defines parsing functions used by isort for parsing import definitions"""
2+
import re
23
from collections import OrderedDict, defaultdict
34
from functools import partial
45
from itertools import chain
@@ -36,18 +37,18 @@ def _infer_line_separator(contents: str) -> str:
3637
return "\n"
3738

3839

39-
def _normalize_line(raw_line: str) -> Tuple[str, str]:
40+
def normalize_line(raw_line: str) -> Tuple[str, str]:
4041
"""Normalizes import related statements in the provided line.
4142
4243
Returns (normalized_line: str, raw_line: str)
4344
"""
44-
line = raw_line.replace("from.import ", "from . import ")
45-
line = line.replace("from.cimport ", "from . cimport ")
45+
line = re.sub(r"from(\.+)cimport ", r"from \g<1> cimport ", raw_line)
46+
line = re.sub(r"from(\.+)import ", r"from \g<1> import ", line)
4647
line = line.replace("import*", "import *")
47-
line = line.replace(" .import ", " . import ")
48-
line = line.replace(" .cimport ", " . cimport ")
48+
line = re.sub(r" (\.+)import ", r" \g<1> import ", line)
49+
line = re.sub(r" (\.+)cimport ", r" \g<1> cimport ", line)
4950
line = line.replace("\t", " ")
50-
return (line, raw_line)
51+
return line, raw_line
5152

5253

5354
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]:
6364
return None
6465

6566

66-
def _strip_syntax(import_string: str) -> str:
67+
def strip_syntax(import_string: str) -> str:
6768
import_string = import_string.replace("_import", "[[i]]")
6869
import_string = import_string.replace("_cimport", "[[ci]]")
6970
for remove_syntax in ["\\", "(", ")", ","]:
@@ -263,7 +264,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte
263264
statements[-1] = f"{statements[-1]}#{end_of_line_comment[0]}"
264265

265266
for statement in statements:
266-
line, raw_line = _normalize_line(statement)
267+
line, raw_line = normalize_line(statement)
267268
type_of_import = import_type(line, config) or ""
268269
raw_lines = [raw_line]
269270
if not type_of_import:
@@ -275,7 +276,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte
275276
nested_comments = {}
276277
import_string, comment = parse_comments(line)
277278
comments = [comment] if comment else []
278-
line_parts = [part for part in _strip_syntax(import_string).strip().split(" ") if part]
279+
line_parts = [part for part in strip_syntax(import_string).strip().split(" ") if part]
279280
if type_of_import == "from" and len(line_parts) == 2 and comments:
280281
nested_comments[line_parts[-1]] = comments[0]
281282

@@ -285,7 +286,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte
285286
index += 1
286287
if new_comment:
287288
comments.append(new_comment)
288-
stripped_line = _strip_syntax(line).strip()
289+
stripped_line = strip_syntax(line).strip()
289290
if (
290291
type_of_import == "from"
291292
and stripped_line
@@ -309,7 +310,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte
309310
and ")" not in line.split("#")[0]
310311
and index < line_count
311312
):
312-
stripped_line = _strip_syntax(line).strip()
313+
stripped_line = strip_syntax(line).strip()
313314
if (
314315
type_of_import == "from"
315316
and stripped_line
@@ -325,7 +326,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte
325326
index += 1
326327
if new_comment:
327328
comments.append(new_comment)
328-
stripped_line = _strip_syntax(line).strip()
329+
stripped_line = strip_syntax(line).strip()
329330
if (
330331
type_of_import == "from"
331332
and stripped_line
@@ -336,7 +337,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte
336337
import_string += line_separator + line
337338
raw_lines.append(line)
338339

339-
stripped_line = _strip_syntax(line).strip()
340+
stripped_line = strip_syntax(line).strip()
340341
if (
341342
type_of_import == "from"
342343
and stripped_line
@@ -377,7 +378,7 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte
377378

378379
just_imports = [
379380
item.replace("{|", "{ ").replace("|}", " }")
380-
for item in _strip_syntax(import_string).split()
381+
for item in strip_syntax(import_string).split()
381382
]
382383

383384
attach_comments_to: Optional[List[Any]] = None

tests/unit/test_parse.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import pytest
12
from hypothesis import given
23
from hypothesis import strategies as st
34

@@ -58,7 +59,7 @@ def test_fuzz__infer_line_separator(contents):
5859

5960
@given(import_string=st.text())
6061
def test_fuzz__strip_syntax(import_string):
61-
parse._strip_syntax(import_string=import_string)
62+
parse.strip_syntax(import_string=import_string)
6263

6364

6465
@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):
8182
section_comments=section_comments,
8283
needs_import=needs_import,
8384
)
85+
86+
87+
@pytest.mark.parametrize(
88+
"raw_line, expected",
89+
(
90+
("from . cimport a", "from . cimport a"),
91+
("from.cimport a", "from . cimport a"),
92+
("from..cimport a", "from .. cimport a"),
93+
("from . import a", "from . import a"),
94+
("from.import a", "from . import a"),
95+
("from..import a", "from .. import a"),
96+
("import *", "import *"),
97+
("import*", "import *"),
98+
("from . import a", "from . import a"),
99+
("from .import a", "from . import a"),
100+
("from ..import a", "from .. import a"),
101+
("from . cimport a", "from . cimport a"),
102+
("from .cimport a", "from . cimport a"),
103+
("from ..cimport a", "from .. cimport a"),
104+
("from\t.\timport a", "from . import a"),
105+
),
106+
)
107+
def test_normalize_line(raw_line, expected):
108+
line, returned_raw_line = parse.normalize_line(raw_line)
109+
assert line == expected
110+
assert returned_raw_line == raw_line

0 commit comments

Comments
 (0)