This file is indexed.

/usr/lib/python3/dist-packages/boltons/tbutils.py is in python3-boltons 17.1.0-1.

This file is owned by root:root, with mode 0o644.

The actual contents of the file can be viewed below.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
# -*- coding: utf-8 -*-
"""One of the oft-cited tenets of Python is that it is better to ask
forgiveness than permission. That is, there are many cases where it is
more inclusive and correct to handle exceptions than spend extra lines
and execution time checking for conditions. This philosophy makes good
exception handling features all the more important. Unfortunately
Python's :mod:`traceback` module is woefully behind the times.

The ``tbutils`` module provides two disparate but complementary featuresets:

  1. With :class:`ExceptionInfo` and :class:`TracebackInfo`, the
     ability to extract, construct, manipulate, format, and serialize
     exceptions, tracebacks, and callstacks.
  2. With :class:`ParsedException`, the ability to find and parse tracebacks
     from captured output such as logs and stdout.

There is also the :class:`ContextualTracebackInfo` variant of
:class:`TracebackInfo`, which includes much more information from each
frame of the callstack, including values of locals and neighboring
lines of code.
"""

from __future__ import print_function

import re
import sys
import linecache


if str is bytes:  # py2
    text = unicode
else:  # py3
    text = str


# TODO: chaining primitives?  what are real use cases where these help?

# TODO: print_* for backwards compatability
# __all__ = ['extract_stack', 'extract_tb', 'format_exception',
#            'format_exception_only', 'format_list', 'format_stack',
#            'format_tb', 'print_exc', 'format_exc', 'print_exception',
#            'print_last', 'print_stack', 'print_tb']


__all__ = ['ExceptionInfo', 'TracebackInfo', 'Callpoint',
           'ContextualExceptionInfo', 'ContextualTracebackInfo',
           'ContextualCallpoint', 'print_exception', 'ParsedException']


class Callpoint(object):
    """The Callpoint is a lightweight object used to represent a single
    entry in the code of a call stack. It stores the code-related
    metadata of a given frame. Available attributes are the same as
    the parameters below.

    Args:
        func_name (str): the function name
        lineno (int): the line number
        module_name (str): the module name
        module_path (str): the filesystem path of the module
        lasti (int): the index of bytecode execution
        line (str): the single-line code content (if available)

    """
    __slots__ = ('func_name', 'lineno', 'module_name', 'module_path', 'lasti',
                 'line')

    def __init__(self, module_name, module_path, func_name,
                 lineno, lasti, line=None):
        self.func_name = func_name
        self.lineno = lineno
        self.module_name = module_name
        self.module_path = module_path
        self.lasti = lasti
        self.line = line

    def to_dict(self):
        "Get a :class:`dict` copy of the Callpoint. Useful for serialization."
        ret = {}
        for slot in self.__slots__:
            try:
                val = getattr(self, slot)
            except AttributeError:
                pass
            else:
                ret[slot] = str(val) if isinstance(val, _DeferredLine) else val
        return ret

    @classmethod
    def from_current(cls, level=1):
        "Creates a Callpoint from the location of the calling function."
        frame = sys._getframe(level)
        return cls.from_frame(frame)

    @classmethod
    def from_frame(cls, frame):
        "Create a Callpoint object from data extracted from the given frame."
        func_name = frame.f_code.co_name
        lineno = frame.f_lineno
        module_name = frame.f_globals.get('__name__', '')
        module_path = frame.f_code.co_filename
        lasti = frame.f_lasti
        line = _DeferredLine(module_path, lineno, frame.f_globals)
        return cls(module_name, module_path, func_name,
                   lineno, lasti, line=line)

    @classmethod
    def from_tb(cls, tb):
        """Create a Callpoint from the traceback of the current
        exception. Main difference with :meth:`from_frame` is that
        ``lineno`` and ``lasti`` come from the traceback, which is to
        say the line that failed in the try block, not the line
        currently being executed (in the except block).
        """
        func_name = tb.tb_frame.f_code.co_name
        lineno = tb.tb_lineno
        lasti = tb.tb_lasti
        module_name = tb.tb_frame.f_globals.get('__name__', '')
        module_path = tb.tb_frame.f_code.co_filename
        line = _DeferredLine(module_path, lineno, tb.tb_frame.f_globals)
        return cls(module_name, module_path, func_name,
                   lineno, lasti, line=line)

    def __repr__(self):
        cn = self.__class__.__name__
        args = [getattr(self, s, None) for s in self.__slots__]
        if not any(args):
            return super(Callpoint, self).__repr__()
        else:
            return '%s(%s)' % (cn, ', '.join([repr(a) for a in args]))

    def tb_frame_str(self):
        """Render the Callpoint as it would appear in a standard printed
        Python traceback. Returns a string with filename, line number,
        function name, and the actual code line of the error on up to
        two lines.
        """
        ret = '  File "%s", line %s, in %s\n' % (self.module_path,
                                                 self.lineno,
                                                 self.func_name)
        if self.line:
            ret += '    %s\n' % (str(self.line).strip(),)
        return ret


