Spaces:
Running
Running
# Copyright 2017 The Abseil Authors. | |
# | |
# Licensed under the Apache License, Version 2.0 (the "License"); | |
# you may not use this file except in compliance with the License. | |
# You may obtain a copy of the License at | |
# | |
# http://www.apache.org/licenses/LICENSE-2.0 | |
# | |
# Unless required by applicable law or agreed to in writing, software | |
# distributed under the License is distributed on an "AS IS" BASIS, | |
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
# See the License for the specific language governing permissions and | |
# limitations under the License. | |
"""Adds support for parameterized tests to Python's unittest TestCase class. | |
A parameterized test is a method in a test case that is invoked with different | |
argument tuples. | |
A simple example:: | |
class AdditionExample(parameterized.TestCase): | |
@parameterized.parameters( | |
(1, 2, 3), | |
(4, 5, 9), | |
(1, 1, 3)) | |
def testAddition(self, op1, op2, result): | |
self.assertEqual(result, op1 + op2) | |
Each invocation is a separate test case and properly isolated just | |
like a normal test method, with its own setUp/tearDown cycle. In the | |
example above, there are three separate testcases, one of which will | |
fail due to an assertion error (1 + 1 != 3). | |
Parameters for individual test cases can be tuples (with positional parameters) | |
or dictionaries (with named parameters):: | |
class AdditionExample(parameterized.TestCase): | |
@parameterized.parameters( | |
{'op1': 1, 'op2': 2, 'result': 3}, | |
{'op1': 4, 'op2': 5, 'result': 9}, | |
) | |
def testAddition(self, op1, op2, result): | |
self.assertEqual(result, op1 + op2) | |
If a parameterized test fails, the error message will show the | |
original test name and the parameters for that test. | |
The id method of the test, used internally by the unittest framework, is also | |
modified to show the arguments (but note that the name reported by `id()` | |
doesn't match the actual test name, see below). To make sure that test names | |
stay the same across several invocations, object representations like:: | |
>>> class Foo(object): | |
... pass | |
>>> repr(Foo()) | |
'<__main__.Foo object at 0x23d8610>' | |
are turned into ``__main__.Foo``. When selecting a subset of test cases to run | |
on the command-line, the test cases contain an index suffix for each argument | |
in the order they were passed to :func:`parameters` (eg. testAddition0, | |
testAddition1, etc.) This naming scheme is subject to change; for more reliable | |
and stable names, especially in test logs, use :func:`named_parameters` instead. | |
Tests using :func:`named_parameters` are similar to :func:`parameters`, except | |
only tuples or dicts of args are supported. For tuples, the first parameter arg | |
has to be a string (or an object that returns an apt name when converted via | |
``str()``). For dicts, a value for the key ``testcase_name`` must be present and | |
must be a string (or an object that returns an apt name when converted via | |
``str()``):: | |
class NamedExample(parameterized.TestCase): | |
@parameterized.named_parameters( | |
('Normal', 'aa', 'aaa', True), | |
('EmptyPrefix', '', 'abc', True), | |
('BothEmpty', '', '', True)) | |
def testStartsWith(self, prefix, string, result): | |
self.assertEqual(result, string.startswith(prefix)) | |
class NamedExample(parameterized.TestCase): | |
@parameterized.named_parameters( | |
{'testcase_name': 'Normal', | |
'result': True, 'string': 'aaa', 'prefix': 'aa'}, | |
{'testcase_name': 'EmptyPrefix', | |
'result': True, 'string': 'abc', 'prefix': ''}, | |
{'testcase_name': 'BothEmpty', | |
'result': True, 'string': '', 'prefix': ''}) | |
def testStartsWith(self, prefix, string, result): | |
self.assertEqual(result, string.startswith(prefix)) | |
Named tests also have the benefit that they can be run individually | |
from the command line:: | |
$ testmodule.py NamedExample.testStartsWithNormal | |
. | |
-------------------------------------------------------------------- | |
Ran 1 test in 0.000s | |
OK | |
Parameterized Classes | |
===================== | |
If invocation arguments are shared across test methods in a single | |
TestCase class, instead of decorating all test methods | |
individually, the class itself can be decorated:: | |
@parameterized.parameters( | |
(1, 2, 3), | |
(4, 5, 9)) | |
class ArithmeticTest(parameterized.TestCase): | |
def testAdd(self, arg1, arg2, result): | |
self.assertEqual(arg1 + arg2, result) | |
def testSubtract(self, arg1, arg2, result): | |
self.assertEqual(result - arg1, arg2) | |
Inputs from Iterables | |
===================== | |
If parameters should be shared across several test cases, or are dynamically | |
created from other sources, a single non-tuple iterable can be passed into | |
the decorator. This iterable will be used to obtain the test cases:: | |
class AdditionExample(parameterized.TestCase): | |
@parameterized.parameters( | |
c.op1, c.op2, c.result for c in testcases | |
) | |
def testAddition(self, op1, op2, result): | |
self.assertEqual(result, op1 + op2) | |
Single-Argument Test Methods | |
============================ | |
If a test method takes only one argument, the single arguments must not be | |
wrapped into a tuple:: | |
class NegativeNumberExample(parameterized.TestCase): | |
@parameterized.parameters( | |
-1, -3, -4, -5 | |
) | |
def testIsNegative(self, arg): | |
self.assertTrue(IsNegative(arg)) | |
List/tuple as a Single Argument | |
=============================== | |
If a test method takes a single argument of a list/tuple, it must be wrapped | |
inside a tuple:: | |
class ZeroSumExample(parameterized.TestCase): | |
@parameterized.parameters( | |
([-1, 0, 1], ), | |
([-2, 0, 2], ), | |
) | |
def testSumIsZero(self, arg): | |
self.assertEqual(0, sum(arg)) | |
Cartesian product of Parameter Values as Parameterized Test Cases | |
================================================================= | |
If required to test method over a cartesian product of parameters, | |
`parameterized.product` may be used to facilitate generation of parameters | |
test combinations:: | |
class TestModuloExample(parameterized.TestCase): | |
@parameterized.product( | |
num=[0, 20, 80], | |
modulo=[2, 4], | |
expected=[0] | |
) | |
def testModuloResult(self, num, modulo, expected): | |
self.assertEqual(expected, num % modulo) | |
This results in 6 test cases being created - one for each combination of the | |
parameters. It is also possible to supply sequences of keyword argument dicts | |
as elements of the cartesian product:: | |
@parameterized.product( | |
(dict(num=5, modulo=3, expected=2), | |
dict(num=7, modulo=4, expected=3)), | |
dtype=(int, float) | |
) | |
def testModuloResult(self, num, modulo, expected, dtype): | |
self.assertEqual(expected, dtype(num) % modulo) | |
This results in 4 test cases being created - for each of the two sets of test | |
data (supplied as kwarg dicts) and for each of the two data types (supplied as | |
a named parameter). Multiple keyword argument dicts may be supplied if required. | |
Async Support | |
============= | |
If a test needs to call async functions, it can inherit from both | |
parameterized.TestCase and another TestCase that supports async calls, such | |
as [asynctest](https://github.com/Martiusweb/asynctest):: | |
import asynctest | |
class AsyncExample(parameterized.TestCase, asynctest.TestCase): | |
@parameterized.parameters( | |
('a', 1), | |
('b', 2), | |
) | |
async def testSomeAsyncFunction(self, arg, expected): | |
actual = await someAsyncFunction(arg) | |
self.assertEqual(actual, expected) | |
""" | |
from collections import abc | |
import functools | |
import inspect | |
import itertools | |
import re | |
import types | |
import unittest | |
import warnings | |
from absl.testing import absltest | |
_ADDR_RE = re.compile(r'\<([a-zA-Z0-9_\-\.]+) object at 0x[a-fA-F0-9]+\>') | |
_NAMED = object() | |
_ARGUMENT_REPR = object() | |
_NAMED_DICT_KEY = 'testcase_name' | |
class NoTestsError(Exception): | |
"""Raised when parameterized decorators do not generate any tests.""" | |
class DuplicateTestNameError(Exception): | |
"""Raised when a parameterized test has the same test name multiple times.""" | |
def __init__(self, test_class_name, new_test_name, original_test_name): | |
super(DuplicateTestNameError, self).__init__( | |
'Duplicate parameterized test name in {}: generated test name {!r} ' | |
'(generated from {!r}) already exists. Consider using ' | |
'named_parameters() to give your tests unique names and/or renaming ' | |
'the conflicting test method.'.format( | |
test_class_name, new_test_name, original_test_name)) | |
def _clean_repr(obj): | |
return _ADDR_RE.sub(r'<\1>', repr(obj)) | |
def _non_string_or_bytes_iterable(obj): | |
return (isinstance(obj, abc.Iterable) and not isinstance(obj, str) and | |
not isinstance(obj, bytes)) | |
def _format_parameter_list(testcase_params): | |
if isinstance(testcase_params, abc.Mapping): | |
return ', '.join('%s=%s' % (argname, _clean_repr(value)) | |
for argname, value in testcase_params.items()) | |
elif _non_string_or_bytes_iterable(testcase_params): | |
return ', '.join(map(_clean_repr, testcase_params)) | |
else: | |
return _format_parameter_list((testcase_params,)) | |
def _async_wrapped(func): | |
async def wrapper(*args, **kwargs): | |
return await func(*args, **kwargs) | |
return wrapper | |
class _ParameterizedTestIter(object): | |
"""Callable and iterable class for producing new test cases.""" | |
def __init__(self, test_method, testcases, naming_type, original_name=None): | |
"""Returns concrete test functions for a test and a list of parameters. | |
The naming_type is used to determine the name of the concrete | |
functions as reported by the unittest framework. If naming_type is | |
_FIRST_ARG, the testcases must be tuples, and the first element must | |
have a string representation that is a valid Python identifier. | |
Args: | |
test_method: The decorated test method. | |
testcases: (list of tuple/dict) A list of parameter tuples/dicts for | |
individual test invocations. | |
naming_type: The test naming type, either _NAMED or _ARGUMENT_REPR. | |
original_name: The original test method name. When decorated on a test | |
method, None is passed to __init__ and test_method.__name__ is used. | |
Note test_method.__name__ might be different than the original defined | |
test method because of the use of other decorators. A more accurate | |
value is set by TestGeneratorMetaclass.__new__ later. | |
""" | |
self._test_method = test_method | |
self.testcases = testcases | |
self._naming_type = naming_type | |
if original_name is None: | |
original_name = test_method.__name__ | |
self._original_name = original_name | |
self.__name__ = _ParameterizedTestIter.__name__ | |
def __call__(self, *args, **kwargs): | |
raise RuntimeError('You appear to be running a parameterized test case ' | |
'without having inherited from parameterized.' | |
'TestCase. This is bad because none of ' | |
'your test cases are actually being run. You may also ' | |
'be using another decorator before the parameterized ' | |
'one, in which case you should reverse the order.') | |
def __iter__(self): | |
test_method = self._test_method | |
naming_type = self._naming_type | |
def make_bound_param_test(testcase_params): | |
def bound_param_test(self): | |
if isinstance(testcase_params, abc.Mapping): | |
return test_method(self, **testcase_params) | |
elif _non_string_or_bytes_iterable(testcase_params): | |
return test_method(self, *testcase_params) | |
else: | |
return test_method(self, testcase_params) | |
if naming_type is _NAMED: | |
# Signal the metaclass that the name of the test function is unique | |
# and descriptive. | |
bound_param_test.__x_use_name__ = True | |
testcase_name = None | |
if isinstance(testcase_params, abc.Mapping): | |
if _NAMED_DICT_KEY not in testcase_params: | |
raise RuntimeError( | |
'Dict for named tests must contain key "%s"' % _NAMED_DICT_KEY) | |
# Create a new dict to avoid modifying the supplied testcase_params. | |
testcase_name = testcase_params[_NAMED_DICT_KEY] | |
testcase_params = { | |
k: v for k, v in testcase_params.items() if k != _NAMED_DICT_KEY | |
} | |
elif _non_string_or_bytes_iterable(testcase_params): | |
if not isinstance(testcase_params[0], str): | |
raise RuntimeError( | |
'The first element of named test parameters is the test name ' | |
'suffix and must be a string') | |
testcase_name = testcase_params[0] | |
testcase_params = testcase_params[1:] | |
else: | |
raise RuntimeError( | |
'Named tests must be passed a dict or non-string iterable.') | |
test_method_name = self._original_name | |
# Support PEP-8 underscore style for test naming if used. | |
if (test_method_name.startswith('test_') | |
and testcase_name | |
and not testcase_name.startswith('_')): | |
test_method_name += '_' | |
bound_param_test.__name__ = test_method_name + str(testcase_name) | |
elif naming_type is _ARGUMENT_REPR: | |
# If it's a generator, convert it to a tuple and treat them as | |
# parameters. | |
if isinstance(testcase_params, types.GeneratorType): | |
testcase_params = tuple(testcase_params) | |
# The metaclass creates a unique, but non-descriptive method name for | |
# _ARGUMENT_REPR tests using an indexed suffix. | |
# To keep test names descriptive, only the original method name is used. | |
# To make sure test names are unique, we add a unique descriptive suffix | |
# __x_params_repr__ for every test. | |
params_repr = '(%s)' % (_format_parameter_list(testcase_params),) | |
bound_param_test.__x_params_repr__ = params_repr | |
else: | |
raise RuntimeError('%s is not a valid naming type.' % (naming_type,)) | |
bound_param_test.__doc__ = '%s(%s)' % ( | |
bound_param_test.__name__, _format_parameter_list(testcase_params)) | |
if test_method.__doc__: | |
bound_param_test.__doc__ += '\n%s' % (test_method.__doc__,) | |
if inspect.iscoroutinefunction(test_method): | |
return _async_wrapped(bound_param_test) | |
return bound_param_test | |
return (make_bound_param_test(c) for c in self.testcases) | |
def _modify_class(class_object, testcases, naming_type): | |
assert not getattr(class_object, '_test_params_reprs', None), ( | |
'Cannot add parameters to %s. Either it already has parameterized ' | |
'methods, or its super class is also a parameterized class.' % ( | |
class_object,)) | |
# NOTE: _test_params_repr is private to parameterized.TestCase and it's | |
# metaclass; do not use it outside of those classes. | |
class_object._test_params_reprs = test_params_reprs = {} | |
for name, obj in class_object.__dict__.copy().items(): | |
if (name.startswith(unittest.TestLoader.testMethodPrefix) | |
and isinstance(obj, types.FunctionType)): | |
delattr(class_object, name) | |
methods = {} | |
_update_class_dict_for_param_test_case( | |
class_object.__name__, methods, test_params_reprs, name, | |
_ParameterizedTestIter(obj, testcases, naming_type, name)) | |
for meth_name, meth in methods.items(): | |
setattr(class_object, meth_name, meth) | |
def _parameter_decorator(naming_type, testcases): | |
"""Implementation of the parameterization decorators. | |
Args: | |
naming_type: The naming type. | |
testcases: Testcase parameters. | |
Raises: | |
NoTestsError: Raised when the decorator generates no tests. | |
Returns: | |
A function for modifying the decorated object. | |
""" | |
def _apply(obj): | |
if isinstance(obj, type): | |
_modify_class(obj, testcases, naming_type) | |
return obj | |
else: | |
return _ParameterizedTestIter(obj, testcases, naming_type) | |
if (len(testcases) == 1 and | |
not isinstance(testcases[0], tuple) and | |
not isinstance(testcases[0], abc.Mapping)): | |
# Support using a single non-tuple parameter as a list of test cases. | |
# Note that the single non-tuple parameter can't be Mapping either, which | |
# means a single dict parameter case. | |
assert _non_string_or_bytes_iterable(testcases[0]), ( | |
'Single parameter argument must be a non-string non-Mapping iterable') | |
testcases = testcases[0] | |
if not isinstance(testcases, abc.Sequence): | |
testcases = list(testcases) | |
if not testcases: | |
raise NoTestsError( | |
'parameterized test decorators did not generate any tests. ' | |
'Make sure you specify non-empty parameters, ' | |
'and do not reuse generators more than once.') | |
return _apply | |
def parameters(*testcases): | |
"""A decorator for creating parameterized tests. | |
See the module docstring for a usage example. | |
Args: | |
*testcases: Parameters for the decorated method, either a single | |
iterable, or a list of tuples/dicts/objects (for tests with only one | |
argument). | |
Raises: | |
NoTestsError: Raised when the decorator generates no tests. | |
Returns: | |
A test generator to be handled by TestGeneratorMetaclass. | |
""" | |
return _parameter_decorator(_ARGUMENT_REPR, testcases) | |
def named_parameters(*testcases): | |
"""A decorator for creating parameterized tests. | |
See the module docstring for a usage example. For every parameter tuple | |
passed, the first element of the tuple should be a string and will be appended | |
to the name of the test method. Each parameter dict passed must have a value | |
for the key "testcase_name", the string representation of that value will be | |
appended to the name of the test method. | |
Args: | |
*testcases: Parameters for the decorated method, either a single iterable, | |
or a list of tuples or dicts. | |
Raises: | |
NoTestsError: Raised when the decorator generates no tests. | |
Returns: | |
A test generator to be handled by TestGeneratorMetaclass. | |
""" | |
return _parameter_decorator(_NAMED, testcases) | |
def product(*kwargs_seqs, **testgrid): | |
"""A decorator for running tests over cartesian product of parameters values. | |
See the module docstring for a usage example. The test will be run for every | |
possible combination of the parameters. | |
Args: | |
*kwargs_seqs: Each positional parameter is a sequence of keyword arg dicts; | |
every test case generated will include exactly one kwargs dict from each | |
positional parameter; these will then be merged to form an overall list | |
of arguments for the test case. | |
**testgrid: A mapping of parameter names and their possible values. Possible | |
values should given as either a list or a tuple. | |
Raises: | |
NoTestsError: Raised when the decorator generates no tests. | |
Returns: | |
A test generator to be handled by TestGeneratorMetaclass. | |
""" | |
for name, values in testgrid.items(): | |
assert isinstance(values, (list, tuple)), ( | |
'Values of {} must be given as list or tuple, found {}'.format( | |
name, type(values))) | |
prior_arg_names = set() | |
for kwargs_seq in kwargs_seqs: | |
assert ((isinstance(kwargs_seq, (list, tuple))) and | |
all(isinstance(kwargs, dict) for kwargs in kwargs_seq)), ( | |
'Positional parameters must be a sequence of keyword arg' | |
'dicts, found {}' | |
.format(kwargs_seq)) | |
if kwargs_seq: | |
arg_names = set(kwargs_seq[0]) | |
assert all(set(kwargs) == arg_names for kwargs in kwargs_seq), ( | |
'Keyword argument dicts within a single parameter must all have the ' | |
'same keys, found {}'.format(kwargs_seq)) | |
assert not (arg_names & prior_arg_names), ( | |
'Keyword argument dict sequences must all have distinct argument ' | |
'names, found duplicate(s) {}' | |
.format(sorted(arg_names & prior_arg_names))) | |
prior_arg_names |= arg_names | |
assert not (prior_arg_names & set(testgrid)), ( | |
'Arguments supplied in kwargs dicts in positional parameters must not ' | |
'overlap with arguments supplied as named parameters; found duplicate ' | |
'argument(s) {}'.format(sorted(prior_arg_names & set(testgrid)))) | |
# Convert testgrid into a sequence of sequences of kwargs dicts and combine | |
# with the positional parameters. | |
# So foo=[1,2], bar=[3,4] --> [[{foo: 1}, {foo: 2}], [{bar: 3, bar: 4}]] | |
testgrid = (tuple({k: v} for v in vs) for k, vs in testgrid.items()) | |
testgrid = tuple(kwargs_seqs) + tuple(testgrid) | |
# Create all possible combinations of parameters as a cartesian product | |
# of parameter values. | |
testcases = [ | |
dict(itertools.chain.from_iterable(case.items() | |
for case in cases)) | |
for cases in itertools.product(*testgrid) | |
] | |
return _parameter_decorator(_ARGUMENT_REPR, testcases) | |
class TestGeneratorMetaclass(type): | |
"""Metaclass for adding tests generated by parameterized decorators.""" | |
def __new__(cls, class_name, bases, dct): | |
# NOTE: _test_params_repr is private to parameterized.TestCase and it's | |
# metaclass; do not use it outside of those classes. | |
test_params_reprs = dct.setdefault('_test_params_reprs', {}) | |
for name, obj in dct.copy().items(): | |
if (name.startswith(unittest.TestLoader.testMethodPrefix) and | |
_non_string_or_bytes_iterable(obj)): | |
# NOTE: `obj` might not be a _ParameterizedTestIter in two cases: | |
# 1. a class-level iterable named test* that isn't a test, such as | |
# a list of something. Such attributes get deleted from the class. | |
# | |
# 2. If a decorator is applied to the parameterized test, e.g. | |
# @morestuff | |
# @parameterized.parameters(...) | |
# def test_foo(...): ... | |
# | |
# This is OK so long as the underlying parameterized function state | |
# is forwarded (e.g. using functool.wraps() and **without** | |
# accessing explicitly accessing the internal attributes. | |
if isinstance(obj, _ParameterizedTestIter): | |
# Update the original test method name so it's more accurate. | |
# The mismatch might happen when another decorator is used inside | |
# the parameterized decrators, and the inner decorator doesn't | |
# preserve its __name__. | |
obj._original_name = name | |
iterator = iter(obj) | |
dct.pop(name) | |
_update_class_dict_for_param_test_case( | |
class_name, dct, test_params_reprs, name, iterator) | |
# If the base class is a subclass of parameterized.TestCase, inherit its | |
# _test_params_reprs too. | |
for base in bases: | |
# Check if the base has _test_params_reprs first, then check if it's a | |
# subclass of parameterized.TestCase. Otherwise when this is called for | |
# the parameterized.TestCase definition itself, this raises because | |
# itself is not defined yet. This works as long as absltest.TestCase does | |
# not define _test_params_reprs. | |
base_test_params_reprs = getattr(base, '_test_params_reprs', None) | |
if base_test_params_reprs and issubclass(base, TestCase): | |
for test_method, test_method_id in base_test_params_reprs.items(): | |
# test_method may both exists in base and this class. | |
# This class's method overrides base class's. | |
# That's why it should only inherit it if it does not exist. | |
test_params_reprs.setdefault(test_method, test_method_id) | |
return type.__new__(cls, class_name, bases, dct) | |
def _update_class_dict_for_param_test_case( | |
test_class_name, dct, test_params_reprs, name, iterator): | |
"""Adds individual test cases to a dictionary. | |
Args: | |
test_class_name: The name of the class tests are added to. | |
dct: The target dictionary. | |
test_params_reprs: The dictionary for mapping names to test IDs. | |
name: The original name of the test case. | |
iterator: The iterator generating the individual test cases. | |
Raises: | |
DuplicateTestNameError: Raised when a test name occurs multiple times. | |
RuntimeError: If non-parameterized functions are generated. | |
""" | |
for idx, func in enumerate(iterator): | |
assert callable(func), 'Test generators must yield callables, got %r' % ( | |
func,) | |
if not (getattr(func, '__x_use_name__', None) or | |
getattr(func, '__x_params_repr__', None)): | |
raise RuntimeError( | |
'{}.{} generated a test function without using the parameterized ' | |
'decorators. Only tests generated using the decorators are ' | |
'supported.'.format(test_class_name, name)) | |
if getattr(func, '__x_use_name__', False): | |
original_name = func.__name__ | |
new_name = original_name | |
else: | |
original_name = name | |
new_name = '%s%d' % (original_name, idx) | |
if new_name in dct: | |
raise DuplicateTestNameError(test_class_name, new_name, original_name) | |
dct[new_name] = func | |
test_params_reprs[new_name] = getattr(func, '__x_params_repr__', '') | |
class TestCase(absltest.TestCase, metaclass=TestGeneratorMetaclass): | |
"""Base class for test cases using the parameters decorator.""" | |
# visibility: private; do not call outside this class. | |
def _get_params_repr(self): | |
return self._test_params_reprs.get(self._testMethodName, '') | |
def __str__(self): | |
params_repr = self._get_params_repr() | |
if params_repr: | |
params_repr = ' ' + params_repr | |
return '{}{} ({})'.format( | |
self._testMethodName, params_repr, | |
unittest.util.strclass(self.__class__)) | |
def id(self): | |
"""Returns the descriptive ID of the test. | |
This is used internally by the unittesting framework to get a name | |
for the test to be used in reports. | |
Returns: | |
The test id. | |
""" | |
base = super(TestCase, self).id() | |
params_repr = self._get_params_repr() | |
if params_repr: | |
# We include the params in the id so that, when reported in the | |
# test.xml file, the value is more informative than just "test_foo0". | |
# Use a space to separate them so that it's copy/paste friendly and | |
# easy to identify the actual test id. | |
return '{} {}'.format(base, params_repr) | |
else: | |
return base | |
# This function is kept CamelCase because it's used as a class's base class. | |
def CoopTestCase(other_base_class): # pylint: disable=invalid-name | |
"""Returns a new base class with a cooperative metaclass base. | |
This enables the TestCase to be used in combination | |
with other base classes that have custom metaclasses, such as | |
``mox.MoxTestBase``. | |
Only works with metaclasses that do not override ``type.__new__``. | |
Example:: | |
from absl.testing import parameterized | |
class ExampleTest(parameterized.CoopTestCase(OtherTestCase)): | |
... | |
Args: | |
other_base_class: (class) A test case base class. | |
Returns: | |
A new class object. | |
""" | |
# If the other base class has a metaclass of 'type' then trying to combine | |
# the metaclasses will result in an MRO error. So simply combine them and | |
# return. | |
if type(other_base_class) == type: # pylint: disable=unidiomatic-typecheck | |
warnings.warn( | |
'CoopTestCase is only necessary when combining with a class that uses' | |
' a metaclass. Use multiple inheritance like this instead: class' | |
f' ExampleTest(paramaterized.TestCase, {other_base_class.__name__}):', | |
stacklevel=2, | |
) | |
class CoopTestCaseBase(other_base_class, TestCase): | |
pass | |
return CoopTestCaseBase | |
else: | |
class CoopMetaclass(type(other_base_class), TestGeneratorMetaclass): # pylint: disable=unused-variable | |
pass | |
class CoopTestCaseBase(other_base_class, TestCase, metaclass=CoopMetaclass): | |
pass | |
return CoopTestCaseBase | |