RaiseExceptionMeta
元类隐式装饰 Validator
类中的所有校验函数atme
: @me
用于存放临时的代码片断或其它内容。pyparamvalidate
: 新建一个与项目名称同名的package,为了方便发布至 pypi
。core
: 用于存放核心代码。tests
: 用于存放测试代码。utils
: 用于存放一些工具类或方法。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)
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)
执行 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 ==============================
RaiseExceptionMeta
类是一个元类,它的作用是在类定义时自动为类的成员方法(包括普通方法、静态方法和类方法)添加异常处理的装饰器,简化异常处理逻辑。__new__
方法用于修改类的行为,在 RaiseExceptionMeta
类中,它会遍历类的字典(包括属性和方法),并对其中的静态方法、类方法和普通成员方法进行修改。__new__
是在对象实例创建之前调用的方法,用于创建并返回一个新的实例;__init__
是实例创建之后调用的方法,用于对实例进行初始化。点击返回主目录