From 262ac218efb918cc1221501868c9940eb849345d Mon Sep 17 00:00:00 2001 From: Ansh Bajpai Date: Thu, 9 Jul 2026 09:51:27 +0530 Subject: [PATCH] Fix UnicodeEncodeError on cp1252 terminals (piped Windows output) The recursion marker in the call tree and the arrow in comparison output cannot be encoded by cp1252, the default encoding for piped output on many Windows setups. The run then crashes with UnicodeEncodeError after tracing already succeeded. Fix: probe sys.stdout.encoding once and fall back to ASCII equivalents (@ and ->) when the preferred char cannot be encoded. Co-Authored-By: Claude Fable 5 --- oracletrace/compare.py | 6 ++-- oracletrace/tracer.py | 17 ++++++++++- tests/test_encoding.py | 66 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 tests/test_encoding.py diff --git a/oracletrace/compare.py b/oracletrace/compare.py index 1656235..15eeb2f 100644 --- a/oracletrace/compare.py +++ b/oracletrace/compare.py @@ -1,8 +1,10 @@ -from .tracer import TracerData, FunctionData +from .tracer import TracerData, FunctionData, _safe_char from rich import print from typing import Any, Dict, List, Set, Optional from dataclasses import dataclass +_ARROW = _safe_char("→", "->") + @dataclass class RegressionData: name: str @@ -59,7 +61,7 @@ def compare_traces( print( f"{name}\n" - f" total_time: {old_time:.4f}s → {new_time:.4f}s " + f" total_time: {old_time:.4f}s {_ARROW} {new_time:.4f}s " f"[{color}]({percent:+.2f}%)[/]\n" ) if not show_only_regressions or percent > threshold else ... diff --git a/oracletrace/tracer.py b/oracletrace/tracer.py index 7297725..cfff533 100644 --- a/oracletrace/tracer.py +++ b/oracletrace/tracer.py @@ -18,6 +18,21 @@ ProfileEvent: TypeAlias = Literal["call", "return", "c_call", "c_return", "c_exception"] ProfileFunction: TypeAlias = Callable[[FrameType, ProfileEvent, Any], object] + +def _safe_char(preferred: str, fallback: str) -> str: + # Use the unicode char only when stdout can actually encode it — + # piped output on Windows is often cp1252, where rich raises + # UnicodeEncodeError and the whole run crashes after tracing finished + enc = getattr(sys.stdout, "encoding", None) or "utf-8" + try: + preferred.encode(enc) + return preferred + except (UnicodeEncodeError, LookupError): + return fallback + + +_CYCLE = _safe_char("↻", "@") + @dataclass class TracerMetadata: root_path: str @@ -271,7 +286,7 @@ def add_nodes(parent_node: Tree, parent_key: str, current_path: set[str]) -> Non _total_time = self._func_time[child_key] # Detect recursion to prevent infinite loops in the tree if child_key in current_path: - parent_node.add(f"[red]↻ {child_key}[/] ({count}x)") + parent_node.add(f"[red]{_CYCLE} {child_key}[/] ({count}x)") continue node_text = f"{child_key} [dim]({count}x, {_total_time:.4f}s)[/]" diff --git a/tests/test_encoding.py b/tests/test_encoding.py new file mode 100644 index 0000000..dcef7f3 --- /dev/null +++ b/tests/test_encoding.py @@ -0,0 +1,66 @@ +import os +import subprocess +import sys +import tempfile +from pathlib import Path + +from oracletrace.tracer import _safe_char + +REPO_ROOT = Path(__file__).resolve().parents[1] + +RECURSIVE_SCRIPT = """\ +def rec(n): + if n: + rec(n - 1) + +def main(): + rec(3) + +main() +""" + + +class _FakeStdout: + def __init__(self, encoding): + self.encoding = encoding + + +def test_safe_char_keeps_unicode_when_encodable(monkeypatch): + monkeypatch.setattr(sys, "stdout", _FakeStdout("utf-8")) + assert _safe_char("↻", "@") == "↻" + + +def test_safe_char_falls_back_on_cp1252(monkeypatch): + monkeypatch.setattr(sys, "stdout", _FakeStdout("cp1252")) + assert _safe_char("↻", "@") == "@" + assert _safe_char("→", "->") == "->" + + +def test_safe_char_survives_missing_encoding(monkeypatch): + monkeypatch.setattr(sys, "stdout", _FakeStdout(None)) + assert _safe_char("→", "->") == "→" # falls back to utf-8 + + +def test_piped_cp1252_output_does_not_crash(): + # Piped output on Windows often defaults to cp1252, which cannot encode + # the recursion marker in the call tree; the run then dies with + # UnicodeEncodeError AFTER tracing succeeded. Forced via PYTHONIOENCODING + # so the regression is caught on any platform. + env = { + **os.environ, + "PYTHONPATH": str(REPO_ROOT), + "PYTHONIOENCODING": "cp1252", + } + with tempfile.TemporaryDirectory(dir=REPO_ROOT) as tmp: + script = Path(tmp) / "script.py" + script.write_text(RECURSIVE_SCRIPT, encoding="utf-8") + result = subprocess.run( + [sys.executable, "-m", "oracletrace", "script.py"], + capture_output=True, + text=True, + cwd=tmp, + env=env, + ) + assert result.returncode == 0, result.stderr + assert "UnicodeEncodeError" not in result.stderr + assert "rec" in result.stdout