Completed
Push — master ( 95c270...39bbc3 )
by Stephan
46s
created

ExceptionHandler   A

Complexity

Total Complexity 17

Size/Duplication

Total Lines 123
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 123
rs 10
wmc 17

5 Methods

Rating   Name   Duplication   Size   Complexity  
A __init__() 0 4 1
B make_details_dialog() 0 32 1
A send_report() 0 12 1
F __call__() 0 27 9
B make_info_dialog() 0 40 5
1
"""Graphical exception handler for PyGTK applications
2
3
(c) 2003 Gustavo J A M Carneiro gjc at inescporto.pt
4
(c) 2004-2005 Filip Van Raemdonck
5
(c) 2009, 2011, 2017 Stephan Sokolow
6
7
http://www.daa.com.au/pipermail/pygtk/2003-August/005775.html
8
Message-ID: <[email protected]>
9
"The license is whatever you want."
10
11
Instructions: import gtkexcepthook; gtkexcepthook.enable()
12
13
Changes from Van Raemdonck version:
14
 - Refactored code for maintainability and added MyPy type annotations
15
 - Switched from auto-enable to gtkexcepthook.enable() to silence PyFlakes
16
   false positives. (Borrowed naming convention from cgitb)
17
 - Split out traceback import to silence PyFlakes warning.
18
 - Started to resolve PyLint complaints
19
20
@todo: Polish this up to meet my code formatting and clarity standards.
21
@todo: Clean up the SMTP support. It's a mess.
22
@todo: Confirm there isn't any other generally-applicable information that
23
       could be included in the debugging dump.
24
@todo: Consider the pros and cons of offering a function which allows
25
       app-specific debugging information to be registered for inclusion.
26
"""
27
28
__author__ = "Filip Van Daemdonck; Stephan Sokolow"
29
__authors__ = ["Filip Van Daemdonck", "Stephan Sokolow"]
30
__license__ = "whatever you want"
31
32
import inspect, linecache, pydoc, sys
33
# import traceback
34
from cStringIO import StringIO
35
from gettext import gettext as _
36
from pprint import pformat
37
from smtplib import SMTP
38
39
try:
40
    import pygtk
41
    pygtk.require('2.0')
42
except ImportError:
43
    pass
44
45
import gtk, pango
46
47
MYPY = False
48
if MYPY:
49
    # pylint: disable=unused-import
50
    from typing import Any, Optional, Type  # NOQA
51
del MYPY
52
53
# == Analyzer Backend ==
54
55
# TODO: Decide what to do with this
56
# def analyse(exctyp, value, tback):
57
#     trace = StringIO()
58
#     traceback.print_exception(exctyp, value, tback, None, trace)
59
#     return trace
60
61
def lookup(name, frame, lcls):
62
    # TODO: MyPy type signature
63
    '''Find the value for a given name in the given frame'''
64
    if name in lcls:
65
        return 'local', lcls[name]
66
    elif name in frame.f_globals:
67
        return 'global', frame.f_globals[name]
68
    elif '__builtins__' in frame.f_globals:
69
        builtins = frame.f_globals['__builtins__']
70
        if isinstance(builtins, dict):
71
            if name in builtins:
72
                return 'builtin', builtins[name]
73
        else:
74
            if hasattr(builtins, name):
75
                return 'builtin', getattr(builtins, name)
76
    return None, []
77
78
def analyse(exctyp, value, tback):
79
    # TODO: MyPy type signature
80
    import tokenize, keyword
81
82
    trace = StringIO()
83
    nlines = 3
84
    frecs = inspect.getinnerframes(tback, nlines)
85
    trace.write('Traceback (most recent call last):\n')
86
    # pylint: disable=unused-variable
87
    for frame, fname, lineno, funcname, context, cindex in frecs:
88
        trace.write('  File "%s", line %d, ' % (fname, lineno))
89
        args, varargs, varkw, lcls = inspect.getargvalues(frame)
90
91
        def readline(lno=[lineno], *args):
92
            if args:
93
                print args
94
            try:
95
                return linecache.getline(fname, lno[0])
96
            finally:
97
                lno[0] += 1
98
        _all, prev, name, scope = {}, None, '', None
99
        for ttype, tstr, stup, etup, lin in tokenize.generate_tokens(readline):
100
            if ttype == tokenize.NAME and tstr not in keyword.kwlist:
101
                if name:
102
                    if name[-1] == '.':
103
                        try:
104
                            val = getattr(prev, tstr)
105
                        except AttributeError:
106
                            # XXX skip the rest of this identifier only
107
                            break
108
                        name += tstr
109
                else:
110
                    assert not name and not scope
111
                    scope, val = lookup(tstr, frame, lcls)
112
                    name = tstr
113
                try:
114
                    if val:
115
                        prev = val
116
                except:
117
                    pass
118
                # TODO
119
                # print('  found', scope, 'name', name, 'val', val, 'in',
120
                #       prev, 'for token', tstr)
121
            elif tstr == '.':
122
                if prev:
123
                    name += '.'
124
            else:
125
                if name:
126
                    _all[name] = (scope, prev)
127
                prev, name, scope = None, '', None
128
                if ttype == tokenize.NEWLINE:
129
                    break
130
131
        trace.write(funcname +
132
          inspect.formatargvalues(args, varargs, varkw, lcls,
133
            formatvalue=lambda v: '=' + pydoc.text.repr(v)) + '\n')
134
        trace.write(''.join(['    ' + x.replace('\t', '  ')
135
                             for x in context if x.strip()]))
136
        if len(_all):
