一个库声称支持 Python 3.10 到 3.14,谁来保证?靠 tox 或 nox:它们为每个版本建一个干净的虚拟环境,在里面跑同一套命令。这篇用同一份「3.14 才通得过」的检查,分别用两个工具跑一遍,对比它们的配置方式、产物和速度。
feat.py(检查内容) def features(): out = {"python": ".".join(map(str, sys.version_info[:3]))} try: import annotationlib; out["annotationlib"] = True # 3.14 才有 except ImportError: out["annotationlib"] = False try: compile('t"hello {1}"', "<t>", "eval"); out["t_strings"] = True # 3.14 才有的语法 except SyntaxError: out["t_strings"] = False return out
check.py 调用 features() 并断言 t-string 可用。本机有 Homebrew 装的 3.12.14 和 3.14.7,两个版本正好用来对照。为了让实验可重复,我把 virtualenv 的缓存目录指到了临时目录,没有动系统上的配置。
[tox]
envlist = py312, py314
skipsdist = true
[testenv]
skip_install = true
commands = python check.py
$ tox (第一次: 需要创建两个虚拟环境)
返回码 1 | 耗时 0.8 s
py312: commands[0]> python check.py
解释器: .tox/py312 | 版本 3.12.14
annotationlib(3.14 才有): False | t-string 语法(3.14 才有): False
py312: exit 1 (0.02 seconds) <项目>> python check.py pid=92858
py312: FAIL ✖ in 0.44 seconds
py314: commands[0]> python check.py
解释器: .tox/py314 | 版本 3.14.7
annotationlib(3.14 才有): True | t-string 语法(3.14 才有): True
py312: FAIL code 1 (0.44=setup[0.42]+cmd[0.02] seconds)
py314: OK (0.21=setup[0.18]+cmd[0.02] seconds)
AssertionError: 本项目需要 3.14 的 t-string
.tox 目录: ['.gitignore', '.tmp', 'CACHEDIR.TAG', 'py312', 'py314'] | py312 里的 python: <brew>/python@3.12/3.12.14/Frameworks/Python.framework/Versions/3.12/bin/python3.12
$ tox (第二次: 环境已存在, 直接复用)
返回码 1 | 耗时 0.2 s
$ tox -r (强制重建环境)
返回码 1 | 耗时 0.5 s
tox 的汇总行:
py312: FAIL ✖ in 0.44 seconds
py312: FAIL code 1 (0.44=setup[0.42]+cmd[0.02] seconds)
py314: OK (0.21=setup[0.18]+cmd[0.02] seconds)
evaluation failed :( (0.67 seconds)tox.ini 里只写了 envlist = py312, py314,tox 按环境名找解释器,在 .tox/py312、.tox/py314 各建一个虚拟环境(py312 里的 python 最终指向 Homebrew 的 3.12.14)。同样的 python check.py,在 3.12 上因为 annotationlib 不存在、t-string 语法也不支持而断言失败(退出码 1),在 3.14 上通过。汇总行里 py312: FAIL code 1、py314: OK,整体 evaluation failed——任何一个环境失败,进程退出码非零,CI 就会变红。
速度上:首次 0.8 s,第二次复用环境 0.2 s,tox -r 强制重建 0.5 s。这里环境里什么都没装(skip_install = true),所以创建很快;真实项目里装依赖会让「首次」慢得多,这也是 tox 默认复用环境的原因。
import nox
nox.options.default_venv_backend = "venv"
nox.options.reuse_existing_virtualenvs = False
@nox.session(python=["3.12", "3.14"])
def tests(session):
session.run("python", "check.py")
@nox.session(python="3.14")
def only_new(session):
# 用 Python 写逻辑: 按解释器版本决定要不要做某件事
if session.python == "3.14":
session.log("这一步只在 3.14 上做")
session.run("python", "-c", "import sys; print(' only_new 在', sys.version_info[:2])")
$ nox -l
Sessions defined in <项目>/noxfile.py:
* tests-3.12
* tests-3.14
* only_new
sessions marked with * are selected, sessions marked with - are skipped.
$ nox -s tests 返回码 1 | 耗时 2.9 s
解释器: .nox/tests-3-12 | 版本 3.12.14
annotationlib(3.14 才有): False | t-string 语法(3.14 才有): False
解释器: .nox/tests-3-14 | 版本 3.14.7
annotationlib(3.14 才有): True | t-string 语法(3.14 才有): True
nox > Running session tests-3.12
nox > Creating virtual environment (venv) using python3.12 in .nox/tests-3-12
nox > python check.py
nox > Command python check.py failed with exit code 1
nox > Session tests-3.12 failed.
nox > Running session tests-3.14
nox > Creating virtual environment (venv) using python3 in .nox/tests-3-14
nox > python check.py
nox > Session tests-3.14 was successful in a second.
nox > Ran 2 sessions in 2 seconds:
nox > * tests-3.12: failed, took a second
nox > * tests-3.14: success, took a second
.nox 目录: ['.gitignore', 'CACHEDIR.TAG', 'tests-3-12', 'tests-3-14']
$ nox -s only_new
only_new 在 (3, 14)
nox > Running session only_new
nox > Creating virtual environment (venv) using python3.14 in .nox/only_new
nox > 这一步只在 3.14 上做
nox > python -c 'import sys; print('"'"' only_new 在'"'"', sys.version_info[:2])'
nox > Session only_new was successful in a second.@nox.session(python=["3.12", "3.14"]) 会展开成 tests-3.12、tests-3.14 两个 session(nox -l 列出它们);每个 session 在 .nox/ 下建自己的环境(目录名里的点被换成短横线)。结果与 tox 一致:3.12 失败、3.14 成功。日志里每个 session 会写明「用 python3.12 / python3 / python3.14 创建虚拟环境」——注意 3.14 那个 session 用的是 python3,它恰好指向 3.14,所以是否找到了预期的版本,要靠 sys.version 自己确认,这也是上面 check.py 要把版本打印出来的原因。
nox 最大的区别是 session 就是普通 Python 函数:only_new 里用 if session.python == "3.14" 决定要不要执行某一步。tox 想做类似的条件逻辑,得靠 ini 里的条件因子和替换语法。选哪个更多是风格问题:配置简单、大部分项目只需要「矩阵 + 命令」时 tox 很省事;流程需要逻辑判断时 nox 更自然。
tox -p)、依赖安装与缓存、tox 与 uv 的集成、free-threaded 版本作为矩阵成员、Windows 上的解释器发现