Skip to content

Commit 57d0a13

Browse files
DanielNoordCopilot
andcommitted
Implement support for sorting lazy imports
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent 22adde2 commit 57d0a13

5 files changed

Lines changed: 316 additions & 89 deletions

File tree

isort/_parse_utils.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,10 @@ def normalize_from_import_string(import_string: str) -> str:
166166

167167

168168
# TODO: Return a `StrEnum` once we no longer support Python 3.10.
169-
def import_type(line: str, config: Config) -> Literal["from", "straight"] | None:
170-
"""If the current line is an import line it will return its type (from or straight)"""
169+
def import_type(
170+
line: str, config: Config
171+
) -> Literal["from", "straight", "lazy_from", "lazy_straight"] | None:
172+
"""If the current line is an import line it will return its type."""
171173
if config.honor_noqa and line.lower().rstrip().endswith("noqa"):
172174
return None
173175
if "isort:skip" in line or "isort: skip" in line or "isort: split" in line:
@@ -176,4 +178,8 @@ def import_type(line: str, config: Config) -> Literal["from", "straight"] | None
176178
return "straight"
177179
if line.startswith("from "):
178180
return "from"
181+
if line.startswith("lazy import "):
182+
return "lazy_straight"
183+
if line.startswith("lazy from "):
184+
return "lazy_from"
179185
return None

isort/core.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@
1212
from .settings import FILE_SKIP_COMMENTS
1313

