Mercurial > repos > shellac > guppy_basecaller
comparison env/lib/python3.7/site-packages/boltons/tbutils.py @ 0:26e78fe6e8c4 draft
"planemo upload commit c699937486c35866861690329de38ec1a5d9f783"
| author | shellac |
|---|---|
| date | Sat, 02 May 2020 07:14:21 -0400 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| -1:000000000000 | 0:26e78fe6e8c4 |
|---|---|
| 1 # -*- coding: utf-8 -*- | |
| 2 """One of the oft-cited tenets of Python is that it is better to ask | |
| 3 forgiveness than permission. That is, there are many cases where it is | |
| 4 more inclusive and correct to handle exceptions than spend extra lines | |
| 5 and execution time checking for conditions. This philosophy makes good | |
| 6 exception handling features all the more important. Unfortunately | |
| 7 Python's :mod:`traceback` module is woefully behind the times. | |
| 8 | |
| 9 The ``tbutils`` module provides two disparate but complementary featuresets: | |
| 10 | |
| 11 1. With :class:`ExceptionInfo` and :class:`TracebackInfo`, the | |
| 12 ability to extract, construct, manipulate, format, and serialize | |
| 13 exceptions, tracebacks, and callstacks. | |
| 14 2. With :class:`ParsedException`, the ability to find and parse tracebacks | |
| 15 from captured output such as logs and stdout. | |
| 16 | |
| 17 There is also the :class:`ContextualTracebackInfo` variant of | |
| 18 :class:`TracebackInfo`, which includes much more information from each | |
| 19 frame of the callstack, including values of locals and neighboring | |
| 20 lines of code. | |
| 21 """ | |
| 22 | |
| 23 from __future__ import print_function | |
| 24 | |
| 25 import re | |
| 26 import sys | |
| 27 import linecache | |
| 28 | |
| 29 | |
| 30 try: | |
| 31 text = unicode # Python 2 | |
| 32 except NameError: | |
| 33 text = str # Python 3 | |
| 34 | |
| 35 | |
| 36 # TODO: chaining primitives? what are real use cases where these help? | |
| 37 | |
| 38 # TODO: print_* for backwards compatibility | |
| 39 # __all__ = ['extract_stack', 'extract_tb', 'format_exception', | |
| 40 # 'format_exception_only', 'format_list', 'format_stack', | |
| 41 # 'format_tb', 'print_exc', 'format_exc', 'print_exception', | |
| 42 # 'print_last', 'print_stack', 'print_tb'] | |
| 43 | |
| 44 | |
| 45 __all__ = ['ExceptionInfo', 'TracebackInfo', 'Callpoint', | |
| 46 'ContextualExceptionInfo', 'ContextualTracebackInfo', | |
| 47 'ContextualCallpoint', 'print_exception', 'ParsedException'] | |
| 48 | |
| 49 | |
| 50 class Callpoint(object): | |
| 51 """The Callpoint is a lightweight object used to represent a single | |
| 52 entry in the code of a call stack. It stores the code-related | |
| 53 metadata of a given frame. Available attributes are the same as | |
| 54 the parameters below. | |
| 55 | |
| 56 Args: | |
| 57 func_name (str): the function name | |
| 58 lineno (int): the line number | |
| 59 module_name (str): the module name | |
| 60 module_path (str): the filesystem path of the module | |
| 61 lasti (int): the index of bytecode execution | |
| 62 line (str): the single-line code content (if available) | |
| 63 | |
| 64 """ | |
| 65 __slots__ = ('func_name', 'lineno', 'module_name', 'module_path', 'lasti', | |
| 66 'line') | |
| 67 | |
| 68 def __init__(self, module_name, module_path, func_name, | |
| 69 lineno, lasti, line=None): | |
| 70 self.func_name = func_name | |
| 71 self.lineno = lineno | |
| 72 self.module_name = module_name | |
| 73 self.module_path = module_path | |
| 74 self.lasti = lasti | |
| 75 self.line = line | |
| 76 | |
| 77 def to_dict(self): | |
| 78 "Get a :class:`dict` copy of the Callpoint. Useful for serialization." | |
| 79 ret = {} | |
| 80 for slot in self.__slots__: | |
| 81 try: | |
| 82 val = getattr(self, slot) | |
| 83 except AttributeError: | |
| 84 pass | |
| 85 else: | |
| 86 ret[slot] = str(val) if isinstance(val, _DeferredLine) else val | |
| 87 return ret | |
| 88 | |
| 89 @classmethod | |
| 90 def from_current(cls, level=1): | |
| 91 "Creates a Callpoint from the location of the calling function." | |
| 92 frame = sys._getframe(level) | |
| 93 return cls.from_frame(frame) | |
| 94 | |
| 95 @classmethod | |
| 96 def from_frame(cls, frame): | |
| 97 "Create a Callpoint object from data extracted from the given frame." | |
| 98 func_name = frame.f_code.co_name | |
| 99 lineno = frame.f_lineno | |
| 100 module_name = frame.f_globals.get('__name__', '') | |
| 101 module_path = frame.f_code.co_filename | |
| 102 lasti = frame.f_lasti | |
| 103 line = _DeferredLine(module_path, lineno, frame.f_globals) | |
| 104 return cls(module_name, module_path, func_name, | |
| 105 lineno, lasti, line=line) | |
| 106 | |
| 107 @classmethod | |
| 108 def from_tb(cls, tb): | |
| 109 """Create a Callpoint from the traceback of the current | |
| 110 exception. Main difference with :meth:`from_frame` is that | |
| 111 ``lineno`` and ``lasti`` come from the traceback, which is to | |
| 112 say the line that failed in the try block, not the line | |
| 113 currently being executed (in the except block). | |
| 114 """ | |
| 115 func_name = tb.tb_frame.f_code.co_name | |
| 116 lineno = tb.tb_lineno | |
| 117 lasti = tb.tb_lasti | |
| 118 module_name = tb.tb_frame.f_globals.get('__name__', '') | |
| 119 module_path = tb.tb_frame.f_code.co_filename | |
| 120 line = _DeferredLine(module_path, lineno, tb.tb_frame.f_globals) | |
| 121 return cls(module_name, module_path, func_name, | |
| 122 lineno, lasti, line=line) | |
| 123 | |
| 124 def __repr__(self): | |
| 125 cn = self.__class__.__name__ | |
| 126 args = [getattr(self, s, None) for s in self.__slots__] | |
| 127 if not any(args): | |
| 128 return super(Callpoint, self).__repr__() | |
| 129 else: | |
| 130 return '%s(%s)' % (cn, ', '.join([repr(a) for a in args])) | |
| 131 | |
| 132 def tb_frame_str(self): | |
| 133 """Render the Callpoint as it would appear in a standard printed | |
| 134 Python traceback. Returns a string with filename, line number, | |
| 135 function name, and the actual code line of the error on up to | |
| 136 two lines. | |
| 137 """ | |
| 138 ret = ' File "%s", line %s, in %s\n' % (self.module_path, | |
| 139 self.lineno, | |
| 140 self.func_name) | |
| 141 if self.line: | |
| 142 ret += ' %s\n' % (str(self.line).strip(),) | |
| 143 return ret | |
| 144 | |
| 145 | |
| 146 class _DeferredLine(object): | |
| 147 """The _DeferredLine type allows Callpoints and TracebackInfos to be | |
| 148 constructed without potentially hitting the filesystem, as is the | |
| 149 normal behavior of the standard Python :mod:`traceback` and | |
| 150 :mod:`linecache` modules. Calling :func:`str` fetches and caches | |
| 151 the line. | |
| 152 | |
| 153 Args: | |
| 154 filename (str): the path of the file containing the line | |
| 155 lineno (int): the number of the line in question | |
| 156 module_globals (dict): an optional dict of module globals, | |
| 157 used to handle advanced use cases using custom module loaders. | |
| 158 | |
| 159 """ | |
| 160 __slots__ = ('filename', 'lineno', '_line', '_mod_name', '_mod_loader') | |
| 161 | |
| 162 def __init__(self, filename, lineno, module_globals=None): | |
| 163 self.filename = filename | |
| 164 self.lineno = lineno | |
| 165 # TODO: this is going away when we fix linecache | |
| 166 # TODO: (mark) read about loader | |
| 167 if module_globals is None: | |
| 168 self._mod_name = None | |
| 169 self._mod_loader = None | |
| 170 else: | |
| 171 self._mod_name = module_globals.get('__name__') | |
| 172 self._mod_loader = module_globals.get('__loader__') | |
| 173 | |
| 174 def __eq__(self, other): | |
| 175 return (self.lineno, self.filename) == (other.lineno, other.filename) | |
| 176 | |
| 177 def __ne__(self, other): | |
| 178 return not self == other | |
| 179 | |
| 180 def __str__(self): | |
| 181 ret = getattr(self, '_line', None) | |
| 182 if ret is not None: | |
| 183 return ret | |
| 184 try: | |
| 185 linecache.checkcache(self.filename) | |
| 186 mod_globals = {'__name__': self._mod_name, | |
| 187 '__loader__': self._mod_loader} | |
| 188 line = linecache.getline(self.filename, | |
| 189 self.lineno, | |
| 190 mod_globals) | |
| 191 line = line.rstrip() | |
| 192 except KeyError: | |
| 193 line = '' | |
| 194 self._line = line | |
| 195 return line | |
| 196 | |
| 197 def __repr__(self): | |
| 198 return repr(str(self)) | |
| 199 | |
| 200 def __len__(self): | |
| 201 return len(str(self)) | |
| 202 | |
| 203 | |
| 204 # TODO: dedup frames, look at __eq__ on _DeferredLine | |
| 205 class TracebackInfo(object): | |
| 206 """The TracebackInfo class provides a basic representation of a stack | |
| 207 trace, be it from an exception being handled or just part of | |
| 208 normal execution. It is basically a wrapper around a list of | |
| 209 :class:`Callpoint` objects representing frames. | |
| 210 | |
| 211 Args: | |
| 212 frames (list): A list of frame objects in the stack. | |
| 213 | |
| 214 .. note :: | |
| 215 | |
| 216 ``TracebackInfo`` can represent both exception tracebacks and | |
| 217 non-exception tracebacks (aka stack traces). As a result, there | |
| 218 is no ``TracebackInfo.from_current()``, as that would be | |
| 219 ambiguous. Instead, call :meth:`TracebackInfo.from_frame` | |
| 220 without the *frame* argument for a stack trace, or | |
| 221 :meth:`TracebackInfo.from_traceback` without the *tb* argument | |
| 222 for an exception traceback. | |
| 223 """ | |
| 224 callpoint_type = Callpoint | |
| 225 | |
| 226 def __init__(self, frames): | |
| 227 self.frames = frames | |
| 228 | |
| 229 @classmethod | |
| 230 def from_frame(cls, frame=None, level=1, limit=None): | |
| 231 """Create a new TracebackInfo *frame* by recurring up in the stack a | |
| 232 max of *limit* times. If *frame* is unset, get the frame from | |
| 233 :func:`sys._getframe` using *level*. | |
| 234 | |
| 235 Args: | |
| 236 frame (types.FrameType): frame object from | |
| 237 :func:`sys._getframe` or elsewhere. Defaults to result | |
| 238 of :func:`sys.get_frame`. | |
| 239 level (int): If *frame* is unset, the desired frame is | |
| 240 this many levels up the stack from the invocation of | |
| 241 this method. Default ``1`` (i.e., caller of this method). | |
| 242 limit (int): max number of parent frames to extract | |
| 243 (defaults to :data:`sys.tracebacklimit`) | |
| 244 | |
| 245 """ | |
| 246 ret = [] | |
| 247 if frame is None: | |
| 248 frame = sys._getframe(level) | |
| 249 if limit is None: | |
| 250 limit = getattr(sys, 'tracebacklimit', 1000) | |
| 251 n = 0 | |
| 252 while frame is not None and n < limit: | |
| 253 item = cls.callpoint_type.from_frame(frame) | |
| 254 ret.append(item) | |
| 255 frame = frame.f_back | |
| 256 n += 1 | |
| 257 ret.reverse() | |
| 258 return cls(ret) | |
| 259 | |
| 260 @classmethod | |
| 261 def from_traceback(cls, tb=None, limit=None): | |
| 262 """Create a new TracebackInfo from the traceback *tb* by recurring | |
| 263 up in the stack a max of *limit* times. If *tb* is unset, get | |
| 264 the traceback from the currently handled exception. If no | |
| 265 exception is being handled, raise a :exc:`ValueError`. | |
| 266 | |
| 267 Args: | |
| 268 | |
| 269 frame (types.TracebackType): traceback object from | |
| 270 :func:`sys.exc_info` or elsewhere. If absent or set to | |
| 271 ``None``, defaults to ``sys.exc_info()[2]``, and | |
| 272 raises a :exc:`ValueError` if no exception is | |
| 273 currently being handled. | |
| 274 limit (int): max number of parent frames to extract | |
| 275 (defaults to :data:`sys.tracebacklimit`) | |
| 276 | |
| 277 """ | |
| 278 ret = [] | |
| 279 if tb is None: | |
| 280 tb = sys.exc_info()[2] | |
| 281 if tb is None: | |
| 282 raise ValueError('no tb set and no exception being handled') | |
| 283 if limit is None: | |
| 284 limit = getattr(sys, 'tracebacklimit', 1000) | |
| 285 n = 0 | |
| 286 while tb is not None and n < limit: | |
| 287 item = cls.callpoint_type.from_tb(tb) | |
| 288 ret.append(item) | |
| 289 tb = tb.tb_next | |
| 290 n += 1 | |
| 291 return cls(ret) | |
| 292 | |
| 293 @classmethod | |
| 294 def from_dict(cls, d): | |
| 295 "Complements :meth:`TracebackInfo.to_dict`." | |
| 296 # TODO: check this. | |
| 297 return cls(d['frames']) | |
| 298 | |
| 299 def to_dict(self): | |
| 300 """Returns a dict with a list of :class:`Callpoint` frames converted | |
| 301 to dicts. | |
| 302 """ | |
| 303 return {'frames': [f.to_dict() for f in self.frames]} | |
| 304 | |
| 305 def __len__(self): | |
| 306 return len(self.frames) | |
| 307 | |
| 308 def __iter__(self): | |
| 309 return iter(self.frames) | |
| 310 | |
| 311 def __repr__(self): | |
| 312 cn = self.__class__.__name__ | |
| 313 | |
| 314 if self.frames: | |
| 315 frame_part = ' last=%r' % (self.frames[-1],) | |
| 316 else: | |
| 317 frame_part = '' | |
| 318 | |
| 319 return '<%s frames=%s%s>' % (cn, len(self.frames), frame_part) | |
| 320 | |
| 321 def __str__(self): | |
| 322 return self.get_formatted() | |
| 323 | |
| 324 def get_formatted(self): | |
| 325 """Returns a string as formatted in the traditional Python | |
| 326 built-in style observable when an exception is not caught. In | |
| 327 other words, mimics :func:`traceback.format_tb` and | |
| 328 :func:`traceback.format_stack`. | |
| 329 """ | |
| 330 ret = 'Traceback (most recent call last):\n' | |
| 331 ret += ''.join([f.tb_frame_str() for f in self.frames]) | |
| 332 return ret | |
| 333 | |
| 334 | |
| 335 class ExceptionInfo(object): | |
| 336 """An ExceptionInfo object ties together three main fields suitable | |
| 337 for representing an instance of an exception: The exception type | |
| 338 name, a string representation of the exception itself (the | |
| 339 exception message), and information about the traceback (stored as | |
| 340 a :class:`TracebackInfo` object). | |
| 341 | |
| 342 These fields line up with :func:`sys.exc_info`, but unlike the | |
| 343 values returned by that function, ExceptionInfo does not hold any | |
| 344 references to the real exception or traceback. This property makes | |
| 345 it suitable for serialization or long-term retention, without | |
| 346 worrying about formatting pitfalls, circular references, or leaking memory. | |
| 347 | |
| 348 Args: | |
| 349 | |
| 350 exc_type (str): The exception type name. | |
| 351 exc_msg (str): String representation of the exception value. | |
| 352 tb_info (TracebackInfo): Information about the stack trace of the | |
| 353 exception. | |
| 354 | |
| 355 Like the :class:`TracebackInfo`, ExceptionInfo is most commonly | |
| 356 instantiated from one of its classmethods: :meth:`from_exc_info` | |
| 357 or :meth:`from_current`. | |
| 358 """ | |
| 359 | |
| 360 #: Override this in inherited types to control the TracebackInfo type used | |
| 361 tb_info_type = TracebackInfo | |
| 362 | |
| 363 def __init__(self, exc_type, exc_msg, tb_info): | |
| 364 # TODO: additional fields for SyntaxErrors | |
| 365 self.exc_type = exc_type | |
| 366 self.exc_msg = exc_msg | |
| 367 self.tb_info = tb_info | |
| 368 | |
| 369 @classmethod | |
| 370 def from_exc_info(cls, exc_type, exc_value, traceback): | |
| 371 """Create an :class:`ExceptionInfo` object from the exception's type, | |
| 372 value, and traceback, as returned by :func:`sys.exc_info`. See | |
| 373 also :meth:`from_current`. | |
| 374 """ | |
| 375 type_str = exc_type.__name__ | |
| 376 type_mod = exc_type.__module__ | |
| 377 if type_mod not in ("__main__", "__builtin__", "exceptions", "builtins"): | |
| 378 type_str = '%s.%s' % (type_mod, type_str) | |
| 379 val_str = _some_str(exc_value) | |
| 380 tb_info = cls.tb_info_type.from_traceback(traceback) | |
| 381 return cls(type_str, val_str, tb_info) | |
| 382 | |
| 383 @classmethod | |
| 384 def from_current(cls): | |
| 385 """Create an :class:`ExceptionInfo` object from the current exception | |
| 386 being handled, by way of :func:`sys.exc_info`. Will raise an | |
| 387 exception if no exception is currently being handled. | |
| 388 """ | |
| 389 return cls.from_exc_info(*sys.exc_info()) | |
| 390 | |
| 391 def to_dict(self): | |
| 392 """Get a :class:`dict` representation of the ExceptionInfo, suitable | |
| 393 for JSON serialization. | |
| 394 """ | |
| 395 return {'exc_type': self.exc_type, | |
| 396 'exc_msg': self.exc_msg, | |
| 397 'exc_tb': self.tb_info.to_dict()} | |
| 398 | |
| 399 def __repr__(self): | |
| 400 cn = self.__class__.__name__ | |
| 401 try: | |
| 402 len_frames = len(self.tb_info.frames) | |
| 403 last_frame = ', last=%r' % (self.tb_info.frames[-1],) | |
| 404 except Exception: | |
| 405 len_frames = 0 | |
| 406 last_frame = '' | |
| 407 args = (cn, self.exc_type, self.exc_msg, len_frames, last_frame) | |
| 408 return '<%s [%s: %s] (%s frames%s)>' % args | |
| 409 | |
| 410 def get_formatted(self): | |
| 411 """Returns a string formatted in the traditional Python | |
| 412 built-in style observable when an exception is not caught. In | |
| 413 other words, mimics :func:`traceback.format_exception`. | |
| 414 """ | |
| 415 # TODO: add SyntaxError formatting | |
| 416 tb_str = self.tb_info.get_formatted() | |
| 417 return ''.join([tb_str, '%s: %s' % (self.exc_type, self.exc_msg)]) | |
| 418 | |
| 419 def get_formatted_exception_only(self): | |
| 420 return '%s: %s' % (self.exc_type, self.exc_msg) | |
| 421 | |
| 422 | |
| 423 class ContextualCallpoint(Callpoint): | |
| 424 """The ContextualCallpoint is a :class:`Callpoint` subtype with the | |
| 425 exact same API and storing two additional values: | |
| 426 | |
| 427 1. :func:`repr` outputs for local variables from the Callpoint's scope | |
| 428 2. A number of lines before and after the Callpoint's line of code | |
| 429 | |
| 430 The ContextualCallpoint is used by the :class:`ContextualTracebackInfo`. | |
| 431 """ | |
| 432 def __init__(self, *a, **kw): | |
| 433 self.local_reprs = kw.pop('local_reprs', {}) | |
| 434 self.pre_lines = kw.pop('pre_lines', []) | |
| 435 self.post_lines = kw.pop('post_lines', []) | |
| 436 super(ContextualCallpoint, self).__init__(*a, **kw) | |
| 437 | |
| 438 @classmethod | |
| 439 def from_frame(cls, frame): | |
| 440 "Identical to :meth:`Callpoint.from_frame`" | |
| 441 ret = super(ContextualCallpoint, cls).from_frame(frame) | |
| 442 ret._populate_local_reprs(frame.f_locals) | |
| 443 ret._populate_context_lines() | |
| 444 return ret | |
| 445 | |
| 446 @classmethod | |
| 447 def from_tb(cls, tb): | |
| 448 "Identical to :meth:`Callpoint.from_tb`" | |
| 449 ret = super(ContextualCallpoint, cls).from_tb(tb) | |
| 450 ret._populate_local_reprs(tb.tb_frame.f_locals) | |
| 451 ret._populate_context_lines() | |
| 452 return ret | |
| 453 | |
| 454 def _populate_context_lines(self, pivot=8): | |
| 455 DL, lineno = _DeferredLine, self.lineno | |
| 456 try: | |
| 457 module_globals = self.line.module_globals | |
| 458 except Exception: | |
| 459 module_globals = None | |
| 460 start_line = max(0, lineno - pivot) | |
| 461 pre_lines = [DL(self.module_path, ln, module_globals) | |
| 462 for ln in range(start_line, lineno)] | |
| 463 self.pre_lines[:] = pre_lines | |
| 464 post_lines = [DL(self.module_path, ln, module_globals) | |
| 465 for ln in range(lineno + 1, lineno + 1 + pivot)] | |
| 466 self.post_lines[:] = post_lines | |
| 467 return | |
| 468 | |
| 469 def _populate_local_reprs(self, f_locals): | |
| 470 local_reprs = self.local_reprs | |
| 471 for k, v in f_locals.items(): | |
| 472 try: | |
| 473 local_reprs[k] = repr(v) | |
| 474 except Exception: | |
| 475 surrogate = '<unprintable %s object>' % type(v).__name__ | |
| 476 local_reprs[k] = surrogate | |
| 477 return | |
| 478 | |
| 479 def to_dict(self): | |
| 480 """ | |
| 481 Same principle as :meth:`Callpoint.to_dict`, but with the added | |
| 482 contextual values. With ``ContextualCallpoint.to_dict()``, | |
| 483 each frame will now be represented like:: | |
| 484 | |
| 485 {'func_name': 'print_example', | |
| 486 'lineno': 0, | |
| 487 'module_name': 'example_module', | |
| 488 'module_path': '/home/example/example_module.pyc', | |
| 489 'lasti': 0, | |
| 490 'line': 'print "example"', | |
| 491 'locals': {'variable': '"value"'}, | |
| 492 'pre_lines': ['variable = "value"'], | |
| 493 'post_lines': []} | |
| 494 | |
| 495 The locals dictionary and line lists are copies and can be mutated | |
| 496 freely. | |
| 497 """ | |
| 498 ret = super(ContextualCallpoint, self).to_dict() | |
| 499 ret['locals'] = dict(self.local_reprs) | |
| 500 | |
| 501 # get the line numbers and textual lines | |
| 502 # without assuming DeferredLines | |
| 503 start_line = self.lineno - len(self.pre_lines) | |
| 504 pre_lines = [{'lineno': start_line + i, 'line': str(l)} | |
| 505 for i, l in enumerate(self.pre_lines)] | |
| 506 # trim off leading empty lines | |
| 507 for i, item in enumerate(pre_lines): | |
| 508 if item['line']: | |
| 509 break | |
| 510 if i: | |
| 511 pre_lines = pre_lines[i:] | |
| 512 ret['pre_lines'] = pre_lines | |
| 513 | |
| 514 # now post_lines | |
| 515 post_lines = [{'lineno': self.lineno + i, 'line': str(l)} | |
| 516 for i, l in enumerate(self.post_lines)] | |
| 517 _last = 0 | |
| 518 for i, item in enumerate(post_lines): | |
| 519 if item['line']: | |
| 520 _last = i | |
| 521 post_lines = post_lines[:_last + 1] | |
| 522 ret['post_lines'] = post_lines | |
| 523 return ret | |
| 524 | |
| 525 | |
| 526 class ContextualTracebackInfo(TracebackInfo): | |
| 527 """The ContextualTracebackInfo type is a :class:`TracebackInfo` | |
| 528 subtype that is used by :class:`ContextualExceptionInfo` and uses | |
| 529 the :class:`ContextualCallpoint` as its frame-representing | |
| 530 primitive. | |
| 531 """ | |
| 532 callpoint_type = ContextualCallpoint | |
| 533 | |
| 534 | |
| 535 class ContextualExceptionInfo(ExceptionInfo): | |
| 536 """The ContextualTracebackInfo type is a :class:`TracebackInfo` | |
| 537 subtype that uses the :class:`ContextualCallpoint` as its | |
| 538 frame-representing primitive. | |
| 539 | |
| 540 It carries with it most of the exception information required to | |
| 541 recreate the widely recognizable "500" page for debugging Django | |
| 542 applications. | |
| 543 """ | |
| 544 tb_info_type = ContextualTracebackInfo | |
| 545 | |
| 546 | |
| 547 # TODO: clean up & reimplement -- specifically for syntax errors | |
| 548 def format_exception_only(etype, value): | |
| 549 """Format the exception part of a traceback. | |
| 550 | |
| 551 The arguments are the exception type and value such as given by | |
| 552 sys.last_type and sys.last_value. The return value is a list of | |
| 553 strings, each ending in a newline. | |
| 554 | |
| 555 Normally, the list contains a single string; however, for | |
| 556 SyntaxError exceptions, it contains several lines that (when | |
| 557 printed) display detailed information about where the syntax | |
| 558 error occurred. | |
| 559 | |
| 560 The message indicating which exception occurred is always the last | |
| 561 string in the list. | |
| 562 | |
| 563 """ | |
| 564 # Gracefully handle (the way Python 2.4 and earlier did) the case of | |
| 565 # being called with (None, None). | |
| 566 if etype is None: | |
| 567 return [_format_final_exc_line(etype, value)] | |
| 568 | |
| 569 stype = etype.__name__ | |
| 570 smod = etype.__module__ | |
| 571 if smod not in ("__main__", "builtins", "exceptions"): | |
| 572 stype = smod + '.' + stype | |
| 573 | |
| 574 if not issubclass(etype, SyntaxError): | |
| 575 return [_format_final_exc_line(stype, value)] | |
| 576 | |
| 577 # It was a syntax error; show exactly where the problem was found. | |
| 578 lines = [] | |
| 579 filename = value.filename or "<string>" | |
| 580 lineno = str(value.lineno) or '?' | |
| 581 lines.append(' File "%s", line %s\n' % (filename, lineno)) | |
| 582 badline = value.text | |
| 583 offset = value.offset | |
| 584 if badline is not None: | |
| 585 lines.append(' %s\n' % badline.strip()) | |
| 586 if offset is not None: | |
| 587 caretspace = badline.rstrip('\n')[:offset].lstrip() | |
| 588 # non-space whitespace (likes tabs) must be kept for alignment | |
| 589 caretspace = ((c.isspace() and c or ' ') for c in caretspace) | |
| 590 # only three spaces to account for offset1 == pos 0 | |
| 591 lines.append(' %s^\n' % ''.join(caretspace)) | |
| 592 msg = value.msg or "<no detail available>" | |
| 593 lines.append("%s: %s\n" % (stype, msg)) | |
| 594 return lines | |
| 595 | |
| 596 | |
| 597 # TODO: use asciify, improved if necessary | |
| 598 def _some_str(value): | |
| 599 try: | |
| 600 return str(value) | |
| 601 except Exception: | |
| 602 pass | |
| 603 try: | |
| 604 value = text(value) | |
| 605 return value.encode("ascii", "backslashreplace") | |
| 606 except Exception: | |
| 607 pass | |
| 608 return '<unprintable %s object>' % type(value).__name__ | |
| 609 | |
| 610 | |
| 611 def _format_final_exc_line(etype, value): | |
| 612 valuestr = _some_str(value) | |
| 613 if value is None or not valuestr: | |
| 614 line = "%s\n" % etype | |
| 615 else: | |
| 616 line = "%s: %s\n" % (etype, valuestr) | |
| 617 return line | |
| 618 | |
| 619 | |
| 620 def print_exception(etype, value, tb, limit=None, file=None): | |
| 621 """Print exception up to 'limit' stack trace entries from 'tb' to 'file'. | |
| 622 | |
| 623 This differs from print_tb() in the following ways: (1) if | |
| 624 traceback is not None, it prints a header "Traceback (most recent | |
| 625 call last):"; (2) it prints the exception type and value after the | |
| 626 stack trace; (3) if type is SyntaxError and value has the | |
| 627 appropriate format, it prints the line where the syntax error | |
| 628 occurred with a caret on the next line indicating the approximate | |
| 629 position of the error. | |
| 630 """ | |
| 631 | |
| 632 if file is None: | |
| 633 file = sys.stderr | |
| 634 if tb: | |
| 635 tbi = TracebackInfo.from_traceback(tb, limit) | |
| 636 print(str(tbi), end='', file=file) | |
| 637 | |
| 638 for line in format_exception_only(etype, value): | |
| 639 print(line, end='', file=file) | |
| 640 | |
| 641 | |
| 642 def fix_print_exception(): | |
| 643 """ | |
| 644 Sets the default exception hook :func:`sys.excepthook` to the | |
| 645 :func:`tbutils.print_exception` that uses all the ``tbutils`` | |
| 646 facilities to provide slightly more correct output behavior. | |
| 647 """ | |
| 648 sys.excepthook = print_exception | |
| 649 | |
| 650 | |
| 651 _frame_re = re.compile(r'^File "(?P<filepath>.+)", line (?P<lineno>\d+)' | |
| 652 r', in (?P<funcname>.+)$') | |
| 653 _se_frame_re = re.compile(r'^File "(?P<filepath>.+)", line (?P<lineno>\d+)') | |
| 654 | |
| 655 | |
| 656 # TODO: ParsedException generator over large bodies of text | |
| 657 | |
| 658 class ParsedException(object): | |
| 659 """Stores a parsed traceback and exception as would be typically | |
| 660 output by :func:`sys.excepthook` or | |
| 661 :func:`traceback.print_exception`. | |
| 662 | |
| 663 .. note: | |
| 664 | |
| 665 Does not currently store SyntaxError details such as column. | |
| 666 | |
| 667 """ | |
| 668 def __init__(self, exc_type_name, exc_msg, frames=None): | |
| 669 self.exc_type = exc_type_name | |
| 670 self.exc_msg = exc_msg | |
| 671 self.frames = list(frames or []) | |
| 672 | |
| 673 @property | |
| 674 def source_file(self): | |
| 675 """ | |
| 676 The file path of module containing the function that raised the | |
| 677 exception, or None if not available. | |
| 678 """ | |
| 679 try: | |
| 680 return self.frames[-1]['filepath'] | |
| 681 except IndexError: | |
| 682 return None | |
| 683 | |
| 684 def to_dict(self): | |
| 685 "Get a copy as a JSON-serializable :class:`dict`." | |
| 686 return {'exc_type': self.exc_type, | |
| 687 'exc_msg': self.exc_msg, | |
| 688 'frames': list(self.frames)} | |
| 689 | |
| 690 def __repr__(self): | |
| 691 cn = self.__class__.__name__ | |
| 692 return ('%s(%r, %r, frames=%r)' | |
| 693 % (cn, self.exc_type, self.exc_msg, self.frames)) | |
| 694 | |
| 695 def to_string(self): | |
| 696 """Formats the exception and its traceback into the standard format, | |
| 697 as returned by the traceback module. | |
| 698 | |
| 699 ``ParsedException.from_string(text).to_string()`` should yield | |
| 700 ``text``. | |
| 701 """ | |
| 702 lines = [u'Traceback (most recent call last):'] | |
| 703 | |
| 704 for frame in self.frames: | |
| 705 lines.append(u' File "%s", line %s, in %s' % (frame['filepath'], | |
| 706 frame['lineno'], | |
| 707 frame['funcname'])) | |
| 708 source_line = frame.get('source_line') | |
| 709 if source_line: | |
| 710 lines.append(u' %s' % (source_line,)) | |
| 711 if self.exc_msg: | |
| 712 lines.append(u'%s: %s' % (self.exc_type, self.exc_msg)) | |
| 713 else: | |
| 714 lines.append(u'%s' % (self.exc_type,)) | |
| 715 return u'\n'.join(lines) | |
| 716 | |
| 717 @classmethod | |
| 718 def from_string(cls, tb_str): | |
| 719 """Parse a traceback and exception from the text *tb_str*. This text | |
| 720 is expected to have been decoded, otherwise it will be | |
| 721 interpreted as UTF-8. | |
| 722 | |
| 723 This method does not search a larger body of text for | |
| 724 tracebacks. If the first line of the text passed does not | |
| 725 match one of the known patterns, a :exc:`ValueError` will be | |
| 726 raised. This method will ignore trailing text after the end of | |
| 727 the first traceback. | |
| 728 | |
| 729 Args: | |
| 730 tb_str (str): The traceback text (:class:`unicode` or UTF-8 bytes) | |
| 731 """ | |
| 732 if not isinstance(tb_str, text): | |
| 733 tb_str = tb_str.decode('utf-8') | |
| 734 tb_lines = tb_str.lstrip().splitlines() | |
| 735 | |
| 736 # First off, handle some ignored exceptions. These can be the | |
| 737 # result of exceptions raised by __del__ during garbage | |
| 738 # collection | |
| 739 while tb_lines: | |
| 740 cl = tb_lines[-1] | |
| 741 if cl.startswith('Exception ') and cl.endswith('ignored'): | |
| 742 tb_lines.pop() | |
| 743 else: | |
| 744 break | |
| 745 if tb_lines and tb_lines[0].strip() == 'Traceback (most recent call last):': | |
| 746 start_line = 1 | |
| 747 frame_re = _frame_re | |
| 748 elif len(tb_lines) > 1 and tb_lines[-2].lstrip().startswith('^'): | |
| 749 # This is to handle the slight formatting difference | |
| 750 # associated with SyntaxErrors, which also don't really | |
| 751 # have tracebacks | |
| 752 start_line = 0 | |
| 753 frame_re = _se_frame_re | |
| 754 else: | |
| 755 raise ValueError('unrecognized traceback string format') | |
| 756 | |
| 757 frames = [] | |
| 758 line_no = start_line | |
| 759 while True: | |
| 760 frame_line = tb_lines[line_no].strip() | |
| 761 frame_match = frame_re.match(frame_line) | |
| 762 if frame_match: | |
| 763 frame_dict = frame_match.groupdict() | |
| 764 try: | |
| 765 next_line = tb_lines[line_no + 1] | |
| 766 except IndexError: | |
| 767 # We read what we could | |
| 768 next_line = '' | |
| 769 next_line_stripped = next_line.strip() | |
| 770 if ( | |
| 771 frame_re.match(next_line_stripped) or | |
| 772 # The exception message will not be indented | |
| 773 # This check is to avoid overrunning on eval-like | |
| 774 # tracebacks where the last frame doesn't have source | |
| 775 # code in the traceback | |
| 776 not next_line.startswith(' ') | |
| 777 ): | |
| 778 frame_dict['source_line'] = '' | |
| 779 else: | |
| 780 frame_dict['source_line'] = next_line_stripped | |
| 781 line_no += 1 | |
| 782 else: | |
| 783 break | |
| 784 line_no += 1 | |
| 785 frames.append(frame_dict) | |
| 786 | |
| 787 try: | |
| 788 exc_line = '\n'.join(tb_lines[line_no:]) | |
| 789 exc_type, _, exc_msg = exc_line.partition(': ') | |
| 790 except Exception: | |
| 791 exc_type, exc_msg = '', '' | |
| 792 | |
| 793 return cls(exc_type, exc_msg, frames) | |
| 794 | |
| 795 | |
| 796 ParsedTB = ParsedException # legacy alias |
