Skip to content

Commit 5f77a1b

Browse files
Issue #19105: pprint now more efficiently uses free space at the right.
1 parent 5789750 commit 5f77a1b

3 files changed

Lines changed: 147 additions & 39 deletions

File tree

Lib/pprint.py

Lines changed: 59 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ def _format(self, object, stream, indent, allowance, context, level):
161161
return
162162
rep = self._repr(object, context, level - 1)
163163
typ = type(object)
164-
max_width = self._width - 1 - indent - allowance
164+
max_width = self._width - indent - allowance
165165
sepLines = len(rep) > max_width
166166
write = stream.write
167167

@@ -174,24 +174,14 @@ def _format(self, object, stream, indent, allowance, context, level):
174174
length = len(object)
175175
if length:
176176
context[objid] = 1
177-
indent = indent + self._indent_per_level
178177
if issubclass(typ, _OrderedDict):
179178
items = list(object.items())
180179
else:
181180
items = sorted(object.items(), key=_safe_tuple)
182-
key, ent = items[0]
183-
rep = self._repr(key, context, level)
184-
write(rep)
185-
write(': ')
186-
self._format(ent, stream, indent + len(rep) + 2,
187-
allowance + 1, context, level)
188-
if length > 1:
189-
for key, ent in items[1:]:
190-
rep = self._repr(key, context, level)
191-
write(',\n%s%s: ' % (' '*indent, rep))
192-
self._format(ent, stream, indent + len(rep) + 2,
193-
allowance + 1, context, level)
194-
indent = indent - self._indent_per_level
181+
self._format_dict_items(items, stream,
182+
indent + self._indent_per_level,
183+
allowance + 1,
184+
context, level)
195185
del context[objid]
196186
write('}')
197187
return
@@ -207,7 +197,10 @@ def _format(self, object, stream, indent, allowance, context, level):
207197
endchar = ']'
208198
elif issubclass(typ, tuple):
209199
write('(')
210-
endchar = ')'
200+
if length == 1:
201+
endchar = ',)'
202+
else:
203+
endchar = ')'
211204
else:
212205
if not length:
213206
write(rep)
@@ -227,10 +220,9 @@ def _format(self, object, stream, indent, allowance, context, level):
227220
context[objid] = 1
228221
self._format_items(object, stream,
229222
indent + self._indent_per_level,
230-
allowance + 1, context, level)
223+
allowance + len(endchar),
224+
context, level)
231225
del context[objid]
232-
if issubclass(typ, tuple) and length == 1:
233-
write(',')
234226
write(endchar)
235227
return
236228

@@ -239,19 +231,27 @@ def _format(self, object, stream, indent, allowance, context, level):
239231
lines = object.splitlines(True)
240232
if level == 1:
241233
indent += 1
242-
max_width -= 2
234+
allowance += 1
235+
max_width1 = max_width = self._width - indent
243236
for i, line in enumerate(lines):
244237
rep = repr(line)
245-
if len(rep) <= max_width:
238+
if i == len(lines) - 1:
239+
max_width1 -= allowance
240+
if len(rep) <= max_width1:
246241
chunks.append(rep)
247242
else:
248243
# A list of alternating (non-space, space) strings
249-
parts = re.split(r'(\s+)', line) + ['']
244+
parts = re.findall(r'\S*\s*', line)
245+
assert parts
246+
assert not parts[-1]
247+
parts.pop() # drop empty last part
248+
max_width2 = max_width
250249
current = ''
251-
for i in range(0, len(parts), 2):
252-
part = parts[i] + parts[i+1]
250+
for j, part in enumerate(parts):
253251
candidate = current + part
254-
if len(repr(candidate)) > max_width:
252+
if j == len(parts) - 1 and i == len(lines) - 1:
253+
max_width2 -= allowance
254+
if len(repr(candidate)) > max_width2:
255255
if current:
256256
chunks.append(repr(current))
257257
current = part
@@ -273,12 +273,41 @@ def _format(self, object, stream, indent, allowance, context, level):
273273
return
274274
write(rep)
275275

