diff --git a/Lib/pprint.py b/Lib/pprint.py index 7355021998081dc..6b574218277a573 100644 --- a/Lib/pprint.py +++ b/Lib/pprint.py @@ -187,6 +187,11 @@ def isreadable(self, object): return readable and not recursive def _format(self, object, stream, indent, allowance, context, level): + # Width of any "key: " prefix already written on the current line by + # _format_child(). In expand mode `indent` is the block indent and so + # does not include it, but it still consumes width here. + prefix_len = self._pending_prefix_len + self._pending_prefix_len = 0 objid = id(object) if objid in context: stream.write(_recursion(object)) @@ -194,7 +199,7 @@ def _format(self, object, stream, indent, allowance, context, level): self._readable = False return rep = self._repr(object, context, level) - max_width = self._width - indent - allowance + max_width = self._width - indent - prefix_len - allowance if len(rep) > max_width: p = self._dispatch.get(type(object).__repr__, None) # Lazy import to improve module import time @@ -232,6 +237,22 @@ def _child_indent(self, indent, prefix_len): return indent return indent + prefix_len + # Set by _format_child() immediately before it calls _format(), and + # consumed there. Passing it out of band keeps _format()'s signature + # unchanged for third-party subclasses that override it. + _pending_prefix_len = 0 + + def _format_child(self, object, stream, indent, allowance, context, level, + prefix_len): + if self._expand: + # Aligned mode folds the prefix into the indent (see + # _child_indent), so only expand mode needs to report it. + self._pending_prefix_len = prefix_len + try: + self._format(object, stream, indent, allowance, context, level) + finally: + self._pending_prefix_len = 0 + def _write_indent_padding(self, write): if self._expand: if self._indent_per_level > 0: @@ -303,13 +324,14 @@ def _pprint_ordered_dict(self, object, stream, indent, allowance, context, level return cls = object.__class__ stream.write(cls.__name__ + '(') - self._format( + self._format_child( list(object.items()), stream, self._child_indent(indent, len(cls.__name__) + 1), allowance + 1, context, level, + len(cls.__name__) + 1, ) stream.write(')') @@ -498,13 +520,14 @@ def _pprint_bytearray(self, object, stream, indent, allowance, context, level): def _pprint_mappingproxy(self, object, stream, indent, allowance, context, level): stream.write('mappingproxy(') - self._format( + self._format_child( object.copy(), stream, self._child_indent(indent, 13), allowance + 1, context, level, + 13, ) stream.write(')') @@ -540,13 +563,14 @@ def _format_dict_items(self, items, stream, indent, allowance, context, rep = self._repr(key, context, level) write(rep) write(': ') - self._format( + self._format_child( ent, stream, self._child_indent(indent, len(rep) + 2), allowance if last else 1, context, level, + len(rep) + 2, ) if not last: write(delimnl) @@ -566,13 +590,14 @@ def _format_namespace_items(self, items, stream, indent, allowance, context, lev # recursive dataclass repr. write("...") else: - self._format( + self._format_child( ent, stream, self._child_indent(indent, len(key) + 1), allowance if last else 1, context, level, + len(key) + 1, ) if not last: write(delimnl) diff --git a/Lib/test/test_pprint.py b/Lib/test/test_pprint.py index 041c2072b9e253a..dcb1cd693074cbb 100644 --- a/Lib/test/test_pprint.py +++ b/Lib/test/test_pprint.py @@ -1646,6 +1646,26 @@ def test_expand_dict(self): 'corge': 7, }""") + def test_expand_respects_width_with_long_keys(self): + # gh-155596: in expand mode the width of the "key: " prefix was not + # counted when deciding whether a value fits on the current line, so + # values under long keys could overflow width. + obj = {'a' * 12: 1, 'b' * 20: 2, 'c' * 30: {'d' * 5: 3, 'e' * 40: 3}} + result = pprint.pformat(obj, expand=True) + self.assertEqual(result, +"""\ +{ + 'aaaaaaaaaaaa': 1, + 'bbbbbbbbbbbbbbbbbbbb': 2, + 'cccccccccccccccccccccccccccccc': { + 'ddddd': 3, + 'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee': 3, + }, +}""") + # The nested value must be broken up rather than overflowing. + self.assertTrue(all(len(line) <= 80 for line in result.splitlines()), + max(result.splitlines(), key=len)) + def test_expand_ordered_dict(self): dummy_ordered_dict = collections.OrderedDict( [ @@ -1895,7 +1915,11 @@ def test_expand_chainmap(self): 'baz': 123, 'corge': 7, 'foo': 'bar', - 'quux': ['foo', 'bar', 'baz'], + 'quux': [ + 'foo', + 'bar', + 'baz', + ], 'qux': { 'baz': 123, 'foo': 'bar', @@ -1939,7 +1963,10 @@ def test_expand_deque(self): 'corge': 7, 'foo': 'bar', 'quux': ['foo', 'bar', 'baz'], - 'qux': {'baz': 123, 'foo': 'bar'}, + 'qux': { + 'baz': 123, + 'foo': 'bar', + }, }, 'foo', 'bar', diff --git a/Misc/NEWS.d/next/Library/2026-08-17-12-07-38.gh-issue-155596.Rt4mZq.rst b/Misc/NEWS.d/next/Library/2026-08-17-12-07-38.gh-issue-155596.Rt4mZq.rst new file mode 100644 index 000000000000000..cbd8bc838685139 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-17-12-07-38.gh-issue-155596.Rt4mZq.rst @@ -0,0 +1,5 @@ +Fix :func:`pprint.pprint` and :func:`pprint.pformat` with ``expand=True`` +not honouring *width* for nested values. The width of the ``'key':`` +prefix was not counted when deciding whether a value fitted on the current +line, so values under long keys could overflow *width* instead of being +expanded.