]> arthur.barton.de Git - bup.git/blob - lib/bup/compat.py
compat: add ExitStack
[bup.git] / lib / bup / compat.py
1
2 from __future__ import absolute_import, print_function
3 from binascii import hexlify
4 from traceback import print_exception
5 import os, sys
6
7 # Please see CODINGSTYLE for important exception handling guidelines
8 # and the rationale behind add_ex_tb(), add_ex_ctx(), etc.
9
10 py_maj = sys.version_info[0]
11 py3 = py_maj >= 3
12
13 if py3:
14
15     # pylint: disable=unused-import
16     from contextlib import ExitStack, nullcontext
17     from mmap import mmap
18     from os import environb as environ
19     from os import fsdecode, fsencode
20     from shlex import quote
21     # pylint: disable=undefined-variable
22     # (for python2 looking here)
23     ModuleNotFoundError = ModuleNotFoundError
24     input = input
25     range = range
26     str_type = str
27     int_types = (int,)
28
29     def hexstr(b):
30         """Return hex string (not bytes as with hexlify) representation of b."""
31         return b.hex()
32
33     def reraise(ex):
34         raise ex.with_traceback(sys.exc_info()[2])
35
36     def add_ex_tb(ex):
37         """Do nothing (already handled by Python 3 infrastructure)."""
38         return ex
39
40     def add_ex_ctx(ex, context_ex):
41         """Do nothing (already handled by Python 3 infrastructure)."""
42         return ex
43
44     class pending_raise:
45         """If rethrow is true, rethrow ex (if any), unless the body throws.
46
47         (Supports Python 2 compatibility.)
48
49         """
50         def __init__(self, ex, rethrow=True):
51             self.ex = ex
52             self.rethrow = rethrow
53         def __enter__(self):
54             return None
55         def __exit__(self, exc_type, exc_value, traceback):
56             if not exc_type and self.ex and self.rethrow:
57                 raise self.ex
58
59     def items(x):
60         return x.items()
61
62     def argv_bytes(x):
63         """Return the original bytes passed to main() for an argv argument."""
64         return fsencode(x)
65
66     def bytes_from_uint(i):
67         return bytes((i,))
68
69     def bytes_from_byte(b):  # python > 2: b[3] returns ord('x'), not b'x'
70         return bytes((b,))
71
72     byte_int = lambda x: x
73
74     def buffer(object, offset=None, size=None):
75         if size:
76             assert offset is not None
77             return memoryview(object)[offset:offset + size]
78         if offset:
79             return memoryview(object)[offset:]
80         return memoryview(object)
81
82     def getcwd():
83         return fsencode(os.getcwd())
84
85 else:  # Python 2
86
87     from contextlib import contextmanager
88     import mmap as py_mmap
89
90     ModuleNotFoundError = ImportError
91
92     def fsdecode(x):
93         return x
94
95     def fsencode(x):
96         return x
97
98     from pipes import quote
99     # pylint: disable=unused-import
100     from os import environ, getcwd
101
102     # pylint: disable=unused-import
103     from bup.py2raise import reraise
104
105     @contextmanager
106     def nullcontext(enter_result=None):
107         yield enter_result
108
109     # on py3 this causes errors, obviously
110     # pylint: disable=undefined-variable
111     input = raw_input
112     # pylint: disable=undefined-variable
113     range = xrange
114     # pylint: disable=undefined-variable
115     str_type = basestring
116     # pylint: disable=undefined-variable
117     int_types = (int, long)
118
119     hexstr = hexlify
120
121     def add_ex_tb(ex):
122         """Add a traceback to ex if it doesn't already have one.  Return ex.
123
124         """
125         if not getattr(ex, '__traceback__', None):
126             ex.__traceback__ = sys.exc_info()[2]
127         return ex
128
129     def add_ex_ctx(ex, context_ex):
130         """Make context_ex the __context__ of ex (unless it already has one).
131         Return ex.
132
133         """
134         if context_ex:
135             if not getattr(ex, '__context__', None):
136                 ex.__context__ = context_ex
137         return ex
138
139     class pending_raise:
140         """If rethrow is true, rethrow ex (if any), unless the body throws.
141
142         If the body does throw, make any provided ex the __context__
143         of the newer exception (assuming there's no existing
144         __context__).  Ensure the exceptions have __tracebacks__.
145         (Supports Python 2 compatibility.)
146
147         """
148         def __init__(self, ex, rethrow=True):
149             self.ex = ex
150             self.rethrow = rethrow
151         def __enter__(self):
152             if self.ex:
153                 add_ex_tb(self.ex)
154         def __exit__(self, exc_type, exc_value, traceback):
155             if exc_value:
156                 if self.ex:
157                     add_ex_tb(exc_value)
158                     add_ex_ctx(exc_value, self.ex)
159                 return
160             if self.rethrow and self.ex:
161                 raise self.ex
162
163     def dump_traceback(ex):
164         stack = [ex]
165         next_ex = getattr(ex, '__context__', None)
166         while next_ex:
167             stack.append(next_ex)
168             next_ex = getattr(next_ex, '__context__', None)
169         stack = reversed(stack)
170         ex = next(stack)
171         tb = getattr(ex, '__traceback__', None)
172         print_exception(type(ex), ex, tb)
173         for ex in stack:
174             print('\nDuring handling of the above exception, another exception occurred:\n',
175                   file=sys.stderr)
176             tb = getattr(ex, '__traceback__', None)
177             print_exception(type(ex), ex, tb)
178
179     class ExitStack:
180         def __init__(self):
181             self.contexts = []
182
183         def __enter__(self):
184             return self
185
186         def __exit__(self, value_type, value, traceback):
187             init_value = value
188             for ctx in reversed(self.contexts):
189                 try:
190                     ctx.__exit__(value_type, value, traceback)
191                 except BaseException as ex:
192                     add_ex_tb(ex)
193                     if value:
194                         add_ex_ctx(ex, value)
195                     value_type = type(ex)
196                     value = ex
197                     traceback = ex.__traceback__
198             if value is not init_value:
199                 raise value
200
201         def enter_context(self, x):
202             self.contexts.append(x)
203
204     def items(x):
205         return x.iteritems()
206
207     def argv_bytes(x):
208         """Return the original bytes passed to main() for an argv argument."""
209         return x
210
211     bytes_from_uint = chr
212
213     def bytes_from_byte(b):
214         return b
215
216     byte_int = ord
217
218     buffer = buffer
219
220     assert not hasattr(py_mmap.mmap, '__enter__')
221     assert not hasattr(py_mmap.mmap, '__exit__')
222
223     class mmap(py_mmap.mmap):
224         def __enter__(self):
225             return self
226         def __exit__(self, type, value, traceback):
227             with pending_raise(value, rethrow=False):
228                 self.close()
229
230 try:
231     import bup_main
232 except ModuleNotFoundError:
233     bup_main = None
234
235 if bup_main:
236     def get_argvb():
237         "Return a new list containing the current process argv bytes."
238         return bup_main.argv()
239     if py3:
240         def get_argv():
241             "Return a new list containing the current process argv strings."
242             return [x.decode(errors='surrogateescape') for x in bup_main.argv()]
243     else:
244         def get_argv():
245             return bup_main.argv()
246 else:
247     def get_argvb():
248         raise Exception('get_argvb requires the bup_main module');
249     def get_argv():
250         raise Exception('get_argv requires the bup_main module');
251
252 def wrap_main(main):
253     """Run main() and raise a SystemExit with the return value if it
254     returns, pass along any SystemExit it raises, convert
255     KeyboardInterrupts into exit(130), and print a Python 3 style
256     contextual backtrace for other exceptions in both Python 2 and
257     3)."""
258     try:
259         sys.exit(main())
260     except KeyboardInterrupt as ex:
261         sys.exit(130)
262     except SystemExit as ex:
263         raise
264     except BaseException as ex:
265         if py3:
266             raise
267         add_ex_tb(ex)
268         dump_traceback(ex)
269         sys.exit(1)
270
271
272 # Excepting wrap_main() in the traceback, these should produce similar output:
273 #   python2 lib/bup/compat.py
274 #   python3 lib/bup/compat.py
275 # i.e.:
276 #   diff -u <(python2 lib/bup/compat.py 2>&1) <(python3 lib/bup/compat.py 2>&1)
277 #
278 # Though the python3 output for 'second' will include a stacktrace
279 # starting from wrap_main, rather than from outer().
280
281 if __name__ == '__main__':
282
283     def inner():
284         raise Exception('first')
285
286     def outer():
287         try:
288             inner()
289         except Exception as ex:
290             add_ex_tb(ex)
291             try:
292                 raise Exception('second')
293             except Exception as ex2:
294                 raise add_ex_ctx(add_ex_tb(ex2), ex)
295
296     wrap_main(outer)