From a302e3661553a8a3fee765990380d805b4a73fb1 Mon Sep 17 00:00:00 2001 From: "David R. MacIver" Date: Wed, 9 Oct 2024 11:09:47 +0100 Subject: [PATCH] Add better support for pretty-printing record types --- hypothesis-python/RELEASE.rst | 4 + .../src/hypothesis/vendor/pretty.py | 32 +++++ hypothesis-python/tests/cover/test_pretty.py | 118 ++++++++++++++++++ 3 files changed, 154 insertions(+) create mode 100644 hypothesis-python/RELEASE.rst diff --git a/hypothesis-python/RELEASE.rst b/hypothesis-python/RELEASE.rst new file mode 100644 index 0000000000..080ef34e59 --- /dev/null +++ b/hypothesis-python/RELEASE.rst @@ -0,0 +1,4 @@ +RELEASE_TYPE: minor + +This improves the formatting of dataclasses and attrs classes when printing +falsifying examples. diff --git a/hypothesis-python/src/hypothesis/vendor/pretty.py b/hypothesis-python/src/hypothesis/vendor/pretty.py index b2204d2d51..2fad6e1ff0 100644 --- a/hypothesis-python/src/hypothesis/vendor/pretty.py +++ b/hypothesis-python/src/hypothesis/vendor/pretty.py @@ -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)] @@ -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 diff --git a/hypothesis-python/tests/cover/test_pretty.py b/hypothesis-python/tests/cover/test_pretty.py index 85bf597a96..e18a8fdb9c 100644 --- a/hypothesis-python/tests/cover/test_pretty.py +++ b/hypothesis-python/tests/cover/test_pretty.py @@ -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 @@ -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)"