_repr.py 5.1 KB

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