Skip to content

Commit 11159d2

Browse files
authored
bpo-43080: pprint for dataclass instances (GH-24389)
* Added pprint support for dataclass instances which don't have a custom __repr__.
1 parent 695d47b commit 11159d2

File tree

5 files changed

+133
-14
lines changed

5 files changed

+133
-14
lines changed

Doc/library/pprint.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ Dictionaries are sorted by key before the display is computed.
2828
.. versionchanged:: 3.9
2929
Added support for pretty-printing :class:`types.SimpleNamespace`.
3030

31+
.. versionchanged:: 3.10
32+
Added support for pretty-printing :class:`dataclasses.dataclass`.
33+
3134
The :mod:`pprint` module defines one class:
3235

3336
.. First the implementation class:

Doc/whatsnew/3.10.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -820,6 +820,12 @@ identification from `freedesktop.org os-release
820820
<https://www.freedesktop.org/software/systemd/man/os-release.html>`_ standard file.
821821
(Contributed by Christian Heimes in :issue:`28468`)
822822
823+
pprint
824+
------
825+
826+
:mod:`pprint` can now pretty-print :class:`dataclasses.dataclass` instances.
827+
(Contributed by Lewis Gaul in :issue:`43080`.)
828+
823829
py_compile
824830
----------
825831

Lib/pprint.py

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"""
3636

3737
import collections as _collections
38+
import dataclasses as _dataclasses
3839
import re
3940
import sys as _sys
4041
import types as _types
@@ -178,8 +179,26 @@ def _format(self, object, stream, indent, allowance, context, level):
178179
p(self, object, stream, indent, allowance, context, level + 1)
179180
del context[objid]
180181
return
182+
elif (_dataclasses.is_dataclass(object) and
183+
not isinstance(object, type) and
184+
object.__dataclass_params__.repr and
185+
# Check dataclass has generated repr method.
186+
hasattr(object.__repr__, "__wrapped__") and
187+
"__create_fn__" in object.__repr__.__wrapped__.__qualname__):
188+
context[objid] = 1
189+
self._pprint_dataclass(object, stream, indent, allowance, context, level + 1)
190+
del context[objid]
191+
return
181192
stream.write(rep)
182193

194+
def _pprint_dataclass(self, object, stream, indent, allowance, context, level):
195+
cls_name = object.__class__.__name__
196+
indent += len(cls_name) + 1
197+
items = [(f.name, getattr(object, f.name)) for f in _dataclasses.fields(object) if f.repr]
198+
stream.write(cls_name + '(')
199+
self._format_namespace_items(items, stream, indent, allowance, context, level)
200+
stream.write(')')
201+
183202
_dispatch = {}
184203

185204
def _pprint_dict(self, object, stream, indent, allowance, context, level):
@@ -346,21 +365,9 @@ def _pprint_simplenamespace(self, object, stream, indent, allowance, context, le
346365
else:
347366
cls_name = object.__class__.__name__
348367
indent += len(cls_name) + 1
349-
delimnl = ',\n' + ' ' * indent
350368
items = object.__dict__.items()
351-
last_index = len(items) - 1
352-
353369
stream.write(cls_name + '(')
354-
for i, (key, ent) in enumerate(items):
355-
stream.write(key)
356-
stream.write('=')
357-
358-
last = i == last_index
359-
self._format(ent, stream, indent + len(key) + 1,
360-
allowance if last else 1,
361-
context, level)
362-
if not last:
363-
stream.write(delimnl)
370+
self._format_namespace_items(items, stream, indent, allowance, context, level)
364371
stream.write(')')
365372

366373
_dispatch[_types.SimpleNamespace.__repr__] = _pprint_simplenamespace
@@ -382,6 +389,25 @@ def _format_dict_items(self, items, stream, indent, allowance, context,
382389
if not last:
383390
write(delimnl)
384391

392+
def _format_namespace_items(self, items, stream, indent, allowance, context, level):
393+
write = stream.write
394+
delimnl = ',\n' + ' ' * indent
395+
last_index = len(items) - 1
396+
for i, (key, ent) in enumerate(items):
397+
last = i == last_index
398+
write(key)
399+
write('=')
400+
if id(ent) in context:
401+
# Special-case representation of recursion to match standard
402+
# recursive dataclass repr.
403+
write("...")
404+
else:
405+
self._format(ent, stream, indent + len(key) + 1,
406+
allowance if last else 1,
407+
context, level)
408+
if not last:
409+
write(delimnl)
410+
385411
def _format_items(self, items, stream, indent, allowance, context, level):
386412
write = stream.write
387413
indent += self._indent_per_level

Lib/test/test_pprint.py

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# -*- coding: utf-8 -*-
22

33
import collections
4+
import dataclasses
45
import io
56
import itertools
67
import pprint
@@ -66,6 +67,38 @@ class dict_custom_repr(dict):
6667
def __repr__(self):
6768
return '*'*len(dict.__repr__(self))
6869

70+
@dataclasses.dataclass
71+
class dataclass1:
72+
field1: str
73+
field2: int
74+
field3: bool = False
75+
field4: int = dataclasses.field(default=1, repr=False)
76+
77+
@dataclasses.dataclass
78+
class dataclass2:
79+
a: int = 1
80+
def __repr__(self):
81+
return "custom repr that doesn't fit within pprint width"
82+
83+
@dataclasses.dataclass(repr=False)
84+
class dataclass3:
85+
a: int = 1
86+
87+
@dataclasses.dataclass
88+
class dataclass4:
89+
a: "dataclass4"
90+
b: int = 1
91+
92+
@dataclasses.dataclass
93+
class dataclass5:
94+
a: "dataclass6"
95+
b: int = 1
96+
97+
@dataclasses.dataclass
98+
class dataclass6:
99+
c: "dataclass5"
100+
d: int = 1
101+
69102
class Unorderable:
70103
def __repr__(self):
71104
return str(id(self))
@@ -428,7 +461,7 @@ def test_simple_namespace(self):
428461
lazy=7,
429462
dog=8,
430463
)
431-
formatted = pprint.pformat(ns, width=60)
464+
formatted = pprint.pformat(ns, width=60, indent=4)
432465
self.assertEqual(formatted, """\
433466
namespace(the=0,
434467
quick=1,
@@ -465,6 +498,56 @@ class AdvancedNamespace(types.SimpleNamespace): pass
465498
lazy=7,
466499
dog=8)""")
467500

501+
def test_empty_dataclass(self):
502+
dc = dataclasses.make_dataclass("MyDataclass", ())()
503+
formatted = pprint.pformat(dc)
504+
self.assertEqual(formatted, "MyDataclass()")
505+
506+
def test_small_dataclass(self):
507+
dc = dataclass1("text", 123)
508+
formatted = pprint.pformat(dc)
509+
self.assertEqual(formatted, "dataclass1(field1='text', field2=123, field3=False)")
510+
511+
def test_larger_dataclass(self):
512+
dc = dataclass1("some fairly long text", int(1e10), True)
513+
formatted = pprint.pformat([dc, dc], width=60, indent=4)
514+
self.assertEqual(formatted, """\
515+
[ dataclass1(field1='some fairly long text',
516+
field2=10000000000,
517+
field3=True),
518+
dataclass1(field1='some fairly long text',
519+
field2=10000000000,
520+
field3=True)]""")
521+
522+
def test_dataclass_with_repr(self):
523+
dc = dataclass2()
524+
formatted = pprint.pformat(dc, width=20)
525+
self.assertEqual(formatted, "custom repr that doesn't fit within pprint width")
526+
527+
def test_dataclass_no_repr(self):
528+
dc = dataclass3()
529+
formatted = pprint.pformat(dc, width=10)
530+
self.assertRegex(formatted, r"<test.test_pprint.dataclass3 object at \w+>")
531+
532+
def test_recursive_dataclass(self):
533+
dc = dataclass4(None)
534+
dc.a = dc
535+
formatted = pprint.pformat(dc, width=10)
536+
self.assertEqual(formatted, """\
537+
dataclass4(a=...,
538+
b=1)""")
539+
540+
def test_cyclic_dataclass(self):
541+
dc5 = dataclass5(None)
542+
dc6 = dataclass6(None)
543+
dc5.a = dc6
544+
dc6.c = dc5
545+
formatted = pprint.pformat(dc5, width=10)
546+
self.assertEqual(formatted, """\
547+
dataclass5(a=dataclass6(c=...,
548+
d=1),
549+
b=1)""")
550+
468551
def test_subclassing(self):
469552
# length(repr(obj)) > width
470553
o = {'names with spaces': 'should be presented using repr()',
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:mod:`pprint` now has support for :class:`dataclasses.dataclass`. Patch by Lewis Gaul.

0 commit comments

Comments
 (0)