pyvalid

The pyvalid is the Python validation tool for checking a function’s input parameters and return values.

Purposes of the pyvalid package:

  1. Provide an ability to validate a user input (such as usernames, phone numbers, emails, dates and times, etc) and minimize the amount of code required for the implementation of the comprehensive validation systems;

  2. Add an additional layer of dynamic code analysis for the development and testing stages — pyvalid will raise the exception if a function accepts or returns unexpected values and it’s always possible to disable pyvalid in production if needed.

  3. Help to catch runtime issues.

How to install

  • With PyPI: pip install -U pyvalid

  • Manually: python setup.py install

How to use

The schema below reveals the general structure of the pyvalid package:

../_images/pyvalid-map.png

The package consists of two decorators: accepts and returns, which validates the function’s input and output values accordingly. To know how to validate the data, accepts and returns decorators should receive the information about excepted values/types/validators.

The very basic example below shows how to use accepts and returns decorators.

from pyvalid import accepts, returns


@accepts(int, int)
@returns(float)
def divide(num_1, num_2):
    return num_1 / num_2

divide(8, 42)
# Returns float value

divide('Python', 42)
# Raises the ArgumentValidationError exception, since the 1st argument is
# the str value, when we're expecting int values only.

If just specifying an expected type or value is not enough, then it’s worth to use the custom validator. All the built-in validators are located in the pyvalid.validators module and it’s also possible to create a new one using the is_validator decorator or through extending the AbstractValidator class.

We can flexibly control the state of the pyvalid validation using the pyvalid.switch module. This module provides an ability to switch the pyvalid on/off.

In most cases, it’s worth to use the pyvalid features to validate incoming/outcoming data, such as: user input, the data sent to the API, etc.

But it’s also possible to use the pyvalid package as a part of the CI/CD processes only:

  1. Apply the accepts and returns decorators to all needed functions and methods.

  2. Perform unit testing, integration testing, etc.

  3. The accepts and returns decorators will raise exceptions in case if the input/output data is not valid.

  4. Collect information about raised exceptions and fix the code, which causes them.

  5. Turn off the pyvalid before going live in order to avoid unnecessary exceptions in production.

pyvalid.accepts

pyvalid.accepts(*allowed_arg_values, **allowed_kwargs_values)

The decorator which validates input parameters of the wrapped function.

To use it, we need to specify the list of allowed types or values. If the function’s input doesn’t match the allowed types/values, one of the following errors will be thrown:

  • pyvalid.ArgumentValidationError — when the actual type/value of the function’s argument is different from the expected one;

  • pyvalid.InvalidArgumentNumberError — when the number/position of function’s arguments is incorrect.

Examples of usage:

Let’s define the multiply function, which accepts only int values, and see how does it work with other types.

from pyvalid import accepts


@accepts(int, int)
def multiply(num_1, num_2):
    return num_1 * num_2


multiply(4, 2)
# Returns 8.

multiply(3.14, 8)
# Raises the ArgumentValidationError exception, since the 1st argument is the
# float value, when we're expecting int values only.

multiply(3, 'pyvalid')
# Raises the ArgumentValidationError exception, since the 2nd argument is the
# str value, when we're expecting int values only.

multiply(128)
# Raises the InvalidArgumentNumberError exception, since the second argument
# is missing.

pyvalid.returns

pyvalid.returns(*allowed_return_values)

The decorator which validates the value returned by the wrapped function.

To use it, we need to specify the list of expected return types or values. If the function’s return value doesn’t match the allowed types/values, the pyvalid.InvalidReturnTypeError error will be thrown.

Examples of usage:

Let’s define the multiply function, which returns only int values, and see how does it work with other types.

from pyvalid import returns


@returns(int)
def multiply(num_1, num_2):
    return num_1 * num_2


multiply(4, 2)
# Returns 8.

multiply(3.14, 8)
# Raises the InvalidReturnTypeError exception, since the function returns the
# float value, when we're expecting int values only.

multiply(3, 'pyvalid')
# Raises the InvalidReturnTypeError exception, since the function returns the
# str value, when we're expecting int values only.

pyvalid.switch

This module is designed to control the state of the pyvalid’s validation system. With help of the pyvalid.switch module it’s possible to turn off the pyvalid’s validation so it won’t raise an exception anymore even in case of invalid data. And, of course, it’s always possible to turn on the validation system back, when needed.

Example:

from pyvalid import switch, accepts


@accepts(str)
def say_hello(name):
    print('Hello,', name)


