Skip to content

fix: make sorted __all__ and literals black-compatible (#2280)#2576

Open
lord-haffi wants to merge 6 commits into
PyCQA:mainfrom
lord-haffi:issue/2280
Open

fix: make sorted __all__ and literals black-compatible (#2280)#2576
lord-haffi wants to merge 6 commits into
PyCQA:mainfrom
lord-haffi:issue/2280

Conversation

@lord-haffi

@lord-haffi lord-haffi commented Jul 6, 2026

Copy link
Copy Markdown

Resolves: #2280, #1815

Problem

--sort-reexports (and the # isort: list/dict/tuple/… comments) format the sorted result with stdlib pprint. Under --profile=black the result is single-quoted, packed in pprint's wrap style and lacks a trailing comma — output black immediately reformats, so isort + black never reach a fixpoint.

Fix

Format the sorted literal to be black-compatible: double quotes, a single line when it fits line_length, otherwise one item per line (vertical-hanging-indent) with a trailing comma when include_trailing_comma is set, keeping the literal's own bracket type. It's a fixed black/PEP 8 layout — it deliberately does not reproduce every multi_line_output mode for literals (VHI is what black uses).

isort's output for a long __all__ under --profile=black, before → after this fix:

-__all__ = ['Address', 'AliasAddress', 'BankAccountConnection', 'Certificate',
- 'ConcessionFee']
+__all__ = [
+    "Address",
+    "AliasAddress",
+    "BankAccountConnection",
+    "Certificate",
+    "ConcessionFee",
+]

Why drop pprint

pprint exposes only width; quote style, wrap mode and trailing comma are hardcoded to non-black output, so it could never satisfy --profile=black. It was the sole cause of the bug, so it's removed entirely — dict sorting now uses the same formatter and gets the same fix.

Also fixes a --check crash on multi-line __all__

Emitting multi-line __all__ exposed a latent bug in the --sort-reexports rollback: it wrote the __all__ opening line then did output_stream.seek(output_stream.tell() - reexport_rollback) to overwrite it with the sorted literal. In check mode the output is a no-op stream whose tell() is always 0, so the seek went to a negative position and raised ValueError: Negative seek position. The seek target is now clamped to 0; real seekable output is unaffected.

Note: sorted string literals are now double-quoted by default (isort has no quote setting); existing literal-sort test expectations are updated to match.

isort sorted __all__ (--sort-reexports) and inline literals (# isort: list,
# isort: dict, # isort: tuple, ...) through stdlib pprint, which reads only
line_length and ignores quote style, wrapping mode and trailing commas. Under
--profile=black this produced single-quoted, pprint-wrapped, comma-less output
that black immediately reformatted, so isort + black never reached a fixpoint.

Replace the pprint path with a small black-compatible formatter in isort.literal:

- black-style quoting: prefer double quotes, fall back to single only to avoid
  escaping, and defer to repr() for backslash/control chars and non-strings;
- a single line when it fits line_length, otherwise vertical-hanging-indent
  with a trailing comma controlled by include_trailing_comma;
- the literal's own bracket type is preserved, keeping the mandatory trailing
  comma that a one-element tuple needs.

pprint and ISortPrettyPrinter are removed entirely; dict sorting is formatted
the same way, fixing the identical black incompatibility for # isort: dict.

Closes PyCQA#2280
# Conflicts:
#	tests/unit/test_literal.py
#	tests/unit/test_regressions.py
…tive (PyCQA#2280)

The reexport handler wrote the `__all__` opening line to the output and then did
`output_stream.seek(output_stream.tell() - reexport_rollback)` to overwrite it with
the sorted literal. In check mode the output is a no-op stream (`_EmptyIO`) whose
`tell()` is always 0, so a multi-line `__all__` made the seek target negative and
raised `ValueError: Negative seek position`. Because the black-compatible formatter
now emits multi-line `__all__`, `isort --check` crashed on isort's own output.

Clamp the seek target to 0. Real seekable output is unaffected: there `tell()` is
always >= the rollback length, so the clamp is a no-op.
@lord-haffi

Copy link
Copy Markdown
Author

@Helveg Please take a look. For me it would work. I deliberately did not try to merge it with the formatting logic for the imports. In my head it made sense to keep that seperated from this logic here even though it is very similar at some parts. What are your thoughts on this?

@lord-haffi

Copy link
Copy Markdown
Author

I just realized, it probably addresses the same problem as #2573.

@lord-haffi

Copy link
Copy Markdown
Author

Claude says:

Overlap with #2573

Heads-up that #2573 (thanks @HarperZ9) targets the same area and also closes #1815, so these two should be reconciled. This PR also resolves #1815 and is a superset of #2573 — I ran #2573 locally against the relevant cases to compare fairly:

Case #2573 this PR (#2576)
# isort: tuple under --profile=black (#1815) wraps to VHI but keeps single quotes (via pprint.pformat), so black --check still reformats it double-quoted, black --check clean
Long __all__ under --profile=black single-quoted VHI → not black-stable black-stable (verified on a real 196-entry __all__)
isort --check on a multi-line __all__ preceded by imports still raises ValueError: Negative seek position fixed
# isort: dict unchanged (still pprint) same config-aware formatting

Root differences:

  • Wrap sorted literals with black profile settings #2573 adds VHI wrapping only for the over-length + use_parentheses + VERTICAL_HANGING_INDENT path and otherwise keeps pprint, which emits single quotes — so the output isn't black-stable (black renormalizes the quotes).
  • This PR removes pprint from literal sorting entirely and applies black's quote rule + single-line-or-VHI to every literal type, and additionally clamps the sort_reexports rollback seek in core.py (isort --check on a multi-line __all__ currently crashes — see the check-mode note above).

@codecov

codecov Bot commented Jul 6, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.39%. Comparing base (fd8bd07) to head (339aaba).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2576      +/-   ##
==========================================
+ Coverage   99.32%   99.39%   +0.06%     
==========================================
  Files          41       41              
  Lines        3111     3137      +26     
  Branches      674      680       +6     
==========================================
+ Hits         3090     3118      +28     
  Misses         11       11              
+ Partials       10        8       -2     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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 respect --profile=black

1 participant