Bug: scope no_color color-stripping to the Outputter lifetime - #346
Merged
Conversation
Saving and restoring colorama's global Fore/Style state around the Outputter (init -> __exit__) prevents no_color=True from permanently blanking the process-wide color singletons, which previously left later compare() calls -- and any other in-process colorama users -- colorless. In-block behavior (including column alignment) is unchanged. Adds a regression test asserting the color constants are blanked inside the context and restored on exit. Closes #345 Co-Authored-By: Claude <noreply@anthropic.com>
…o-color-state-leak # Conflicts: # CHANGELOG.md
danielfromearth
marked this pull request as ready for review
August 22, 2026 21:50
…o-color-state-leak Brings the branch up to date with develop (1.15.0a10). All conflicts were incidental additive churn, with no contested logic: - CHANGELOG.md: both sides appended bullets under `### Fixed`; kept all of them. - tests/test_printing.py: both sides added imports and appended tests; kept both imports and all three tests. ncompare/printing.py auto-merged: develop's changes there (the column_widths property, the ValueError for a wrong width count, the _add_to_history rewrite) are in regions disjoint from this branch's __exit__ restore. Merging onto current develop is also what finally lets CI test this branch. The pull-request workflow filters on the base branch, and `develop` was only recently added to that filter; because a pull_request run reads the workflow from the merge ref, and this branch's merge ref was both stale and conflicted, the unit suite had never run on this PR.
…r is built The `__exit__` restore added earlier scopes the no-color mutation to the Outputter's lifetime, but only for callers that use the context manager. Nothing obliges them to: `tests/conftest.py` builds bare Outputters, and a library user can too. Such an Outputter still blanked colorama's process-wide `Fore`/`Style` permanently, which is the symptom issue #345 describes. Snapshot the pristine color constants at import, before any Outputter can touch them, and restore from that snapshot whenever an Outputter is constructed with color enabled. The mutation is now bounded from both ends: `__exit__` undoes it for context-manager users, and asking for color repairs it for everyone else. `__exit__` keeps restoring the per-instance snapshot rather than the pristine one, so a no-color Outputter nested inside another behaves correctly. Blanking the singletons is still load-bearing rather than cosmetic, so in-block behavior is untouched: `side_by_side` pads the leading gutter by `len(Fore.RED)` to pay for the escape sequence it prefixes onto the first column, and blanking makes that compensation zero. The expanded comment records why. Also drops `self._no_color`, which was never read. Tests: - test_no_color_leak_is_repaired_by_a_later_colorized_outputter builds a no-color Outputter without `with`, then asserts a colorized one repairs the state. Red on pre-fix code and on the `__exit__`-only version; green now. - test_no_color_flag_suppresses_ansi_escape_sequences covers the CLI contract, which had none. It needs a pseudo-terminal: colorama strips escapes when stdout is not a terminal, so over a pipe colorized and no-color runs are identical. - test_no_color_report_is_aligned_the_same_as_the_colorized_one pins the gutter arithmetic above by asserting that stripping ANSI from a colorized report reproduces the no-color report exactly. Verified to fail when the compensation is removed. 48 passed, 1 deselected. pre-commit clean, mypy included.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #346 +/- ##
===========================================
+ Coverage 95.81% 96.61% +0.80%
===========================================
Files 9 9
Lines 550 562 +12
===========================================
+ Hits 527 543 +16
+ Misses 23 19 -4
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
GitHub Issue: #345
Description
Outputter.__init__blanked colorama's process-wideFore/Stylesingletonswhen
no_color=Trueand never restored them, so the change leaked:compare(..., no_color=True)thencompare(...)in the sameinterpreter produced colorless output on the second call; and
The mutation is now bounded from both ends:
__exit__restores the per-instance snapshot taken in__init__, scopingthe mutation to the
Outputter's lifetime (compare()already uses it as acontext manager). Restoring the per-instance values rather than pristine ones
keeps a no-color
Outputternested inside another behaving correctly.Outputterrestores a pristine snapshot taken atimport, before any
Outputtercan touch the singletons. This covers callersthat never use the context manager —
tests/conftest.pybuilds bareOutputters, and a library user can too — for whom__exit__never runs andthe leak was still permanent.
In-block behavior is unchanged. Blanking the constants is load-bearing rather
than cosmetic:
side_by_sidepads the leading gutter bylen(Fore.RED)to payfor the escape sequence it prefixes onto the first column, and blanking makes
that compensation zero. The expanded comment in
__init__records this, and a newtest pins it.
Also drops
self._no_color, which was added but never read.A fuller fix (routing color through the instance instead of mutating globals at
all) remains the follow-up noted in #345.
Local test steps
uv run pytest→ 48 passed, 1 deselected (baseline ondevelopis 44 passed,1 deselected).
uv run pre-commit run --all-files→ all hooks pass, mypy included.Four tests now cover
no_color, three of them added in this revision:test_no_color_state_is_restored_after_context_exit(tests/test_printing.py,from the earlier revision of this PR) — the color constants are blank inside
with Outputter(no_color=True)and restored afterward.test_no_color_leak_is_repaired_by_a_later_colorized_outputter(tests/test_printing.py,new) — builds a no-color
Outputterwithoutwith, so__exit__never runs,then asserts that constructing a colorized
Outputterrepairs the global state.test_no_color_flag_suppresses_ansi_escape_sequences(tests/test_cli.py, new) —the
--no-colorCLI contract, which had no coverage at all. This one needs apseudo-terminal: colorama strips escape sequences when stdout is not a
terminal, so over a plain pipe a colorized run and a no-color run are
byte-identical and the flag is untestable. Under a pty the default run emits
escape sequences and
--no-coloremits none. Skipped on non-POSIX, sinceptyis Unix-only.
test_no_color_report_is_aligned_the_same_as_the_colorized_one(tests/test_cli.py,new) — pins the gutter arithmetic described above by asserting that stripping ANSI
from a colorized report reproduces the
--no-colorreport exactly.Red-before-green, verified by swapping in each earlier version of
printing.pyand re-running:printing.py..._restored_after_context_exit..._repaired_by_a_later_colorized_outputterdevelop(no restore at all)__exit__-only restoretest_no_color_report_is_aligned_the_same_as_the_colorized_onewas likewiseconfirmed to fail when
extra_style_space = " " * len(default_color)is replacedwith
"", i.e. it genuinely guards the compensation rather than passing byconstruction.
Manual check of the API symptom in the issue, which no test exercises end to end:
compare(a, b, no_color=True)followed bycompare(a, b)in one interpreter nowleaves
Fore.REDat'\x1b[31m', and unrelated colorama use still colorizes.Overview of integration done
[n/a] — unit-level only; no external services are involved. The comparison
report itself is unchanged, which the golden-file tests in
tests/test_complete_file_output.pyconfirm (txt/csv/xlsx all still matchtests/data/a-b_test_golden_file.*), as does the unchangedEXPECTED_ATL06_DIFFERENCES = 4958.Worth flagging for the reviewer: until this branch was brought up to date, it had
never been tested by CI at all. The pull-request workflow filters on the base
branch,
developwas only recently added to that filter (96a33e9), and apull_requestrun reads its workflow definition from the merge ref — which here wasboth stale and conflicted, so no run could be scheduled. The failure mode is that
the check list stays short rather than turning red, which is easy to misread as
"tested".
Merging
developin fixed that. The suite has now run on this PR and passed onPython 3.11, 3.12, 3.13 and 3.14 (Linux), collecting 48 selected / 1 deselected,
matching the local run — and that is the first Linux run these changes have had,
which matters for anything near the HDF5 stack.
codecov/patchandcodecov/projectboth pass. Confirmed from the CI log rather than assumed: thepseudo-terminal test executes on Linux rather than silently skipping.
PR Acceptance Checklist
CHANGELOG.mdupdated — one bullet under### Fixedfor Bug: no_color=True permanently mutates colorama's global Fore/Style state #345.--no-color's documented behavior is unchanged, only its leakage.📚 Documentation preview 📚: https://ncompare--346.org.readthedocs.build/en/346/