-
Notifications
You must be signed in to change notification settings - Fork 218
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Argument validation decorator #185
Argument validation decorator #185
Conversation
FWIW, I implemented something similar for one of my projects, which I'd like to share here to add some color to how such a decorator should work if it's added to voluptuous. My implementation works with named args, varargs and keyword args. Although I'm new to voluptuous, I think this is the right feature set for a decorator that is going to ship with the library. For my solution, I used Here's my implementation and tests: import functools
import inspect
import itertools
from collections import OrderedDict
from future.utils import iteritems
from future.utils import viewkeys
def apply_schema(**schema_map):
def decorator(func):
arg_spec = inspect.getargspec(func)
varargs = arg_spec.varargs
keywords = arg_spec.keywords
arg_count = len(arg_spec.args)
arg_positions = OrderedDict(
(arg, pos)
for pos, arg in enumerate(arg_spec.args)
)
@functools.wraps(func)
def wrapper(*args, **kwargs):
converted = {}
for arg_name, pos in iteritems(arg_positions):
if pos >= len(args):
break
if arg_name in schema_map:
converted[pos] = schema_map[arg_name](args[pos])
if varargs in schema_map:
for pos, arg in enumerate(itertools.islice(args, arg_count, None), arg_count):
converted[pos] = schema_map[varargs](arg)
if keywords in schema_map:
for kw in viewkeys(kwargs):
if kw in arg_positions:
if kw in schema_map:
kwargs[kw] = schema_map[kw](kwargs[kw])
else:
kwargs[kw] = schema_map[keywords](kwargs[kw])
else:
for kw in viewkeys(kwargs) & viewkeys(schema_map):
kwargs[kw] = schema_map[kw](kwargs[kw])
if converted:
validated_args = (converted.get(i, arg) for i, arg in enumerate(args))
else:
validated_args = args
return func(*validated_args, **kwargs)
return wrapper
return decorator
def test_apply_schema():
schema = voluptuous.Schema({'foo': voluptuous.Coerce(int)})
input_arg = {'foo': '1'}
expect = {'foo': 1}
@apply_schema(arg=schema)
def func_named(arg=None):
assert arg == expect
@apply_schema(arg=schema)
def func_keywords(**kwargs):
assert kwargs['arg'] == expect
@apply_schema(arg=schema)
def func_positional(arg):
assert arg == expect
@apply_schema(args=schema)
def func_varargs(*args):
for arg in args:
assert arg == expect
@apply_schema(named=voluptuous.Schema(int), kwargs=schema)
def func_all_keywords(ignored=None, named=None, **kwargs):
assert type(named) is int
for arg in kwargs.values():
assert arg == expect
func_positional(input_arg)
func_positional(arg=input_arg)
func_named(input_arg)
func_named(arg=input_arg)
func_keywords(arg=input_arg)
func_all_keywords(named=1, ignored=None, a=input_arg, b=input_arg)
func_varargs(input_arg, input_arg) Edit: noticed that I was missing coverage on the case where a named argument should not have a schema applied, so I fixed the test case for |
👍 Thanks for the ideas! In my opinion using Although I like the idea of calling the decorator Sum up:
|
Hmm... my decorator expects the kwargs with which it's invokes to match the names of the args used in the function signature, so that it can apply the schema strictly to the args passed at the call site. This precludes any reserved kwargs, such as I don't see an obvious way to use I think that succinct compatibility is possible with my solution by changing the decorator setup to be like so: def decorator(func):
if hasattr(inspect, 'getfullargspec'):
arg_spec = inspect.getfullargspec(func)
keywords = arg_spec.varkw
else:
arg_spec = inspect.getargspec(func)
keywords = arg_spec.keywords
varargs = arg_spec.varargs
arg_count = len(arg_spec.args)
arg_positions = OrderedDict(
(arg, pos)
for pos, arg in enumerate(arg_spec.args)
) Edit: I confirmed that this compatibility update works with my existing tests, plus this new case: if PY3:
@apply_schema(args=schema, arg=schema)
def func_keywords_only(ignored, *args, arg=None, **kwargs):
assert arg == expect
func_keywords_only(None, input_arg, input_arg, extra=None, arg=input_arg) |
Hey! Here is one of my projects called ratatoskr. It has a decorator called protectron that realizes this functionality. It can also be used with voluptuous. |
Hey @ngergo I personally like the idea of decorator apply_schema. Please resolve the conflicts and get this merged to master. Thanks |
Hey @tusharmakkar08 , I'll refresh the PR and rebase to the current |
I like the idea of a function validator, but I don't really like the usage of this implementation because it's more verbose than it needs to be IMO. I'd prefer something like this: @validate(arg0=int, arg1=int, __returns__=string)
def func(arg0, arg1=1):
return "hello" |
Hey @alecthomas , @tusharmakkar08 I've refactored my patch according to your proposal. I like this way of specifying conditions to inputs and outputs... Honestly, I'd prefer passing parameters as a dict for that's much easier to review and can be generalised easier if a validator needs to be used more than one time. There is only one thing I'd change here... We should use |
I'm happy with |
Done then. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a really great change, thanks for putting in the effort. Just a couple of minor changes.
@@ -1017,16 +1017,75 @@ def wrapper(*args, **kwargs): | |||
|
|||
return decorator | |||
|
|||
def _args_to_dict(func, args): | |||
""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you make this docstring style (here and elsewhere) consistent with the rest of Voluptuous please?
ret = args_dict.copy() | ||
ret.update(kwargs_dict) | ||
return ret | ||
|
||
|
||
def validate_schema(*a, **kw): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I realise this is not part of your change, but would you remind renaming this to just "validate"? It is currently undocumented, so I'm not concerned with breaking backwards compatibility.
if len(schema_arguments) == 0: | ||
output = func(**arguments) | ||
else: | ||
input_schema = Schema(schema_arguments) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull this out of the inner function, otherwise it will be rebuilding the schema on every call. To handle the main if case (len(schema_arguments) == 0
), make it a no-op lambda returning its arg in that situation.
output = func(**validated_arguments) | ||
|
||
if returns_defined: | ||
output_schema = Schema(returns) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above; pull this out of the inner function.
""" | ||
Returns argument names as values as key-value pairs. | ||
|
||
:input `func` that is a function object |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you want to document the parameters it should be in correct RST format.
|
||
def validate_schema(*a, **kw): | ||
schema = Schema(*a, **kw) | ||
""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This docstring should be a bit more descriptive I think.
Hey @alecthomas, Finished the changes u'd requested. I've put them into separated patches to make it easier for u to review. If ur ok with it I'll merge them into one commit. |
Looks good! Once you've squashed the commits I'll merge it in. Thanks for doing all that. |
Introducing decorator that is able to validate input arguments and return value of the decorated function. Before calling the wrapped function, the validators specified in the decorator's argument list will be tested against the values passed at the function call. ``` @validate_schema(arg1=int, arg2=int) def foo(arg1, arg2): return arg1 * arg2 ``` After calling the function with the validated arguments, schema specified in `RETURNS_KEY` (currently `__return__`) will be applied against the output. ``` @validate_schema(arg1=int, arg2=int, __returns__=int) def foo(arg1, arg2): return arg1 * arg2 ``` See more in the related test cases. Signed-off-by: Gergő Nagy <grigori.grant@gmail.com>
Done. :) |
Introducing decorator that is able to validate input arguments and return value
of the decorated function.
Before calling the wrapped function, the validators specified in the
decorator's argument list will be tested against the values passed at
the function call.
After calling the function with the validated arguments, schema
specified in
RETURNS_KEY
(currently__returns__
) will be appliedagainst the output.
See more in the related test cases.