From 674f23824328709562303941e9546097b360b4fc Mon Sep 17 00:00:00 2001 From: David Wolever Date: Sat, 9 Jan 2021 16:00:47 -0500 Subject: [PATCH 1/6] Enable pytest4 (tests will fail) --- tox.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) Index: parameterized-0.8.1/tox.ini =================================================================== --- parameterized-0.8.1.orig/tox.ini +++ parameterized-0.8.1/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist=py{27,35,36,py}-{nose,nose2,pytest2,pytest3,unit,unit2},py{37,38,39}-{nose,nose2,pytest3,unit,unit2} +envlist=py{27,35,36,py}-{nose,nose2,pytest2,pytest3,pytest4,unit,unit2},py{37,38,39}-{nose,nose2,pytest3,pytest4,unit,unit2} [testenv] deps= nose @@ -7,13 +7,13 @@ deps= nose2: nose2 pytest2: pytest>=2,<3 pytest3: pytest>=3,<4 - #pytest4: pytest>=4,<5 + pytest4: pytest>=4,<5 unit2: unittest2 commands= nose: nosetests nose2: nose2 pytest2: py.test parameterized/test.py pytest3: py.test parameterized/test.py - #pytest4: py.test parameterized/test.py + pytest4: py.test parameterized/test.py unit: python -m unittest parameterized.test unit2: unit2 parameterized.test Index: parameterized-0.8.1/parameterized/parameterized.py =================================================================== --- parameterized-0.8.1.orig/parameterized/parameterized.py +++ parameterized-0.8.1/parameterized/parameterized.py @@ -19,9 +19,14 @@ except ImportError: class SkipTest(Exception): pass +try: + import pytest +except ImportError: + pytest = None + PY3 = sys.version_info[0] == 3 PY2 = sys.version_info[0] == 2 - +PYTEST4 = pytest and pytest.__version__ >= '4.0.0' if PY3: # Python 3 doesn't have an InstanceType, so just use a dummy type. @@ -352,6 +357,120 @@ class parameterized(object): 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 PYTEST4 and detect_runner() == "pytest": + # pytest >= 4 compatibility is... a bit of work. Basically, the + # only way (I can find) of implementing parameterized testing with + # pytest >= 4 is through the ``@pytest.mark.parameterized`` + # decorator. This decorator has some strange requirements around + # the name and number of arguments to the test function, so this + # wrapper works around that by: + # 1. Introspecting the original test function to determine the + # names and default values of all arguments. + # 2. Creating a new function with the same arguments, but none + # of them are optional:: + # + # def foo(a, b=42): ... + # + # Becomes: + # + # def parameterized_pytest_wrapper_foo(a, b): ... + # + # 3. Merging the ``@parameterized`` parameters with the argument + # default values. + # 4. Generating a list of ``pytest.param(...)`` values, and passing + # that into ``@pytest.mark.parameterized``. + # Some work also needs to be done to support the documented usage + # of ``mock.patch``, which also adds complexity. + 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 with parameters %r " + "(see: 'no value for arguments' in " + "https://github.com/wolever/parameterized#faq)" %( + test_func, + ", ".join( + repr(arg) + for (arg, val) in p.items() + if val is Undefined + ), + i, + ) + ) + + pytest_params.append(pytest.param(*[ + p.get(arg) for arg in func_args_no_self + ])) + + namespace = { + "__parameterized_original_test_func": test_func_wrapped, + } + wrapper_name = "parameterized_pytest_wrapper_%s" %(test_func.__name__, ) + exec( + "def %s(%s, *__args): return __parameterized_original_test_func(%s, *__args)" %( + 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) @@ -366,7 +485,7 @@ class parameterized(object): ) %(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: @@ -383,21 +502,6 @@ class parameterized(object): 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): @@ -618,6 +722,11 @@ def parameterized_class(attrs, input_val 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: Index: parameterized-0.8.1/parameterized/test.py =================================================================== --- parameterized-0.8.1.orig/parameterized/test.py +++ parameterized-0.8.1/parameterized/test.py @@ -6,8 +6,9 @@ from unittest import TestCase from nose.tools import assert_equal, assert_raises from .parameterized import ( - PY3, PY2, parameterized, param, parameterized_argument_value_pairs, - short_repr, detect_runner, parameterized_class, SkipTest, + PY3, PY2, PYTEST4, parameterized, param, + parameterized_argument_value_pairs, short_repr, detect_runner, + parameterized_class, SkipTest, ) def assert_contains(haystack, needle): @@ -40,6 +41,7 @@ def expect(skip, tests=None): test_params = [ (42, ), + (42, "bar_val"), "foo0", param("foo1"), param("foo2", bar=42), @@ -50,6 +52,7 @@ expect("standalone", [ "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) @@ -63,6 +66,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) @@ -95,10 +99,16 @@ if not PYTEST: 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 @@ -137,19 +147,20 @@ class TestParameterizedExpandWithMockPat mock_fdopen._mock_name, mock_getpid._mock_name)) -@mock.patch("os.getpid") -class TestParameterizedExpandWithNoExpand(object): - expect("generator", [ - "test_patch_class_no_expand(42, 51, 'umask', 'getpid')", - ]) +if not (PYTEST4 and detect_runner() == 'pytest'): + @mock.patch("os.getpid") + class TestParameterizedExpandWithNoExpand(object): + expect("generator", [ + "test_patch_class_no_expand(42, 51, 'umask', 'getpid')", + ]) - @parameterized([(42, 51)]) - @mock.patch("os.umask") - def test_patch_class_no_expand(self, foo, bar, mock_umask, mock_getpid): - missing_tests.remove("test_patch_class_no_expand" - "(%r, %r, %r, %r)" % - (foo, bar, mock_umask._mock_name, - mock_getpid._mock_name)) + @parameterized([(42, 51)]) + @mock.patch("os.umask") + def test_patch_class_no_expand(self, foo, bar, mock_umask, mock_getpid): + missing_tests.remove("test_patch_class_no_expand" + "(%r, %r, %r, %r)" % + (foo, bar, mock_umask._mock_name, + mock_getpid._mock_name)) class TestParameterizedExpandWithNoMockPatchForClass(TestCase): @@ -214,6 +225,7 @@ 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) @@ -221,20 +233,21 @@ class TestParamerizedOnTestCase(TestCase 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)) @@ -373,6 +386,8 @@ def tearDownModule(): def test_old_style_classes(): if PY3: raise SkipTest("Py3 doesn't have old-style classes") + if PYTEST4 and detect_runner() == 'pytest': + raise SkipTest("We're not going to worry about old style classes with pytest 4") class OldStyleClass: @parameterized(["foo"]) def parameterized_method(self, param): @@ -552,3 +567,16 @@ class TestUnicodeDocstring(object): def test_with_docstring(self, param): """ Это док-стринг, содержащий не-ascii символы """ pass + +if PYTEST4 and detect_runner() == 'pytest': + def test_missing_argument_error(): + try: + @parameterized([ + (1, ), + ]) + def foo(a, b): + pass + except ValueError as e: + assert_contains(repr(e), "no value for arguments: 'b'") + else: + raise AssertionError("Expected exception not raised") Index: parameterized-0.8.1/README.rst =================================================================== --- parameterized-0.8.1.orig/README.rst +++ parameterized-0.8.1/README.rst @@ -9,11 +9,9 @@ Parameterized testing with any Python te :alt: Circle CI :target: https://circleci.com/gh/wolever/parameterized - -Parameterized testing in Python sucks. - -``parameterized`` fixes that. For everything. Parameterized testing for nose, -parameterized testing for py.test, parameterized testing for unittest. +``parameterized`` provides universal parameterized testing for Python: +parameterized testing for nose, parameterized testing for py.test, +parameterized testing for unittest, parameterized testing for Django. .. code:: python @@ -131,7 +129,7 @@ With unittest (and unittest2):: (note: because unittest does not support test decorators, only tests created with ``@parameterized.expand`` will be executed) -With green:: +With `green`__ :: $ green test_math.py -vvv test_math @@ -161,6 +159,7 @@ With green:: OK (passes=9) +__ https://github.com/CleanCut/green Installation ------------ @@ -237,16 +236,16 @@ __ https://travis-ci.org/wolever/paramet - yes - yes * - py.test 4 - - no** - - no** - - no** - - no** - - no** - - no** - - no** - - no** - - no** - - no** + - yes + - yes + - yes + - yes + - yes + - yes + - yes + - yes + - yes + - yes * - py.test fixtures - no† - no† @@ -285,8 +284,6 @@ __ https://travis-ci.org/wolever/paramet \*: py.test 2 does `does not appear to work (#71)`__ under Python 3. Please comment on the related issues if you are affected. -\*\*: py.test 4 is not yet supported (but coming!) in `issue #34`__ - †: py.test fixture support is documented in `issue #81`__ __ https://github.com/wolever/parameterized/issues/71 @@ -575,7 +572,6 @@ which controls the name of the parameter test_concat (test_concat.TestConcatenation_0_say_cheese__) ... ok - Using with Single Parameters ............................ @@ -616,15 +612,42 @@ can be confusing. The ``@mock.patch(...) .. code:: python - @mock.patch("os.getpid") class TestOS(object): @parameterized(...) @mock.patch("os.fdopen") @mock.patch("os.umask") - def test_method(self, param1, param2, ..., mock_umask, mock_fdopen, mock_getpid): + def test_method(self, param1, param2, ..., mock_umask, mock_fdopen): ... -Note: the same holds true when using ``@parameterized.expand``. +Note 1: the same holds true when using ``@parameterized.expand``. + +Note 2: ``@mock.patch`` is supported with all runners, including ``pytest``, +*except* when used as a *class decorator* with ``pytest>=4``. + +Parameterized testing with Django +................................. + +``parameterized`` enables parameterized testing with Django with +``@parameterized.expand``:: + + from django.test import TestCase + + class DjangoTestCase(TestCase): + @parameterized.expand([ + ("negative", -1.5, -2.0), + ("integer", 1, 1.0), + ("large fraction", 1.6, 1), + ]) + def test_floor(self, name, input, expected): + assert_equal(math.floor(input), expected) + +Which will yield:: + + $ python manage.py test + ... + test_floor_0_negative (test_math.DjangoTestCase) ... ok + test_floor_1_integer (test_math.DjangoTestCase) ... ok + test_floor_2_large_fraction (test_math.DjangoTestCase) ... ok Migrating from ``nose-parameterized`` to ``parameterized`` @@ -650,7 +673,7 @@ What happened to ``nose-parameterized``? only made sense to change the name! What do you mean when you say "nose is best supported"? - There are small caveates with ``py.test`` and ``unittest``: ``py.test`` + There are small caveats with ``py.test`` and ``unittest``: ``py.test`` does not show the parameter values (ex, it will show ``test_add[0]`` instead of ``test_add[1, 2, 3]``), and ``unittest``/``unittest2`` do not support test generators so ``@parameterized.expand`` must be used. @@ -664,3 +687,26 @@ Why do I get an ``AttributeError: 'funct You've likely installed the ``parametrized`` (note the missing *e*) package. Use ``parameterized`` (with the *e*) instead and you'll be all set. + +What is the ``no value for arguments`` error when using ``pytest>=4``? + The ``no value for arguments`` error occurs with ``pytest>=4`` when the + parameters for a method do not supply values for all the test function + arguments. + + For example, consider:: + + @parameterized([ + (1, ), + (2, 3), + ]) + def test_foo(a, b): + pass + + In this case, the error will be ``no value for arguments: 'b' with + paramters (1, )``, because the parameter ``(1, )`` does not provide + a value for the argument ``b``. + + Because ``pytest.mark.parametrized`` - which is used to implement + parametrized testing with ``pytest>=4`` - depends fairly heavily on + argument names, this can also come up if other decorators are used (for + example, if ``@mock.patch`` is used as a class decorator).