| import re |
| import inspect |
| from functools import wraps |
| from collections import namedtuple |
| |
| from nose.tools import nottest |
| from unittest import TestCase |
| |
| from . import compat |
| |
| if compat.PY3: |
| def new_instancemethod(f, *args): |
| return f |
| |
| # Python 3 doesn't have an InstanceType, so just use a dummy type. |
| class InstanceType(): |
| pass |
| else: |
| import new |
| new_instancemethod = new.instancemethod |
| from types import InstanceType |
| |
| _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 |
| if isinstance(args, compat.string_types): |
| args = (args, ) |
| return cls(*args) |
| |
| def __repr__(self): |
| return "param(*%r, **%r)" %self |
| |
| 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): |
| self.get_input = self.input_as_callable(input) |
| |
| def __call__(self, test_func): |
| self.assert_not_in_testcase_subclass() |
| |
| @wraps(test_func) |
| def parameterized_helper_method(test_self=None): |
| f = test_func |
| if test_self is not None: |
| # If we are a test method (which we suppose to be true if we |
| # are being passed a "self" argument), we first need to create |
| # an instance method, attach it to the instance of the test |
| # class, then pull it back off to turn it into a bound method. |
| # If we don't do this, Nose gets cranky. |
| f = self.make_bound_method(test_self, test_func) |
| # Note: because nose is so very picky, the more obvious |
| # ``return self.yield_nose_tuples(f)`` won't work here. |
| for nose_tuple in self.yield_nose_tuples(f): |
| yield nose_tuple |
| |
| test_func.__name__ = "_helper_for_%s" %(test_func.__name__, ) |
| parameterized_helper_method.parameterized_input = input |
| parameterized_helper_method.parameterized_func = test_func |
| return parameterized_helper_method |
| |
| def yield_nose_tuples(self, func): |
| for args in self.get_input(): |
| p = param.from_decorator(args) |
| # ... then yield that as a tuple. If those steps aren't |
| # followed precicely, Nose gets upset and doesn't run the test |
| # or doesn't run setup methods. |
| yield self.param_as_nose_tuple(p, func) |
| |
| def param_as_nose_tuple(self, p, func): |
| nose_func = func |
| nose_args = p.args |
| if p.kwargs: |
| nose_func = wraps(func)(lambda args, kwargs: func(*args, **kwargs)) |
| nose_args = (p.args, p.kwargs) |
| return (nose_func, ) + nose_args |
| |
| def make_bound_method(self, instance, func): |
| cls = type(instance) |
| if issubclass(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)." |
| ) %(instance, )) |
| im_f = new_instancemethod(func, None, cls) |
| setattr(cls, func.__name__, im_f) |
| return getattr(instance, func.__name__) |
| |
| 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): |
| if not hasattr(input_values, "__iter__"): |
| raise ValueError("expected iterable input; got %r" %(input, )) |
| return input_values |
| |
| @classmethod |
| def expand(cls, input, testcase_func_name=None): |
| """ 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. |
| |
| >>> @parameterized.expand([("foo", 1, 2)]) |
| ... def test_add1(name, input, expected): |
| ... actual = add1(input) |
| ... assert_equal(actual, expected) |
| ... |
| >>> locals() |
| ... 'test_add1_foo_0': <function ...> ... |
| >>> |
| """ |
| |
| def parameterized_expand_wrapper(f): |
| stack = inspect.stack() |
| frame = stack[1] |
| frame_locals = frame[0].f_locals |
| |
| base_name = f.__name__ |
| get_input = cls.input_as_callable(input) |
| for num, args in enumerate(get_input()): |
| p = param.from_decorator(args) |
| if testcase_func_name: |
| # Caller wants to over-ride default test case func/method naming scheme. |
| name = testcase_func_name(f, num, p) |
| else: |
| name_suffix = "_%s" %(num, ) |
| if len(p.args) > 0 and isinstance(p.args[0], compat.string_types): |
| name_suffix += "_" + cls.to_safe_name(p.args[0]) |
| name = base_name + name_suffix |
| frame_locals[name] = cls.param_as_standalone_func(p, f, name) |
| return nottest(f) |
| return parameterized_expand_wrapper |
| |
| @classmethod |
| def param_as_standalone_func(cls, p, func, name): |
| |
| @wraps(func) |
| def standalone_func(*a): |
| return func(*(a + p.args), **p.kwargs) |
| |
| standalone_func.__name__ = name |
| return standalone_func |
| |
| @classmethod |
| def to_safe_name(cls, s): |
| return str(re.sub("[^a-zA-Z0-9_]+", "_", s)) |