Skip to content

Commit 857c3c9

Browse files
authored
Merge pull request #4133 from HypothesisWorks/DRMacIver/better-record-printing
Add better support for pretty-printing record types
2 parents 228437f + a302e36 commit 857c3c9

File tree

3 files changed

+154
-0
lines changed

3 files changed

+154
-0
lines changed

hypothesis-python/RELEASE.rst

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
RELEASE_TYPE: minor
2+
3+
This improves the formatting of dataclasses and attrs classes when printing
4+
falsifying examples.

hypothesis-python/src/hypothesis/vendor/pretty.py

+32
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,24 @@ def pretty(self, obj):
214214
meth = cls._repr_pretty_
215215
if callable(meth):
216216
return meth(obj, self, cycle)
217+
if hasattr(cls, "__attrs_attrs__"):
218+
return pprint_fields(
219+
obj,
220+
self,
221+
cycle,
222+
[at.name for at in cls.__attrs_attrs__ if at.init],
223+
)
224+
if hasattr(cls, "__dataclass_fields__"):
225+
return pprint_fields(
226+
obj,
227+
self,
228+
cycle,
229+
[
230+
k
231+
for k, v in cls.__dataclass_fields__.items()
232+
if v.init
233+
],
234+
)
217235
# Now check for object-specific printers which show how this
218236
# object was constructed (a Hypothesis special feature).
219237
printers = self.known_object_printers[IDKey(obj)]
@@ -714,6 +732,20 @@ def _repr_pprint(obj, p, cycle):
714732
p.text(output_line)
715733

716734

735+
def pprint_fields(obj, p, cycle, fields):
736+
name = obj.__class__.__name__
737+
if cycle:
738+
return p.text(f"{name}(...)")
739+
with p.group(1, name + "(", ")"):
740+
for idx, field in enumerate(fields):
741+
if idx:
742+
p.text(",")
743+
p.breakable()
744+
p.text(field)
745+
p.text("=")
746+
p.pretty(getattr(obj, field))
747+
748+
717749
def _function_pprint(obj, p, cycle):
718750
"""Base pprint for all functions and builtin functions."""
719751
from hypothesis.internal.reflection import get_pretty_function_description

hypothesis-python/tests/cover/test_pretty.py

+118
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,14 @@
4949

5050
import re
5151
import struct
52+
import sys
5253
import warnings
5354
from collections import Counter, OrderedDict, defaultdict, deque
55+
from dataclasses import dataclass, field
5456
from enum import Enum, Flag
5557
from functools import partial
5658

59+
import attrs
5760
import pytest
5861

5962
from hypothesis import given, strategies as st
@@ -758,3 +761,118 @@ def test_pprint_extremely_large_integers():
758761
got = p.getvalue()
759762
assert got == f"{x:#_x}" # hexadecimal with underscores
760763
assert eval(got) == x
764+
765+
766+
class ReprDetector:
767+
def _repr_pretty_(self, p, cycle):
768+
"""Exercise the IPython callback interface."""
769+
p.text("GOOD")
770+
771+
def __repr__(self):
772+
return "BAD"
773+
774+
775+
@dataclass
776+
class SomeDataClass:
777+
x: object
778+
779+
780+
def test_pretty_prints_data_classes():
781+
assert pretty.pretty(SomeDataClass(ReprDetector())) == "SomeDataClass(x=GOOD)"
782+
783+
784+
@attrs.define
785+
class SomeAttrsClass:
786+
x: ReprDetector
787+
788+
789+
@pytest.mark.skipif(sys.version_info[:2] >= (3, 14), reason="FIXME-py314")
790+
def test_pretty_prints_attrs_classes():
791+
assert pretty.pretty(SomeAttrsClass(ReprDetector())) == "SomeAttrsClass(x=GOOD)"
792+
793+
794+
@attrs.define
795+
class SomeAttrsClassWithCustomPretty:
796+
def _repr_pretty_(self, p, cycle):
797+
"""Exercise the IPython callback interface."""
798+
p.text("I am a banana")
799+
800+
801+
def test_custom_pretty_print_method_overrides_field_printing():
802+
assert pretty.pretty(SomeAttrsClassWithCustomPretty()) == "I am a banana"
803+
804+
805+
@attrs.define
806+
class SomeAttrsClassWithLotsOfFields:
807+
a: int
808+
b: int
809+
c: int
810+
d: int
811+
e: int
812+
f: int
813+
g: int
814+
h: int
815+
i: int
816+
j: int
817+
k: int
818+
l: int
819+
m: int
820+
n: int
821+
o: int
822+
p: int
823+
q: int
824+
r: int
825+
s: int
826+
827+
828+
@pytest.mark.skipif(sys.version_info[:2] >= (3, 14), reason="FIXME-py314")
829+
def test_will_line_break_between_fields():
830+
obj = SomeAttrsClassWithLotsOfFields(
831+
**{
832+
at.name: 12345678900000000000000001
833+
for at in SomeAttrsClassWithLotsOfFields.__attrs_attrs__
834+
}
835+
)
836+
assert "\n" in pretty.pretty(obj)
837+
838+
839+
@attrs.define
840+
class SomeDataClassWithNoFields: ...
841+
842+
843+
def test_prints_empty_dataclass_correctly():
844+
assert pretty.pretty(SomeDataClassWithNoFields()) == "SomeDataClassWithNoFields()"
845+
846+
847+
def test_handles_cycles_in_dataclass():
848+
x = SomeDataClass(x=1)
849+
x.x = x
850+
851+
assert pretty.pretty(x) == "SomeDataClass(x=SomeDataClass(...))"
852+
853+
854+
@dataclass
855+
class DataClassWithNoInitField:
856+
x: int
857+
y: int = field(init=False)
858+
859+
860+
def test_does_not_include_no_init_fields_in_dataclass_printing():
861+
record = DataClassWithNoInitField(x=1)
862+
assert pretty.pretty(record) == "DataClassWithNoInitField(x=1)"
863+
record.y = 1
864+
assert pretty.pretty(record) == "DataClassWithNoInitField(x=1)"
865+
866+
867+
@attrs.define
868+
class AttrsClassWithNoInitField:
869+
x: int
870+
y: int = attrs.field(init=False)
871+
872+
873+
@pytest.mark.skipif(sys.version_info[:2] >= (3, 14), reason="FIXME-py314")
874+
def test_does_not_include_no_init_fields_in_attrs_printing():
875+
record = AttrsClassWithNoInitField(x=1)
876+
assert pretty.pretty(record) == "AttrsClassWithNoInitField(x=1)"
877+
record.y = 1
878+
assert pretty.pretty(record) == "AttrsClassWithNoInitField(x=1)"

0 commit comments

Comments
 (0)