276+
def _format_dict_items(self, items, stream, indent, allowance, context,
277+
level):
278+
write = stream.write
279+
delimnl = ',\n' + ' ' * indent
280+
last_index = len(items) - 1
281+
for i, (key, ent) in enumerate(items):
282+
last = i == last_index
283+
rep = self._repr(key, context, level)
284+
write(rep)
285+
write(': ')
286+
self._format(ent, stream, indent + len(rep) + 2,
287+
allowance if last else 1,
288+
context, level)
289+
if not last:
290+
write(delimnl)
291+
276292
def _format_items(self, items, stream, indent, allowance, context, level):
277293
write = stream.write
278294
delimnl = ',\n' + ' ' * indent
279295
delim = ''
280-
width = max_width = self._width - indent - allowance + 2
281-
for ent in items:
296+
width = max_width = self._width - indent + 1
297+
it = iter(items)
298+
try:
299+
next_ent = next(it)
300+
except StopIteration:
301+
return
302+
last = False
303+
while not last:
304+
ent = next_ent
305+
try:
306+
next_ent = next(it)
307+
except StopIteration:
308+
last = True
309+
max_width -= allowance
310+
width -= allowance
282311
if self._compact:
283312
rep = self._repr(ent, context, level)
284313
w = len(rep) + 2
@@ -294,7 +323,9 @@ def _format_items(self, items, stream, indent, allowance, context, level):
294323
continue
295324
write(delim)
296325
delim = delimnl
297-
self._format(ent, stream, indent, allowance, context, level)
326+
self._format(ent, stream, indent,
327+
allowance if last else 1,
328+
context, level)
298329

