/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/cleanup.py

  • Committer: Jelmer Vernooij
  • Date: 2019-12-23 01:39:21 UTC
  • mfrom: (7424 work)
  • mto: This revision was merged to the branch mainline in revision 7425.
  • Revision ID: jelmer@jelmer.uk-20191223013921-2kzd0wlcoylgxksk
Merge trunk.

Show diffs side-by-side

added added

removed removed

Lines of Context:
16
16
 
17
17
"""Helpers for managing cleanup functions and the errors they might raise.
18
18
 
19
 
The usual way to run cleanup code in Python is::
20
 
 
21
 
    try:
22
 
        do_something()
23
 
    finally:
24
 
        cleanup_something()
25
 
 
26
 
However if both `do_something` and `cleanup_something` raise an exception
27
 
Python will forget the original exception and propagate the one from
28
 
cleanup_something.  Unfortunately, this is almost always much less useful than
29
 
the original exception.
30
 
 
31
 
If you want to be certain that the first, and only the first, error is raised,
32
 
then use::
33
 
 
34
 
    operation = OperationWithCleanups(do_something)
35
 
    operation.add_cleanup(cleanup_something)
36
 
    operation.run_simple()
37
 
 
38
 
This is more inconvenient (because you need to make every try block a
39
 
function), but will ensure that the first error encountered is the one raised,
40
 
while also ensuring all cleanups are run.  See OperationWithCleanups for more
41
 
details.
 
19
This currently just contains a copy of contextlib.ExitStack, available
 
20
even on older versions of Python.
42
21
"""
43
22
 
44
23
from __future__ import absolute_import
45
24
 
46
25
from collections import deque
47
 
from . import (
48
 
    debug,
49
 
    trace,
50
 
    )
51
 
 
52
 
 
53
 
def _log_cleanup_error(exc):
54
 
    trace.mutter('Cleanup failed:')
55
 
    trace.log_exception_quietly()
56
 
    if 'cleanup' in debug.debug_flags:
57
 
        trace.warning('brz: warning: Cleanup failed: %s', exc)
58
 
 
59
 
 
60
 
def _run_cleanup(func, *args, **kwargs):
61
 
    """Run func(*args, **kwargs), logging but not propagating any error it
62
 
    raises.
63
 
 
64
 
    :returns: True if func raised no errors, else False.
65
 
    """
66
 
    try:
67
 
        func(*args, **kwargs)
68
 
    except KeyboardInterrupt:
69
 
        raise
70
 
    except Exception as exc:
71
 
        _log_cleanup_error(exc)
72
 
        return False
73
 
    return True
74
 
 
75
 
 
76
 
def _run_cleanups(funcs):
77
 
    """Run a series of cleanup functions."""
78
 
    for func, args, kwargs in funcs:
79
 
        _run_cleanup(func, *args, **kwargs)
80
 
 
81
 
 
82
 
class ObjectWithCleanups(object):
83
 
    """A mixin for objects that hold a cleanup list.
84
 
 
85
 
    Subclass or client code can call add_cleanup and then later `cleanup_now`.
86
 
    """
87
 
 
88
 
    def __init__(self):
89
 
        self.cleanups = deque()
90
 
 
91
 
    def add_cleanup(self, cleanup_func, *args, **kwargs):
92
 
        """Add a cleanup to run.
93
 
 
94
 
        Cleanups may be added at any time.
95
 
        Cleanups will be executed in LIFO order.
 
26
import sys
 
27
 
 
28
 
 
29
try:
 
30
    from contextlib import ExitStack
 
31
except ImportError:
 
32
    # Copied from the Python standard library on Python 3.4.
 
33
    # Copyright: Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008,
 
34
    #   2009, 2010, 2011 Python Software Foundation
 
35
    #
 
36
    # PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
 
37
    # --------------------------------------------
 
38
    # .
 
39
    # 1. This LICENSE AGREEMENT is between the Python Software Foundation
 
40
    # ("PSF"), and the Individual or Organization ("Licensee") accessing and
 
41
    # otherwise using this software ("Python") in source or binary form and
 
42
    # its associated documentation.
 
43
    # .
 
44
    # 2. Subject to the terms and conditions of this License Agreement, PSF hereby
 
45
    # grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
 
46
    # analyze, test, perform and/or display publicly, prepare derivative works,
 
47
    # distribute, and otherwise use Python alone or in any derivative version,
 
48
    # provided, however, that PSF's License Agreement and PSF's notice of copyright,
 
49
    # i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
 
50
    # 2011 Python Software Foundation; All Rights Reserved" are retained in Python
 
51
    # alone or in any derivative version prepared by Licensee.
 
52
    # .
 
53
    # 3. In the event Licensee prepares a derivative work that is based on
 
54
    # or incorporates Python or any part thereof, and wants to make
 
55
    # the derivative work available to others as provided herein, then
 
56
    # Licensee hereby agrees to include in any such work a brief summary of
 
57
    # the changes made to Python.
 
58
    # .
 
59
    # 4. PSF is making Python available to Licensee on an "AS IS"
 
60
    # basis.  PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
 
61
    # IMPLIED.  BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
 
62
    # DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
 
63
    # FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
 
64
    # INFRINGE ANY THIRD PARTY RIGHTS.
 
65
    # .
 
66
    # 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
 
67
    # FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
 
68
    # A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
 
69
    # OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
 
70
    # .
 
71
    # 6. This License Agreement will automatically terminate upon a material
 
72
    # breach of its terms and conditions.
 
73
    # .
 
74
    # 7. Nothing in this License Agreement shall be deemed to create any
 
75
    # relationship of agency, partnership, or joint venture between PSF and
 
76
    # Licensee.  This License Agreement does not grant permission to use PSF
 
77
    # trademarks or trade name in a trademark sense to endorse or promote
 
78
    # products or services of Licensee, or any third party.
 
79
    # .
 
80
    # 8. By copying, installing or otherwise using Python, Licensee
 
81
    # agrees to be bound by the terms and conditions of this License
 
82
    # Agreement.
 
83
 
 
84
    def _reraise_with_existing_context(exc_details):
 
85
        # Use 3 argument raise in Python 2,
 
86
        # but use exec to avoid SyntaxError in Python 3
 
87
        exc_type, exc_value, exc_tb = exc_details
 
88
        exec("raise exc_type, exc_value, exc_tb")
 
89
 
 
90
 
 
91
    # Inspired by discussions on http://bugs.python.org/issue13585
 
92
    class ExitStack(object):
 
93
        """Context manager for dynamic management of a stack of exit callbacks
 
