使用 RaiseExceptionMeta 元类隐式装饰 Validator 类中的所有校验函数

目录

  • 一、前置说明
    • 1、总体目录
    • 2、相关回顾
    • 3、本节目标
  • 二、操作步骤
    • 1、项目目录
    • 2、代码实现
    • 3、测试代码
    • 4、日志输出
  • 三、后置说明
    • 1、要点小结
    • 2、下节准备

一、前置说明

1、总体目录

  • 《 pyparamvalidate 参数校验器,从编码到发布全过程》

2、相关回顾

  • 将 validator 校验器从 ParameterValidator 中抽离出来

3、本节目标

  • 使用 RaiseExceptionMeta 元类隐式装饰 Validator 类中的所有校验函数

二、操作步骤

1、项目目录

使用 RaiseExceptionMeta 元类隐式装饰 Validator 类中的所有校验函数_第1张图片

  • atme : @me 用于存放临时的代码片断或其它内容。
  • pyparamvalidate : 新建一个与项目名称同名的package,为了方便发布至 pypi
  • core : 用于存放核心代码。
  • tests : 用于存放测试代码。
  • utils : 用于存放一些工具类或方法。

2、代码实现

pyparamvalidate/core/validator.py

import functools
import inspect
import os
from pyparamvalidate.utils.dict_utils import DictUtility