299330
def _repr(self, object, context, level):
300331
repr, readable, recursive = self.format(object, context.copy(),

Lib/test/test_pprint.py

Lines changed: 86 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -191,11 +191,53 @@ def test_nested_indentations(self):
191191
o2 = dict(first=1, second=2, third=3)
192192
o = [o1, o2]
193193
expected = """\
194+
[ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
195+
{'first': 1, 'second': 2, 'third': 3}]"""
196+
self.assertEqual(pprint.pformat(o, indent=4, width=42), expected)
197+
expected = """\
194198
[ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
195199
{ 'first': 1,
196200
'second': 2,
197201
'third': 3}]"""
198-
self.assertEqual(pprint.pformat(o, indent=4, width=42), expected)
202+
self.assertEqual(pprint.pformat(o, indent=4, width=41), expected)
203+
204+
def test_width(self):
205+
expected = """\
206+
[[[[[[1, 2, 3],
207+
'1 2']]]],
208+
{1: [1, 2, 3],
209+
2: [12, 34]},
210+
'abc def ghi',
211+
('ab cd ef',),
212+
set2({1, 23}),
213+
[[[[[1, 2, 3],
214+
'1 2']]]]]"""
215+
o = eval(expected)
216+
self.assertEqual(pprint.pformat(o, width=15), expected)
217+
self.assertEqual(pprint.pformat(o, width=16), expected)
218+
self.assertEqual(pprint.pformat(o, width=25), expected)
219+
self.assertEqual(pprint.pformat(o, width=14), """\
220+
[[[[[[1,
221+
2,
222+
3],
223+
'1 '
224+
'2']]]],
225+
{1: [1,
226+
2,
227+
3],
228+
2: [12,
229+
34]},
230+
'abc def '
231+
'ghi',
232+
('ab cd '
233+
'ef',),
234+
set2({1,
235+
23}),
236+
[[[[[1,
237+
2,
238+
3],
239+
'1 '
240+
'2']]]]]""")
199241

200242
def test_sorted_dict(self):
201243
# Starting in Python 2.5, pprint sorts dict displays by key regardless
@@ -535,13 +577,12 @@ def test_sort_unorderable_values(self):
535577
def test_str_wrap(self):
536578
# pprint tries to wrap strings intelligently
537579
fox = 'the quick brown fox jumped over a lazy dog'
538-
self.assertEqual(pprint.pformat(fox, width=20), """\
539-
('the quick '
540-
'brown fox '
541-
'jumped over a '
542-
'lazy dog')""")
580+
self.assertEqual(pprint.pformat(fox, width=19), """\
581+
('the quick brown '
582+
'fox jumped over '
583+
'a lazy dog')""")
543584
self.assertEqual(pprint.pformat({'a': 1, 'b': fox, 'c': 2},
544-
width=26), """\
585+
width=25), """\
545586
{'a': 1,
546587
'b': 'the quick brown '
547588
'fox jumped over '
@@ -553,12 +594,34 @@ def test_str_wrap(self):
553594
# - non-ASCII is allowed
554595
# - an apostrophe doesn't disrupt the pprint
555596
special = "Portons dix bons \"whiskys\"\nà l'avocat goujat\t qui fumait au zoo"
556-
self.assertEqual(pprint.pformat(special, width=21), """\
557-
('Portons dix '
558-
'bons "whiskys"\\n'
597+
self.assertEqual(pprint.pformat(special, width=68), repr(special))
598+
self.assertEqual(pprint.pformat(special, width=31), """\
599+
('Portons dix bons "whiskys"\\n'
600+
"à l'avocat goujat\\t qui "
601+
'fumait au zoo')""")
602+
self.assertEqual(pprint.pformat(special, width=20), """\
603+
('Portons dix bons '
604+
'"whiskys"\\n'
559605
"à l'avocat "
560606
'goujat\\t qui '
561607
'fumait au zoo')""")
608+
self.assertEqual(pprint.pformat([[[[[special]]]]], width=35), """\
609+
[[[[['Portons dix bons "whiskys"\\n'
610+
"à l'avocat goujat\\t qui "
611+
'fumait au zoo']]]]]""")
612+
self.assertEqual(pprint.pformat([[[[[special]]]]], width=25), """\
613+
[[[[['Portons dix bons '
614+
'"whiskys"\\n'
615+
"à l'avocat "
616+
'goujat\\t qui '
617+
'fumait au zoo']]]]]""")
618+
self.assertEqual(pprint.pformat([[[[[special]]]]], width=23), """\
619+
[[[[['Portons dix '
620+
'bons "whiskys"\\n'
621+
"à l'avocat "
622+
'goujat\\t qui '
623+
'fumait au '
624+
'zoo']]]]]""")
562625
# An unwrappable string is formatted as its repr
563626
unwrappable = "x" * 100
564627
self.assertEqual(pprint.pformat(unwrappable, width=80), repr(unwrappable))
@@ -581,7 +644,19 @@ def test_compact(self):
581644
14, 15],
582645
[], [0], [0, 1], [0, 1, 2], [0, 1, 2, 3],
583646
[0, 1, 2, 3, 4]]"""
584-
self.assertEqual(pprint.pformat(o, width=48, compact=True), expected)
647+
self.assertEqual(pprint.pformat(o, width=47, compact=True), expected)
648+
649+
def test_compact_width(self):
650+
levels = 20
651+
number = 10
652+
o = [0] * number
653+
for i in range(levels - 1):
654+
o = [o]
655+
for w in range(levels * 2 + 1, levels + 3 * number - 1):
656+
lines = pprint.pformat(o, width=w, compact=True).splitlines()
657+
maxwidth = max(map(len, lines))
658+
self.assertLessEqual(maxwidth, w)
659+
self.assertGreater(maxwidth, w - 3)
585660

586661

587662
class DottedPrettyPrinter(pprint.PrettyPrinter):

Misc/NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ Core and Builtins
1313
Library
1414
-------
1515

16+
- Issue #19105: pprint now more efficiently uses free space at the right.
17+
1618
- Issue #14910: Add allow_abbrev parameter to argparse.ArgumentParser. Patch by
1719
Jonathan Paugh, Steven Bethard, paul j3 and Daniel Eriksson.
1820

0 commit comments

Comments
 (0)