class _DeferredLine(object):
    """The _DeferredLine type allows Callpoints and TracebackInfos to be
    constructed without potentially hitting the filesystem, as is the
    normal behavior of the standard Python :mod:`traceback` and
    :mod:`linecache` modules. Calling :func:`str` fetches and caches
    the line.

    Args:
        filename (str): the path of the file containing the line
        lineno (int): the number of the line in question
        module_globals (dict): an optional dict of module globals,
            used to handle advanced use cases using custom module loaders.

    """
    def __init__(self, filename, lineno, module_globals=None):
        self.filename = filename
        self.lineno = lineno
        # TODO: this is going away when we fix linecache
        # TODO: (mark) read about loader
        self.module_globals = {}
        if module_globals is not None:
            for k in ('__name__', '__loader__'):
                v = module_globals.get(k)
                if v is None:
                    self.module_globals[k] = v

    def __eq__(self, other):
        return (self.lineno, self.filename) == (other.lineno, other.filename)

    def __ne__(self, other):
        return not self == other

    def __str__(self):
        if hasattr(self, '_line'):
            return self._line
        try:
            linecache.checkcache(self.filename)
            line = linecache.getline(self.filename,
                                     self.lineno,
                                     self.module_globals)
            line = line.rstrip()
        except KeyError:
            line = ''
        self._line = line
        return line

    def __repr__(self):
        return repr(str(self))

    def __len__(self):
        return len(str(self))


# TODO: dedup frames, look at __eq__ on _DeferredLine
class TracebackInfo(object):
    """The TracebackInfo class provides a basic representation of a stack
    trace, be it from an exception being handled or just part of
    normal execution. It is basically a wrapper around a list of
    :class:`Callpoint` objects representing frames.

    Args:
        frames (list): A list of frame objects in the stack.

    .. note ::

      ``TracebackInfo`` can represent both exception tracebacks and
      non-exception tracebacks (aka stack traces). As a result, there
      is no ``TracebackInfo.from_current()``, as that would be
      ambiguous. Instead, call :meth:`TracebackInfo.from_frame`
      without the *frame* argument for a stack trace, or
      :meth:`TracebackInfo.from_traceback` without the *tb* argument
      for an exception traceback.
    """
    callpoint_type = Callpoint

    def __init__(self, frames):
        self.frames = frames

    @classmethod
    def from_frame(cls, frame=None, level=1, limit=None):
        """Create a new TracebackInfo *frame* by recurring up in the stack a
        max of *limit* times. If *frame* is unset, get the frame from
        :func:`sys._getframe` using *level*.

        Args:
            frame (types.FrameType): frame object from
                :func:`sys._getframe` or elsewhere. Defaults to result
                of :func:`sys.get_frame`.
            level (int): If *frame* is unset, the desired frame is
                this many levels up the stack from the invocation of
                this method. Default ``1`` (i.e., caller of this method).
            limit (int): max number of parent frames to extract
                (defaults to :data:`sys.tracebacklimit`)

        """
        ret = []
        if frame is None:
            frame = sys._getframe(1)
        if limit is None:
            limit = getattr(sys, 'tracebacklimit', 1000)
        n = 0
        while frame is not None and n < limit:
            item = cls.callpoint_type.from_frame(frame)
            ret.append(item)
            frame = frame.f_back
            n += 1
        ret.reverse()
        return cls(ret)

    @classmethod
    def from_traceback(cls, tb=None, limit=None):
        """Create a new TracebackInfo from the traceback *tb* by recurring
        up in the stack a max of *limit* times. If *tb* is unset, get
        the traceback from the currently handled exception. If no
        exception is being handled, raise a :exc:`ValueError`.

        Args:

            frame (types.TracebackType): traceback object from
                :func:`sys.exc_info` or elsewhere. If absent or set to
                ``None``, defaults to ``sys.exc_info()[2]``, and
                raises a :exc:`ValueError` if no exception is
                currently being handled.
            limit (int): max number of parent frames to extract
                (defaults to :data:`sys.tracebacklimit`)

        """
        ret = []
        if tb is None:
            tb = sys.exc_info()[2]
            if tb is None:
                raise ValueError('no tb set and no exception being handled')
        if limit is None:
            limit = getattr(sys, 'tracebacklimit', 1000)
        n = 0
        while tb is not None and n < limit:
            item = cls.callpoint_type.from_tb(tb)
            ret.append(item)
            tb = tb.tb_next
            n += 1
        return cls(ret)

    @classmethod
    def from_dict(cls, d):
        "Complements :meth:`TracebackInfo.to_dict`."
        # TODO: check this.
        return cls(d['frames'])

    def to_dict(self):
        """Returns a dict with a list of :class:`Callpoint` frames converted
        to dicts.
        """
        return {'frames': [f.to_dict() for f in self.frames]}

    def __len__(self):
        return len(self.frames)

    def __iter__(self):
        return iter(self.frames)

    def __repr__(self):
        cn = self.__class__.__name__

        if self.frames:
            frame_part = ' last=%r' % (self.frames[-1],)
        else:
            frame_part = ''

        return '<%s frames=%s%s>' % (cn, len(self.frames), frame_part)

    def __str__(self):
        return self.get_formatted()

    def get_formatted(self):
        """Returns a string as formatted in the traditional Python
        built-in style observable when an exception is not caught. In
        other words, mimics :func:`traceback.format_tb` and
        :func:`traceback.format_stack`.
        """
        ret = 'Traceback (most recent call last):\n'
        ret += ''.join([f.tb_frame_str() for f in self.frames])
        return ret