94
 
 
95
        For example:
 
96
 
 
97
            with ExitStack() as stack:
 
98
                files = [stack.enter_context(open(fname)) for fname in filenames]
 
99
                # All opened files will automatically be closed at the end of
 
100
                # the with statement, even if attempts to open files later
 
101
                # in the list raise an exception
 
102
 
96
103
        """
97
 
        self.cleanups.appendleft((cleanup_func, args, kwargs))
98
 
 
99
 
    def cleanup_now(self):
100
 
        _run_cleanups(self.cleanups)
101
 
        self.cleanups.clear()
102
 
 
103
 
 
104
 
class OperationWithCleanups(ObjectWithCleanups):
105
 
    """A way to run some code with a dynamic cleanup list.
106
 
 
107
 
    This provides a way to add cleanups while the function-with-cleanups is
108
 
    running.
109
 
 
110
 
    Typical use::
111
 
 
112
 
        operation = OperationWithCleanups(some_func)
113
 
        operation.run(args...)
114
 
 
115
 
    where `some_func` is::
116
 
 
117
 
        def some_func(operation, args, ...):
118
 
            do_something()
119
 
            operation.add_cleanup(something)
120
 
            # etc
121
 
 
122
 
    Note that the first argument passed to `some_func` will be the
123
 
    OperationWithCleanups object.  To invoke `some_func` without that, use
124
 
    `run_simple` instead of `run`.
125
 
    """
126
 
 
127
 
    def __init__(self, func):
128
 
        super(OperationWithCleanups, self).__init__()
129
 
        self.func = func
130
 
 
131
 
    def run(self, *args, **kwargs):
132
 
        return _do_with_cleanups(
133
 
            self.cleanups, self.func, self, *args, **kwargs)
134
 
 
135
 
    def run_simple(self, *args, **kwargs):
136
 
        return _do_with_cleanups(
137
 
            self.cleanups, self.func, *args, **kwargs)
138
 
 
139
 
 
140
 
def _do_with_cleanups(cleanup_funcs, func, *args, **kwargs):
141
 
    """Run `func`, then call all the cleanup_funcs.
142
 
 
143
 
    All the cleanup_funcs are guaranteed to be run.  The first exception raised
144
 
    by func or any of the cleanup_funcs is the one that will be propagted by
145
 
    this function (subsequent errors are caught and logged).
146
 
 
147
 
    Conceptually similar to::
148
 
 
149
 
        try:
150
 
            return func(*args, **kwargs)
151
 
        finally:
152
 
            for cleanup, cargs, ckwargs in cleanup_funcs:
153
 
                cleanup(*cargs, **ckwargs)
154
 
 
155
 
    It avoids several problems with using try/finally directly:
156
 
     * an exception from func will not be obscured by a subsequent exception
157
 
       from a cleanup.
158
 
     * an exception from a cleanup will not prevent other cleanups from
159
 
       running (but the first exception encountered is still the one
160
 
       propagated).
161
 
 
162
 
    Unike `_run_cleanup`, `_do_with_cleanups` can propagate an exception from a
163
 
    cleanup, but only if there is no exception from func.
