[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