def raise_exception(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        bound_args = inspect.signature(func).bind(*args, **kwargs).arguments

        validate_field = kwargs.get('validate_field', None) or bound_args.get('validate_field', None)
        exception_msg = kwargs.get('exception_msg', None) or bound_args.get('exception_msg', None)

        result = func(*args, **kwargs)
        if not result and exception_msg is not None:
            exception_msg = f"'{validate_field}' value error: {exception_msg}" if validate_field else f"{exception_msg}"
            raise ValueError(exception_msg)
        return result

    return wrapper


class RaiseExceptionMeta(type):
    def __new__(cls, name, bases, dct):

        # 遍历类的字典,包括属性和方法
        for key, value in dct.items():

            # 如果是静态方法,则将它替换为一个新的静态方法,新的静态方法调用 raise_exception 函数,将原静态方法作为参数传递给raise_exception
            if isinstance(value, staticmethod):
                dct[key] = staticmethod(raise_exception(value.__func__))

            # 如果是类方法,则将它替换为一个新的类方法,新的类方法调用 raise_exception 函数,将原类方法作为参数传递给raise_exception
            if isinstance(value, classmethod):
                dct[key] = classmethod(raise_exception(value.__func__))

            # 如果是普通的成员方法,则将它替换为一个新的函数,新函数调用 raise_exception 函数,将原函数作为参数传递给raise_exception
            if inspect.isfunction(value):
                dct[key] = raise_exception(value)

        #  调用父类 type 的 __new__ 方法,以创建类的新实例,传递给父类的参数包括类的名称 name、基类 bases 和更新后的类字典 dct。
        return type.__new__(cls, name, bases, dct)


class Validator(metaclass=RaiseExceptionMeta):

    def is_string(self, value, validate_field=None, exception_msg=None):
        return isinstance(value, str)

    def is_int(self, value, validate_field=None, exception_msg=None):
        return isinstance(value, int)

    def is_positive(self, value, validate_field=None, exception_msg=None):
        return value > 0

    def is_float(self, value, validate_field=None, exception_msg=None):
        return isinstance(value, float)

    def is_list(self, value, validate_field=None, exception_msg=None):
        return isinstance(value, list)

    def is_dict(self, value, validate_field=None, exception_msg=None):
        return isinstance(value, dict)

    def is_set(self, value, validate_field=None, exception_msg=None):
        return isinstance(value, set)

    def is_tuple(self, value, validate_field=None, exception_msg=None):
        return isinstance(value, tuple)

    def is_not_none(self, value, validate_field=None, exception_msg=None):
        return value is not None

    def is_not_empty(self, value, validate_field=None, exception_msg=None):
        return bool(value)

    def is_allowed_value(self, value, allowed_values, validate_field=None, exception_msg=None):
        return value in allowed_values

    def max_length(self, value, max_length, validate_field=None, exception_msg=None):
        return len(value) <= max_length

    def min_length(self, value, min_length, validate_field=None, exception_msg=None):
        return len(value) >= min_length

    def is_substring(self, sub_string, super_string, validate_field=None, exception_msg=None):
        return sub_string in super_string

    def is_subset(self, subset, superset, validate_field=None, exception_msg=None):
        return subset.issubset(superset)

    def is_sublist(self, sublist, superlist, validate_field=None, exception_msg=None):
        return set(sublist).issubset(set(superlist))

    def contains_substring(self, superstring, substring, validate_field=None, exception_msg=None):
        return substring in superstring

    def contains_subset(self, superset, subset, validate_field=None, exception_msg=None):
        return subset.issubset(superset)

    def contains_sublist(self, superlist, sublist, validate_field=None, exception_msg=None):
        return set(sublist).issubset(set(superlist))

    def is_file_suffix(self, path, file_suffix, validate_field=None, exception_msg=None):
        return path.endswith(file_suffix)

    def is_file(self, path, validate_field=None, exception_msg=None):
        return os.path.isfile(path)

    def is_dir(self, path, validate_field=None, exception_msg=None):
        return os.path.isdir(path)

    def is_similar_dict(self, target_dict, reference_dict, validate_field=None, exception_msg=None,
                        ignore_keys_whitespace=True):
        dict_util = DictUtility()
        return dict_util.is_similar_dict(target_dict, reference_dict, ignore_keys_whitespace)

    def is_method(self, value, validate_field=None, exception_msg=None):
        return callable(value)

3、测试代码

pyparamvalidate/tests/test_validator.py

import os

import pytest

from pyparamvalidate.core.validator import Validator

validator = Validator()

def test_is_string():
    assert validator.is_string(value="test", validate_field='value', exception_msg='value must be string')

    with pytest.raises(ValueError) as exc_info:
        validator.is_string(value=123, validate_field='value', exception_msg='value must be string')
    assert "value must be string" in str(exc_info.value)


def test_is_int():
    assert validator.is_int(value=42, validate_field='value', exception_msg='value must be integer')

    with pytest.raises(ValueError) as exc_info:
        validator.is_int(value="test", validate_field='value', exception_msg='value must be integer')
    assert "value must be integer" in str(exc_info.value)


def test_is_positive():
    assert validator.is_positive(value=42, validate_field='value', exception_msg='value must be positive')

    with pytest.raises(ValueError) as exc_info:
        validator.is_positive(value=-1, validate_field='value', exception_msg='value must be positive')
    assert "value must be positive" in str(exc_info.value)


def test_is_float():
    assert validator.is_float(value=3.14, validate_field='value', exception_msg='value must be float')

    with pytest.raises(ValueError) as exc_info:
        validator.is_float(value="test", validate_field='value', exception_msg='value must be float')
    assert "value must be float" in str(exc_info.value)


def test_is_list():
    assert validator.is_list(value=[1, 2, 3], validate_field='value', exception_msg='value must be list')

    with pytest.raises(ValueError) as exc_info:
        validator.is_list(value="test", validate_field='value', exception_msg='value must be list')
    assert "value must be list" in str(exc_info.value)


def test_is_dict():
    assert validator.is_dict(value={"key": "value"}, validate_field='value', exception_msg='value must be dict')

    with pytest.raises(ValueError) as exc_info:
        validator.is_dict(value=[1, 2, 3], validate_field='value', exception_msg='value must be dict')
    assert "value must be dict" in str(exc_info.value)


def test_is_set():
    assert validator.is_set(value={1, 2, 3}, validate_field='value', exception_msg='value must be set')

    with pytest.raises(ValueError) as exc_info:
        validator.is_set(value=[1, 2, 3], validate_field='value', exception_msg='value must be set')
    assert "value must be set" in str(exc_info.value)


def test_is_tuple():
    assert validator.is_tuple(value=(1, 2, 3), validate_field='value', exception_msg='value must be tuple')

    with pytest.raises(ValueError) as exc_info:
        validator.is_tuple(value=[1, 2, 3], validate_field='value', exception_msg='value must be tuple')
    assert "value must be tuple" in str(exc_info.value)


def test_is_not_none():
    assert validator.is_not_none(value="test", validate_field='value', exception_msg='value must not be None')

    with pytest.raises(ValueError) as exc_info:
        validator.is_not_none(value=None, validate_field='value', exception_msg='value must not be None')
    assert "value must not be None" in str(exc_info.value)


def test_is_not_empty():
    assert validator.is_not_empty(value="test", validate_field='value', exception_msg='value must not be empty')

    with pytest.raises(ValueError) as exc_info:
        validator.is_not_empty(value="", validate_field='value', exception_msg='value must not be empty')
    assert "value must not be empty" in str(exc_info.value)


def test_is_allowed_value():
    assert validator.is_allowed_value(value=3, allowed_values=[1, 2, 3, 4, 5], validate_field='value',
                                      exception_msg='value must be in allowed_values')

    with pytest.raises(ValueError) as exc_info:
        validator.is_allowed_value(value=6, allowed_values=[1, 2, 3, 4, 5], validate_field='value',
                                   exception_msg='value must be in allowed_values')
    assert "value must be in allowed_values" in str(exc_info.value)


def test_max_length():
    assert validator.max_length(value="test", max_length=5, validate_field='value',
                                exception_msg='value length must be less than or equal to 5')

    with pytest.raises(ValueError) as exc_info:
        validator.max_length(value="test", max_length=3, validate_field='value',
                             exception_msg='value length must be less than or equal to 3')
    assert "value length must be less than or equal to 3" in str(exc_info.value)


def test_min_length():
    assert validator.min_length(value="test", min_length=3, validate_field='value',
                                exception_msg='value length must be greater than or equal to 3')

    with pytest.raises(ValueError) as exc_info:
        validator.min_length(value="test", min_length=5, validate_field='value',
                             exception_msg='value length must be greater than or equal to 5')
    assert "value length must be greater than or equal to 5" in str(exc_info.value)


def test_is_substring():
    assert validator.is_substring(sub_string="st", super_string="test", validate_field='sub_string',
                                   exception_msg='sub_string must be a substring of super_string')

    with pytest.raises(ValueError) as exc_info:
        validator.is_substring(sub_string="abc", super_string="test", validate_field='sub_string',
                                exception_msg='sub_string must be a substring of super_string')
    assert "sub_string must be a substring of super_string" in str(exc_info.value)


def test_is_subset():
    assert validator.is_subset(subset={1, 2}, superset={1, 2, 3, 4}, validate_field='subset',
                               exception_msg='subset must be a subset of superset')

    with pytest.raises(ValueError) as exc_info:
        validator.is_subset(subset={5, 6}, superset={1, 2, 3, 4}, validate_field='subset',
                            exception_msg='subset must be a subset of superset')
    assert "subset must be a subset of superset" in str(exc_info.value)


def test_is_sublist():
    assert validator.is_sublist(sublist=[1, 2], superlist=[1, 2, 3, 4], validate_field='sublist',
                                exception_msg='sublist must be a sublist of superlist')

    with pytest.raises(ValueError) as exc_info:
        validator.is_sublist(sublist=[5, 6], superlist=[1, 2, 3, 4], validate_field='sublist',
                             exception_msg='sublist must be a sublist of superlist')
    assert "sublist must be a sublist of superlist" in str(exc_info.value)


def test_contains_substring():
    assert validator.contains_substring(superstring="test", substring="es", validate_field='superstring',
                                        exception_msg='superstring must contain substring')

    with pytest.raises(ValueError) as exc_info:
        validator.contains_substring(superstring="test", substring="abc", validate_field='superstring',
                                     exception_msg='superstring must contain substring')
    assert "superstring must contain substring" in str(exc_info.value)


def test_contains_subset():
    assert validator.contains_subset(superset={1, 2, 3, 4}, subset={1, 2}, validate_field='superset',
                                    exception_msg='superset must contain subset')

    with pytest.raises(ValueError) as exc_info:
        validator.contains_subset(superset={1, 2, 3, 4}, subset={5, 6}, validate_field='superset',
                                 exception_msg='superset must contain subset')
    assert "superset must contain subset" in str(exc_info.value)


def test_contains_sublist():
    assert validator.contains_sublist(superlist=[1, 2, 3, 4], sublist=[1, 2], validate_field='superlist',
                                      exception_msg='superlist must contain sublist')

    with pytest.raises(ValueError) as exc_info:
        validator.contains_sublist(superlist=[1, 2, 3, 4], sublist=[5, 6], validate_field='superlist',
                                   exception_msg='superlist must contain sublist')
    assert "superlist must contain sublist" in str(exc_info.value)


def test_is_file_suffix():
    assert validator.is_file_suffix(path="example.txt", file_suffix=".txt", validate_field='path',
                                    exception_msg='path must have the specified file suffix')

    with pytest.raises(ValueError) as exc_info:
        validator.is_file_suffix(path="example.txt", file_suffix=".csv", validate_field='path',
                                 exception_msg='path must have the specified file suffix')
    assert "path must have the specified file suffix" in str(exc_info.value)


def test_is_file():
    assert validator.is_file(path=__file__, validate_field='path', exception_msg='path must be an existing file')

    with pytest.raises(ValueError) as exc_info:
        validator.is_file(path="nonexistent_file.txt", validate_field='path',
                          exception_msg='path must be an existing file')
    assert "path must be an existing file" in str(exc_info.value)


def test_is_dir():
    assert validator.is_dir(path=os.path.dirname(__file__), validate_field='path',
                            exception_msg='path must be an existing directory')

    with pytest.raises(ValueError) as exc_info:
        validator.is_dir(path="nonexistent_directory", validate_field='path',
                         exception_msg='path must be an existing directory')
    assert "path must be an existing directory" in str(exc_info.value)


def test_is_similar_dict():
    dict1 = {"key1": "value1", "key2": "value2"}
    dict2 = {"key1": "value2", "key2": "value3"}
    assert validator.is_similar_dict(target_dict=dict1, reference_dict=dict2, validate_field='target_dict',
                                     exception_msg='target_dict must be similar to reference_dict')

    dict3 = {"key2": "value1", "key3": "value3"}
    with pytest.raises(ValueError) as exc_info:
        validator.is_similar_dict(target_dict=dict1, reference_dict=dict3, validate_field='target_dict',
                                  exception_msg='target_dict must be similar to reference_dict')
    assert "target_dict must be similar to reference_dict" in str(exc_info.value)


def test_is_method():
    assert validator.is_method(value=print, validate_field='value', exception_msg='value must be a callable method')

    with pytest.raises(ValueError) as exc_info:
        validator.is_method(value="test", validate_field='value', exception_msg='value must be a callable method')
    assert "value must be a callable method" in str(exc_info.value)



4、日志输出

执行 test 的日志如下,验证通过:

============================= test session starts =============================
collecting ... collected 24 items

test_validator.py::test_is_string PASSED                                 [  4%]
test_validator.py::test_is_int PASSED                                    [  8%]
test_validator.py::test_is_positive PASSED                               [ 12%]
test_validator.py::test_is_float PASSED                                  [ 16%]
test_validator.py::test_is_list PASSED                                   [ 20%]
test_validator.py::test_is_dict PASSED                                   [ 25%]
test_validator.py::test_is_set PASSED                                    [ 29%]
test_validator.py::test_is_tuple PASSED                                  [ 33%]
test_validator.py::test_is_not_none PASSED                               [ 37%]
test_validator.py::test_is_not_empty PASSED                              [ 41%]
test_validator.py::test_is_allowed_value PASSED                          [ 45%]
test_validator.py::test_max_length PASSED                                [ 50%]
test_validator.py::test_min_length PASSED                                [ 54%]
test_validator.py::test_is_substring PASSED                              [ 58%]
test_validator.py::test_is_subset PASSED                                 [ 62%]
test_validator.py::test_is_sublist PASSED                                [ 66%]
test_validator.py::test_contains_substring PASSED                        [ 70%]
test_validator.py::test_contains_subset PASSED                           [ 75%]
test_validator.py::test_contains_sublist PASSED                          [ 79%]
test_validator.py::test_is_file_suffix PASSED                            [ 83%]
test_validator.py::test_is_file PASSED                                   [ 87%]
test_validator.py::test_is_dir PASSED                                    [ 91%]
test_validator.py::test_is_similar_dict PASSED                           [ 95%]
test_validator.py::test_is_method PASSED                                 [100%]

============================= 24 passed in 0.03s ==============================

三、后置说明

1、要点小结

  • RaiseExceptionMeta 类是一个元类,它的作用是在类定义时自动为类的成员方法(包括普通方法、静态方法和类方法)添加异常处理的装饰器,简化异常处理逻辑。
  • __new__ 方法用于修改类的行为,在 RaiseExceptionMeta 类中,它会遍历类的字典(包括属性和方法),并对其中的静态方法、类方法和普通成员方法进行修改。
  • __new__ 是在对象实例创建之前调用的方法,用于创建并返回一个新的实例;__init__ 是实例创建之后调用的方法,用于对实例进行初始化。

2、下节准备

  • 为 validator 对象添加链式调用功能,并 return 校验后的值

点击返回主目录

你可能感兴趣的:(pyparamvalidate,参数校验器从编码到发布全过程,python)