class ExceptionInfo(object):
    """An ExceptionInfo object ties together three main fields suitable
    for representing an instance of an exception: The exception type
    name, a string representation of the exception itself (the
    exception message), and information about the traceback (stored as
    a :class:`TracebackInfo` object).

    These fields line up with :func:`sys.exc_info`, but unlike the
    values returned by that function, ExceptionInfo does not hold any
    references to the real exception or traceback. This property makes
    it suitable for serialization or long-term retention, without
    worrying about formatting pitfalls, circular references, or leaking memory.

    Args:

        exc_type (str): The exception type name.
        exc_msg (str): String representation of the exception value.
        tb_info (TracebackInfo): Information about the stack trace of the
            exception.

    Like the :class:`TracebackInfo`, ExceptionInfo is most commonly
    instantiated from one of its classmethods: :meth:`from_exc_info`
    or :meth:`from_current`.
    """

    #: Override this in inherited types to control the TracebackInfo type used
    tb_info_type = TracebackInfo

    def __init__(self, exc_type, exc_msg, tb_info):
        # TODO: additional fields for SyntaxErrors
        self.exc_type = exc_type
        self.exc_msg = exc_msg
        self.tb_info = tb_info

    @classmethod
    def from_exc_info(cls, exc_type, exc_value, traceback):
        """Create an :class:`ExceptionInfo` object from the exception's type,
        value, and traceback, as returned by :func:`sys.exc_info`. See
        also :meth:`from_current`.
        """
        type_str = exc_type.__name__
        type_mod = exc_type.__module__
        if type_mod not in ("__main__", "__builtin__", "exceptions", "builtins"):
            type_str = '%s.%s' % (type_mod, type_str)
        val_str = _some_str(exc_value)
        tb_info = cls.tb_info_type.from_traceback(traceback)
        return cls(type_str, val_str, tb_info)

    @classmethod
    def from_current(cls):
        """Create an :class:`ExceptionInfo` object from the current exception
        being handled, by way of :func:`sys.exc_info`. Will raise an
        exception if no exception is currently being handled.
        """
        return cls.from_exc_info(*sys.exc_info())

    def to_dict(self):
        """Get a :class:`dict` representation of the ExceptionInfo, suitable
        for JSON serialization.
        """
        return {'exc_type': self.exc_type,
                'exc_msg': self.exc_msg,
                'exc_tb': self.tb_info.to_dict()}

    def __repr__(self):
        cn = self.__class__.__name__
        try:
            len_frames = len(self.tb_info.frames)
            last_frame = ', last=%r' % (self.tb_info.frames[-1],)
        except Exception:
            len_frames = 0
            last_frame = ''
        args = (cn, self.exc_type, self.exc_msg, len_frames, last_frame)
        return '<%s [%s: %s] (%s frames%s)>' % args

    def get_formatted(self):
        """Returns a string formatted in the traditional Python
        built-in style observable when an exception is not caught. In
        other words, mimics :func:`traceback.format_exception`.
        """
        # TODO: add SyntaxError formatting
        tb_str = self.tb_info.get_formatted()
        return ''.join([tb_str, '%s: %s' % (self.exc_type, self.exc_msg)])

    def get_formatted_exception_only(self):
        return '%s: %s' % (self.exc_type, self.exc_msg)


