Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions isort/_parse_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,16 @@ def normalize_from_import_string(import_string: str) -> str:


# TODO: Return a `StrEnum` once we no longer support Python 3.10.
def import_type(line: str, config: Config) -> Literal["from", "straight"] | None:
"""If the current line is an import line it will return its type (from or straight)"""
def import_type(
line: str, config: Config
) -> Literal["from", "straight", "lazy_from", "lazy_straight"] | None:
"""If the current line is an import line it will return its type.

Possible return values are ``"straight"`` (``import X``), ``"from"``
(``from X import Y``), ``"lazy_straight"`` (``lazy import X``), and
``"lazy_from"`` (``lazy from X import Y``). Returns ``None`` for
non-import lines or lines that should be skipped.
"""
if config.honor_noqa and line.lower().rstrip().endswith("noqa"):
return None
if "isort:skip" in line or "isort: skip" in line or "isort: split" in line:
Expand All @@ -176,4 +184,8 @@ def import_type(line: str, config: Config) -> Literal["from", "straight"] | None
return "straight"
if line.startswith("from "):
return "from"
if line.startswith("lazy import "):
return "lazy_straight"
if line.startswith("lazy from "):
return "lazy_from"
return None
10 changes: 9 additions & 1 deletion isort/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,15 @@
from .settings import FILE_SKIP_COMMENTS

