Skip to content

Commit 26acdcd

Browse files
ShlomoSteptclaude
andcommitted
Refactor _github_repo to thread-safe contextvars
The module-level _github_repo variable posed a thread-safety risk when processing multiple sessions concurrently. This refactors to use Python's contextvars module which provides thread-local storage. Changes: - Add contextvars import - Create _github_repo_var ContextVar with None default - Add get_github_repo() accessor function (thread-safe) - Add set_github_repo() setter function (thread-safe) - Keep _github_repo module variable for backward compatibility - Update all internal usages to use new functions - Update test to use new API The backward-compatible design ensures existing code that reads _github_repo directly continues to work. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 9a877f2 commit 26acdcd

File tree

2 files changed

+56
-14
lines changed

2 files changed

+56
-14
lines changed

src/claude_code_transcripts/__init__.py

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Convert Claude Code session JSON to a clean mobile-friendly HTML page with pagination."""
22

3+
import contextvars
34
import json
45
import html
56
import os
@@ -261,9 +262,49 @@ def extract_text_from_content(content):
261262
return ""
262263

263264

264-
# Module-level variable for GitHub repo (set by generate_html)
265+
# Thread-safe context variable for GitHub repo (set by generate_html)
266+
# Using contextvars ensures thread-safety when processing multiple sessions concurrently
267+
_github_repo_var: contextvars.ContextVar[str | None] = contextvars.ContextVar(
268+
"_github_repo", default=None
269+
)
270+
271+
# Backward compatibility: module-level variable that tests may still access
272+
# This is deprecated - use get_github_repo() and set_github_repo() instead
265273
_github_repo = None
266274

275+
276+
def get_github_repo() -> str | None:
277+
"""Get the current GitHub repo from the thread-local context.
278+
279+
This is the thread-safe way to access the GitHub repo setting.
280+
Falls back to the module-level _github_repo for backward compatibility.
281+
282+
Returns:
283+
The GitHub repository in 'owner/repo' format, or None if not set.
284+
"""
285+
ctx_value = _github_repo_var.get()
286+
if ctx_value is not None:
287+
return ctx_value
288+
# Fallback for backward compatibility
289+
return _github_repo
290+
291+
292+
def set_github_repo(repo: str | None) -> contextvars.Token[str | None]:
293+
"""Set the GitHub repo in the thread-local context.
294+
295+
This is the thread-safe way to set the GitHub repo. Also updates
296+
the module-level _github_repo for backward compatibility.
297+
298+
Args:
299+
repo: The GitHub repository in 'owner/repo' format, or None.
300+
301+
Returns:
302+
A token that can be used to reset the value later.
303+
"""
304+
global _github_repo
305+
_github_repo = repo
306+
return _github_repo_var.set(repo)
307+
267308
# API constants
268309
API_BASE_URL = "https://api.anthropic.com/v1"
269310
ANTHROPIC_VERSION = "2023-06-01"
@@ -1016,7 +1057,9 @@ def render_content_block(block):
10161057
commit_hash = match.group(1)
10171058
commit_msg = match.group(2)
10181059
parts.append(
1019-
_macros.commit_card(commit_hash, commit_msg, _github_repo)
1060+
_macros.commit_card(
1061+
commit_hash, commit_msg, get_github_repo()
1062+
)
10201063
)
10211064
last_end = match.end()
10221065

@@ -2035,9 +2078,8 @@ def generate_html(json_path, output_dir, github_repo=None):
20352078
"Warning: Could not auto-detect GitHub repo. Commit links will be disabled."
20362079
)
20372080

2038-
# Set module-level variable for render functions
2039-
global _github_repo
2040-
_github_repo = github_repo
2081+
# Set thread-safe context variable for render functions
2082+
set_github_repo(github_repo)
20412083

20422084
conversations = []
20432085
current_conv = None
@@ -2191,7 +2233,7 @@ def generate_html(json_path, output_dir, github_repo=None):
21912233
# Add commits as separate timeline items
21922234
for commit_ts, commit_hash, commit_msg, page_num, conv_idx in all_commits:
21932235
item_html = _macros.index_commit(
2194-
commit_hash, commit_msg, commit_ts, _github_repo
2236+
commit_hash, commit_msg, commit_ts, get_github_repo()
21952237
)
21962238
timeline_items.append((commit_ts, "commit", item_html))
21972239

@@ -2533,9 +2575,8 @@ def generate_html_from_session_data(session_data, output_dir, github_repo=None):
25332575
if github_repo:
25342576
click.echo(f"Auto-detected GitHub repo: {github_repo}")
25352577

2536-
# Set module-level variable for render functions
2537-
global _github_repo
2538-
_github_repo = github_repo
2578+
# Set thread-safe context variable for render functions
2579+
set_github_repo(github_repo)
25392580

25402581
conversations = []
25412582
current_conv = None
@@ -2689,7 +2730,7 @@ def generate_html_from_session_data(session_data, output_dir, github_repo=None):
26892730
# Add commits as separate timeline items
26902731
for commit_ts, commit_hash, commit_msg, page_num, conv_idx in all_commits:
26912732
item_html = _macros.index_commit(
2692-
commit_hash, commit_msg, commit_ts, _github_repo
2733+
commit_hash, commit_msg, commit_ts, get_github_repo()
26932734
)
26942735
timeline_items.append((commit_ts, "commit", item_html))
26952736

tests/test_generate_html.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -526,11 +526,12 @@ def test_tool_result_with_ansi_codes(self):
526526

527527
def test_tool_result_with_commit(self, snapshot_html):
528528
"""Test tool result with git commit output."""
529-
# Need to set the global _github_repo for commit link rendering
529+
# Need to set the github repo for commit link rendering
530+
# Using the thread-safe set_github_repo function
530531
import claude_code_transcripts
531532

532-
old_repo = claude_code_transcripts._github_repo
533-
claude_code_transcripts._github_repo = "example/repo"
533+
old_repo = claude_code_transcripts.get_github_repo()
534+
claude_code_transcripts.set_github_repo("example/repo")
534535
try:
535536
block = {
536537
"type": "tool_result",
@@ -540,7 +541,7 @@ def test_tool_result_with_commit(self, snapshot_html):
540541
result = render_content_block(block)
541542
assert result == snapshot_html
542543
finally:
543-
claude_code_transcripts._github_repo = old_repo
544+
claude_code_transcripts.set_github_repo(old_repo)
544545

545546
def test_tool_result_with_ansi_codes_snapshot(self, snapshot_html):
546547
"""Test ANSI escape code stripping with snapshot comparison.

0 commit comments

Comments
 (0)