print('Is pyvalid enabled:', switch.is_enabled())
# Outputs: "Is pyvalid enabled: True"

say_hello(3)
# Raises the ArgumentValidationError exception, since the pyvalid's validation
# is enabled and the "name" argument didn't pass the validation

switch.turn_off()
# Now the pyvalid's validation is disabled

print('Is pyvalid enabled:', switch.is_enabled())
# Outputs: "Is pyvalid enabled: False"

say_hello(3)
# Outputs: "Hello, 3"

switch.turn_on()
# And now the pyvalid's validation is enabled back
pyvalid.switch.turn_on()[source]

Enables the pyvalid’s validation system.

pyvalid.switch.turn_off()[source]

Disables the pyvalid’s validation system.

pyvalid.switch.is_enabled()[source]

Returns True if the pyvalid’s validation system is enabled otherwise returns False value.

pyvalid.validators

pyvalid.validators.is_validator

alias of pyvalid.validators.__base.Validator

class pyvalid.validators.Validator(**func_kwargs)[source]

Bases: collections.abc.Callable

__init__(func)[source]

Initialize self. See help(type(self)) for accurate signature.

class pyvalid.validators.AbstractValidator[source]

Bases: pyvalid.validators.__base.Validator

abstract property checkers
__init__()[source]

Initialize self. See help(type(self)) for accurate signature.

class pyvalid.validators.IterableValidator(**func_kwargs)[source]

Bases: pyvalid.validators.__base.AbstractValidator

This class performs validation on the content of the iterable to check if the given iterable is valid or not. The iterable can be either a list, tuple or even keys or values of a dictionary.

Example:

validator = IterableValidator(
    empty_allowed=False, elements_type=int, min_val=0, max_val=50
)
@accepts(validator)
def example([1, 3, 7, 10]):
    pass
iterable_types = (<class 'list'>, <class 'tuple'>, <class 'dict'>, <class 'set'>)
classmethod empty_checker(val, empty_allowed)[source]

Checks if the iterable is empty or not.

Parameters
  • val (collections.abc.Iterable) – Iterable whose contents needs to be validated.

  • empty_allowed (bool) – If this flag is set to False, this method raises exception and terminates the execution if the iterable is empty. If set to True, it raises a warning and continues with the execution.

Returns (bool):
True:

If the iterable is not empty.

False:

If the iterable is empty.

classmethod element_type_checker(val, elements_type)[source]

Checks if all the elements of the iterable are of required type.

Parameters
  • val (collections.abc.Iterable) – Iterable whose contents needs to be validated.

  • elements_type (datatype) – Expected type of all the elements in the iterator.

Returns (bool):
True:

If all elements of the iterable are of required type.

False:

If at least one element of the iterable is not of required type.

classmethod elements_min_val_checker(val, min_val)[source]

Checks if all the elements of the iterable are greater than or equal to the specified value.

Parameters
  • val (collections.abc.Iterable) – Iterable whose contents needs to be validated.

  • min_val (int) – Expected minimum value the iterable must contain.

Returns (bool):
True:

If all the elements of the iterable are greater than or equal to the <min_val>.

False:

If at least one element of the iterable is less than the <min_val>.

classmethod elements_max_val_checker(val, max_val)[source]

Checks if all the elements of the iterable are less than or equal to the specified value.

Parameters
  • val (collections.abc.Iterable) – Iterable whose contents needs to be validated.

  • max_val (int) – Expected maximum value the iterable must contain.

Returns (bool):
True:

If all the elements of the iterable are less than or equal to the <max_val>.

False:

If at least one element of the iterable is greater than the <max_val>.

property checkers
__init__(**kwargs)[source]

Initialize self. See help(type(self)) for accurate signature.

class pyvalid.validators.NumberValidator(**func_kwargs)[source]

Bases: pyvalid.validators.__base.AbstractValidator

number_types = (<class 'int'>, <class 'float'>)
classmethod min_val_checker(val, min_val)[source]
classmethod max_val_checker(val, max_val)[source]
classmethod in_range_checker(val, in_range)[source]
classmethod not_in_range_checker(val, not_in_range)[source]
property checkers
__init__(**kwargs)[source]

Initialize self. See help(type(self)) for accurate signature.

class pyvalid.validators.StringValidator(**func_kwargs)[source]

Bases: pyvalid.validators.__base.AbstractValidator

classmethod min_len_checker(val, min_len)[source]
classmethod max_len_checker(val, max_len)[source]
classmethod in_range_checker(val, in_range)[source]
classmethod not_in_range_checker(val, not_in_range)[source]
classmethod re_checker(val, pattern, flags=0)[source]
property checkers
__init__(**kwargs)[source]

