Skip to content

Code Analysis Pluggy

Tu Shaokun edited this page Nov 5, 2026 · 1 revision

pluggy 的跨文件回调与注册时机

上游 PR #695 修复了一个筛选错误。插件可以把名为 alias 的方法注册到另一个名字的钩子。旧代码用插件上有没有同名属性来决定该排除谁,漏掉了这个别名。新版改查该钩子已经登记的插件对象。上游差异与回归测试

哪些位置可以静态核实?

要查的事 #695 后版源码
登记的是哪个方法? _manager.py:221–240 读取标记,创建 HookImpl,用 specname 或属性名选定钩子。_hooks.py:696 保存函数。
谁从名单里排除插件? _manager.py:603–619 用当前注册对象筛选。_hooks.py:650–655 在读取实现列表时排除这些对象。
到底从哪里执行? _hooks.py:530–545 把名单交给执行器,_manager.py:173–184 默认转交给 _multicall。普通插件方法在 _callers.py:126 被调用。_hooks.py、_callers.py 在这次 PR 中均未改动。

用同一公开 Labs 提交(bf0a29e7)检查 pluggy 变更前后,每版覆盖 31 个 Python 文件,记录 235 条导入。逐处用固定源码和 Python 模块查找结果比对,50 个指向 pluggy 或其子模块的绝对导入位置全部吻合,其中 46 个在测试目录。Labs 能用限定名区分新测试里的同名方法。Python 调用记录为 0,单凭这些输出无法确定某次分派实际选择的插件。注册与筛选 · 普通调用位置

在同一测试文件搜索 def he_method1( 有 52 个声明命中。沿变更中实际使用的字段查找注册、筛选和调用,9 处源码命中分布在三个实现文件,其中包括包装器的其他分支。这个对照记录可到达的位置,未测量读者用时。本地双版本研究工具 c3dddd5c 的变更选择限于 JS/TS,在这次纯 Python 变更上选中 0 个文件。

注册时机如何改变结果?

先写下源码预期,再把同一探针分别放在三个干净进程中运行。另一个也叫 run 的方法被注册给 other,三个版本都只在调用 other 时执行它。

运行输入 #695 前 2e916c21 #695 后 3d5417a4 上游后续 #827 的固定提交 f3974a10
已注册别名与普通实现,再排除别名插件 keep、alias keep keep
先把普通插件列为排除项,再注册它 空 late 空
不筛选,调用已注册的普通插件 late late late

同一行分派代码可以执行上述任何一组结果。判断具体目标要知道钩子名、注册对象,以及创建筛选器时插件是否已经注册。#827 一列只记录该提案固定提交的运行行为。后续提案

用 Git 和 Python 3.13 复现

在临时目录抓取固定提交。脚本直接读取各版本的 src,只依赖 Python 标准库。每个提交运行一个独立进程。

set -eu
w=$(mktemp -d)
git init -q "$w/source"
cat > "$w/probe.py" <<'PYPROBE'
import json
from pathlib import Path
import sys

root = Path(sys.argv[1]).resolve()
src = root / "src"
sys.path.insert(0, str(src))
import pluggy

assert Path(pluggy.__file__).resolve() == src / "pluggy" / "__init__.py"
spec = pluggy.HookspecMarker("trial")
impl = pluggy.HookimplMarker("trial")

class Hooks:
    @spec
    def run(self):
        pass

    @spec
    def other(self):
        pass

class Alias:
    @impl(specname="run")
    def alternate(self):
        return "alias"

class Keep:
    @impl
    def run(self):
        return "keep"

class Decoy:
    @impl(specname="other")
    def run(self):
        return "decoy"

class Late:
    @impl
    def run(self):
        return "late"

def manager():
    pm = pluggy.PluginManager("trial")
    pm.add_hookspecs(Hooks)
    return pm

pm = manager()
alias = Alias()
for item in (alias, Keep(), Decoy()):
    pm.register(item)
registered = pm.subset_hook_caller("run", [alias])
late_pm = manager()
late = Late()
before_registration = late_pm.subset_hook_caller("run", [late])
late_pm.register(late)
out = {
    "full": pm.hook.run(), "subset": registered(), "other": pm.hook.other(),
    "lateSubset": before_registration(), "lateFull": late_pm.hook.run(),
}
assert out["full"] == ["keep", "alias"] and out["other"] == ["decoy"]
assert out["lateFull"] == ["late"]
print(json.dumps(out, sort_keys=True))
PYPROBE
for sha in 2e916c21f3e5b60865d3f1f2d6f0932d61c77707 3d5417a4eff449e17e5bb3e1545b63cbf0e0c768 f3974a104f4112ac14e98bd04f0430408f536d8e; do
  git -C "$w/source" fetch -q --depth=1 https://github.com/pytest-dev/pluggy.git "$sha"
  git -C "$w/source" checkout -q --detach FETCH_HEAD
  python3.13 -I -B -S "$w/probe.py" "$w/source"
done

返回团队验证记录

Clone this wiki locally