mypy importlib 模块函数

mypy importlib module functions

我正在使用 importlib 在运行时导入模块。这些模块是我的应用程序的插件,必须实现 1 个或多个模块级功能。我已经开始向我的应用程序添加类型注释,但我从 mypy 中收到一条错误消息

Module has no attribute "generate_configuration"

其中 "generate_configuration" 是模块函数之一。

在这个例子中,模块只需要有一个generate_configuration函数。该函数接受一个字典参数。

def generate_configuration(data: Dict[str, DataFrame]) -> None: ...

我一直在寻找如何指定模块的接口,但我能找到的只有 class 个接口。有人可以指出一些说明如何执行此操作的文档吗?我的 google-fu 在这方面让我失望了。

加载此模块的代码如下所示。错误是由最后一行产生的。

plugin_directory = os.path.join(os.path.abspath(directory), 'Configuration-Generation-Plugins')
plugins = (
    module_file
    for module_file in Path(plugin_directory).glob('*.py')
)
sys.path.insert(0, plugin_directory)
for plugin in plugins:
    plugin_module = import_module(plugin.stem)
    plugin_module.generate_configuration(directory, points_list)

importlib.import_module 的类型注释只是 returns types.ModuleType

来自 the typeshed source:

def import_module(name: str, package: Optional[str] = ...) -> types.ModuleType: ...

这意味着 plugin_module 的显示类型是 Module -- 它没有您的特定属性。

由于mypy是一个静态分析工具,它无法知道那个import的return值有特定的接口。

这是我的建议:

  1. 为你的模块创建一个类型接口(它不必被实例化,它只会帮助 mypy 解决问题)

    class ModuleInterface:
        @staticmethod
        def generate_configuration(data: Dict[str, DataFrame]) -> None: ...
    
  2. 创建一个导入模块的函数,您可能需要添加 # type: ignore,但如果您使用 __import__ 而不是 import_module,您也许可以避免此限制

    def import_module_with_interface(modname: str) -> ModuleInterface:
        return __import__(modname, fromlist=['_trash'])  # might need to ignore the type here
    
  3. 欣赏类型:)

我用来验证这个想法的示例代码:

class ModuleInterface:
    @staticmethod
    def compute_foo(bar: str) -> str: ...


def import_module_with_interface(modname: str) -> ModuleInterface:
    return __import__(modname, fromlist=['_trash'])


def myf() -> None:
    mod = import_module_with_interface('test2')
    # mod.compute_foo()  # test.py:12: error: Too few arguments for "compute_foo" of "ModuleInterface"
    mod.compute_foo('hi')

我做了更多研究,最终确定了一个稍微不同的解决方案,它使用 typing.cast

解决方案仍然使用 中的静态方法定义。

from typing import Dict
from pandas import DataFrame

class ConfigurationGenerationPlugin(ModuleType):
@staticmethod
    def generate_configuration(directory: str, points_list: Dict[str, DataFrame]) -> None: ...

导入模块的代码然后使用 typing.cast() 设置正确的类型。

plugin_directory = os.path.join(os.path.abspath(directory), 'Configuration-Generation-Plugins')
plugins = (
    module_file
    for module_file in Path(plugin_directory).glob('*.py')
    if not module_file.stem.startswith('lib')
)
sys.path.insert(0, plugin_directory)
for plugin in plugins:
    plugin_module = cast(ConfigurationGenerationPlugin, import_module(plugin.stem))
    plugin_module.generate_configuration(directory, points_list)

我不确定我对必须将 ConfigurationGenerationPlugin class 或 cast() 调用添加到代码中只是为了让 mypy 开心的感觉如何。但是,我暂时要坚持下去。