comparison env/lib/python3.9/site-packages/boltons/excutils.py @ 0:4f3585e2f14b draft default tip

"planemo upload commit 60cee0fc7c0cda8592644e1aad72851dec82c959"
author shellac
date Mon, 22 Mar 2021 18:12:50 +0000
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:4f3585e2f14b
1 # -*- coding: utf-8 -*-
2
3 import sys
4 import traceback
5 import linecache
6 from collections import namedtuple
7
8 # TODO: last arg or first arg? (last arg makes it harder to *args
9 # into, but makes it more readable in the default exception
10 # __repr__ output)
11 # TODO: Multiexception wrapper
12
13
14 __all__ = ['ExceptionCauseMixin']
15
16
17 class ExceptionCauseMixin(Exception):
18 """
19 A mixin class for wrapping an exception in another exception, or
20 otherwise indicating an exception was caused by another exception.
21
22 This is most useful in concurrent or failure-intolerant scenarios,
23 where just because one operation failed, doesn't mean the remainder
24 should be aborted, or that it's the appropriate time to raise
25 exceptions.
26
27 This is still a work in progress, but an example use case at the
28 bottom of this module.
29
30 NOTE: when inheriting, you will probably want to put the
31 ExceptionCauseMixin first. Builtin exceptions are not good about
32 calling super()
33 """
34
35 cause = None
36
37 def __new__(cls, *args, **kw):
38 cause = None
39 if args and isinstance(args[0], Exception):
40 cause, args = args[0], args[1:]
41 ret = super(ExceptionCauseMixin, cls).__new__(cls, *args, **kw)
42 ret.cause = cause
43 if cause is None:
44 return ret
45 root_cause = getattr(cause, 'root_cause', None)
46 if root_cause is None:
47 ret.root_cause = cause
48 else:
49 ret.root_cause = root_cause
50
51 full_trace = getattr(cause, 'full_trace', None)
52 if full_trace is not None:
53 ret.full_trace = list(full_trace)
54 ret._tb = list(cause._tb)
55 ret._stack = list(cause._stack)
56 return ret
57
58 try:
59 exc_type, exc_value, exc_tb = sys.exc_info()
60 if exc_type is None and exc_value is None:
61 return ret
62 if cause is exc_value or root_cause is exc_value:
63 # handles when cause is the current exception or when
64 # there are multiple wraps while handling the original
65 # exception, but a cause was never provided
66 ret._tb = _extract_from_tb(exc_tb)
67 ret._stack = _extract_from_frame(exc_tb.tb_frame)
68 ret.full_trace = ret._stack[:-1] + ret._tb
69 finally:
70 del exc_tb
71 return ret
72
73 def get_str(self):
74 """
75 Get formatted the formatted traceback and exception
76 message. This function exists separately from __str__()
77 because __str__() is somewhat specialized for the built-in
78 traceback module's particular usage.
79 """
80 ret = []
81 trace_str = self._get_trace_str()
82 if trace_str:
83 ret.extend(['Traceback (most recent call last):\n', trace_str])
84 ret.append(self._get_exc_str())
85 return ''.join(ret)
86
87 def _get_message(self):
88 args = getattr(self, 'args', [])
89 if self.cause:
90 args = args[1:]
91 if args and args[0]:
92 return args[0]
93 return ''
94
95 def _get_trace_str(self):
96 if not self.cause:
97 return super(ExceptionCauseMixin, self).__repr__()
98 if self.full_trace:
99 return ''.join(traceback.format_list(self.full_trace))
100 return ''
101
102 def _get_exc_str(self, incl_name=True):
103 cause_str = _format_exc(self.root_cause)
104 message = self._get_message()
105 ret = []
106 if incl_name:
107 ret = [self.__class__.__name__, ': ']
108 if message:
109 ret.extend([message, ' (caused by ', cause_str, ')'])
110 else:
111 ret.extend([' caused by ', cause_str])
112 return ''.join(ret)
113
114 def __str__(self):
115 if not self.cause:
116 return super(ExceptionCauseMixin, self).__str__()
117 trace_str = self._get_trace_str()
118 ret = []
119 if trace_str:
120 message = self._get_message()
121 if message:
122 ret.extend([message, ' --- '])
123 ret.extend(['Wrapped traceback (most recent call last):\n',
124 trace_str,
125 self._get_exc_str(incl_name=True)])
126 return ''.join(ret)
127 else:
128 return self._get_exc_str(incl_name=False)
129
130
131 def _format_exc(exc, message=None):
132 if message is None:
133 message = exc
134 exc_str = traceback._format_final_exc_line(exc.__class__.__name__, message)
135 return exc_str.rstrip()
136
137
138 _BaseTBItem = namedtuple('_BaseTBItem', 'filename, lineno, name, line')
139
140
141 class _TBItem(_BaseTBItem):
142 def __repr__(self):
143 ret = super(_TBItem, self).__repr__()
144 ret += ' <%r>' % self.frame_id
145 return ret
146
147
148 class _DeferredLine(object):
149 def __init__(self, filename, lineno, module_globals=None):
150 self.filename = filename
151 self.lineno = lineno
152 module_globals = module_globals or {}
153 self.module_globals = dict([(k, v) for k, v in module_globals.items()
154 if k in ('__name__', '__loader__')])
155
156 def __eq__(self, other):
157 return (self.lineno, self.filename) == (other.lineno, other.filename)
158
159 def __ne__(self, other):
160 return (self.lineno, self.filename) != (other.lineno, other.filename)
161
162 def __str__(self):
163 if hasattr(self, '_line'):
164 return self._line
165 linecache.checkcache(self.filename)
166 line = linecache.getline(self.filename,
167 self.lineno,
168 self.module_globals)
169 if line:
170 line = line.strip()
171 else:
172 line = None
173 self._line = line
174 return line
175
176 def __repr__(self):
177 return repr(str(self))
178
179 def __len__(self):
180 return len(str(self))
181
182 def strip(self):
183 return str(self).strip()
184
185
186 def _extract_from_frame(f=None, limit=None):
187 ret = []
188 if f is None:
189 f = sys._getframe(1) # cross-impl yadayada
190 if limit is None:
191 limit = getattr(sys, 'tracebacklimit', 1000)
192 n = 0
193 while f is not None and n < limit:
194 filename = f.f_code.co_filename
195 lineno = f.f_lineno
196 name = f.f_code.co_name
197 line = _DeferredLine(filename, lineno, f.f_globals)
198 item = _TBItem(filename, lineno, name, line)
199 item.frame_id = id(f)
200 ret.append(item)
201 f = f.f_back
202 n += 1
203 ret.reverse()
204 return ret
205
206
207 def _extract_from_tb(tb, limit=None):
208 ret = []
209 if limit is None:
210 limit = getattr(sys, 'tracebacklimit', 1000)
211 n = 0
212 while tb is not None and n < limit:
213 filename = tb.tb_frame.f_code.co_filename
214 lineno = tb.tb_lineno
215 name = tb.tb_frame.f_code.co_name
216 line = _DeferredLine(filename, lineno, tb.tb_frame.f_globals)
217 item = _TBItem(filename, lineno, name, line)
218 item.frame_id = id(tb.tb_frame)
219 ret.append(item)
220 tb = tb.tb_next
221 n += 1
222 return ret
223
224
225 # An Example/Prototest:
226
227
228 class MathError(ExceptionCauseMixin, ValueError):
229 pass
230
231
232 def whoops_math():
233 return 1/0
234
235
236 def math_lol(n=0):
237 if n < 3:
238 return math_lol(n=n+1)
239 try:
240 return whoops_math()
241 except ZeroDivisionError as zde:
242 exc = MathError(zde, 'ya done messed up')
243 raise exc
244
245 def main():
246 try:
247 math_lol()
248 except ValueError as me:
249 exc = MathError(me, 'hi')
250 raise exc
251
252
253 if __name__ == '__main__':
254 try:
255 main()
256 except Exception:
257 import pdb;pdb.post_mortem()
258 raise