1"""Tools to provide pretty/human-readable display of objects."""
2
3from __future__ import annotations as _annotations
4
5import types
6from collections.abc import Callable, Collection, Generator, Iterable
7from typing import TYPE_CHECKING, Any, ForwardRef, cast
8
9import typing_extensions
10from typing_extensions import TypeAlias
11from typing_inspection import typing_objects
12from typing_inspection.introspection import is_union_origin
13
14from . import _typing_extra
15
16if TYPE_CHECKING:
17 # TODO remove type error comments when we drop support for Python 3.9
18 ReprArgs: TypeAlias = Iterable[tuple[str | None, Any]] # pyright: ignore[reportGeneralTypeIssues]
19 RichReprResult: TypeAlias = Iterable[Any | tuple[Any] | tuple[str, Any] | tuple[str, Any, Any]] # pyright: ignore[reportGeneralTypeIssues]
20
21
22class PlainRepr(str):
23 """String class where repr doesn't include quotes. Useful with Representation when you want to return a string
24 representation of something that is valid (or pseudo-valid) python.
25 """
26
27 def __repr__(self) -> str:
28 return str(self)
29
30
31class Representation:
32 # Mixin to provide `__str__`, `__repr__`, and `__pretty__` and `__rich_repr__` methods.
33 # `__pretty__` is used by [devtools](https://python-devtools.helpmanual.io/).
34 # `__rich_repr__` is used by [rich](https://rich.readthedocs.io/en/stable/pretty.html).
35 # (this is not a docstring to avoid adding a docstring to classes which inherit from Representation)
36
37 __slots__ = ()
38
39 def __repr_args__(self) -> ReprArgs:
40 """Returns the attributes to show in __str__, __repr__, and __pretty__ this is generally overridden.
41
42 Can either return:
43 * name - value pairs, e.g.: `[('foo_name', 'foo'), ('bar_name', ['b', 'a', 'r'])]`
44 * or, just values, e.g.: `[(None, 'foo'), (None, ['b', 'a', 'r'])]`
45 """
46 attrs_names = cast(Collection[str], self.__slots__)
47 if not attrs_names and hasattr(self, '__dict__'):
48 attrs_names = self.__dict__.keys()
49 attrs = ((s, getattr(self, s)) for s in attrs_names)
50 return [(a, v if v is not self else self.__repr_recursion__(v)) for a, v in attrs if v is not None]
51
52 def __repr_name__(self) -> str:
53 """Name of the instance's class, used in __repr__."""
54 return self.__class__.__name__
55
56 def __repr_recursion__(self, object: Any) -> str:
57 """Returns the string representation of a recursive object."""
58 # This is copied over from the stdlib `pprint` module:
59 return f'<Recursion on {type(object).__name__} with id={id(object)}>'
60
61 def __repr_str__(self, join_str: str) -> str:
62 return join_str.join(repr(v) if a is None else f'{a}={v!r}' for a, v in self.__repr_args__())
63
64 def __pretty__(self, fmt: Callable[[Any], Any], **kwargs: Any) -> Generator[Any]:
65 """Used by devtools (https://python-devtools.helpmanual.io/) to pretty print objects."""
66 yield self.__repr_name__() + '('
67 yield 1
68 for name, value in self.__repr_args__():
69 if name is not None:
70 yield name + '='
71 yield fmt(value)
72 yield ','
73 yield 0
74 yield -1
75 yield ')'
76
77 def __rich_repr__(self) -> RichReprResult:
78 """Used by Rich (https://rich.readthedocs.io/en/stable/pretty.html) to pretty print objects."""
79 for name, field_repr in self.__repr_args__():
80 if name is None:
81 yield field_repr
82 else:
83 yield name, field_repr
84
85 def __str__(self) -> str:
86 return self.__repr_str__(' ')
87
88 def __repr__(self) -> str:
89 return f'{self.__repr_name__()}({self.__repr_str__(", ")})'
90
91
92def display_as_type(obj: Any) -> str:
93 """Pretty representation of a type, should be as close as possible to the original type definition string.
94
95 Takes some logic from `typing._type_repr`.
96 """
97 if isinstance(obj, (types.FunctionType, types.BuiltinFunctionType)):
98 return obj.__name__
99 elif obj is ...:
100 return '...'
101 elif isinstance(obj, Representation):
102 return repr(obj)
103 elif isinstance(obj, ForwardRef) or typing_objects.is_typealiastype(obj):
104 return str(obj)
105
106 if not isinstance(obj, (_typing_extra.typing_base, _typing_extra.WithArgsTypes, type)):
107 obj = obj.__class__
108
109 if is_union_origin(typing_extensions.get_origin(obj)):
110 args = ', '.join(map(display_as_type, typing_extensions.get_args(obj)))
111 return f'Union[{args}]'
112 elif isinstance(obj, _typing_extra.WithArgsTypes):
113 if typing_objects.is_literal(typing_extensions.get_origin(obj)):
114 args = ', '.join(map(repr, typing_extensions.get_args(obj)))
115 else:
116 args = ', '.join(map(display_as_type, typing_extensions.get_args(obj)))
117 try:
118 return f'{obj.__qualname__}[{args}]'
119 except AttributeError:
120 return str(obj).replace('typing.', '').replace('typing_extensions.', '') # handles TypeAliasType in 3.12
121 elif isinstance(obj, type):
122 return obj.__qualname__
123 else:
124 return repr(obj).replace('typing.', '').replace('typing_extensions.', '')