Initialize self. See help(type(self)) for accurate signature.

class pyvalid.validators.TensorValidator(**func_kwargs)[source]

Bases: pyvalid.validators.__base.AbstractValidator

Performs certain checks to check if the given tensor is valid or not.

Example:

X = torch.Tensor([[0.0500, 0.0000, 0.0688, 0.0843, 0.0000, 0.0000, 0.1896, 0.0105],
            [0.0500, 0.0000, 0.0528, 0.1810, 0.0000, 0.0000, 0.1470, 0.0000]])
validator = TensorValidator(
    tensor_type="torch.FloatTensor", empty_check=True, nan_check=True, dim=2
)

@accepts(validator)
def example(X):
    pass
classmethod tensor_type_checker(val, tensor_type)[source]

Checks the tensor types with CPU variants.

Parameters
  • val (torch.Tensor) – Tensor whose type is to be validated.

  • tensor_type (str) – Expected type of tensor. Ex: “torch.IntTensor”, “torch.FloatTensor”, “torch.ByteTensor”.

Returns (bool):
True:

IIf the type of given tensor matches the required type.

False:

If the type of given tensor does not match the required type.

classmethod dimension_checker(val, dim)[source]

Checks if given tensor is of dimension <dim>.

Parameters
  • val (torch.Tensor) – Tensor whose dimension is to be validated.

  • dim (int) – Expected dimension of the tensor.

Returns (bool):
True:

If the given tensor is of required dimension.

False:

If the given tensor is not of required dimension.

classmethod empty_checker(val, empty_allowed)[source]

Checks if the tensor is empty or not.

Parameters
  • val (torch.Tensor) – Tensor whose contents needs to be validated.

  • empty_allowed (bool) – If this flag is set to False, this method raises exception and terminates the execution if the tensor is empty. If set to True, it raises a warning and continues with the execution.

Returns (bool):
True:

If the tensor is not empty.

False:

If the tensor is empty.

classmethod nan_checker(val, nans_allowed)[source]

Checks if the tensor has np.NaN values or not. :param val: Tensor to be validated. :type val: torch.Tensor :param nans_allowed: If this flag is set to False, this method raises exception and

terminates the execution if the tensor has NaNs. If set to True, it raises a warning and continues with the execution.

Returns (bool):
True:

If the given tensor is free of NaNs.

False:

If the given tensor contains NaNs.

property checkers
__init__(**kwargs)[source]

Initialize self. See help(type(self)) for accurate signature.

pyvalid.ArgumentValidationError

Raised when the function’s parameter contains the value is different from the expected one.

pyvalid.ArgumentValidationError.args
pyvalid.ArgumentValidationError.with_traceback()

Exception.with_traceback(tb) – set self.__traceback__ to tb and return self.

pyvalid.InvalidArgumentNumberError

Raised when the number or position of arguments supplied to a function is incorrect.

pyvalid.InvalidArgumentNumberError.args
pyvalid.InvalidArgumentNumberError.with_traceback()

Exception.with_traceback(tb) – set self.__traceback__ to tb and return self.

pyvalid.InvalidReturnTypeError

Raised when a function returns the value different from the expected one.

pyvalid.InvalidReturnTypeError.args
pyvalid.InvalidReturnTypeError.with_traceback()

Exception.with_traceback(tb) – set self.__traceback__ to tb and return self.

Examples

Function calculate in the example below has the following limitations:

  • Function should return int or float values only;

  • First parameter must be str value;

  • Second parameter must be int value or be equal to the 2.0;

  • Third parameter must be int or float value.

from pyvalid import accepts, returns


@returns(int, float)
@accepts(str, (int, 2.0), (int, float))
def calculate(operator, val1, val2, val3):
    expression = '{v1} {op} {v2} {op} {v3}'.format(
        op=operator, v1=val1, v2=val2, v3=val3
    )
    return eval(expression)


calculate('*', 2, 3, 4)
# Returns 24.

calculate(operator='*', val1=2, val2=3.0, val3=4)
# Returns 24.0.

calculate('*', 2.0, 3, 4)
# Still returns 24.0.

calculate('*', 3.14, 3, 4)
# Raises the ArgumentValidationError exception, because the second
# argument is not valid.

calculate('*', 2, 3, '"4"')
# Raises the InvalidReturnTypeError exception, because of invalid return
# value: function returns the str value, when only int and float values
# are allowed.

