diff --git a/tests/unit/test_cassettes.py b/tests/unit/test_cassettes.py index bc7d2437..9b573bf2 100644 --- a/tests/unit/test_cassettes.py +++ b/tests/unit/test_cassettes.py @@ -201,3 +201,19 @@ class Test(object): assert issubclass(Test.attribute, VCRHTTPSConnection) assert VCRHTTPSConnection is not Test.attribute assert Test.attribute is old_attribute + + +def test_use_cassette_decorated_functions_are_reentrant(): + info = {"second": False} + original_conn = httplib.HTTPConnection + @Cassette.use('whatever', inject=True) + def test_function(cassette): + if info['second']: + assert httplib.HTTPConnection is not info['first_conn'] + else: + info['first_conn'] = httplib.HTTPConnection + info['second'] = True + test_function() + assert httplib.HTTPConnection is info['first_conn'] + test_function() + assert httplib.HTTPConnection is original_conn diff --git a/vcr/cassette.py b/vcr/cassette.py index c95f0695..c01a763a 100644 --- a/vcr/cassette.py +++ b/vcr/cassette.py @@ -50,6 +50,14 @@ def _patch_generator(self, cassette): cassette._save() def __enter__(self): + # This assertion is here to prevent the dangerous behavior + # that would result from forgetting about a __finish before + # completing it. + # How might this condition be met? Here is an example: + # context_decorator = Cassette.use('whatever') + # with context_decorator: + # with context_decorator: + # pass assert self.__finish is None, "Cassette already open." path, kwargs = self._args_getter() self.__finish = self._patch_generator(self.cls.load(path, **kwargs)) @@ -61,7 +69,11 @@ def __exit__(self, *args): @wrapt.decorator def __call__(self, function, instance, args, kwargs): - with self as cassette: + # This awkward cloning thing is done to ensure that decorated + # functions are reentrant. Reentrancy is required for thread + # safety and the correct operation of recursive functions. + clone = type(self)(self.cls, self._args_getter) + with clone as cassette: if cassette.inject: return function(cassette, *args, **kwargs) else: