目录
- 一、前置说明
- 1、总体目录
- 2、相关回顾
- 3、本节目标
- 二、操作步骤
- 1、项目目录
- 2、代码实现
- 3、测试代码
- 4、日志输出
- 三、后置说明
- 1、要点小结
- 2、下节准备
一、前置说明
1、总体目录
- 《 pyparamvalidate 参数校验器,从编码到发布全过程》
2、相关回顾
- pyparamvalidate 重构背景和需求分析
3、本节目标
- 将
validator
校验器从ParameterValidator
中抽离出来
二、操作步骤
1、项目目录
validator.py
: 用于存放抽离出来的validator
校验函数的代码。test_validator.py
:存放测试validator
校验函数的代码。__init__.py
: 将pyparamvalidate
转为python
的package
包,用于统一管理import
。
2、代码实现
pyparamvalidate/core/validator.py
import functools
import inspect
import osfrom pyparamvalidate import DictUtilitydef raise_exception(func):@functools.wraps(func)def wrapper(*args, **kwargs):bound_args = inspect.signature(func).bind(*args, **kwargs).argumentsvalidate_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 resultreturn wrapper@raise_exception
def is_string(value, validate_field=None, exception_msg=None):return isinstance(value, str)@raise_exception
def is_string(value, validate_field=None, exception_msg=None):return isinstance(value, str)@raise_exception
def is_int(value, validate_field=None, exception_msg=None):return isinstance(value, int)@raise_exception
def is_positive(value, validate_field=None, exception_msg=None):return value > 0@raise_exception
def is_float(value, validate_field=None, exception_msg=None):return isinstance(value, float)@raise_exception
def is_list(value, validate_field=None, exception_msg=None):return isinstance(value, list)@raise_exception
def is_dict(value, validate_field=None, exception_msg=None):return isinstance(value, dict)@raise_exception
def is_set(value, validate_field=None, exception_msg=None):return isinstance(value, set)@raise_exception
def is_tuple(value, validate_field=None, exception_msg=None):return isinstance(value, tuple)@raise_exception
def is_not_none(value, validate_field=None, exception_msg=None):return value is not None@raise_exception
def is_not_empty(value, validate_field=None, exception_msg=None):return bool(value)@raise_exception
def is_allowed_value(value, allowed_values, validate_field=None, exception_msg=None):return value in allowed_values@raise_exception
def max_length(value, max_length, validate_field=None, exception_msg=None):return len(value) <= max_length@raise_exception
def min_length(value, min_length, validate_field=None, exception_msg=None):return len(value) >= min_length@raise_exception
def is_substring(sub_string, super_string, validate_field=None, exception_msg=None):return sub_string in super_string@raise_exception
def is_subset(subset, superset, validate_field=None, exception_msg=None):return subset.issubset(superset)@raise_exception
def is_sublist(sublist, superlist, validate_field=None, exception_msg=None):return set(sublist).issubset(set(superlist))@raise_exception
def contains_substring(superstring, substring, validate_field=None, exception_msg=None):return substring in superstring@raise_exception
def contains_subset(superset, subset, validate_field=None, exception_msg=None):return subset.issubset(superset)@raise_exception
def contains_sublist(superlist, sublist, validate_field=None, exception_msg=None):return set(sublist).issubset(set(superlist))@raise_exception
def is_file_suffix(path, file_suffix, validate_field=None, exception_msg=None):return path.endswith(file_suffix)@raise_exception
def is_file(path, validate_field=None, exception_msg=None):return os.path.isfile(path)@raise_exception
def is_dir(path, validate_field=None, exception_msg=None):return os.path.isdir(path)@raise_exception
def is_similar_dict(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)@raise_exception
def is_method(value, validate_field=None, exception_msg=None):return callable(value)
3、测试代码
pyparamvalidate/tests/test_validator.py
import pytestfrom atme.demo_v2.validator_v1 import *def test_is_string():assert is_string(value="test", validate_field='value', exception_msg='value must be string')with pytest.raises(ValueError) as exc_info: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 is_int(value=42, validate_field='value', exception_msg='value must be integer')with pytest.raises(ValueError) as exc_info: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 is_positive(value=42, validate_field='value', exception_msg='value must be positive')with pytest.raises(ValueError) as exc_info: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 is_float(value=3.14, validate_field='value', exception_msg='value must be float')with pytest.raises(ValueError) as exc_info: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 is_list(value=[1, 2, 3], validate_field='value', exception_msg='value must be list')with pytest.raises(ValueError) as exc_info: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 is_dict(value={"key": "value"}, validate_field='value', exception_msg='value must be dict')with pytest.raises(ValueError) as exc_info: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 is_set(value={1, 2, 3}, validate_field='value', exception_msg='value must be set')with pytest.raises(ValueError) as exc_info: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 is_tuple(value=(1, 2, 3), validate_field='value', exception_msg='value must be tuple')with pytest.raises(ValueError) as exc_info: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 is_not_none(value="test", validate_field='value', exception_msg='value must not be None')with pytest.raises(ValueError) as exc_info: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 is_not_empty(value="test", validate_field='value', exception_msg='value must not be empty')with pytest.raises(ValueError) as exc_info: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 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: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 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: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 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: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 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: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 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: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 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: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 contains_substring(superstring="test", substring="es", validate_field='superstring', exception_msg='superstring must contain substring')with pytest.raises(ValueError) as exc_info: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 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: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 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: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 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: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 is_file(path=__file__, validate_field='path', exception_msg='path must be an existing file')with pytest.raises(ValueError) as exc_info: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 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: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 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: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 is_method(value=print, validate_field='value', exception_msg='value must be a callable method')with pytest.raises(ValueError) as exc_info: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 itemstest_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.04s ==============================
三、后置说明
1、要点小结
- 校验函数中的关键字参数
validate_field=None, exception_msg=None
虽然没有被直接调用,但它实际在raise_exception
装饰器中被隐式调用了。
- 显示优于隐式,不建议用
**kwargs
代替validate_field=None, exception_msg=None
,方便在编辑器pycharm
中为调用方智能提示参数。
- 如果校验函数中有多个参数,永远将被校验的参数放在最前面、参照参数放在后面。
@raise_exception def is_sublist(sublist, superlist, validate_field=None, exception_msg=None):return set(sublist).issubset(set(superlist))@raise_exception def contains_substring(superstring, substring, validate_field=None, exception_msg=None):return substring in superstring@raise_exception def is_similar_dict(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)
- 在每一个校验函数上都添加
@raise_exception
装饰器,显得有点累赘,可以继续优化。
2、下节准备
- 使用 RaiseExceptionMeta 元类隐式装饰 Validator 类中的所有校验函数。
点击返回主目录