Skip to content

Commit

Permalink
Mostly implement pytest4 compatibility
Browse files Browse the repository at this point in the history
  • Loading branch information
wolever committed Jan 10, 2021
1 parent 674f238 commit d1b0d1a
Show file tree
Hide file tree
Showing 2 changed files with 119 additions and 25 deletions.
115 changes: 99 additions & 16 deletions parameterized/parameterized.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
class SkipTest(Exception):
pass

try:
import pytest
except ImportError:
pytest = None

PY3 = sys.version_info[0] == 3
PY2 = sys.version_info[0] == 2

Expand Down Expand Up @@ -352,6 +357,94 @@ def __init__(self, input, doc_func=None, skip_on_empty=False):
def __call__(self, test_func):
self.assert_not_in_testcase_subclass()

input = self.get_input()
wrapper = self._wrap_test_func(test_func, input)
wrapper.parameterized_input = input
wrapper.parameterized_func = test_func
test_func.__name__ = "_parameterized_original_%s" %(test_func.__name__, )

return wrapper

def _wrap_test_func(self, test_func, input):
""" Wraps a test function so that it will appropriately handle
parameterization.
In the general case, the wrapper will enumerate the input, yielding
test cases.
In the case of pytest4, the wrapper will use
``@pytest.mark.parametrize`` to parameterize the test function. """

if not input:
if not self.skip_on_empty:
raise ValueError(
"Parameters iterable is empty (hint: use "
"`parameterized([], skip_on_empty=True)` to skip "
"this test when the input is empty)"
)
return wraps(test_func)(skip_on_empty_helper)

if pytest and pytest.__version__ > '4.0.0':
Undefined = object()
test_func_wrapped = test_func
test_func_real, mock_patchings = unwrap_mock_patch_func(test_func_wrapped)
func_argspec = getargspec(test_func_real)

func_args = func_argspec.args
if mock_patchings:
func_args = func_args[:-len(mock_patchings)]

func_args_no_self = func_args
if func_args_no_self[:1] == ["self"]:
func_args_no_self = func_args_no_self[1:]

args_with_default = dict(
(arg, Undefined)
for arg in func_args_no_self
)
for (arg, default) in zip(reversed(func_args_no_self), reversed(func_argspec.defaults or [])):
args_with_default[arg] = default

pytest_params = []
for i in input:
p = dict(args_with_default)
for (arg, val) in zip(func_args_no_self, i.args):
p[arg] = val
p.update(i.kwargs)

# Sanity check: all arguments should now be defined
if any(v is Undefined for v in p.values()):
raise ValueError(
"When parameterizing function %r: no value for arguments: %s" %(
test_func,
", ".join(
repr(arg)
for (arg, val) in p.items()
if val is Undefined
),
)
)

pytest_params.append(pytest.param(*[
p.get(arg) for arg in func_args_no_self
]))

namespace = {
"__test_func": test_func_wrapped,
}
wrapper_name = "parameterized_pytest_wrapper_%s" %(test_func.__name__, )
exec(
"def %s(%s): return __test_func(%s)" %(
wrapper_name,
",".join(func_args),
",".join(func_args),
),
namespace,
namespace,
)

return pytest.mark.parametrize(",".join(func_args_no_self), pytest_params)(namespace[wrapper_name])

@wraps(test_func)
def wrapper(test_self=None):
test_cls = test_self and type(test_self)
Expand All @@ -366,7 +459,7 @@ def wrapper(test_self=None):
) %(test_self, ))

original_doc = wrapper.__doc__
for num, args in enumerate(wrapper.parameterized_input):
for num, args in enumerate(input):
p = param.from_decorator(args)
unbound_func, nose_tuple = self.param_as_nose_tuple(test_self, test_func, num, p)
try:
Expand All @@ -383,21 +476,6 @@ def wrapper(test_self=None):
if test_self is not None:
delattr(test_cls, test_func.__name__)
wrapper.__doc__ = original_doc

input = self.get_input()
if not input:
if not self.skip_on_empty:
raise ValueError(
"Parameters iterable is empty (hint: use "
"`parameterized([], skip_on_empty=True)` to skip "
"this test when the input is empty)"
)
wrapper = wraps(test_func)(skip_on_empty_helper)

