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