~launchpad-pqm/launchpad/devel

13723.1.1 by Gary Poster
incremental commit
1
# Copyright 2011 Canonical Ltd.  This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
3
4
"""Replacement for some standard library traceback module functions.
5
6
These honor traceback supplements as defined in zope.exceptions.
7
"""
8
9
__metaclass__ = type
10
__all__ = [
11
    'extract_stack',
13723.1.3 by Gary Poster
tests and implementation for sqltrace profile code
12
    'extract_tb',
13723.1.1 by Gary Poster
incremental commit
13
    'format_list',
14
    'print_list',
15
    'print_stack',
16
    ]
17
18
import linecache
19
import sys
20
import traceback
21
14550.1.1 by Steve Kowalik
Run format-imports over lib/lp and lib/canonical/launchpad
22
13723.1.1 by Gary Poster
incremental commit
23
DEBUG_EXCEPTION_FORMATTER = False
13776.2.3 by Gary Poster
respond to review
24
EXPLOSIVE_ERRORS = (SystemExit, MemoryError, KeyboardInterrupt)
13723.1.1 by Gary Poster
incremental commit
25
26
13776.2.1 by Gary Poster
fix ++profile++sqltrace for pages that have traceback hints that must be rendered immediately, like the root page of the site.
27
def _try_except(callable, *args, **kwargs):
28
    try:
29
        return callable(*args, **kwargs)
13776.2.3 by Gary Poster
respond to review
30
    except EXPLOSIVE_ERRORS:
13776.2.1 by Gary Poster
fix ++profile++sqltrace for pages that have traceback hints that must be rendered immediately, like the root page of the site.
31
        raise
32
    except:
33
        if DEBUG_EXCEPTION_FORMATTER:
34
            traceback.print_exc()
35
    # return None
36
37
13723.1.1 by Gary Poster
incremental commit
38
def _get_frame(f):
39
    "if the frame is None, make one."
40
    if f is None:
41
        try:
42
            raise ZeroDivisionError
43
        except ZeroDivisionError:
44
            f = sys.exc_info()[2].tb_frame.f_back.f_back
45
    return f
46
47
48
def _fmt(string):
49
    "Return the string as deemed suitable for the extra information."
50
    return '   - %s' % string
51
52
53
def print_list(extracted_list, file=None):
54
    """Print the list of tuples as returned by extract_tb() or
55
    extract_stack() as a formatted stack trace to the given file."""
56
    if file is None:
57
        file = sys.stderr
58
    for line in format_list(extracted_list):
59
        file.write(line)
60
61
62
def format_list(extracted_list):
63
    """Format a list of traceback entry tuples for printing.
64
65
    Given a list of tuples as returned by extract_tb() or
66
    extract_stack(), return a list of strings ready for printing.
67
    Each string in the resulting list corresponds to the item with the
68
    same index in the argument list.  Each string ends in a newline;
69
    the strings may contain internal newlines as well, for those items
13723.1.3 by Gary Poster
tests and implementation for sqltrace profile code
70
    whose source text line or supplement or info are not None.
13723.1.1 by Gary Poster
incremental commit
71
    """
72
    list = []
13723.1.3 by Gary Poster
tests and implementation for sqltrace profile code
73
    for filename, lineno, name, line, modname, supp, info in extracted_list:
13723.1.1 by Gary Poster
incremental commit
74
        item = []
75
        item.append(
76
               '  File "%s", line %d, in %s' % (filename, lineno, name))
77
        if line:
78
            item.append('    %s' % line.strip())
79
        # The "supp" and "info" bits are adapted from zope.exceptions.
13723.1.3 by Gary Poster
tests and implementation for sqltrace profile code
80
        try:
81
            if supp:
82
                if supp['source_url']:
83
                    item.append(_fmt(supp['source_url']))
84
                if supp['line']:
85
                    if supp['column']:
86
                        item.append(
87
                            _fmt('Line %(line)s, Column %(column)s' % supp))
88
                    else:
89
                        item.append(_fmt('Line %(line)s' % supp))
90
                elif supp['column']:
91
                    item.append(_fmt('Column %(column)s' % supp))
92
                if supp['expression']:
93
                    item.append(_fmt('Expression: %(expression)s' % supp))
94
                if supp['warnings']:
95
                    for warning in supp['warnings']:
96
                        item.append(_fmt('Warning: %s' % warning))
97
                if supp['extra']:
13723.1.5 by Gary Poster
pacify lint
98
                    item.append(supp['extra'])  # We do not include a prefix.
13723.1.3 by Gary Poster
tests and implementation for sqltrace profile code
99
            if info:
100
                item.append(_fmt(info))
