目录
- 一、前置说明
- 1、总体目录
- 2、相关回顾
- 3、本节目标
- 二、操作步骤
- 1、项目目录
- 2、代码实现
- 3、测试代码
- 4、日志输出
- 三、后置说明
- 1、要点小结
- 2、下节准备
一、前置说明
1、总体目录
- 《 pyparamvalidate 参数校验器,从编码到发布全过程》
2、相关回顾
- param_validator 核心代码实现
3、本节目标
param_validator
常用校验器的实现
二、操作步骤
1、项目目录
atme
:@me
用于存放临时的代码片断或其它内容。pyparamvalidate
: 新建一个与项目名称同名的package,为了方便发布至pypi
。core
: 用于存放核心代码。tests
: 用于存放测试代码。utils
: 用于存放一些工具类或方法。
2、代码实现
pyparamvalidate/core/param_validator.py
import functools
import inspect
import os.pathclass ParameterValidationError(Exception):def __init__(self, func, parameter_name, parameter_value, param_rule_description=None, exception_msg=None):self.func_name = func.__name__self.parameter_name = parameter_nameself.parameter_value = parameter_valueself.param_rule_description = param_rule_descriptionself.exception_msg = exception_msgsuper().__init__(self.error_message())def error_message(self):return (f"Parameter '{self.parameter_name}' in function '{self.func_name}' is invalid. "f"\t{'Error: ' + self.exception_msg if self.exception_msg else ''}"f"\t{'Please refer to: ' + self.param_rule_description if self.param_rule_description else ''}")class ParameterValidator:def __init__(self, param_name, param_rule_des=None):""":param param_name: 参数名:param param_rule_des: 该参数的规则描述"""self.param_name = param_nameself.param_rule_description = param_rule_des# 用于收集校验器self.validators = []def __call__(self, func):@functools.wraps(func)def wrapper(*args, **kwargs):# 获取函数的参数和参数值bound_args = inspect.signature(func).bind(*args, **kwargs).arguments# 如果函数被装饰,且以关键字参数传值,则从 kwargs 中取参数值if self.param_name in kwargs:value = kwargs[self.param_name]# # 如果函数被装饰,且以位置参数传值,则从 bound_args 中取参数值elif self.param_name in bound_args:value = bound_args.get(self.param_name)# 如果函数没有被装饰,则直接执行返回结果else:return func(*args, **kwargs)# 使用验证器进行验证for validator, exception_msg in self.validators:# 如果没验证通过,则抛出异常if not validator(value):raise ParameterValidationError(func, self.param_name, value,self.param_rule_description, exception_msg)# 所有参数校验通过,则执行函数返回结果return func(*args, **kwargs)return wrapperdef add_validator(self, validator_function, exception_msg=None):self.validators.append((validator_function, exception_msg))return selfdef is_string(self, exception_msg=None):return self.add_validator(lambda value: isinstance(value, str), exception_msg)def is_int(self, exception_msg=None):return self.add_validator(lambda value: isinstance(value, int), exception_msg)def is_positive(self, exception_msg=None):return self.add_validator(lambda value: value > 0, exception_msg)def is_float(self, exception_msg=None):return self.add_validator(lambda value: isinstance(value, float), exception_msg)def is_list(self, exception_msg=None):return self.add_validator(lambda value: isinstance(value, list), exception_msg)def is_dict(self, exception_msg=None):return self.add_validator(lambda value: isinstance(value, dict), exception_msg)def is_set(self, exception_msg=None):return self.add_validator(lambda value: isinstance(value, set), exception_msg)def is_tuple(self, exception_msg=None):return self.add_validator(lambda value: isinstance(value, tuple), exception_msg)def is_not_none(self, exception_msg=None):return self.add_validator(lambda value: value is not None, exception_msg)def is_not_empty(self, exception_msg=None):return self.add_validator(lambda value: bool(value), exception_msg)def is_allowed_value(self, allowed_values, exception_msg=None):return self.add_validator(lambda value: value in allowed_values, exception_msg)def max_length(self, max_length, exception_msg=None):return self.add_validator(lambda value: len(value) <= max_length, exception_msg)def min_length(self, min_length, exception_msg=None):return self.add_validator(lambda value: len(value) >= min_length, exception_msg)def is_substring(self, super_string, exception_msg=None):return self.add_validator(lambda value: value in super_string, exception_msg)def is_subset(self, superset, exception_msg=None):return self.add_validator(lambda value: value.issubset(superset), exception_msg)def is_sublist(self, super_list, exception_msg=None):return self.add_validator(lambda value: set(value).issubset(set(super_list)), exception_msg)def contains_substring(self, substring, exception_msg=None):return self.add_validator(lambda value: substring in value, exception_msg)def contains_subset(self, subset, exception_msg=None):return self.add_validator(lambda value: subset.issubset(value), exception_msg)def contains_sublist(self, sublist, exception_msg=None):return self.add_validator(lambda value: set(sublist).issubset(set(value)), exception_msg)def is_file_suffix(self, file_suffix, exception_msg=None):return self.add_validator(lambda value: value.endswith(file_suffix), exception_msg)def is_file(self, exception_msg=None):return self.add_validator(lambda value: os.path.isfile(value), exception_msg)def is_dir(self, exception_msg=None):return self.add_validator(lambda value: os.path.isdir(value), exception_msg)def is_method(self, exception_msg=None):def method_check(value):return callable(value)return self.add_validator(method_check, exception_msg)def custom_validator(self, validator_function, exception_msg=None):return self.add_validator(validator_function, exception_msg)
3、测试代码
pyparamvalidate/tests/test_param_validator.py
import osimport pytestfrom pyparamvalidate.core.param_validator import ParameterValidator, ParameterValidationErrordef test_is_string_validator_passing_01():"""不描述参数规则"""@ParameterValidator("param").is_string()def example_function(param):print(param)return paramassert example_function(param="test") == "test"with pytest.raises(ParameterValidationError) as exc_info:example_function(param=123)print(exc_info.value)assert "invalid" in str(exc_info.value)def test_is_string_validator_passing_02():"""在校验器中描述参数规则"""@ParameterValidator("param").is_string("Value must be a string")def example_function(param):print(param)return paramassert example_function(param="test") == "test"with pytest.raises(ParameterValidationError) as exc_info:example_function(param=123)print(exc_info.value)assert "Value must be a string" in str(exc_info.value)def test_is_string_validator_passing_03():"""在 ParameterValidator 实例化时描述参数规则"""@ParameterValidator("param", param_rule_des="Value must be a string").is_string()def example_function(param):print(param)return paramassert example_function(param="test") == "test"with pytest.raises(ParameterValidationError) as exc_info:example_function(param=123)print(exc_info.value)assert "Value must be a string" in str(exc_info.value)def test_is_int_validator():@ParameterValidator("param").is_int("Value must be an integer")def example_function(param):return paramassert example_function(param=123) == 123with pytest.raises(ParameterValidationError) as exc_info:example_function(param="test")assert "Value must be an integer" in str(exc_info.value)def test_is_positive_validator():@ParameterValidator("param").is_positive("Value must be positive")def example_function(param):return paramassert example_function(param=5) == 5with pytest.raises(ParameterValidationError) as exc_info:example_function(param=-3)assert "Value must be positive" in str(exc_info.value)def test_is_float_validator():@ParameterValidator("param").is_float("Value must be a float")def example_function(param):return paramassert example_function(param=3.14) == 3.14with pytest.raises(ParameterValidationError) as exc_info:example_function(param="test")assert "Value must be a float" in str(exc_info.value)def test_is_list_validator():@ParameterValidator("param").is_list("Value must be a list")def example_function(param):return paramassert example_function(param=[1, 2, 3]) == [1, 2, 3]with pytest.raises(ParameterValidationError) as exc_info:example_function(param="test")assert "Value must be a list" in str(exc_info.value)def test_is_dict_validator():@ParameterValidator("param").is_dict("Value must be a dictionary")def example_function(param):return paramassert example_function(param={"key": "value"}) == {"key": "value"}with pytest.raises(ParameterValidationError) as exc_info:example_function(param=[1, 2, 3])assert "Value must be a dictionary" in str(exc_info.value)def test_is_set_validator():@ParameterValidator("param").is_set("Value must be a set")def example_function(param):return paramassert example_function(param={1, 2, 3}) == {1, 2, 3}with pytest.raises(ParameterValidationError) as exc_info:example_function(param="test")assert "Value must be a set" in str(exc_info.value)def test_is_tuple_validator():@ParameterValidator("param").is_tuple("Value must be a tuple")def example_function(param):return paramassert example_function(param=(1, 2, 3)) == (1, 2, 3)with pytest.raises(ParameterValidationError) as exc_info:example_function(param="test")assert "Value must be a tuple" in str(exc_info.value)def test_is_not_none_validator():@ParameterValidator("param").is_not_none("Value must not be None")def example_function(param):return paramassert example_function(param="test") == "test"with pytest.raises(ParameterValidationError) as exc_info:example_function(param=None)assert "Value must not be None" in str(exc_info.value)def test_is_not_empty_validator():@ParameterValidator("param").is_not_empty("Value must not be empty")def example_function(param):return paramassert example_function(param="test") == "test"with pytest.raises(ParameterValidationError) as exc_info:example_function(param="")assert "Value must not be empty" in str(exc_info.value)def test_max_length_validator():@ParameterValidator("param").max_length(5, "Value must have max length of 5")def example_function(param):return paramassert example_function(param="test") == "test"with pytest.raises(ParameterValidationError) as exc_info:example_function(param="toolongtext")assert "Value must have max length of 5" in str(exc_info.value)def test_min_length_validator():@ParameterValidator("param").min_length(5, "Value must have min length of 5")def example_function(param):return paramassert example_function(param="toolongtext") == "toolongtext"with pytest.raises(ParameterValidationError) as exc_info:example_function(param="test")assert "Value must have min length of 5" in str(exc_info.value)def test_is_substring_validator():@ParameterValidator("param").is_substring("superstring", "Value must be a substring")def example_function(param):return paramassert example_function(param="string") == "string"with pytest.raises(ParameterValidationError) as exc_info:example_function(param="test")assert "Value must be a substring" in str(exc_info.value)def test_is_subset_validator():@ParameterValidator("param").is_subset({1, 2, 3}, "Value must be a subset")def example_function(param):return paramassert example_function(param={1, 2}) == {1, 2}with pytest.raises(ParameterValidationError) as exc_info:example_function(param={4, 5})assert "Value must be a subset" in str(exc_info.value)def test_is_sublist_validator():@ParameterValidator("param").is_sublist([1, 2, 3], "Value must be a sub-list")def example_function(param):return paramassert example_function(param=[1, 2]) == [1, 2]with pytest.raises(ParameterValidationError) as exc_info:example_function(param=[1, 2, 3, 4, 5])assert "Value must be a sub-list" in str(exc_info.value)def test_contains_substring_validator():@ParameterValidator("param").contains_substring("substring", "Value must contain substring")def example_function(param):return paramassert example_function(param="This is a substring") == "This is a substring"with pytest.raises(ParameterValidationError) as exc_info:example_function(param="test")assert "Value must contain substring" in str(exc_info.value)def test_contains_subset_validator():@ParameterValidator("param").contains_subset({1, 2, 3}, "Value must contain a subset")def example_function(param):return paramassert example_function(param={1, 2, 3, 4, 5}) == {1, 2, 3, 4, 5}with pytest.raises(ParameterValidationError) as exc_info:example_function(param={4, 5})assert "Value must contain a subset" in str(exc_info.value)def test_contains_sublist_validator():@ParameterValidator("param").contains_sublist([1, 2, 3], "Value must contain a sub-list")def example_function(param):return paramassert example_function(param=[1, 2, 3, 4, 5]) == [1, 2, 3, 4, 5]with pytest.raises(ParameterValidationError) as exc_info:example_function(param=[4, 5])assert "Value must contain a sub-list" in str(exc_info.value)def test_is_file_suffix_validator():@ParameterValidator("param").is_file_suffix(".txt", "Value must have .txt suffix")def example_function(param):return paramassert example_function(param="example.txt") == "example.txt"with pytest.raises(ParameterValidationError) as exc_info:example_function(param="example.jpg")assert "Value must have .txt suffix" in str(exc_info.value)def test_is_file_validator():@ParameterValidator("param").is_file("Value must be a valid file path")def example_function(param):return paramassert example_function(param=__file__) == __file__with pytest.raises(ParameterValidationError) as exc_info:example_function(param="/nonexistent/file.txt")assert "Value must be a valid file path" in str(exc_info.value)def test_is_dir_validator():@ParameterValidator("param").is_dir("Value must be a valid directory path")def example_function(param):return paramassert example_function(param=os.path.dirname(__file__)) == os.path.dirname(__file__)with pytest.raises(ParameterValidationError) as exc_info:example_function(param="/nonexistent/directory")assert "Value must be a valid directory path" in str(exc_info.value)def test_is_method_validator():def method():...@ParameterValidator("param").is_method("Value must be a callable method")def example_function(param):return paramassert example_function(param=method)with pytest.raises(ParameterValidationError) as exc_info:example_function(param="not a method")assert "Value must be a callable method" in str(exc_info.value)def test_custom_validator():def custom_check(value):return value % 2 == 0@ParameterValidator("param").custom_validator(custom_check, "Value must be an even number")def example_function(param):return paramassert example_function(param=4) == 4with pytest.raises(ParameterValidationError) as exc_info:example_function(param=5)assert "Value must be an even number" in str(exc_info.value)def test_multiple_custom_validators():def custom_check_1(value):return value % 2 == 0def custom_check_2(value):return value > 0@ParameterValidator("param").custom_validator(custom_check_1, "Value must be an even number").custom_validator(custom_check_2, "Value must be a positive number")def example_function(param):return paramassert example_function(param=4) == 4with pytest.raises(ParameterValidationError) as exc_info:example_function(param=5)assert "Value must be an even number" in str(exc_info.value)with pytest.raises(ParameterValidationError) as exc_info:example_function(param=-2)assert "Value must be a positive number" in str(exc_info.value)def test_complex_validator():@ParameterValidator("name").is_string("Name must be a string").is_not_empty("Name cannot be empty")@ParameterValidator("age").is_int("Age must be an integer").is_positive("Age must be a positive number")@ParameterValidator("gender").is_allowed_value(["male", "female"], "Gender must be either 'male' or 'female'")@ParameterValidator("description").is_string("Description must be a string").is_not_empty("Description cannot be empty")def example_function(name, age, gender='male', **kwargs):description = kwargs.get("description")return name, age, gender, description# 正向测试用例result = example_function(name="John", age=25, gender="male", description="A person")assert result == ("John", 25, "male", "A person")# 反向测试用例:测试 name 不是字符串的情况with pytest.raises(ParameterValidationError) as exc_info:example_function(name=123, age=25, gender="male", description="A person")assert "Name must be a string" in str(exc_info.value)# 反向测试用例:测试 age 不是正整数的情况with pytest.raises(ParameterValidationError) as exc_info:example_function(name="John", age="25", gender="male", description="A person")assert "Age must be an integer" in str(exc_info.value)# 反向测试用例:测试 gender 不是预定义值的情况with pytest.raises(ParameterValidationError) as exc_info:example_function(name="John", age=25, gender="other", description="A person")assert "Gender must be either 'male' or 'female'" in str(exc_info.value)# 反向测试用例:测试 description 不是字符串的情况with pytest.raises(ParameterValidationError) as exc_info:example_function(name="John", age=25, gender="male", description=123)assert "Description must be a string" in str(exc_info.value)# 反向测试用例:测试 description 是空字符串的情况with pytest.raises(ParameterValidationError) as exc_info:example_function(name="John", age=25, gender="male", description="")assert "Description cannot be empty" in str(exc_info.value)
4、日志输出
执行 test
的日志如下,验证通过:
============================= test session starts =============================
collecting ... collected 27 itemstest_param_validator.py::test_is_string_validator_passing_01 PASSED [ 3%]test
Parameter 'param' in function 'example_function' is invalid. test_param_validator.py::test_is_string_validator_passing_02 PASSED [ 7%]test
Parameter 'param' in function 'example_function' is invalid. Error: Value must be a string test_param_validator.py::test_is_string_validator_passing_03 PASSED [ 11%]test
Parameter 'param' in function 'example_function' is invalid. Please refer to: Value must be a stringtest_param_validator.py::test_is_int_validator PASSED [ 14%]
test_param_validator.py::test_is_positive_validator PASSED [ 18%]
test_param_validator.py::test_is_float_validator PASSED [ 22%]
test_param_validator.py::test_is_list_validator PASSED [ 25%]
test_param_validator.py::test_is_dict_validator PASSED [ 29%]
test_param_validator.py::test_is_set_validator PASSED [ 33%]
test_param_validator.py::test_is_tuple_validator PASSED [ 37%]
test_param_validator.py::test_is_not_none_validator PASSED [ 40%]
test_param_validator.py::test_is_not_empty_validator PASSED [ 44%]
test_param_validator.py::test_max_length_validator PASSED [ 48%]
test_param_validator.py::test_min_length_validator PASSED [ 51%]
test_param_validator.py::test_is_substring_validator PASSED [ 55%]
test_param_validator.py::test_is_subset_validator PASSED [ 59%]
test_param_validator.py::test_is_sublist_validator PASSED [ 62%]
test_param_validator.py::test_contains_substring_validator PASSED [ 66%]
test_param_validator.py::test_contains_subset_validator PASSED [ 70%]
test_param_validator.py::test_contains_sublist_validator PASSED [ 74%]
test_param_validator.py::test_is_file_suffix_validator PASSED [ 77%]
test_param_validator.py::test_is_file_validator PASSED [ 81%]
test_param_validator.py::test_is_dir_validator PASSED [ 85%]
test_param_validator.py::test_is_method_validator PASSED [ 88%]
test_param_validator.py::test_custom_validator PASSED [ 92%]
test_param_validator.py::test_multiple_custom_validators PASSED [ 96%]
test_param_validator.py::test_complex_validator PASSED [100%]============================= 27 passed in 0.05s ==============================
三、后置说明
1、要点小结
is_string
:检查参数是否为字符串。is_int
:检查参数是否为整数。is_positive
:检查参数是否为正数。is_float
:检查参数是否为浮点数。is_list
:检查参数是否为列表。is_dict
:检查参数是否为字典。is_set
:检查参数是否为集合。is_tuple
:检查参数是否为元组。is_not_none
:检查参数是否不为None。is_not_empty
:检查参数是否不为空(对于字符串、列表、字典、集合等)。is_allowed_value
:检查参数是否在指定的允许值范围内。max_length
:检查参数的长度是否不超过指定的最大值。min_length
:检查参数的长度是否不小于指定的最小值。is_substring
:检查参数是否为指定字符串的子串。is_subset
:检查参数是否为指定集合的子集。is_sublist
:检查参数是否为指定列表的子列表。contains_substring
:检查参数是否包含指定字符串。contains_subset
:检查参数是否包含指定集合。contains_sublist
:检查参数是否包含指定列表。is_file
:检查参数是否为有效的文件。is_dir
:检查参数是否为有效的目录。is_file_suffix
:检查参数是否以指定文件后缀结尾。is_method
:检查参数是否为可调用的方法(函数)。
除了以上内置验证器外,还可以使用 custom_validator
方法添加自定义验证器,请参考 test_multiple_custom_validators
测试用例。
2、下节准备
- 添加
is_similar_dict
校验器 : 如果key值相同,value类型相同,则判定为True,支持比对嵌套字典
点击返回主目录