137
            trace.write('  variables: %s\n' % pformat(_all, indent=3))
138
139
    trace.write('%s: %s' % (exctyp.__name__, value))
140
    return trace
141
142
# == GTK+ Frontend ==
143
144
class ExceptionHandler(object):
145
    """GTK-based graphical exception handler"""
146
    cached_tback = None
147
148
    def __init__(self, feedback_email=None, smtp_server=None):
149
        # type: (str, str) -> None
150
        self.email = feedback_email
151
        self.smtphost = smtp_server or 'localhost'
152
153
    def make_info_dialog(self):
154
        # type: () -> gtk.MessageDialog
155
        """Initialize and return the top-level dialog"""
156
157
        # pylint: disable=no-member
158
        dialog = gtk.MessageDialog(parent=None, flags=0,
159
                                   type=gtk.MESSAGE_WARNING,
160
                                   buttons=gtk.BUTTONS_NONE)
161
        dialog.set_title(_("Bug Detected"))
162
        if gtk.check_version(2, 4, 0) is not None:
163
            dialog.set_has_separator(False)
164
165
        primary = _("<big><b>A programming error has been detected during the "
166
                    "execution of this program.</b></big>")
167
        secondary = _("It probably isn't fatal, but should be reported to the "
168
                      "developers nonetheless.")
169
170
        if self.email:
171
            dialog.add_button(_("Report..."), 3)
172
        else:
173
            secondary += _("\n\nPlease remember to include the contents of "
174
                           "the Details dialog.")
175
        try:
176
            setsec = dialog.format_secondary_text
177
        except AttributeError:
178
            raise
179
            # TODO
180
            # dialog.vbox.get_children()[0].get_children()[1].set_markup(
181
            #    '%s\n\n%s' % (primary, secondary))
182
            # lbl.set_property("use-markup", True)
183
        else:
184
            del setsec
185
            dialog.set_markup(primary)
186
            dialog.format_secondary_text(secondary)
187
188
        dialog.add_button(_("Details..."), 2)
189
        dialog.add_button(gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE)
190
        dialog.add_button(gtk.STOCK_QUIT, 1)
191
192
        return dialog
193
194
    @staticmethod
195
    def make_details_dialog(parent, text):
196
        # type: (gtk.MessageDialog, str) -> gtk.MessageDialog
197
        """Initialize and return the details dialog"""
198
199
        # pylint: disable=no-member
200
        details = gtk.Dialog(_("Bug Details"), parent,
201
          gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
202
          (gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE, ))
203
        details.set_property("has-separator", False)
204
205
        textview = gtk.TextView()
206
        textview.show()
207
        textview.set_editable(False)
208
        textview.modify_font(pango.FontDescription("Monospace"))
209
210
        swin = gtk.ScrolledWindow()
211
        swin.show()
212
        swin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
213
        swin.add(textview)
214
        details.vbox.add(swin)
215
        textbuffer = textview.get_buffer()
216
        textbuffer.set_text(text)
217
218
        # Set the default size to just over 60% of the screen's dimensions
219
        screen = gtk.gdk.screen_get_default()
220
        monitor = screen.get_monitor_at_window(parent.window)
221
        area = gtk.gdk.screen_get_default().get_monitor_geometry(monitor)
222
        width, height = area.width // 1.6, area.height // 1.6
223
        details.set_default_size(int(width), int(height))
224
225
        return details
226
227
    def send_report(self, traceback):
228
        # type: (str) -> None
229
        """Send the given traceback as a bug report."""
230
231
        # TODO: prettyprint, deal with problems in sending feedback, &tc
232
        message = ('From: buggy_application"\nTo: bad_programmer\n'
233
            'Subject: Exception feedback\n\n%s' % traceback)
234
235
        smtp = SMTP()
236
        smtp.connect(self.smtphost)
237
        smtp.sendmail(self.email, (self.email,), message)
238
        smtp.quit()
239
240
    def __call__(self, exctyp, value, tback):
241
        # type: (Type[BaseException], BaseException, Any) -> None
242
        """Custom sys.excepthook callback which displays a GTK+ dialog"""
243
        # pylint: disable=no-member
244
245
        dialog = self.make_info_dialog()
246
        while True:
247
            resp = dialog.run()
248
249
            # Generate and cache a traceback on demand
250
            if resp in (2, 3) and self.cached_tback is None:
251
                self.cached_tback = analyse(exctyp, value, tback).getvalue()
252
253
            if resp == 3:
254
                self.send_report(self.cached_tback)
255
            elif resp == 2:
256
                details = self.make_details_dialog(dialog, self.cached_tback)
257
                details.run()
258
                details.destroy()
259
            elif resp == 1 and gtk.main_level() > 0:
260
                gtk.main_quit()
261
262
            # Only the "Details" dialog loops back when closed
263
            if resp != 2:
264
                break
265
266
        dialog.destroy()
267
268
def enable(feedback_email=None, smtp_server=None):  # type: (str, str) -> None
269
    """Call this to set gtkexcepthook as the default exception handler"""
270
    sys.excepthook = ExceptionHandler(feedback_email, smtp_server)
271
272
if __name__ == '__main__':
273
    class TestFodder(object):  # pylint: disable=too-few-public-methods
274
        """Just something interesting to show in the augmented traceback"""
275
        y = 'Test'
276
277
        def __init__(self):  # type: () -> None
278
            self.z = self  # pylint: disable=invalid-name
279
    x = TestFodder()
280
    w = ' e'
281
282
    enable()
283
    raise Exception(x.z.y + w)
284
285
# vim: set sw=4 sts=4 :
286