【pytest框架源码分析四】pluggy源码分析之hook执行

pluggy的主要执行方法在_callers.py中,这里简单介绍下。

python 复制代码
def _multicall(
    hook_name: str,
    hook_impls: Sequence[HookImpl],
    caller_kwargs: Mapping[str, object],
    firstresult: bool,
) -> object | list[object]:
    """Execute a call into multiple python functions/methods and return the
    result(s).

    ``caller_kwargs`` comes from HookCaller.__call__().
    """
    __tracebackhide__ = True
    results: list[object] = []
    exception = None
    only_new_style_wrappers = True
    try:  # run impl and wrapper setup functions in a loop
        teardowns: list[Teardown] = []
        try:
            for hook_impl in reversed(hook_impls):
                try:
                    args = [caller_kwargs[argname] for argname in hook_impl.argnames]
                except KeyError:
                    for argname in hook_impl.argnames:
                        if argname not in caller_kwargs:
                            raise HookCallError(
                                f"hook call must provide argument {argname!r}"
                            )

                if hook_impl.hookwrapper:
                    only_new_style_wrappers = False
                    try:
                        # If this cast is not valid, a type error is raised below,
                        # which is the desired response.
                        res = hook_impl.function(*args)
                        wrapper_gen = cast(Generator[None, Result[object], None], res)
                        next(wrapper_gen)  # first yield
                        teardowns.append((wrapper_gen, hook_impl))
                    except StopIteration:
                        _raise_wrapfail(wrapper_gen, "did not yield")
                elif hook_impl.wrapper:
                    try:
                        # If this cast is not valid, a type error is raised below,
                        # which is the desired response.
                        res = hook_impl.function(*args)
                        function_gen = cast(Generator[None, object, object], res)
                        next(function_gen)  # first yield
                        teardowns.append(function_gen)
                    except StopIteration:
                        _raise_wrapfail(function_gen, "did not yield")
                else:
                    res = hook_impl.function(*args)
                    if res is not None:
                        results.append(res)
                        if firstresult:  # halt further impl calls
                            break
        except BaseException as exc:
            exception = exc
    finally:
        # Fast path - only new-style wrappers, no Result.
        if only_new_style_wrappers:
            if firstresult:  # first result hooks return a single value
                result = results[0] if results else None
            else:
                result = results

            # run all wrapper post-yield blocks
            for teardown in reversed(teardowns):
                try:
                    if exception is not None:
                        teardown.throw(exception)  # type: ignore[union-attr]
                    else:
                        teardown.send(result)  # type: ignore[union-attr]
                    # Following is unreachable for a well behaved hook wrapper.
                    # Try to force finalizers otherwise postponed till GC action.
                    # Note: close() may raise if generator handles GeneratorExit.
                    teardown.close()  # type: ignore[union-attr]
                except StopIteration as si:
                    result = si.value
                    exception = None
                    continue
                except BaseException as e:
                    exception = e
                    continue
                _raise_wrapfail(teardown, "has second yield")  # type: ignore[arg-type]

            if exception is not None:
                raise exception.with_traceback(exception.__traceback__)
            else:
                return result

        # Slow path - need to support old-style wrappers.
        else:
            if firstresult:  # first result hooks return a single value
                outcome: Result[object | list[object]] = Result(
                    results[0] if results else None, exception
                )
            else:
                outcome = Result(results, exception)

            # run all wrapper post-yield blocks
            for teardown in reversed(teardowns):
                if isinstance(teardown, tuple):
                    try:
                        teardown[0].send(outcome)
                    except StopIteration:
                        pass
                    except BaseException as e:
                        _warn_teardown_exception(hook_name, teardown[1], e)
                        raise
                    else:
                        _raise_wrapfail(teardown[0], "has second yield")
                else:
                    try:
                        if outcome._exception is not None:
                            teardown.throw(outcome._exception)
                        else:
                            teardown.send(outcome._result)
                        # Following is unreachable for a well behaved hook wrapper.
                        # Try to force finalizers otherwise postponed till GC action.
                        # Note: close() may raise if generator handles GeneratorExit.
                        teardown.close()
                    except StopIteration as si:
                        outcome.force_result(si.value)
                        continue
                    except BaseException as e:
                        outcome.force_exception(e)
                        continue
                    _raise_wrapfail(teardown, "has second yield")

            return outcome.get_result()

