Skip to content

Commit 8759666

Browse files
authored
gh-155596: Fix pprint expand mode ignoring width for nested values (#155926)
1 parent d1878cc commit 8759666

3 files changed

Lines changed: 64 additions & 7 deletions

File tree

Lib/pprint.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -187,14 +187,19 @@ def isreadable(self, object):
187187
return readable and not recursive
188188

189189
def _format(self, object, stream, indent, allowance, context, level):
190+
# Width of any "key: " prefix already written on the current line by
191+
# _format_child(). In expand mode `indent` is the block indent and so
192+
# does not include it, but it still consumes width here.
193+
prefix_len = self._pending_prefix_len
194+
self._pending_prefix_len = 0
190195
objid = id(object)
191196
if objid in context:
192197
stream.write(_recursion(object))
193198
self._recursive = True
194199
self._readable = False
195200
return
196201
rep = self._repr(object, context, level)
197-
max_width = self._width - indent - allowance
202+
max_width = self._width - indent - prefix_len - allowance
198203
if len(rep) > max_width:
199204
p = self._dispatch.get(type(object).__repr__, None)
200205
# Lazy import to improve module import time
@@ -232,6 +237,22 @@ def _child_indent(self, indent, prefix_len):
232237
return indent
233238
return indent + prefix_len
234239

240+
# Set by _format_child() immediately before it calls _format(), and
241+
# consumed there. Passing it out of band keeps _format()'s signature
242+
# unchanged for third-party subclasses that override it.
243+
_pending_prefix_len = 0
244+
245+
def _format_child(self, object, stream, indent, allowance, context, level,
246+
prefix_len):
247+
if self._expand:
248+
# Aligned mode folds the prefix into the indent (see
249+
# _child_indent), so only expand mode needs to report it.
250+
self._pending_prefix_len = prefix_len
251+
try:
252+
self._format(object, stream, indent, allowance, context, level)
253+
finally:
254+
self._pending_prefix_len = 0
255+
235256
def _write_indent_padding(self, write):
236257
if self._expand:
237258
if self._indent_per_level > 0:
@@ -303,13 +324,14 @@ def _pprint_ordered_dict(self, object, stream, indent, allowance, context, level
303324
return
304325
cls = object.__class__
305326
stream.write(cls.__name__ + '(')
306-
self._format(
327+
self._format_child(
307328
list(object.items()),
308329
stream,
309330
self._child_indent(indent, len(cls.__name__) + 1),
310331
allowance + 1,
311332
context,
312333
level,
334+
len(cls.__name__) + 1,
313335
)
314336
stream.write(')')
315337

@@ -498,13 +520,14 @@ def _pprint_bytearray(self, object, stream, indent, allowance, context, level):
498520

499521
def _pprint_mappingproxy(self, object, stream, indent, allowance, context, level):
500522
stream.write('mappingproxy(')
501-
self._format(
523+
self._format_child(
502524
object.copy(),
503525
stream,
504526
self._child_indent(indent, 13),
505527
allowance + 1,
506528
context,
507529
level,
530+
13,
508531
)
509532
stream.write(')')
510533

@@ -540,13 +563,14 @@ def _format_dict_items(self, items, stream, indent, allowance, context,
540563
rep = self._repr(key, context, level)
541564
write(rep)
542565
write(': ')
543-
self._format(
566+
self._format_child(
544567
ent,
545568
stream,
546569
self._child_indent(indent, len(rep) + 2),
547570
allowance if last else 1,
548571
context,
549572
level,
573+
len(rep) + 2,
550574
)
551575
if not last:
552576
write(delimnl)
@@ -566,13 +590,14 @@ def _format_namespace_items(self, items, stream, indent, allowance, context, lev
566590
# recursive dataclass repr.
567591
write("...")
568592
else:
569-
self._format(
593+
self._format_child(
570594
ent,
571595
stream,
572596
self._child_indent(indent, len(key) + 1),
573597
allowance if last else 1,
574598
context,
575599
level,
600+
len(key) + 1,
576601
)
577602
if not last:
578603
write(delimnl)

Lib/test/test_pprint.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1646,6 +1646,26 @@ def test_expand_dict(self):
16461646
'corge': 7,
16471647
}""")
16481648

1649+
def test_expand_respects_width_with_long_keys(self):
1650+
# gh-155596: in expand mode the width of the "key: " prefix was not
1651+
# counted when deciding whether a value fits on the current line, so
1652+
# values under long keys could overflow width.
1653+
obj = {'a' * 12: 1, 'b' * 20: 2, 'c' * 30: {'d' * 5: 3, 'e' * 40: 3}}
1654+
result = pprint.pformat(obj, expand=True)
1655+
self.assertEqual(result,
1656+
"""\
1657+
{
1658+
'aaaaaaaaaaaa': 1,
1659+
'bbbbbbbbbbbbbbbbbbbb': 2,
1660+
'cccccccccccccccccccccccccccccc': {
1661+
'ddddd': 3,
1662+
'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee': 3,
1663+
},
1664+
}""")
1665+
# The nested value must be broken up rather than overflowing.
1666+
self.assertTrue(all(len(line) <= 80 for line in result.splitlines()),
1667+
max(result.splitlines(), key=len))
1668+
16491669
def test_expand_ordered_dict(self):
16501670
dummy_ordered_dict = collections.OrderedDict(
16511671
[
@@ -1895,7 +1915,11 @@ def test_expand_chainmap(self):
18951915
'baz': 123,
18961916
'corge': 7,
18971917
'foo': 'bar',
1898-
'quux': ['foo', 'bar', 'baz'],
1918+
'quux': [
1919+
'foo',
1920+
'bar',
1921+
'baz',
1922+
],
18991923
'qux': {
19001924
'baz': 123,
19011925
'foo': 'bar',
@@ -1939,7 +1963,10 @@ def test_expand_deque(self):
19391963
'corge': 7,
19401964
'foo': 'bar',
19411965
'quux': ['foo', 'bar', 'baz'],
1942-
'qux': {'baz': 123, 'foo': 'bar'},
1966+
'qux': {
1967+
'baz': 123,
1968+
'foo': 'bar',
1969+
},
19431970
},
19441971
'foo',
19451972
'bar',
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fix :func:`pprint.pprint` and :func:`pprint.pformat` with ``expand=True``
2+
not honouring *width* for nested values. The width of the ``'key':``
3+
prefix was not counted when deciding whether a value fitted on the current
4+
line, so values under long keys could overflow *width* instead of being
5+
expanded.

0 commit comments

Comments
 (0)