13776.2.3 by Gary Poster
respond to review
101
        except EXPLOSIVE_ERRORS:
13723.1.3 by Gary Poster
tests and implementation for sqltrace profile code
102
            raise
103
        except:
104
            # The values above may not stringify properly, or who knows what
105
            # else.  Be defensive.
106
            if DEBUG_EXCEPTION_FORMATTER:
107
                traceback.print_exc()
108
            # else just swallow the exception.
13723.1.5 by Gary Poster
pacify lint
109
        item.append('')  # This gives us a trailing newline.
13723.1.3 by Gary Poster
tests and implementation for sqltrace profile code
110
        list.append('\n'.join(item))
13723.1.1 by Gary Poster
incremental commit
111
    return list
112
113
114
def print_stack(f=None, limit=None, file=None):
115
    """Print a stack trace from its invocation point.
116
117
    The optional 'f' argument can be used to specify an alternate
118
    stack frame at which to start. The optional 'limit' and 'file'
119
    arguments have the same meaning as for print_exception().
120
    """
121
    print_list(extract_stack(_get_frame(f), limit), file)
122
123
124
def _get_limit(limit):
125
    "Return the limit or the globally-set limit, if any."
126
    if limit is None:
127
        # stdlib uses hasattr here.
128
        if hasattr(sys, 'tracebacklimit'):
129
            limit = sys.tracebacklimit
130
    return limit
131
132
133
def _get_frame_data(f, lineno):
134
    "Given a frame and a lineno, return data for each item of extract_*."
135
    # Adapted from zope.exceptions.
13776.2.1 by Gary Poster
fix ++profile++sqltrace for pages that have traceback hints that must be rendered immediately, like the root page of the site.
136
    try_except = _try_except  # This is a micro-optimization.
13723.1.3 by Gary Poster
tests and implementation for sqltrace profile code
137
    modname = f.f_globals.get('__name__')
13723.1.1 by Gary Poster
incremental commit
138
    # Output a traceback supplement, if any.
13776.2.1 by Gary Poster
fix ++profile++sqltrace for pages that have traceback hints that must be rendered immediately, like the root page of the site.
139
    supplement_dict = info = None
13723.1.1 by Gary Poster
incremental commit
140
    if '__traceback_supplement__' in f.f_locals:
141
        # Use the supplement defined in the function.
142
        tbs = f.f_locals['__traceback_supplement__']
143
    elif '__traceback_supplement__' in f.f_globals:
144
        # Use the supplement defined in the module.
145
        tbs = f.f_globals['__traceback_supplement__']
146
    else:
147
        tbs = None
148
    if tbs is not None:
149
        factory = tbs[0]
150
        args = tbs[1:]
13776.2.1 by Gary Poster
fix ++profile++sqltrace for pages that have traceback hints that must be rendered immediately, like the root page of the site.
151
        supplement = try_except(factory, *args)
152
        if supplement is not None:
13723.1.3 by Gary Poster
tests and implementation for sqltrace profile code
153
            # It might be nice if supplements could be dicts, for simplicity.
154
            # Historically, though, they are objects.
13776.2.1 by Gary Poster
fix ++profile++sqltrace for pages that have traceback hints that must be rendered immediately, like the root page of the site.
155
            # We will turn the supplement into a dict of strings, so that we
156
            # have "getInfo" pre-processed and so that we are not holding on
157
            # to anything from the frame.
13723.1.1 by Gary Poster
incremental commit
158
            extra = None
159
            getInfo = getattr(supplement, 'getInfo', None)
160
            if getInfo is not None:
13776.2.1 by Gary Poster
fix ++profile++sqltrace for pages that have traceback hints that must be rendered immediately, like the root page of the site.
161
                extra = try_except(getInfo)
162
                extra = try_except(str, extra) if extra is not None else None
163
            warnings = []
164
            # The outer try-except is for the iteration.
165
            try:
166
                for warning in getattr(supplement, 'warnings', ()):
167
                    if warning is not None:
168
                        warning = try_except(str, warning)
169
                    if warning is not None:
170
                        warnings.append(warning)
13776.2.3 by Gary Poster
respond to review
171
            except EXPLOSIVE_ERRORS:
13776.2.1 by Gary Poster
fix ++profile++sqltrace for pages that have traceback hints that must be rendered immediately, like the root page of the site.
172
                raise
173
            except:
174
                if DEBUG_EXCEPTION_FORMATTER:
175
                    traceback.print_exc()
176
            supplement_dict = dict(warnings=warnings, extra=extra)
177
            for key in ('source_url', 'line', 'column', 'expression'):
178
                value = getattr(supplement, key, None)
179
                if value is not None:
180
                    value = try_except(str, value)
181
                supplement_dict[key] = value
