diff --git a/conformity/tests/test_fields.py b/conformity/tests/test_fields.py index 5284d5d..9a98089 100644 --- a/conformity/tests/test_fields.py +++ b/conformity/tests/test_fields.py @@ -35,7 +35,7 @@ def test_complex(self): self.assertEqual( schema.errors({"child_ids": [1, 2, "ten"]}), [ - "Key child_ids: Element 2: Not an integer", + "Key child_ids: Index 2: Not a integer", "Key address missing", ], ) @@ -54,7 +54,6 @@ def test_complex(self): [], ) - def test_polymorph(self): card = Dictionary({ diff --git a/conformity/tests/test_validator.py b/conformity/tests/test_validator.py index 4845fa8..4aa12a4 100644 --- a/conformity/tests/test_validator.py +++ b/conformity/tests/test_validator.py @@ -3,7 +3,13 @@ import unittest from ..fields import Dictionary, UnicodeString -from ..validator import validate, validate_call, ValidationError, PositionalError +from ..validator import ( + validate, + validate_call, + validate_method, + ValidationError, + PositionalError +) class ValidatorTests(unittest.TestCase): @@ -27,7 +33,6 @@ def test_validate(self): with self.assertRaises(ValidationError): validate(schema, {"name": "Andrew", "greeeeeeting": "Ahoy-hoy"}) - def test_validate_call(self): schema = Dictionary({ @@ -56,3 +61,34 @@ def greeter(name, greeting="Hello"): with self.assertRaises(PositionalError): greeter("Andrew") + + def test_validate_method(self): + + schema = Dictionary({ + "name": UnicodeString(max_length=20), + "greeting": UnicodeString(), + }, optional_keys=["greeting"]) + + class Greeter(object): + @classmethod + @validate_method(schema, UnicodeString()) + def greeter(cls, name, greeting="Hello"): + # Special case to check return value stuff + if name == "error": + return 5 + return "%s, %s!" % (greeting, name) + + self.assertEqual(Greeter.greeter(name="Andrew"), "Hello, Andrew!") + self.assertEqual(Greeter.greeter(name="Andrew", greeting="Ahoy"), "Ahoy, Andrew!") + + with self.assertRaises(ValidationError): + Greeter.greeter(name="Andrewverylongnameperson") + + with self.assertRaises(ValidationError): + Greeter.greeter(name="Andrew", greeeeeeting="Boo") + + with self.assertRaises(ValidationError): + Greeter.greeter(name="error") + + with self.assertRaises(PositionalError): + Greeter.greeter("Andrew") diff --git a/conformity/validator.py b/conformity/validator.py index ea5b8cd..b6d11e8 100644 --- a/conformity/validator.py +++ b/conformity/validator.py @@ -1,3 +1,7 @@ +import types +from functools import partial + + class ValidationError(ValueError): """ Error raised when a value fails to validate. @@ -22,7 +26,7 @@ def validate(schema, value, noun="value"): raise ValidationError("Invalid %s:\n - %s" % (noun, "\n - ".join(errors))) -def validate_call(kwargs, returns): +def validate_call(kwargs, returns, is_method=False): """ Decorator which runs validation on a callable's arguments and its return value. Pass a schema for the kwargs and for the return value. Positional @@ -31,7 +35,14 @@ def validate_call(kwargs, returns): def decorator(func): def inner(*passed_args, **passed_kwargs): # Enforce no positional args - if passed_args: + # first argument of instance method and class method is always positonal so we need + # to make expception for them. Static methods are still validated according to standard rules + # this check happens before methods are bound, so instance method is still a regular function + max_allowed_passed_args_len = 0 + if is_method and type(func) in (types.FunctionType, classmethod): + max_allowed_passed_args_len = 1 + + if len(passed_args) > max_allowed_passed_args_len: raise PositionalError("You cannot call this with positional arguments.") # Validate keyword arguments validate(kwargs, passed_kwargs, "keyword arguments") @@ -41,8 +52,12 @@ def inner(*passed_args, **passed_kwargs): validate(returns, return_value, "return value") return return_value inner.__wrapped__ = func - # caveat: checking for f.__validated__ will only work if - # @validate_call is the outermost decorator + # caveat: checking for f.__validated__ will only work if @validate_call + # is not masked by other decorators except for @classmethod or @staticmethod inner.__validated__ = True return inner return decorator + +# use @validate_method for methods. If it's a class or static method, +# @classdecorator/@staticmethod should be outmost, while @validate_method second outmost +validate_method = partial(validate_call, is_method=True)