目录
- 一、前置说明
- 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 os
from pyparamvalidate 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
@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 pytest
from 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 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.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 类中的所有校验函数。
点击返回主目录