pytest 6.2.1 发布解读:bug 修复版本中的两个关键修复(approx 数组协议回归与 Windows UNC 导入路径)
【免费下载链接】pytestThe pytest framework makes it easy to write small tests, yet scales to support complex functional testing项目地址: https://gitcode.com/GitHub_Trending/py/pytest
pytest 6.2.1 是 pytest 官方于 2020 年 12 月 15 日发布的一个纯 bug 修复版本(bug-fix release),作为 6.2.0 的 drop-in replacement(可直接替换安装)。本文基于本仓库中的发布公告 release-6.2.1.rst 与完整变更日志 changelog.rst 展开,深入解析该版本修复的两个核心问题——approx对数组协议(array protocol)类对象的比较回归、以及 Windows UNC 路径下测试模块导入时报ImportPathMismatchError的缺陷,并结合仓库源码与测试用例说明其底层原理。读完本文,你将理解这两个 bug 的成因、修复思路,以及如何验证与规避类似问题。
版本概况与升级方式
本仓库的发布公告文档 release-6.2.1.rst 明确指出,pytest 6.2.1 是一个 bug-fix release,且是 6.2.0 的直接替代版本,升级不会带来破坏性变更。升级命令非常简单:
pip install --upgrade pytest该版本的完整变更日志记录在仓库文档 changelog.rst 中,标题为 "pytest 6.2.1 (2020-12-15)",其中共包含两项 Bug Fixes:
:issue:7678``:修复在 Windows 上通过 UNC 挂载路径(UNC mounted path)加载宿主机器上编译文件时误抛ImportPathMismatchError的问题。:issue:8132``:修复approx的回归问题——6.2.0 中approx对非数值类型不再抛出TypeError,而是回退到普通比较;但对于 TensorFlowtf.DeviceArray等数组类对象,6.2.0 开始反而比较失败。
两项修复虽然看似独立,但都指向 pytest 内部两个核心机制:测试模块导入(import_path)与近似值比较(approx)。下面逐一深入。
修复一:Windows UNC 路径下的 ImportPathMismatchError
问题背景
pytest 在收集测试文件时,会通过import_path()函数将测试模块导入 Python。为了确保导入的模块与磁盘上的文件对应一致,pytest 会在导入后校验模块的__file__属性与实际路径是否匹配;不一致时抛出ImportPathMismatchError。
该异常定义在 pathlib.py 中:
class ImportPathMismatchError(ImportError): """Raised on import_path() if there is a mismatch of __file__'s. This can happen when `import_path` is called multiple times with different filenames that has the same basename but reside in packages (for example "/tests1/test_foo.py" and "/tests2/test_foo.py"). """从源码注释可以看到,该异常原本设计用于捕获“不同目录下存在同名测试模块”导致的模块名冲突场景。但在 6.2.1 之前的某些 Windows 环境下,宿主机器上编译的.pyc缓存文件从 UNC 挂载路径加载时,路径规范化(normalization)的结果可能与原始路径不一致,导致 pytest 误判为路径不匹配而抛出该异常,进而导致测试收集失败。
导入机制与三种模式
修复的核心围绕 import_path() 展开。该函数支持三种导入模式(ImportMode,见 pathlib.py):
| 模式 | 说明 |
|---|---|
prepend | 将包含被测模块(或包)的目录置于sys.path开头后再以importlib.import_module导入 |
append | 与prepend类似,但目录被追加到sys.path末尾(若尚未在路径中) |
importlib | 使用importlib提供的更细粒度机制导入,完全不改动sys.path,从而允许在不同位置存在同名测试模块 |
从源码看,ImportPathMismatchError只在prepend和append两种模式下抛出(见 pathlib.py 中 "Only raised inprependandappendmodes" 的说明),因为这两种模式依赖模块在sys.modules中的唯一名称,一旦磁盘路径与模块__file__对不上,就说明存在同名冲突。而importlib模式为每个模块基于root锚点生成唯一名称,天然规避了该问题。
pytest 在 python.py 中导入了ImportPathMismatchError,并在收集测试模块时捕获处理(python.py)。6.2.1 的修复即针对import_path内部对路径比较逻辑的校正,使 UNC 路径下规范化后的__file__不再触发误报。
规避建议
如果你在 Windows 上通过 UNC 挂载路径运行 pytest 且遇到此类收集异常,可以:
- 升级到 6.2.1 或更高版本(本修复已包含);
- 或在
pytest.ini/pyproject.toml中为收集阶段显式指定导入模式,例如使用importlib模式:
# pytest.ini [pytest] consider_namespace_packages = true说明:
importlib模式是避免同名模块冲突最彻底的方式,也是 pytest 后续版本演进的方向。
修复二:approx 对数组协议类对象的比较回归
问题成因
approx是 pytest 内置的浮点数近似比较工具。6.2.0 之前,像tf.DeviceArray这类既不属于 Python 标准数值类型层级、也不继承自numpy.ndarray的数组类对象,会“落入”标量(scalar)分支,恰好能在单元素场景下与标量正确比较;6.2.0 调整了类型分发逻辑后,这类对象既不能走标量分支,也没有被当作 numpy 数组处理,导致比较失败(见 changelog.rst)。
修复方案:识别数组协议
6.2.1 的修复方案是:只要对象暴露了数组协议(array protocol)且不是标量,就将其转换为numpy.ndarray再参与比较。这一逻辑实现在 approx.py 的_as_numpy_array()辅助函数中:
def _as_numpy_array(obj: object) -> ndarray | None: """ Return an ndarray if the given object is implicitly convertible to ndarray, and numpy is already imported, otherwise None. """ np: Any = sys.modules.get("numpy") if np is not None: # avoid infinite recursion on numpy scalars, which have __array__ if np.isscalar(obj): return None elif isinstance(obj, np.ndarray): return obj elif hasattr(obj, "__array__") or hasattr(obj, "__array_interface__"): return np.asarray(obj) return None关键细节:
- 通过
sys.modules.get("numpy")判断 numpy 是否已加载,避免强制引入 numpy 依赖; - 先排除 numpy 标量(
np.isscalar),防止标量自带的__array__方法造成无限递归; - 对
np.ndarray直接返回;对实现了__array__或__array_interface__的对象(如 TensorFlowDeviceArray),调用np.asarray(obj)转为 ndarray; - 该函数在 approx.py 的
ApproxBase比较流程中被调用,expected侧也会经过同样转换,因此approx(ndarray)、approx(DeviceScalar(...))等组合均能正常工作。
测试用例佐证
仓库测试 testing/python/approx.py 中专门保留了针对该回归的回归测试test_numpy_array_protocol,其注释直接标注 "See issue #8132",并在测试中构造了模拟tf.DeviceArray的类:
class DeviceArray: def __init__(self, value, size): self.value = value self.size = size def __array__(self): return self.value * np.ones(self.size) expected = 1 actual = 1 + 1e-6 assert approx(expected) == DeviceArray(actual, size=1) assert approx(expected) == DeviceArray(actual, size=2)同时,测试 testing/python/approx.py 的test_numpy_array_implicit_conversion验证了仅实现__array__的“隐式可转换”类型也能正确参与approx比较,说明修复对数组协议类对象是通用的,而非仅针对单一框架。
使用建议
approx完整用法可参考其文档字符串(approx.py):它支持标量、列表、字典、numpy数组等类型,默认相对容差rtol=1e-5、绝对容差atol=1e-8,等价于numpy.isclose的默认行为。当被测对象是tf.DeviceArray、torch.Tensor(通过__array__)等第三方框架的数组时,直接与approx比较即可,无需手动调用np.asarray转换:
import pytest def test_device_array(): expected = 1.0 actual = 1.0 + 1e-6 # 模拟框架返回的数组类对象 assert approx(expected) == actual如何验证当前仓库中的相关代码
如果你想在本仓库中亲身体验这两个修复:
- 查看异常定义与导入逻辑:阅读 pathlib.py,重点对比
prepend/append与importlib三种模式对sys.path与sys.modules的处理差异; - 查看 approx 修复:阅读 approx.py 的
_as_numpy_array,并运行对应回归测试:
python -m pytest testing/python/approx.py -k "array_protocol or implicit_conversion"- 查看完整变更日志:changelog.rst 中保留了 6.2.1 的全部修复记录,可作为历史版本行为的权威参考。
小结
pytest 6.2.1 是一个小而精的 bug-fix 版本:approx的数组协议支持让tf.DeviceArray等第三方数组对象重新“可用且可测”,Windows UNC 路径下的导入误报修复则提升了企业级共享目录场景下的稳定性。两个修复都体现了 pytest 工程实践中的两个重要原则:类型分发要基于对象能力(protocol/duck-typing)而非继承层级,以及路径导入要区分环境差异并保证失败信息可诊断。理解这两个案例,也有助于你在编写测试框架或插件时规避同类陷阱。
Happy testing.
【免费下载链接】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),仅供参考