This commit is contained in:
grecoe 2021-03-15 14:33:03 -04:00
Родитель abd6cbab56
Коммит dfd630244d
14 изменённых файлов: 567 добавлений и 7 удалений

Просмотреть файл

@ -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

6
pyproject.toml Normal file
Просмотреть файл

@ -0,0 +1,6 @@
[build-system]
requires = [
"setuptools>=42",
"wheel"
]
build-backend = "setuptools.build_meta"

25
setup.cfg Normal file
Просмотреть файл

@ -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

106
src/Readme.md Normal file
Просмотреть файл

@ -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])

71
test_class.py Normal file
Просмотреть файл

@ -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))

70
test_standalone.py Normal file
Просмотреть файл

@ -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))