[Python-checkins] bpo-43080: pprint for dataclass instances (GH-24389)
ericvsmith
webhook-mailer at python.org
Tue Apr 13 19:59:33 EDT 2021
https://github.com/python/cpython/commit/11159d2c9d6616497ef4cc62953a5c3cc8454afb
commit: 11159d2c9d6616497ef4cc62953a5c3cc8454afb
branch: master
author: Lewis Gaul <lewis.gaul at gmail.com>
committer: ericvsmith <ericvsmith at users.noreply.github.com>
date: 2021-04-13T19:59:24-04:00
summary:
bpo-43080: pprint for dataclass instances (GH-24389)
* Added pprint support for dataclass instances which don't have a custom __repr__.
files:
A Misc/NEWS.d/next/Library/2021-01-31-00-23-13.bpo-43080.-fDg4Q.rst
M Doc/library/pprint.rst
M Doc/whatsnew/3.10.rst
M Lib/pprint.py
M Lib/test/test_pprint.py
diff --git a/Doc/library/pprint.rst b/Doc/library/pprint.rst
index 756c33a7a8668..f45c66fd9f46f 100644
--- a/Doc/library/pprint.rst
+++ b/Doc/library/pprint.rst
@@ -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:
diff --git a/Doc/whatsnew/3.10.rst b/Doc/whatsnew/3.10.rst
index e0e7d19577e2b..b1a33eeb5e61d 100644
--- a/Doc/whatsnew/3.10.rst
+++ b/Doc/whatsnew/3.10.rst
@@ -820,6 +820,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
----------
diff --git a/Lib/pprint.py b/Lib/pprint.py
index b45cfdd99a8e1..13819f3fef212 100644
--- a/Lib/pprint.py
+++ b/Lib/pprint.py
@@ -35,6 +35,7 @@
"""
import collections as _collections
+import dataclasses as _dataclasses
import re
import sys as _sys
import types as _types
@@ -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):
@@ -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
@@ -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
diff --git a/Lib/test/test_pprint.py b/Lib/test/test_pprint.py
index e5d2ac52d1283..6c714fd39e203 100644
--- a/Lib/test/test_pprint.py
+++ b/Lib/test/test_pprint.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
import collections
+import dataclasses
import io
import itertools
import pprint
@@ -66,6 +67,38 @@ class dict_custom_repr(dict):
def __repr__(self):
return '*'*len(dict.__repr__(self))
+ at dataclasses.dataclass
+class dataclass1:
+ field1: str
+ field2: int
+ field3: bool = False
+ field4: int = dataclasses.field(default=1, repr=False)
+
+ at dataclasses.dataclass
+class dataclass2:
+ a: int = 1
+ def __repr__(self):
+ return "custom repr that doesn't fit within pprint width"
+
+ at dataclasses.dataclass(repr=False)
+class dataclass3:
+ a: int = 1
+
+ at dataclasses.dataclass
+class dataclass4:
+ a: "dataclass4"
+ b: int = 1
+
+ at dataclasses.dataclass
+class dataclass5:
+ a: "dataclass6"
+ b: int = 1
+
+ at dataclasses.dataclass
+class dataclass6:
+ c: "dataclass5"
+ d: int = 1
+
class Unorderable:
def __repr__(self):
return str(id(self))
@@ -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,
@@ -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()',
diff --git a/Misc/NEWS.d/next/Library/2021-01-31-00-23-13.bpo-43080.-fDg4Q.rst b/Misc/NEWS.d/next/Library/2021-01-31-00-23-13.bpo-43080.-fDg4Q.rst
new file mode 100644
index 0000000000000..aa59b901739b4
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2021-01-31-00-23-13.bpo-43080.-fDg4Q.rst
@@ -0,0 +1 @@
+:mod:`pprint` now has support for :class:`dataclasses.dataclass`. Patch by Lewis Gaul.
\ No newline at end of file
More information about the Python-checkins
mailing list