Reputation: 39045
Python Data Classes instances also include a string representation method, but its result isn't really sufficient for pretty printing purposes when classes have more than a few fields and/or longer field values.
Basically I'm looking for a way to customize the default dataclasses string representation routine or for a pretty-printer that understands data classes and prints them prettier.
So, it's just a small customization I have in mind: adding a line break after each field while indenting lines after the first one.
For example, instead of
x = InventoryItem('foo', 23)
print(x) # =>
InventoryItem(name='foo', unit_price=23, quantity_on_hand=0)
I want to get a string representation like this:
x = InventoryItem('foo', 23)
print(x) # =>
InventoryItem(
name='foo',
unit_price=23,
quantity_on_hand=0
)
Or similar. Perhaps a pretty-printer could get even fancier, such as aligning the =
assignment characters or something like that.
Of course, it should also work in a recursive fashion, e.g. fields that are also dataclasses should be indented more.
Upvotes: 32
Views: 26893
Reputation: 1377
Python 3.10+ Supports pretty printing dataclasses:
Python 3.10.0b2+ (heads/3.10:f807a4fad4, Sep 4 2021, 18:58:04) [GCC 11.1.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from dataclasses import dataclass
>>> @dataclass
... class Literal:
... value: 'Any'
...
>>> @dataclass
... class Binary:
... left: 'Binary | Literal'
... operator: str
... right: 'Binary | Literal'
...
>>> from pprint import pprint
>>> # magic happens here
>>> pprint(
... Binary(Binary(Literal(2), '*', Literal(100)), '+', Literal(50)))
Binary(left=Binary(left=Literal(value=2),
operator='*',
right=Literal(value=100)),
operator='+',
right=Literal(value=50))
Upvotes: 9
Reputation: 1333
All I cared about was having the fields on separate lines, so I ended up using dataclasses.asdict
along with pprint.pprint
:
from dataclasses import dataclass, asdict
from pprint import pprint
@dataclass
class SomeClass:
there: int
needs: int
to: int
be: int
many: int
fields: int
for_: int
it: int
to2: int
work: int
a = SomeClass(there=1, needs=2, to=3, be=4, many=5, fields=6, for_=7, it=8, to2=9, work=10)
pprint(asdict(a), sort_dicts=False)
Output:
{'there': 1,
'needs': 2,
'to': 3,
'be': 4,
'many': 5,
'fields': 6,
'for_': 7,
'it': 8,
'to2': 9,
'work': 10}
I was using Python 3.9.
Upvotes: 9
Reputation: 1779
You should probably use prettyprinter
but if you can't add dependencies for some reason then you could use this, which is ever so slightly shorter than salt-die's example (because it uses pprint
)
import dataclasses
import pprint
def dcppformat(x, chars=0):
def parts():
if dataclasses.is_dataclass(x):
yield type(x).__name__ + "("
def fields():
for field in dataclasses.fields(x):
nindent = chars + len(field.name) + 4
value = getattr(x, field.name)
rep_value = dcppformat(value)
yield " " * (chars + 3) + indent_body_chars(
"{}={}".format(field.name, rep_value), chars=nindent
)
yield ",\n".join(fields())
yield " " * chars + ")"
else:
yield pprint.pformat(x)
return "\n".join(parts())
def indent(x, level=1):
indent_chars(x, level * 4)
def indent_chars(x, chars=1):
return "\n".join(" " * chars + p for p in x.split("\n"))
def indent_body_chars(x, chars=4):
a, *b = x.split("\n")
if b:
return a + "\n" + indent_chars("\n".join(b), chars=chars,)
else:
return a
def dcpprint(x):
print(dcppformat(x))
def test():
@dataclasses.dataclass
class A:
a: object
b: str
dcpprint(A(a=A(a=None, b={"a": 1, "c": 1, "long": "a" * 100}), b=2))
if __name__ == "__main__":
test()
Upvotes: 1
Reputation: 39045
The pprint
package supports pretty printing only since version 3.10 (NB: Python 3.10 was released in 2021).
Example:
[ins] In [1]: from dataclasses import dataclass
...:
...: @dataclass
...: class Point:
...: x: int
...: y: int
...:
...: @dataclass
...: class Coords:
...: my_points: list
...: my_dict: dict
...:
...: coords = Coords([Point(1, 2), Point(3, 4)], {'a': (1, 2), (1, 2): 'a'})
[ins] In [15]: pprint.pprint(coords, width=20)
Coords(my_points=[Point(x=1,
y=2),
Point(x=3,
y=4)],
my_dict={'a': (1,
2),
(1, 2): 'a'})
When using Python 3.9 or older, there is the prettyprinter package that supports dataclasses and provides some nice pretty-printing features.
Example:
[ins] In [1]: from dataclasses import dataclass
...:
...: @dataclass
...: class Point:
...: x: int
...: y: int
...:
...: @dataclass
...: class Coords:
...: my_points: list
...: my_dict: dict
...:
...: coords = Coords([Point(1, 2), Point(3, 4)], {'a': (1, 2), (1, 2): 'a'})
[nav] In [2]: import prettyprinter as pp
[ins] In [3]: pp.pprint(coords)
Coords(my_points=[Point(x=1, y=2), Point(x=3, y=4)], my_dict={'a': (1, 2), (1, 2): 'a'})
The dataclasses support isn't enabled, by default, thus:
[nav] In [4]: pp.install_extras()
[ins] In [5]: pp.pprint(coords)
Coords(
my_points=[Point(x=1, y=2), Point(x=3, y=4)],
my_dict={'a': (1, 2), (1, 2): 'a'}
)
Or to force indenting of all fields:
[ins] In [6]: pp.pprint(coords, width=1)
Coords(
my_points=[
Point(
x=1,
y=2
),
Point(
x=3,
y=4
)
],
my_dict={
'a': (
1,
2
),
(
1,
2
): 'a'
}
)
Prettyprinter can even syntax-highlight! (cf. cpprint()
)
Considerations:
prettyprinter
isn't part of the python standard libraryprettyprinter
is pretty-printing very slowly, i.e. much slower than the standard pprint
, e.g. for checking if a value is a default value, it's compared against a default-constructed valueUpvotes: 27
Reputation: 854
We can use dataclasses.fields
to recurse through nested dataclasses and pretty print them:
from collections.abc import Mapping, Iterable
from dataclasses import is_dataclass, fields
def pretty_print(obj, indent=4):
"""
Pretty prints a (possibly deeply-nested) dataclass.
Each new block will be indented by `indent` spaces (default is 4).
"""
print(stringify(obj, indent))
def stringify(obj, indent=4, _indents=0):
if isinstance(obj, str):
return f"'{obj}'"
if not is_dataclass(obj) and not isinstance(obj, (Mapping, Iterable)):
return str(obj)
this_indent = indent * _indents * ' '
next_indent = indent * (_indents + 1) * ' '
start, end = f'{type(obj).__name__}(', ')' # dicts, lists, and tuples will re-assign this
if is_dataclass(obj):
body = '\n'.join(
f'{next_indent}{field.name}='
f'{stringify(getattr(obj, field.name), indent, _indents + 1)},' for field in fields(obj)
)
elif isinstance(obj, Mapping):
if isinstance(obj, dict):
start, end = '{}'
body = '\n'.join(
f'{next_indent}{stringify(key, indent, _indents + 1)}: '
f'{stringify(value, indent, _indents + 1)},' for key, value in obj.items()
)
else: # is Iterable
if isinstance(obj, list):
start, end = '[]'
elif isinstance(obj, tuple):
start = '('
body = '\n'.join(
f'{next_indent}{stringify(item, indent, _indents + 1)},' for item in obj
)
return f'{start}\n{body}\n{this_indent}{end}'
We can test it with a nested dataclass:
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
@dataclass
class Coords:
my_points: list
my_dict: dict
coords = Coords([Point(1, 2), Point(3, 4)], {'a': (1, 2), (1, 2): 'a'})
pretty_print(coords)
# Coords(
# my_points=[
# Point(
# x=1,
# y=2,
# ),
# Point(
# x=3,
# y=4,
# ),
# ],
# my_dict={
# 'a': (
# 1,
# 2,
# ),
# (
# 1,
# 2,
# ): 'a',
# },
# )
This should be general enough to cover most cases. Hope this helps!
Upvotes: 5