diff --git a/docs/configuration/options.md b/docs/configuration/options.md index 6308ac03..94d9c958 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -232,6 +232,19 @@ Forces line endings to the specified value. If not set, values will be guessed p - --le - --line-ending +## Sort Re-exports + +Specifies whether to sort re-exports (`__all__` collections) automatically. + +**Type:** Bool +**Default:** `False` +**Config default:** `false` +**Python & Config File Name:** sort_reexports +**CLI Flags:** + +- --srx +- --sort-reexports + ## Sections What sections isort should display imports for and in what order diff --git a/isort/core.py b/isort/core.py index 4c77a1d9..297e7260 100644 --- a/isort/core.py +++ b/isort/core.py @@ -24,11 +24,7 @@ "# isort: unique-tuple", "# isort: assignments", ) -LITERAL_TYPE_MAPPING = { - "(": "tuple", - "[": "list", - "{": "dict", -} +LITERAL_TYPE_MAPPING = {"(": "tuple", "[": "list", "{": "set"} def process( @@ -78,8 +74,8 @@ def process( end_of_file: bool = False verbose_output: List[str] = [] lines_before: List[str] = [] - auto_reexporting: bool = False - line_index: int = 0 + is_reexport: bool = False + sort_section_pointer: int = 0 if config.float_to_top: new_input = "" @@ -137,6 +133,8 @@ def process( line_separator = "\n" if code_sorting and code_sorting_section: + if is_reexport: + output_stream.seek(sort_section_pointer, 0) sorted_code = textwrap.indent( isort.literal.assignment( code_sorting_section, @@ -152,7 +150,7 @@ def process( line_separator=line_separator, ignore_whitespace=config.ignore_whitespace, ) - line_index += output_stream.write(sorted_code) + sort_section_pointer += output_stream.write(sorted_code) else: stripped_line = line.strip() if stripped_line and not line_separator: @@ -233,12 +231,13 @@ def process( code_sorting_indent = line[: -len(line.lstrip())] not_imports = True elif config.sort_reexports and stripped_line.startswith("__all__"): - code_sorting = LITERAL_TYPE_MAPPING[stripped_line.split(" = ")[1][0]] + _, rhs = stripped_line.split("=") + code_sorting = LITERAL_TYPE_MAPPING.get(rhs.lstrip()[0], "tuple") code_sorting_indent = line[: -len(line.lstrip())] not_imports = True code_sorting_section += line - auto_reexporting = True - line_index -= len(line) - 1 + is_reexport = True + sort_section_pointer -= len(line) elif code_sorting: if not stripped_line: sorted_code = textwrap.indent( @@ -256,14 +255,14 @@ def process( line_separator=line_separator, ignore_whitespace=config.ignore_whitespace, ) - if auto_reexporting: - output_stream.seek(line_index, 0) - line_index += output_stream.write(sorted_code) + if is_reexport: + output_stream.seek(sort_section_pointer, 0) + sort_section_pointer += output_stream.write(sorted_code) not_imports = True code_sorting = False code_sorting_section = "" code_sorting_indent = "" - auto_reexporting = False + is_reexport = False else: code_sorting_section += line line = "" @@ -357,7 +356,7 @@ def process( else: not_imports = True - line_index += len(line) + sort_section_pointer += len(line) if not_imports: diff --git a/isort/literal.py b/isort/literal.py index 62feea13..d2100699 100644 --- a/isort/literal.py +++ b/isort/literal.py @@ -47,8 +47,9 @@ 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 = variable_name.lstrip() + variable_name, literal = code.split("=") + variable_name = variable_name.strip() + literal = literal.lstrip() try: value = ast.literal_eval(literal) except Exception as error: diff --git a/isort/main.py b/isort/main.py index 293d2ed8..26310048 100644 --- a/isort/main.py +++ b/isort/main.py @@ -323,6 +323,7 @@ def _build_arg_parser() -> argparse.ArgumentParser: help="Override the format used to print success.", ) general_group.add_argument( + "--srx", "--sort-reexports", dest="sort_reexports", action="store_true", diff --git a/tests/unit/test_isort.py b/tests/unit/test_isort.py index 6f4f03a0..5de6ce69 100644 --- a/tests/unit/test_isort.py +++ b/tests/unit/test_isort.py @@ -5591,3 +5591,84 @@ def test_infinite_loop_in_unmatched_parenthesis() -> None: # ensure other cases are handled correctly assert isort.code(test_input) == "from os import path, walk\n" + + +def test_reexport() -> None: + test_input = """__all__ = ('foo', 'bar') +""" + expd_output = """__all__ = ('bar', 'foo') +""" + assert isort.code(test_input, config=Config(sort_reexports=True)) == expd_output + + +def test_reexport_leave_alone_if_not_enabled() -> None: + test_input = """__all__ = ('foo', 'bar') +""" + assert isort.code(test_input) == test_input + + +def test_reexport_multiline() -> None: + test_input = """__all__ = ( + 'foo', + 'bar', +) +""" + expd_output = """__all__ = ('bar', 'foo') +""" + assert isort.code(test_input, config=Config(sort_reexports=True)) == expd_output + + +def test_reexport_list() -> None: + test_input = """__all__ = ['foo', 'bar'] +""" + expd_output = """__all__ = ['bar', 'foo'] +""" + assert isort.code(test_input, config=Config(sort_reexports=True)) == expd_output + + +def test_reexport_set() -> None: + test_input = """__all__ = {'foo', 'bar'} +""" + expd_output = """__all__ = {'bar', 'foo'} +""" + assert isort.code(test_input, config=Config(sort_reexports=True)) == expd_output + + +def test_reexport_bare() -> None: + test_input = """__all__ = 'foo', 'bar' +""" + expd_output = """__all__ = ('bar', 'foo') +""" + assert isort.code(test_input, config=Config(sort_reexports=True)) == expd_output + + +def test_reexport_no_spaces() -> None: + test_input = """__all__=('foo', 'bar') +""" + expd_output = """__all__ = ('bar', 'foo') +""" + assert isort.code(test_input, config=Config(sort_reexports=True)) == expd_output + + +def test_reexport_not_first_line() -> None: + test_input = """import random + + __all__ = ('foo', 'bar') +""" + expd_output = """import random + + __all__ = ('bar', 'foo') +""" + assert isort.code(test_input, config=Config(sort_reexports=True)) == expd_output + + +def test_reexport_not_last_line() -> None: + test_input = """__all__ = ('foo', 'bar') + + meme = "rickroll" +""" + expd_output = """__all__ = ('bar', 'foo') + + meme = "rickroll" +""" + assert isort.code(test_input, config=Config(sort_reexports=True)) == expd_output