class ContextualCallpoint(Callpoint):
    """The ContextualCallpoint is a :class:`Callpoint` subtype with the
    exact same API and storing two additional values:

      1. :func:`repr` outputs for local variables from the Callpoint's scope
      2. A number of lines before and after the Callpoint's line of code

    The ContextualCallpoint is used by the :class:`ContextualTracebackInfo`.
    """
    def __init__(self, *a, **kw):
        self.local_reprs = kw.pop('local_reprs', {})
        self.pre_lines = kw.pop('pre_lines', [])
        self.post_lines = kw.pop('post_lines', [])
        super(ContextualCallpoint, self).__init__(*a, **kw)

    @classmethod
    def from_frame(cls, frame):
        "Identical to :meth:`Callpoint.from_frame`"
        ret = super(ContextualCallpoint, cls).from_frame(frame)
        ret._populate_local_reprs(frame.f_locals)
        ret._populate_context_lines()
        return ret

    @classmethod
    def from_tb(cls, tb):
        "Identical to :meth:`Callpoint.from_tb`"
        ret = super(ContextualCallpoint, cls).from_tb(tb)
        ret._populate_local_reprs(tb.tb_frame.f_locals)
        ret._populate_context_lines()
        return ret

    def _populate_context_lines(self, pivot=8):
        DL, lineno = _DeferredLine, self.lineno
        try:
            module_globals = self.line.module_globals
        except Exception:
            module_globals = None
        start_line = max(0, lineno - pivot)
        pre_lines = [DL(self.module_path, ln, module_globals)
                     for ln in range(start_line, lineno)]
        self.pre_lines[:] = pre_lines
        post_lines = [DL(self.module_path, ln, module_globals)
                      for ln in range(lineno + 1, lineno + 1 + pivot)]
        self.post_lines[:] = post_lines
        return

    def _populate_local_reprs(self, f_locals):
        local_reprs = self.local_reprs
        for k, v in f_locals.items():
            try:
                local_reprs[k] = repr(v)
            except Exception:
                surrogate = '<unprintable %s object>' % type(v).__name__
                local_reprs[k] = surrogate
        return

    def to_dict(self):
        """
        Same principle as :meth:`Callpoint.to_dict`, but with the added
        contextual values. With ``ContextualCallpoint.to_dict()``,
        each frame will now be represented like::

          {'func_name': 'print_example',
           'lineno': 0,
           'module_name': 'example_module',
           'module_path': '/home/example/example_module.pyc',
           'lasti': 0,
           'line': 'print "example"',
           'locals': {'variable': '"value"'},
           'pre_lines': ['variable = "value"'],
           'post_lines': []}

        The locals dictionary and line lists are copies and can be mutated
        freely.
        """
        ret = super(ContextualCallpoint, self).to_dict()
        ret['locals'] = dict(self.local_reprs)

        # get the line numbers and textual lines
        # without assuming DeferredLines
        start_line = self.lineno - len(self.pre_lines)
        pre_lines = [{'lineno': start_line + i, 'line': str(l)}
                     for i, l in enumerate(self.pre_lines)]
        # trim off leading empty lines
        for i, item in enumerate(pre_lines):
            if item['line']:
                break
        if i:
            pre_lines = pre_lines[i:]
        ret['pre_lines'] = pre_lines

        # now post_lines
        post_lines = [{'lineno': self.lineno + i, 'line': str(l)}
                      for i, l in enumerate(self.post_lines)]
        _last = 0
        for i, item in enumerate(post_lines):
            if item['line']:
                _last = i
        post_lines = post_lines[:_last + 1]
        ret['post_lines'] = post_lines
        return ret


