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