fix: avoid os.environ mutation for color control - #1125
Merged
noahnu merged 1 commit intoJun 26, 2026
Conversation
syrupy signaled "disable color" by temporarily mutating os.environ via env_context. Mutating the environment calls glibc setenv, which is not thread safe and races with getenv called from native code in other threads, and could segfault unrelated code (for example a database driver in a pytest-django live_server request handler) during the pytest_assertrepr_compare and diff_snapshots paths. Signal color suppression through a contextvars.ContextVar instead. terminal._is_color_disabled() now consults that ContextVar in addition to the external NO_COLOR and ANSI_COLORS_DISABLED variables, which are still read (reads are safe, only writes race). Both call sites use the new disable_color() context manager, so no os.environ writes happen. The now-unused env_context helper is removed from syrupy.utils; its only remaining user was the osenv test fixture, which now defines it locally. Closes syrupy-project#955
Codecov Report✅ All modified and coverable lines are covered by tests. @@ Coverage Diff @@
## main #1125 +/- ##
=======================================
Coverage 97.43% 97.43%
=======================================
Files 24 24
Lines 1754 1754
=======================================
Hits 1709 1709
Misses 45 45 🚀 New features to boost your workflow:
|
noahnu
approved these changes
Jun 26, 2026
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.
Description
syrupy signaled "disable color" by temporarily mutating
os.environ(through theenv_contexthelper). Mutating the environment calls glibcsetenv, which is not thread safe and races withgetenvcalled from native code in other threads. During thepytest_assertrepr_compareanddiff_snapshotspaths this could segfault unrelated code, for example a database driver running in apytest-djangolive_serverrequest handler, as reported in #955.This replaces the environment mutation with a
contextvars.ContextVar.terminal._is_color_disabled()now consults that ContextVar in addition to the externalNO_COLORandANSI_COLORS_DISABLEDvariables, which are still read (reads are safe, only writes race). Both call sites,__terminal_color(when--snapshot-no-colorsis set) andSnapshotReporter.diff_snapshots, use the newdisable_color()context manager, so noos.environwrites happen anywhere in the path.The now-unused
env_contexthelper is removed fromsyrupy.utils. It was undocumented and not part of__all__, and was the source of the race. Its only remaining user was theosenvtest fixture, which now defines the helper locally inconftest.py(setting a real environment variable inside a single threaded test is fine).#956 narrowed when the mutation happened; this removes it entirely.
Related Issues
os.environfrom the assertion hook while other threads read the environment.Checklist
Additional Comments
New tests in
tests/syrupy/test_terminal.pycover the toggle behavior, that the externalNO_COLORvariable still disables color, and crucially thatdisable_color()does not touchos.environ. Behavior for users is unchanged:--snapshot-no-colors, externalNO_COLOR/ANSI_COLORS_DISABLED, and the diff color suppression all work as before.