class ContextualTracebackInfo(TracebackInfo):
    """The ContextualTracebackInfo type is a :class:`TracebackInfo`
    subtype that is used by :class:`ContextualExceptionInfo` and uses
    the :class:`ContextualCallpoint` as its frame-representing
    primitive.
    """
    callpoint_type = ContextualCallpoint


class ContextualExceptionInfo(ExceptionInfo):
    """The ContextualTracebackInfo type is a :class:`TracebackInfo`
    subtype that uses the :class:`ContextualCallpoint` as its
    frame-representing primitive.

    It carries with it most of the exception information required to
    recreate the widely recognizable "500" page for debugging Django
    applications.
    """
    tb_info_type = ContextualTracebackInfo


# TODO: clean up & reimplement -- specifically for syntax errors
def format_exception_only(etype, value):
    """Format the exception part of a traceback.

    The arguments are the exception type and value such as given by
    sys.last_type and sys.last_value. The return value is a list of
    strings, each ending in a newline.

    Normally, the list contains a single string; however, for
    SyntaxError exceptions, it contains several lines that (when
    printed) display detailed information about where the syntax
    error occurred.

    The message indicating which exception occurred is always the last
    string in the list.

    """
    # Gracefully handle (the way Python 2.4 and earlier did) the case of
    # being called with (None, None).
    if etype is None:
        return [_format_final_exc_line(etype, value)]

    stype = etype.__name__
    smod = etype.__module__
    if smod not in ("__main__", "builtins", "exceptions"):
        stype = smod + '.' + stype

    if not issubclass(etype, SyntaxError):
        return [_format_final_exc_line(stype, value)]

    # It was a syntax error; show exactly where the problem was found.
    lines = []
    filename = value.filename or "<string>"
    lineno = str(value.lineno) or '?'
    lines.append('  File "%s", line %s\n' % (filename, lineno))
    badline = value.text
    offset = value.offset
    if badline is not None:
        lines.append('    %s\n' % badline.strip())
        if offset is not None:
            caretspace = badline.rstrip('\n')[:offset].lstrip()
            # non-space whitespace (likes tabs) must be kept for alignment
            caretspace = ((c.isspace() and c or ' ') for c in caretspace)
            # only three spaces to account for offset1 == pos 0
            lines.append('   %s^\n' % ''.join(caretspace))
    msg = value.msg or "<no detail available>"
    lines.append("%s: %s\n" % (stype, msg))
    return lines


# TODO: use asciify, improved if necessary
def _some_str(value):
    try:
        return str(value)
    except Exception:
        pass
    try:
        value = unicode(value)
        return value.encode("ascii", "backslashreplace")
    except Exception:
        pass
    return '<unprintable %s object>' % type(value).__name__


def _format_final_exc_line(etype, value):
    valuestr = _some_str(value)
    if value is None or not valuestr:
        line = "%s\n" % etype
    else:
        line = "%s: %s\n" % (etype, valuestr)
    return line


def print_exception(etype, value, tb, limit=None, file=None):
    """Print exception up to 'limit' stack trace entries from 'tb' to 'file'.

    This differs from print_tb() in the following ways: (1) if
    traceback is not None, it prints a header "Traceback (most recent
    call last):"; (2) it prints the exception type and value after the
    stack trace; (3) if type is SyntaxError and value has the
    appropriate format, it prints the line where the syntax error
    occurred with a caret on the next line indicating the approximate
    position of the error.
    """

    if file is None:
        file = sys.stderr
    if tb:
        tbi = TracebackInfo.from_traceback(tb, limit)
        print(str(tbi), end='', file=file)

    for line in format_exception_only(etype, value):
        print(line, end='', file=file)


def fix_print_exception():
    """
    Sets the default exception hook :func:`sys.excepthook` to the
    :func:`tbutils.print_exception` that uses all the ``tbutils``
    facilities to provide slightly more correct output behavior.
    """
    sys.excepthook = print_exception


_frame_re = re.compile(r'^File "(?P<filepath>.+)", line (?P<lineno>\d+)'
                       r', in (?P<funcname>.+)$')
_se_frame_re = re.compile(r'^File "(?P<filepath>.+)", line (?P<lineno>\d+)')


