我如何为一个 pluggy hook 规范注释类型?
How can I annotate types for a pluggy hook specification?
我想将类型注释添加到我的插件挂钩规范中,以便可以对挂钩实现进行类型检查。使用 pluggy documentation:
中的这个简化示例
import pluggy # type: ignore
hookspec = pluggy.HookspecMarker("myproject")
hookimpl = pluggy.HookimplMarker("myproject")
class MySpec(object):
"""A hook specification namespace."""
@hookspec
def myhook(self, arg1, arg2):
"""My special little hook that you can customize."""
class Plugin_1(object):
"""A hook implementation namespace."""
@hookimpl
def myhook(self, arg1, arg2):
print("inside Plugin_1.myhook()")
return arg1 + arg2 + "a" # intentional error
# create a manager and add the spec
pm = pluggy.PluginManager("myproject")
pm.add_hookspecs(MySpec)
# register plugins
pm.register(Plugin_1())
# call our `myhook` hook
# intentional incompatible type for parameter arg2
results = pm.hook.myhook(arg1=1, arg2="1")
print(results)
我认为正确有效的注释应该是:
def myhook(self, arg1: int, arg2: int) -> int: ...
我尝试将此注释添加到 hookspec。如我所料,这是行不通的。我相信这是因为 pluggy 实现的间接寻址是动态的。代码必须是 运行 以便 PluginManager
的 add_hookspecs()
方法可以定义可用的钩子。
我看到 pm.hook
是 pluggy.hooks._HookRelay
类型,pm.hook.myhook
是 pluggy.hooks._HookCaller
的一个实例,它有一个 __call__()
方法。
我尝试使用 stubgen
为 pluggy 制作一组 .pyi
文件,然后以两种不同的方式将注释添加到 pluggy.hooks._HookCaller
:
class _HookCaller:
def __init__(self, trace: Any) -> None: ...
def myhook(self, arg1: int, arg2: int) -> int: ...
def __call__(self, arg1: int, arg2: int) -> int: ...
当我执行 MYPYPATH=./stubs mypy --verboes example.py
时,我可以看到 hooks.pyi
正在被解析,但未检测到参数类型不匹配。即使我从 import pluggy
.
中删除 # type: ignore
注释,此行为也是一致的
问题:
- 是否可以将
myhook()
挂钩的类型注释定义为外部 .pyi
文件?
- 如果是这样,那个
.pyi
文件会包含什么?我应该把它存储在哪里,以便 mypy
在类型检查 运行 时提取它?
- 是否可以进行注释,使挂钩实现者和挂钩调用者都获得有用的类型提示?
第一个问题是 @hookspec
消除了 myhook
方法的类型提示:
from typing import TypeVar, Callable, Any, cast
# Improvement suggested by @oremanj on python/typing gitter
F = TypeVar("F", bound=Callable[..., Any])
hookspec = cast(Callable[[F], F], pluggy.HookspecMarker("myproject"))
该解决方法取消了对外部 .pyi
文件的要求。只需使用现有的钩子规范来定义类型提示。这解决了问题 1 和问题 2:您不需要 .pyi
文件。就用typing.cast()
给mypy一个提示,它不能从静态分析中学习:
# Add cast so that mypy knows that pm.hook
# is actually a MySpec instance. Without this
# hint there really is no way for mypy to know
# this.
pm.hook = cast(MySpec, pm.hook)
这可以通过添加注释来检查:
# Uncomment these when running through mypy to see
# how mypy regards the type
reveal_type(pm.hook)
reveal_type(pm.hook.myhook)
reveal_type(MySpec.myhook)
运行 这通过 mypy:
plug.py:24: error: Unsupported operand types for + ("int" and "str")
plug.py:42: error: Revealed type is 'plug.MySpec'
plug.py:43: error: Revealed type is 'def (arg1: builtins.int, arg2: builtins.int) -> builtins.int'
plug.py:44: error: Revealed type is 'def (self: plug.MySpec, arg1: builtins.int, arg2: builtins.int) -> builtins.int'
plug.py:47: error: Argument "arg2" to "myhook" of "MySpec" has incompatible type "str"; expected "int"
现在 mypy
捕获了 hook 调用者和 hook 实现的类型问题 (Q3)!
完整代码:
import pluggy # type: ignore
from typing import TypeVar, Callable, Any, cast
# Improvement suggested by @oremanj on python/typing gitter
F = TypeVar("F", bound=Callable[..., Any])
hookspec = cast(Callable[[F], F], pluggy.HookspecMarker("myproject"))
hookimpl = pluggy.HookimplMarker("myproject")
class MySpec(object):
"""A hook specification namespace."""
@hookspec
def myhook(self, arg1: int, arg2: int) -> int:
"""My special little hook that you can customize."""
class Plugin_1(object):
"""A hook implementation namespace."""
@hookimpl
def myhook(self, arg1: int, arg2: int) -> int:
print("inside Plugin_1.myhook()")
return arg1 + arg2 + 'a'
# create a manager and add the spec
pm = pluggy.PluginManager("myproject")
pm.add_hookspecs(MySpec)
# register plugins
pm.register(Plugin_1())
# Add cast so that mypy knows that pm.hook
# is actually a MySpec instance. Without this
# hint there really is no way for mypy to know
# this.
pm.hook = cast(MySpec, pm.hook)
# Uncomment these when running through mypy to see
# how mypy regards the type
# reveal_type(pm.hook)
# reveal_type(pm.hook.myhook)
# reveal_type(MySpec.myhook)
# this will now be caught by mypy
results = pm.hook.myhook(arg1=1, arg2="1")
print(results)
Brad 的回答中的很多事情都可以在 pluggy.pyi 文件中完成。我有 pluggy.pyi:
的这个(可能非常不完整)内容
from types import ModuleType
from typing import Callable, Type, TypeVar, Generic, Any
F = TypeVar("F", bound=Callable[..., Any])
class HookspecMarker:
def __init__(self, name: str) -> None:
...
def __call__(self, func: F) -> F:
...
class HookimplMarker:
def __init__(self, name: str) -> None:
...
def __call__(self, func: F) -> F:
...
Spec = TypeVar("Spec")
class PluginManager(Generic[Spec]):
def __init__(self, name: str) -> None:
...
def load_setuptools_entrypoints(self, name: str) -> None:
...
def add_hookspecs(self, module: Type[Spec]) -> None:
...
def register(self, module: ModuleType) -> None:
...
hook: Spec
这允许我创建一个插件管理器,如下所示:
import pluggy
from typing import TYPE_CHECKING
from .hookspecs import PluginSpec
from .plugins import localplugins
if TYPE_CHECKING:
PluginManager = pluggy.PluginManager[PluginSpec]
else:
PluginManager = pluggy.PluginManager
plugin: PluginManager = pluggy.PluginManager("mypackage")
plugin.add_hookspecs(PluginSpec)
plugin.load_setuptools_entrypoints("mypackage")
plugin.register(localplugins)
hookspec 需要是静态的 class 方法:
from typing import Any
import pluggy
hookspec = pluggy.HookspecMarker("mypackage")
class PluginSpec:
@staticmethod
@hookspec
def plugin_func(*args: Any) -> None:
...
我想将类型注释添加到我的插件挂钩规范中,以便可以对挂钩实现进行类型检查。使用 pluggy documentation:
中的这个简化示例import pluggy # type: ignore
hookspec = pluggy.HookspecMarker("myproject")
hookimpl = pluggy.HookimplMarker("myproject")
class MySpec(object):
"""A hook specification namespace."""
@hookspec
def myhook(self, arg1, arg2):
"""My special little hook that you can customize."""
class Plugin_1(object):
"""A hook implementation namespace."""
@hookimpl
def myhook(self, arg1, arg2):
print("inside Plugin_1.myhook()")
return arg1 + arg2 + "a" # intentional error
# create a manager and add the spec
pm = pluggy.PluginManager("myproject")
pm.add_hookspecs(MySpec)
# register plugins
pm.register(Plugin_1())
# call our `myhook` hook
# intentional incompatible type for parameter arg2
results = pm.hook.myhook(arg1=1, arg2="1")
print(results)
我认为正确有效的注释应该是:
def myhook(self, arg1: int, arg2: int) -> int: ...
我尝试将此注释添加到 hookspec。如我所料,这是行不通的。我相信这是因为 pluggy 实现的间接寻址是动态的。代码必须是 运行 以便 PluginManager
的 add_hookspecs()
方法可以定义可用的钩子。
我看到 pm.hook
是 pluggy.hooks._HookRelay
类型,pm.hook.myhook
是 pluggy.hooks._HookCaller
的一个实例,它有一个 __call__()
方法。
我尝试使用 stubgen
为 pluggy 制作一组 .pyi
文件,然后以两种不同的方式将注释添加到 pluggy.hooks._HookCaller
:
class _HookCaller:
def __init__(self, trace: Any) -> None: ...
def myhook(self, arg1: int, arg2: int) -> int: ...
def __call__(self, arg1: int, arg2: int) -> int: ...
当我执行 MYPYPATH=./stubs mypy --verboes example.py
时,我可以看到 hooks.pyi
正在被解析,但未检测到参数类型不匹配。即使我从 import pluggy
.
# type: ignore
注释,此行为也是一致的
问题:
- 是否可以将
myhook()
挂钩的类型注释定义为外部.pyi
文件? - 如果是这样,那个
.pyi
文件会包含什么?我应该把它存储在哪里,以便mypy
在类型检查 运行 时提取它? - 是否可以进行注释,使挂钩实现者和挂钩调用者都获得有用的类型提示?
第一个问题是 @hookspec
消除了 myhook
方法的类型提示:
from typing import TypeVar, Callable, Any, cast
# Improvement suggested by @oremanj on python/typing gitter
F = TypeVar("F", bound=Callable[..., Any])
hookspec = cast(Callable[[F], F], pluggy.HookspecMarker("myproject"))
该解决方法取消了对外部 .pyi
文件的要求。只需使用现有的钩子规范来定义类型提示。这解决了问题 1 和问题 2:您不需要 .pyi
文件。就用typing.cast()
给mypy一个提示,它不能从静态分析中学习:
# Add cast so that mypy knows that pm.hook
# is actually a MySpec instance. Without this
# hint there really is no way for mypy to know
# this.
pm.hook = cast(MySpec, pm.hook)
这可以通过添加注释来检查:
# Uncomment these when running through mypy to see
# how mypy regards the type
reveal_type(pm.hook)
reveal_type(pm.hook.myhook)
reveal_type(MySpec.myhook)
运行 这通过 mypy:
plug.py:24: error: Unsupported operand types for + ("int" and "str")
plug.py:42: error: Revealed type is 'plug.MySpec'
plug.py:43: error: Revealed type is 'def (arg1: builtins.int, arg2: builtins.int) -> builtins.int'
plug.py:44: error: Revealed type is 'def (self: plug.MySpec, arg1: builtins.int, arg2: builtins.int) -> builtins.int'
plug.py:47: error: Argument "arg2" to "myhook" of "MySpec" has incompatible type "str"; expected "int"
现在 mypy
捕获了 hook 调用者和 hook 实现的类型问题 (Q3)!
完整代码:
import pluggy # type: ignore
from typing import TypeVar, Callable, Any, cast
# Improvement suggested by @oremanj on python/typing gitter
F = TypeVar("F", bound=Callable[..., Any])
hookspec = cast(Callable[[F], F], pluggy.HookspecMarker("myproject"))
hookimpl = pluggy.HookimplMarker("myproject")
class MySpec(object):
"""A hook specification namespace."""
@hookspec
def myhook(self, arg1: int, arg2: int) -> int:
"""My special little hook that you can customize."""
class Plugin_1(object):
"""A hook implementation namespace."""
@hookimpl
def myhook(self, arg1: int, arg2: int) -> int:
print("inside Plugin_1.myhook()")
return arg1 + arg2 + 'a'
# create a manager and add the spec
pm = pluggy.PluginManager("myproject")
pm.add_hookspecs(MySpec)
# register plugins
pm.register(Plugin_1())
# Add cast so that mypy knows that pm.hook
# is actually a MySpec instance. Without this
# hint there really is no way for mypy to know
# this.
pm.hook = cast(MySpec, pm.hook)
# Uncomment these when running through mypy to see
# how mypy regards the type
# reveal_type(pm.hook)
# reveal_type(pm.hook.myhook)
# reveal_type(MySpec.myhook)
# this will now be caught by mypy
results = pm.hook.myhook(arg1=1, arg2="1")
print(results)
Brad 的回答中的很多事情都可以在 pluggy.pyi 文件中完成。我有 pluggy.pyi:
的这个(可能非常不完整)内容from types import ModuleType
from typing import Callable, Type, TypeVar, Generic, Any
F = TypeVar("F", bound=Callable[..., Any])
class HookspecMarker:
def __init__(self, name: str) -> None:
...
def __call__(self, func: F) -> F:
...
class HookimplMarker:
def __init__(self, name: str) -> None:
...
def __call__(self, func: F) -> F:
...
Spec = TypeVar("Spec")
class PluginManager(Generic[Spec]):
def __init__(self, name: str) -> None:
...
def load_setuptools_entrypoints(self, name: str) -> None:
...
def add_hookspecs(self, module: Type[Spec]) -> None:
...
def register(self, module: ModuleType) -> None:
...
hook: Spec
这允许我创建一个插件管理器,如下所示:
import pluggy
from typing import TYPE_CHECKING
from .hookspecs import PluginSpec
from .plugins import localplugins
if TYPE_CHECKING:
PluginManager = pluggy.PluginManager[PluginSpec]
else:
PluginManager = pluggy.PluginManager
plugin: PluginManager = pluggy.PluginManager("mypackage")
plugin.add_hookspecs(PluginSpec)
plugin.load_setuptools_entrypoints("mypackage")
plugin.register(localplugins)
hookspec 需要是静态的 class 方法:
from typing import Any
import pluggy
hookspec = pluggy.HookspecMarker("mypackage")
class PluginSpec:
@staticmethod
@hookspec
def plugin_func(*args: Any) -> None:
...