Fix #66: mock.patch crashes when parameterized.expand tests raise an error
diff --git a/parameterized/parameterized.py b/parameterized/parameterized.py index 96a662f..eb2880a 100644 --- a/parameterized/parameterized.py +++ b/parameterized/parameterized.py
@@ -63,26 +63,6 @@ def skip_on_empty_helper(*a, **kw): raise SkipTest("parameterized input is empty") -def reapply_patches_if_need(func): - - def dummy_wrapper(orgfunc): - @wraps(orgfunc) - def dummy_func(*args, **kwargs): - return orgfunc(*args, **kwargs) - return dummy_func - - if hasattr(func, 'patchings'): - func = dummy_wrapper(func) - tmp_patchings = func.patchings - delattr(func, 'patchings') - for patch_obj in tmp_patchings: - func = patch_obj.decorate_callable(func) - return func - -def delete_patches_if_need(func): - if hasattr(func, 'patchings'): - func.patchings[:] = [] - class param(_param): """ Represents a single parameter to a test case. @@ -496,39 +476,57 @@ digits = len(str(len(paramters) - 1)) for num, p in enumerate(paramters): name = name_func(f, "{num:0>{digits}}".format(digits=digits, num=num), p) - # If the original function has patches applied by 'mock.patch', - # re-construct all patches on the just former decoration layer - # of param_as_standalone_func so as not to share - # patch objects between new functions - nf = reapply_patches_if_need(f) - frame_locals[name] = cls.param_as_standalone_func(p, nf, name) + frame_locals[name] = cls.param_as_standalone_func(p, f, name) frame_locals[name].__doc__ = doc_func(f, num, p) - # Delete original patches to prevent new function from evaluating - # original patching object as well as re-constructed patches. - delete_patches_if_need(f) + if hasattr(f, 'patchings'): + import mock + # There's a "bug" in mock where it will crash if an exception + # is raised in a function that has "patchings" but that + # "patchings" list is empty. Normally this would never happen, + # but it does for us, because we've moved the patchings + # from the inner (wrapped) function to the outer (expanded) + # function. Work around this by adding a dummy patch. + f.patchings[:] = [ + mock.patch("os.__parameterized_mock_patch_helper__", new=None, create=True), + ] f.__test__ = False return parameterized_expand_wrapper @classmethod def param_as_standalone_func(cls, p, func, name): + inner_func = func @wraps(func) def standalone_func(*a): - return func(*(a + p.args), **p.kwargs) + return inner_func(*(a + p.args), **p.kwargs) standalone_func.__name__ = name + if hasattr(func, 'patchings'): + # This is some disgusting code, but regrettably necessary for + # legacy reasons. Basically, because of the way mock support was + # originally implemented, there's an assumption that arguments + # will have the order: + # 1. params + # 2. method mocks + # 3. class mocks + # And the only way to ensure this order is for: + # 1. The mock patching to happen in the "inner" function + # 2. The outer function to share a "patches" list with the inner + # function (so the class-level patch decorator will append to + # the outer function's patch list, but it will be applied in + # the inner function) + def mock_patch_helper_inner_func(*a, **kw): + return func(*a, **kw) + inner_func = mock_patch_helper_inner_func + for patching in func.patchings: + inner_func = patching.decorate_callable(inner_func) + standalone_func.patchings = inner_func.patchings + # place_as is used by py.test to determine what source file should be # used for this test. standalone_func.place_as = func - # Remove __wrapped__ because py.test will try to look at __wrapped__ - # to determine which parameters should be used with this test case, - # and obviously we don't need it to do any parameterization. - try: - del standalone_func.__wrapped__ - except AttributeError: - pass return standalone_func @classmethod
diff --git a/parameterized/test.py b/parameterized/test.py index bda509c..3b21cf6 100644 --- a/parameterized/test.py +++ b/parameterized/test.py
@@ -118,12 +118,9 @@ mock_getpid._mock_name)) expect([ - "test_multiple_function_patch_decorator" - "(42, 51, 'umask', 'fdopen', 'getpid')", - "test_multiple_function_patch_decorator" - "('foo0', 'bar0', 'umask', 'fdopen', 'getpid')", - "test_multiple_function_patch_decorator" - "('foo1', 'bar1', 'umask', 'fdopen', 'getpid')", + "test_multiple_function_patch_decorator(42, 51, 'umask', 'fdopen', 'getpid')", + "test_multiple_function_patch_decorator('foo0', 'bar0', 'umask', 'fdopen', 'getpid')", + "test_multiple_function_patch_decorator('foo1', 'bar1', 'umask', 'fdopen', 'getpid')", ]) @parameterized.expand([(42, 51), ("foo0", "bar0"), param("foo1", "bar1")]) @@ -186,7 +183,7 @@ @parameterized.expand([(42,)]) @mock.patch("os.umask", new=lambda x: x) - def test_one_function_patch_decorator_with_new(foo, mock_umask): + def test_one_function_patch_decorator_with_new(self, foo): raise ValueError()