优化 ParamValidator,让编辑器Pycharm智能提示校验方法

目录

一、前置说明

1、总体目录

2、相关回顾

3、本节目标

  • 了解 __getattribute__ 的特性
  • 使用 __getattribute__ 结合 Validator 类中的方法,让编辑器 Pycharm 智能提示 ParamValidator 类中的方法

二、操作步骤

1、项目目录

  • atme : @me 用于存放临时的代码片断或其它内容。
  • pyparamvalidate : 新建一个与项目名称同名的package,为了方便发布至 pypi
  • core : 用于存放核心代码。
  • tests : 用于存放测试代码。
  • utils : 用于存放一些工具类或方法。

2、代码实现

atme/demo/validator_v6/param_validator.py

python 复制代码
import inspect
from functools import wraps
from typing import TypeVar, Callable

from atme.demo.validator_v6.validator import Validator

Self = TypeVar('Self', bound='ParameterValidator')


class ParameterValidator:
    def __init__(self, param_name: str, param_rule_des=None):
        """
        :param param_name: 参数名
        :param param_rule_des: 该参数的规则描述
        """
        self.param_name = param_name
        self.param_rule_des = param_rule_des

        self._validators = []

    def __getattribute__(self, name: str):
        """
        __getattribute__ 在每次访问对象的属性时都会触发,不管属性是否存在。
        通过重写 __getattribute__,可以自定义属性的获取逻辑,实现了对特定属性的直接访问(param_name 、param_rule_des 、 _validators),
        而对于其他属性,则创建名为 validator_method 的函数,将其作为属性返回
        """

        '''
        如果获取到的属性名为 param_name 、param_rule_des 、 _validators , 则使用 object.__getattribute__(self, name) 直接获取对象的属性值。
        不对 self.param_name 、 self.param_rule_des 、 self._validators 做改变
        '''
        if name in ['param_name', 'param_rule_des', '_validators']:
            return object.__getattribute__(self, name)

        '''
        如果属性名不在上述列表中,说明用户正在访问一个不存在的属性,这时创建了一个函数 collect_validator_method
        
        以用户使用 ParamValidator("param").is_string(exception_msg='param must be string').is_not_empty() 为例,代码执行过程如下:

        1. 当用户调用 ParamValidator("param").is_string(exception_msg='param must be string') 时,
        2. 由于 is_string 方法不存在,__getattr__ 方法被调用,返回 validator_method 函数(此时未被调用),is_string 方法实际上是 validator_method 函数的引用,
        3. 当执行 is_string(exception_msg='param must be string') 时,is_string 方法被调用, 使用关键字参数传递 exception_msg='param must be string',
        4. 实际上是执行了 validator_method(exception_msg='param must be string') , validator_method 函数完成调用后,执行函数体中的逻辑:
             - 向 self._validators 中添加了一个元组 ('is_string', (),  {'exception_msg': 'param  must  be  string'})
             - 返回 self 对象
        5. self 对象继续调用 is_not_empty(), 形成链式调用效果,此时的 validator_method 函数的引用就是 is_not_empty, 调用过程与 1-4 相同。
        '''

        def validator_method(*args, **kwargs):
            self._validators.append((name, args, kwargs))
            return self

        return validator_method

    def __call__(self, func: Callable) -> Callable:
        @wraps(func)
        def wrapper(*args, **kwargs):
            # 获取函数的参数和参数值
            bound_args = inspect.signature(func).bind(*args, **kwargs).arguments

            if self.param_name in kwargs:
                # 如果函数被装饰,且以关键字参数传值,则从 kwargs 中取参数值
                value = kwargs[self.param_name]
            else:
                # 如果函数被装饰,且以位置参数传值,则从 bound_args 中取参数值
                value = bound_args.get(self.param_name)

            # 实例化 Validator 对象
            validator = Validator(value, field=self.param_name, rule_des=self.param_rule_des)

            # 遍历所有校验器(注意:这里使用 vargs, vkwargs,避免覆盖原函数的 args, kwargs)
            for method_name, vargs, vkwargs in self._validators:
                # 通过 函数名 反射获取校验函数对象
                validate_method = getattr(validator, method_name)

                # 执行校验函数
                validate_method(*vargs, **vkwargs)

            # 执行原函数
            return func(*args, **kwargs)

        return wrapper

    '''
    ==============================分隔符===============================
    
    以下所有方法,是从 Validator 类中复制过来,目的是:
    
    - 让编辑器如 Pycharm 智能提示 ParameterValidator 本类中可以使用的校验方法;
    - 这些方法仅供 Pycharm 智能提示使用,没有任何实际作用;
        可以是:
            def is_string(self, exception_msg=None) -> Self:
                ...
        也可以是:
            def is_string(self, exception_msg=None) -> Self:
                return isinstance(self.value, str)            
    - ParameterValidator 类的实例通过 __getattribute__ 方法动态收集用户的调用方法;
    - 然后使用 __call__ 方法反射调用 Validator 类中的调用方法
    
    在模块中定义了: Self = TypeVar('Self', bound='ParameterValidator'),目的是:
    
    - 方便从 Validator 类中复制校验方法,粘贴之后不做任何代码层面的修改:
    - 方便链式调用,如: @ParameterValidator("param").is_string().is_not_empty()
    '''

    def is_string(self, exception_msg=None) -> Self:
        return isinstance(self.value, str)

    def is_not_empty(self, exception_msg=None) -> Self:
        return bool(self.value)