# TODO: ParsedException generator over large bodies of text

class ParsedException(object):
    """Stores a parsed traceback and exception as would be typically
    output by :func:`sys.excepthook` or
    :func:`traceback.print_exception`.

    .. note:

       Does not currently store SyntaxError details such as column.

    """
    def __init__(self, exc_type_name, exc_msg, frames=None):
        self.exc_type = exc_type_name
        self.exc_msg = exc_msg
        self.frames = list(frames or [])

    @property
    def source_file(self):
        """
        The file path of module containing the function that raised the
        exception, or None if not available.
        """
        try:
            return self.frames[-1]['filepath']
        except IndexError:
            return None

    def to_dict(self):
        "Get a copy as a JSON-serializable :class:`dict`."
        return {'exc_type': self.exc_type,
                'exc_msg': self.exc_msg,
                'frames': list(self.frames)}

    def __repr__(self):
        cn = self.__class__.__name__
        return ('%s(%r, %r, frames=%r)'
                % (cn, self.exc_type, self.exc_msg, self.frames))

    def to_string(self):
        """Formats the exception and its traceback into the standard format,
        as returned by the traceback module.

        ``ParsedException.from_string(text).to_string()`` should yield
        ``text``.
        """
        lines = [u'Traceback (most recent call last):']

        for frame in self.frames:
            lines.append(u'  File "%s", line %s, in %s' % (frame['filepath'],
                                                           frame['lineno'],
                                                           frame['funcname']))
            source_line = frame.get('source_line')
            if source_line:
                lines.append(u'    %s' % (source_line,))
        if self.exc_msg:
            lines.append(u'%s: %s' % (self.exc_type, self.exc_msg))
        else:
            lines.append(u'%s' % (self.exc_type,))
        return u'\n'.join(lines)

    @classmethod
    def from_string(cls, tb_str):
        """Parse a traceback and exception from the text *tb_str*. This text
        is expected to have been decoded, otherwise it will be
        interpreted as UTF-8.

        This method does not search a larger body of text for
        tracebacks. If the first line of the text passed does not
        match one of the known patterns, a :exc:`ValueError` will be
        raised. This method will ignore trailing text after the end of
        the first traceback.

        Args:
            tb_str (str): The traceback text (:class:`unicode` or UTF-8 bytes)
        """
        if not isinstance(tb_str, text):
            tb_str = tb_str.decode('utf-8')
        tb_lines = tb_str.lstrip().splitlines()

        # First off, handle some ignored exceptions. These can be the
        # result of exceptions raised by __del__ during garbage
        # collection
        while tb_lines:
            cl = tb_lines[-1]
            if cl.startswith('Exception ') and cl.endswith('ignored'):
                tb_lines.pop()
            else:
                break
        if tb_lines and tb_lines[0].strip() == 'Traceback (most recent call last):':
            start_line = 1
            frame_re = _frame_re
        elif len(tb_lines) > 1 and tb_lines[-2].lstrip().startswith('^'):
            # This is to handle the slight formatting difference
            # associated with SyntaxErrors, which also don't really
            # have tracebacks
            start_line = 0
            frame_re = _se_frame_re
        else:
            raise ValueError('unrecognized traceback string format')

        frames = []
        line_no = start_line
        while True:
            frame_line = tb_lines[line_no].strip()
            frame_match = frame_re.match(frame_line)
            if frame_match:
                frame_dict = frame_match.groupdict()
                try:
                    next_line = tb_lines[line_no + 1]
                except IndexError:
                    # We read what we could
                    next_line = ''
                next_line_stripped = next_line.strip()
                if (
                        frame_re.match(next_line_stripped) or
                        # The exception message will not be indented
                        # This check is to avoid overrunning on eval-like
                        # tracebacks where the last frame doesn't have source
                        # code in the traceback
                        not next_line.startswith(' ')
                ):
                    frame_dict['source_line'] = ''
                else:
                    frame_dict['source_line'] = next_line_stripped
                    line_no += 1
            else:
                break
            line_no += 1
            frames.append(frame_dict)

        try:
            exc_line = '\n'.join(tb_lines[line_no:])
            exc_type, _, exc_msg = exc_line.partition(': ')
        except Exception:
            exc_type, exc_msg = '', ''

        return cls(exc_type, exc_msg, frames)


ParsedTB = ParsedException  # legacy alias