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