本文关于Python项目工程化的文档虽然详尽,但往往让人困惑于各种工具链的复杂配置,难以理解每个依赖的实际作用。为了解决这个问题,本文将通过一份完整的实操手册,带你从零开始,亲手创建一个真实的Python项目。我们不仅会配置完整的开发工具链,还会详细解释每一步做什么、为什么这样做、以及还有哪些选择,让你通过实践真正掌握现代Python项目的构建与管理亲手创建一个真实的Python项目。
一、准备工作
1.1 你需要准备什么
在开始之前,请确保你有:
1.2 安装uv
什么是uv?
uv是Astral公司开发的Python包管理器,它比pip快10-100倍,可以替代pip、pip-tools、virtualenv、poetry等工具。
为什么选择uv而不是Poetry/pip?
- 速度极快:Rust编写,比pip快10-100倍
- 功能全面:集成虚拟环境管理、依赖解析、锁文件
- 兼容性好:完全兼容pip和pyproject.toml标准
- 简单易用:命令直观,学习成本低
安装uv:
1 2 3 4 5 6 7 8
| irm https://astral.sh/uv/install.ps1 | iex
pip install uv
uv --version
|
拓展:其他Python包管理工具对比
- pip:Python官方,功能基础,速度慢
- Poetry:功能强大,但配置复杂,速度一般
- PDM:支持PEP标准,国产工具
- uv:速度最快,功能完善,推荐新项目使用
1.3 配置Git
如果你还没有配置Git,执行以下命令:
1 2 3 4 5 6 7 8 9 10 11 12
| git config --global user.name "你的名字" git config --global user.email "你的邮箱@example.com"
git config --global init.defaultBranch main
git config --global core.autocrlf true
git config --list
|
为什么需要配置用户名和邮箱?
Git的每次提交都需要记录”谁”在”什么时候”做了”什么修改”。用户名和邮箱就是”谁”的标识。
二、创建远程Git仓库
2.1 在GitHub上创建仓库
- 打开 https://github.com/new
- 填写仓库信息:
- Repository name:
python-demo(你可以自定义)
- Description:Python工程化示例项目
- Visibility:Public(公开)或 Private(私有)
- 不要勾选 “Add a README file”(我们会在本地创建)
- 不要勾选 “Add .gitignore”
- 不要勾选 “Choose a license”
- 点击 Create repository
创建完成后,你会看到一个空仓库页面,记住仓库地址,类似:
1
| https://github.com/你的用户名/python-demo.git
|
为什么不在GitHub上初始化文件?
因为我们要在本地创建项目,如果GitHub上已有文件,推送时会产生冲突。保持GitHub仓库为空,可以让我们直接推送本地内容。
拓展:GitHub vs GitLab vs Gitee
- GitHub:全球最大,开源生态最好,国内访问可能慢
- GitLab:功能强大,可自建,企业常用
- Gitee:国内访问快,但生态较小
本教程使用GitHub,但流程在其他平台也基本相同。
2.2 配置SSH密钥(推荐)
使用SSH可以免密码推送代码,更安全更方便。
步骤1:检查是否已有SSH密钥
1 2 3 4
| ls ~/.ssh/id_ed25519.pub
ls ~/.ssh/id_rsa.pub
|
如果文件存在,跳到步骤3。
步骤2:生成SSH密钥
1 2 3 4
| ssh-keygen -t ed25519 -C "你的邮箱@example.com"
|
步骤3:添加SSH密钥到GitHub
1 2 3 4
| cat ~/.ssh/id_ed25519.pub
Get-Content ~/.ssh/id_ed25519.pub | Set-Clipboard
|
然后:
- 打开 https://github.com/settings/keys
- 点击 New SSH key
- Title填写:
My PC(或任何你能识别的名字)
- Key粘贴刚才复制的内容
- 点击 Add SSH key
步骤4:测试连接
如果看到 Hi 用户名! You've successfully authenticated...,说明配置成功。
为什么推荐SSH而不是HTTPS?
- HTTPS每次推送都需要输入密码(或配置令牌)
- SSH配置一次后永久免密
- SSH更安全,密钥保存在本地
三、本地项目初始化
3.1 创建项目目录
1 2 3 4 5 6 7 8 9
| cd D:\projects
mkdir python-demo cd python-demo
pwd
|
3.2 初始化Git仓库
你会看到:
1 2 3
| On branch main No commits yet nothing to commit (create/copy files and use "git add" to track)
|
这一步做了什么?
git init 在当前目录创建了一个隐藏的 .git 文件夹,这个文件夹存储了Git的所有版本历史和配置信息。从此刻起,这个目录就是一个Git仓库了。
3.3 使用uv初始化Python项目
1 2 3 4 5
| uv init
uv python pin 3.11
|
这会创建以下文件:
1 2 3 4 5
| python-demo/ ├── .python-version # Python版本锁定 ├── pyproject.toml # 项目配置文件 ├── README.md # 项目说明 └── main.py # 示例代码
|
为什么需要pyproject.toml?
pyproject.toml 是Python的标准项目配置文件(PEP 518/621),用于:
- 定义项目元信息(名称、版本、作者等)
- 管理依赖
- 配置各种开发工具(black、mypy、pytest等)
它替代了以前的 setup.py、setup.cfg、requirements.txt 等多个文件。
3.4 创建项目结构
1 2 3 4 5 6 7 8 9 10 11 12 13
| mkdir src mkdir src/python_demo
mkdir tests
New-Item -Path src/python_demo/__init__.py -ItemType File New-Item -Path tests/__init__.py -ItemType File
Remove-Item hello.py
|
现在的项目结构:
1 2 3 4 5 6 7 8 9
| python-demo/ ├── .python-version ├── pyproject.toml ├── README.md ├── src/ │ └── python_demo/ │ └── __init__.py └── tests/ └── __init__.py
|
为什么使用 src 目录结构?
这是Python社区推荐的项目结构(src layout),好处是:
- 避免测试时意外导入本地未安装的包
- 明确区分源代码和其他文件
- 更容易配置打包和发布
拓展:项目结构的两种流派
1 2 3 4 5 6 7 8 9 10 11 12
| # Flat layout(扁平结构) python_demo/ ├── python_demo/ # 包直接在根目录 │ └── __init__.py └── tests/
# Src layout(src结构,推荐) python_demo/ ├── src/ │ └── python_demo/ # 包在src目录下 │ └── __init__.py └── tests/
|
3.5 编辑pyproject.toml
用VS Code打开项目:
打开 pyproject.toml,根据你选择的工具链方案进行配置。
方案选择:Ruff vs 传统工具链
在配置之前,你需要选择一种代码质量工具方案:
| 对比项 |
方案A:Ruff(推荐) |
方案B:Black + isort + flake8 |
| 工具数量 |
1个 |
3个 |
| 执行速度 |
极快(Rust编写) |
较慢(Python编写) |
| 配置复杂度 |
简单(统一配置) |
较复杂(多工具配置) |
| 功能覆盖 |
格式化 + 导入排序 + lint |
格式化 + 导入排序 + lint |
| 社区成熟度 |
新兴但发展迅速 |
成熟稳定 |
| 适用场景 |
新项目、追求效率 |
已有项目、团队习惯 |
方案A:使用Ruff(推荐)
Ruff是All-in-one工具,同时替代Black、isort、flake8、pyupgrade等多个工具。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
| [project] name = "python-demo" version = "0.1.0" description = "Python工程化示例项目" readme = "README.md" requires-python = ">=3.11" dependencies = []
[project.optional-dependencies] dev = [ "pytest>=8.0.0", "pytest-cov>=4.1.0", "mypy>=1.8.0", "ruff>=0.3.0", "pre-commit>=3.6.0", ]
[build-system] requires = ["hatchling"] build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel] packages = ["src/python_demo"]
[tool.ruff]
target-version = "py311"
line-length = 120
src = ["src", "tests"]
exclude = [ ".git", ".venv", "__pycache__", "build", "dist", ]
[tool.ruff.lint]
select = [ "E", "W", "F", "I", "B", "C4", "UP", ]
ignore = [ "E501", ]
[tool.ruff.lint.isort]
known-first-party = ["python_demo"]
[tool.ruff.format]
quote-style = "double" indent-style = "space" line-ending = "auto"
[tool.mypy] python_version = "3.11" warn_return_any = true warn_unused_configs = true ignore_missing_imports = true
[tool.pytest.ini_options] testpaths = ["tests"] python_files = ["test_*.py"] python_functions = ["test_*"] addopts = [ "-v", "--tb=short", ]
[tool.coverage.run] source = ["src"] branch = true
[tool.coverage.report] exclude_lines = [ "pragma: no cover", "if TYPE_CHECKING:", "if __name__ == .__main__.:", ]
|
Ruff常用命令:
1 2 3 4 5 6 7 8 9 10 11
| uv run ruff check .
uv run ruff check --fix .
uv run ruff format .
uv run ruff format --check .
|
方案B:使用Black + isort + flake8(传统方案)
如果你更熟悉传统工具链,或者团队已有使用习惯,可以选择这个方案。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113
| [project] name = "python-demo" version = "0.1.0" description = "Python工程化示例项目" readme = "README.md" requires-python = ">=3.11" dependencies = []
[project.optional-dependencies] dev = [ "pytest>=8.0.0", "pytest-cov>=4.1.0", "mypy>=1.8.0", "black>=24.0.0", "isort>=5.13.0", "flake8>=7.0.0", "flake8-bugbear>=24.0.0", "pre-commit>=3.6.0", ]
[build-system] requires = ["hatchling"] build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel] packages = ["src/python_demo"]
[tool.black]
line-length = 120
target-version = ["py311"]
include = '\.pyi?$'
exclude = ''' /( \.git | \.venv | __pycache__ | build | dist )/ '''
[tool.isort]
profile = "black"
line_length = 120
known_first_party = ["python_demo"]
src_paths = ["src", "tests"]
skip = [".git", ".venv", "__pycache__", "build", "dist"]
sections = ["FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"]
[tool.mypy] python_version = "3.11" warn_return_any = true warn_unused_configs = true ignore_missing_imports = true
[tool.pytest.ini_options] testpaths = ["tests"] python_files = ["test_*.py"] python_functions = ["test_*"] addopts = [ "-v", "--tb=short", ]
[tool.coverage.run] source = ["src"] branch = true
[tool.coverage.report] exclude_lines = [ "pragma: no cover", "if TYPE_CHECKING:", "if __name__ == .__main__.:", ]
|
方案B额外配置:创建.flake8文件
由于flake8不支持pyproject.toml,需要在项目根目录创建 .flake8 文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| [flake8]
max-line-length = 120
exclude = .git, .venv, __pycache__, build, dist
ignore = E501, W503, E203
max-complexity = 10
extend-select = B,B9
|
方案B常用命令:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| uv run black .
uv run black --check .
uv run isort .
uv run isort --check-only .
uv run flake8 .
|
两种方案的功能对应关系
| 功能 |
Ruff命令 |
传统工具命令 |
| 代码格式化 |
ruff format . |
black . |
| 检查格式 |
ruff format --check . |
black --check . |
| 导入排序 |
ruff check --select I --fix . |
isort . |
| 代码检查 |
ruff check . |
flake8 . |
| 自动修复 |
ruff check --fix . |
需手动修复 |
建议:新项目推荐使用方案A(Ruff),配置简单、速度快、功能完整。如果是已有项目或团队已有使用Black/isort/flake8的习惯,可以继续使用方案B。
3.6 安装开发依赖
1 2 3 4 5 6 7
| uv sync --all-extras
|
验证安装(根据你选择的方案):
方案A:Ruff
1 2 3 4 5 6 7 8
| .\.venv\Scripts\Activate.ps1
ruff --version mypy --version pytest --version pre-commit --version
|
方案B:Black + isort + flake8
1 2 3 4 5 6 7 8 9 10
| .\.venv\Scripts\Activate.ps1
black --version isort --version flake8 --version mypy --version pytest --version pre-commit --version
|
什么是锁文件(uv.lock)?
锁文件记录了所有依赖的精确版本,确保团队成员和CI环境安装完全相同的依赖版本。
四、配置开发工具链
4.1 创建.gitignore
创建 .gitignore 文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| # Python __pycache__/ *.py[cod] *$py.class *.so .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg
# 虚拟环境 .venv/ venv/ ENV/
# IDE .idea/ .vscode/ *.swp *.swo
# 测试和覆盖率 .coverage htmlcov/ .pytest_cache/ .mypy_cache/ .ruff_cache/
# 系统文件 .DS_Store Thumbs.db
# 项目特定 *.log *.tmp
|
为什么需要.gitignore?
有些文件不应该被提交到Git仓库:
- 自动生成的文件(
__pycache__)
- 本地环境相关的文件(
.venv)
- 包含敏感信息的文件
- IDE配置文件(可选)
4.2 更新README.md
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| # Python Demo
Python工程化示例项目,演示现代Python项目的最佳实践。
## 功能特性
- 使用uv管理依赖 - Ruff代码检查和格式化 - MyPy类型检查 - Pytest单元测试 - Pre-commit自动化检查 - GitHub Actions CI/CD
## 快速开始
### 环境要求
- Python 3.11+ - uv
### 安装
```bash # 克隆项目 git clone https://github.com/你的用户名/python-demo.git cd python-demo
# 安装依赖 uv sync --all-extras
# 激活虚拟环境 # Windows .\.venv\Scripts\Activate.ps1 # Linux/macOS source .venv/bin/activate
|
开发
1 2 3 4 5 6 7 8 9 10 11
| uv run pytest
uv run ruff check .
uv run ruff format .
uv run mypy src/
|
项目结构
python-demo/
├── src/
│ └── python_demo/ # 源代码
├── tests/ # 测试代码
├── pyproject.toml # 项目配置
└── README.md
许可证
MIT
4.3 验证工具是否正常工作
根据你选择的方案,验证工具是否正常工作:
方案A:Ruff版验证命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| .\.venv\Scripts\Activate.ps1
uv run ruff check .
uv run ruff format --check .
uv run mypy src/
uv run pytest
|
方案B:传统工具链验证命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| .\.venv\Scripts\Activate.ps1
uv run black --check .
uv run isort --check-only .
uv run flake8 .
uv run mypy src/
uv run pytest
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| ---
## 五、VS Code配置
### 5.1 安装推荐扩展
根据你选择的工具方案,安装对应的VS Code扩展:
#### 方案A:Ruff版扩展
| 扩展 | ID | 用途 | |------|-----|------| | Python | ms-python.python | Python基础支持 | | Pylance | ms-python.vscode-pylance | 类型检查和智能提示 | | Ruff | charliermarsh.ruff | Ruff集成 | | Even Better TOML | tamasfe.even-better-toml | TOML语法高亮 |
安装命令:
```powershell code --install-extension ms-python.python code --install-extension ms-python.vscode-pylance code --install-extension charliermarsh.ruff code --install-extension tamasfe.even-better-toml
|
方案B:传统工具链扩展
| 扩展 |
ID |
用途 |
| Python |
ms-python.python |
Python基础支持 |
| Pylance |
ms-python.vscode-pylance |
类型检查和智能提示 |
| Black Formatter |
ms-python.black-formatter |
Black格式化 |
| isort |
ms-python.isort |
导入排序 |
| Flake8 |
ms-python.flake8 |
代码检查 |
| Even Better TOML |
tamasfe.even-better-toml |
TOML语法高亮 |
安装命令:
1 2 3 4 5 6
| code --install-extension ms-python.python code --install-extension ms-python.vscode-pylance code --install-extension ms-python.black-formatter code --install-extension ms-python.isort code --install-extension ms-python.flake8 code --install-extension tamasfe.even-better-toml
|
5.2 创建工作区配置
根据你选择的方案,创建 .vscode/settings.json:
方案A:Ruff版settings.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| { "python.defaultInterpreterPath": "${workspaceFolder}/.venv/Scripts/python.exe",
"[python]": { "editor.defaultFormatter": "charliermarsh.ruff", "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.fixAll.ruff": "explicit", "source.organizeImports.ruff": "explicit" } },
"ruff.configurationPreference": "filesystemFirst",
"python.analysis.typeCheckingMode": "basic", "python.analysis.autoImportCompletions": true,
"editor.rulers": [120], "files.trimTrailingWhitespace": true, "files.insertFinalNewline": true }
|
方案B:Black + isort版settings.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| { "python.defaultInterpreterPath": "${workspaceFolder}/.venv/Scripts/python.exe",
"[python]": { "editor.defaultFormatter": "ms-python.black-formatter", "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.organizeImports": "explicit" } },
"black-formatter.args": ["--config", "pyproject.toml"],
"isort.args": ["--settings-path", "pyproject.toml"],
"flake8.args": [],
"python.analysis.typeCheckingMode": "basic", "python.analysis.autoImportCompletions": true,
"editor.rulers": [120], "files.trimTrailingWhitespace": true, "files.insertFinalNewline": true }
|
5.3 创建推荐扩展配置
根据你的方案,创建 .vscode/extensions.json:
方案A:Ruff版extensions.json
1 2 3 4 5 6 7 8
| { "recommendations": [ "ms-python.python", "ms-python.vscode-pylance", "charliermarsh.ruff", "tamasfe.even-better-toml" ] }
|
方案B:传统工具链版extensions.json
1 2 3 4 5 6 7 8 9 10
| { "recommendations": [ "ms-python.python", "ms-python.vscode-pylance", "ms-python.black-formatter", "ms-python.isort", "ms-python.flake8", "tamasfe.even-better-toml" ] }
|
为什么需要这个文件?
当团队成员打开项目时,VS Code会提示安装推荐的扩展,确保团队使用统一的开发环境。
5.4 选择Python解释器
- 按
Ctrl+Shift+P 打开命令面板
- 输入
Python: Select Interpreter
- 选择
.venv 中的Python解释器
六、编写示例代码
现在让我们编写一些实际的代码,来验证我们的工具链。
6.1 创建核心模块
创建 src/python_demo/calculator.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
| """简单计算器模块。
这个模块提供基本的数学运算功能,用于演示Python项目工程化。 """
from typing import Union
Number = Union[int, float]
def add(a: Number, b: Number) -> Number: """两数相加。
Args: a: 第一个数 b: 第二个数
Returns: 两数之和
Examples: >>> add(1, 2) 3 >>> add(1.5, 2.5) 4.0 """ return a + b
def subtract(a: Number, b: Number) -> Number: """两数相减。
Args: a: 被减数 b: 减数
Returns: 两数之差 """ return a - b
def multiply(a: Number, b: Number) -> Number: """两数相乘。
Args: a: 第一个数 b: 第二个数
Returns: 两数之积 """ return a * b
def divide(a: Number, b: Number) -> float: """两数相除。
Args: a: 被除数 b: 除数
Returns: 两数之商
Raises: ValueError: 当除数为0时抛出 """ if b == 0: raise ValueError("除数不能为0") return a / b
class Calculator: """计算器类,支持链式运算。
Attributes: result: 当前计算结果
Examples: >>> calc = Calculator(10) >>> calc.add(5).multiply(2).result 30 """
def __init__(self, initial_value: Number = 0) -> None: """初始化计算器。
Args: initial_value: 初始值,默认为0 """ self.result: Number = initial_value
def add(self, value: Number) -> "Calculator": """加法运算。
Args: value: 要加的值
Returns: 返回self以支持链式调用 """ self.result = add(self.result, value) return self
def subtract(self, value: Number) -> "Calculator": """减法运算。
Args: value: 要减的值
Returns: 返回self以支持链式调用 """ self.result = subtract(self.result, value) return self
def multiply(self, value: Number) -> "Calculator": """乘法运算。
Args: value: 要乘的值
Returns: 返回self以支持链式调用 """ self.result = multiply(self.result, value) return self
def divide(self, value: Number) -> "Calculator": """除法运算。
Args: value: 要除的值
Returns: 返回self以支持链式调用
Raises: ValueError: 当除数为0时抛出 """ self.result = divide(self.result, value) return self
def reset(self, value: Number = 0) -> "Calculator": """重置计算器。
Args: value: 重置后的值,默认为0
Returns: 返回self以支持链式调用 """ self.result = value return self
|
6.2 更新__init__.py
更新 src/python_demo/__init__.py:
1 2 3 4 5 6 7 8 9
| """Python Demo - Python工程化示例项目。
这个包提供简单的计算器功能,用于演示Python项目的工程化实践。 """
from python_demo.calculator import Calculator, add, divide, multiply, subtract
__version__ = "0.1.0" __all__ = ["Calculator", "add", "subtract", "multiply", "divide"]
|
6.3 编写测试
创建 tests/test_calculator.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
| """计算器模块测试。"""
import pytest
from python_demo.calculator import Calculator, add, divide, multiply, subtract
class TestBasicOperations: """基本运算函数测试。"""
def test_add_integers(self) -> None: """测试整数加法。""" assert add(1, 2) == 3 assert add(-1, 1) == 0 assert add(0, 0) == 0
def test_add_floats(self) -> None: """测试浮点数加法。""" assert add(1.5, 2.5) == 4.0 assert add(0.1, 0.2) == pytest.approx(0.3)
def test_subtract(self) -> None: """测试减法。""" assert subtract(5, 3) == 2 assert subtract(3, 5) == -2 assert subtract(0, 0) == 0
def test_multiply(self) -> None: """测试乘法。""" assert multiply(3, 4) == 12 assert multiply(-2, 3) == -6 assert multiply(0, 100) == 0
def test_divide(self) -> None: """测试除法。""" assert divide(10, 2) == 5.0 assert divide(7, 2) == 3.5 assert divide(-10, 2) == -5.0
def test_divide_by_zero(self) -> None: """测试除以零的情况。""" with pytest.raises(ValueError, match="除数不能为0"): divide(10, 0)
class TestCalculator: """计算器类测试。"""
def test_initial_value(self) -> None: """测试初始值。""" calc = Calculator() assert calc.result == 0
calc = Calculator(10) assert calc.result == 10
def test_chain_operations(self) -> None: """测试链式运算。""" calc = Calculator(10) result = calc.add(5).multiply(2).subtract(10).divide(2).result assert result == 10.0
def test_reset(self) -> None: """测试重置功能。""" calc = Calculator(100) calc.add(50).reset() assert calc.result == 0
calc.reset(42) assert calc.result == 42
@pytest.mark.parametrize( "a, b, expected", [ (1, 2, 3), (0, 0, 0), (-1, -1, -2), (100, 200, 300), (1.5, 2.5, 4.0), ], ) def test_add_parametrized(a: float, b: float, expected: float) -> None: """参数化测试:加法。""" assert add(a, b) == expected
|
6.4 验证代码
根据你选择的方案,运行工具来检查代码:
方案A:Ruff版验证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| uv run ruff check .
uv run ruff check --fix .
uv run ruff format .
uv run mypy src/
uv run pytest
uv run pytest --cov=src --cov-report=term-missing
|
方案B:传统工具链验证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| uv run black .
uv run isort .
uv run flake8 .
uv run mypy src/
uv run pytest
uv run pytest --cov=src --cov-report=term-missing
|
命令对照表:
| 功能 |
Ruff(方案A) |
传统工具链(方案B) |
| 代码检查 |
ruff check . |
flake8 . |
| 自动修复 |
ruff check --fix . |
需手动修复 |
| 代码格式化 |
ruff format . |
black . |
| 导入排序 |
包含在 ruff check |
isort . |
| 类型检查 |
mypy src/ |
mypy src/ |
| 运行测试 |
pytest |
pytest |
七、pre-commit配置
7.1 什么是pre-commit?
pre-commit是一个Git钩子管理框架。它可以在你执行git commit之前自动运行一系列检查。
为什么需要它?
- 强制质量检查:忘记手动检查?pre-commit会自动检查
- 团队一致性:确保所有人提交的代码都经过相同的检查
- 节省CI时间:本地先检查,减少CI失败
工作原理:
当你运行 pre-commit install 后,它会在 .git/hooks/ 目录下创建脚本文件。Git 在执行 commit 或 push 时会自动调用这些脚本。
7.2 理解配置文件结构
.pre-commit-config.yaml 是 pre-commit 的核心配置文件,理解它的结构非常重要:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
fail_fast: true default_stages: [pre-commit]
repos: - repo: https://github.com/psf/black rev: 24.3.0 hooks: - id: black
- repo: local hooks: - id: pytest entry: uv run pytest stages: [pre-push]
|
核心概念:
| 配置项 |
作用 |
repos |
列表,定义从哪些来源获取 hooks |
repo |
远程 Git 仓库 URL 或 local |
rev |
仓库版本(标签/commit),用于锁定版本 |
id |
hook 的唯一标识,由仓库定义 |
stages |
指定运行阶段,覆盖 default_stages |
additional_dependencies |
补充安装额外依赖 |
重要提示: pre-commit 使用独立的隔离环境(存储在 ~/.cache/pre-commit/),与项目虚拟环境完全分离。远程仓库的工具会自动下载安装,确保团队成员使用完全相同版本。
多个 repo 的作用:
每个 repo 代表一个 hooks 来源(类似”应用商店”)。不同工具由不同团队维护,所以需要从多个 repo 获取:
1 2 3 4 5 6 7 8 9 10 11
| repos: - repo: https://github.com/pre-commit/pre-commit-hooks hooks: - id: trailing-whitespace - id: end-of-file-fixer
- repo: https://github.com/psf/black hooks: - id: black
|
执行顺序: 同一阶段的 hooks 按定义顺序执行,所以格式化工具应放在检查工具之前。
7.3 创建pre-commit配置
根据你在 3.5 节选择的工具方案,创建对应的 .pre-commit-config.yaml:
方案A:Ruff版pre-commit配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
|
fail_fast: true
default_stages: [pre-commit]
repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml - id: check-toml - id: check-json - id: check-merge-conflict - id: detect-private-key - id: check-added-large-files args: ['--maxkb=1000']
- repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.3.0 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy rev: v1.8.0 hooks: - id: mypy additional_dependencies: [] args: [--config-file, pyproject.toml]
- repo: local hooks: - id: pytest name: pytest entry: uv run pytest -x -q language: system types: [python] stages: [pre-push] pass_filenames: false
|
方案B:Black + isort + flake8版pre-commit配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
|
fail_fast: true
default_stages: [pre-commit]
repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml - id: check-toml - id: check-json - id: check-merge-conflict - id: detect-private-key - id: check-added-large-files args: ['--maxkb=1000']
- repo: https://github.com/psf/black rev: 24.2.0 hooks: - id: black args: [--config, pyproject.toml]
- repo: https://github.com/pycqa/isort rev: 5.13.2 hooks: - id: isort args: [--settings-path, pyproject.toml]
- repo: https://github.com/pycqa/flake8 rev: 7.0.0 hooks: - id: flake8 additional_dependencies: - flake8-bugbear>=24.0.0
- repo: https://github.com/pre-commit/mirrors-mypy rev: v1.8.0 hooks: - id: mypy additional_dependencies: [] args: [--config-file, pyproject.toml]
- repo: local hooks: - id: pytest name: pytest entry: uv run pytest -x -q language: system types: [python] stages: [pre-push] pass_filenames: false
|
注意:方案B的pre-commit会依次运行Black → isort → flake8,确保代码先格式化再检查。
7.4 安装pre-commit钩子
1 2 3 4 5 6 7 8
| uv run pre-commit install
uv run pre-commit install --hook-type pre-push
cat .git/hooks/pre-commit
|
pre-commit install 做了什么?
这个命令把 pre-commit 框架”挂载”到 Git 的提交流程中。安装后,每次 git commit 时,Git 会自动调用 .git/hooks/pre-commit 脚本。
支持的钩子类型(共 8 种):
| 钩子类型 |
触发时机 |
典型用途 |
pre-commit |
git commit 前 |
代码格式化、lint 检查 |
pre-push |
git push 前 |
运行测试、安全检查 |
commit-msg |
提交消息写入后 |
校验提交消息格式 |
pre-merge-commit |
合并前 |
检查合并冲突 |
post-checkout |
git checkout 后 |
环境初始化 |
post-commit |
提交完成后 |
通知、日志记录 |
post-merge |
合并完成后 |
依赖更新检查 |
prepare-commit-msg |
编辑提交消息前 |
自动生成提交消息模板 |
阶段确定机制:
Hook 运行在哪个阶段,按以下优先级确定:
- 单个 hook 的
stages 字段(最高优先级)
- 全局
default_stages(次优先级)
- 工具默认值(最低优先级)
1 2 3 4 5 6 7 8 9 10 11 12
| default_stages: [pre-commit]
repos: - repo: https://github.com/psf/black hooks: - id: black
- repo: local hooks: - id: pytest stages: [pre-push]
|
7.5 手动运行pre-commit
1 2 3 4 5
| uv run pre-commit run --all-files
uv run pre-commit run
|
pre-commit的工作流程:
方案A(Ruff):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| git add . # 暂存文件 git commit -m "xxx" # 触发pre-commit ↓ [trailing-whitespace] 检查行尾空格 ↓ [end-of-file-fixer] 检查文件结尾 ↓ [ruff] 检查代码规范 ↓ [ruff-format] 检查格式 ↓ [mypy] 类型检查 ↓ 全部通过 → 提交成功 任一失败 → 提交取消,显示错误信息
|
方案B(Black + isort + flake8):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| git add . # 暂存文件 git commit -m "xxx" # 触发pre-commit ↓ [trailing-whitespace] 检查行尾空格 ↓ [end-of-file-fixer] 检查文件结尾 ↓ [black] 代码格式化 ↓ [isort] 导入排序 ↓ [flake8] 代码检查 ↓ [mypy] 类型检查 ↓ 全部通过 → 提交成功 任一失败 → 提交取消,显示错误信息
|
八、GitHub Actions配置
8.1 什么是GitHub Actions?
GitHub Actions是GitHub提供的CI/CD服务。它可以在代码推送、PR创建等事件时自动运行测试和检查。
为什么需要CI/CD?
- 双重保障:本地pre-commit可能被跳过(
--no-verify),CI是最后的防线
- 多环境测试:可以在多个Python版本、多个操作系统上测试
- 自动化发布:可以自动发布到PyPI
8.2 创建GitHub Actions配置
根据你选择的工具方案,创建 .github/workflows/ci.yml:
方案A:Ruff版CI配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
| name: CI
on: push: branches: [main] pull_request: branches: [main]
jobs: lint: name: Lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- name: Install uv uses: astral-sh/setup-uv@v4 with: version: "latest"
- name: Set up Python run: uv python install
- name: Install dependencies run: uv sync --all-extras
- name: Ruff check run: uv run ruff check .
- name: Ruff format check run: uv run ruff format --check .
- name: MyPy run: uv run mypy src/
test: name: Test (Python ${{ matrix.python-version }}) runs-on: ubuntu-latest needs: lint strategy: matrix: python-version: ["3.11", "3.12"]
steps: - uses: actions/checkout@v4
- name: Install uv uses: astral-sh/setup-uv@v4 with: version: "latest"
- name: Set up Python ${{ matrix.python-version }} run: uv python install ${{ matrix.python-version }}
- name: Install dependencies run: uv sync --all-extras
- name: Run tests run: uv run pytest --cov=src --cov-report=xml
- name: Upload coverage uses: codecov/codecov-action@v4 with: files: coverage.xml fail_ci_if_error: false
|
方案B:Black + isort + flake8版CI配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
| name: CI
on: push: branches: [main] pull_request: branches: [main]
jobs: lint: name: Lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- name: Install uv uses: astral-sh/setup-uv@v4 with: version: "latest"
- name: Set up Python run: uv python install
- name: Install dependencies run: uv sync --all-extras
- name: Black format check run: uv run black --check .
- name: isort check run: uv run isort --check-only .
- name: flake8 check run: uv run flake8 .
- name: MyPy run: uv run mypy src/
test: name: Test (Python ${{ matrix.python-version }}) runs-on: ubuntu-latest needs: lint strategy: matrix: python-version: ["3.11", "3.12"]
steps: - uses: actions/checkout@v4
- name: Install uv uses: astral-sh/setup-uv@v4 with: version: "latest"
- name: Set up Python ${{ matrix.python-version }} run: uv python install ${{ matrix.python-version }}
- name: Install dependencies run: uv sync --all-extras
- name: Run tests run: uv run pytest --cov=src --cov-report=xml
- name: Upload coverage uses: codecov/codecov-action@v4 with: files: coverage.xml fail_ci_if_error: false
|
8.3 理解工作流配置
触发条件 (on):
1 2 3 4 5
| on: push: branches: [main] pull_request: branches: [main]
|
拓展:其他常用触发条件
1 2 3 4 5 6
| on: schedule: - cron: '0 0 * * *' workflow_dispatch: release: types: [published]
|
作业依赖 (needs):
矩阵策略 (strategy.matrix):
1 2 3 4 5
| strategy: matrix: python-version: ["3.11", "3.12"] os: [ubuntu-latest, windows-latest]
|
九、完整工作流演示
现在让我们把所有内容串起来,完成一次完整的工作流。
9.1 首次提交并推送
1 2 3 4 5 6 7 8 9 10 11 12
|
git status
git add .
git commit -m "feat: 初始化项目结构和工具链"
|
9.2 关联远程仓库并推送
1 2 3 4 5 6 7 8 9 10 11
| git remote add origin git@github.com:你的用户名/python-demo.git
git remote -v
git push -u origin main
|
9.3 查看CI结果
- 打开 https://github.com/你的用户名/python-demo
- 点击 Actions 标签
- 你应该能看到正在运行或已完成的工作流
- 点击工作流可以查看详细日志
9.4 日常开发流程
从现在开始,你的日常开发流程是:
方案A(Ruff):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
uv run ruff check . uv run mypy src/ uv run pytest
git add . git commit -m "feat: 添加新功能"
git push
|
方案B(传统工具链):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
uv run black --check . uv run isort --check-only . uv run flake8 . uv run mypy src/ uv run pytest
git add . git commit -m "feat: 添加新功能"
git push
|
9.5 处理pre-commit失败
如果pre-commit检查失败:
1 2 3 4 5 6 7 8 9 10 11 12 13
|
git add . git commit -m "feat: 添加新功能"
git commit --no-verify -m "hotfix: 紧急修复"
|
十、总结与拓展
10.1 我们完成了什么
| 步骤 |
内容 |
为什么 |
| Git仓库 |
GitHub + SSH |
版本控制和协作 |
| 项目结构 |
src layout |
专业的项目组织 |
| 依赖管理 |
uv + pyproject.toml |
快速、现代的包管理 |
| 代码质量 |
Ruff 或 Black+isort+flake8 + MyPy |
自动检查和修复 |
| 测试 |
pytest + coverage |
保证代码正确性 |
| IDE配置 |
VS Code |
高效的开发体验 |
| 本地检查 |
pre-commit |
提交前自动检查 |
| CI/CD |
GitHub Actions |
持续集成保障 |
10.2 工具链总览
方案A(Ruff):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 开发流程: 编写代码 → 保存(自动格式化)→ git add → git commit ↓ pre-commit运行 ├── trailing-whitespace ├── ruff check ├── ruff format └── mypy ↓ git push ↓ pre-push运行 └── pytest ↓ GitHub Actions ├── lint作业 └── test作业(多版本)
|
方案B(传统工具链):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 开发流程: 编写代码 → 保存(自动格式化)→ git add → git commit ↓ pre-commit运行 ├── trailing-whitespace ├── black ├── isort ├── flake8 └── mypy ↓ git push ↓ pre-push运行 └── pytest ↓ GitHub Actions ├── lint作业 └── test作业(多版本)
|
10.3 进阶学习路线
你已经掌握了基础,接下来可以学习:
测试进阶
- pytest fixtures
- 参数化测试
- Mock和打桩
- 集成测试
CI/CD进阶
代码质量进阶
- 更多Ruff规则
- 自定义MyPy插件
- 安全检查(bandit)
文档
10.4 常见问题
Q: pre-commit太慢了怎么办?
A: 可以配置只检查变更的文件,或者在CI中运行完整检查,本地只运行快速检查。Ruff方案通常比传统工具链快很多。
Q: 如何在两种方案之间切换?
A: 按以下步骤操作:
- 修改
pyproject.toml 中的 [project.optional-dependencies] dev依赖
- 替换对应的工具配置节(
[tool.ruff] 或 [tool.black]/[tool.isort])
- 更新
.pre-commit-config.yaml 使用对应的hooks
- 更新
.vscode/settings.json 和 .vscode/extensions.json
- 运行
uv sync --all-extras 重新安装依赖
Q: 如何在现有项目中应用这套工具?
A: 渐进式采用:
Ruff方案:
- 先添加配置文件
- 运行
ruff check --fix 修复简单问题
- 运行
ruff format 格式化代码
- 提交修复
- 启用pre-commit
传统工具链方案:
- 先添加配置文件
- 运行
black . 格式化代码
- 运行
isort . 排序导入
- 运行
flake8 . 检查问题并手动修复
- 提交修复
- 启用pre-commit
项目文件清单
完成本教程后,你的项目结构应该是:
方案A(Ruff):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| python-demo/ ├── .github/ │ └── workflows/ │ └── ci.yml # GitHub Actions配置 ├── .vscode/ │ ├── extensions.json # 推荐扩展 │ └── settings.json # 工作区设置 ├── src/ │ └── python_demo/ │ ├── __init__.py # 包初始化 │ └── calculator.py # 计算器模块 ├── tests/ │ ├── __init__.py │ └── test_calculator.py # 测试文件 ├── .gitignore # Git忽略配置 ├── .pre-commit-config.yaml # pre-commit配置 ├── .python-version # Python版本 ├── pyproject.toml # 项目配置 ├── README.md # 项目说明 └── uv.lock # 依赖锁文件
|
方案B(传统工具链):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| python-demo/ ├── .github/ │ └── workflows/ │ └── ci.yml # GitHub Actions配置 ├── .vscode/ │ ├── extensions.json # 推荐扩展 │ └── settings.json # 工作区设置 ├── src/ │ └── python_demo/ │ ├── __init__.py # 包初始化 │ └── calculator.py # 计算器模块 ├── tests/ │ ├── __init__.py │ └── test_calculator.py # 测试文件 ├── .flake8 # flake8配置(额外文件) ├── .gitignore # Git忽略配置 ├── .pre-commit-config.yaml # pre-commit配置 ├── .python-version # Python版本 ├── pyproject.toml # 项目配置 ├── README.md # 项目说明 └── uv.lock # 依赖锁文件
|
相关文章
祝你学习愉快!🎉