diff options
| author | nkozlovskiy <[email protected]> | 2023-09-29 12:24:06 +0300 |
|---|---|---|
| committer | nkozlovskiy <[email protected]> | 2023-09-29 12:41:34 +0300 |
| commit | e0e3e1717e3d33762ce61950504f9637a6e669ed (patch) | |
| tree | bca3ff6939b10ed60c3d5c12439963a1146b9711 /contrib/python/parameterized | |
| parent | 38f2c5852db84c7b4d83adfcb009eb61541d1ccd (diff) | |
add ydb deps
Diffstat (limited to 'contrib/python/parameterized')
| -rw-r--r-- | contrib/python/parameterized/py2/LICENSE.txt | 27 | ||||
| -rw-r--r-- | contrib/python/parameterized/py2/README.rst | 666 | ||||
| -rw-r--r-- | contrib/python/parameterized/py3/.dist-info/METADATA | 669 | ||||
| -rw-r--r-- | contrib/python/parameterized/py3/.dist-info/top_level.txt | 1 | ||||
| -rw-r--r-- | contrib/python/parameterized/py3/LICENSE.txt | 27 | ||||
| -rw-r--r-- | contrib/python/parameterized/py3/README.rst | 654 | ||||
| -rw-r--r-- | contrib/python/parameterized/py3/parameterized/__init__.py | 3 | ||||
| -rw-r--r-- | contrib/python/parameterized/py3/parameterized/parameterized.py | 732 | ||||
| -rw-r--r-- | contrib/python/parameterized/py3/ya.make | 23 | ||||
| -rw-r--r-- | contrib/python/parameterized/ya.make | 18 |
10 files changed, 2820 insertions, 0 deletions
diff --git a/contrib/python/parameterized/py2/LICENSE.txt b/contrib/python/parameterized/py2/LICENSE.txt new file mode 100644 index 00000000000..e58a36e52d8 --- /dev/null +++ b/contrib/python/parameterized/py2/LICENSE.txt @@ -0,0 +1,27 @@ +Unless stated otherwise in the source files, all code is copyright 2010 David +Wolever <[email protected]>. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY DAVID WOLEVER ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +EVENT SHALL DAVID WOLEVER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The views and conclusions contained in the software and documentation are those +of the authors and should not be interpreted as representing official policies, +either expressed or implied, of David Wolever. diff --git a/contrib/python/parameterized/py2/README.rst b/contrib/python/parameterized/py2/README.rst new file mode 100644 index 00000000000..4da8c05f165 --- /dev/null +++ b/contrib/python/parameterized/py2/README.rst @@ -0,0 +1,666 @@ +Parameterized testing with any Python test framework +==================================================== + +.. image:: https://img.shields.io/pypi/v/parameterized.svg + :alt: PyPI + :target: https://pypi.org/project/parameterized/ + +.. image:: https://circleci.com/gh/wolever/parameterized.svg?style=svg + :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. + +.. code:: python + + # test_math.py + from nose.tools import assert_equal + from parameterized import parameterized, parameterized_class + + import unittest + import math + + @parameterized([ + (2, 2, 4), + (2, 3, 8), + (1, 9, 1), + (0, 9, 0), + ]) + def test_pow(base, exponent, expected): + assert_equal(math.pow(base, exponent), expected) + + class TestMathUnitTest(unittest.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) + + @parameterized_class(('a', 'b', 'expected_sum', 'expected_product'), [ + (1, 2, 3, 2), + (5, 5, 10, 25), + ]) + class TestMathClass(unittest.TestCase): + def test_add(self): + assert_equal(self.a + self.b, self.expected_sum) + + def test_multiply(self): + assert_equal(self.a * self.b, self.expected_product) + + @parameterized_class([ + { "a": 3, "expected": 2 }, + { "b": 5, "expected": -4 }, + ]) + class TestMathClassDict(unittest.TestCase): + a = 1 + b = 1 + + def test_subtract(self): + assert_equal(self.a - self.b, self.expected) + + +With nose (and nose2):: + + $ nosetests -v test_math.py + test_floor_0_negative (test_math.TestMathUnitTest) ... ok + test_floor_1_integer (test_math.TestMathUnitTest) ... ok + test_floor_2_large_fraction (test_math.TestMathUnitTest) ... ok + test_math.test_pow(2, 2, 4, {}) ... ok + test_math.test_pow(2, 3, 8, {}) ... ok + test_math.test_pow(1, 9, 1, {}) ... ok + test_math.test_pow(0, 9, 0, {}) ... ok + test_add (test_math.TestMathClass_0) ... ok + test_multiply (test_math.TestMathClass_0) ... ok + test_add (test_math.TestMathClass_1) ... ok + test_multiply (test_math.TestMathClass_1) ... ok + test_subtract (test_math.TestMathClassDict_0) ... ok + + ---------------------------------------------------------------------- + Ran 12 tests in 0.015s + + OK + +As the package name suggests, nose is best supported and will be used for all +further examples. + + +With py.test (version 2.0 and above):: + + $ py.test -v test_math.py + ============================= test session starts ============================== + platform darwin -- Python 3.6.1, pytest-3.1.3, py-1.4.34, pluggy-0.4.0 + collecting ... collected 13 items + + test_math.py::test_pow::[0] PASSED + test_math.py::test_pow::[1] PASSED + test_math.py::test_pow::[2] PASSED + test_math.py::test_pow::[3] PASSED + test_math.py::TestMathUnitTest::test_floor_0_negative PASSED + test_math.py::TestMathUnitTest::test_floor_1_integer PASSED + test_math.py::TestMathUnitTest::test_floor_2_large_fraction PASSED + test_math.py::TestMathClass_0::test_add PASSED + test_math.py::TestMathClass_0::test_multiply PASSED + test_math.py::TestMathClass_1::test_add PASSED + test_math.py::TestMathClass_1::test_multiply PASSED + test_math.py::TestMathClassDict_0::test_subtract PASSED + ==================== 12 passed, 4 warnings in 0.16 seconds ===================== + +With unittest (and unittest2):: + + $ python -m unittest -v test_math + test_floor_0_negative (test_math.TestMathUnitTest) ... ok + test_floor_1_integer (test_math.TestMathUnitTest) ... ok + test_floor_2_large_fraction (test_math.TestMathUnitTest) ... ok + test_add (test_math.TestMathClass_0) ... ok + test_multiply (test_math.TestMathClass_0) ... ok + test_add (test_math.TestMathClass_1) ... ok + test_multiply (test_math.TestMathClass_1) ... ok + test_subtract (test_math.TestMathClassDict_0) ... ok + + ---------------------------------------------------------------------- + Ran 8 tests in 0.001s + + OK + +(note: because unittest does not support test decorators, only tests created +with ``@parameterized.expand`` will be executed) + +With green:: + + $ green test_math.py -vvv + test_math + TestMathClass_1 + . test_method_a + . test_method_b + TestMathClass_2 + . test_method_a + . test_method_b + TestMathClass_3 + . test_method_a + . test_method_b + TestMathUnitTest + . test_floor_0_negative + . test_floor_1_integer + . test_floor_2_large_fraction + TestMathClass_0 + . test_add + . test_multiply + TestMathClass_1 + . test_add + . test_multiply + TestMathClassDict_0 + . test_subtract + + Ran 12 tests in 0.121s + + OK (passes=9) + + +Installation +------------ + +:: + + $ pip install parameterized + + +Compatibility +------------- + +`Yes`__ (mostly). + +__ https://travis-ci.org/wolever/parameterized + +.. list-table:: + :header-rows: 1 + :stub-columns: 1 + + * - + - Py2.6 + - Py2.7 + - Py3.4 + - Py3.5 + - Py3.6 + - Py3.7 + - Py3.8 + - Py3.9 + - PyPy + - ``@mock.patch`` + * - nose + - yes + - yes + - yes + - yes + - yes + - yes + - yes + - yes + - yes + - yes + * - nose2 + - yes + - yes + - yes + - yes + - yes + - yes + - yes + - yes + - yes + - yes + * - py.test 2 + - yes + - yes + - no* + - no* + - no* + - no* + - yes + - yes + - yes + - yes + * - py.test 3 + - yes + - yes + - yes + - yes + - yes + - yes + - yes + - yes + - yes + - yes + * - py.test 4 + - no** + - no** + - no** + - no** + - no** + - no** + - no** + - no** + - no** + - no** + * - py.test fixtures + - no† + - no† + - no† + - no† + - no† + - no† + - no† + - no† + - no† + - no† + * - | unittest + | (``@parameterized.expand``) + - yes + - yes + - yes + - yes + - yes + - yes + - yes + - yes + - yes + - yes + * - | unittest2 + | (``@parameterized.expand``) + - yes + - yes + - yes + - yes + - yes + - yes + - yes + - yes + - yes + - yes + +\*: 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 +__ https://github.com/wolever/parameterized/issues/34 +__ https://github.com/wolever/parameterized/issues/81 + +Dependencies +------------ + +(this section left intentionally blank) + + +Exhaustive Usage Examples +-------------------------- + +The ``@parameterized`` and ``@parameterized.expand`` decorators accept a list +or iterable of tuples or ``param(...)``, or a callable which returns a list or +iterable: + +.. code:: python + + from parameterized import parameterized, param + + # A list of tuples + @parameterized([ + (2, 3, 5), + (3, 5, 8), + ]) + def test_add(a, b, expected): + assert_equal(a + b, expected) + + # A list of params + @parameterized([ + param("10", 10), + param("10", 16, base=16), + ]) + def test_int(str_val, expected, base=10): + assert_equal(int(str_val, base=base), expected) + + # An iterable of params + @parameterized( + param.explicit(*json.loads(line)) + for line in open("testcases.jsons") + ) + def test_from_json_file(...): + ... + + # A callable which returns a list of tuples + def load_test_cases(): + return [ + ("test1", ), + ("test2", ), + ] + @parameterized(load_test_cases) + def test_from_function(name): + ... + +.. ** + +Note that, when using an iterator or a generator, all the items will be loaded +into memory before the start of the test run (we do this explicitly to ensure +that generators are exhausted exactly once in multi-process or multi-threaded +testing environments). + +The ``@parameterized`` decorator can be used test class methods, and standalone +functions: + +.. code:: python + + from parameterized import parameterized + + class AddTest(object): + @parameterized([ + (2, 3, 5), + ]) + def test_add(self, a, b, expected): + assert_equal(a + b, expected) + + @parameterized([ + (2, 3, 5), + ]) + def test_add(a, b, expected): + assert_equal(a + b, expected) + + +And ``@parameterized.expand`` can be used to generate test methods in +situations where test generators cannot be used (for example, when the test +class is a subclass of ``unittest.TestCase``): + +.. code:: python + + import unittest + from parameterized import parameterized + + class AddTestCase(unittest.TestCase): + @parameterized.expand([ + ("2 and 3", 2, 3, 5), + ("3 and 5", 2, 3, 5), + ]) + def test_add(self, _, a, b, expected): + assert_equal(a + b, expected) + +Will create the test cases:: + + $ nosetests example.py + test_add_0_2_and_3 (example.AddTestCase) ... ok + test_add_1_3_and_5 (example.AddTestCase) ... ok + + ---------------------------------------------------------------------- + Ran 2 tests in 0.001s + + OK + +Note that ``@parameterized.expand`` works by creating new methods on the test +class. If the first parameter is a string, that string will be added to the end +of the method name. For example, the test case above will generate the methods +``test_add_0_2_and_3`` and ``test_add_1_3_and_5``. + +The names of the test cases generated by ``@parameterized.expand`` can be +customized using the ``name_func`` keyword argument. The value should +be a function which accepts three arguments: ``testcase_func``, ``param_num``, +and ``params``, and it should return the name of the test case. +``testcase_func`` will be the function to be tested, ``param_num`` will be the +index of the test case parameters in the list of parameters, and ``param`` +(an instance of ``param``) will be the parameters which will be used. + +.. code:: python + + import unittest + from parameterized import parameterized + + def custom_name_func(testcase_func, param_num, param): + return "%s_%s" %( + testcase_func.__name__, + parameterized.to_safe_name("_".join(str(x) for x in param.args)), + ) + + class AddTestCase(unittest.TestCase): + @parameterized.expand([ + (2, 3, 5), + (2, 3, 5), + ], name_func=custom_name_func) + def test_add(self, a, b, expected): + assert_equal(a + b, expected) + +Will create the test cases:: + + $ nosetests example.py + test_add_1_2_3 (example.AddTestCase) ... ok + test_add_2_3_5 (example.AddTestCase) ... ok + + ---------------------------------------------------------------------- + Ran 2 tests in 0.001s + + OK + + +The ``param(...)`` helper class stores the parameters for one specific test +case. It can be used to pass keyword arguments to test cases: + +.. code:: python + + from parameterized import parameterized, param + + @parameterized([ + param("10", 10), + param("10", 16, base=16), + ]) + def test_int(str_val, expected, base=10): + assert_equal(int(str_val, base=base), expected) + + +If test cases have a docstring, the parameters for that test case will be +appended to the first line of the docstring. This behavior can be controlled +with the ``doc_func`` argument: + +.. code:: python + + from parameterized import parameterized + + @parameterized([ + (1, 2, 3), + (4, 5, 9), + ]) + def test_add(a, b, expected): + """ Test addition. """ + assert_equal(a + b, expected) + + def my_doc_func(func, num, param): + return "%s: %s with %s" %(num, func.__name__, param) + + @parameterized([ + (5, 4, 1), + (9, 6, 3), + ], doc_func=my_doc_func) + def test_subtraction(a, b, expected): + assert_equal(a - b, expected) + +:: + + $ nosetests example.py + Test addition. [with a=1, b=2, expected=3] ... ok + Test addition. [with a=4, b=5, expected=9] ... ok + 0: test_subtraction with param(*(5, 4, 1)) ... ok + 1: test_subtraction with param(*(9, 6, 3)) ... ok + + ---------------------------------------------------------------------- + Ran 4 tests in 0.001s + + OK + +Finally ``@parameterized_class`` parameterizes an entire class, using +either a list of attributes, or a list of dicts that will be applied to the +class: + +.. code:: python + + from yourapp.models import User + from parameterized import parameterized_class + + @parameterized_class([ + { "username": "user_1", "access_level": 1 }, + { "username": "user_2", "access_level": 2, "expected_status_code": 404 }, + ]) + class TestUserAccessLevel(TestCase): + expected_status_code = 200 + + def setUp(self): + self.client.force_login(User.objects.get(username=self.username)[0]) + + def test_url_a(self): + response = self.client.get('/url') + self.assertEqual(response.status_code, self.expected_status_code) + + def tearDown(self): + self.client.logout() + + + @parameterized_class(("username", "access_level", "expected_status_code"), [ + ("user_1", 1, 200), + ("user_2", 2, 404) + ]) + class TestUserAccessLevel(TestCase): + def setUp(self): + self.client.force_login(User.objects.get(username=self.username)[0]) + + def test_url_a(self): + response = self.client.get("/url") + self.assertEqual(response.status_code, self.expected_status_code) + + def tearDown(self): + self.client.logout() + + +The ``@parameterized_class`` decorator accepts a ``class_name_func`` argument, +which controls the name of the parameterized classes generated by +``@parameterized_class``: + +.. code:: python + + from parameterized import parameterized, parameterized_class + + def get_class_name(cls, num, params_dict): + # By default the generated class named includes either the "name" + # parameter (if present), or the first string value. This example shows + # multiple parameters being included in the generated class name: + return "%s_%s_%s%s" %( + cls.__name__, + num, + parameterized.to_safe_name(params_dict['a']), + parameterized.to_safe_name(params_dict['b']), + ) + + @parameterized_class([ + { "a": "hello", "b": " world!", "expected": "hello world!" }, + { "a": "say ", "b": " cheese :)", "expected": "say cheese :)" }, + ], class_name_func=get_class_name) + class TestConcatenation(TestCase): + def test_concat(self): + self.assertEqual(self.a + self.b, self.expected) + +:: + + $ nosetests -v test_math.py + test_concat (test_concat.TestConcatenation_0_hello_world_) ... ok + test_concat (test_concat.TestConcatenation_0_say_cheese__) ... ok + + + +Using with Single Parameters +............................ + +If a test function only accepts one parameter and the value is not iterable, +then it is possible to supply a list of values without wrapping each one in a +tuple: + +.. code:: python + + @parameterized([1, 2, 3]) + def test_greater_than_zero(value): + assert value > 0 + +Note, however, that if the single parameter *is* iterable (such as a list or +tuple), then it *must* be wrapped in a tuple, list, or the ``param(...)`` +helper: + +.. code:: python + + @parameterized([ + ([1, 2, 3], ), + ([3, 3], ), + ([6], ), + ]) + def test_sums_to_6(numbers): + assert sum(numbers) == 6 + +(note, also, that Python requires single element tuples to be defined with a +trailing comma: ``(foo, )``) + + +Using with ``@mock.patch`` +.......................... + +``parameterized`` can be used with ``mock.patch``, but the argument ordering +can be confusing. The ``@mock.patch(...)`` decorator must come *below* the +``@parameterized(...)``, and the mocked parameters must come *last*: + +.. 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): + ... + +Note: the same holds true when using ``@parameterized.expand``. + + +Migrating from ``nose-parameterized`` to ``parameterized`` +---------------------------------------------------------- + +To migrate a codebase from ``nose-parameterized`` to ``parameterized``: + +1. Update your requirements file, replacing ``nose-parameterized`` with + ``parameterized``. + +2. Replace all references to ``nose_parameterized`` with ``parameterized``:: + + $ perl -pi -e 's/nose_parameterized/parameterized/g' your-codebase/ + +3. You're done! + + +FAQ +--- + +What happened to ``nose-parameterized``? + Originally only nose was supported. But now everything is supported, and it + 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`` + 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. + +Why not use ``@pytest.mark.parametrize``? + Because spelling is difficult. Also, ``parameterized`` doesn't require you + to repeat argument names, and (using ``param``) it supports optional + keyword arguments. + +Why do I get an ``AttributeError: 'function' object has no attribute 'expand'`` with ``@parameterized.expand``? + You've likely installed the ``parametrized`` (note the missing *e*) + package. Use ``parameterized`` (with the *e*) instead and you'll be all + set. diff --git a/contrib/python/parameterized/py3/.dist-info/METADATA b/contrib/python/parameterized/py3/.dist-info/METADATA new file mode 100644 index 00000000000..531c64fee64 --- /dev/null +++ b/contrib/python/parameterized/py3/.dist-info/METADATA @@ -0,0 +1,669 @@ +Metadata-Version: 2.1 +Name: parameterized +Version: 0.9.0 +Summary: Parameterized testing with any Python test framework +Author-email: David Wolever <[email protected]> +License: FreeBSD +Project-URL: Homepage, https://github.com/wolever/parameterized +Classifier: Programming Language :: Python :: 3 +Classifier: License :: OSI Approved :: BSD License +Requires-Python: >=3.7 +Description-Content-Type: text/x-rst +License-File: LICENSE.txt +Provides-Extra: dev +Requires-Dist: jinja2 ; extra == 'dev' + +Parameterized testing with any Python test framework +==================================================== + +.. image:: https://img.shields.io/pypi/v/parameterized + :alt: PyPI + :target: https://pypi.org/project/parameterized/ + +.. image:: https://img.shields.io/pypi/dm/parameterized + :alt: PyPI - Downloads + :target: https://pypi.org/project/parameterized/ + +.. image:: https://circleci.com/gh/wolever/parameterized.svg?style=svg + :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. + +.. code:: python + + # test_math.py + from nose.tools import assert_equal + from parameterized import parameterized, parameterized_class + + import unittest + import math + + @parameterized([ + (2, 2, 4), + (2, 3, 8), + (1, 9, 1), + (0, 9, 0), + ]) + def test_pow(base, exponent, expected): + assert_equal(math.pow(base, exponent), expected) + + class TestMathUnitTest(unittest.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) + + @parameterized_class(('a', 'b', 'expected_sum', 'expected_product'), [ + (1, 2, 3, 2), + (5, 5, 10, 25), + ]) + class TestMathClass(unittest.TestCase): + def test_add(self): + assert_equal(self.a + self.b, self.expected_sum) + + def test_multiply(self): + assert_equal(self.a * self.b, self.expected_product) + + @parameterized_class([ + { "a": 3, "expected": 2 }, + { "b": 5, "expected": -4 }, + ]) + class TestMathClassDict(unittest.TestCase): + a = 1 + b = 1 + + def test_subtract(self): + assert_equal(self.a - self.b, self.expected) + + +With nose (and nose2):: + + $ nosetests -v test_math.py + test_floor_0_negative (test_math.TestMathUnitTest) ... ok + test_floor_1_integer (test_math.TestMathUnitTest) ... ok + test_floor_2_large_fraction (test_math.TestMathUnitTest) ... ok + test_math.test_pow(2, 2, 4, {}) ... ok + test_math.test_pow(2, 3, 8, {}) ... ok + test_math.test_pow(1, 9, 1, {}) ... ok + test_math.test_pow(0, 9, 0, {}) ... ok + test_add (test_math.TestMathClass_0) ... ok + test_multiply (test_math.TestMathClass_0) ... ok + test_add (test_math.TestMathClass_1) ... ok + test_multiply (test_math.TestMathClass_1) ... ok + test_subtract (test_math.TestMathClassDict_0) ... ok + + ---------------------------------------------------------------------- + Ran 12 tests in 0.015s + + OK + +As the package name suggests, nose is best supported and will be used for all +further examples. + + +With py.test (version 2.0 and above):: + + $ py.test -v test_math.py + ============================= test session starts ============================== + platform darwin -- Python 3.6.1, pytest-3.1.3, py-1.4.34, pluggy-0.4.0 + collecting ... collected 13 items + + test_math.py::test_pow::[0] PASSED + test_math.py::test_pow::[1] PASSED + test_math.py::test_pow::[2] PASSED + test_math.py::test_pow::[3] PASSED + test_math.py::TestMathUnitTest::test_floor_0_negative PASSED + test_math.py::TestMathUnitTest::test_floor_1_integer PASSED + test_math.py::TestMathUnitTest::test_floor_2_large_fraction PASSED + test_math.py::TestMathClass_0::test_add PASSED + test_math.py::TestMathClass_0::test_multiply PASSED + test_math.py::TestMathClass_1::test_add PASSED + test_math.py::TestMathClass_1::test_multiply PASSED + test_math.py::TestMathClassDict_0::test_subtract PASSED + ==================== 12 passed, 4 warnings in 0.16 seconds ===================== + +With unittest (and unittest2):: + + $ python -m unittest -v test_math + test_floor_0_negative (test_math.TestMathUnitTest) ... ok + test_floor_1_integer (test_math.TestMathUnitTest) ... ok + test_floor_2_large_fraction (test_math.TestMathUnitTest) ... ok + test_add (test_math.TestMathClass_0) ... ok + test_multiply (test_math.TestMathClass_0) ... ok + test_add (test_math.TestMathClass_1) ... ok + test_multiply (test_math.TestMathClass_1) ... ok + test_subtract (test_math.TestMathClassDict_0) ... ok + + ---------------------------------------------------------------------- + Ran 8 tests in 0.001s + + OK + +(note: because unittest does not support test decorators, only tests created +with ``@parameterized.expand`` will be executed) + +With green:: + + $ green test_math.py -vvv + test_math + TestMathClass_1 + . test_method_a + . test_method_b + TestMathClass_2 + . test_method_a + . test_method_b + TestMathClass_3 + . test_method_a + . test_method_b + TestMathUnitTest + . test_floor_0_negative + . test_floor_1_integer + . test_floor_2_large_fraction + TestMathClass_0 + . test_add + . test_multiply + TestMathClass_1 + . test_add + . test_multiply + TestMathClassDict_0 + . test_subtract + + Ran 12 tests in 0.121s + + OK (passes=9) + + +Installation +------------ + +:: + + $ pip install parameterized + + +Compatibility +------------- + +`Yes`__ (mostly). + +__ https://app.circleci.com/pipelines/github/wolever/parameterized?branch=master + +.. list-table:: + :header-rows: 1 + :stub-columns: 1 + + * - + - Py3.7 + - Py3.8 + - Py3.9 + - Py3.10 + - Py3.11 + - PyPy3 + - ``@mock.patch`` + * - nose + - yes + - yes + - yes + - yes + - no§ + - no§ + - yes + * - nose2 + - yes + - yes + - yes + - yes + - yes + - yes + - yes + * - py.test 2 + - no* + - no* + - no* + - no* + - no* + - no* + - no* + * - py.test 3 + - yes + - yes + - yes + - yes + - no* + - no* + - yes + * - py.test 4 + - no** + - no** + - no** + - no** + - no** + - no** + - no** + * - py.test fixtures + - no† + - no† + - no† + - no† + - no† + - no† + - no† + * - | unittest + | (``@parameterized.expand``) + - yes + - yes + - yes + - yes + - yes + - yes + - yes + * - | unittest2 + | (``@parameterized.expand``) + - yes + - yes + - yes + - yes + - no§ + - no§ + - yes + +§: nose and unittest2 - both of which were last updated in 2015 - sadly do not +appear to support Python 3.10 or 3.11. + +\*: `py.test 2 does not appear to work under Python 3 (#71)`__, and +`py.test 3 does not appear to work under Python 3.10 or 3.11 (#154)`__. + +\*\*: 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 +__ https://github.com/wolever/parameterized/issues/154 +__ https://github.com/wolever/parameterized/issues/34 +__ https://github.com/wolever/parameterized/issues/81 + +Dependencies +------------ + +(this section left intentionally blank) + + +Exhaustive Usage Examples +-------------------------- + +The ``@parameterized`` and ``@parameterized.expand`` decorators accept a list +or iterable of tuples or ``param(...)``, or a callable which returns a list or +iterable: + +.. code:: python + + from parameterized import parameterized, param + + # A list of tuples + @parameterized([ + (2, 3, 5), + (3, 5, 8), + ]) + def test_add(a, b, expected): + assert_equal(a + b, expected) + + # A list of params + @parameterized([ + param("10", 10), + param("10", 16, base=16), + ]) + def test_int(str_val, expected, base=10): + assert_equal(int(str_val, base=base), expected) + + # An iterable of params + @parameterized( + param.explicit(*json.loads(line)) + for line in open("testcases.jsons") + ) + def test_from_json_file(...): + ... + + # A callable which returns a list of tuples + def load_test_cases(): + return [ + ("test1", ), + ("test2", ), + ] + @parameterized(load_test_cases) + def test_from_function(name): + ... + +.. ** + +Note that, when using an iterator or a generator, all the items will be loaded +into memory before the start of the test run (we do this explicitly to ensure +that generators are exhausted exactly once in multi-process or multi-threaded +testing environments). + +The ``@parameterized`` decorator can be used test class methods, and standalone +functions: + +.. code:: python + + from parameterized import parameterized + + class AddTest(object): + @parameterized([ + (2, 3, 5), + ]) + def test_add(self, a, b, expected): + assert_equal(a + b, expected) + + @parameterized([ + (2, 3, 5), + ]) + def test_add(a, b, expected): + assert_equal(a + b, expected) + + +And ``@parameterized.expand`` can be used to generate test methods in +situations where test generators cannot be used (for example, when the test +class is a subclass of ``unittest.TestCase``): + +.. code:: python + + import unittest + from parameterized import parameterized + + class AddTestCase(unittest.TestCase): + @parameterized.expand([ + ("2 and 3", 2, 3, 5), + ("3 and 5", 3, 5, 8), + ]) + def test_add(self, _, a, b, expected): + assert_equal(a + b, expected) + +Will create the test cases:: + + $ nosetests example.py + test_add_0_2_and_3 (example.AddTestCase) ... ok + test_add_1_3_and_5 (example.AddTestCase) ... ok + + ---------------------------------------------------------------------- + Ran 2 tests in 0.001s + + OK + +Note that ``@parameterized.expand`` works by creating new methods on the test +class. If the first parameter is a string, that string will be added to the end +of the method name. For example, the test case above will generate the methods +``test_add_0_2_and_3`` and ``test_add_1_3_and_5``. + +The names of the test cases generated by ``@parameterized.expand`` can be +customized using the ``name_func`` keyword argument. The value should +be a function which accepts three arguments: ``testcase_func``, ``param_num``, +and ``params``, and it should return the name of the test case. +``testcase_func`` will be the function to be tested, ``param_num`` will be the +index of the test case parameters in the list of parameters, and ``param`` +(an instance of ``param``) will be the parameters which will be used. + +.. code:: python + + import unittest + from parameterized import parameterized + + def custom_name_func(testcase_func, param_num, param): + return "%s_%s" %( + testcase_func.__name__, + parameterized.to_safe_name("_".join(str(x) for x in param.args)), + ) + + class AddTestCase(unittest.TestCase): + @parameterized.expand([ + (2, 3, 5), + (2, 3, 5), + ], name_func=custom_name_func) + def test_add(self, a, b, expected): + assert_equal(a + b, expected) + +Will create the test cases:: + + $ nosetests example.py + test_add_1_2_3 (example.AddTestCase) ... ok + test_add_2_3_5 (example.AddTestCase) ... ok + + ---------------------------------------------------------------------- + Ran 2 tests in 0.001s + + OK + + +The ``param(...)`` helper class stores the parameters for one specific test +case. It can be used to pass keyword arguments to test cases: + +.. code:: python + + from parameterized import parameterized, param + + @parameterized([ + param("10", 10), + param("10", 16, base=16), + ]) + def test_int(str_val, expected, base=10): + assert_equal(int(str_val, base=base), expected) + + +If test cases have a docstring, the parameters for that test case will be +appended to the first line of the docstring. This behavior can be controlled +with the ``doc_func`` argument: + +.. code:: python + + from parameterized import parameterized + + @parameterized([ + (1, 2, 3), + (4, 5, 9), + ]) + def test_add(a, b, expected): + """ Test addition. """ + assert_equal(a + b, expected) + + def my_doc_func(func, num, param): + return "%s: %s with %s" %(num, func.__name__, param) + + @parameterized([ + (5, 4, 1), + (9, 6, 3), + ], doc_func=my_doc_func) + def test_subtraction(a, b, expected): + assert_equal(a - b, expected) + +:: + + $ nosetests example.py + Test addition. [with a=1, b=2, expected=3] ... ok + Test addition. [with a=4, b=5, expected=9] ... ok + 0: test_subtraction with param(*(5, 4, 1)) ... ok + 1: test_subtraction with param(*(9, 6, 3)) ... ok + + ---------------------------------------------------------------------- + Ran 4 tests in 0.001s + + OK + +Finally ``@parameterized_class`` parameterizes an entire class, using +either a list of attributes, or a list of dicts that will be applied to the +class: + +.. code:: python + + from yourapp.models import User + from parameterized import parameterized_class + + @parameterized_class([ + { "username": "user_1", "access_level": 1 }, + { "username": "user_2", "access_level": 2, "expected_status_code": 404 }, + ]) + class TestUserAccessLevel(TestCase): + expected_status_code = 200 + + def setUp(self): + self.client.force_login(User.objects.get(username=self.username)[0]) + + def test_url_a(self): + response = self.client.get('/url') + self.assertEqual(response.status_code, self.expected_status_code) + + def tearDown(self): + self.client.logout() + + + @parameterized_class(("username", "access_level", "expected_status_code"), [ + ("user_1", 1, 200), + ("user_2", 2, 404) + ]) + class TestUserAccessLevel(TestCase): + def setUp(self): + self.client.force_login(User.objects.get(username=self.username)[0]) + + def test_url_a(self): + response = self.client.get("/url") + self.assertEqual(response.status_code, self.expected_status_code) + + def tearDown(self): + self.client.logout() + + +The ``@parameterized_class`` decorator accepts a ``class_name_func`` argument, +which controls the name of the parameterized classes generated by +``@parameterized_class``: + +.. code:: python + + from parameterized import parameterized, parameterized_class + + def get_class_name(cls, num, params_dict): + # By default the generated class named includes either the "name" + # parameter (if present), or the first string value. This example shows + # multiple parameters being included in the generated class name: + return "%s_%s_%s%s" %( + cls.__name__, + num, + parameterized.to_safe_name(params_dict['a']), + parameterized.to_safe_name(params_dict['b']), + ) + + @parameterized_class([ + { "a": "hello", "b": " world!", "expected": "hello world!" }, + { "a": "say ", "b": " cheese :)", "expected": "say cheese :)" }, + ], class_name_func=get_class_name) + class TestConcatenation(TestCase): + def test_concat(self): + self.assertEqual(self.a + self.b, self.expected) + +:: + + $ nosetests -v test_math.py + test_concat (test_concat.TestConcatenation_0_hello_world_) ... ok + test_concat (test_concat.TestConcatenation_0_say_cheese__) ... ok + + + +Using with Single Parameters +............................ + +If a test function only accepts one parameter and the value is not iterable, +then it is possible to supply a list of values without wrapping each one in a +tuple: + +.. code:: python + + @parameterized([1, 2, 3]) + def test_greater_than_zero(value): + assert value > 0 + +Note, however, that if the single parameter *is* iterable (such as a list or +tuple), then it *must* be wrapped in a tuple, list, or the ``param(...)`` +helper: + +.. code:: python + + @parameterized([ + ([1, 2, 3], ), + ([3, 3], ), + ([6], ), + ]) + def test_sums_to_6(numbers): + assert sum(numbers) == 6 + +(note, also, that Python requires single element tuples to be defined with a +trailing comma: ``(foo, )``) + + +Using with ``@mock.patch`` +.......................... + +``parameterized`` can be used with ``mock.patch``, but the argument ordering +can be confusing. The ``@mock.patch(...)`` decorator must come *below* the +``@parameterized(...)``, and the mocked parameters must come *last*: + +.. 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): + ... + +Note: the same holds true when using ``@parameterized.expand``. + + +Migrating from ``nose-parameterized`` to ``parameterized`` +---------------------------------------------------------- + +To migrate a codebase from ``nose-parameterized`` to ``parameterized``: + +1. Update your requirements file, replacing ``nose-parameterized`` with + ``parameterized``. + +2. Replace all references to ``nose_parameterized`` with ``parameterized``:: + + $ perl -pi -e 's/nose_parameterized/parameterized/g' your-codebase/ + +3. You're done! + + +FAQ +--- + +What happened to Python 2.X, 3.5, and 3.6 support? + As of version 0.9.0, ``parameterized`` no longer supports Python 2.X, 3.5, + or 3.6. Previous versions of ``parameterized`` - 0.8.1 being the latest - + will continue to work, but will not receive any new features or bug fixes. + +What do you mean when you say "nose is best supported"? + There are small caveates 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. + +Why not use ``@pytest.mark.parametrize``? + Because spelling is difficult. Also, ``parameterized`` doesn't require you + to repeat argument names, and (using ``param``) it supports optional + keyword arguments. + +Why do I get an ``AttributeError: 'function' object has no attribute 'expand'`` with ``@parameterized.expand``? + You've likely installed the ``parametrized`` (note the missing *e*) + package. Use ``parameterized`` (with the *e*) instead and you'll be all + set. + +What happened to ``nose-parameterized``? + Originally only nose was supported. But now everything is supported, and it + only made sense to change the name! diff --git a/contrib/python/parameterized/py3/.dist-info/top_level.txt b/contrib/python/parameterized/py3/.dist-info/top_level.txt new file mode 100644 index 00000000000..f543eed175e --- /dev/null +++ b/contrib/python/parameterized/py3/.dist-info/top_level.txt @@ -0,0 +1 @@ +parameterized diff --git a/contrib/python/parameterized/py3/LICENSE.txt b/contrib/python/parameterized/py3/LICENSE.txt new file mode 100644 index 00000000000..e58a36e52d8 --- /dev/null +++ b/contrib/python/parameterized/py3/LICENSE.txt @@ -0,0 +1,27 @@ +Unless stated otherwise in the source files, all code is copyright 2010 David +Wolever <[email protected]>. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY DAVID WOLEVER ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +EVENT SHALL DAVID WOLEVER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The views and conclusions contained in the software and documentation are those +of the authors and should not be interpreted as representing official policies, +either expressed or implied, of David Wolever. diff --git a/contrib/python/parameterized/py3/README.rst b/contrib/python/parameterized/py3/README.rst new file mode 100644 index 00000000000..2afd207a5c7 --- /dev/null +++ b/contrib/python/parameterized/py3/README.rst @@ -0,0 +1,654 @@ +Parameterized testing with any Python test framework +==================================================== + +.. image:: https://img.shields.io/pypi/v/parameterized + :alt: PyPI + :target: https://pypi.org/project/parameterized/ + +.. image:: https://img.shields.io/pypi/dm/parameterized + :alt: PyPI - Downloads + :target: https://pypi.org/project/parameterized/ + +.. image:: https://circleci.com/gh/wolever/parameterized.svg?style=svg + :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. + +.. code:: python + + # test_math.py + from nose.tools import assert_equal + from parameterized import parameterized, parameterized_class + + import unittest + import math + + @parameterized([ + (2, 2, 4), + (2, 3, 8), + (1, 9, 1), + (0, 9, 0), + ]) + def test_pow(base, exponent, expected): + assert_equal(math.pow(base, exponent), expected) + + class TestMathUnitTest(unittest.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) + + @parameterized_class(('a', 'b', 'expected_sum', 'expected_product'), [ + (1, 2, 3, 2), + (5, 5, 10, 25), + ]) + class TestMathClass(unittest.TestCase): + def test_add(self): + assert_equal(self.a + self.b, self.expected_sum) + + def test_multiply(self): + assert_equal(self.a * self.b, self.expected_product) + + @parameterized_class([ + { "a": 3, "expected": 2 }, + { "b": 5, "expected": -4 }, + ]) + class TestMathClassDict(unittest.TestCase): + a = 1 + b = 1 + + def test_subtract(self): + assert_equal(self.a - self.b, self.expected) + + +With nose (and nose2):: + + $ nosetests -v test_math.py + test_floor_0_negative (test_math.TestMathUnitTest) ... ok + test_floor_1_integer (test_math.TestMathUnitTest) ... ok + test_floor_2_large_fraction (test_math.TestMathUnitTest) ... ok + test_math.test_pow(2, 2, 4, {}) ... ok + test_math.test_pow(2, 3, 8, {}) ... ok + test_math.test_pow(1, 9, 1, {}) ... ok + test_math.test_pow(0, 9, 0, {}) ... ok + test_add (test_math.TestMathClass_0) ... ok + test_multiply (test_math.TestMathClass_0) ... ok + test_add (test_math.TestMathClass_1) ... ok + test_multiply (test_math.TestMathClass_1) ... ok + test_subtract (test_math.TestMathClassDict_0) ... ok + + ---------------------------------------------------------------------- + Ran 12 tests in 0.015s + + OK + +As the package name suggests, nose is best supported and will be used for all +further examples. + + +With py.test (version 2.0 and above):: + + $ py.test -v test_math.py + ============================= test session starts ============================== + platform darwin -- Python 3.6.1, pytest-3.1.3, py-1.4.34, pluggy-0.4.0 + collecting ... collected 13 items + + test_math.py::test_pow::[0] PASSED + test_math.py::test_pow::[1] PASSED + test_math.py::test_pow::[2] PASSED + test_math.py::test_pow::[3] PASSED + test_math.py::TestMathUnitTest::test_floor_0_negative PASSED + test_math.py::TestMathUnitTest::test_floor_1_integer PASSED + test_math.py::TestMathUnitTest::test_floor_2_large_fraction PASSED + test_math.py::TestMathClass_0::test_add PASSED + test_math.py::TestMathClass_0::test_multiply PASSED + test_math.py::TestMathClass_1::test_add PASSED + test_math.py::TestMathClass_1::test_multiply PASSED + test_math.py::TestMathClassDict_0::test_subtract PASSED + ==================== 12 passed, 4 warnings in 0.16 seconds ===================== + +With unittest (and unittest2):: + + $ python -m unittest -v test_math + test_floor_0_negative (test_math.TestMathUnitTest) ... ok + test_floor_1_integer (test_math.TestMathUnitTest) ... ok + test_floor_2_large_fraction (test_math.TestMathUnitTest) ... ok + test_add (test_math.TestMathClass_0) ... ok + test_multiply (test_math.TestMathClass_0) ... ok + test_add (test_math.TestMathClass_1) ... ok + test_multiply (test_math.TestMathClass_1) ... ok + test_subtract (test_math.TestMathClassDict_0) ... ok + + ---------------------------------------------------------------------- + Ran 8 tests in 0.001s + + OK + +(note: because unittest does not support test decorators, only tests created +with ``@parameterized.expand`` will be executed) + +With green:: + + $ green test_math.py -vvv + test_math + TestMathClass_1 + . test_method_a + . test_method_b + TestMathClass_2 + . test_method_a + . test_method_b + TestMathClass_3 + . test_method_a + . test_method_b + TestMathUnitTest + . test_floor_0_negative + . test_floor_1_integer + . test_floor_2_large_fraction + TestMathClass_0 + . test_add + . test_multiply + TestMathClass_1 + . test_add + . test_multiply + TestMathClassDict_0 + . test_subtract + + Ran 12 tests in 0.121s + + OK (passes=9) + + +Installation +------------ + +:: + + $ pip install parameterized + + +Compatibility +------------- + +`Yes`__ (mostly). + +__ https://app.circleci.com/pipelines/github/wolever/parameterized?branch=master + +.. list-table:: + :header-rows: 1 + :stub-columns: 1 + + * - + - Py3.7 + - Py3.8 + - Py3.9 + - Py3.10 + - Py3.11 + - PyPy3 + - ``@mock.patch`` + * - nose + - yes + - yes + - yes + - yes + - no§ + - no§ + - yes + * - nose2 + - yes + - yes + - yes + - yes + - yes + - yes + - yes + * - py.test 2 + - no* + - no* + - no* + - no* + - no* + - no* + - no* + * - py.test 3 + - yes + - yes + - yes + - yes + - no* + - no* + - yes + * - py.test 4 + - no** + - no** + - no** + - no** + - no** + - no** + - no** + * - py.test fixtures + - no† + - no† + - no† + - no† + - no† + - no† + - no† + * - | unittest + | (``@parameterized.expand``) + - yes + - yes + - yes + - yes + - yes + - yes + - yes + * - | unittest2 + | (``@parameterized.expand``) + - yes + - yes + - yes + - yes + - no§ + - no§ + - yes + +§: nose and unittest2 - both of which were last updated in 2015 - sadly do not +appear to support Python 3.10 or 3.11. + +\*: `py.test 2 does not appear to work under Python 3 (#71)`__, and +`py.test 3 does not appear to work under Python 3.10 or 3.11 (#154)`__. + +\*\*: 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 +__ https://github.com/wolever/parameterized/issues/154 +__ https://github.com/wolever/parameterized/issues/34 +__ https://github.com/wolever/parameterized/issues/81 + +Dependencies +------------ + +(this section left intentionally blank) + + +Exhaustive Usage Examples +-------------------------- + +The ``@parameterized`` and ``@parameterized.expand`` decorators accept a list +or iterable of tuples or ``param(...)``, or a callable which returns a list or +iterable: + +.. code:: python + + from parameterized import parameterized, param + + # A list of tuples + @parameterized([ + (2, 3, 5), + (3, 5, 8), + ]) + def test_add(a, b, expected): + assert_equal(a + b, expected) + + # A list of params + @parameterized([ + param("10", 10), + param("10", 16, base=16), + ]) + def test_int(str_val, expected, base=10): + assert_equal(int(str_val, base=base), expected) + + # An iterable of params + @parameterized( + param.explicit(*json.loads(line)) + for line in open("testcases.jsons") + ) + def test_from_json_file(...): + ... + + # A callable which returns a list of tuples + def load_test_cases(): + return [ + ("test1", ), + ("test2", ), + ] + @parameterized(load_test_cases) + def test_from_function(name): + ... + +.. ** + +Note that, when using an iterator or a generator, all the items will be loaded +into memory before the start of the test run (we do this explicitly to ensure +that generators are exhausted exactly once in multi-process or multi-threaded +testing environments). + +The ``@parameterized`` decorator can be used test class methods, and standalone +functions: + +.. code:: python + + from parameterized import parameterized + + class AddTest(object): + @parameterized([ + (2, 3, 5), + ]) + def test_add(self, a, b, expected): + assert_equal(a + b, expected) + + @parameterized([ + (2, 3, 5), + ]) + def test_add(a, b, expected): + assert_equal(a + b, expected) + + +And ``@parameterized.expand`` can be used to generate test methods in +situations where test generators cannot be used (for example, when the test +class is a subclass of ``unittest.TestCase``): + +.. code:: python + + import unittest + from parameterized import parameterized + + class AddTestCase(unittest.TestCase): + @parameterized.expand([ + ("2 and 3", 2, 3, 5), + ("3 and 5", 3, 5, 8), + ]) + def test_add(self, _, a, b, expected): + assert_equal(a + b, expected) + +Will create the test cases:: + + $ nosetests example.py + test_add_0_2_and_3 (example.AddTestCase) ... ok + test_add_1_3_and_5 (example.AddTestCase) ... ok + + ---------------------------------------------------------------------- + Ran 2 tests in 0.001s + + OK + +Note that ``@parameterized.expand`` works by creating new methods on the test +class. If the first parameter is a string, that string will be added to the end +of the method name. For example, the test case above will generate the methods +``test_add_0_2_and_3`` and ``test_add_1_3_and_5``. + +The names of the test cases generated by ``@parameterized.expand`` can be +customized using the ``name_func`` keyword argument. The value should +be a function which accepts three arguments: ``testcase_func``, ``param_num``, +and ``params``, and it should return the name of the test case. +``testcase_func`` will be the function to be tested, ``param_num`` will be the +index of the test case parameters in the list of parameters, and ``param`` +(an instance of ``param``) will be the parameters which will be used. + +.. code:: python + + import unittest + from parameterized import parameterized + + def custom_name_func(testcase_func, param_num, param): + return "%s_%s" %( + testcase_func.__name__, + parameterized.to_safe_name("_".join(str(x) for x in param.args)), + ) + + class AddTestCase(unittest.TestCase): + @parameterized.expand([ + (2, 3, 5), + (2, 3, 5), + ], name_func=custom_name_func) + def test_add(self, a, b, expected): + assert_equal(a + b, expected) + +Will create the test cases:: + + $ nosetests example.py + test_add_1_2_3 (example.AddTestCase) ... ok + test_add_2_3_5 (example.AddTestCase) ... ok + + ---------------------------------------------------------------------- + Ran 2 tests in 0.001s + + OK + + +The ``param(...)`` helper class stores the parameters for one specific test +case. It can be used to pass keyword arguments to test cases: + +.. code:: python + + from parameterized import parameterized, param + + @parameterized([ + param("10", 10), + param("10", 16, base=16), + ]) + def test_int(str_val, expected, base=10): + assert_equal(int(str_val, base=base), expected) + + +If test cases have a docstring, the parameters for that test case will be +appended to the first line of the docstring. This behavior can be controlled +with the ``doc_func`` argument: + +.. code:: python + + from parameterized import parameterized + + @parameterized([ + (1, 2, 3), + (4, 5, 9), + ]) + def test_add(a, b, expected): + """ Test addition. """ + assert_equal(a + b, expected) + + def my_doc_func(func, num, param): + return "%s: %s with %s" %(num, func.__name__, param) + + @parameterized([ + (5, 4, 1), + (9, 6, 3), + ], doc_func=my_doc_func) + def test_subtraction(a, b, expected): + assert_equal(a - b, expected) + +:: + + $ nosetests example.py + Test addition. [with a=1, b=2, expected=3] ... ok + Test addition. [with a=4, b=5, expected=9] ... ok + 0: test_subtraction with param(*(5, 4, 1)) ... ok + 1: test_subtraction with param(*(9, 6, 3)) ... ok + + ---------------------------------------------------------------------- + Ran 4 tests in 0.001s + + OK + +Finally ``@parameterized_class`` parameterizes an entire class, using +either a list of attributes, or a list of dicts that will be applied to the +class: + +.. code:: python + + from yourapp.models import User + from parameterized import parameterized_class + + @parameterized_class([ + { "username": "user_1", "access_level": 1 }, + { "username": "user_2", "access_level": 2, "expected_status_code": 404 }, + ]) + class TestUserAccessLevel(TestCase): + expected_status_code = 200 + + def setUp(self): + self.client.force_login(User.objects.get(username=self.username)[0]) + + def test_url_a(self): + response = self.client.get('/url') + self.assertEqual(response.status_code, self.expected_status_code) + + def tearDown(self): + self.client.logout() + + + @parameterized_class(("username", "access_level", "expected_status_code"), [ + ("user_1", 1, 200), + ("user_2", 2, 404) + ]) + class TestUserAccessLevel(TestCase): + def setUp(self): + self.client.force_login(User.objects.get(username=self.username)[0]) + + def test_url_a(self): + response = self.client.get("/url") + self.assertEqual(response.status_code, self.expected_status_code) + + def tearDown(self): + self.client.logout() + + +The ``@parameterized_class`` decorator accepts a ``class_name_func`` argument, +which controls the name of the parameterized classes generated by +``@parameterized_class``: + +.. code:: python + + from parameterized import parameterized, parameterized_class + + def get_class_name(cls, num, params_dict): + # By default the generated class named includes either the "name" + # parameter (if present), or the first string value. This example shows + # multiple parameters being included in the generated class name: + return "%s_%s_%s%s" %( + cls.__name__, + num, + parameterized.to_safe_name(params_dict['a']), + parameterized.to_safe_name(params_dict['b']), + ) + + @parameterized_class([ + { "a": "hello", "b": " world!", "expected": "hello world!" }, + { "a": "say ", "b": " cheese :)", "expected": "say cheese :)" }, + ], class_name_func=get_class_name) + class TestConcatenation(TestCase): + def test_concat(self): + self.assertEqual(self.a + self.b, self.expected) + +:: + + $ nosetests -v test_math.py + test_concat (test_concat.TestConcatenation_0_hello_world_) ... ok + test_concat (test_concat.TestConcatenation_0_say_cheese__) ... ok + + + +Using with Single Parameters +............................ + +If a test function only accepts one parameter and the value is not iterable, +then it is possible to supply a list of values without wrapping each one in a +tuple: + +.. code:: python + + @parameterized([1, 2, 3]) + def test_greater_than_zero(value): + assert value > 0 + +Note, however, that if the single parameter *is* iterable (such as a list or +tuple), then it *must* be wrapped in a tuple, list, or the ``param(...)`` +helper: + +.. code:: python + + @parameterized([ + ([1, 2, 3], ), + ([3, 3], ), + ([6], ), + ]) + def test_sums_to_6(numbers): + assert sum(numbers) == 6 + +(note, also, that Python requires single element tuples to be defined with a +trailing comma: ``(foo, )``) + + +Using with ``@mock.patch`` +.......................... + +``parameterized`` can be used with ``mock.patch``, but the argument ordering +can be confusing. The ``@mock.patch(...)`` decorator must come *below* the +``@parameterized(...)``, and the mocked parameters must come *last*: + +.. 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): + ... + +Note: the same holds true when using ``@parameterized.expand``. + + +Migrating from ``nose-parameterized`` to ``parameterized`` +---------------------------------------------------------- + +To migrate a codebase from ``nose-parameterized`` to ``parameterized``: + +1. Update your requirements file, replacing ``nose-parameterized`` with + ``parameterized``. + +2. Replace all references to ``nose_parameterized`` with ``parameterized``:: + + $ perl -pi -e 's/nose_parameterized/parameterized/g' your-codebase/ + +3. You're done! + + +FAQ +--- + +What happened to Python 2.X, 3.5, and 3.6 support? + As of version 0.9.0, ``parameterized`` no longer supports Python 2.X, 3.5, + or 3.6. Previous versions of ``parameterized`` - 0.8.1 being the latest - + will continue to work, but will not receive any new features or bug fixes. + +What do you mean when you say "nose is best supported"? + There are small caveates 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. + +Why not use ``@pytest.mark.parametrize``? + Because spelling is difficult. Also, ``parameterized`` doesn't require you + to repeat argument names, and (using ``param``) it supports optional + keyword arguments. + +Why do I get an ``AttributeError: 'function' object has no attribute 'expand'`` with ``@parameterized.expand``? + You've likely installed the ``parametrized`` (note the missing *e*) + package. Use ``parameterized`` (with the *e*) instead and you'll be all + set. + +What happened to ``nose-parameterized``? + Originally only nose was supported. But now everything is supported, and it + only made sense to change the name! diff --git a/contrib/python/parameterized/py3/parameterized/__init__.py b/contrib/python/parameterized/py3/parameterized/__init__.py new file mode 100644 index 00000000000..9dae02d13eb --- /dev/null +++ b/contrib/python/parameterized/py3/parameterized/__init__.py @@ -0,0 +1,3 @@ +from .parameterized import parameterized, param, parameterized_class + +__version__ = "0.9.0" diff --git a/contrib/python/parameterized/py3/parameterized/parameterized.py b/contrib/python/parameterized/py3/parameterized/parameterized.py new file mode 100644 index 00000000000..56dc535fd86 --- /dev/null +++ b/contrib/python/parameterized/py3/parameterized/parameterized.py @@ -0,0 +1,732 @@ +import re +import sys +import inspect +import warnings +from typing import Iterable +from functools import wraps +from types import MethodType as MethodType +from collections import namedtuple + +try: + from unittest import mock +except ImportError: + try: + import mock + except ImportError: + mock = None + +try: + from collections import OrderedDict as MaybeOrderedDict +except ImportError: + MaybeOrderedDict = dict + +from unittest import TestCase + +try: + from unittest import SkipTest +except ImportError: + class SkipTest(Exception): + pass + +# NOTE: even though Python 2 support has been dropped, these checks have been +# left in place to avoid merge conflicts. They can be removed in the future, and +# future code can be written to assume Python 3. +PY3 = sys.version_info[0] == 3 +PY2 = sys.version_info[0] == 2 + + +if PY3: + # Python 3 doesn't have an InstanceType, so just use a dummy type. + class InstanceType(): + pass + lzip = lambda *a: list(zip(*a)) + text_type = str + string_types = str, + bytes_type = bytes + def make_method(func, instance, type): + if instance is None: + return func + return MethodType(func, instance) +else: + from types import InstanceType + lzip = zip + text_type = unicode + bytes_type = str + string_types = basestring, + def make_method(func, instance, type): + return MethodType(func, instance, type) + +def to_text(x): + if isinstance(x, text_type): + return x + try: + return text_type(x, "utf-8") + except UnicodeDecodeError: + return text_type(x, "latin1") + +CompatArgSpec = namedtuple("CompatArgSpec", "args varargs keywords defaults") + + +def getargspec(func): + if PY2: + return CompatArgSpec(*inspect.getargspec(func)) + args = inspect.getfullargspec(func) + if args.kwonlyargs: + raise TypeError(( + "parameterized does not (yet) support functions with keyword " + "only arguments, but %r has keyword only arguments. " + "Please open an issue with your usecase if this affects you: " + "https://github.com/wolever/parameterized/issues/new" + ) %(func, )) + return CompatArgSpec(*args[:4]) + + +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'): + is_original_async = inspect.iscoroutinefunction(func) + func = dummy_wrapper(func) + tmp_patchings = func.patchings + delattr(func, 'patchings') + for patch_obj in tmp_patchings: + if is_original_async: + func = patch_obj.decorate_async_callable(func) + else: + func = patch_obj.decorate_callable(func) + return func + + +# `parameterized.expand` strips out `mock` patches from the source method in favor of re-applying them over the +# generated methods instead. Sadly, this can cause problems with old versions of the `mock` package, as shown in +# https://bugs.python.org/issue40126 (bpo-40126). +# +# Long story short, bpo-40126 arises whenever the `patchings` list of a `mock`-decorated method is left fully empty. +# +# The bug has been fixed in the `mock` code itself since: +# - Python 3.7.8-rc1, 3.8.3-rc1 and later (for the `unittest.mock` package) [0][1]. +# - Version 4 of the `mock` backport package (https://pypi.org/project/mock/) [2]. +# +# To work around the problem when running old `mock` versions, we avoid fully stripping out patches from the source +# method in favor of replacing them with a "dummy" no-op patch instead. +# +# [0] https://docs.python.org/release/3.7.10/whatsnew/changelog.html#python-3-7-8-release-candidate-1 +# [1] https://docs.python.org/release/3.8.10/whatsnew/changelog.html#python-3-8-3-release-candidate-1 +# [2] https://mock.readthedocs.io/en/stable/changelog.html#b1 + +PYTHON_DOESNT_HAVE_FIX_FOR_BPO_40126 = ( + sys.version_info[:3] < (3, 7, 8) or (sys.version_info[:2] >= (3, 8) and sys.version_info[:3] < (3, 8, 3)) +) + +try: + import mock as _mock_backport +except ImportError: + _mock_backport = None + +MOCK_BACKPORT_DOESNT_HAVE_FIX_FOR_BPO_40126 = _mock_backport is not None and _mock_backport.version_info[0] < 4 + +AVOID_CLEARING_MOCK_PATCHES = PYTHON_DOESNT_HAVE_FIX_FOR_BPO_40126 or MOCK_BACKPORT_DOESNT_HAVE_FIX_FOR_BPO_40126 + + +class DummyPatchTarget(object): + dummy_attribute = None + + @staticmethod + def create_dummy_patch(): + if mock is not None: + return mock.patch.object(DummyPatchTarget(), "dummy_attribute", new=None) + else: + raise ImportError("Missing mock package") + + +def delete_patches_if_need(func): + if hasattr(func, 'patchings'): + if AVOID_CLEARING_MOCK_PATCHES: + func.patchings[:] = [DummyPatchTarget.create_dummy_patch()] + else: + func.patchings[:] = [] + + +_param = namedtuple("param", "args kwargs") + +class param(_param): + """ Represents a single parameter to a test case. + + For example:: + + >>> p = param("foo", bar=16) + >>> p + param("foo", bar=16) + >>> p.args + ('foo', ) + >>> p.kwargs + {'bar': 16} + + Intended to be used as an argument to ``@parameterized``:: + + @parameterized([ + param("foo", bar=16), + ]) + def test_stuff(foo, bar=16): + pass + """ + + def __new__(cls, *args , **kwargs): + return _param.__new__(cls, args, kwargs) + + @classmethod + def explicit(cls, args=None, kwargs=None): + """ Creates a ``param`` by explicitly specifying ``args`` and + ``kwargs``:: + + >>> param.explicit([1,2,3]) + param(*(1, 2, 3)) + >>> param.explicit(kwargs={"foo": 42}) + param(*(), **{"foo": "42"}) + """ + args = args or () + kwargs = kwargs or {} + return cls(*args, **kwargs) + + @classmethod + def from_decorator(cls, args): + """ Returns an instance of ``param()`` for ``@parameterized`` argument + ``args``:: + + >>> param.from_decorator((42, )) + param(args=(42, ), kwargs={}) + >>> param.from_decorator("foo") + param(args=("foo", ), kwargs={}) + """ + if isinstance(args, param): + return args + elif isinstance(args, (str, bytes)) or not isinstance(args, Iterable): + args = (args, ) + try: + return cls(*args) + except TypeError as e: + if "after * must be" not in str(e): + raise + raise TypeError( + "Parameters must be tuples, but %r is not (hint: use '(%r, )')" + %(args, args), + ) + + def __repr__(self): + return "param(*%r, **%r)" %self + + +class QuietOrderedDict(MaybeOrderedDict): + """ When OrderedDict is available, use it to make sure that the kwargs in + doc strings are consistently ordered. """ + __str__ = dict.__str__ + __repr__ = dict.__repr__ + + +def parameterized_argument_value_pairs(func, p): + """Return tuples of parameterized arguments and their values. + + This is useful if you are writing your own doc_func + function and need to know the values for each parameter name:: + + >>> def func(a, foo=None, bar=42, **kwargs): pass + >>> p = param(1, foo=7, extra=99) + >>> parameterized_argument_value_pairs(func, p) + [("a", 1), ("foo", 7), ("bar", 42), ("**kwargs", {"extra": 99})] + + If the function's first argument is named ``self`` then it will be + ignored:: + + >>> def func(self, a): pass + >>> p = param(1) + >>> parameterized_argument_value_pairs(func, p) + [("a", 1)] + + Additionally, empty ``*args`` or ``**kwargs`` will be ignored:: + + >>> def func(foo, *args): pass + >>> p = param(1) + >>> parameterized_argument_value_pairs(func, p) + [("foo", 1)] + >>> p = param(1, 16) + >>> parameterized_argument_value_pairs(func, p) + [("foo", 1), ("*args", (16, ))] + """ + argspec = getargspec(func) + arg_offset = 1 if argspec.args[:1] == ["self"] else 0 + + named_args = argspec.args[arg_offset:] + + result = lzip(named_args, p.args) + named_args = argspec.args[len(result) + arg_offset:] + varargs = p.args[len(result):] + + result.extend([ + (name, p.kwargs.get(name, default)) + for (name, default) + in zip(named_args, argspec.defaults or []) + ]) + + seen_arg_names = set([ n for (n, _) in result ]) + keywords = QuietOrderedDict(sorted([ + (name, p.kwargs[name]) + for name in p.kwargs + if name not in seen_arg_names + ])) + + if varargs: + result.append(("*%s" %(argspec.varargs, ), tuple(varargs))) + + if keywords: + result.append(("**%s" %(argspec.keywords, ), keywords)) + + return result + + +def short_repr(x, n=64): + """ A shortened repr of ``x`` which is guaranteed to be ``unicode``:: + + >>> short_repr("foo") + u"foo" + >>> short_repr("123456789", n=4) + u"12...89" + """ + + x_repr = to_text(repr(x)) + if len(x_repr) > n: + x_repr = x_repr[:n//2] + "..." + x_repr[len(x_repr) - n//2:] + return x_repr + + +def default_doc_func(func, num, p): + if func.__doc__ is None: + return None + + all_args_with_values = parameterized_argument_value_pairs(func, p) + + # Assumes that the function passed is a bound method. + descs = ["%s=%s" %(n, short_repr(v)) for n, v in all_args_with_values] + + # The documentation might be a multiline string, so split it + # and just work with the first string, ignoring the period + # at the end if there is one. + first, nl, rest = func.__doc__.lstrip().partition("\n") + suffix = "" + if first.endswith("."): + suffix = "." + first = first[:-1] + args = "%s[with %s]" %(len(first) and " " or "", ", ".join(descs)) + return "".join( + to_text(x) + for x in [first.rstrip(), args, suffix, nl, rest] + ) + + +def default_name_func(func, num, p): + base_name = func.__name__ + name_suffix = "_%s" %(num, ) + + if len(p.args) > 0 and isinstance(p.args[0], string_types): + name_suffix += "_" + parameterized.to_safe_name(p.args[0]) + return base_name + name_suffix + + +_test_runner_override = None +_test_runner_guess = False +_test_runners = set(["unittest", "unittest2", "nose", "nose2", "pytest"]) +_test_runner_aliases = { + "_pytest": "pytest", +} + + +def set_test_runner(name): + global _test_runner_override + if name not in _test_runners: + raise TypeError( + "Invalid test runner: %r (must be one of: %s)" + %(name, ", ".join(_test_runners)), + ) + _test_runner_override = name + + +def detect_runner(): + """ Guess which test runner we're using by traversing the stack and looking + for the first matching module. This *should* be reasonably safe, as + it's done during test discovery where the test runner should be the + stack frame immediately outside. """ + if _test_runner_override is not None: + return _test_runner_override + global _test_runner_guess + if _test_runner_guess is False: + stack = inspect.stack() + for record in reversed(stack): + frame = record[0] + module = frame.f_globals.get("__name__").partition(".")[0] + if module in _test_runner_aliases: + module = _test_runner_aliases[module] + if module in _test_runners: + _test_runner_guess = module + break + if record[1].endswith("python2.6/unittest.py"): + _test_runner_guess = "unittest" + break + else: + _test_runner_guess = None + return _test_runner_guess + + + +class parameterized(object): + """ Parameterize a test case:: + + class TestInt(object): + @parameterized([ + ("A", 10), + ("F", 15), + param("10", 42, base=42) + ]) + def test_int(self, input, expected, base=16): + actual = int(input, base=base) + assert_equal(actual, expected) + + @parameterized([ + (2, 3, 5) + (3, 5, 8), + ]) + def test_add(a, b, expected): + assert_equal(a + b, expected) + """ + + def __init__(self, input, doc_func=None, skip_on_empty=False): + self.get_input = self.input_as_callable(input) + self.doc_func = doc_func or default_doc_func + self.skip_on_empty = skip_on_empty + + def __call__(self, test_func): + self.assert_not_in_testcase_subclass() + + @wraps(test_func) + def wrapper(test_self=None): + test_cls = test_self and type(test_self) + if test_self is not None: + if issubclass(test_cls, InstanceType): + raise TypeError(( + "@parameterized can't be used with old-style classes, but " + "%r has an old-style class. Consider using a new-style " + "class, or '@parameterized.expand' " + "(see http://stackoverflow.com/q/54867/71522 for more " + "information on old-style classes)." + ) %(test_self, )) + + original_doc = wrapper.__doc__ + for num, args in enumerate(wrapper.parameterized_input): + p = param.from_decorator(args) + unbound_func, nose_tuple = self.param_as_nose_tuple(test_self, test_func, num, p) + try: + wrapper.__doc__ = nose_tuple[0].__doc__ + # Nose uses `getattr(instance, test_func.__name__)` to get + # a method bound to the test instance (as opposed to a + # method bound to the instance of the class created when + # tests were being enumerated). Set a value here to make + # sure nose can get the correct test method. + if test_self is not None: + setattr(test_cls, test_func.__name__, unbound_func) + yield nose_tuple + finally: + 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): + nose_func = wraps(func)(lambda *args: func(*args[:-1], **args[-1])) + nose_func.__doc__ = self.doc_func(func, num, p) + # Track the unbound function because we need to setattr the unbound + # function onto the class for nose to work (see comments above), and + # Python 3 doesn't let us pull the function out of a bound method. + unbound_func = nose_func + if test_self is not None: + # Under nose on Py2 we need to return an unbound method to make + # sure that the `self` in the method is properly shared with the + # `self` used in `setUp` and `tearDown`. But only there. Everyone + # else needs a bound method. + func_self = ( + None if PY2 and detect_runner() == "nose" else + test_self + ) + nose_func = make_method(nose_func, func_self, type(test_self)) + return unbound_func, (nose_func, ) + p.args + (p.kwargs or {}, ) + + def assert_not_in_testcase_subclass(self): + parent_classes = self._terrible_magic_get_defining_classes() + if any(issubclass(cls, TestCase) for cls in parent_classes): + raise Exception("Warning: '@parameterized' tests won't work " + "inside subclasses of 'TestCase' - use " + "'@parameterized.expand' instead.") + + def _terrible_magic_get_defining_classes(self): + """ Returns the set of parent classes of the class currently being defined. + Will likely only work if called from the ``parameterized`` decorator. + This function is entirely @brandon_rhodes's fault, as he suggested + the implementation: http://stackoverflow.com/a/8793684/71522 + """ + stack = inspect.stack() + if len(stack) <= 4: + return [] + frame = stack[4] + code_context = frame[4] and frame[4][0].strip() + if not (code_context and code_context.startswith("class ")): + return [] + _, _, parents = code_context.partition("(") + parents, _, _ = parents.partition(")") + return eval("[" + parents + "]", frame[0].f_globals, frame[0].f_locals) + + @classmethod + def input_as_callable(cls, input): + if callable(input): + return lambda: cls.check_input_values(input()) + input_values = cls.check_input_values(input) + return lambda: input_values + + @classmethod + def check_input_values(cls, input_values): + # Explicitly convery non-list inputs to a list so that: + # 1. A helpful exception will be raised if they aren't iterable, and + # 2. Generators are unwrapped exactly once (otherwise `nosetests + # --processes=n` has issues; see: + # https://github.com/wolever/nose-parameterized/pull/31) + if not isinstance(input_values, list): + input_values = list(input_values) + return [ param.from_decorator(p) for p in input_values ] + + @classmethod + def expand(cls, input, name_func=None, doc_func=None, skip_on_empty=False, + namespace=None, **legacy): + """ A "brute force" method of parameterizing test cases. Creates new + test cases and injects them into the namespace that the wrapped + function is being defined in. Useful for parameterizing tests in + subclasses of 'UnitTest', where Nose test generators don't work. + + :param input: An iterable of values to pass to the test function. + :param name_func: A function that takes a single argument (the + value from the input iterable) and returns a string to use as + the name of the test case. If not provided, the name of the + test case will be the name of the test function with the + parameter value appended. + :param doc_func: A function that takes a single argument (the + value from the input iterable) and returns a string to use as + the docstring of the test case. If not provided, the docstring + of the test case will be the docstring of the test function. + :param skip_on_empty: If True, the test will be skipped if the + input iterable is empty. If False, a ValueError will be raised + if the input iterable is empty. + :param namespace: The namespace (dict-like) to inject the test cases + into. If not provided, the namespace of the test function will + be used. + + >>> @parameterized.expand([("foo", 1, 2)]) + ... def test_add1(name, input, expected): + ... actual = add1(input) + ... assert_equal(actual, expected) + ... + >>> locals() + ... 'test_add1_foo_0': <function ...> ... + >>> + """ + + if "testcase_func_name" in legacy: + warnings.warn("testcase_func_name= is deprecated; use name_func=", + DeprecationWarning, stacklevel=2) + if not name_func: + name_func = legacy["testcase_func_name"] + + if "testcase_func_doc" in legacy: + warnings.warn("testcase_func_doc= is deprecated; use doc_func=", + DeprecationWarning, stacklevel=2) + if not doc_func: + doc_func = legacy["testcase_func_doc"] + + doc_func = doc_func or default_doc_func + name_func = name_func or default_name_func + + def parameterized_expand_wrapper(f, instance=None): + frame_locals = namespace + if frame_locals is None: + frame_locals = inspect.currentframe().f_back.f_locals + + parameters = cls.input_as_callable(input)() + + if not parameters: + if not skip_on_empty: + raise ValueError( + "Parameters iterable is empty (hint: use " + "`parameterized.expand([], skip_on_empty=True)` to skip " + "this test when the input is empty)" + ) + return wraps(f)(skip_on_empty_helper) + + digits = len(str(len(parameters) - 1)) + for num, p in enumerate(parameters): + 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].__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) + + f.__test__ = False + + return parameterized_expand_wrapper + + @classmethod + def param_as_standalone_func(cls, p, func, name): + if inspect.iscoroutinefunction(func): + @wraps(func) + async def standalone_func(*a, **kw): + return await func(*(a + p.args), **p.kwargs, **kw) + else: + @wraps(func) + def standalone_func(*a, **kw): + return func(*(a + p.args), **p.kwargs, **kw) + + standalone_func.__name__ = name + + # 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 + def to_safe_name(cls, s): + if not isinstance(s, str): + s = str(s) + return str(re.sub("[^a-zA-Z0-9_]+", "_", s)) + + +def parameterized_class(attrs, input_values=None, class_name_func=None, classname_func=None): + """ Parameterizes a test class by setting attributes on the class. + + Can be used in two ways: + + 1) With a list of dictionaries containing attributes to override:: + + @parameterized_class([ + { "username": "foo" }, + { "username": "bar", "access_level": 2 }, + ]) + class TestUserAccessLevel(TestCase): + ... + + 2) With a tuple of attributes, then a list of tuples of values: + + @parameterized_class(("username", "access_level"), [ + ("foo", 1), + ("bar", 2) + ]) + class TestUserAccessLevel(TestCase): + ... + + """ + + if isinstance(attrs, string_types): + attrs = [attrs] + + input_dicts = ( + attrs if input_values is None else + [dict(zip(attrs, vals)) for vals in input_values] + ) + + class_name_func = class_name_func or default_class_name_func + + if classname_func: + warnings.warn( + "classname_func= is deprecated; use class_name_func= instead. " + "See: https://github.com/wolever/parameterized/pull/74#issuecomment-613577057", + DeprecationWarning, + stacklevel=2, + ) + class_name_func = lambda cls, idx, input: classname_func(cls, idx, input_dicts) + + def decorator(base_class): + test_class_module = sys.modules[base_class.__module__].__dict__ + for idx, input_dict in enumerate(input_dicts): + test_class_dict = dict(base_class.__dict__) + test_class_dict.update(input_dict) + + name = class_name_func(base_class, idx, input_dict) + + test_class_module[name] = type(name, (base_class, ), test_class_dict) + + # We need to leave the base class in place (see issue #73), but if we + # leave the test_ methods in place, the test runner will try to pick + # them up and run them... which doesn't make sense, since no parameters + # will have been applied. + # Address this by iterating over the base class and remove all test + # methods. + for method_name in list(base_class.__dict__): + if method_name.startswith("test"): + delattr(base_class, method_name) + return base_class + + return decorator + + +def get_class_name_suffix(params_dict): + if "name" in params_dict: + return parameterized.to_safe_name(params_dict["name"]) + + params_vals = ( + params_dict.values() if PY3 else + (v for (_, v) in sorted(params_dict.items())) + ) + return parameterized.to_safe_name(next(( + v for v in params_vals + if isinstance(v, string_types) + ), "")) + + +def default_class_name_func(cls, num, params_dict): + suffix = get_class_name_suffix(params_dict) + return "%s_%s%s" %( + cls.__name__, + num, + suffix and "_" + suffix, + ) diff --git a/contrib/python/parameterized/py3/ya.make b/contrib/python/parameterized/py3/ya.make new file mode 100644 index 00000000000..70d243f622d --- /dev/null +++ b/contrib/python/parameterized/py3/ya.make @@ -0,0 +1,23 @@ +# Generated by devtools/yamaker (pypi). + +PY3_LIBRARY() + +VERSION(0.9.0) + +LICENSE(BSD-3-Clause) + +NO_LINT() + +PY_SRCS( + TOP_LEVEL + parameterized/__init__.py + parameterized/parameterized.py +) + +RESOURCE_FILES( + PREFIX contrib/python/parameterized/py3/ + .dist-info/METADATA + .dist-info/top_level.txt +) + +END() diff --git a/contrib/python/parameterized/ya.make b/contrib/python/parameterized/ya.make new file mode 100644 index 00000000000..8a6d8706c0a --- /dev/null +++ b/contrib/python/parameterized/ya.make @@ -0,0 +1,18 @@ +PY23_LIBRARY() + +LICENSE(Service-Py23-Proxy) + +IF (PYTHON2) + PEERDIR(contrib/python/parameterized/py2) +ELSE() + PEERDIR(contrib/python/parameterized/py3) +ENDIF() + +NO_LINT() + +END() + +RECURSE( + py2 + py3 +) |
