1#  fixtures: Fixtures with cleanups for testing and convenience.
2#
3# Copyright (c) 2010, Robert Collins <robertc@robertcollins.net>
4#
5# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause
6# license at the users choice. A copy of both licenses are available in the
7# project source as Apache-2.0 and BSD. You may not use this file except in
8# compliance with one of these two licences.
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
13# license you chose for the specific language governing permissions and
14# limitations under that license.
15
16__all__ = [
17    'CallMany',
18    ]
19
20import sys
21
22from testtools.compat import (
23    reraise,
24    )
25from testtools.helpers import try_import
26
27
28class MultipleExceptions(Exception):
29    """Report multiple exc_info tuples in self.args."""
30
31MultipleExceptions = try_import(
32    "testtools.MultipleExceptions", MultipleExceptions)
33
34
35class CallMany(object):
36    """A stack of functions which will all be called on __call__.
37
38    CallMany also acts as a context manager for convenience.
39
40    Functions are called in last pushed first executed order.
41
42    This is used by Fixture to manage its addCleanup feature.
43    """
44
45    def __init__(self):
46        self._cleanups = []
47
48    def push(self, cleanup, *args, **kwargs):
49        """Add a function to be called from __call__.
50
51        On __call__ all functions are called - see __call__ for details on how
52        multiple exceptions are handled.
53
54        :param cleanup: A callable to call during cleanUp.
55        :param *args: Positional args for cleanup.
56        :param kwargs: Keyword args for cleanup.
57        :return: None
58        """
59        self._cleanups.append((cleanup, args, kwargs))
60
61    def __call__(self, raise_errors=True):
62        """Run all the registered functions.
63
64        :param raise_errors: Deprecated parameter from before testtools gained
65            MultipleExceptions. raise_errors defaults to True. When True
66            if exception(s) are raised while running functions, they are
67            re-raised after all the functions have run.  If multiple exceptions
68            are raised, they are all wrapped into a MultipleExceptions object,
69            and that is raised.
70
71            Thus, to catch a specific exception from a function run by __call__,
72            you need to catch both the exception and MultipleExceptions, and
73            then check within a MultipleExceptions instance for an occurance of
74            the type you wish to catch.
75        :return: Either None or a list of the exc_info() for each exception
76            that occured if raise_errors was False.
77        """
78        cleanups = reversed(self._cleanups)
79        self._cleanups = []
80        result = []
81        for cleanup, args, kwargs in cleanups:
82            try:
83                cleanup(*args, **kwargs)
84            except Exception:
85                result.append(sys.exc_info())
86        if result and raise_errors:
87            if 1 == len(result):
88                error = result[0]
89                reraise(error[0], error[1], error[2])
90            else:
91                raise MultipleExceptions(*result)
92        if not raise_errors:
93            return result
94
95    def __enter__(self):
96        return self
97
98    def __exit__(self, exc_type, exc_val, exc_tb):
99        self()
100        return False  # Propagate exceptions from the with body.
101