Skip to content

Commit

Permalink
Merge pull request #4133 from HypothesisWorks/DRMacIver/better-record…
Browse files Browse the repository at this point in the history
…-printing

Add better support for pretty-printing record types
  • Loading branch information
Zac-HD authored Oct 12, 2024
2 parents 228437f + a302e36 commit 857c3c9
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 0 deletions.
4 changes: 4 additions & 0 deletions hypothesis-python/RELEASE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
RELEASE_TYPE: minor

This improves the formatting of dataclasses and attrs classes when printing
falsifying examples.
32 changes: 32 additions & 0 deletions hypothesis-python/src/hypothesis/vendor/pretty.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,24 @@ def pretty(self, obj):
meth = cls._repr_pretty_
if callable(meth):
return meth(obj, self, cycle)
if hasattr(cls, "__attrs_attrs__"):
return pprint_fields(
obj,
self,
cycle,
[at.name for at in cls.__attrs_attrs__ if at.init],
)
if hasattr(cls, "__dataclass_fields__"):
return pprint_fields(
obj,
self,
cycle,
[
k
for k, v in cls.__dataclass_fields__.items()
if v.init
],
)
# Now check for object-specific printers which show how this
# object was constructed (a Hypothesis special feature).
printers = self.known_object_printers[IDKey(obj)]
Expand Down Expand Up @@ -714,6 +732,20 @@ def _repr_pprint(obj, p, cycle):
p.text(output_line)


def pprint_fields(obj, p, cycle, fields):
name = obj.__class__.__name__
if cycle:
return p.text(f"{name}(...)")
with p.group(1, name + "(", ")"):
for idx, field in enumerate(fields):
if idx:
p.text(",")
p.breakable()
p.text(field)
p.text("=")
p.pretty(getattr(obj, field))


def _function_pprint(obj, p, cycle):
"""Base pprint for all functions and builtin functions."""
from hypothesis.internal.reflection import get_pretty_function_description
Expand Down
118 changes: 118 additions & 0 deletions hypothesis-python/tests/cover/test_pretty.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,14 @@

import re
import struct
import sys
import warnings
from collections import Counter, OrderedDict, defaultdict, deque
from dataclasses import dataclass, field
from enum import Enum, Flag
from functools import partial

import attrs
import pytest

from hypothesis import given, strategies as st
Expand Down Expand Up @@ -758,3 +761,118 @@ def test_pprint_extremely_large_integers():
got = p.getvalue()
assert got == f"{x:#_x}" # hexadecimal with underscores
assert eval(got) == x


class ReprDetector:
def _repr_pretty_(self, p, cycle):
"""Exercise the IPython callback interface."""
p.text("GOOD")

def __repr__(self):
return "BAD"


@dataclass
class SomeDataClass:
x: object


def test_pretty_prints_data_classes():
assert pretty.pretty(SomeDataClass(ReprDetector())) == "SomeDataClass(x=GOOD)"


@attrs.define
class SomeAttrsClass:
x: ReprDetector


@pytest.mark.skipif(sys.version_info[:2] >= (3, 14), reason="FIXME-py314")
def test_pretty_prints_attrs_classes():
assert pretty.pretty(SomeAttrsClass(ReprDetector())) == "SomeAttrsClass(x=GOOD)"


@attrs.define
class SomeAttrsClassWithCustomPretty:
def _repr_pretty_(self, p, cycle):
"""Exercise the IPython callback interface."""
p.text("I am a banana")


def test_custom_pretty_print_method_overrides_field_printing():
assert pretty.pretty(SomeAttrsClassWithCustomPretty()) == "I am a banana"


@attrs.define
class SomeAttrsClassWithLotsOfFields:
a: int
b: int
c: int
d: int
e: int
f: int
g: int
h: int
i: int
j: int
k: int
l: int
m: int
n: int
o: int
p: int
q: int
r: int
s: int


@pytest.mark.skipif(sys.version_info[:2] >= (3, 14), reason="FIXME-py314")
def test_will_line_break_between_fields():
obj = SomeAttrsClassWithLotsOfFields(
**{
at.name: 12345678900000000000000001
for at in SomeAttrsClassWithLotsOfFields.__attrs_attrs__
}
)
assert "\n" in pretty.pretty(obj)


@attrs.define
class SomeDataClassWithNoFields: ...


def test_prints_empty_dataclass_correctly():
assert pretty.pretty(SomeDataClassWithNoFields()) == "SomeDataClassWithNoFields()"


def test_handles_cycles_in_dataclass():
x = SomeDataClass(x=1)
x.x = x

assert pretty.pretty(x) == "SomeDataClass(x=SomeDataClass(...))"


@dataclass
class DataClassWithNoInitField:
x: int
y: int = field(init=False)


def test_does_not_include_no_init_fields_in_dataclass_printing():
record = DataClassWithNoInitField(x=1)
assert pretty.pretty(record) == "DataClassWithNoInitField(x=1)"
record.y = 1
assert pretty.pretty(record) == "DataClassWithNoInitField(x=1)"


@attrs.define
class AttrsClassWithNoInitField:
x: int
y: int = attrs.field(init=False)


@pytest.mark.skipif(sys.version_info[:2] >= (3, 14), reason="FIXME-py314")
def test_does_not_include_no_init_fields_in_attrs_printing():
record = AttrsClassWithNoInitField(x=1)
assert pretty.pretty(record) == "AttrsClassWithNoInitField(x=1)"
record.y = 1
assert pretty.pretty(record) == "AttrsClassWithNoInitField(x=1)"

0 comments on commit 857c3c9

Please sign in to comment.