1414
CIMPORT_IDENTIFIERS = ("cimport ", "cimport*", "from.cimport")
15-
IMPORT_START_IDENTIFIERS = ("from ", "from.import", "import ", "import*", *CIMPORT_IDENTIFIERS)
15+
_BASE_IMPORT_IDENTIFIERS = ("from ", "from.import", "import ", "import*")
16+
IMPORT_START_IDENTIFIERS = (
17+
*_BASE_IMPORT_IDENTIFIERS,
18+
*(f"lazy {identifier}" for identifier in _BASE_IMPORT_IDENTIFIERS),
19+
*CIMPORT_IDENTIFIERS,
20+
)
1621
DOCSTRING_INDICATORS = ('"""', "'''")
1722
COMMENT_INDICATORS = (*DOCSTRING_INDICATORS, "'", '"', "#")
1823
CODE_SORT_COMMENTS = (

isort/output.py

Lines changed: 128 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import itertools
33
from collections.abc import Iterable
44
from functools import partial
5-
from typing import Any
5+
from typing import Any, Literal
66

77
from isort.format import format_simplified
88

@@ -35,96 +35,43 @@ def sorted_imports(
3535
sections: Iterable[str] = itertools.chain(parsed.sections, config.forced_separate)
3636

3737
if config.no_sections:
38-
parsed.imports["no_sections"] = {"straight": {}, "from": {}}
38+
parsed.imports["no_sections"] = {
39+
"straight": {},
40+
"from": {},
41+
"lazy_straight": {},
42+
"lazy_from": {},
43+
}
3944
base_sections: tuple[str, ...] = ()
4045
for section in sections:
4146
if section == "FUTURE":
4247
base_sections = ("FUTURE",)
4348
continue
44-
parsed.imports["no_sections"]["straight"].update(
45-
parsed.imports[section].get("straight", {})
49+
parsed.imports["no_sections"]["straight"].update(parsed.imports[section]["straight"])
50+
parsed.imports["no_sections"]["from"].update(parsed.imports[section]["from"])
51+
parsed.imports["no_sections"]["lazy_straight"].update(
52+
parsed.imports[section]["lazy_straight"]
4653
)
47-
parsed.imports["no_sections"]["from"].update(parsed.imports[section].get("from", {}))
54+
parsed.imports["no_sections"]["lazy_from"].update(parsed.imports[section]["lazy_from"])
4855
sections = (*base_sections, "no_sections")
4956

5057
output: list[str] = []
5158
seen_headings: set[str] = set()
5259
pending_lines_before = False
5360
for section in sections:
54-
straight_modules: Iterable[str] = parsed.imports[section]["straight"]
55-
if not config.only_sections:
56-
straight_modules = sorting.sort(
57-
config,
58-
straight_modules,
59-
key=lambda key: sorting.module_key(
60-
key, config, section_name=section, straight_import=True
61-
),
62-
reverse=config.reverse_sort,
63-
)
64-
65-
from_modules: Iterable[str] = parsed.imports[section]["from"]
66-
if not config.only_sections:
67-
from_modules = sorting.sort(
68-
config,
69-
from_modules,
70-
key=lambda key: sorting.module_key(key, config, section_name=section),
71-
reverse=config.reverse_sort,
72-
)
73-
74-
if config.star_first:
75-
star_modules = []
76-
other_modules = []
77-
for module in from_modules:
78-
if "*" in parsed.imports[section]["from"][module]:
79-
star_modules.append(module)
80-
else:
81-
other_modules.append(module)
82-
from_modules = star_modules + other_modules
83-
84-
straight_imports = _with_straight_imports(
85-
parsed, config, straight_modules, section, remove_imports, import_type
86-
)
87-
from_imports = _with_from_imports(
88-
parsed, config, from_modules, section, remove_imports, import_type
61+
section_output = _build_import_group(
62+
parsed, config, section, remove_imports, import_type, is_lazy=False
8963
)
9064

91-
lines_between = [""] * (
92-
config.lines_between_types if from_modules and straight_modules else 0
65+
# PEP 810 lazy imports always follow all eager imports within the section.
66+
lazy_section_output = _build_import_group(
67+
parsed, config, section, remove_imports, import_type, is_lazy=True
9368
)
94-
if config.from_first or section == "FUTURE":
95-
section_output = from_imports + lines_between + straight_imports
96-
else:
97-
section_output = straight_imports + lines_between + from_imports
9869

99-
if config.force_sort_within_sections:
100-
# collapse comments
101-
comments_above = []
102-
new_section_output: list[str] = []
103-
for line in section_output:
104-
if not line:
105-
continue
106-
if line.startswith("#"):
107-
comments_above.append(line)
108-
elif comments_above:
109-
new_section_output.append(_LineWithComments(line, comments_above))
110-
comments_above = []
111-
else:
112-
new_section_output.append(line)
113-
# only_sections options is not imposed if force_sort_within_sections is True
114-
new_section_output = sorting.sort(
115-
config,
116-
new_section_output,
117-
key=partial(sorting.section_key, config=config),
118-
reverse=config.reverse_sort,
119-
)
120-
121-
# uncollapse comments
122-
section_output = []
123-
for line in new_section_output:
124-
comments = getattr(line, "comments", ())
125-
if comments:
126-
section_output.extend(comments)
127-
section_output.append(str(line))
70+
if lazy_section_output:
71+
if section_output:
72+
section_output += [""] * config.lines_between_types + lazy_section_output
73+
else:
74+
section_output = lazy_section_output
12875

12976
section_name = section
13077
no_lines_before = section_name in config.no_lines_before
@@ -243,6 +190,95 @@ def sorted_imports(
243190

244191
return _output_as_string(formatted_output, parsed.line_separator)
245192

193+
# Ignore DeepSource cyclomatic complexity check for this function.
194+
# skipcq: PY-R1000
195+
def _build_import_group(
196+
parsed: parse.ParsedContent,
197+
config: Config,
198+
section: str,
199+
remove_imports: list[str],
200+
import_type: str,
201+
*,
202+
is_lazy: bool,
203+
) -> list[str]:
204+
"""Build the sorted import lines for one group (eager or lazy) within a section."""
205+
straight_key: Literal["lazy_straight", "straight"] = "lazy_straight" if is_lazy else "straight"
206+
from_key: Literal["lazy_from", "from"] = "lazy_from" if is_lazy else "from"
207+
208+
straight_modules: Iterable[str] = parsed.imports[section][straight_key]
209+
if not config.only_sections:
210+
straight_modules = sorting.sort(
211+
config,
212+
straight_modules,
213+
key=lambda key: sorting.module_key(
214+
key, config, section_name=section, straight_import=True
215+
),
216+
reverse=config.reverse_sort,
217+
)
218+
219+
from_modules: Iterable[str] = parsed.imports[section][from_key]
220+
if not config.only_sections:
221+
from_modules = sorting.sort(
222+
config,
223+
from_modules,
224+
key=lambda key: sorting.module_key(key, config, section_name=section),
225+
reverse=config.reverse_sort,
226+
)
227+
228+
if not is_lazy and config.star_first:
229+
star_modules = []
230+
other_modules = []
231+
for module in from_modules:
232+
if "*" in parsed.imports[section]["from"][module]:
233+
star_modules.append(module)
234+
else:
235+
other_modules.append(module)
236+
from_modules = star_modules + other_modules
237+
238+
straight_imports = _with_straight_imports(
239+
parsed, config, straight_modules, section, remove_imports, import_type, is_lazy=is_lazy
240+
)
241+
from_imports = _with_from_imports(
242+
parsed, config, from_modules, section, remove_imports, import_type, is_lazy=is_lazy
243+
)
244+
245+
lines_between = [""] * (config.lines_between_types if from_modules and straight_modules else 0)
246+
if config.from_first or section == "FUTURE":
247+
group_output = from_imports + lines_between + straight_imports
248+
else:
249+
group_output = straight_imports + lines_between + from_imports
250+
251+
if config.force_sort_within_sections:
252+
# collapse comments
253+
comments_above: list[str] = []
254+
new_group_output: list[str] = []
255+
for line in group_output:
256+
if not line:
257+
continue
258+
if line.startswith("#"):
259+
comments_above.append(line)
260+
elif comments_above:
261+
new_group_output.append(_LineWithComments(line, comments_above))
262+
comments_above = []
263+
else:
264+
new_group_output.append(line)
265+
# only_sections option is not imposed if force_sort_within_sections is True
266+
new_group_output = sorting.sort(
267+
config,
268+
new_group_output,
269+
key=partial(sorting.section_key, config=config),
270+
reverse=config.reverse_sort,
271+
)
272+
# uncollapse comments
273+
group_output = []
274+
for line in new_group_output:
275+
line_comments = getattr(line, "comments", ())
276+
if line_comments:
277+
group_output.extend(line_comments)
278+
group_output.append(str(line))
279+
280+
return group_output
281+
246282

247283
# Ignore DeepSource cyclomatic complexity check for this function. It was
248284
# already complex when this check was enabled.
@@ -254,14 +290,21 @@ def _with_from_imports(
254290
section: str,
255291
remove_imports: list[str],
256292
import_type: str,
293+
*,
294+
is_lazy: bool,
257295
) -> list[str]:
258296
output: list[str] = []
297+
import_key: Literal["lazy_from", "from"] = "lazy_from" if is_lazy else "from"
298+
259299
for module in from_modules:
260300
if module in remove_imports:
261301
continue
262302

263303
import_start = f"from {module} {import_type} "
264-
from_imports = list(parsed.imports[section]["from"][module])
304+
if is_lazy:
305+
import_start = f"lazy {import_start}"
306+
from_imports = list(parsed.imports[section][import_key][module])
307+
265308
if (
266309
not config.no_inline_sort
267310
or (config.force_single_line and module not in config.single_line_exclusions)
@@ -299,7 +342,7 @@ def _with_from_imports(
299342
for from_import in copy.copy(from_imports):
300343
if from_import in as_imports:
301344
idx = from_imports.index(from_import)
302-
if parsed.imports[section]["from"][module][from_import]:
345+
if parsed.imports[section][import_key][module][from_import]:
303346
from_imports[(idx + 1) : (idx + 1)] = as_imports.pop(from_import)
304347
else:
305348
from_imports[idx : (idx + 1)] = as_imports.pop(from_import)
@@ -347,7 +390,7 @@ def _with_from_imports(
347390
)
348391
if from_import in as_imports:
349392
if (
350-
parsed.imports[section]["from"][module][from_import]
393+
parsed.imports[section][import_key][module][from_import]
351394
and not only_show_as_imports
352395
):
353396
output.append(
@@ -403,7 +446,7 @@ def _with_from_imports(
403446
parsed.categorized_comments["straight"].get(f"{module}.{from_import}") or []
404447
)
405448
if (
406-
parsed.imports[section]["from"][module][from_import]
449+
parsed.imports[section][import_key][module][from_import]
407450
and not only_show_as_imports
408451
):
409452
specific_comment = (
@@ -540,7 +583,7 @@ def _with_from_imports(
540583
from_imports[0] not in as_imports
541584
or (
542585
config.combine_as_imports
543-
and parsed.imports[section]["from"][module][from_import]
586+
and parsed.imports[section][import_key][module][from_import]
544587
)
545588
):
546589
from_import_section.append(from_imports.pop(0))
@@ -633,9 +676,13 @@ def _with_straight_imports(
633676
section: str,
634677
remove_imports: list[str],
635678
import_type: str,
679+
*,
680+
is_lazy: bool,
636681
) -> list[str]:
637682
output: list[str] = []
638683

684+
import_type = f"lazy {import_type}" if is_lazy else import_type
685+
639686
as_imports = any(module in parsed.as_map["straight"] for module in straight_modules)
640687

641688
# combine_straight_imports only works for bare imports, 'as' imports not included
@@ -675,7 +722,7 @@ def _with_straight_imports(
675722

676723
import_definition = []
677724
if module in parsed.as_map["straight"]:
678-
if parsed.imports[section]["straight"][module]:
725+
if parsed.imports[section]["lazy_straight" if is_lazy else "straight"][module]:
679726
import_definition.append((f"{import_type} {module}", module))
680727
import_definition.extend(
681728
(f"{import_type} {module} as {as_import}", f"{module} as {as_import}")

0 commit comments

Comments
 (0)