环境搭建:Python版本选择、虚拟环境创建、依赖管理、asyncio库版本兼容性
做CI异步测试,环境搭建是第一步,也是最容易踩坑的一步。今天我会从实战角度,把Python版本、虚拟环境、依赖管理、asyncio兼容性这些事儿一次讲透。你想想看,如果环境没搭好,后面再怎么调试都是白费力气。
Python版本选择:别用最新,也别用最旧
做CI异步测试,Python版本怎么挑?我个人的习惯是——看你的项目跑在哪个CI平台上。GitHub Actions的默认虚拟机、GitLab Runner、Jenkins节点,它们的系统镜像里预装了哪些Python版本,心里要有数。
举个例子,我记得2023年有个项目,直接用了Python 3.12,结果CI环境只支持到3.11,部署上去全崩了。后来我学乖了,统一用3.10或3.11。
建议版本范围:
- Python 3.8~3.11:首选。asyncio在3.8引入了一些关键修正,3.10/3.11表现最稳定。
- Python 3.12:慎用。部分第三方库还没完全适配,特别是异步协程相关的绑定库。
- Python 3.7以下:别碰。asyncio的API不全,而且很多CI平台已经不再支持。
真有特殊需要,我建议在项目根目录放一个.python-version文件,或者用pyproject.toml明确指定。CI脚本里也写死版本,别用python3这种软链接。
为什么会这样?说白了就是版本兼容性问题。CI环境里预装的Python版本经常比你本地的低,你不显式指定,等着翻车吧。
虚拟环境创建:隔离是第一要务
虚拟环境,我一般用venv。轻量、内置、不依赖第三方。虽然conda也可以,但CI环境下装miniconda太慢了。
创建命令很简单,但有坑。
python3 -m venv .venv
source .venv/bin/activate
嗯,这里要注意:CI脚本里不要用source,而是直接用路径的Python解释器,或者用./.venv/bin/python调用。我曾经在GitLab CI里用了source activate,结果下一行脚本的环境变量没继承,依赖全装到系统里去了……
CI环境下虚拟环境推荐做法:
- 在CI配置的
before_script阶段创建环境 - 用
pip install -r requirements.txt直接装在虚拟环境里 - 每次CI runner都从零开始创建,避免缓存污染
- 如果依赖下载慢,可以用
pip cache但小心版本锁定
我个人还有个习惯:把虚拟环境目录.venv加到.gitignore里,然后CI脚本里重新创建。这样本地环境和CI环境保持一致。
依赖管理:锁定版本,别用通配符
依赖管理,核心就一句话——锁定所有依赖的精确版本。别写requests>=2.28这种范围,你永远不知道CI环境下pip会给你装哪个版本。
推荐用pip freeze > requirements.txt生成锁文件。但是要注意,asyncio相关库的依赖树可能比较复杂。
| 包名 | 建议版本 | 说明 |
|---|---|---|
| pytest-asyncio | ≥0.21, <0.24 | 0.24改了测试收集模式,小心兼容 |
| aiohttp | 3.8, 3.9 | 3.9开始强制要求Python 3.8+ |
| httpx | 0.24, 0.27 | async支持完整,但注意event loop策略 |
| anyio | 3.6, 4.0 | 如果你同时支持trio和asyncio |
我曾经踩过一个坑:某次升级了aiohttp到4.0,结果asyncio的event loop在CI上莫名其妙报RuntimeError,折腾了半天,发现是底层依赖yarl不兼容。从那以后,我每次改依赖都会跑一遍pip check。
# 推荐的锁定流程
pip install -e ".[dev]"
pip freeze --exclude-editable > requirements.txt
pip check # 确保没有冲突
警告:如果项目使用了setup.py或pyproject.toml的动态版本,pip freeze可能会输出本地路径。建议用--exclude-editable参数,或者用pip list --format=freeze。
asyncio库版本兼容性:那些年我们追过的API变化
asyncio本身是Python标准库,版本跟随Python一起更新。但很多第三方异步库对asyncio的版本有隐式要求。
我列出几个实际项目中常见的兼容性问题:
- Python 3.8:asyncio.run()支持
debug=True参数,但loop.create_task()还不支持name参数 - Python 3.9:新增了
TaskGroup(不过严格来说是3.11才稳定) - Python 3.10:
asyncio.get_running_loop()行为更严格,很多旧代码会报错 - Python 3.11:
TaskGroup和Timeout上下文管理器正式可用
你的异步测试代码如果用了asyncio.wait_for、asyncio.gather,基本没大问题。但如果你用了loop对象操作(比如loop.run_until_complete),在Python 3.10+上会被警告甚至移除。
我在项目中验证兼容性的方法:
# 在CI脚本中加入这段检查
import sys
import asyncio
print(f"Python {sys.version}")
print(f"asyncio {asyncio.__version__ if hasattr(asyncio, '__version__') else 'builtin'}")
# 测试基本协程
async def test():
await asyncio.sleep(0.1)
return "ok"
result = asyncio.run(test())
assert result == "ok"
为什么要在CI里加这个?因为不同Python小版本的asyncio实现细节不同,比如3.7和3.8的run()实现就完全不同。你想想看,本地跑得好好的,CI上报个RuntimeError: Event loop is closed,多郁闷。
我个人还会在requirements.txt里加上pytest-asyncio时指定版本范围,因为pytest-asyncio的版本和asyncio的绑定很紧。0.21版本支持@pytest.mark.asyncio装饰器,但0.23改了默认loop作用域。我曾经因为升级了pytest-asyncio,导致所有异步测试都收集不到,CI全红。
最终推荐的环境组合:
| Python版本 | 3.10.11 或 3.11.5 |
| 虚拟环境工具 | venv (Python内置) |
| 依赖管理 | pip + requirements.txt (锁死版本) |
| asyncio测试框架 | pytest-asyncio 0.21.1 |
| 兼容性验证 | CI脚本中运行asyncio基本检查 |
嗯,环境搭建这个东西,看似基础,但每一条背后都有血泪教训。我做的《CI异步测试任务实战指南》后续的所有内容,都基于这个环境。所以今天花20分钟搭好环境,后面能省下你一整天调试时间。