repr.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. """Object representations for debugging purposes. Unlike the default
  2. repr, these expose more information and produce HTML instead of ASCII.
  3. Together with the CSS and JavaScript of the debugger this gives a
  4. colorful and more compact output.
  5. """
  6. from __future__ import annotations
  7. import codecs
  8. import re
  9. import sys
  10. import typing as t
  11. from collections import deque
  12. from traceback import format_exception_only
  13. from markupsafe import escape
  14. missing = object()
  15. _paragraph_re = re.compile(r"(?:\r\n|\r|\n){2,}")
  16. RegexType = type(_paragraph_re)
  17. HELP_HTML = """\
  18. <div class=box>
  19. <h3>%(title)s</h3>
  20. <pre class=help>%(text)s</pre>
  21. </div>\
  22. """
  23. OBJECT_DUMP_HTML = """\
  24. <div class=box>
  25. <h3>%(title)s</h3>
  26. %(repr)s
  27. <table>%(items)s</table>
  28. </div>\
  29. """
  30. def debug_repr(obj: object) -> str:
  31. """Creates a debug repr of an object as HTML string."""
  32. return DebugReprGenerator().repr(obj)
  33. def dump(obj: object = missing) -> None:
  34. """Print the object details to stdout._write (for the interactive
  35. console of the web debugger.
  36. """
  37. gen = DebugReprGenerator()
  38. if obj is missing:
  39. rv = gen.dump_locals(sys._getframe(1).f_locals)
  40. else:
  41. rv = gen.dump_object(obj)
  42. sys.stdout._write(rv) # type: ignore
  43. class _Helper:
  44. """Displays an HTML version of the normal help, for the interactive
  45. debugger only because it requires a patched sys.stdout.
  46. """
  47. def __repr__(self) -> str:
  48. return "Type help(object) for help about object."
  49. def __call__(self, topic: t.Any | None = None) -> None:
  50. if topic is None:
  51. sys.stdout._write(f"<span class=help>{self!r}</span>") # type: ignore
  52. return
  53. import pydoc
  54. pydoc.help(topic)
  55. rv = sys.stdout.reset() # type: ignore
  56. paragraphs = _paragraph_re.split(rv)
  57. if len(paragraphs) > 1:
  58. title = paragraphs[0]
  59. text = "\n\n".join(paragraphs[1:])
  60. else:
  61. title = "Help"
  62. text = paragraphs[0]
  63. sys.stdout._write(HELP_HTML % {"title": title, "text": text}) # type: ignore
  64. helper = _Helper()
  65. def _add_subclass_info(
  66. inner: str, obj: object, base: t.Type | tuple[t.Type, ...]
  67. ) -> str:
  68. if isinstance(base, tuple):
  69. for cls in base:
  70. if type(obj) is cls:
  71. return inner
  72. elif type(obj) is base:
  73. return inner
  74. module = ""
  75. if obj.__class__.__module__ not in ("__builtin__", "exceptions"):
  76. module = f'<span class="module">{obj.__class__.__module__}.</span>'
  77. return f"{module}{type(obj).__name__}({inner})"
  78. def _sequence_repr_maker(
  79. left: str, right: str, base: t.Type, limit: int = 8
  80. ) -> t.Callable[[DebugReprGenerator, t.Iterable, bool], str]:
  81. def proxy(self: DebugReprGenerator, obj: t.Iterable, recursive: bool) -> str:
  82. if recursive:
  83. return _add_subclass_info(f"{left}...{right}", obj, base)
  84. buf = [left]
  85. have_extended_section = False
  86. for idx, item in enumerate(obj):
  87. if idx:
  88. buf.append(", ")
  89. if idx == limit:
  90. buf.append('<span class="extended">')
  91. have_extended_section = True
  92. buf.append(self.repr(item))
  93. if have_extended_section:
  94. buf.append("</span>")
  95. buf.append(right)
  96. return _add_subclass_info("".join(buf), obj, base)
  97. return proxy
  98. class DebugReprGenerator:
  99. def __init__(self) -> None:
  100. self._stack: list[t.Any] = []
  101. list_repr = _sequence_repr_maker("[", "]", list)
  102. tuple_repr = _sequence_repr_maker("(", ")", tuple)
  103. set_repr = _sequence_repr_maker("set([", "])", set)
  104. frozenset_repr = _sequence_repr_maker("frozenset([", "])", frozenset)
  105. deque_repr = _sequence_repr_maker(
  106. '<span class="module">collections.</span>deque([', "])", deque
  107. )
  108. def regex_repr(self, obj: t.Pattern) -> str:
  109. pattern = repr(obj.pattern)
  110. pattern = codecs.decode(pattern, "unicode-escape", "ignore")
  111. pattern = f"r{pattern}"
  112. return f're.compile(<span class="string regex">{pattern}</span>)'
  113. def string_repr(self, obj: str | bytes, limit: int = 70) -> str:
  114. buf = ['<span class="string">']
  115. r = repr(obj)
  116. # shorten the repr when the hidden part would be at least 3 chars
  117. if len(r) - limit > 2:
  118. buf.extend(
  119. (
  120. escape(r[:limit]),
  121. '<span class="extended">',
  122. escape(r[limit:]),
  123. "</span>",
  124. )
  125. )
  126. else:
  127. buf.append(escape(r))
  128. buf.append("</span>")
  129. out = "".join(buf)
  130. # if the repr looks like a standard string, add subclass info if needed
  131. if r[0] in "'\"" or (r[0] == "b" and r[1] in "'\""):
  132. return _add_subclass_info(out, obj, (bytes, str))
  133. # otherwise, assume the repr distinguishes the subclass already
  134. return out
  135. def dict_repr(
  136. self,
  137. d: dict[int, None] | dict[str, int] | dict[str | int, int],
  138. recursive: bool,
  139. limit: int = 5,
  140. ) -> str:
  141. if recursive:
  142. return _add_subclass_info("{...}", d, dict)
  143. buf = ["{"]
  144. have_extended_section = False
  145. for idx, (key, value) in enumerate(d.items()):
  146. if idx:
  147. buf.append(", ")
  148. if idx == limit - 1:
  149. buf.append('<span class="extended">')
  150. have_extended_section = True
  151. buf.append(
  152. f'<span class="pair"><span class="key">{self.repr(key)}</span>:'
  153. f' <span class="value">{self.repr(value)}</span></span>'
  154. )
  155. if have_extended_section:
  156. buf.append("</span>")
  157. buf.append("}")
  158. return _add_subclass_info("".join(buf), d, dict)
  159. def object_repr(self, obj: type[dict] | t.Callable | type[list] | None) -> str:
  160. r = repr(obj)
  161. return f'<span class="object">{escape(r)}</span>'
  162. def dispatch_repr(self, obj: t.Any, recursive: bool) -> str:
  163. if obj is helper:
  164. return f'<span class="help">{helper!r}</span>'
  165. if isinstance(obj, (int, float, complex)):
  166. return f'<span class="number">{obj!r}</span>'
  167. if isinstance(obj, str) or isinstance(obj, bytes):
  168. return self.string_repr(obj)
  169. if isinstance(obj, RegexType):
  170. return self.regex_repr(obj)
  171. if isinstance(obj, list):
  172. return self.list_repr(obj, recursive)
  173. if isinstance(obj, tuple):
  174. return self.tuple_repr(obj, recursive)
  175. if isinstance(obj, set):
  176. return self.set_repr(obj, recursive)
  177. if isinstance(obj, frozenset):
  178. return self.frozenset_repr(obj, recursive)
  179. if isinstance(obj, dict):
  180. return self.dict_repr(obj, recursive)
  181. if isinstance(obj, deque):
  182. return self.deque_repr(obj, recursive)
  183. return self.object_repr(obj)
  184. def fallback_repr(self) -> str:
  185. try:
  186. info = "".join(format_exception_only(*sys.exc_info()[:2]))
  187. except Exception:
  188. info = "?"
  189. return (
  190. '<span class="brokenrepr">'
  191. f"&lt;broken repr ({escape(info.strip())})&gt;</span>"
  192. )
  193. def repr(self, obj: object) -> str:
  194. recursive = False
  195. for item in self._stack:
  196. if item is obj:
  197. recursive = True
  198. break
  199. self._stack.append(obj)
  200. try:
  201. try:
  202. return self.dispatch_repr(obj, recursive)
  203. except Exception:
  204. return self.fallback_repr()
  205. finally:
  206. self._stack.pop()
  207. def dump_object(self, obj: object) -> str:
  208. repr = None
  209. items: list[tuple[str, str]] | None = None
  210. if isinstance(obj, dict):
  211. title = "Contents of"
  212. items = []
  213. for key, value in obj.items():
  214. if not isinstance(key, str):
  215. items = None
  216. break
  217. items.append((key, self.repr(value)))
  218. if items is None:
  219. items = []
  220. repr = self.repr(obj)
  221. for key in dir(obj):
  222. try:
  223. items.append((key, self.repr(getattr(obj, key))))
  224. except Exception:
  225. pass
  226. title = "Details for"
  227. title += f" {object.__repr__(obj)[1:-1]}"
  228. return self.render_object_dump(items, title, repr)
  229. def dump_locals(self, d: dict[str, t.Any]) -> str:
  230. items = [(key, self.repr(value)) for key, value in d.items()]
  231. return self.render_object_dump(items, "Local variables in frame")
  232. def render_object_dump(
  233. self, items: list[tuple[str, str]], title: str, repr: str | None = None
  234. ) -> str:
  235. html_items = []
  236. for key, value in items:
  237. html_items.append(f"<tr><th>{escape(key)}<td><pre class=repr>{value}</pre>")
  238. if not html_items:
  239. html_items.append("<tr><td><em>Nothing</em>")
  240. return OBJECT_DUMP_HTML % {
  241. "title": escape(title),
  242. "repr": f"<pre class=repr>{repr if repr else ''}</pre>",
  243. "items": "\n".join(html_items),
  244. }