使用 Mypy 插件对动态属性创建进行类型检查

问题描述 投票:0回答:1

我有一个代码库,它使用一种非常奇怪的模式来定义命令行选项。看起来像这样:

# opts.py

def group():
    o = OptionsGroup()
    return o, o.define

options = _SomeOptionsSingletonClass()

def define(name: str, type_: Type, default: bool, ...):
    # Create and set attribute on `options` singleton object to track this option.
    ...

class OptionsGroup:
    def define(name: str, type_: Type, default: bool, ...):
        define(*args)

然后,在代码库中的各个文件中,模式 1

# some_module.py
from opts import group

options, define = group()

define("foo", type="str", ...)

# ...

print(options.foo) # How the heck do I typecheck this?

或模式 2:

# some_module.py
from opts import options, define

define("foo", type="str", ...)

# ...

print(options.foo) # How the heck do I typecheck this?

我们广泛使用

mypy
来对代码进行类型检查,其中许多选项结构都处于需要覆盖的关键路径中。

潜在的解决方案

  • 我无法删除或重构这个模式。它深深嵌入到大型生产代码库中。
  • 我不知道如何将这种构造硬塞到 Python 静态类型系统中。
  • 当前的解决方案是一个非常脆弱的脚本,它在执行之前尝试解析这些结构并将它们重写为可检查的类型
    mypy
    。我不喜欢这个解决方案,因为它意味着
    mypy
    无法针对 actual 代码库运行,而且脚本的逻辑非常脆弱。 (我可以修复脆弱性)。
  • 我探索过使用
    mypy
    插件来推断类型并将类型添加到
    options
    值。
    • 我发现我无法让
      get_function_signature_hook()
      get_function_hook()
      get_method_signature_hook()
      get_method_hook()
      钩子在模式 1 中对
      define()
      的任何调用上触发。
    • 我无法轻松使用分析类型钩子,因为涉及的类没有实现
      __getattr__()
      ,并且更改这些类的结构非常危险。

我强烈希望使用

mypy
插件或其他允许类型检查来理解我的实际源文件中的结构的策略,而不是使用经过修改的源树。

我的问题

广泛地说:是否有更好的解决方案来检查此模式的类型?

狭义上:有什么方法可以让

mypy
调用我的插件,同时评估对
define()
的调用和/或对
options
属性的引用,以便我可以合成这些属性引用的类型?

mypy python-typing
1个回答
0
投票

因此,下面的解决方案确实涉及稍微更改模式,但与定义完全兼容,因为它提供了围绕它的包装器。希望这对您有用!

我们定义一个类对象,然后将其用于类型检查和定义变量。您可以根据需要更新模式,因为它不应该与定义中发生的情况发生冲突(如果定义使用字符串作为类型,您可能需要在define_all中定义映射)。我在下面对模式 2 进行了描述,因为它更简单,但没有理由不能与模式 1 等效:

from typing import cast

# opts.py
class _SomeOptionsSingletonClass:
    pass

options = _SomeOptionsSingletonClass()

def define(name: str, type_: type):
    # Just made this up as an example use
    print(f"defined attr, {name}, type {type_}")
    if type_ == str:
        setattr(options, name, "test")
    elif type_ == int:
        setattr(options, name, 1)
    else:
        assert False

# New code for opts.py
class BaseTypesToAdd:
    pass

def define_all(t: type[BaseTypesToAdd]):
    for name, type_ in t.__annotations__.items():
        define(name=name, type_=type_)

# NewPattern2

class NewOptions(BaseTypesToAdd):
    foo: str
    bar: int

define_all(NewOptions)

options = cast(NewOptions, options)
print(options.foo)
© www.soinside.com 2019 - 2024. All rights reserved.