Skip to content

bpo-43080: pprint for dataclass instances #24389

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Apr 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Doc/library/pprint.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ Dictionaries are sorted by key before the display is computed.
.. versionchanged:: 3.9
Added support for pretty-printing :class:`types.SimpleNamespace`.

.. versionchanged:: 3.10
Added support for pretty-printing :class:`dataclasses.dataclass`.

The :mod:`pprint` module defines one class:

.. First the implementation class:
Expand Down
6 changes: 6 additions & 0 deletions Doc/whatsnew/3.10.rst
Original file line number Diff line number Diff line change
Expand Up @@ -811,6 +811,12 @@ identification from `freedesktop.org os-release
<https://www.freedesktop.org/software/systemd/man/os-release.html>`_ standard file.
(Contributed by Christian Heimes in :issue:`28468`)

pprint
------

:mod:`pprint` can now pretty-print :class:`dataclasses.dataclass` instances.
(Contributed by Lewis Gaul in :issue:`43080`.)

py_compile
----------

Expand Down
52 changes: 39 additions & 13 deletions Lib/pprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"""

import collections as _collections
import dataclasses as _dataclasses
import re
import sys as _sys
import types as _types
Expand Down Expand Up @@ -178,8 +179,26 @@ def _format(self, object, stream, indent, allowance, context, level):
p(self, object, stream, indent, allowance, context, level + 1)
del context[objid]
return
elif (_dataclasses.is_dataclass(object) and
not isinstance(object, type) and
object.__dataclass_params__.repr and
# Check dataclass has generated repr method.
hasattr(object.__repr__, "__wrapped__") and
"__create_fn__" in object.__repr__.__wrapped__.__qualname__):
context[objid] = 1
self._pprint_dataclass(object, stream, indent, allowance, context, level + 1)
del context[objid]
return
stream.write(rep)

def _pprint_dataclass(self, object, stream, indent, allowance, context, level):
cls_name = object.__class__.__name__
indent += len(cls_name) + 1
items = [(f.name, getattr(object, f.name)) for f in _dataclasses.fields(object) if f.repr]
stream.write(cls_name + '(')
self._format_namespace_items(items, stream, indent, allowance, context, level)
stream.write(')')

_dispatch = {}

def _pprint_dict(self, object, stream, indent, allowance, context, level):
Expand Down Expand Up @@ -346,21 +365,9 @@ def _pprint_simplenamespace(self, object, stream, indent, allowance, context, le
else:
cls_name = object.__class__.__name__
indent += len(cls_name) + 1
delimnl = ',\n' + ' ' * indent
items = object.__dict__.items()
last_index = len(items) - 1

stream.write(cls_name + '(')
for i, (key, ent) in enumerate(items):
stream.write(key)
stream.write('=')

last = i == last_index
self._format(ent, stream, indent + len(key) + 1,
allowance if last else 1,
context, level)
if not last:
stream.write(delimnl)
self._format_namespace_items(items, stream, indent, allowance, context, level)
stream.write(')')

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

def _format_namespace_items(self, items, stream, indent, allowance, context, level):
write = stream.write
delimnl = ',\n' + ' ' * indent
last_index = len(items) - 1
for i, (key, ent) in enumerate(items):
last = i == last_index
write(key)
write('=')
if id(ent) in context:
# Special-case representation of recursion to match standard
# recursive dataclass repr.
write("...")
else:
self._format(ent, stream, indent + len(key) + 1,
allowance if last else 1,
context, level)
if not last:
write(delimnl)

def _format_items(self, items, stream, indent, allowance, context, level):
write = stream.write
indent += self._indent_per_level
Expand Down
85 changes: 84 additions & 1 deletion Lib/test/test_pprint.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-

import collections
import dataclasses
import io
import itertools
import pprint
Expand Down Expand Up @@ -66,6 +67,38 @@ class dict_custom_repr(dict):
def __repr__(self):
return '*'*len(dict.__repr__(self))

@dataclasses.dataclass
class dataclass1:
field1: str
field2: int
field3: bool = False
field4: int = dataclasses.field(default=1, repr=False)

@dataclasses.dataclass
class dataclass2:
a: int = 1
def __repr__(self):
return "custom repr that doesn't fit within pprint width"

@dataclasses.dataclass(repr=False)
class dataclass3:
a: int = 1

@dataclasses.dataclass
class dataclass4:
a: "dataclass4"
b: int = 1

@dataclasses.dataclass
class dataclass5:
a: "dataclass6"
b: int = 1

@dataclasses.dataclass
class dataclass6:
c: "dataclass5"
d: int = 1

class Unorderable:
def __repr__(self):
return str(id(self))
Expand Down Expand Up @@ -428,7 +461,7 @@ def test_simple_namespace(self):
lazy=7,
dog=8,
)
formatted = pprint.pformat(ns, width=60)
formatted = pprint.pformat(ns, width=60, indent=4)
self.assertEqual(formatted, """\
namespace(the=0,
quick=1,
Expand Down Expand Up @@ -465,6 +498,56 @@ class AdvancedNamespace(types.SimpleNamespace): pass
lazy=7,
dog=8)""")

def test_empty_dataclass(self):
dc = dataclasses.make_dataclass("MyDataclass", ())()
formatted = pprint.pformat(dc)
self.assertEqual(formatted, "MyDataclass()")

def test_small_dataclass(self):
dc = dataclass1("text", 123)
formatted = pprint.pformat(dc)
self.assertEqual(formatted, "dataclass1(field1='text', field2=123, field3=False)")

def test_larger_dataclass(self):
dc = dataclass1("some fairly long text", int(1e10), True)
formatted = pprint.pformat([dc, dc], width=60, indent=4)
self.assertEqual(formatted, """\
[ dataclass1(field1='some fairly long text',
field2=10000000000,
field3=True),
dataclass1(field1='some fairly long text',
field2=10000000000,
field3=True)]""")

def test_dataclass_with_repr(self):
dc = dataclass2()
formatted = pprint.pformat(dc, width=20)
self.assertEqual(formatted, "custom repr that doesn't fit within pprint width")

def test_dataclass_no_repr(self):
dc = dataclass3()
formatted = pprint.pformat(dc, width=10)
self.assertRegex(formatted, r"<test.test_pprint.dataclass3 object at \w+>")

def test_recursive_dataclass(self):
dc = dataclass4(None)
dc.a = dc
formatted = pprint.pformat(dc, width=10)
self.assertEqual(formatted, """\
dataclass4(a=...,
b=1)""")

def test_cyclic_dataclass(self):
dc5 = dataclass5(None)
dc6 = dataclass6(None)
dc5.a = dc6
dc6.c = dc5
formatted = pprint.pformat(dc5, width=10)
self.assertEqual(formatted, """\
dataclass5(a=dataclass6(c=...,
d=1),
b=1)""")

def test_subclassing(self):
# length(repr(obj)) > width
o = {'names with spaces': 'should be presented using repr()',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:mod:`pprint` now has support for :class:`dataclasses.dataclass`. Patch by Lewis Gaul.