其前面的调用过程可参考前面几篇,这里主要介绍下_multicall方法本身。

其入参如下:

hook_name:hook的name,为我们所写方法的名称

hook_impls:hook的impl列表,impl中包括插件,插件的方法及其配置,一个方法一个impl

caller_kwargs:调用hook方法时的入参,即我们编写impl方法本身的入参

firstresult:是否只需要首次结果,bool类型

下面是方法中的正式流程

1.首先定义了个teardown的list,这个是teardown的定义。这里teardown主要是为了生成器函数准备的,在我们调用插件时,如果希望有些函数中某些步骤在所有方法最后执行,则在函数中加入yield,并且wrapper设置成true。则该函数yield前的步骤先按顺序执行,yield之后的在所有函数执行完成后执行。

python 复制代码
Teardown = Union[
    Tuple[Generator[None, Result[object], None], HookImpl],
    Generator[None, object, object],
]

2.遍历hook_impls,注意这里reversed,将hook_impls反转了,这是为什么先添加的hook_impl后执行,后添加的先执行。

3.获取调用时的所有args,如果缺少参数,则报错。

4.接下来判断hookwrapper和wrapper参数是否为true,如果为true,这两个处理过程类似。都是先执行yield之前的内容,然后返回生成器函数放在teardowns里,留待所有函数执行完成再执行。如果不为true,则正常执行函数,并把返回值放到results中。如果只取第一次结果,则直接break跳出循环。

5.然后到finally,这里主要是处理生成器函数,即上方hookwrapper和wrapper参数为true的情况。

(1)如果是wrappers是true,先根据firstresult取result,然后再取teardowns中的内容。如果上面的执行有异常,则在这里抛出异常;如果没有异常,则继续执行send()方法,这一步大都会抛出异常,因为函数中只会有一个yield,如果这边不抛出异常,说明函数中有两个yeild,后面会提示报错"has second yield"。最后返回result(如果没有声明wrappers和hookwrapper参数,也是走该路径)--注意我们编写的方法中只能有一个yield。

(2)如果hookwrapper是true,也是先判断是否取firstresult,然后和上面的exception组成Result类型的outcome。接下来和上面类似,取teardowns中的内容,这里多个判断isinstance(teardown, tuple),主要和上面的处理有关,整体是一致的,有异常抛出异常,无异常则继续执行。最后返回result。

这边wrappers路径的处理过程要比hookwrapper简单。我们后面可以用wrappers参数来处理生成器函数。

相关推荐
厦门辰迈智慧科技有限公司2 小时前
城市综合管廊监测与维护一体化解决方案
物联网·自动化·监测
sy_cora6 小时前
IEEE 列表会议第五届机器人、自动化与智能控制国际会议
运维·人工智能·机器人·自动化
云手机管家8 小时前
CDN加速对云手机延迟的影响
运维·服务器·网络·容器·智能手机·矩阵·自动化
云手机管家8 小时前
账号风控突破:云手机设备指纹篡改检测与反制技术解析
android·运维·网络协议·网络安全·智能手机·矩阵·自动化
bjwuzh12 小时前
使用pytest实现参数化后,控制台输出的日志是乱码
pytest
struggle202519 小时前
AgenticSeek开源的完全本地的 Manus AI。无需 API,享受一个自主代理,它可以思考、浏览 Web 和编码,只需支付电费。
人工智能·开源·自动化
北漂老男孩19 小时前
ChromeDriver 技术生态与应用场景深度解析
java·爬虫·python·自动化
逸雨清风1 天前
Chrome更新到136以后selenium等自动化浏览器失效
selenium·自动化
狮智先生1 天前
【学习笔记】点云自动化聚类简要总结
笔记·学习·自动化
国科安芯1 天前
高精度降压稳压技术在现代工业自动化中的应用
运维·自动化