ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

pytest 插件启动加载失败的正确分类与退出码:从裸回溯到 USAGE_ERROR(4) / INTERNAL_ERROR(3)

pytest 插件启动加载失败的正确分类与退出码:从裸回溯到 USAGE_ERROR(4) / INTERNAL_ERROR(3) pytest 插件启动加载失败的正确分类与退出码从裸回溯到 USAGE_ERROR(4) / INTERNAL_ERROR(3)【免费下载链接】pytestThe pytest framework makes it easy to write small tests, yet scales to support complex functional testing项目地址: https://gitcode.com/GitHub_Trending/py/pytestpytest 在启动阶段加载插件时一旦发生异常过去会直接抛出原始回溯并以偶然的退出码1结束导致脚本、CI 与调用方无法区分用错了命令与插件自身有缺陷这两类截然不同的故障。本篇文章基于当前仓库中 changelog/993.breaking.rst 所记录的破坏性变更完整讲解 pytest 如何将插件导入失败细分为USAGE_ERROR4与INTERNAL_ERROR3两类并深入_pytest.config源码与 acceptance 测试说明底层判定逻辑、pytest.main()返回值行为变化以及读者在-p、pytest_plugins、PYTEST_PLUGINS、pytest11entry point 各场景下应如何据此排查问题。背景旧行为为什么危险在本次变更之前插件在启动阶段导入失败时会以原始回溯的形式逃逸escaping as a raw traceback进程以附带产生的退出码1TESTS_FAILED退出。这在两个维度上带来问题错误分类缺失1在 pytest 的语义里是测试失败一个插件导入错误与一批普通断言失败在退出码上完全无法区分CI 与上层脚本只能靠解析 stderr 文本猜测原因偶然性退出码1并非插件加载失败的设计结果而是异常未捕获、从主流程裸奔出去后的副产品行为不可预测且不可编程化处理。此外当被导入的插件抛出一个不带任何参数的异常例如裸raise ImportError时pytest 内部还会触发一个IndexError进一步掩盖真实错误。新行为把插件加载失败分成两类依据 changelog/993.breaking.rst本次变更将启动期的插件导入失败明确划分为两种语义并分别映射到 pytest.ExitCode 枚举中的不同退出码场景触发条件退出码错误语义插件找不到通过-p、pytest_plugins或PYTEST_PLUGINS指定的模块根本不存在USAGE_ERROR4用户用法错误等价于conftest.py导入失败插件找到但导入时抛异常插件模块存在但模块顶层代码报错包括损坏的pytest11entry point、或插件缺少其依赖的第三方模块INTERNAL_ERROR3插件自身的缺陷保留完整回溯便于上报两种情形的核心差异在于pytest 是否被指到了一个不存在的东西被指向不存在的模块说明是使用者配置错误 → 用法错误USAGE_ERROR4模块明明存在却在自己导入时崩溃说明是插件作者的代码问题 → 内部错误INTERNAL_ERROR3。这与官方文档 doc/en/reference/exit-codes.rst 中更新的退出码说明保持一致退出码 3 涵盖执行测试时发生内部错误或插件在导入时抛异常退出码 4 涵盖命令行用法错误包括找不到的插件或导入失败的conftest.py。插件从哪些入口被加载理解分类逻辑前先确认 pytest 在启动阶段会从哪些来源加载插件。对应实现位于 src/_pytest/config/init.py 的PytestPluginManager-p/--plugins命令行参数由consider_pluginarg()src/_pytest/config/init.py处理支持-p no:xxx禁用某个插件PYTEST_PLUGINS环境变量由consider_env()src/_pytest/config/init.py读取值为逗号分隔的插件名列表pytest_plugins模块级变量由consider_module()src/_pytest/config/init.py读取_get_plugin_specs_as_list()src/_pytest/config/init.py统一把字符串、逗号分隔字符串与序列解析成插件名列表pytest11entry point自动加载的已安装插件通过load_setuptools_entrypoints(pytest11, ...)src/_pytest/config/init.py加载。上述所有入口最终都会汇聚到import_plugin()方法src/_pytest/config/init.py因此新分类逻辑对四种来源一视同仁。源码级判定import_plugin()如何区分两类失败import_plugin()中真正决定分类的是对ModuleNotFoundError的精细化处理其逻辑与辅助函数_is_missing_module()src/_pytest/config/init.py配合完成except ModuleNotFoundError as e: if _is_missing_module(e, importspec): # 插件本身无处可寻——pytest 被指向了一个不存在的目标属于用法错误。 raise UsageError(fError importing plugin {modname}: {e}) from e # 插件导入的*其他*模块缺失插件已被找到缺陷在插件自身而非用法。 raise PluginImportFailure(modname) from e except UsageError: raise except Exception as e: raise PluginImportFailure(modname) from e_is_missing_module()的判定规则是报错模块名等于被请求的importspec或被请求的importspec以报错模块名开头即被请求的是某个缺失父包下的子模块此时说明要找的东西本身不存在归类为用法错误反之若ModuleNotFoundError报的是其他模块插件自己的依赖没装则说明插件已被定位归类为插件缺陷内部错误。其余任何Exception非UsageError、非Skipped都会包装成PluginImportFailure抛出特别地Skipped异常会走skipped_plugins列表单独记录而插件显式抛出的pytest.UsageError会被原样保留其用法错误语义。顶层处理与pytest.main()返回值变化分类完成后异常需要被捕获并转换成退出码。pytest 为此定义了两个专门异常类ConftestImportFailuresrc/_pytest/config/init.py携带失败路径与底层causePluginImportFailuresrc/_pytest/config/init.py文档字符串明确写道插件被找到但在导入时抛异常与插件完全找不到必须刻意区分——找不到是UsageError而导入崩溃是插件缺陷、按内部错误上报。这两个类共同服务于主流程_main()src/_pytest/config/init.pytry: config _prepareconfig(new_args, plugins, progprog) except ConftestImportFailure as e: print_conftest_import_error(e, filesys.stderr) return ExitCode.USAGE_ERROR except PluginImportFailure as e: print_plugin_import_error(e, filesys.stderr) return ExitCode.INTERNAL_ERROR公共入口pytest.main()src/_pytest/config/init.py因此不再抛出ImportError而是直接返回对应的ExitCode。这是文档明确强调的破坏性变更任何以编程方式调用pytest.main()并依赖插件导入失败抛异常的代码都需要改为检查返回值。相关测试在 testing/acceptance_test.py 中专门验证了这一点向pytest.main(..., plugins[invalid.module])传入不存在的插件时返回值恰为ExitCode.USAGE_ERROR而非抛异常。错误输出本身则由print_plugin_import_error()src/_pytest/config/init.py与print_conftest_import_error()src/_pytest/config/init.py负责二者共用_print_import_error()src/_pytest/config/init.py用红色输出一行标题Error while loading plugin ...或ImportError while loading conftest ...随后通过filter_traceback_for_import_failure()src/_pytest/config/init.py过滤掉指向 pytest 内部与 importlib 的栈帧只保留用户代码部分再以short风格打印回溯——这正是文档所述保留 traceback的实现机制。附带修复裸raise ImportError不再触发IndexError文档还提到本次变更顺带修复了 pytest 内部的一个IndexError当插件抛出不带任何参数的异常时如raise ImportError旧代码在解包异常参数时会发生越界。修复后此类异常统一按INTERNAL_ERROR3处理。对应的回归测试位于 testing/acceptance_test.py构造一个内容为raise ImportError的插件并用-p myplugin加载断言退出码为INTERNAL_ERROR、stderr 中不出现IndexError、且出现Error while loading plugin myplugin.标题。测试矩阵六种入口 × 两类结果pytest 用TestStartupPluginImportErrors测试类testing/acceptance_test.py系统性地覆盖了分类逻辑是理解本行为最直观的行为规范入口插件缺失插件损坏-p nameUSAGE_ERROR4stderr 含Error importing plugin nosuchpluginINTERNAL_ERROR3stderr 含Error while loading plugin与回溯conftest.py中pytest_plugins [...]USAGE_ERROR4INTERNAL_ERROR3PYTEST_PLUGINS环境变量USAGE_ERROR4INTERNAL_ERROR3pytest11entry point—INTERNAL_ERROR3见test_broken_via_entry_point几个容易误解的边界情况在测试中都有明确结论conftest.py导入失败仍然是用法错误4conftest.py不是插件不适用内部错误分类见test_conftest_import_failure_stays_a_usage_error插件缺少依赖 ≠ 用法错误插件存在但import nosuchdependency失败属于插件缺陷返回INTERNAL_ERROR见test_missing_dependency_is_not_a_usage_error父包存在但子模块不存在-p mypkg.nosuchmodule仍按找不到处理返回USAGE_ERROR见test_missing_submodule_of_existing_package插件显式抛pytest.UsageError保持用法错误语义透传输出ERROR: config trouble见test_usage_error_passes_through。实用排查指引掌握新分类后实际排错可以按以下步骤快速定位看退出码返回4USAGE_ERROR说明 pytest 被指到了不存在的插件或conftest.py无法导入——检查-p参数拼写、pytest_plugins列表与PYTEST_PLUGINS环境变量是否有误返回3INTERNAL_ERROR说明插件自身或其依赖有问题——检查插件顶层的import、初始化代码以及已安装插件的pytest11entry point 是否指向了已损坏/未安装的模块。看 stderr 标题行Error importing plugin ...对应缺失用法错误Error while loading plugin ...对应导入崩溃内部错误ImportError while loading conftest ...对应conftest.py失败。编程调用时检查返回值调用pytest.main()后与 pytest.ExitCode 枚举比对from pytest import ExitCode不要再依赖捕获ImportError。复现与最小化可用python -m pytest -p 插件名直接验证单个插件若怀疑是pytest11自动加载的第三方插件所致可结合PYTEST_DISABLE_PLUGIN_AUTOLOAD环境变量分批排查。总之这一变更把启动期插件故障从不可编程的裸回溯收敛为语义清晰、可机器判断的退出码分类体系让 pytest 的使用者、插件作者和 CI 流水线都能准确区分用错了与插件坏了。【免费下载链接】pytestThe pytest framework makes it easy to write small tests, yet scales to support complex functional testing项目地址: https://gitcode.com/GitHub_Trending/py/pytest创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表