wrapper.parameterized_input = input
wrapper.parameterized_func = test_func
test_func.__name__ = "_parameterized_original_%s" %(test_func.__name__, )

return wrapper

def param_as_nose_tuple(self, test_self, func, num, p):
Expand Down Expand Up @@ -618,6 +696,11 @@ def decorator(base_class):

return decorator

def unwrap_mock_patch_func(f):
if not hasattr(f, "patchings"):
return (f, [])
real_func, patchings = unwrap_mock_patch_func(f.__wrapped__)
return (real_func, patchings + f.patchings)

def get_class_name_suffix(params_dict):
if "name" in params_dict:
Expand Down
29 changes: 20 additions & 9 deletions parameterized/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def expect(skip, tests=None):

test_params = [
(42, ),
(42, "bar_val"),
"foo0",
param("foo1"),
param("foo2", bar=42),
Expand All @@ -50,6 +51,7 @@ def expect(skip, tests=None):
"test_naked_function('foo1', bar=None)",
"test_naked_function('foo2', bar=42)",
"test_naked_function(42, bar=None)",
"test_naked_function(42, bar='bar_val')",
])

@parameterized(test_params)
Expand All @@ -63,6 +65,7 @@ class TestParameterized(object):
"test_instance_method('foo1', bar=None)",
"test_instance_method('foo2', bar=42)",
"test_instance_method(42, bar=None)",
"test_instance_method(42, bar='bar_val')",
])

@parameterized(test_params)
Expand Down Expand Up @@ -95,10 +98,16 @@ def test_setup(self, count, *a):
missing_tests.remove("test_setup(%s)" %(self.actual_order, ))


def custom_naming_func(custom_tag):
def custom_naming_func(custom_tag, kw_name):
def custom_naming_func(testcase_func, param_num, param):
return testcase_func.__name__ + ('_%s_name_' % custom_tag) + str(param.args[0])

return (
testcase_func.__name__ +
'_%s_name_' %(custom_tag, ) +
str(param.args[0]) +
# This ... is a bit messy, to properly handle the values in
# `test_params`, but ... it should work.
'_%s' %(param.args[1] if len(param.args) > 1 else param.kwargs.get(kw_name), )
)
return custom_naming_func


Expand Down Expand Up @@ -214,27 +223,29 @@ class TestParamerizedOnTestCase(TestCase):
"test_on_TestCase('foo1', bar=None)",
"test_on_TestCase('foo2', bar=42)",
"test_on_TestCase(42, bar=None)",
"test_on_TestCase(42, bar='bar_val')",
])

@parameterized.expand(test_params)
def test_on_TestCase(self, foo, bar=None):
missing_tests.remove("test_on_TestCase(%r, bar=%r)" %(foo, bar))

expect([
"test_on_TestCase2_custom_name_42(42, bar=None)",
"test_on_TestCase2_custom_name_foo0('foo0', bar=None)",
"test_on_TestCase2_custom_name_foo1('foo1', bar=None)",
"test_on_TestCase2_custom_name_foo2('foo2', bar=42)",
"test_on_TestCase2_custom_name_42_None(42, bar=None)",
"test_on_TestCase2_custom_name_42_bar_val(42, bar='bar_val')",
"test_on_TestCase2_custom_name_foo0_None('foo0', bar=None)",
"test_on_TestCase2_custom_name_foo1_None('foo1', bar=None)",
"test_on_TestCase2_custom_name_foo2_42('foo2', bar=42)",
])

@parameterized.expand(test_params,
name_func=custom_naming_func("custom"))
name_func=custom_naming_func("custom", "bar"))
def test_on_TestCase2(self, foo, bar=None):
stack = inspect.stack()
frame = stack[1]
frame_locals = frame[0].f_locals
nose_test_method_name = frame_locals['a'][0]._testMethodName
expected_name = "test_on_TestCase2_custom_name_" + str(foo)
expected_name = "test_on_TestCase2_custom_name_" + str(foo) + "_" + str(bar)
assert_equal(nose_test_method_name, expected_name,
"Test Method name '%s' did not get customized to expected: '%s'" %
(nose_test_method_name, expected_name))
Expand Down

0 comments on commit d1b0d1a

Please sign in to comment.