Skip to content

Preserve the magic trailing comma in --sort-reexports#2589

Open
dchaudhari7177 wants to merge 3 commits into
PyCQA:mainfrom
dchaudhari7177:fix/sort-reexports-magic-trailing-comma
Open

Preserve the magic trailing comma in --sort-reexports#2589
dchaudhari7177 wants to merge 3 commits into
PyCQA:mainfrom
dchaudhari7177:fix/sort-reexports-magic-trailing-comma

Conversation

@dchaudhari7177

Copy link
Copy Markdown

Summary

Closes #2578.

--sort-reexports runs the __all__ literal through ast.literal_eval and re-renders it, which loses the original formatting. _format_collection then picks one line whenever the result fits within line_length, so a trailing comma in the source was silently dropped:

__all__ = (
    "SecondClass",
    "FirstClass",
)
# became
__all__ = ("FirstClass", "SecondClass")

That trailing comma is black's magic trailing comma — a request for one element per line — so this preserves it.

#2582 covered the same issue and was withdrawn by its author; this is an independent implementation.

Implementation

_has_magic_trailing_comma tokenizes the literal and checks whether the token before the closing bracket is a comma. Tokenizing rather than scanning text matters: a bracket or comma inside a string element ("b#)", "a,") or a trailing comment would otherwise be mistaken for the end of the literal. There is a test for exactly that.

When a magic trailing comma is found, assignment derives a config with line_length=0 and include_trailing_comma=True, which forces the vertical-hanging-indent branch of _format_collection. This uses the existing Config(config=..., ...) derivation pattern already used by _indented_config, and deliberately avoids changing the signature of the registered sort functions, since register_type is public API.

One-element tuples are excluded. __all__ = ("foo",) needs its comma to stay a tuple — that is syntax, not a formatting request, and black does not explode it either. Without this carve-out every single-export __all__ would get exploded.

Behaviour change to existing tests

Four existing tests asserted the current output and had to be updated: test_reexport_multiline, test_reexport_multiline_import, test_reexport_multiline_in_center, test_reexport_multiline_long_rollback. Every one of them feeds in a literal that has a magic trailing comma and asserted it collapsed to a single line — i.e. they pinned the reported bug. Their inputs are unchanged; only the expected output now stays exploded.

Worth a look during review, since it is the part that is a deliberate behaviour change rather than a fix. The two rollback/centre tests still pass, so the reexport_rollback seek logic is fine with multi-line output.

test_reexport_list, test_reexport_set and test_reexport_bare are untouched and still pass — none of them have a trailing comma, and the bare __all__ = 'foo', 'bar' form has no brackets so detection correctly ignores it.

Tests

Four added: the magic comma is preserved; a literal without one still collapses; a one-element tuple is left alone; and the tokenizer-based detection is not fooled by strings or comments.

Revert-verified: with literal.py reverted, the two behaviour tests fail and the two control tests still pass.

Local results (Windows, Python 3.12)

pytest tests/unit604 passed, 4 failed, 5 skipped. The same 4 fail on a clean tree (test_importable, test_windows_diff_too_large_misrepresentative_issue_1348, test_isort_supports_shared_profiles_issue_970, test_sort_configurable_sort_issue_1732); baseline is 600 passed. ruff check, ruff format --check and mypy isort/literal.py are all clean.

--sort-reexports re-renders __all__ from the literal_eval'd value, and
_format_collection collapses to a single line whenever the result fits in
line_length. A trailing comma in the source was therefore dropped, against
black's magic-trailing-comma rule that it asks for one element per line.

Detect the trailing comma by tokenizing the literal, so a bracket or comma
inside a string element, or a trailing comment, is not mistaken for the end
of the literal, and force the vertical-hanging-indent branch when one is
found.

A one-element tuple is excluded: its trailing comma is required syntax
rather than a formatting request, and black does not explode it either.

Four existing reexport tests asserted the collapsed output for inputs that
carry a magic trailing comma; their expected output is updated.
IndentationError is a subclass of SyntaxError, so catching both is
redundant (DeepSource PYL-W0714).
@codecov

codecov Bot commented Jul 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.39%. Comparing base (68356ea) to head (741ee0b).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #2589   +/-   ##
=======================================
  Coverage   99.39%   99.39%           
=======================================
  Files          41       41           
  Lines        3147     3156    +9     
  Branches      680      681    +1     
=======================================
+ Hits         3128     3137    +9     
  Misses         11       11           
  Partials        8        8           
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@DanielNoord DanielNoord left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we fix this without touching the expected output for some of these tests? The expected output for some of those cases shouldn't change.

Per review: the magic-trailing-comma preservation should not change the
established collapsed output of the import-focused reexport tests
(test_reexport_multiline_import / _in_center / _long_rollback). Their
__all__ trailing comma was incidental, not a deliberate magic comma, so
drop it from those inputs — they keep asserting the original collapsed
'__all__ = ["bar", "foo"]'. Only test_reexport_multiline (the deliberate
multiline __all__) now demonstrates the preserved comma, alongside the
dedicated new tests.
@dchaudhari7177

Copy link
Copy Markdown
Author

Good call — reverted the expected-output changes on the three import-focused tests (test_reexport_multiline_import, _in_center, _long_rollback). Their __all__ trailing comma was incidental rather than a deliberate magic comma, so I dropped it from those inputs; they now keep asserting the original collapsed __all__ = ["bar", "foo"].

Only test_reexport_multiline (whose __all__ is a deliberately multi-line, trailing-comma tuple) still demonstrates the preserved comma, alongside the dedicated new tests (test_reexport_preserves_magic_trailing_comma, …_without_…_stays_collapsed, …_single_element_tuple_comma_is_not_magic, …_ignores_strings_and_comments). Full test_isort.py + test_literal.py pass. Happy to adjust which cases stay unchanged if you had a different split in mind.

@DanielNoord DanielNoord left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are still changing expected outputs. Ideally, you should not need to touch any of the existing tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

--sort-reexports does not retain trailing commas in short __all__ exports

2 participants