Skip to content

Commit c0d009c

Browse files
masonjiansamuelcolvinclaude
authored
Restore PrettyFormat streams after reentrant calls (#169)
Co-authored-by: Samuel Colvin <s@muelcolvin.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
1 parent c349820 commit c0d009c

2 files changed

Lines changed: 56 additions & 2 deletions

File tree

‎devtools/prettier.py‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ def __init__(
6666
width: int = 120,
6767
yield_from_generators: bool = True,
6868
):
69+
self._stream = io.StringIO()
6970
self._indent_step = indent_step
7071
self._c = indent_char
7172
self._repr_strings = repr_strings
@@ -87,9 +88,15 @@ def __init__(
8788
]
8889

8990
def __call__(self, value: Any, *, indent: int = 0, indent_first: bool = False, highlight: bool = False) -> str:
91+
# save and restore the stream so `pformat` can be called from inside a `__repr__` without clobbering
92+
# the output of the outer call, see #162
93+
previous_stream = self._stream
9094
self._stream = io.StringIO()
91-
self._format(value, indent_current=indent, indent_first=indent_first)
92-
s = self._stream.getvalue()
95+
try:
96+
self._format(value, indent_current=indent, indent_first=indent_first)
97+
s = self._stream.getvalue()
98+
finally:
99+
self._stream = previous_stream
93100
pyg = get_pygments()
94101
if highlight and pyg:
95102
pygments, pyg_lexer, pyg_formatter = pyg

‎tests/test_prettier.py‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,53 @@ def test_print(capsys: pytest.CaptureFixture[str]):
3333
assert stderr == ''
3434

3535

36+
@pytest.mark.parametrize('depth', [1, 3])
37+
@pytest.mark.parametrize('container', [False, True])
38+
def test_reentrant_repr(depth: int, container: bool):
39+
formatter = PrettyFormat(simple_cutoff=0)
40+
41+
class Reentrant:
42+
def __init__(self, depth: int) -> None:
43+
self.depth = depth
44+
45+
def __repr__(self) -> str:
46+
value = Reentrant(self.depth - 1) if self.depth > 1 else 1
47+
return formatter(value)
48+
49+
value = [Reentrant(depth), 2] if container else Reentrant(depth)
50+
expected = '[\n 1,\n 2,\n]' if container else '1'
51+
assert formatter(value) == expected
52+
assert formatter(3) == '3'
53+
54+
55+
def test_reentrant_pprint(capsys: pytest.CaptureFixture[str]):
56+
class Reentrant:
57+
def __repr__(self) -> str:
58+
return pformat(1)
59+
60+
pprint(Reentrant())
61+
assert strip_ansi(capsys.readouterr().out) == '1\n'
62+
63+
64+
def test_reentrant_exception():
65+
formatter = PrettyFormat(simple_cutoff=0)
66+
67+
class Broken:
68+
def __repr__(self) -> str:
69+
raise ValueError('broken repr')
70+
71+
class Reentrant:
72+
def __repr__(self) -> str:
73+
with pytest.raises(ValueError, match='broken repr'):
74+
formatter(Broken())
75+
return '1'
76+
77+
assert formatter([Reentrant(), 2]) == '[\n 1,\n 2,\n]'
78+
with pytest.raises(ValueError, match='broken repr'):
79+
formatter(Broken())
80+
assert formatter([1, 2]) == '[\n 1,\n 2,\n]'
81+
82+
3683
def test_colours():
3784
v = pformat({1: 2, 3: 4}, highlight=True)
3885
assert v.startswith('\x1b'), repr(v)

0 commit comments

Comments
 (0)