Initial Drop
This commit is contained in:
Родитель
abd6cbab56
Коммит
dfd630244d
11
README.md
11
README.md
|
@ -1,14 +1,11 @@
|
|||
# Project
|
||||
|
||||
> This repo has been populated by an initial template to help get you started. Please
|
||||
> make sure to update the content to build a great experience for community-building.
|
||||
Parameter validation utility for Python methods whether they are standalone or in a class.
|
||||
|
||||
As the maintainer of this project, please make a few updates:
|
||||
See src/Readme.md for more details.
|
||||
|
||||
- Improving this README.MD file to provide a great experience
|
||||
- Updating SUPPORT.MD with content about this project's support experience
|
||||
- Understanding the security reporting process in SECURITY.MD
|
||||
- Remove this section from the README
|
||||
See test_class.py for using the utility with a class.
|
||||
See test_standalone for using with standalone methods.
|
||||
|
||||
## Contributing
|
||||
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
[build-system]
|
||||
requires = [
|
||||
"setuptools>=42",
|
||||
"wheel"
|
||||
]
|
||||
build-backend = "setuptools.build_meta"
|
|
@ -0,0 +1,25 @@
|
|||
[metadata]
|
||||
# replace with your username:
|
||||
name = ai-param-validation
|
||||
version = 0.0.1
|
||||
author = Dan Grecoe
|
||||
author_email = grecoe@microsoft.com
|
||||
description = A method parameter validation decorator
|
||||
long_description = file: README.md
|
||||
long_description_content_type = text/markdown
|
||||
url = https://github.com/pypa/sampleproject
|
||||
project_urls =
|
||||
Bug Tracker = https://github.com/microsoft/ai-py-param-validation/issues
|
||||
classifiers =
|
||||
Programming Language :: Python :: 3
|
||||
License :: OSI Approved :: MIT License
|
||||
Operating System :: OS Independent
|
||||
|
||||
[options]
|
||||
package_dir =
|
||||
= src
|
||||
packages = find:
|
||||
python_requires = >=3.6
|
||||
|
||||
[options.packages.find]
|
||||
where = src
|
|
@ -0,0 +1,106 @@
|
|||
# Parameter Validation
|
||||
When developing software, one of the lowest hanging fruit items you can choose to manage is validation of parameters being passed to your methods.
|
||||
|
||||
This is true whether the method is free standing (outside a class) or contained within a class.
|
||||
|
||||
This repo has a decorator class that can be used in both cases.
|
||||
|
||||
## Usage
|
||||
To use this functionality, you need to use the validation_decorator.ParameterValidator to decorate your methods.
|
||||
|
||||
Regardless of method type (standalone or class) you can use one of two options
|
||||
|
||||
### 1 - Function has well defined arguments
|
||||
If you have well defined arguments such as below, this is straight forward
|
||||
|
||||
```python
|
||||
"""
|
||||
This function takes two parameters, both of which are expected to be integers.
|
||||
|
||||
The input to ParameterValidation is a list of tuples where
|
||||
[0] = Expected parameter type
|
||||
[1] = True if it can be None, False otherwise
|
||||
|
||||
There MUST be the same number of definitions passed to ParameterValidator as the number of parameters passed to the method itself.
|
||||
"""
|
||||
@ParameterValidator((int, False), (int, False))
|
||||
def add(num: int, num: int):
|
||||
print("Hello from standalone function")
|
||||
```
|
||||
|
||||
### 2 - Function takes a variable number of parameters in kwargs (dict)
|
||||
```python
|
||||
"""
|
||||
This function takes only the kwargs parameter.
|
||||
|
||||
However, internally the function will expect two parameters pass (contrived, yes) for left and right.
|
||||
|
||||
The input to ParameterValidation is a kwargs input where the name of the parameter is the name expected to be found in kwargs, the value is a tuple identical to that used above.
|
||||
|
||||
In this instance, only the incoming values in kwargs are validated against the input to the ParameterValidator class. There can be more parameters, but those will not be tested.
|
||||
|
||||
In reality, the check should be on inputs that you EXPECT to be there.
|
||||
If you allow None (type,True) and the parameter is not in kwargs, no error will be raised.
|
||||
"""
|
||||
@ParameterValidator(left=(int, False), right=(int, False))
|
||||
def add(**kwargs):
|
||||
print("Hello from standalone function")
|
||||
```
|
||||
|
||||
## Example 1 - Standalone Function with defined arguments
|
||||
|
||||
Standalone method with arguments
|
||||
```python
|
||||
@ParameterValidator((int, False), (str, False), (list, True))
|
||||
def myfunc(num, str, list):
|
||||
print("Hello from standalone function")
|
||||
```
|
||||
Test it using splatting and with set parameters
|
||||
```python
|
||||
# Splatting
|
||||
single_args = [1, "hey", None]
|
||||
print("Standalone Args Splatting -")
|
||||
myfunc(*single_args)
|
||||
|
||||
# Standard call
|
||||
print("Standalone Args Standard -")
|
||||
myfunc(1, "hey", None)
|
||||
```
|
||||
|
||||
# Example 2 - Standalone function using kwargs
|
||||
Function
|
||||
```python
|
||||
# Test standalone method with kwargs
|
||||
@ParameterValidator(age=(int, False), name=(str, False), addresses=(list, True))
|
||||
def mykwfunc(**kwargs):
|
||||
print("Hello from kwargs standalone function")
|
||||
```
|
||||
And test this
|
||||
```python
|
||||
print("Standalone Kwargs Standard -")
|
||||
mykwfunc(age=25, name="Fred Jones")
|
||||
```
|
||||
|
||||
# Example 3 - Class with both types of functions
|
||||
Class definition:
|
||||
```python
|
||||
class TestDecorator:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@ParameterValidator((int, False), (str, False), (list, True))
|
||||
def myfunc(self, num: int, str: str, list: typing.List[object]):
|
||||
print("Hello from class method")
|
||||
|
||||
@ParameterValidator(age=(int, False), name=(str, False), addresses=(list, True))
|
||||
def mykwfunc(self, **kwargs):
|
||||
print("Hello from kwargs class function")
|
||||
```
|
||||
And finally, test the class
|
||||
```python
|
||||
td = TestDecorator()
|
||||
print("Class Args Standard -")
|
||||
td.myfunc(1, "str", [])
|
||||
print("Class Kwargs Standard -")
|
||||
td.mykwfunc(age=25, name="Fred Jones")
|
||||
```
|
|
@ -0,0 +1,4 @@
|
|||
"""
|
||||
(c) Microsoft. All rights reserved.
|
||||
"""
|
||||
from paramvalidator.validator import ParameterValidator, ParameterValidationException
|
|
@ -0,0 +1,8 @@
|
|||
"""
|
||||
(c) Microsoft. All rights reserved.
|
||||
"""
|
||||
from paramvalidator.exceptions.validation_exception import ParameterValidationException
|
||||
from paramvalidator.exceptions.none_validation_exception import ParameterNoneValidationException
|
||||
from paramvalidator.exceptions.type_validation_exception import ParameterTypeValidationException
|
||||
from paramvalidator.exceptions.kwarg_validation_exception import ParameterKwargValidationException
|
||||
from paramvalidator.exceptions.count_validation_exception import ParameterCountValidationException
|
|
@ -0,0 +1,19 @@
|
|||
"""
|
||||
(c) Microsoft. All rights reserved.
|
||||
"""
|
||||
import typing
|
||||
from .validation_exception import ParameterValidationException
|
||||
|
||||
|
||||
class ParameterCountValidationException(ParameterValidationException):
|
||||
"""
|
||||
Exception when an expected kwarg is not present
|
||||
"""
|
||||
def __init__(self, func: typing.Callable[..., None], recieved: int):
|
||||
err = "Expected {} args in func {} in module {} but {} were given.".format(
|
||||
func.__code__.co_argcount,
|
||||
func.__qualname__,
|
||||
func.__module__,
|
||||
recieved
|
||||
)
|
||||
super().__init__(err)
|
|
@ -0,0 +1,18 @@
|
|||
"""
|
||||
(c) Microsoft. All rights reserved.
|
||||
"""
|
||||
import typing
|
||||
from .validation_exception import ParameterValidationException
|
||||
|
||||
|
||||
class ParameterKwargValidationException(ParameterValidationException):
|
||||
"""
|
||||
Exception when an expected kwarg is not present
|
||||
"""
|
||||
def __init__(self, func: typing.Callable[..., None], expected: str):
|
||||
err = "Missing required kwarg - {} - in func {} in module {}.".format(
|
||||
expected,
|
||||
func.__qualname__,
|
||||
func.__module__
|
||||
)
|
||||
super().__init__(err)
|
|
@ -0,0 +1,18 @@
|
|||
"""
|
||||
(c) Microsoft. All rights reserved.
|
||||
"""
|
||||
import typing
|
||||
from .validation_exception import ParameterValidationException
|
||||
|
||||
|
||||
class ParameterNoneValidationException(ParameterValidationException):
|
||||
"""
|
||||
Exception when incoming value is None but None is not allowed.
|
||||
"""
|
||||
def __init__(self, func: typing.Callable[..., None], param: int):
|
||||
err = "Unexpected None value found in func {} in module {}, parameter {}.".format(
|
||||
func.__qualname__,
|
||||
func.__module__,
|
||||
param
|
||||
)
|
||||
super().__init__(err)
|
|
@ -0,0 +1,20 @@
|
|||
"""
|
||||
(c) Microsoft. All rights reserved.
|
||||
"""
|
||||
import typing
|
||||
from .validation_exception import ParameterValidationException
|
||||
|
||||
|
||||
class ParameterTypeValidationException(ParameterValidationException):
|
||||
"""
|
||||
Exception when the incoming type does not match the expected type
|
||||
"""
|
||||
def __init__(self, func: typing.Callable[..., None], param: int, input_type: object, expected_type: object):
|
||||
err = "Arg type mismatch in func {} in module {}, parameter {} type does not match expected {} != {}.".format(
|
||||
func.__qualname__,
|
||||
func.__module__,
|
||||
param,
|
||||
str(input_type),
|
||||
str(expected_type)
|
||||
)
|
||||
super().__init__(err)
|
|
@ -0,0 +1,14 @@
|
|||
"""
|
||||
(c) Microsoft. All rights reserved.
|
||||
"""
|
||||
|
||||
|
||||
class ParameterValidationException(Exception):
|
||||
"""
|
||||
Base exception for parameter validation
|
||||
"""
|
||||
def __init__(self, message):
|
||||
self.message = message
|
||||
|
||||
def __str__(self):
|
||||
return self.message
|
|
@ -0,0 +1,184 @@
|
|||
"""
|
||||
(c) Microsoft. All rights reserved.
|
||||
|
||||
Validating function/method inputs can prevent many problems down the road
|
||||
once software has been released. This quick and somewhat painless process
|
||||
can be achieved either through several lines of validation code in each
|
||||
method or can be managed in Python by decorating a function with the
|
||||
ParameterValidator below.
|
||||
|
||||
This class can be used with:
|
||||
- Stand alone methods
|
||||
- Class methods
|
||||
- Regardless of method type, it can validate:
|
||||
- Methods with a set number of arguments
|
||||
- Methods that rely on kwargs for arguments.
|
||||
"""
|
||||
import typing
|
||||
from paramvalidator.exceptions import (
|
||||
ParameterNoneValidationException,
|
||||
ParameterTypeValidationException,
|
||||
ParameterKwargValidationException,
|
||||
ParameterCountValidationException,
|
||||
ParameterValidationException
|
||||
)
|
||||
|
||||
|
||||
class ParameterValidator:
|
||||
"""
|
||||
Function decorator to validate arguments to a function. This can be used
|
||||
to ensure that parameters are (1) present, (2) meet a type requirement and
|
||||
(3) actually have a value and are not None (if desired)
|
||||
|
||||
For functions that expect a set number of arguments, you seed the class with
|
||||
a free formed list of tuples that are in the form
|
||||
(
|
||||
Argument expected type,
|
||||
Boolean - true = can be None, false = must be present
|
||||
)
|
||||
|
||||
i.e. ( (int, True), (str, False), (list, True))
|
||||
|
||||
For functions that expect to use the kwargs for variable arguments you can also
|
||||
validate that certain required fields are always in the input with a slight change
|
||||
to the above format, you set up kwargs using the name of the field expected with
|
||||
the same format above.
|
||||
|
||||
i.e. ( age=(int, True), name=(str, False), addresses=(list, True))
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Collect the parameters passed to this instance for future
|
||||
validation against the calling function.
|
||||
"""
|
||||
self.validation_args = args
|
||||
self.validation_kwargs = kwargs
|
||||
|
||||
def __call__(self, func):
|
||||
"""
|
||||
Returns a wrapper to the function that when called it will validate
|
||||
the parameters for the given call.
|
||||
|
||||
This works with both standalone and class methods.
|
||||
|
||||
Parameters:
|
||||
func: The calling function
|
||||
|
||||
Returns:
|
||||
wrapper function that will be called when the subscribed function
|
||||
is called.
|
||||
"""
|
||||
def wrapper(*args, **kwargs):
|
||||
"""
|
||||
Wraps an existing function and validates the parameters passed in meet
|
||||
the rules set forth when declaring the instance of ParameterValidator.
|
||||
|
||||
Parameters:
|
||||
args: The list of arguments passed to the method
|
||||
kwargs: The dictionary of arguments passed into the method
|
||||
|
||||
Returns:
|
||||
Whatever the original function returns.
|
||||
|
||||
Throws:
|
||||
ParameterTypeValidationException if there is an issue
|
||||
"""
|
||||
args_to_validate = args
|
||||
if len(args) and func.__qualname__.startswith(args[0].__class__.__name__):
|
||||
# This is a class method and we do NOT want to validate self.
|
||||
args_to_validate = args[1:]
|
||||
|
||||
if len(args_to_validate) and (len(args) != func.__code__.co_argcount):
|
||||
raise ParameterCountValidationException(func, len(args))
|
||||
|
||||
if len(args_to_validate):
|
||||
self._validate_args_arguments(func, args_to_validate, self.validation_args)
|
||||
elif len(kwargs) and len(self.validation_kwargs):
|
||||
self._validate_kwargs_arguments(func, kwargs, self.validation_kwargs)
|
||||
else:
|
||||
raise ParameterCountValidationException(func, 0)
|
||||
|
||||
return func(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
def _validate_args_arguments(self, func: typing.Callable[..., None], call_arguments: typing.List[object], validation_args: typing.List[tuple]):
|
||||
"""
|
||||
Validation for functions that take in an *args set of arguments. These must be a
|
||||
known set of arguments as defined in the method declaration.
|
||||
|
||||
Parameters:
|
||||
func : The calling function
|
||||
call_arguments: The list of provided arguments to the method.
|
||||
validation_args: The list of tuples for validation, but cannot be the kwargs validation.
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
Throws:
|
||||
ParameterTypeValidationException if there is an issue
|
||||
"""
|
||||
param_index = 1
|
||||
for (argument, validation) in zip(call_arguments, validation_args):
|
||||
self._validate_argument(param_index, func, argument, validation)
|
||||
param_index += 1
|
||||
|
||||
def _validate_kwargs_arguments(self, func: typing.Callable[..., None], call_arguments: typing.Dict[str, object], validation_args: typing.Dict[str, tuple]):
|
||||
"""
|
||||
Validates the kwargs against the validation kwargs passed in.
|
||||
|
||||
Parameters:
|
||||
func: The calling function
|
||||
call_arguments: The kwargs passed to the function
|
||||
validation_args: The kwargs passed to this instance
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
Throws:
|
||||
ParameterTypeValidationException if there is an issue
|
||||
"""
|
||||
param_index = 1
|
||||
for expected_arg in validation_args.keys():
|
||||
# Overload the boolean in the validation, if it's True and not present
|
||||
# this is not an error.
|
||||
if (not validation_args[expected_arg][1]) and (expected_arg not in call_arguments):
|
||||
raise ParameterKwargValidationException(func, expected_arg)
|
||||
elif validation_args[expected_arg][1] and (expected_arg not in call_arguments):
|
||||
# Allowed None (or not present) and not there. no error.
|
||||
pass
|
||||
else:
|
||||
self._validate_argument(
|
||||
param_index,
|
||||
func,
|
||||
call_arguments[expected_arg],
|
||||
validation_args[expected_arg])
|
||||
param_index += 1
|
||||
|
||||
def _validate_argument(self, param_index: int, func: typing.Callable[..., None], argument: object, validation: tuple):
|
||||
"""
|
||||
Performs the actual validation on an incoming argument value and the corresponding
|
||||
validation tuple (holding type information and bool which identifies whether None
|
||||
values are accepted)
|
||||
|
||||
Parameters:
|
||||
- param_index : Index of this parameter. Useful for args type calls, less so with
|
||||
kwargs type calls.
|
||||
- func : Calling function
|
||||
- argument : Actual value passed in
|
||||
- validation : Tuple with type and None type acceptance
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
Throws:
|
||||
ParameterTypeValidationException if there is an issue
|
||||
"""
|
||||
if not validation[1] and (argument is None):
|
||||
# Not allowed None but is
|
||||
raise ParameterNoneValidationException(func, param_index)
|
||||
elif validation[1] and argument is None:
|
||||
# Allow None and it's None
|
||||
pass
|
||||
elif not isinstance(argument, validation[0]):
|
||||
# Not none and have value, types to not match.
|
||||
raise ParameterTypeValidationException(func, param_index, type(argument), validation[0])
|
|
@ -0,0 +1,71 @@
|
|||
"""
|
||||
(c) Microsoft. All rights reserved.
|
||||
"""
|
||||
import sys
|
||||
import typing
|
||||
sys.path.append("./src")
|
||||
from src.paramvalidator import ParameterValidator, ParameterValidationException
|
||||
|
||||
"""
|
||||
This file tests out using ParameterValidator on class methods. Note that you CAN
|
||||
use it on the __init__ function as well assuming you have parameters other than self.
|
||||
"""
|
||||
|
||||
|
||||
class TestClass:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
"""
|
||||
In this case our method pre-defines all of it's parameters, i.e.
|
||||
|
||||
int, string, list
|
||||
|
||||
The int and string are required, but the list is not.
|
||||
|
||||
ParameterValidator inputs line up directly against the defined parameters
|
||||
in the method signature.
|
||||
"""
|
||||
@ParameterValidator((int, False), (str, False), (list, True))
|
||||
def myfunc(self, num: int, str: str, list: typing.List[object]):
|
||||
print("Hello from class method")
|
||||
|
||||
"""
|
||||
In this case our method pre-defines all of it's parameters, i.e.
|
||||
|
||||
int, string, list
|
||||
|
||||
The int and string are required, but the list is not (as with the above function).
|
||||
|
||||
ParameterValidator inputs are defined as any kwargs would be for any other entry point.
|
||||
The difference being, there is no one to one mapping here and those identified should
|
||||
generally be thought of as required parameters for your method to work.
|
||||
|
||||
However, if the tuple bool value is True, if the parameter is not in kwargs it will
|
||||
not produce a failure.
|
||||
"""
|
||||
@ParameterValidator(age=(int, False), name=(str, False), addresses=(list, True))
|
||||
def mykwfunc(self, **kwargs):
|
||||
print("Hello from kwargs class function")
|
||||
|
||||
|
||||
td = TestClass()
|
||||
print("Class Args Standard - success")
|
||||
td.myfunc(1, "str", [])
|
||||
|
||||
try:
|
||||
print("Class Args Standard - inappropriate None")
|
||||
td.myfunc(None, "hey", None)
|
||||
except ParameterValidationException as ex:
|
||||
print("\tException caught", ex.__class__.__name__)
|
||||
print("\t",str(ex))
|
||||
|
||||
print("Class Kwargs Standard - success")
|
||||
td.mykwfunc(age=25, name="Fred Jones", uncheckedvalue="this will not be checked")
|
||||
|
||||
try:
|
||||
print("Class Args Standard - invalid data type")
|
||||
td.mykwfunc(age=25, name="hey", addresses="main st")
|
||||
except ParameterValidationException as ex:
|
||||
print("\tException caught", ex.__class__.__name__)
|
||||
print("\t",str(ex))
|
|
@ -0,0 +1,70 @@
|
|||
"""
|
||||
(c) Microsoft. All rights reserved.
|
||||
"""
|
||||
import sys
|
||||
sys.path.append("./src")
|
||||
from src.paramvalidator import ParameterValidator, ParameterValidationException
|
||||
|
||||
"""
|
||||
This file tests ParameterValidator on standalone Python methods.
|
||||
"""
|
||||
|
||||
|
||||
"""
|
||||
In this case our method pre-defines all of it's parameters, i.e.
|
||||
|
||||
int, string, list
|
||||
|
||||
The int and string are required, but the list is not.
|
||||
|
||||
ParameterValidator inputs line up directly against the defined parameters
|
||||
in the method signature.
|
||||
"""
|
||||
@ParameterValidator((int, False), (str, False), (list, True))
|
||||
def myfunc(num, str, list):
|
||||
print("Hello from standalone function")
|
||||
|
||||
|
||||
# Splatting
|
||||
single_args = [1, "hey", None]
|
||||
print("Standalone Args Splatting - success")
|
||||
myfunc(*single_args)
|
||||
|
||||
|
||||
# Standard call but make first parameter None where it's not allowed
|
||||
try:
|
||||
print("Standalone Args Standard - failure on invalid type for parameter")
|
||||
myfunc("1", "hey", None)
|
||||
except ParameterValidationException as ex:
|
||||
print("\tException caught", ex.__class__.__name__)
|
||||
print("\t",str(ex))
|
||||
|
||||
|
||||
"""
|
||||
In this case our method pre-defines all of it's parameters, i.e.
|
||||
|
||||
int, string, list
|
||||
|
||||
The int and string are required, but the list is not (as with the above function).
|
||||
|
||||
ParameterValidator inputs are defined as any kwargs would be for any other entry point.
|
||||
The difference being, there is no one to one mapping here and those identified should
|
||||
generally be thought of as required parameters for your method to work.
|
||||
|
||||
However, if the tuple bool value is True, if the parameter is not in kwargs it will
|
||||
not produce a failure.
|
||||
"""
|
||||
@ParameterValidator(age=(int, False), name=(str, False), addresses=(list, True))
|
||||
def mykwfunc(**kwargs):
|
||||
print("Hello from kwargs standalone function")
|
||||
|
||||
|
||||
print("Standalone Kwargs Standard - success")
|
||||
mykwfunc(age=25, name="Fred Jones")
|
||||
|
||||
try:
|
||||
print("Standalone Kwargs Standard - failure on missing required param")
|
||||
mykwfunc(age=25)
|
||||
except ParameterValidationException as ex:
|
||||
print("\tException caught", ex.__class__.__name__)
|
||||
print("\t",str(ex))
|
Загрузка…
Ссылка в новой задаче