def expect_types(*_pos, **named): """ Preprocessing decorator that verifies inputs have expected types. Usage ----- >>> @expect_types(x=int, y=str) ... def foo(x, y): ... return x, y ... >>> foo(2, '3') (2, '3') >>> foo(2.0, '3') Traceback (most recent call last): ... TypeError: foo() expected an argument of type 'int' for argument 'x', but got float instead. # noqa """ if _pos: raise TypeError("expect_types() only takes keyword arguments.") 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_, )) return preprocess(**valmap(_expect_type, named))
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_dtypes(*_pos, **named): """ Preprocessing decorator that verifies inputs have expected numpy dtypes. Usage ----- >>> from numpy import dtype, arange >>> @expect_dtypes(x=dtype(int)) ... def foo(x, y): ... return x, y ... >>> foo(arange(3), 'foo') (array([0, 1, 2]), 'foo') >>> foo(arange(3, dtype=float), 'foo') Traceback (most recent call last): ... TypeError: foo() expected an argument with dtype 'int64' for argument 'x', but got dtype 'float64' instead. # noqa """ if _pos: raise TypeError("expect_dtypes() only takes keyword arguments.") 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, )) return preprocess(**valmap(_expect_dtype, named))
def expect_kinds(**named): """ Preprocessing decorator that verifies inputs have expected dtype kinds. Usage ----- >>> from numpy import int64, int32, float32 >>> @expect_kinds(x='i') ... def foo(x): ... return x ... >>> foo(int64(2)) 2 >>> foo(int32(2)) 2 >>> foo(float32(2)) # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS Traceback (most recent call last): ... TypeError: ...foo() expected a numpy object of kind 'i' for argument 'x', but got 'f' instead. """ 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_dtypes(*_pos, **named): """ Preprocessing decorator that verifies inputs have expected numpy dtypes. Usage ----- >>> from numpy import dtype, arange >>> @expect_dtypes(x=dtype(int)) ... def foo(x, y): ... return x, y ... >>> foo(arange(3), 'foo') (array([0, 1, 2]), 'foo') >>> foo(arange(3, dtype=float), 'foo') Traceback (most recent call last): ... TypeError: foo() expected an argument with dtype 'int64' for argument 'x', but got dtype 'float64' instead. # noqa """ if _pos: raise TypeError("expect_dtypes() only takes keyword arguments.") 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, ) ) return preprocess(**valmap(_expect_dtype, named))
def expect_element(*_pos, **named): """ Preprocessing decorator that verifies inputs are elements of some expected collection. Usage ----- >>> @expect_element(x=('a', 'b')) ... def foo(x): ... return x.upper() ... >>> foo('a') 'A' >>> foo('b') 'B' >>> foo('c') Traceback (most recent call last): ... ValueError: foo() expected a value in ('a', 'b') for argument 'x', but got 'c' instead. # noqa Notes ----- 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. """ if _pos: raise TypeError("expect_element() only takes keyword arguments.") return preprocess(**valmap(_expect_element, named))
def expect_kinds(**named): """ Preprocessing decorator that verifies inputs have expected dtype kinds. Usage ----- >>> from numpy import int64, int32, float32 >>> @expect_kinds(x='i') ... def foo(x): ... return x ... >>> foo(int64(2)) 2 >>> foo(int32(2)) 2 >>> foo(float32(2)) Traceback (most recent call last): ...n TypeError: foo() expected a numpy object of kind 'i' for argument 'x', but got 'f' instead. # noqa """ 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 test_preprocess_co_filename(self): def undecorated(): pass decorated = preprocess()(undecorated) assert undecorated.__code__.co_filename == decorated.__code__.co_filename
def expect_types(*_pos, **named): """ Preprocessing decorator that verifies inputs have expected types. Usage ----- >>> @expect_types(x=int, y=str) ... def foo(x, y): ... return x, y ... >>> foo(2, '3') (2, '3') >>> foo(2.0, '3') Traceback (most recent call last): ... TypeError: foo() expected an argument of type 'int' for argument 'x', but got float instead. # noqa """ if _pos: raise TypeError("expect_types() only takes keyword arguments.") 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_, ) ) return preprocess(**valmap(_expect_type, named))
def expect_dtypes(**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, ) ) @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=_qualified_name(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))
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_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_types(__funcname=_qualified_name, **named): """ Preprocessing decorator that verifies inputs have expected types. Examples -------- >>> @expect_types(x=int, y=str) ... def foo(x, y): ... return x, y ... >>> foo(2, '3') (2, '3') >>> foo(2.0, '3') # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS Traceback (most recent call last): ... TypeError: ...foo() expected a value of type int for argument 'x', but got float instead. 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 named.items(): 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 expect_dtypes(*_pos, **named): """ Preprocessing decorator that verifies inputs have expected numpy dtypes. Usage ----- >>> from numpy import dtype, arange >>> @expect_dtypes(x=dtype(int)) ... def foo(x, y): ... return x, y ... >>> foo(arange(3), 'foo') (array([0, 1, 2]), 'foo') >>> foo(arange(3, dtype=float), 'foo') Traceback (most recent call last): ... TypeError: foo() expected an argument with dtype 'int64' for argument 'x', but got dtype 'float64' instead. # noqa """ if _pos: raise TypeError("expect_dtypes() only takes keyword arguments.") 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) ) def _expect_dtype(_dtype_or_dtype_tuple): """ Factory for dtype-checking functions that work the @preprocess decorator. """ # Slightly different messages for dtype and tuple of dtypes. if isinstance(_dtype_or_dtype_tuple, tuple): allowed_dtypes = _dtype_or_dtype_tuple else: allowed_dtypes = (_dtype_or_dtype_tuple,) template = ( "%(funcname)s() expected a value with dtype {dtype_str} " "for argument '%(argname)s', but got %(actual)r instead." ).format(dtype_str=" or ".join(repr(d.name) for d in allowed_dtypes)) def check_dtype(value): return getattr(value, "dtype", None) not in allowed_dtypes def display_bad_value(value): # If the bad value has a dtype, but it's wrong, show the dtype # name. try: return value.dtype.name except AttributeError: return value return make_check(exc_type=TypeError, template=template, pred=check_dtype, actual=display_bad_value) return preprocess(**valmap(_expect_dtype, named))
def expect_types(__funcname=_qualified_name, **named): """ Preprocessing decorator that verifies inputs have expected types. Examples -------- >>> @expect_types(x=int, y=str) ... def foo(x, y): ... return x, y ... >>> foo(2, '3') (2, '3') >>> foo(2.0, '3') # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS Traceback (most recent call last): ... TypeError: ...foo() expected a value of type int for argument 'x', but got float instead. 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_co_filename(self): def undecorated(): pass decorated = preprocess()(undecorated) self.assertEqual( undecorated.__code__.co_filename, decorated.__code__.co_filename, )
def expect_element(__funcname=_qualified_name, **named): """ Preprocessing decorator that verifies inputs are elements of some expected collection. Examples -------- >>> @expect_element(x=('a', 'b')) ... def foo(x): ... return x.upper() ... >>> foo('a') 'A' >>> foo('b') 'B' >>> foo('c') # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS Traceback (most recent call last): ... ValueError: ...foo() expected a value in ('a', 'b') for argument 'x', but got 'c' instead. 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 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 expect_dimensions(__funcname=_qualified_name, **dimensions): """ Preprocessing decorator that verifies inputs are numpy arrays with a specific dimensionality. Examples -------- >>> from numpy import array >>> @expect_dimensions(x=1, y=2) ... def foo(x, y): ... return x[0] + y[0, 0] ... >>> foo(array([1, 1]), array([[1, 1], [2, 2]])) 2 >>> foo(array([1, 1]), array([1, 1])) # doctest: +NORMALIZE_WHITESPACE ... # doctest: +ELLIPSIS Traceback (most recent call last): ... ValueError: ...foo() expected a 2-D array for argument 'y', but got a 1-D array instead. """ 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 expect_types(*_pos, **named): """ Preprocessing decorator that verifies inputs have expected types. Usage ----- >>> @expect_types(x=int, y=str) ... def foo(x, y): ... return x, y ... >>> foo(2, '3') (2, '3') >>> foo(2.0, '3') # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS Traceback (most recent call last): ... TypeError: ...foo() expected a value of type int for argument 'x', but got float instead. """ if _pos: raise TypeError("expect_types() only takes keyword arguments.") 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( TypeError, template, lambda v: not isinstance(v, type_), compose(_qualified_name, type), ) return preprocess(**valmap(_expect_type, 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 expect_element(*_pos, **named): """ Preprocessing decorator that verifies inputs are elements of some expected collection. Usage ----- >>> @expect_element(x=('a', 'b')) ... def foo(x): ... return x.upper() ... >>> foo('a') 'A' >>> foo('b') 'B' >>> foo('c') # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS Traceback (most recent call last): ... ValueError: ...foo() expected a value in ('a', 'b') for argument 'x', but got 'c' instead. Notes ----- 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. """ if _pos: raise TypeError("expect_element() only takes keyword arguments.") 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, ) return preprocess(**valmap(_expect_element, named))
def expect_element(*_pos, **named): """ Preprocessing decorator that verifies inputs are elements of some expected collection. Usage ----- >>> @expect_element(x=('a', 'b')) ... def foo(x): ... return x.upper() ... >>> foo('a') 'A' >>> foo('b') 'B' >>> foo('c') # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS Traceback (most recent call last): ... ValueError: ...foo() expected a value in ('a', 'b') for argument 'x', but got 'c' instead. Notes ----- 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. """ if _pos: raise TypeError("expect_element() only takes keyword arguments.") 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, ) return preprocess(**valmap(_expect_element, named))
def expect_dimensions(__funcname=_qualified_name, **dimensions): """ Preprocessing decorator that verifies inputs are numpy arrays with a specific dimensionality. Examples -------- >>> from numpy import array >>> @expect_dimensions(x=1, y=2) ... def foo(x, y): ... return x[0] + y[0, 0] ... >>> foo(array([1, 1]), array([[1, 1], [2, 2]])) 2 >>> foo(array([1, 1]), array([1, 1])) # doctest: +NORMALIZE_WHITESPACE ... # doctest: +ELLIPSIS Traceback (most recent call last): ... ValueError: ...foo() expected a 2-D array for argument 'y', but got a 1-D array instead. """ 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_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 restrict_to_dtype(dtype, message_template): """ A factory for decorators that restricting Factor methods to only be callable on Factors with a specific dtype. This is conceptually similar to zipline.utils.input_validation.expect_dtypes, but provides more flexibility for providing error messages that are specifically targeting Factor methods. Parameters ---------- dtype : numpy.dtype The dtype on which the decorated method may be called. message_template : str A template for the error message to be raised. `message_template.format` will be called with keyword arguments `method_name`, `expected_dtype`, and `received_dtype`. Usage ----- @restrict_to_dtype( dtype=float64_dtype, message_template=( "{method_name}() was called on a factor of dtype {received_dtype}." "{method_name}() requires factors of dtype{expected_dtype}." ), ) def some_factor_method(self, ...): self.stuff_that_requires_being_float64(...) """ def processor(factor_method, _, factor_instance): factor_dtype = factor_instance.dtype if factor_dtype != dtype: raise TypeError( message_template.format( method_name=factor_method.__name__, expected_dtype=dtype.name, received_dtype=factor_dtype, )) return factor_instance return preprocess(self=processor)
def restrict_to_dtype(dtype, message_template): """ A factory for decorators that restricting Factor methods to only be callable on Factors with a specific dtype. This is conceptually similar to zipline.utils.input_validation.expect_dtypes, but provides more flexibility for providing error messages that are specifically targeting Factor methods. Parameters ---------- dtype : numpy.dtype The dtype on which the decorated method may be called. message_template : str A template for the error message to be raised. `message_template.format` will be called with keyword arguments `method_name`, `expected_dtype`, and `received_dtype`. Usage ----- @restrict_to_dtype( dtype=float64_dtype, message_template=( "{method_name}() was called on a factor of dtype {received_dtype}." "{method_name}() requires factors of dtype{expected_dtype}." ), ) def some_factor_method(self, ...): self.stuff_that_requires_being_float64(...) """ def processor(factor_method, _, factor_instance): factor_dtype = factor_instance.dtype if factor_dtype != dtype: raise TypeError( message_template.format( method_name=factor_method.__name__, expected_dtype=dtype.name, received_dtype=factor_dtype, ) ) return factor_instance return preprocess(self=processor)
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: @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 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 assert Foo.clsmeth(*args, **kwargs) == ("1", 2.0, 4) assert Foo().method(*args, **kwargs) == ("1", 2.0, 4)
def expect_dimensions(**dimensions): """ Preprocessing decorator that verifies inputs are numpy arrays with a specific dimensionality. Usage ----- >>> from numpy import array >>> @expect_dimensions(x=1, y=2) ... def foo(x, y): ... return x[0] + y[0, 0] ... >>> foo(array([1, 1]), array([[1, 1], [2, 2]])) 2 >>> foo(array([1, 1], array([1, 1]))) Traceback (most recent call last): ... TypeError: foo() expected a 2-D array for argument 'y', but got a 1-D array instead. # noqa """ def _expect_dimension(expected_ndim): def _check(func, argname, argvalue): funcname = _qualified_name(func) 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=funcname, expected=expected_ndim, argname=argname, actual=actual_repr, )) return argvalue return _check return preprocess(**valmap(_expect_dimension, dimensions))
def expect_dimensions(**dimensions): """ Preprocessing decorator that verifies inputs are numpy arrays with a specific dimensionality. Usage ----- >>> from numpy import array >>> @expect_dimensions(x=1, y=2) ... def foo(x, y): ... return x[0] + y[0, 0] ... >>> foo(array([1, 1]), array([[1, 1], [2, 2]])) 2 >>> foo(array([1, 1], array([1, 1]))) Traceback (most recent call last): ... TypeError: foo() expected a 2-D array for argument 'y', but got a 1-D array instead. # noqa """ def _expect_dimension(expected_ndim): def _check(func, argname, argvalue): funcname = _qualified_name(func) 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=funcname, expected=expected_ndim, argname=argname, actual=actual_repr, ) ) return argvalue return _check return preprocess(**valmap(_expect_dimension, dimensions))
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 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). Examples -------- >>> @coerce_types(x=(float, int), y=(int, str)) ... def func(x, y): ... return (x, y) ... >>> func(1.0, 3) (1, '3') """ def _coerce(types): return coerce(*types) return preprocess(**valmap(_coerce, kwargs))
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). Usage ----- >>> @coerce_types(x=(float, int), y=(int, str)) ... def func(x, y): ... return (x, y) ... >>> func(1.0, 3) (1, '3') """ def _coerce(types): return coerce(*types) return preprocess(**valmap(_coerce, kwargs))
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 pytest.raises(TypeError) as excinfo: undecorated(*args, **kwargs) undecorated_errargs = excinfo.value.args with pytest.raises(TypeError) as excinfo: decorated(*args, **kwargs) decorated_errargs = excinfo.value.args assert len(decorated_errargs) == 1 assert len(undecorated_errargs) == 1 assert decorated_errargs[0] == undecorated_errargs[0]
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 pytest.raises(TypeError, match=message): @a_processor def func_with_no_args(): pass with pytest.raises(TypeError, match=message): @a_processor def func_with_arg_named_b(b): pass
def expect_dtypes(*_pos, **named): """ Preprocessing decorator that verifies inputs have expected numpy dtypes. Usage ----- >>> from numpy import dtype, arange >>> @expect_dtypes(x=dtype(int)) ... def foo(x, y): ... return x, y ... >>> foo(arange(3), 'foo') (array([0, 1, 2]), 'foo') >>> foo(arange(3, dtype=float), 'foo') Traceback (most recent call last): ... TypeError: foo() expected an argument with dtype 'int64' for argument 'x', but got dtype 'float64' instead. # noqa """ if _pos: raise TypeError("expect_dtypes() only takes keyword arguments.") 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, ) ) def _expect_dtype(_dtype_or_dtype_tuple): """ Factory for dtype-checking functions that work the @preprocess decorator. """ # Slightly different messages for dtype and tuple of dtypes. if isinstance(_dtype_or_dtype_tuple, tuple): allowed_dtypes = _dtype_or_dtype_tuple else: allowed_dtypes = (_dtype_or_dtype_tuple,) template = ( "%(funcname)s() expected a value with dtype {dtype_str} " "for argument '%(argname)s', but got %(actual)r instead." ).format(dtype_str=' or '.join(repr(d.name) for d in allowed_dtypes)) def check_dtype(value): return getattr(value, 'dtype', None) not in allowed_dtypes def display_bad_value(value): # If the bad value has a dtype, but it's wrong, show the dtype # name. try: return value.dtype.name except AttributeError: return value return make_check( exc_type=TypeError, template=template, pred=check_dtype, actual=display_bad_value, ) return preprocess(**valmap(_expect_dtype, named))
def expect_bounded(__funcname=_qualified_name, **named): """ Preprocessing decorator verifying that inputs fall between bounds. Bounds should be passed as a pair of ``(min_value, max_value)``. Both bounds are checked inclusively. ``None`` may be passed as ``min_value`` or ``max_value`` to signify that the input is only bounded above or below. Usage ----- >>> @expect_bounded(x=(1, 5)) ... def foo(x): ... return x + 1 ... >>> foo(1) 2 >>> foo(5) 6 >>> foo(6) # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS Traceback (most recent call last): ... ValueError: ...foo() expected a value between 1 and 5 for argument 'x', but got 6 instead. >>> @expect_bounded(x=(2, None)) ... def foo(x): ... return x ... >>> foo(100000) 100000 >>> foo(1) # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS Traceback (most recent call last): ... ValueError: ...foo() expected a value greater than or equal to 2 for argument 'x', but got 1 instead. >>> @expect_bounded(x=(None, 5)) ... def foo(x): ... return x ... >>> foo(6) # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS Traceback (most recent call last): ... ValueError: ...foo() expected a value less than or equal to 5 for argument 'x', but got 6 instead. """ 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, )) def _expect_bounded(bounds): (lower, upper) = bounds if lower is None: def should_fail(value): return value > upper predicate_descr = "less than or equal to " + str(upper) elif upper is None: def should_fail(value): return value < lower predicate_descr = "greater than or equal to " + str(lower) else: def should_fail(value): return not (lower <= value <= upper) predicate_descr = "between %s and %s" % bounds template = ( "%(funcname)s() expected a value {predicate}" " for argument '%(argname)s', but got %(actual)s instead.").format( predicate=predicate_descr) return make_check( exc_type=ValueError, template=template, pred=should_fail, actual=repr, funcname=__funcname, ) return preprocess(**valmap(_expect_bounded, named))
def expect_bounded(**named): """ Preprocessing decorator verifying that inputs fall between bounds. Bounds should be passed as a pair of ``(min_value, max_value)``. Both bounds are checked inclusively. ``None`` may be passed as ``min_value`` or ``max_value`` to signify that the input is only bounded above or below. Usage ----- >>> @expect_bounded(x=(1, 5)) ... def foo(x): ... return x + 1 ... >>> foo(1) 2 >>> foo(5) 6 >>> foo(6) # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS Traceback (most recent call last): ... ValueError: ...foo() expected a value between 1 and 5 for argument 'x', but got 6 instead. >>> @expect_bounded(x=(2, None)) ... def foo(x): ... return x ... >>> foo(100000) 100000 >>> foo(1) # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS Traceback (most recent call last): ... ValueError: ...foo() expected a value greater than or equal to 2 for argument 'x', but got 1 instead. >>> @expect_bounded(x=(None, 5)) ... def foo(x): ... return x ... >>> foo(6) # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS Traceback (most recent call last): ... ValueError: ...foo() expected a value less than or equal to 5 for argument 'x', but got 6 instead. """ 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, ) ) def _expect_bounded(bounds): (lower, upper) = bounds if lower is None: should_fail = lambda value: value > upper predicate_descr = "less than or equal to " + str(upper) elif upper is None: should_fail = lambda value: value < lower predicate_descr = "greater than or equal to " + str(lower) else: should_fail = lambda value: not (lower <= value <= upper) predicate_descr = "between %s and %s" % bounds template = ( "%(funcname)s() expected a value {predicate}" " for argument '%(argname)s', but got %(actual)s instead." ).format(predicate=predicate_descr) return make_check( exc_type=ValueError, template=template, pred=should_fail, actual=repr, ) return preprocess(**valmap(_expect_bounded, named))