Files
catalyst/tests/utils/test_preprocess.py
T
Stewart Douglas 4e2039c9b0 ENH: Coerce user input with API method decorator
Previously we have capitalized input strings at different levels in
our code: in the user-facing API methods and in the asset finder.
This commit moves input string capitalization exclusively to the API
method to which the string was supplied. Specifically, the string is
capitalized by a preprocess API method decorator. The preprocess
decorator passes the input string to the newly defined
ensure_upper_case() method, which returns a TypeError if the argument
supplied is not a string.

ensure_upper_case() is defined in a new file, zipline/utils/input_validation.py.
The existing expect_types() method is also moved there.

Various tests in tests/test_assets.py are modified to account for the
fact that the asset finder method lookup_symol() no longer capitalizes
its supplied argument.
2015-10-08 15:41:33 -04:00

228 lines
6.5 KiB
Python

"""
Tests for zipline.utils.validate.
"""
from types import FunctionType
from unittest import TestCase
from nose_parameterized import parameterized
from zipline.utils.preprocess import call, preprocess
from zipline.utils.input_validation import expect_types, optional
def noop(func, argname, argvalue):
assert isinstance(func, FunctionType)
assert isinstance(argname, str)
return argvalue
class PreprocessTestCase(TestCase):
@parameterized.expand([
('too_many', (1, 2, 3), {}),
('too_few', (1,), {}),
('collision', (1,), {'a': 1}),
('unexpected', (1,), {'q': 1}),
])
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 test_preprocess_co_filename(self):
def undecorated():
pass
decorated = preprocess()(undecorated)
self.assertEqual(
undecorated.__code__.co_filename,
decorated.__code__.co_filename,
)
def test_preprocess_preserves_docstring(self):
@preprocess()
def func():
"My awesome docstring"
self.assertEqual(func.__doc__, "My awesome docstring")
def test_preprocess_preserves_function_name(self):
@preprocess()
def arglebargle():
pass
self.assertEqual(arglebargle.__name__, 'arglebargle')
@parameterized.expand([
((1, 2), {}),
((1, 2), {'c': 3}),
((1,), {'b': 2}),
((), {'a': 1, 'b': 2}),
((), {'a': 1, 'b': 2, 'c': 3}),
])
def test_preprocess_no_processors(self, args, kwargs):
@preprocess()
def func(a, b, c=3):
return a, b, c
self.assertEqual(func(*args, **kwargs), (1, 2, 3))
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)
@parameterized.expand([
((1, 2), {}),
((1, 2), {'c': 3}),
((1,), {'b': 2}),
((), {'a': 1, 'b': 2}),
((), {'a': 1, 'b': 2, 'c': 3}),
])
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))
@parameterized.expand([
((1, 2), {}),
((1, 2), {'c': 3}),
((1,), {'b': 2}),
((), {'a': 1, 'b': 2}),
((), {'a': 1, 'b': 2, 'c': 3}),
])
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 test_expect_types(self):
@expect_types(a=int, b=int)
def foo(a, b, c):
return a, b, c
self.assertEqual(foo(1, 2, 3), (1, 2, 3))
self.assertEqual(foo(1, 2, c=3), (1, 2, 3))
self.assertEqual(foo(1, b=2, c=3), (1, 2, 3))
self.assertEqual(foo(1, 2, c='3'), (1, 2, '3'))
for not_int in (str, float):
with self.assertRaises(TypeError) as e:
foo(not_int(1), 2, 3)
self.assertEqual(
e.exception.args[0],
"{modname}.foo() expected a value of type "
"int for argument 'a', but got {t} instead.".format(
modname=foo.__module__,
t=not_int.__name__,
)
)
with self.assertRaises(TypeError):
foo(1, not_int(2), 3)
with self.assertRaises(TypeError):
foo(not_int(1), not_int(2), 3)
def test_expect_types_with_tuple(self):
@expect_types(a=(int, float))
def foo(a):
return a
self.assertEqual(foo(1), 1)
self.assertEqual(foo(1.0), 1.0)
with self.assertRaises(TypeError) as e:
foo('1')
expected_message = (
"{modname}.foo() expected a value of "
"type int or float for argument 'a', but got str instead."
).format(modname=foo.__module__)
self.assertEqual(e.exception.args[0], expected_message)
def test_expect_optional_types(self):
@expect_types(a=optional(int))
def foo(a=None):
return a
self.assertIs(foo(), None)
self.assertIs(foo(None), None)
self.assertIs(foo(a=None), None)
self.assertEqual(foo(1), 1)
self.assertEqual(foo(a=1), 1)
with self.assertRaises(TypeError) as e:
foo('1')
expected_message = (
"{modname}.foo() expected a value of "
"type int or NoneType for argument 'a', but got str instead."
).format(modname=foo.__module__)
self.assertEqual(e.exception.args[0], expected_message)