CIMPORT_IDENTIFIERS = ("cimport ", "cimport*", "from.cimport")
IMPORT_START_IDENTIFIERS = ("from ", "from.import", "import ", "import*", *CIMPORT_IDENTIFIERS)
IMPORT_START_IDENTIFIERS = (
"from ",
"from.import",
"import ",
"import*",
"lazy import ",
"lazy from ",
*CIMPORT_IDENTIFIERS,
)
DOCSTRING_INDICATORS = ('"""', "'''")
COMMENT_INDICATORS = (*DOCSTRING_INDICATORS, "'", '"', "#")
CODE_SORT_COMMENTS = (
Expand Down
86 changes: 78 additions & 8 deletions isort/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def sorted_imports(
sections: Iterable[str] = itertools.chain(parsed.sections, config.forced_separate)

if config.no_sections:
parsed.imports["no_sections"] = {"straight": {}, "from": {}}
parsed.imports["no_sections"] = {"straight": {}, "from": {}, "lazy_straight": {}, "lazy_from": {}}
base_sections: tuple[str, ...] = ()
for section in sections:
if section == "FUTURE":
Expand All @@ -45,6 +45,12 @@ def sorted_imports(
parsed.imports[section].get("straight", {})
)
parsed.imports["no_sections"]["from"].update(parsed.imports[section].get("from", {}))
parsed.imports["no_sections"]["lazy_straight"].update(
parsed.imports[section].get("lazy_straight", {})
)
parsed.imports["no_sections"]["lazy_from"].update(
parsed.imports[section].get("lazy_from", {})
)
sections = (*base_sections, "no_sections")

output: list[str] = []
Expand Down Expand Up @@ -126,6 +132,62 @@ def sorted_imports(
section_output.extend(comments)
section_output.append(str(line))

# PEP 810 lazy imports always follow all eager imports within the section.
lazy_straight_modules = parsed.imports[section].get("lazy_straight", {})
if not config.only_sections:
lazy_straight_modules = sorting.sort(
config,
lazy_straight_modules,
key=lambda key: sorting.module_key(
key, config, section_name=section, straight_import=True
),
reverse=config.reverse_sort,
)

lazy_from_modules = parsed.imports[section].get("lazy_from", {})
if not config.only_sections:
lazy_from_modules = sorting.sort(
config,
lazy_from_modules,
key=lambda key: sorting.module_key(key, config, section_name=section),
reverse=config.reverse_sort,
)

lazy_straight_imports = _with_straight_imports(
parsed,
config,
lazy_straight_modules,
section,
remove_imports,
f"lazy {import_type}",
import_key="lazy_straight",
)
lazy_from_imports = _with_from_imports(
parsed,
config,
lazy_from_modules,
section,
remove_imports,
f"lazy {import_type}",
import_key="lazy_from",
)

lazy_lines_between = [""] * (
config.lines_between_types
if lazy_from_modules and lazy_straight_modules
else 0
)
if config.from_first or section == "FUTURE":
lazy_section_output = lazy_from_imports + lazy_lines_between + lazy_straight_imports
else:
lazy_section_output = lazy_straight_imports + lazy_lines_between + lazy_from_imports

if lazy_section_output:
if section_output:
section_output += [""] * config.lines_between_types + lazy_section_output
else:
section_output = lazy_section_output

section_name = section
no_lines_before = section_name in config.no_lines_before

Expand Down Expand Up @@ -254,14 +316,21 @@ def _with_from_imports(
section: str,
remove_imports: list[str],
import_type: str,
import_key: str = "from",
) -> list[str]:
output: list[str] = []
for module in from_modules:
if module in remove_imports:
continue

import_start = f"from {module} {import_type} "
from_imports = list(parsed.imports[section]["from"][module])
# For lazy from imports the keyword order is ``lazy from X import ...``
# rather than the naive ``from X lazy import ...`` that would result from
# placing ``import_type`` between the module name and ``import``.
if import_type.startswith("lazy "):
import_start = f"lazy from {module} import "
else:
import_start = f"from {module} {import_type} "
from_imports = list(parsed.imports[section][import_key][module])
if (
not config.no_inline_sort
or (config.force_single_line and module not in config.single_line_exclusions)
Expand Down Expand Up @@ -299,7 +368,7 @@ def _with_from_imports(
for from_import in copy.copy(from_imports):
if from_import in as_imports:
idx = from_imports.index(from_import)
if parsed.imports[section]["from"][module][from_import]:
if parsed.imports[section][import_key][module][from_import]:
from_imports[(idx + 1) : (idx + 1)] = as_imports.pop(from_import)
else:
from_imports[idx : (idx + 1)] = as_imports.pop(from_import)
Expand Down Expand Up @@ -347,7 +416,7 @@ def _with_from_imports(
)
if from_import in as_imports:
if (
parsed.imports[section]["from"][module][from_import]
parsed.imports[section][import_key][module][from_import]
and not only_show_as_imports
):
output.append(
Expand Down Expand Up @@ -403,7 +472,7 @@ def _with_from_imports(
parsed.categorized_comments["straight"].get(f"{module}.{from_import}") or []
)
if (
parsed.imports[section]["from"][module][from_import]
parsed.imports[section][import_key][module][from_import]
and not only_show_as_imports
):
specific_comment = (
Expand Down Expand Up @@ -540,7 +609,7 @@ def _with_from_imports(
from_imports[0] not in as_imports
or (
config.combine_as_imports
and parsed.imports[section]["from"][module][from_import]
and parsed.imports[section][import_key][module][from_import]
)
):
from_import_section.append(from_imports.pop(0))
Expand Down Expand Up @@ -633,6 +702,7 @@ def _with_straight_imports(
section: str,
remove_imports: list[str],
import_type: str,
import_key: str = "straight",
) -> list[str]:
output: list[str] = []

Expand Down Expand Up @@ -675,7 +745,7 @@ def _with_straight_imports(

import_definition = []
if module in parsed.as_map["straight"]:
if parsed.imports[section]["straight"][module]:
if parsed.imports[section][import_key][module]:
import_definition.append((f"{import_type} {module}", module))
import_definition.extend(
(f"{import_type} {module} as {as_import}", f"{module} as {as_import}")
Expand Down
36 changes: 31 additions & 5 deletions isort/parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,12 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte
verbose_output: list[str] = []

for section in chain(config.sections, config.forced_separate):
imports[section] = {"straight": OrderedDict(), "from": OrderedDict()}
imports[section] = {
"straight": OrderedDict(),
"from": OrderedDict(),
"lazy_straight": OrderedDict(),
"lazy_from": OrderedDict(),
}
categorized_comments: CommentsDict = {
"from": {},
"straight": {},
Expand Down Expand Up @@ -185,6 +190,15 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte
out_lines.append(raw_line)
continue

# Detect PEP 810 lazy imports (``lazy import X`` / ``lazy from X import Y``).
# We strip the ``lazy `` prefix so the rest of the parsing logic works normally
# on the resulting ``import X`` / ``from X import Y`` string. The original
# lazy type is remembered in ``is_lazy`` and used later when storing the result.
is_lazy = type_of_import in ("lazy_straight", "lazy_from")
if is_lazy:
line = line[len("lazy "):]
type_of_import = type_of_import[len("lazy_"):] # "lazy_straight" → "straight"

if import_index == -1:
import_index = index - 1
nested_comments = {}
Expand Down Expand Up @@ -305,7 +319,7 @@ def _get_next_line() -> tuple[str, str | None]:
if placed_module and placed_module not in imports:
raise MissingSection(import_module=import_from, section=placed_module)

root = imports[placed_module][type_of_import]
root = imports[placed_module]["lazy_from" if is_lazy else type_of_import]
for import_name in just_imports:
associated_comment = nested_comments.get(import_name)
if associated_comment is not None:
Expand Down Expand Up @@ -420,13 +434,25 @@ def _get_next_line() -> tuple[str, str | None]:
" Do you need to define a default section?",
stacklevel=2,
)
imports.setdefault("", {"straight": OrderedDict(), "from": OrderedDict()})
imports.setdefault(
"",
{
"straight": OrderedDict(),
"from": OrderedDict(),
"lazy_straight": OrderedDict(),
"lazy_from": OrderedDict(),
},
)

if placed_module and placed_module not in imports:
raise MissingSection(import_module=module, section=placed_module)

straight_import |= imports[placed_module][type_of_import].get(module, False)
imports[placed_module][type_of_import][module] = straight_import
straight_import |= imports[placed_module][
"lazy_straight" if is_lazy else type_of_import
].get(module, False)
imports[placed_module]["lazy_straight" if is_lazy else type_of_import][
module
] = straight_import

change_count = len(out_lines) - original_line_count

Expand Down
Loading
Loading