13723.1.1 by Gary Poster
incremental commit
182
    info = f.f_locals.get('__traceback_info__', None)
13776.2.1 by Gary Poster
fix ++profile++sqltrace for pages that have traceback hints that must be rendered immediately, like the root page of the site.
183
    info = try_except(str, info) if info is not None else None
13723.1.1 by Gary Poster
incremental commit
184
    # End part adapted from zope.exceptions.
13776.2.1 by Gary Poster
fix ++profile++sqltrace for pages that have traceback hints that must be rendered immediately, like the root page of the site.
185
    co = f.f_code
186
    filename = co.co_filename
187
    name = co.co_name
188
    linecache.checkcache(filename)
189
    line = linecache.getline(filename, lineno, f.f_globals)
190
    if line:
191
        line = line.strip()
192
    else:
193
        line = None
194
    return (filename, lineno, name, line, modname, supplement_dict, info)
13723.1.3 by Gary Poster
tests and implementation for sqltrace profile code
195
196
197
def extract_stack(f=None, limit=None):
13723.1.1 by Gary Poster
incremental commit
198
    """Extract the raw traceback from the current stack frame.
199
13723.1.3 by Gary Poster
tests and implementation for sqltrace profile code
200
    The return value has the same format as for extract_tb().  The optional
13723.1.5 by Gary Poster
pacify lint
201
    'f' and 'limit' arguments have the same meaning as for print_stack().
13723.1.3 by Gary Poster
tests and implementation for sqltrace profile code
202
    Each item in the list is a septuple (filename, line number, function name,
203
    text, module name, optional supplement dict, optional info string), and
204
    the entries are in order from oldest to newest stack frame.
13723.1.1 by Gary Poster
incremental commit
205
    """
206
    f = _get_frame(f)
207
    limit = _get_limit(limit)
208
    list = []
209
    n = 0
13776.2.1 by Gary Poster
fix ++profile++sqltrace for pages that have traceback hints that must be rendered immediately, like the root page of the site.
210
    get_frame_data = _get_frame_data  # This is a micro-optimization.
13723.1.1 by Gary Poster
incremental commit
211
    while f is not None and (limit is None or n < limit):
13776.2.1 by Gary Poster
fix ++profile++sqltrace for pages that have traceback hints that must be rendered immediately, like the root page of the site.
212
        list.append(get_frame_data(f, f.f_lineno))
13723.1.1 by Gary Poster
incremental commit
213
        f = f.f_back
13723.1.5 by Gary Poster
pacify lint
214
        n = n + 1
13723.1.1 by Gary Poster
incremental commit
215
    list.reverse()
216
    return list
217
218
13723.1.3 by Gary Poster
tests and implementation for sqltrace profile code
219
def extract_tb(tb, limit=None):
13723.1.1 by Gary Poster
incremental commit
220
    """Return list of up to limit pre-processed entries from traceback.
221
13723.1.3 by Gary Poster
tests and implementation for sqltrace profile code
222
    This is useful for alternate formatting of stack traces.  If 'limit' is
223
    omitted or None, all entries are extracted.  A pre-processed stack trace
224
    entry is a sextuple (filename, line number, function name, text, module
225
    name, optional supplement dict, optional info string) representing the
226
    information that is printed for a stack trace.  The text is a string with
227
    leading and trailing whitespace stripped; if the source is not available
228
    it is None. The supplement dict has keys 'source_url', 'line', 'column',
229
    'expression', 'warnings' (an iterable), and 'extra', any of which may be
230
    None.
13723.1.1 by Gary Poster
incremental commit
231
    """
13723.1.3 by Gary Poster
tests and implementation for sqltrace profile code
232
    # zope.exceptions handles tracebacks.  This function is implemented just
233
    # to show how this module's patterns might be extended to tracebacks.
13723.1.1 by Gary Poster
incremental commit
234
    limit = _get_limit(limit)
235
    list = []
236
    n = 0
13776.2.1 by Gary Poster
fix ++profile++sqltrace for pages that have traceback hints that must be rendered immediately, like the root page of the site.
237
    get_frame_data = _get_frame_data  # This is a micro-optimization.
13723.1.1 by Gary Poster
incremental commit
238
    while tb is not None and (limit is None or n < limit):
13776.2.1 by Gary Poster
fix ++profile++sqltrace for pages that have traceback hints that must be rendered immediately, like the root page of the site.
239
        list.append(get_frame_data(tb.tb_frame, tb.tb_lineno))
13723.1.1 by Gary Poster
incremental commit
240
        tb = tb.tb_next
13723.1.5 by Gary Poster
pacify lint
241
        n = n + 1
13723.1.1 by Gary Poster
incremental commit
242
    return list