1# encoding: utf-8
2
3"""Classes and functions for kernel related errors and exceptions.
4
5Inheritance diagram:
6
7.. inheritance-diagram:: ipyparallel.error
8   :parts: 3
9
10Authors:
11
12* Brian Granger
13* Min RK
14"""
15from __future__ import print_function
16
17import sys
18import traceback
19
20from ipython_genutils.py3compat import unicode_type
21
22__docformat__ = "restructuredtext en"
23
24# Tell nose to skip this module
25__test__ = {}
26
27#-------------------------------------------------------------------------------
28#  Copyright (C) 2008-2011  The IPython Development Team
29#
30#  Distributed under the terms of the BSD License.  The full license is in
31#  the file COPYING, distributed as part of this software.
32#-------------------------------------------------------------------------------
33
34#-------------------------------------------------------------------------------
35# Error classes
36#-------------------------------------------------------------------------------
37class IPythonError(Exception):
38    """Base exception that all of our exceptions inherit from.
39
40    This can be raised by code that doesn't have any more specific
41    information."""
42
43    pass
44
45class KernelError(IPythonError):
46    pass
47
48class EngineError(KernelError):
49    pass
50
51class NoEnginesRegistered(KernelError):
52    pass
53
54class TaskAborted(KernelError):
55    pass
56
57class TaskTimeout(KernelError):
58    pass
59
60class TimeoutError(KernelError):
61    pass
62
63class UnmetDependency(KernelError):
64    pass
65
66class ImpossibleDependency(UnmetDependency):
67    pass
68
69class DependencyTimeout(ImpossibleDependency):
70    pass
71
72class InvalidDependency(ImpossibleDependency):
73    pass
74
75class RemoteError(KernelError):
76    """Error raised elsewhere"""
77    ename=None
78    evalue=None
79    traceback=None
80    engine_info=None
81
82    def __init__(self, ename, evalue, traceback, engine_info=None):
83        self.ename=ename
84        self.evalue=evalue
85        self.traceback=traceback
86        self.engine_info=engine_info or {}
87        self.args=(ename, evalue)
88
89    def __repr__(self):
90        engineid = self.engine_info.get('engine_id', ' ')
91        return "<Remote[%s]:%s(%s)>"%(engineid, self.ename, self.evalue)
92
93    def __str__(self):
94        return "%s(%s)" % (self.ename, self.evalue)
95
96    def render_traceback(self):
97        """render traceback to a list of lines"""
98        return (self.traceback or "No traceback available").splitlines()
99
100    def _render_traceback_(self):
101        """Special method for custom tracebacks within IPython.
102
103        This will be called by IPython instead of displaying the local traceback.
104
105        It should return a traceback rendered as a list of lines.
106        """
107        return self.render_traceback()
108
109    def print_traceback(self, excid=None):
110        """print my traceback"""
111        print('\n'.join(self.render_traceback()))
112
113
114
115
116class TaskRejectError(KernelError):
117    """Exception to raise when a task should be rejected by an engine.
118
119    This exception can be used to allow a task running on an engine to test
120    if the engine (or the user's namespace on the engine) has the needed
121    task dependencies.  If not, the task should raise this exception.  For
122    the task to be retried on another engine, the task should be created
123    with the `retries` argument > 1.
124
125    The advantage of this approach over our older properties system is that
126    tasks have full access to the user's namespace on the engines and the
127    properties don't have to be managed or tested by the controller.
128    """
129
130
131class CompositeError(RemoteError):
132    """Error for representing possibly multiple errors on engines"""
133    tb_limit = 4 # limit on how many tracebacks to draw
134
135    def __init__(self, message, elist):
136        Exception.__init__(self, *(message, elist))
137        # Don't use pack_exception because it will conflict with the .message
138        # attribute that is being deprecated in 2.6 and beyond.
139        self.msg = message
140        self.elist = elist
141        self.args = [ e[0] for e in elist ]
142
143    def _get_engine_str(self, ei):
144        if not ei:
145            return '[Engine Exception]'
146        else:
147            return '[%s:%s]: ' % (ei['engine_id'], ei['method'])
148
149    def _get_traceback(self, ev):
150        try:
151            tb = ev._ipython_traceback_text
152        except AttributeError:
153            return 'No traceback available'
154        else:
155            return tb
156
157    def __str__(self):
158        s = str(self.msg)
159        for en, ev, etb, ei in self.elist[:self.tb_limit]:
160            engine_str = self._get_engine_str(ei)
161            s = s + '\n' + engine_str + en + ': ' + str(ev)
162        if len(self.elist) > self.tb_limit:
163            s = s + '\n.... %i more exceptions ...' % (len(self.elist) - self.tb_limit)
164        return s
165
166    def __repr__(self):
167        return "CompositeError(%i)" % len(self.elist)
168
169    def render_traceback(self, excid=None):
170        """render one or all of my tracebacks to a list of lines"""
171        lines = []
172        if excid is None:
173            for (en,ev,etb,ei) in self.elist[:self.tb_limit]:
174                lines.append(self._get_engine_str(ei))
175                lines.extend((etb or 'No traceback available').splitlines())
176                lines.append('')
177            if len(self.elist) > self.tb_limit:
178                lines.append(
179                    '... %i more exceptions ...' % (len(self.elist) - self.tb_limit)
180                )
181        else:
182            try:
183                en,ev,etb,ei = self.elist[excid]
184            except:
185                raise IndexError("an exception with index %i does not exist"%excid)
186            else:
187                lines.append(self._get_engine_str(ei))
188                lines.extend((etb or 'No traceback available').splitlines())
189
190        return lines
191
192    def print_traceback(self, excid=None):
193        print('\n'.join(self.render_traceback(excid)))
194
195    def raise_exception(self, excid=0):
196        try:
197            en,ev,etb,ei = self.elist[excid]
198        except:
199            raise IndexError("an exception with index %i does not exist"%excid)
200        else:
201            raise RemoteError(en, ev, etb, ei)
202
203
204def collect_exceptions(rdict_or_list, method='unspecified'):
205    """check a result dict for errors, and raise CompositeError if any exist.
206    Passthrough otherwise."""
207    elist = []
208    if isinstance(rdict_or_list, dict):
209        rlist = rdict_or_list.values()
210    else:
211        rlist = rdict_or_list
212    for r in rlist:
213        if isinstance(r, RemoteError):
214            en, ev, etb, ei = r.ename, r.evalue, r.traceback, r.engine_info
215            # Sometimes we could have CompositeError in our list.  Just take
216            # the errors out of them and put them in our new list.  This
217            # has the effect of flattening lists of CompositeErrors into one
218            # CompositeError
219            if en=='CompositeError':
220                for e in ev.elist:
221                    elist.append(e)
222            else:
223                elist.append((en, ev, etb, ei))
224    if len(elist)==0:
225        return rdict_or_list
226    else:
227        msg = "one or more exceptions from call to method: %s" % (method)
228        # This silliness is needed so the debugger has access to the exception
229        # instance (e in this case)
230        try:
231            raise CompositeError(msg, elist)
232        except CompositeError as e:
233            raise e
234
235def wrap_exception(engine_info={}):
236    etype, evalue, tb = sys.exc_info()
237    stb = traceback.format_exception(etype, evalue, tb)
238    exc_content = {
239        'status' : 'error',
240        'traceback' : stb,
241        'ename' : unicode_type(etype.__name__),
242        'evalue' : unicode_type(evalue),
243        'engine_info' : engine_info
244    }
245    return exc_content
246
247def unwrap_exception(content):
248    err = RemoteError(content['ename'], content['evalue'],
249                ''.join(content['traceback']),
250                content.get('engine_info', {}))
251    return err
252
253