Skip to content

Commit a7da166

Browse files
committed
Issue 3976: fix pprint for sets, frozensets, and dicts containing unorderable types.
1 parent df961cf commit a7da166

File tree

2 files changed

+49
-11
lines changed

2 files changed

+49
-11
lines changed

Lib/pprint.py

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,32 @@ def isrecursive(object):
7070
"""Determine if object requires a recursive representation."""
7171
return _safe_repr(object, {}, None, 0)[2]
7272

73+
class _safe_key:
74+
"""Helper function for key functions when sorting unorderable objects.
75+
76+
The wrapped-object will fallback to an Py2.x style comparison for
77+
unorderable types (sorting first comparing the type name and then by
78+
the obj ids). Does not work recursively, so dict.items() must have
79+
_safe_key applied to both the key and the value.
80+
81+
"""
82+
83+
__slots__ = ['obj']
84+
85+
def __init__(self, obj):
86+
self.obj = obj
87+
88+
def __lt__(self, other):
89+
rv = self.obj.__lt__(other.obj)
90+
if rv is NotImplemented:
91+
rv = (str(type(self.obj)), id(self.obj)) < \
92+
(str(type(other.obj)), id(other.obj))
93+
return rv
94+
95+
def _safe_tuple(t):
96+
"Helper function for comparing 2-tuples"
97+
return _safe_key(t[0]), _safe_key(t[1])
98+
7399
class PrettyPrinter:
74100
def __init__(self, indent=1, width=80, depth=None, stream=None):
75101
"""Handle pretty printing operations onto a stream using a set of
@@ -145,7 +171,7 @@ def _format(self, object, stream, indent, allowance, context, level):
145171
if length:
146172
context[objid] = 1
147173
indent = indent + self._indent_per_level
148-
items = sorted(object.items())
174+
items = sorted(object.items(), key=_safe_tuple)
149175
key, ent = items[0]
150176
rep = self._repr(key, context, level)
151177
write(rep)
@@ -178,14 +204,14 @@ def _format(self, object, stream, indent, allowance, context, level):
178204
return
179205
write('{')
180206
endchar = '}'
181-
object = sorted(object)
207+
object = sorted(object, key=_safe_key)
182208
elif issubclass(typ, frozenset):
183209
if not length:
184210
write('frozenset()')
185211
return
186212
write('frozenset({')
187213
endchar = '})'
188-
object = sorted(object)
214+
object = sorted(object, key=_safe_key)
189215
indent += 10
190216
else:
191217
write('(')
@@ -267,14 +293,7 @@ def _safe_repr(object, context, maxlevels, level):
267293
append = components.append
268294
level += 1
269295
saferepr = _safe_repr
270-
items = object.items()
271-
try:
272-
items = sorted(items)
273-
except TypeError:
274-
def sortkey(item):
275-
key, value = item
276-
return str(type(key)), key, value
277-
items = sorted(items, key=sortkey)
296+
items = sorted(object.items(), key=_safe_tuple)
278297
for k, v in items:
279298
krepr, kreadable, krecur = saferepr(k, context, maxlevels, level)
280299
vrepr, vreadable, vrecur = saferepr(v, context, maxlevels, level)

Lib/test/test_pprint.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import test.support
33
import unittest
44
import test.test_set
5+
import random
56

67
# list, tuple and dict subclasses that do or don't overwrite __repr__
78
class list2(list):
@@ -25,6 +26,10 @@ class dict3(dict):
2526
def __repr__(self):
2627
return dict.__repr__(self)
2728

29+
class Unorderable:
30+
def __repr__(self):
31+
return str(id(self))
32+
2833
class QueryTestCase(unittest.TestCase):
2934

3035
def setUp(self):
@@ -407,6 +412,20 @@ def test_depth(self):
407412
self.assertEqual(pprint.pformat(nested_dict, depth=1), lv1_dict)
408413
self.assertEqual(pprint.pformat(nested_list, depth=1), lv1_list)
409414

415+
def test_sort_unorderable_values(self):
416+
# Issue 3976: sorted pprints fail for unorderable values.
417+
n = 20
418+
keys = [Unorderable() for i in range(n)]
419+
random.shuffle(keys)
420+
skeys = sorted(keys, key=id)
421+
clean = lambda s: s.replace(' ', '').replace('\n','')
422+
423+
self.assertEqual(clean(pprint.pformat(set(keys))),
424+
'{' + ','.join(map(repr, skeys)) + '}')
425+
self.assertEqual(clean(pprint.pformat(frozenset(keys))),
426+
'frozenset({' + ','.join(map(repr, skeys)) + '})')
427+
self.assertEqual(clean(pprint.pformat(dict.fromkeys(keys))),
428+
'{' + ','.join('%r:None' % k for k in skeys) + '}')
410429

411430
class DottedPrettyPrinter(pprint.PrettyPrinter):
412431

0 commit comments

Comments
 (0)