Skip to content

Isolate concurrent pretty-format streams - #180

Open
rupayon123 wants to merge 1 commit into
samuelcolvin:mainfrom
rupayon123:fix/isolate-concurrent-format-streams-20261029
Open

rupayon123 wants to merge 1 commit into
samuelcolvin:mainfrom
rupayon123:fix/isolate-concurrent-format-streams-20261029

Conversation

@rupayon123

@rupayon123 rupayon123 commented Oct 29, 2026 •

Copy link
Copy Markdown

Two overlapping calls to global pformat or a shared PrettyFormat can mix or truncate each other's output. An event-coordinated example produces Second[1] and 2] instead of First[1] and Second[2]. The existing save/restore logic handles reentrancy but still shares the active stream between threads.

Store each call's active stream in a ContextVar and reset its token in finally. Keep the idle instance stream so existing copy and pickle behavior is preserved. No dependency or lock is added.

Validation on macOS arm64:

  • Both deterministic thread regressions fail before the change and pass afterward.
  • Complete suites pass with extras (155 passed, four existing skips) and without extras (148 passed, 11 existing skips).
  • Ruff, strict basedpyright, docs and package builds pass.
  • Independent public API checks exercise 400 concurrent calls each on source, the installed wheel and CPython 3.14.6 with the GIL disabled, with no mismatches. Nested calls, exception recovery, pickle protocols 2–5 and deepcopy also pass.

Other supported CI platforms remain unverified locally. Existing IPython/timer skips and pickle protocol 0/1 errors are unchanged. This does not claim a complete free-threaded optional-extension suite.


Summary by cubic

Fixes concurrent pformat or PrettyFormat calls mixing or truncating output by giving each call its own stream via a ContextVar, instead of sharing the instance stream between threads.

  • Stores each call's active stream in _current_stream and resets its token in finally, keeping the idle instance stream for copy and pickle behavior.
  • Adds tests covering concurrent threads, nested and recursive formatter calls, and exception recovery.

Written for commit 13bd23d. Summary will update on new commits.

View guided diff

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 2 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="tests/test_prettier.py">

<violation number="1" location="tests/test_prettier.py:95">
P2: These workers are non-daemon, so a formatter deadlock leaves them alive after the 10-second `join()` and blocks interpreter shutdown. Create them as daemon threads so this regression test fails instead of hanging CI.</violation>
</file>

Reply to a comment to ask cubic a question or push back. It learns from your replies.

View guided diff | Re-trigger cubic

Comment thread tests/test_prettier.py
assert first_entered.wait(5)
results['second'] = formatter(Second())

threads = [Thread(target=format_first), Thread(target=format_second)]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: These workers are non-daemon, so a formatter deadlock leaves them alive after the 10-second join() and blocks interpreter shutdown. Create them as daemon threads so this regression test fails instead of hanging CI.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At tests/test_prettier.py, line 95:

<comment>These workers are non-daemon, so a formatter deadlock leaves them alive after the 10-second `join()` and blocks interpreter shutdown. Create them as daemon threads so this regression test fails instead of hanging CI.</comment>

<file context>
@@ -61,6 +62,86 @@ def __repr__(self) -> str:
+        assert first_entered.wait(5)
+        results['second'] = formatter(Second())
+
+    threads = [Thread(target=format_first), Thread(target=format_second)]
+    for thread in threads:
+        thread.start()
</file context>
Suggested change
threads = [Thread(target=format_first), Thread(target=format_second)]
threads = [Thread(target=format_first, daemon=True), Thread(target=format_second, daemon=True)]

This branch has not been deployed

No deployments
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.

1 participant