diff --git a/isort/core.py b/isort/core.py index 9824c23b2..c1d1132f3 100644 --- a/isort/core.py +++ b/isort/core.py @@ -27,6 +27,38 @@ LITERAL_TYPE_MAPPING = {"(": "tuple", "[": "list", "{": "set"} +def _net_open_brackets(line: str) -> int: + """Return the net number of open brackets in a line, correctly ignoring + brackets that appear inside string literals or comments. + """ + depth = 0 + in_string = "" + i = 0 + while i < len(line): + char = line[i] + if in_string: + if char == "\\" and len(in_string) == 1: + i += 1 # skip the escaped character + elif line[i : i + len(in_string)] == in_string: + i += len(in_string) - 1 + in_string = "" + elif char in ('"', "'"): + triple = line[i : i + 3] + if triple in ('"""', "'''"): + in_string = triple + i += 2 + else: + in_string = char + elif char == "#": + break # rest of the line is a comment + elif char in ("(", "[", "{"): + depth += 1 + elif char in (")", "]", "}"): + depth -= 1 + i += 1 + return depth + + # Ignore DeepSource cyclomatic complexity check for this function. # skipcq: PY-R1000 def process( @@ -78,6 +110,7 @@ def process( lines_before: list[str] = [] is_reexport: bool = False reexport_rollback: int = 0 + reexport_bracket_depth: int = 0 if config.float_to_top: new_input = "" @@ -236,15 +269,30 @@ def process( code_sorting_indent = line[: -len(line.lstrip())] not_imports = True elif config.sort_reexports and stripped_line.startswith("__all__"): - _, rhs = stripped_line.split("=") + _, rhs = stripped_line.split("=", 1) code_sorting = LITERAL_TYPE_MAPPING.get(rhs.lstrip()[0], "tuple") code_sorting_indent = line[: -len(line.lstrip())] not_imports = True code_sorting_section += line reexport_rollback = len(line) is_reexport = True + reexport_bracket_depth = _net_open_brackets(stripped_line) elif code_sorting: + _process_section = False if not stripped_line: + _process_section = True + elif is_reexport and reexport_bracket_depth <= 0: + # The __all__ assignment is complete; process it and let + # the current line be handled normally (don't set line = "") + _process_section = True + else: + code_sorting_section += line + line = "" + if is_reexport: + reexport_bracket_depth += _net_open_brackets(stripped_line) + if reexport_bracket_depth <= 0: + _process_section = True + if _process_section: sorted_code = textwrap.indent( isort.literal.assignment( code_sorting_section, @@ -271,9 +319,7 @@ def process( code_sorting_section = "" code_sorting_indent = "" is_reexport = False - else: - code_sorting_section += line - line = "" + reexport_bracket_depth = 0 elif ( stripped_line in config.section_comments or stripped_line in config.section_comments_end diff --git a/isort/literal.py b/isort/literal.py index 16ddcba08..003892a5f 100644 --- a/isort/literal.py +++ b/isort/literal.py @@ -48,7 +48,7 @@ def assignment(code: str, sort_type: str, extension: str, config: Config = DEFAU f"Defined sort types are {', '.join(type_mapping.keys())}." ) - variable_name, literal = code.split("=") + variable_name, literal = code.split("=", 1) variable_name = variable_name.strip() literal = literal.lstrip() try: diff --git a/tests/unit/test_isort.py b/tests/unit/test_isort.py index 2a0237f18..e8db57753 100644 --- a/tests/unit/test_isort.py +++ b/tests/unit/test_isort.py @@ -5719,6 +5719,42 @@ def test_reexport_multiline_long_rollback() -> None: assert isort.code(test_input, config=Config(sort_reexports=True)) == expd_output +def test_reexport_multiline_no_trailing_blank_line() -> None: + """Test that sort_reexports works when __all__ is followed immediately by + another assignment without a blank line (issue: too many values to unpack). + """ + test_input = """#!/usr/bin/env python +import importlib.metadata + +__all__ = [ + "FooType", + "BarType", + "some_method", +] +__version__ = importlib.metadata.version("my-package") +""" + expd_output = """#!/usr/bin/env python +import importlib.metadata + +__all__ = ['BarType', 'FooType', 'some_method'] +__version__ = importlib.metadata.version("my-package") +""" + assert isort.code(test_input, config=Config(sort_reexports=True)) == expd_output + + +def test_reexport_singleline_no_trailing_blank_line() -> None: + """Test that sort_reexports works for single-line __all__ followed immediately + by another assignment without a blank line. + """ + test_input = """__all__ = ('foo', 'bar') +__version__ = '1.0' +""" + expd_output = """__all__ = ('bar', 'foo') +__version__ = '1.0' +""" + assert isort.code(test_input, config=Config(sort_reexports=True)) == expd_output + + def test_noqa_multiline_hanging_indent() -> None: test_input = ( "from aaaaaaa import bbbbbbbbbbbbbbbbb, ccccccccccccccccccc, dddddddddddddddd"