164
 
    """
165
 
    try:
166
 
        result = func(*args, **kwargs)
167
 
    except BaseException:
168
 
        # We have an exception from func already, so suppress cleanup errors.
169
 
        _run_cleanups(cleanup_funcs)
170
 
        raise
171
 
    # No exception from func, so allow first cleanup error to propgate.
172
 
    pending_cleanups = iter(cleanup_funcs)
173
 
    try:
174
 
        for cleanup, c_args, c_kwargs in pending_cleanups:
175
 
            cleanup(*c_args, **c_kwargs)
176
 
    except BaseException:
177
 
        # Still run the remaining cleanups but suppress any further errors.
178
 
        _run_cleanups(pending_cleanups)
179
 
        raise
180
 
    # No error, so we can return the result
181
 
    return result
 
104
        def __init__(self):
 
105
            self._exit_callbacks = deque()
 
106
 
 
107
        def pop_all(self):
 
108
            """Preserve the context stack by transferring it to a new instance"""
 
109
            new_stack = type(self)()
 
110
            new_stack._exit_callbacks = self._exit_callbacks
 
111
            self._exit_callbacks = deque()
 
112
            return new_stack
 
113
 
 
114
        def _push_cm_exit(self, cm, cm_exit):
 
115
            """Helper to correctly register callbacks to __exit__ methods"""
 
116
            def _exit_wrapper(*exc_details):
 
117
                return cm_exit(cm, *exc_details)
 
118
            _exit_wrapper.__self__ = cm
 
119
            self.push(_exit_wrapper)
 
120
 
 
121
        def push(self, exit):
 
122
            """Registers a callback with the standard __exit__ method signature
 
123
 
 
124
            Can suppress exceptions the same way __exit__ methods can.
 
125
 
 
126
            Also accepts any object with an __exit__ method (registering a call
 
127
            to the method instead of the object itself)
 
128
            """
 
129
            # We use an unbound method rather than a bound method to follow
 
130
            # the standard lookup behaviour for special methods
 
131
            _cb_type = type(exit)
 
132
            try:
 
133
                exit_method = _cb_type.__exit__
 
134
            except AttributeError:
 
135
                # Not a context manager, so assume its a callable
 
136
                self._exit_callbacks.append(exit)
 
137
            else:
 
138
                self._push_cm_exit(exit, exit_method)
 
139
            return exit # Allow use as a decorator
 
140
 
 
141
        def callback(self, callback, *args, **kwds):
 
142
            """Registers an arbitrary callback and arguments.
 
143
 
 
144
            Cannot suppress exceptions.
 
145
            """
 
146
            def _exit_wrapper(exc_type, exc, tb):
 
147
                callback(*args, **kwds)
 
148
            # We changed the signature, so using @wraps is not appropriate, but
 
149
            # setting __wrapped__ may still help with introspection
 
150
            _exit_wrapper.__wrapped__ = callback
 
151
            self.push(_exit_wrapper)
 
152
            return callback # Allow use as a decorator
 
153
 
 
154
        def enter_context(self, cm):
 
155
            """Enters the supplied context manager
 
156
 
 
157
            If successful, also pushes its __exit__ method as a callback and
 
158
            returns the result of the __enter__ method.
 
159
            """
 
160
            # We look up the special methods on the type to match the with statement
 
161
            _cm_type = type(cm)
 
162
            _exit = _cm_type.__exit__
 
163
            result = _cm_type.__enter__(cm)
 
164
            self._push_cm_exit(cm, _exit)
 
165
            return result
 
166
 
 
167
        def close(self):
 
168
            """Immediately unwind the context stack"""
 
169
            self.__exit__(None, None, None)
 
170
 
 
171
        def __enter__(self):
 
172
            return self
 
173
 
 
174
        def __exit__(self, *exc_details):
 
175
            received_exc = exc_details[0] is not None
 
176
 
 
177
            # We manipulate the exception state so it behaves as though
 
178
            # we were actually nesting multiple with statements
 
179
            frame_exc = sys.exc_info()[1]
 
180
            def _make_context_fixer(frame_exc):
 
181
                return lambda new_exc, old_exc: None
 
182
            _fix_exception_context = _make_context_fixer(frame_exc)
 
183
 
 
184
            # Callbacks are invoked in LIFO order to match the behaviour of
 
185
            # nested context managers
 
186
            suppressed_exc = False
 
187
            pending_raise = False
 
188
            while self._exit_callbacks:
 
189
                cb = self._exit_callbacks.pop()
 
190
                try:
 
191
                    if cb(*exc_details):
 
192
                        suppressed_exc = True
 
193
                        pending_raise = False
 
194
                        exc_details = (None, None, None)
 
195
                except:
 
196
                    new_exc_details = sys.exc_info()
 
197
                    # simulate the stack of exceptions by setting the context
 
198
                    _fix_exception_context(new_exc_details[1], exc_details[1])
 
199
                    pending_raise = True
 
200
                    exc_details = new_exc_details
 
201
            if pending_raise:
 
202
                _reraise_with_existing_context(exc_details)
 
203
            return received_exc and suppressed_exc