3、测试代码

atme/demo/validator_v6/test_param_validator.py

python 复制代码
import pytest

from atme.demo.validator_v6.param_validator import ParameterValidator


def test_is_string_validator_passing_01():
    """
    校验一个参数
    """

    @ParameterValidator("param").is_string(exception_msg='param must be string')
    def example_function(param):
        print(param)
        return param

    assert example_function(param="test") == "test"

    with pytest.raises(ValueError) 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("param2").is_string().is_not_empty()
    @ParameterValidator("param1").is_string().is_not_empty()
    def example_function(param1, param2):
        print(param1, param2)
        return param1, param2

    assert example_function("test1", "test2") == ("test1", "test2")

    with pytest.raises(ValueError) as exc_info:
        example_function(123, 123)

    print(exc_info.value)
    assert "invalid" in str(exc_info.value)

4、日志输出

执行 test 的日志如下,验证通过:

复制代码
============================= test session starts =============================
collecting ... collected 2 items

test_param_validator.py::test_is_string_validator_passing_01 PASSED      [ 50%]test
param error: "123" is invalid. due to: param must be string

test_param_validator.py::test_is_string_validator_passing_02 PASSED      [100%]test1 test2
param2 error: "123" is invalid.


============================== 2 passed in 0.01s ==============================

三、后置说明

1、要点小结

  • __getattribute__ 在每次访问对象的属性时都会触发,不管属性是否存在。
  • 通过重写 __getattribute__,可以自定义属性的获取逻辑,实现了对特定属性的直接访问(param_nameparam_rule_des_validators),而对于其他属性,则创建名为 validator_method 的函数,将其作为属性返回。
  • Validator 类中复制过来的校验方法,是为了让编辑器如 Pycharm 智能提示 ParameterValidator 本类中可以使用的校验方法,没有任何实际作用。
  • 在模块中定义 Self = TypeVar('Self', bound='ParameterValidator'),方便链式调用,如 @ParameterValidator("param").is_string().is_not_empty()
  • 经过优化后,Pycharm 可以正常智能提示可调用的校验方法:

2、下节准备

  • validator 常用校验器的实现

点击返回主目录

相关推荐
EQUINOX11 小时前
【论文阅读】| MoCo精读
论文阅读·人工智能·python·深度学习·机器学习
用户8356290780512 小时前
使用 Python 自动化 Excel 公式和函数:完整指南
后端·python
敲代码的嘎仔2 小时前
实习日志day6--实习日志day6--title命名规范化&businessType纠正&补充缺失的@Log注解&报警与通信模块补充&产出阶段总结文档
java·开发语言·人工智能·git·python·实习·大二
江华森3 小时前
Python 实现高德地图找房(一):环境搭建与数据爬虫
开发语言·爬虫·python
VIP_CQCRE3 小时前
用 Ace Data Cloud 快速接入 OpenAI Chat Completions:对话、流式、多轮和多模态一篇打通
python·ai·openai·api·教程
AOwhisky3 小时前
Python 学习笔记(第三期)——流程控制:条件判断与循环结构
运维·笔记·python·学习·云原生·流程控制·循环
老迟到的茉莉4 小时前
Hermes 是谁?跟 Claude Code 差在哪
开发语言·python
Day(AKA Elin)4 小时前
【Day】MTP(Multi Token Prediction)技术学习
python·深度学习·学习·llama
DogDaoDao5 小时前
【GitHub】 LLVM Project 深度解析:现代编译器基础设施的基石
java·c++·python·程序员·github·编译器·llvm
程序员小远5 小时前
接口测试之文件上传
自动化测试·软件测试·python·测试工具·职场和发展·测试用例·接口测试