The example below demonstrates how to use the accepts and returns decorators in the classes. Please pay attention to the method connect of the class SqlDriver. In these classes we’re using the accepts decorator to validate keyword arguments.

from pyvalid import accepts, returns
from collections.abc import Iterable


class SqlDriver(object):

    @returns(bool)
    @accepts(object, host=str, port=int, usr=str, pwd=str, db=[str, None])
    def connect(self, **kwargs):
        conn_req = 'tsql -S {host} -p {port} -U {usr} -P {pwd} -D {db}'
        conn_req = conn_req.format(**kwargs)
        try:
            print('Establishing connection: "{}"'.format(conn_req))
            # Some code, which may cause the ConnectionError
            return True
        except ConnectionError:
            return False

    @returns(bool)
    def close(self):
        try:
            print('Closing connection')
            # Some code, which may cause the ConnectionError
            return True
        except ConnectionError:
            return False

    @returns(None, dict)
    @accepts(object, str, Iterable)
    def query(self, sql, params=None):
        try:
            if params is not None:
                sql = sql.format(*params)
            query_info = 'Processing request "{}"'.format(sql)
            print(query_info)
            return dict()
            # Some code, which may cause the ConnectionError
        except ConnectionError:
            return None


sql_driver = SqlDriver()

conn_params = {
    'host': '8.8.8.8',
    'port': 1433,
    'usr': 'admin',
    'pwd': 'password',
    'db': 'wiki'
}
sql_driver.connect(**conn_params)

sql = 'SELECT * FROM ProgrammingLang'
pl = sql_driver.query(sql)

sql = 'SELECT * FROM ProgrammingLang WHERE name={}'
python_pl = sql_driver.query(sql, ('Python',))

sql_driver.close()

When we need a bit more complex validators, we may use built-in pyvalid` validators available in the ``pyvalid.validators module. For example, here we’re using the StringValidator validator based on the regular expression and the NumberValidator based on the min/max allowed values:

from pyvalid import accepts, returns
from pyvalid.validators import NumberValidator, StringValidator

@accepts(StringValidator(re_pattern=r'^[A-Za-z]+\s?[A-Za-z]+\s?[A-Za-z]+$'))
@returns(NumberValidator(min_val=0, max_val=10))
def get_review(name):
    message = 'Hello, {}! Please review our application from 0 to 10.'
    print(message.format(name))
    return float(input())

review = get_review('Elon Musk')
print(review)
# Will raise the InvalidReturnTypeError exception only if user enter
# the value, which is not in the [0, 10] range.

another_review = get_review('Elon Musk 2')
# Raises the ArgumentValidationError exception, since the "Elon Musk 2"
# value doesn't match the pattern.

The example below explains how to use the custom validator. It’s pretty easy actually, we just need to apply the pyvalid.validators.is_validator decorator to the validation function.

from pyvalid import accepts
from pyvalid.validators import is_validator


class User(object):

    registered_users = list()

    class Validator(object):

        unsafe_passwords = [
            '111111', '000000', '123123',
            '123456', '12345678', '1234567890',
            'qwerty', 'sunshine', 'password',
        ]

        @classmethod
        @is_validator
        def login_checker(cls, login):
            if isinstance(login, str) and 1 <= len(login) <= 16:
                for reg_user in User.registered_users:
                    if login == reg_user.login:
                        return False
            return True

        @classmethod
        @is_validator
        def password_checker(cls, password):
            return (
                isinstance(password, str)
                and
                6 <= len(password) <= 32
                and
                password not in cls.unsafe_passwords
            )

    def __init__(self, login, password):
        self.__login = None
        self.login = login
        self.__password = None
        self.password = password
        User.registered_users.append(self)

    @property
    def login(self):
        return self.__login

    @login.setter
    @accepts(object, Validator.login_checker)
    def login(self, value):
        self.__login = value

    @property
    def password(self):
        return self.__password

    @password.setter
    @accepts(object, Validator.password_checker)
    def password(self, value):
        self.__password = value


user = User('admin', 'Str0ng_P@ssw0rd!')

print(user.login, user.password)
# Outputs: "admin Str0ng_P@ssw0rd!"

user.password = 'qwerty'
# Raises the ArgumentValidationError exception, because the
# User.Validator.password_checker method returns False.

user = User('admin', 'An0ther_Str0ng_P@ssw0rd!')
# Raises the ArgumentValidationError exception, because the
# User.Validator.login_checker method returns False.

License

This project is distributed under the MIT License.