Skip to content

Commit fbcd23d

Browse files
authored
Merge pull request #2503 from PyCQA/copilot/2462-sort-import-statements
Sort lazy import statements
2 parents 851471b + 07d3275 commit fbcd23d

5 files changed

Lines changed: 317 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: 129 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
@@ -244,6 +191,96 @@ def sorted_imports(
244191
return _output_as_string(formatted_output, parsed.line_separator)
245192

246193

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

263304
import_start = f"from {module} {import_type} "
264-
from_imports = list(parsed.imports[section]["from"][module])
305+
if is_lazy:
306+
import_start = f"lazy {import_start}"
307+
from_imports = list(parsed.imports[section][import_key][module])
308+
265309
if (
266310
not config.no_inline_sort
267311
or (config.force_single_line and module not in config.single_line_exclusions)
@@ -299,7 +343,7 @@ def _with_from_imports(
299343
for from_import in copy.copy(from_imports):
300344
if from_import in as_imports:
301345
idx = from_imports.index(from_import)
302-
if parsed.imports[section]["from"][module][from_import]:
346+
if parsed.imports[section][import_key][module][from_import]:
303347
from_imports[(idx + 1) : (idx + 1)] = as_imports.pop(from_import)
304348
else:
305349
from_imports[idx : (idx + 1)] = as_imports.pop(from_import)
@@ -347,7 +391,7 @@ def _with_from_imports(
347391
)
348392
if from_import in as_imports:
349393
if (
350-
parsed.imports[section]["from"][module][from_import]
394+
parsed.imports[section][import_key][module][from_import]
351395
and not only_show_as_imports
352396
):
353397
output.append(
@@ -403,7 +447,7 @@ def _with_from_imports(
403447
parsed.categorized_comments["straight"].get(f"{module}.{from_import}") or []
404448
)
405449
if (
406-
parsed.imports[section]["from"][module][from_import]
450+
parsed.imports[section][import_key][module][from_import]
407451
and not only_show_as_imports
408452
):
409453
specific_comment = (
@@ -540,7 +584,7 @@ def _with_from_imports(
540584
from_imports[0] not in as_imports
541585
or (
542586
config.combine_as_imports
543-
and parsed.imports[section]["from"][module][from_import]
587+
and parsed.imports[section][import_key][module][from_import]
544588
)
545589
):
546590
from_import_section.append(from_imports.pop(0))
@@ -633,9 +677,13 @@ def _with_straight_imports(
633677
section: str,
634678
remove_imports: list[str],
635679
import_type: str,
680+
*,
681+
is_lazy: bool,
636682
) -> list[str]:
637683
output: list[str] = []
638684

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

641689
# combine_straight_imports only works for bare imports, 'as' imports not included
@@ -675,7 +723,7 @@ def _with_straight_imports(
675723

676724
import_definition = []
677725
if module in parsed.as_map["straight"]:
678-
if parsed.imports[section]["straight"][module]:
726+
if parsed.imports[section]["lazy_straight" if is_lazy else "straight"][module]:
679727
import_definition.append((f"{import_type} {module}", module))
680728
import_definition.extend(
681729
(f"{import_type} {module} as {as_import}", f"{module} as {as_import}")

0 commit comments

Comments
 (0)