【问题标题】:Python Typing: Validation Decorator for Literal typed ArgumentsPython 类型:文字类型参数的验证装饰器
【发布时间】:2021-02-08 02:19:47
【问题描述】:

我经常遇到只接受一组有限值的函数的场景。我知道如何在类型注释中反映这种行为,像这样使用typing.Literal

import typing


def func(a: typing.Literal['foo', 'bar']):
    pass

我想要一个装饰器@validate_literals 来验证参数是否与其类型一致:

@validate_literals
def picky_typed_function(
    binary: typing.Literal[0, 1],
    char: typing.Literal['a', 'b']
) -> None:
    pass

以便根据参数类型定义的限制验证输入,并在违规情况下引发ValueError

picky_typed_function(0, 'a')  # should pass
picky_typed_function(2, 'a')  # should raise "ValueError: binary must be one of (0, 1)')"
picky_typed_function(0, 'c')  # should raise "ValueError: char must be one of ('a', 'b')"
picky_typed_function(0, char='c')  # should raise "ValueError: char must be one of ('a', 'b')"
picky_typed_function(binary=2, char='c')  # should raise "ValueError: binary must be one of (0, 1)"

typing 类型检查设计为静态的,不会在运行时发生。如何利用类型定义进行运行时验证?

【问题讨论】:

    标签: python python-decorators python-typing code-inspection


    【解决方案1】:

    我们可以使用inspect.signature检查修饰(验证)函数的签名,通过typing.get_origin()获取参数注释的“来源”来检查函数的哪些参数被键入为文字别名(或者,对于 __origin__) 并使用 Literal 别名中的 [typing.get_args()] (https://stackoverflow.com/a/64522240/3566606)(并递归地遍历嵌套的 Literal 定义)检索有效值。

    为了做到这一点,剩下要做的就是找出哪些参数已作为位置参数传递并将相应的值映射到参数的名称,以便可以将值与有效值进行比较参数。

    最后,我们使用 functools.wraps 的标准配方构建装饰器。最后,这是代码:

    import inspect
    import typing
    import functools
    
    
    def args_to_kwargs(func: typing.Callable, *args: list, **kwargs: dict) -> dict:
        args_dict = {
            list(inspect.signature(func).parameters.keys())[i]: arg
            for i, arg in enumerate(args)
        }
    
        return {**args_dict, **kwargs}
    
    
    def valid_args_from_literal(annotation: _GenericAlias) -> Set[Any]:
        args = get_args(annotation)
        valid_values = []
    
        for arg in args:
            if typing.get_origin(parameter.annotation) is Literal:
                valid_values += valid_args_from_literal(arg)
            else:
                valid_values += [arg]
    
        return set(valid_values)
    
    
    def validate_literals(func: typing.Callable) -> typing.Callable:
        @functools.wraps(func)
        def validated(*args, **kwargs):
            kwargs = args_to_kwargs(func, *args, **kwargs)
            for name, parameter in inspect.signature(func).parameters.items():
                # use parameter.annotation.__origin__ for Python versions < 3.8
                if typing.get_origin(parameter.annotation) is typing.Literal:
                    valid_values = valid_args_from_literal(parameter.annotation)
                    if kwargs[name] not in valid_values:
                        raise ValueError(
                            f"Argument '{name}' must be one of {valid_values}"
                        )
    
            return func(**kwargs)
    
        return validated
    

    这给出了问题中指定的结果。

    我还发布了一个 python 包runtime-typing 的 alpha 版本来执行运行时类型检查:https://pypi.org/project/runtime-typing/(文档:https://runtime-typing.readthedocs.io)它处理的案例不仅仅是typing.Literal,例如typing.TypeVar 和@987654337 @。

    【讨论】:

      【解决方案2】:
      from typing import Literal
      from valdec.dec import validate
      
      @validate
      def func(a: Literal["foo", "bar"]) -> str:
          return a
      
      assert func("bar") == "bar"
      
      
      @validate("return", exclude=True)
      def func(binary: Literal[0, 1], char: Literal["a", "b"]):
          return binary, char
      
      assert func(0, "a") == (0, "a")
      
      
      func(2, "x")
      # valdec.utils.ValidationArgumentsError: Validation error <class 
      # 'valdec.val_pydantic.ValidationError'>: 2 validation errors for argument 
      # with the name of:
      # binary
      #   unexpected value; permitted: 0, 1 (type=value_error.const; given=2; 
      #   permitted=(0, 1))
      # char
      #   unexpected value; permitted: 'a', 'b' (type=value_error.const; given=x; 
      #   permitted=('a', 'b')).
      
      

      valdec:https://github.com/EvgeniyBurdin/valdec

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2020-07-13
        • 2016-08-21
        • 2011-11-21
        • 2022-08-15
        • 2021-05-19
        • 2018-04-14
        • 2014-03-24
        相关资源
        最近更新 更多