codekingpro/portable-devtools
114k
1"""Object representations for debugging purposes. Unlike the default2repr, these expose more information and produce HTML instead of ASCII.3 4Together with the CSS and JavaScript of the debugger this gives a5colorful and more compact output.6"""7 8from __future__ import annotations9 10import codecs11import re12import sys13import typing as t14from collections import deque15from traceback import format_exception_only16 17from markupsafe import escape18 19missing = object()20_paragraph_re = re.compile(r"(?:\r\n|\r|\n){2,}")21RegexType = type(_paragraph_re)22 23HELP_HTML = """\24<div class=box>25 <h3>%(title)s</h3>26 <pre class=help>%(text)s</pre>27</div>\28"""29OBJECT_DUMP_HTML = """\30<div class=box>31 <h3>%(title)s</h3>32 %(repr)s33 <table>%(items)s</table>34</div>\35"""36 37 38def debug_repr(obj: object) -> str:39 """Creates a debug repr of an object as HTML string."""40 return DebugReprGenerator().repr(obj)41 42 43def dump(obj: object = missing) -> None:44 """Print the object details to stdout._write (for the interactive45 console of the web debugger.46 """47 gen = DebugReprGenerator()48 if obj is missing:49 rv = gen.dump_locals(sys._getframe(1).f_locals)50 else:51 rv = gen.dump_object(obj)52 sys.stdout._write(rv) # type: ignore53 54 55class _Helper:56 """Displays an HTML version of the normal help, for the interactive57 debugger only because it requires a patched sys.stdout.58 """59 60 def __repr__(self) -> str:61 return "Type help(object) for help about object."62 63 def __call__(self, topic: t.Any | None = None) -> None:64 if topic is None:65 sys.stdout._write(f"<span class=help>{self!r}</span>") # type: ignore66 return67 import pydoc68 69 pydoc.help(topic)70 rv = sys.stdout.reset() # type: ignore71 paragraphs = _paragraph_re.split(rv)72 if len(paragraphs) > 1:73 title = paragraphs[0]74 text = "\n\n".join(paragraphs[1:])75 else:76 title = "Help"77 text = paragraphs[0]78 sys.stdout._write(HELP_HTML % {"title": title, "text": text}) # type: ignore79 80 81helper = _Helper()82 83 84def _add_subclass_info(inner: str, obj: object, base: type | tuple[type, ...]) -> str:85 if isinstance(base, tuple):86 for cls in base:87 if type(obj) is cls:88 return inner89 elif type(obj) is base:90 return inner91 module = ""92 if obj.__class__.__module__ not in ("__builtin__", "exceptions"):93 module = f'<span class="module">{obj.__class__.__module__}.</span>'94 return f"{module}{type(obj).__name__}({inner})"95 96 97def _sequence_repr_maker(98 left: str, right: str, base: type, limit: int = 899) -> t.Callable[[DebugReprGenerator, t.Iterable[t.Any], bool], str]:100 def proxy(self: DebugReprGenerator, obj: t.Iterable[t.Any], recursive: bool) -> str:101 if recursive:102 return _add_subclass_info(f"{left}...{right}", obj, base)103 buf = [left]104 have_extended_section = False105 for idx, item in enumerate(obj):106 if idx:107 buf.append(", ")108 if idx == limit:109 buf.append('<span class="extended">')110 have_extended_section = True111 buf.append(self.repr(item))112 if have_extended_section:113 buf.append("</span>")114 buf.append(right)115 return _add_subclass_info("".join(buf), obj, base)116 117 return proxy118 119 120class DebugReprGenerator:121 def __init__(self) -> None:122 self._stack: list[t.Any] = []123 124 list_repr = _sequence_repr_maker("[", "]", list)125 tuple_repr = _sequence_repr_maker("(", ")", tuple)126 set_repr = _sequence_repr_maker("set([", "])", set)127 frozenset_repr = _sequence_repr_maker("frozenset([", "])", frozenset)128 deque_repr = _sequence_repr_maker(129 '<span class="module">collections.</span>deque([', "])", deque130 )131 132 def regex_repr(self, obj: t.Pattern[t.AnyStr]) -> str:133 pattern = repr(obj.pattern)134 pattern = codecs.decode(pattern, "unicode-escape", "ignore")135 pattern = f"r{pattern}"136 return f're.compile(<span class="string regex">{pattern}</span>)'137 138 def string_repr(self, obj: str | bytes, limit: int = 70) -> str:139 buf = ['<span class="string">']140 r = repr(obj)141 142 # shorten the repr when the hidden part would be at least 3 chars143 if len(r) - limit > 2:144 buf.extend(145 (146 escape(r[:limit]),147 '<span class="extended">',148 escape(r[limit:]),149 "</span>",150 )151 )152 else:153 buf.append(escape(r))154 155 buf.append("</span>")156 out = "".join(buf)157 158 # if the repr looks like a standard string, add subclass info if needed159 if r[0] in "'\"" or (r[0] == "b" and r[1] in "'\""):160 return _add_subclass_info(out, obj, (bytes, str))161 162 # otherwise, assume the repr distinguishes the subclass already163 return out164 165 def dict_repr(166 self,167 d: dict[int, None] | dict[str, int] | dict[str | int, int],168 recursive: bool,169 limit: int = 5,170 ) -> str:171 if recursive:172 return _add_subclass_info("{...}", d, dict)173 buf = ["{"]174 have_extended_section = False175 for idx, (key, value) in enumerate(d.items()):176 if idx:177 buf.append(", ")178 if idx == limit - 1:179 buf.append('<span class="extended">')180 have_extended_section = True181 buf.append(182 f'<span class="pair"><span class="key">{self.repr(key)}</span>:'183 f' <span class="value">{self.repr(value)}</span></span>'184 )185 if have_extended_section:186 buf.append("</span>")187 buf.append("}")188 return _add_subclass_info("".join(buf), d, dict)189 190 def object_repr(self, obj: t.Any) -> str:191 r = repr(obj)192 return f'<span class="object">{escape(r)}</span>'193 194 def dispatch_repr(self, obj: t.Any, recursive: bool) -> str:195 if obj is helper:196 return f'<span class="help">{helper!r}</span>'197 if isinstance(obj, (int, float, complex)):198 return f'<span class="number">{obj!r}</span>'199 if isinstance(obj, str) or isinstance(obj, bytes):200 return self.string_repr(obj)201 if isinstance(obj, RegexType):202 return self.regex_repr(obj)203 if isinstance(obj, list):204 return self.list_repr(obj, recursive)205 if isinstance(obj, tuple):206 return self.tuple_repr(obj, recursive)207 if isinstance(obj, set):208 return self.set_repr(obj, recursive)209 if isinstance(obj, frozenset):210 return self.frozenset_repr(obj, recursive)211 if isinstance(obj, dict):212 return self.dict_repr(obj, recursive)213 if isinstance(obj, deque):214 return self.deque_repr(obj, recursive)215 return self.object_repr(obj)216 217 def fallback_repr(self) -> str:218 try:219 info = "".join(format_exception_only(*sys.exc_info()[:2]))220 except Exception:221 info = "?"222 return (223 '<span class="brokenrepr">'224 f"<broken repr ({escape(info.strip())})></span>"225 )226 227 def repr(self, obj: object) -> str:228 recursive = False229 for item in self._stack:230 if item is obj:231 recursive = True232 break233 self._stack.append(obj)234 try:235 try:236 return self.dispatch_repr(obj, recursive)237 except Exception:238 return self.fallback_repr()239 finally:240 self._stack.pop()241 242 def dump_object(self, obj: object) -> str:243 repr = None244 items: list[tuple[str, str]] | None = None245 246 if isinstance(obj, dict):247 title = "Contents of"248 items = []249 for key, value in obj.items():250 if not isinstance(key, str):251 items = None252 break253 items.append((key, self.repr(value)))254 if items is None:255 items = []256 repr = self.repr(obj)257 for key in dir(obj):258 try:259 items.append((key, self.repr(getattr(obj, key))))260 except Exception:261 pass262 title = "Details for"263 title += f" {object.__repr__(obj)[1:-1]}"264 return self.render_object_dump(items, title, repr)265 266 def dump_locals(self, d: dict[str, t.Any]) -> str:267 items = [(key, self.repr(value)) for key, value in d.items()]268 return self.render_object_dump(items, "Local variables in frame")269 270 def render_object_dump(271 self, items: list[tuple[str, str]], title: str, repr: str | None = None272 ) -> str:273 html_items = []274 for key, value in items:275 html_items.append(f"<tr><th>{escape(key)}<td><pre class=repr>{value}</pre>")276 if not html_items:277 html_items.append("<tr><td><em>Nothing</em>")278 return OBJECT_DUMP_HTML % {279 "title": escape(title),280 "repr": f"<pre class=repr>{repr if repr else ''}</pre>",281 "items": "\n".join(html_items),282 }283 