def test_preprocess_bad_processor_name(self): a_processor = preprocess(a=int) # Should work fine. @a_processor def func_with_arg_named_a(a): pass @a_processor def func_with_default_arg_named_a(a=1): pass message = "Got processors for unknown arguments: %s." % {'a'} with self.assertRaises(TypeError) as e: @a_processor def func_with_no_args(): pass self.assertEqual(e.exception.args[0], message) with self.assertRaises(TypeError) as e: @a_processor def func_with_arg_named_b(b): pass self.assertEqual(e.exception.args[0], message)
def expect_dimensions(__funcname=_qualified_name, **dimensions): """ Preprocessing decorator that verifies inputs are numpy arrays with a specific dimensionality. """ if isinstance(__funcname, str): def get_funcname(_): return __funcname else: get_funcname = __funcname def _expect_dimension(expected_ndim): def _check(func, argname, argvalue): actual_ndim = argvalue.ndim if actual_ndim != expected_ndim: if actual_ndim == 0: actual_repr = 'scalar' else: actual_repr = "%d-D array" % actual_ndim raise ValueError( "{func}() expected a {expected:d}-D array" " for argument {argname!r}, but got a {actual}" " instead.".format( func=get_funcname(func), expected=expected_ndim, argname=argname, actual=actual_repr, ) ) return argvalue return _check return preprocess(**valmap(_expect_dimension, dimensions))
def test_preprocess_co_filename(self): def undecorated(): pass decorated = preprocess()(undecorated) self.assertEqual( undecorated.__code__.co_filename, decorated.__code__.co_filename, )
def test_preprocess_on_function(self, args, kwargs): decorators = [ preprocess(a=call(str), b=call(float), c=call(lambda x: x + 1)), ] for decorator in decorators: @decorator def func(a, b, c=3): return a, b, c self.assertEqual(func(*args, **kwargs), ('1', 2.0, 4))
def coerce_types(**kwargs): """ Preprocessing decorator that applies type coercions. Parameters ---------- **kwargs : dict[str -> (type, callable)] Keyword arguments mapping function parameter names to pairs of (from_type, to_type). """ def _coerce(types): return coerce(*types) return preprocess(**valmap(_coerce, kwargs))
def expect_kinds(**named): """ Preprocessing decorator that verifies inputs have expected dtype kinds. """ for name, kind in iteritems(named): if not isinstance(kind, (str, tuple)): raise TypeError( "expect_dtype_kinds() expected a string or tuple of strings" " for argument {name!r}, but got {kind} instead.".format( name=name, kind=dtype, ) ) @preprocess(kinds=call(lambda x: x if isinstance(x, tuple) else (x,))) def _expect_kind(kinds): """ Factory for kind-checking functions that work the @preprocess decorator. """ def error_message(func, argname, value): # If the bad value has a dtype, but it's wrong, show the dtype # kind. Otherwise just show the value. try: value_to_show = value.dtype.kind except AttributeError: value_to_show = value return ( "{funcname}() expected a numpy object of kind {kinds} " "for argument {argname!r}, but got {value!r} instead." ).format( funcname=_qualified_name(func), kinds=' or '.join(map(repr, kinds)), argname=argname, value=value_to_show, ) def _actual_preprocessor(func, argname, argvalue): if getattrs(argvalue, ('dtype', 'kind'), object()) not in kinds: raise TypeError(error_message(func, argname, argvalue)) return argvalue return _actual_preprocessor return preprocess(**valmap(_expect_kind, named))
def _expect_bounded(make_bounded_check, __funcname, **named): def valid_bounds(t): return ( isinstance(t, tuple) and len(t) == 2 and t != (None, None) ) for name, bounds in iteritems(named): if not valid_bounds(bounds): raise TypeError( "expect_bounded() expected a tuple of bounds for" " argument '{name}', but got {bounds} instead.".format( name=name, bounds=bounds, ) ) return preprocess(**valmap(make_bounded_check, named))
def test_preprocess_on_method(self, args, kwargs): decorators = [ preprocess(a=call(str), b=call(float), c=call(lambda x: x + 1)), ] for decorator in decorators: class Foo(object): @decorator def method(self, a, b, c=3): return a, b, c @classmethod @decorator def clsmeth(cls, a, b, c=3): return a, b, c self.assertEqual(Foo.clsmeth(*args, **kwargs), ('1', 2.0, 4)) self.assertEqual(Foo().method(*args, **kwargs), ('1', 2.0, 4))
def expect_types(__funcname=_qualified_name, **named): """ Preprocessing decorator that verifies inputs have expected types. Notes ----- A special argument, __funcname, can be provided as a string to override the function name shown in error messages. This is most often used on __init__ or __new__ methods to make errors refer to the class name instead of the function name. """ for name, type_ in iteritems(named): if not isinstance(type_, (type, tuple)): raise TypeError( "expect_types() expected a type or tuple of types for " "argument '{name}', but got {type_} instead.".format( name=name, type_=type_, ) ) def _expect_type(type_): # Slightly different messages for type and tuple of types. _template = ( "%(funcname)s() expected a value of type {type_or_types} " "for argument '%(argname)s', but got %(actual)s instead." ) if isinstance(type_, tuple): template = _template.format( type_or_types=' or '.join(map(_qualified_name, type_)) ) else: template = _template.format(type_or_types=_qualified_name(type_)) return make_check( exc_type=TypeError, template=template, pred=lambda v: not isinstance(v, type_), actual=compose(_qualified_name, type), funcname=__funcname, ) return preprocess(**valmap(_expect_type, named))
def test_preprocess_doesnt_change_TypeErrors(self, name, args, kwargs): """ Verify that the validate decorator doesn't swallow typeerrors that would be raised when calling a function with invalid arguments """ def undecorated(x, y): return x, y decorated = preprocess(x=noop, y=noop)(undecorated) with self.assertRaises(TypeError) as e: undecorated(*args, **kwargs) undecorated_errargs = e.exception.args with self.assertRaises(TypeError) as e: decorated(*args, **kwargs) decorated_errargs = e.exception.args self.assertEqual(len(decorated_errargs), 1) self.assertEqual(len(undecorated_errargs), 1) self.assertEqual(decorated_errargs[0], undecorated_errargs[0])
def expect_element(__funcname=_qualified_name, **named): """ Preprocessing decorator that verifies inputs are elements of some expected collection. Notes ----- A special argument, __funcname, can be provided as a string to override the function name shown in error messages. This is most often used on __init__ or __new__ methods to make errors refer to the class name instead of the function name. This uses the `in` operator (__contains__) to make the containment check. This allows us to use any custom container as long as the object supports the container protocol. """ def _expect_element(collection): if isinstance(collection, (set, frozenset)): # Special case the error message for set and frozen set to make it # less verbose. collection_for_error_message = tuple(sorted(collection)) else: collection_for_error_message = collection template = ( "%(funcname)s() expected a value in {collection} " "for argument '%(argname)s', but got %(actual)s instead." ).format(collection=collection_for_error_message) return make_check( ValueError, template, complement(op.contains(collection)), repr, funcname=__funcname, ) return preprocess(**valmap(_expect_element, named))
def expect_dtypes(__funcname=_qualified_name, **named): """ Preprocessing decorator that verifies inputs have expected numpy dtypes. Usage ----- >>> from numpy import dtype, arange, int8, float64 >>> @expect_dtypes(x=dtype(int8)) ... def foo(x, y): ... return x, y ... >>> foo(arange(3, dtype=int8), 'foo') (array([0, 1, 2], dtype=int8), 'foo') >>> foo(arange(3, dtype=float64), 'foo') # doctest: +NORMALIZE_WHITESPACE ... # doctest: +ELLIPSIS Traceback (most recent call last): ... TypeError: ...foo() expected a value with dtype 'int8' for argument 'x', but got 'float64' instead. """ for name, type_ in iteritems(named): if not isinstance(type_, (dtype, tuple)): raise TypeError( "expect_dtypes() expected a numpy dtype or tuple of dtypes" " for argument {name!r}, but got {dtype} instead.".format( name=name, dtype=dtype, ) ) if isinstance(__funcname, str): def get_funcname(_): return __funcname else: get_funcname = __funcname @preprocess(dtypes=call(lambda x: x if isinstance(x, tuple) else (x,))) def _expect_dtype(dtypes): """ Factory for dtype-checking functions that work with the @preprocess decorator. """ def error_message(func, argname, value): # If the bad value has a dtype, but it's wrong, show the dtype # name. Otherwise just show the value. try: value_to_show = value.dtype.name except AttributeError: value_to_show = value return ( "{funcname}() expected a value with dtype {dtype_str} " "for argument {argname!r}, but got {value!r} instead." ).format( funcname=get_funcname(func), dtype_str=' or '.join(repr(d.name) for d in dtypes), argname=argname, value=value_to_show, ) def _actual_preprocessor(func, argname, argvalue): if getattr(argvalue, 'dtype', object()) not in dtypes: raise TypeError(error_message(func, argname, argvalue)) return argvalue return _actual_preprocessor return preprocess(**valmap(_expect_dtype, named))