一、前置说明
1、本节目标
- 添加常用校验方法,校验常见数据格式
2、相关回顾
二、操作步骤
1、项目目录
atme
:@me
用于存放临时的代码片断或其它内容。pyparamvalidate
: 新建一个与项目名称同名的package,为了方便发布至pypi
。core
: 用于存放核心代码。tests
: 用于存放测试代码。utils
: 用于存放一些工具类或方法。
2、代码实现
pyparamvalidate/core/validator.py
import functools import inspect import os from typing import TypeVar def _error_prompt(value, exception_msg=None, rule_des=None, field=None): default = f'"{value}" is invalid.' prompt = exception_msg or rule_des prompt = f'{default} due to: {prompt}' if prompt else default prompt = f'{field} error: {prompt}' if field else prompt return prompt def raise_exception(func): @functools.wraps(func) def wrapper(self, *args, **kwargs): bound_args = inspect.signature(func).bind(self, *args, **kwargs).arguments exception_msg = kwargs.get('exception_msg', None) or bound_args.get('exception_msg', None) error_prompt = _error_prompt(self.value, exception_msg, self._rule_des, self._field) result = func(self, *args, **kwargs) if not result: raise ValueError(error_prompt) return self return wrapper class RaiseExceptionMeta(type): def __new__(cls, name, bases, dct): for key, value in dct.items(): if isinstance(value, staticmethod): dct[key] = staticmethod(raise_exception(value.__func__)) if isinstance(value, classmethod): dct[key] = classmethod(raise_exception(value.__func__)) if inspect.isfunction(value) and not key.startswith("__"): dct[key] = raise_exception(value) return super().__new__(cls, name, bases, dct) ''' - TypeVar 是 Python 中用于声明类型变量的工具 - 声明一个类型变量,命名为 'Self', 意思为表示类的实例类型 - bound 参数指定泛型类型变量的上界,即限制 'Self' 必须是 'Validator' 类型或其子类型 ''' Self = TypeVar('Self', bound='Validator') class Validator(metaclass=RaiseExceptionMeta): def __init__(self, value, field=None, rule_des=None): self.value = value self._field = field self._rule_des = rule_des def is_string(self, exception_msg=None) -> Self: """ 将返回类型注解定义为 Self, 支持编辑器如 pycharm 智能提示链式调用方法,如:Validator(input).is_string().is_not_empty() - 从 Python 3.5 版本开始支持类型注解 - 在 Python 3.5 中引入了 PEP 484(Python Enhancement Proposal 484),其中包括了类型注解的概念,并引入了 typing 模块,用于支持类型提示和静态类型检查; - 类型注解允许开发者在函数参数、返回值和变量上添加类型信息,但是在运行时,Python 解释器不会检查这些注解是否正确; - 它们主要用于提供给静态类型检查器或代码编辑器进行,以提供更好的代码提示和错误检测; - Python 运行时并不强制执行这些注解,Python 依然是一门动态类型的语言。 - 本方法中: - 返回值类型为 bool 类型,用于与装饰器函数 raise_exception 配合使用,校验 self.value 是否通过; - 为了支持编辑器如 pycharm 智能识别链式调用方法,将返回类型注解定义为 Self, 如:Validator(input).is_string().is_not_empty(); - Self, 即 'Validator', 由 Self = TypeVar('Self', bound='Validator') 定义; - 如果返回类型不为 Self, 编辑器如 pycharm 在 Validator(input).is_string() 之后,不会智能提示 is_not_empty() """ return isinstance(self.value, str) def is_int(self, exception_msg=None): return isinstance(self.value, int) def is_positive(self, exception_msg=None): return self.value > 0 def is_float(self, exception_msg=None): return isinstance(self.value, float) def is_list(self, exception_msg=None): return isinstance(self.value, list) def is_dict(self, exception_msg=None): return isinstance(self.value, dict) def is_set(self, exception_msg=None): return isinstance(self.value, set) def is_tuple(self, exception_msg=None): return isinstance(self.value, tuple) def is_not_none(self, exception_msg=None): return self.value is not None def is_not_empty(self, exception_msg=None, stripped=True): if stripped: if isinstance(self.value, str): self.value = self.value.strip() return bool(self.value) def is_allowed_value(self, allowed_values, exception_msg=None): return self.value in allowed_values def is_specific_value(self, specific_value, exception_msg=None): return self.value == specific_value def max_length(self, max_length, exception_msg=None): return len(self.value) <= max_length def min_length(self, min_length, exception_msg=None): return len(self.value) >= min_length def is_substring(self, super_string, exception_msg=None): return self.value in super_string def is_subset(self, superset, exception_msg=None): return self.value.issubset(superset) def is_sublist(self, superlist, exception_msg=None): return set(self.value).issubset(set(superlist)) def contains_substring(self, substring, exception_msg=None): return substring in self.value def contains_subset(self, subset, exception_msg=None): return subset.issubset(self.value) def contains_sublist(self, sublist, exception_msg=None): return set(sublist).issubset(set(self.value)) def is_file(self, exception_msg=None): return os.path.isfile(self.value) def is_dir(self, exception_msg=None): return os.path.isdir(self.value) def is_file_suffix(self, file_suffix, exception_msg=None): return self.value.endswith(file_suffix) def is_method(self, exception_msg=None): return callable(self.value)
3、测试代码
pyparamvalidate/tests/test_validator.py
import os import pytest from pyparamvalidate.core.validator import Validator def test_is_string(): assert Validator("test").is_string(exception_msg='value must be string') with pytest.raises(ValueError) as exc_info: Validator(123).is_string(exception_msg='value must be string') assert "value must be string" in str(exc_info.value) def test_is_int(): assert Validator(42).is_int(exception_msg='value must be integer') with pytest.raises(ValueError) as exc_info: Validator("test").is_int(exception_msg='value must be integer') assert "value must be integer" in str(exc_info.value) def test_is_positive(): assert Validator(42).is_positive(exception_msg='value must be positive') with pytest.raises(ValueError) as exc_info: Validator(-1).is_positive(exception_msg='value must be positive') assert "value must be positive" in str(exc_info.value) def test_is_float(): assert Validator(3.14).is_float(exception_msg='value must be float') with pytest.raises(ValueError) as exc_info: Validator("test").is_float(exception_msg='value must be float') assert "value must be float" in str(exc_info.value) def test_is_list(): assert Validator([1, 2, 3]).is_list(exception_msg='value must be list') with pytest.raises(ValueError) as exc_info: Validator("test").is_list(exception_msg='value must be list') assert "value must be list" in str(exc_info.value) def test_is_dict(): assert Validator({"key": "value"}).is_dict(exception_msg='value must be dict') with pytest.raises(ValueError) as exc_info: Validator([1, 2, 3]).is_dict(exception_msg='value must be dict') assert "value must be dict" in str(exc_info.value) def test_is_set(): assert Validator({1, 2, 3}).is_set(exception_msg='value must be set') with pytest.raises(ValueError) as exc_info: Validator([1, 2, 3]).is_set(exception_msg='value must be set') assert "value must be set" in str(exc_info.value) def test_is_tuple(): assert Validator((1, 2, 3)).is_tuple(exception_msg='value must be tuple') with pytest.raises(ValueError) as exc_info: Validator([1, 2, 3]).is_tuple(exception_msg='value must be tuple') assert "value must be tuple" in str(exc_info.value) def test_is_not_none(): assert Validator("test").is_not_none(exception_msg='value must not be None') with pytest.raises(ValueError) as exc_info: Validator(None).is_not_none(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("test").is_not_empty(exception_msg='value must not be empty') with pytest.raises(ValueError) as exc_info: Validator(" ").is_not_empty(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(3).is_allowed_value(allowed_values=[1, 2, 3, 4, 5], exception_msg='value must be in allowed_values') with pytest.raises(ValueError) as exc_info: Validator(6).is_allowed_value(allowed_values=[1, 2, 3, 4, 5], exception_msg='value must be in allowed_values') assert "value must be in allowed_values" in str(exc_info.value) def test_is_specific_value(): assert Validator(3).is_specific_value(specific_value=3, exception_msg='value must be in allowed_values') with pytest.raises(ValueError) as exc_info: Validator(6).is_specific_value(specific_value=3, 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("test").max_length(max_length=5, exception_msg='value length must be less than or equal to 5') with pytest.raises(ValueError) as exc_info: Validator("test").max_length(max_length=3, 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("test").min_length(min_length=3, exception_msg='value length must be greater than or equal to 3') with pytest.raises(ValueError) as exc_info: Validator("test").min_length(min_length=5, 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("st").is_substring(super_string="test", exception_msg='sub_string must be a substring of super_string') with pytest.raises(ValueError) as exc_info: Validator("abc").is_substring(super_string="test", 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({1, 2}).is_subset(superset={1, 2, 3, 4}, exception_msg='subset must be a subset of superset') with pytest.raises(ValueError) as exc_info: Validator({5, 6}).is_subset(superset={1, 2, 3, 4}, 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([1, 2]).is_sublist(superlist=[1, 2, 3, 4], exception_msg='sublist must be a sublist of superlist') with pytest.raises(ValueError) as exc_info: Validator([5, 6]).is_sublist(superlist=[1, 2, 3, 4], 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("test").contains_substring(substring="es", exception_msg='superstring must contain substring') with pytest.raises(ValueError) as exc_info: Validator("test").contains_substring(substring="abc", exception_msg='superstring must contain substring') assert "superstring must contain substring" in str(exc_info.value) def test_contains_subset(): assert Validator({1, 2, 3, 4}).contains_subset(subset={1, 2}, exception_msg='superset must contain subset') with pytest.raises(ValueError) as exc_info: Validator({1, 2, 3, 4}).contains_subset(subset={5, 6}, exception_msg='superset must contain subset') assert "superset must contain subset" in str(exc_info.value) def test_contains_sublist(): assert Validator([1, 2, 3, 4]).contains_sublist(sublist=[1, 2], exception_msg='superlist must contain sublist') with pytest.raises(ValueError) as exc_info: Validator([1, 2, 3, 4]).contains_sublist(sublist=[5, 6], exception_msg='superlist must contain sublist') assert "superlist must contain sublist" in str(exc_info.value) def test_is_file_suffix(): assert Validator("example.txt").is_file_suffix(file_suffix=".txt", exception_msg='path must have the specified file suffix') with pytest.raises(ValueError) as exc_info: Validator("example.txt").is_file_suffix(file_suffix=".csv", 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(__file__).is_file(exception_msg='path must be an existing file') with pytest.raises(ValueError) as exc_info: Validator("path").is_file( 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(os.path.dirname(__file__)).is_dir( exception_msg='path must be an existing directory') with pytest.raises(ValueError) as exc_info: Validator(__file__).is_dir( exception_msg='path must be an existing directory') assert "path must be an existing directory" in str(exc_info.value) def test_is_method(): assert Validator(print).is_method(exception_msg='value must be a callable method') with pytest.raises(ValueError) as exc_info: Validator("test").is_method(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_is_specific_value PASSED [ 50%] test_validator.py::test_max_length PASSED [ 54%] test_validator.py::test_min_length PASSED [ 58%] test_validator.py::test_is_substring PASSED [ 62%] test_validator.py::test_is_subset PASSED [ 66%] test_validator.py::test_is_sublist PASSED [ 70%] test_validator.py::test_contains_substring PASSED [ 75%] test_validator.py::test_contains_subset PASSED [ 79%] test_validator.py::test_contains_sublist PASSED [ 83%] test_validator.py::test_is_file_suffix PASSED [ 87%] test_validator.py::test_is_file PASSED [ 91%] test_validator.py::test_is_dir PASSED [ 95%] test_validator.py::test_is_method PASSED [100%] ============================= 24 passed in 0.02s ==============================
三、后置说明
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
:检查参数是否为可调用的方法(函数)。
注意:要将在 Validator
类中添加的方法,复制粘贴至 ParameterValidator
类中,方便 Pycharm
智能提示。
2、下节准备
- 添加 自定义校验方法,让用户自定义校验规则