【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参数来处理生成器函数。

相关推荐
@TangXin1 小时前
单元测试-pytest框架实践
自动化测试·单元测试·pytest
gs801409 小时前
OpenWebUI提示器:Prompt工程的“智能助手”还是“自动化革命”?
人工智能·自动化·prompt
大囚长10 小时前
deepseek助力运维和监控自动化
运维·自动化
T0uken10 小时前
【C++】使用 CMake 在 Windows 上自动化发布 C++/Qt 应用程序
c++·windows·自动化
木觞清15 小时前
使用 Selenium 和 Requests 自动化获取动态 Referer 和 Sign 的完整指南
selenium·测试工具·自动化
明达技术19 小时前
分布式 IO :开启垃圾分类机械臂自动化新篇章
运维·分布式·自动化
刘小哈哈哈1 天前
iOS UICollectionViewCell 点击事件自动化埋点
ios·自动化·cocoa
小han的日常1 天前
UI自动化框架介绍
运维·ui·自动化
俗人咖2 天前
「Selenium+Python自动化从0到1③|2025最新八种元素定位方法实战解析(附验证代码,定位详情图,百度实战示例图)」
python·selenium·自动化