|
| 1 | +from collections.abc import Callable, Iterator |
| 2 | +from dataclasses import dataclass |
| 3 | +from typing import Any |
| 4 | + |
| 5 | +import pytest |
| 6 | + |
| 7 | +from devtools import Debug |
| 8 | +from devtools.prettier import PrettyFormat, pformat |
| 9 | + |
| 10 | + |
| 11 | +def test_recursive_dict(): |
| 12 | + value: dict[str, Any] = {'before': 1} |
| 13 | + value['self'] = value |
| 14 | + value['after'] = 2 |
| 15 | + expected = "{\n 'before': 1,\n 'self': <Recursion on dict>,\n 'after': 2,\n}" |
| 16 | + assert pformat(value) == expected |
| 17 | + assert pformat(value) == expected |
| 18 | + |
| 19 | + |
| 20 | +@pytest.mark.parametrize('cutoff', [0, 10]) |
| 21 | +def test_recursive_list(cutoff: int): |
| 22 | + value: list[Any] = [1, 2] |
| 23 | + value.append(value) |
| 24 | + assert PrettyFormat(simple_cutoff=cutoff)(value) == '[\n 1,\n 2,\n <Recursion on list>,\n]' |
| 25 | + |
| 26 | + |
| 27 | +def test_mutually_recursive_containers(): |
| 28 | + value: dict[str, Any] = {} |
| 29 | + value['child'] = [value, 1] |
| 30 | + assert pformat(value) == "{\n 'child': [\n <Recursion on dict>,\n 1,\n ],\n}" |
| 31 | + |
| 32 | + |
| 33 | +@pytest.mark.parametrize('slots', [False, True]) |
| 34 | +def test_recursive_dataclass(slots: bool): |
| 35 | + @dataclass(slots=slots) |
| 36 | + class Node: |
| 37 | + child: Any = None |
| 38 | + |
| 39 | + value = Node() |
| 40 | + value.child = value |
| 41 | + assert pformat(value) == 'Node(\n child=<Recursion on Node>,\n)' |
| 42 | + |
| 43 | + |
| 44 | +def test_recursive_pretty(): |
| 45 | + class Pretty: |
| 46 | + def __pretty__(self, fmt: Callable[[Any], Any], skip_exc: type[Exception]) -> Iterator[Any]: |
| 47 | + yield 'Pretty(' |
| 48 | + yield fmt(self) |
| 49 | + yield ')' |
| 50 | + |
| 51 | + assert pformat(Pretty()) == 'Pretty(<Recursion on Pretty>)' |
| 52 | + |
| 53 | + |
| 54 | +def test_recursive_generator(): |
| 55 | + def generate() -> Iterator[Any]: |
| 56 | + yield value |
| 57 | + yield 1 |
| 58 | + |
| 59 | + value = generate() |
| 60 | + assert pformat(value) == '(\n <Recursion on generator>,\n 1,\n)' |
| 61 | + |
| 62 | + |
| 63 | +def test_recursive_reentrant_repr(): |
| 64 | + formatter = PrettyFormat() |
| 65 | + |
| 66 | + class Reentrant: |
| 67 | + def __repr__(self) -> str: |
| 68 | + return formatter(self) |
| 69 | + |
| 70 | + assert formatter(Reentrant()) == '<Recursion on Reentrant>' |
| 71 | + assert formatter(1) == '1' |
| 72 | + |
| 73 | + |
| 74 | +def test_shared_child_is_not_recursive(): |
| 75 | + child = {'value': 1} |
| 76 | + assert PrettyFormat(simple_cutoff=0)([child, child]) == ( |
| 77 | + "[\n {\n 'value': 1,\n },\n {\n 'value': 1,\n },\n]" |
| 78 | + ) |
| 79 | + |
| 80 | + |
| 81 | +def test_formatting_error_does_not_mark_value_as_recursive(): |
| 82 | + formatter = PrettyFormat() |
| 83 | + |
| 84 | + class Broken: |
| 85 | + fail = True |
| 86 | + |
| 87 | + def __repr__(self) -> str: |
| 88 | + if self.fail: |
| 89 | + raise ValueError('broken repr') |
| 90 | + return 'recovered' |
| 91 | + |
| 92 | + value = Broken() |
| 93 | + with pytest.raises(ValueError, match='broken repr'): |
| 94 | + formatter(value) |
| 95 | + value.fail = False |
| 96 | + assert formatter(value) == 'recovered' |
| 97 | + |
| 98 | + |
| 99 | +def test_debug_recursive_value(): |
| 100 | + value: dict[str, Any] = {'before': 1} |
| 101 | + value['self'] = value |
| 102 | + output = str(Debug(highlight=False).format(value)) |
| 103 | + assert "'before': 1" in output |
| 104 | + assert "'self': <Recursion on dict>" in output |
0 commit comments