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