Skip to content

Commit

Permalink
Merge pull request #2 from eventbrite/validate_method
Browse files Browse the repository at this point in the history
workaround for object methods
  • Loading branch information
andrewgodwin authored Oct 4, 2016
2 parents dc9774c + b5b0c82 commit 5b190de
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 8 deletions.
3 changes: 1 addition & 2 deletions conformity/tests/test_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
],
)
Expand All @@ -54,7 +54,6 @@ def test_complex(self):
[],
)


def test_polymorph(self):

card = Dictionary({
Expand Down
40 changes: 38 additions & 2 deletions conformity/tests/test_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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({
Expand Down Expand Up @@ -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")
23 changes: 19 additions & 4 deletions conformity/validator.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import types
from functools import partial


class ValidationError(ValueError):
"""
Error raised when a value fails to validate.
Expand All @@ -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
Expand All @@ -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")
Expand All @@ -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)

0 comments on commit 5b190de

Please sign in to comment.