Python 打包与分发
大约 14 分钟约 4076 字
Python 打包与分发
简介
Python 打包与分发是将 Python 代码组织为可安装、可分发、可复用的软件包的过程。从简单的脚本到复杂的库和 CLI 工具,正确的打包方式决定了代码能否被其他开发者方便地使用,也决定了部署流程的可靠性。
Python 打包生态经历了从 distutils 到 setuptools 到 pyproject.toml 的演进。当前的最佳实践是使用 pyproject.toml(PEP 517/518)作为项目配置文件,配合 build 和 twine 工具进行构建和发布。这种方式比传统的 setup.py 更现代、更声明式、更安全。
打包的核心概念包括:源分发(sdist)——包含源代码的压缩包;Wheel(bdist_wheel)——预编译的二进制分发格式,安装更快;入口点(Entry Points)——将 Python 函数注册为命令行工具。理解这些概念是正确打包的基础。
特点
pyproject.toml 配置
现代项目配置
# pyproject.toml — Python 项目配置文件(推荐)
[build-system]
# 构建系统依赖
requires = ["setuptools>=68.0", "wheel>=0.42"]
build-backend = "setuptools.build_meta"
[project]
# 项目元数据
name = "my-awesome-lib"
version = "1.2.3"
description = "一个功能强大的 Python 库"
readme = "README.md"
license = {text = "MIT"}
requires-python = ">=3.10"
# 作者信息
authors = [
{name = "Zhang San", email = "zhangsan@example.com"},
]
maintainers = [
{name = "Li Si", email = "lisi@example.com"},
]
# 关键词和分类
keywords = ["data-processing", "automation", "cli"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Software Development :: Libraries",
"Typing :: Typed",
]
# 依赖
dependencies = [
"httpx>=0.25.0",
"pydantic>=2.0",
"rich>=13.0",
]
# 可选依赖(extras)
[project.optional-dependencies]
dev = [
"pytest>=7.0",
"pytest-cov>=4.0",
"ruff>=0.1.0",
"mypy>=1.0",
"pre-commit>=3.0",
]
docs = [
"mkdocs>=1.5",
"mkdocs-material>=9.0",
]
excel = [
"openpyxl>=3.1",
"pandas>=2.0",
]
# URL 链接
[project.urls]
Homepage = "https://github.com/example/my-awesome-lib"
Documentation = "https://my-awesome-lib.readthedocs.io"
Repository = "https://github.com/example/my-awesome-lib"
Changelog = "https://github.com/example/my-awesome-lib/blob/main/CHANGELOG.md"
# 命令行入口点
[project.scripts]
my-tool = "my_awesome_lib.cli:main"
my-server = "my_awesome_lib.server:run"
# GUI 入口点
[project.gui-scripts]
my-gui = "my_awesome_lib.gui:launch"
# 插件入口点
[project.entry-points."my_awesome_lib.plugins"]
csv-reader = "my_awesome_lib.plugins.csv:CSVReader"
json-reader = "my_awesome_lib.plugins.json:JSONReader"
# setuptools 特定配置
[tool.setuptools]
packages = {find = {where = ["src"]}}
# 或者明确列出
# packages = ["my_awesome_lib", "my_awesome_lib.utils"]
[tool.setuptools.package-data]
my_awesome_lib = ["py.typed", "data/*.json"]
# 排除
[tool.setuptools.packages.find]
exclude = ["tests*", "docs*", "examples*"]使用 setup.py(兼容性)
# setup.py — 传统配置方式(仍被广泛使用)
# 注意: 新项目推荐使用 pyproject.toml
from setuptools import setup, find_packages
setup(
name="my-awesome-lib",
version="1.2.3",
author="Zhang San",
author_email="zhangsan@example.com",
description="一个功能强大的 Python 库",
long_description=open("README.md").read(),
long_description_content_type="text/markdown",
url="https://github.com/example/my-awesome-lib",
license="MIT",
# 包发现
packages=find_packages(where="src"),
package_dir={"": "src"},
# Python 版本要求
python_requires=">=3.10",
# 依赖
install_requires=[
"httpx>=0.25.0",
"pydantic>=2.0",
"rich>=13.0",
],
# 可选依赖
extras_require={
"dev": [
"pytest>=7.0",
"pytest-cov>=4.0",
"ruff>=0.1.0",
],
"excel": [
"openpyxl>=3.1",
"pandas>=2.0",
],
},
# 入口点
entry_points={
"console_scripts": [
"my-tool=my_awesome_lib.cli:main",
],
},
# 分类
classifiers=[
"Development Status :: 4 - Beta",
"Programming Language :: Python :: 3.10",
"License :: OSI Approved :: MIT License",
],
# 包含非 Python 文件
package_data={
"my_awesome_lib": ["py.typed", "data/*.json"],
},
# 类型标记(PEP 561)
zip_safe=False,
)Wheel 与源分发
# ============================================
# Wheel vs Source Distribution
# ============================================
"""
Wheel (.whl):
- 预编译的二进制分发格式
- 安装速度快(不需要执行 setup.py)
- 文件名格式: {name}-{version}-{python}-{abi}-{platform}.whl
例: my_lib-1.0.0-py3-none-any.whl
my_lib-1.0.0-cp312-cp312-manylinux_2_17_x86_64.whl
Source Distribution (sdist):
- 包含源代码的压缩包 (.tar.gz)
- 安装时需要在目标机器上构建
- 适合包含 C 扩展的包
优先级: pip 优先安装 wheel,没有 wheel 时 fallback 到 sdist
标签说明:
- py3: Python 3
- cp312: CPython 3.12
- none: 纯 Python(无 C 扩展)
- any: 任何平台
- manylinux: Linux 兼容
- macosx: macOS
- win_amd64: Windows 64位
"""
# 构建命令
# 1. 安装构建工具
# pip install build twine
# 2. 构建 wheel 和 sdist
# python -m build
# 输出:
# dist/my_awesome_lib-1.2.3-py3-none-any.whl (wheel)
# dist/my_awesome_lib-1.2.3.tar.gz (sdist)
# 3. 只构建 wheel
# python -m build --wheel
# 4. 只构建 sdist
# python -m build --sdist
# 5. 检查构建产物
# twine check dist/*
# 6. 上传到 PyPI
# twine upload dist/*
# 7. 上传到测试 PyPI
# twine upload --repository testpypi dist/*
# 8. 本地安装测试
# pip install dist/my_awesome_lib-1.2.3-py3-none-any.whlCLI 工具打包
# ============================================
# CLI 工具打包 — 将 Python 脚本变为命令行工具
# ============================================
# 项目结构
"""
my-cli-tool/
├── pyproject.toml
├── src/
│ └── my_cli/
│ ├── __init__.py
│ ├── cli.py # CLI 入口
│ ├── commands/ # 子命令
│ │ ├── __init__.py
│ │ ├── init.py
│ │ ├── build.py
│ │ └── deploy.py
│ └── utils.py
└── tests/
"""
# src/my_cli/cli.py
import click
import sys
from typing import Optional
@click.group()
@click.version_option(version="1.2.3", prog_name="my-cli")
@click.option("--verbose", "-v", is_flag=True, help="详细输出")
@click.pass_context
def main(ctx: click.Context, verbose: bool):
"""My CLI Tool — 一个强大的命令行工具"""
ctx.ensure_object(dict)
ctx.obj["verbose"] = verbose
@main.command()
@click.argument("name")
@click.option("--template", "-t", default="basic", help="项目模板")
@click.option("--directory", "-d", type=click.Path(), help="目标目录")
def init(name: str, template: str, directory: Optional[str]):
"""初始化新项目"""
click.echo(f"创建项目: {name} (模板: {template})")
if directory:
click.echo(f"目标目录: {directory}")
@main.command()
@click.option("--target", "-t", default="development", help="构建目标")
@click.option("--output", "-o", type=click.Path(), help="输出路径")
@click.pass_context
def build(ctx: click.Context, target: str, output: Optional[str]):
"""构建项目"""
verbose = ctx.obj.get("verbose", False)
if verbose:
click.echo(f"构建目标: {target}")
with click.progressbar(range(100), label="构建中") as bar:
for item in bar:
pass
click.secho("构建完成!", fg="green", bold=True)
@main.command()
@click.argument("environment")
@click.confirmation_option(prompt="确认部署?")
def deploy(environment: str):
"""部署到指定环境"""
click.echo(f"部署到: {environment}")
click.secho(f"部署成功!", fg="green")
@main.command()
def config():
"""查看配置"""
click.echo("当前配置:")
click.echo(" API URL: https://api.example.com")
click.echo(" Region: cn-east-1")
if __name__ == "__main__":
main()
# pyproject.toml 入口点配置
# [project.scripts]
# my-cli = "my_cli.cli:main"使用 argparse(标准库方案)
# src/my_cli/cli_argparse.py — 不依赖第三方库的 CLI
import argparse
import sys
def create_parser():
"""创建 CLI 解析器"""
parser = argparse.ArgumentParser(
prog="my-tool",
description="My CLI Tool",
epilog="更多信息: https://github.com/example/my-tool",
)
parser.add_argument("--version", action="version", version="%(prog)s 1.2.3")
parser.add_argument("-v", "--verbose", action="store_true", help="详细输出")
subparsers = parser.add_subparsers(dest="command", help="可用命令")
# init 子命令
init_parser = subparsers.add_parser("init", help="初始化项目")
init_parser.add_argument("name", help="项目名称")
init_parser.add_argument("-t", "--template", default="basic", help="模板")
# build 子命令
build_parser = subparsers.add_parser("build", help="构建项目")
build_parser.add_argument("-t", "--target", default="dev", help="构建目标")
build_parser.add_argument("-o", "--output", help="输出路径")
return parser
def main():
parser = create_parser()
args = parser.parse_args()
if args.command == "init":
print(f"创建项目: {args.name} (模板: {args.template})")
elif args.command == "build":
print(f"构建: target={args.target}")
else:
parser.print_help()
if __name__ == "__main__":
main()PyInstaller 可执行文件
# ============================================
# PyInstaller — 将 Python 应用打包为独立可执行文件
# ============================================
"""
# 安装
pip install pyinstaller
# 基础打包(单文件)
pyinstaller --onefile my_script.py
# 带 GUI 的应用(无控制台窗口)
pyinstaller --onefile --windowed my_gui_app.py
# 指定图标
pyinstaller --onefile --icon=app.ico my_script.py
# 指定名称
pyinstaller --onefile --name="MyApp" my_script.py
# 包含数据文件
pyinstaller --onefile --add-data="config.yaml:." my_script.py
# 排除不需要的模块(减小体积)
pyinstaller --onefile --exclude-module matplotlib --exclude-module numpy my_script.py
# 详细输出(排错用)
pyinstaller --onefile --debug=all my_script.py
"""
# PyInstaller 配置文件: my_app.spec
"""
# my_app.spec
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['src/my_app/main.py'],
pathex=[],
binaries=[],
datas=[
('src/my_app/data', 'data'), # 包含数据文件
('config.yaml', '.'), # 包含配置文件
],
hiddenimports=[
'my_app.plugins.csv',
'my_app.plugins.json',
],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[
'tkinter',
'matplotlib',
'numpy',
'scipy',
],
noarchive=False,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='MyApp',
debug=False,
bootloader_ignore_signals=False,
strip=True, # 去除调试符号,减小体积
upx=True, # 使用 UPX 压缩
console=True, # True=控制台应用, False=GUI应用
icon='assets/app.ico',
)
# macOS App Bundle
# app = BUNDLE(exe, name='MyApp.app', icon='app.icns', info_plist={})
"""
# 常见问题解决
"""
1. ImportError: 找不到模块
解决: 使用 --hidden-import 或在 spec 中添加 hiddenimports
2. 文件找不到
解决: 使用 sys._MEIPASS 获取运行时资源路径
"""
import sys
import os
def resource_path(relative_path: str) -> str:
"""获取资源文件的绝对路径(兼容 PyInstaller 打包)"""
if hasattr(sys, '_MEIPASS'):
# PyInstaller 打包后的临时目录
return os.path.join(sys._MEIPASS, relative_path)
# 开发环境
return os.path.join(os.path.dirname(__file__), relative_path)
# 使用
# config_path = resource_path("config.yaml")Nuitka 编译
# ============================================
# Nuitka — 将 Python 编译为 C 代码再编译为可执行文件
# ============================================
"""
# 安装
pip install nuitka
# 基础编译
nuitka --onefile my_script.py
# 启用插件
nuitka --onefile --enable-plugin=pyqt5 my_gui_app.py
# macOS App Bundle
nuitka --onefile --macos-create-app-bundle my_app.py
# Windows 服务
nuitka --onefile --windows-service my_service.py
# 优化选项
nuitka --onefile \
--lto=yes \ # 链接时优化
--jobs=4 \ # 并行编译
--python-flag=no_site \ # 不包含 site-packages
--nofollow-import-to=tkinter \ # 排除模块
--nofollow-import-to=test \
my_script.py
# PyInstaller vs Nuitka 对比:
# | 特性 | PyInstaller | Nuitka |
# |--------------|-------------------|-------------------|
# | 原理 | 打包+压缩 | 编译为C再编译 |
# | 启动速度 | 较慢(需解压) | 快 |
# | 运行速度 | 不变 | 可能提升 |
# | 代码保护 | 弱(可反编译) | 强(编译为机器码) |
# | 体积 | 较大 | 较大 |
# | 构建速度 | 快 | 慢(需要编译) |
# | 兼容性 | 好 | 部分库不兼容 |
"""Docker 打包
# Dockerfile — Python 应用容器化
# 多阶段构建: 减小最终镜像体积
# 阶段 1: 构建
FROM python:3.12-slim AS builder
WORKDIR /build
# 先复制依赖文件(利用 Docker 缓存)
COPY requirements.txt .
# 安装依赖到虚拟环境
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
RUN pip install --no-cache-dir -r requirements.txt
# 阶段 2: 运行
FROM python:3.12-slim
# 安全: 非 root 用户
RUN groupadd -r appuser && useradd -r -g appuser -d /app -s /sbin/nologin appuser
WORKDIR /app
# 从构建阶段复制虚拟环境
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# 复制应用代码
COPY src/ ./src/
COPY pyproject.toml .
# 安装应用(editable 模式不需要)
RUN pip install --no-deps .
# 切换用户
USER appuser
EXPOSE 8000
# 健康检查
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"
CMD ["python", "-m", "my_app.main"]# docker-compose.yml — 多服务打包
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "8000:8000"
environment:
- APP_ENV=production
volumes:
- ./data:/app/data # 数据持久化
# 开发环境(挂载源代码)
app-dev:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "8000:8000"
volumes:
- ./src:/app/src # 代码热更新
- ./tests:/app/tests
environment:
- APP_ENV=development私有 PyPI (devpi)
# ============================================
# devpi — 私有 PyPI 服务器
# ============================================
"""
# 安装
pip install devpi-server devpi-web
# 初始化
devpi-init
# 启动服务
devpi-server --host 0.0.0.0 --port 3141
# 创建用户和索引
devpi use http://localhost:3141
devpi login root --password ''
devpi user -c myteam password=teamsecret email=team@example.com
devpi login myteam --password teamsecret
devpi index -c team/prod bases=root/pypi volatile=False
devpi index -c team/dev bases=team/prod volatile=True
# 上传包
devpi upload --index team/prod
# 从私有 PyPI 安装
pip install --index-url http://localhost:3141/team/prod/+simple/ \
--trusted-host localhost \
my-internal-lib
# pip.conf 配置(永久使用)
# [global]
# index-url = http://pypi.company.com/team/prod/+simple/
# trusted-host = pypi.company.com
# 客户端 devpi 配置
# devpi use http://pypi.company.com/team/prod
"""
# pip.conf / pip.ini 位置:
# Linux: ~/.config/pip/pip.conf
# macOS: ~/Library/Application Support/pip/pip.conf
# Windows: %APPDATA%\pip\pip.ini版本管理 (SemVer)
# ============================================
# 语义化版本 (Semantic Versioning)
# ============================================
"""
版本格式: MAJOR.MINOR.PATCH[-PRERELEASE][+BUILD]
MAJOR: 不兼容的 API 变更
MINOR: 向后兼容的功能新增
PATCH: 向后兼容的 Bug 修复
PRERELEASE: alpha, beta, rc
1.0.0-alpha.1 < 1.0.0-beta.1 < 1.0.0-rc.1 < 1.0.0
示例:
0.1.0 — 初始开发版本
0.2.0 — 新增功能
1.0.0 — 第一个稳定版
1.1.0 — 新增功能(兼容 1.0)
1.1.1 — Bug 修复
2.0.0 — 破坏性变更
2.0.0-beta.1 — 测试版
"""
# 使用 setuptools_scm 自动管理版本(基于 Git tag)
# pyproject.toml:
# [build-system]
# requires = ["setuptools>=64", "setuptools_scm>=8"]
#
# [project]
# dynamic = ["version"]
#
# [tool.setuptools_scm]
# version_scheme = "guess-next-dev"
# local_scheme = "node-and-date"
# 手动版本管理工具
class VersionManager:
"""版本管理工具"""
@staticmethod
def parse_version(version_str: str) -> dict:
"""解析版本号"""
import re
pattern = r'^(\d+)\.(\d+)\.(\d+)(?:-(alpha|beta|rc)\.(\d+))?$'
match = re.match(pattern, version_str)
if not match:
raise ValueError(f"无效版本号: {version_str}")
major, minor, patch = int(match.group(1)), int(match.group(2)), int(match.group(3))
prerelease = match.group(4)
prerelease_num = int(match.group(5)) if match.group(5) else None
return {
"major": major,
"minor": minor,
"patch": patch,
"prerelease": prerelease,
"prerelease_num": prerelease_num,
"full": version_str,
}
@staticmethod
def bump_version(version_str: str, bump_type: str) -> str:
"""升级版本号"""
v = VersionManager.parse_version(version_str)
if bump_type == "major":
return f"{v['major'] + 1}.0.0"
elif bump_type == "minor":
return f"{v['major']}.{v['minor'] + 1}.0"
elif bump_type == "patch":
return f"{v['major']}.{v['minor']}.{v['patch'] + 1}"
else:
raise ValueError(f"未知类型: {bump_type}")
@staticmethod
def generate_changelog(version: str, changes: list) -> str:
"""生成变更日志"""
from datetime import datetime
date_str = datetime.now().strftime("%Y-%m-%d")
categories = {"feat": [], "fix": [], "break": [], "other": []}
for change in changes:
if change.startswith("feat"):
categories["feat"].append(change)
elif change.startswith("fix"):
categories["fix"].append(change)
elif change.startswith("break"):
categories["break"].append(change)
else:
categories["other"].append(change)
lines = [f"## {version} ({date_str})", ""]
if categories["break"]:
lines.append("### 破坏性变更")
for c in categories["break"]:
lines.append(f"- {c}")
lines.append("")
if categories["feat"]:
lines.append("### 新功能")
for c in categories["feat"]:
lines.append(f"- {c}")
lines.append("")
if categories["fix"]:
lines.append("### Bug 修复")
for c in categories["fix"]:
lines.append(f"- {c}")
lines.append("")
return "\n".join(lines)
# 使用示例
vm = VersionManager()
print(vm.bump_version("1.2.3", "patch")) # 1.2.4
print(vm.bump_version("1.2.3", "minor")) # 1.3.0
print(vm.bump_version("1.2.3", "major")) # 2.0.0CI/CD 发布流水线
# .github/workflows/publish.yml — 自动发布到 PyPI
name: Publish to PyPI
on:
release:
types: [published]
workflow_dispatch:
inputs:
test:
description: '发布到 TestPyPI'
type: boolean
default: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: 安装依赖
run: |
pip install -e ".[dev]"
- name: 运行测试
run: |
pytest --cov=src tests/
- name: 类型检查
run: mypy src/
- name: 代码风格
run: ruff check src/
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # setuptools_scm 需要 git 历史
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: 安装构建工具
run: pip install build twine
- name: 构建包
run: python -m build
- name: 检查包
run: twine check dist/*
- name: 上传 artifacts
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
publish-testpypi:
needs: build
runs-on: ubuntu-latest
if: ${{ github.event.inputs.test == 'true' }}
environment:
name: testpypi
url: https://test.pypi.org/p/my-awesome-lib
permissions:
id-token: write # Trusted Publishing
steps:
- name: 下载 artifacts
uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- name: 发布到 TestPyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
publish-pypi:
needs: build
runs-on: ubuntu-latest
if: ${{ github.event_name == 'release' }}
environment:
name: pypi
url: https://pypi.org/p/my-awesome-lib
permissions:
id-token: write
steps:
- name: 下载 artifacts
uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- name: 发布到 PyPI
uses: pypa/gh-action-pypi-publish@release/v1优点
缺点
性能注意事项
- Wheel 安装速度:比 sdist 快 10-100 倍,尽量提供 wheel
- PyInstaller 启动:--onefile 模式需要解压到临时目录,启动慢 2-5 秒
- Nuitka 编译时间:中大型项目编译需要 5-30 分钟
- Docker 镜像大小:slim 基础镜像 + 多阶段构建可控制在 100MB 以内
- PyPI 上传大小:单个文件限制 60MB,超大包需联系 PyPI 管理员
- pip 安装依赖解析:依赖多时解析耗时,使用 pip cache 加速
总结
Python 打包与分发的核心是使用 pyproject.toml 标准化项目配置,通过 build + twine 构建和发布到 PyPI。对于 CLI 工具,使用 entry_points 注册命令行入口。对于独立可执行文件,PyInstaller 适合快速打包,Nuitka 适合需要代码保护的场景。生产环境建议使用私有 PyPI (devpi) 管理内部包。
关键知识点
- pyproject.toml — 现代 Python 项目配置标准(PEP 517/518)
- Wheel vs sdist — 预编译二进制 vs 源代码分发
- Entry Points — console_scripts 注册 CLI 命令
- PyInstaller — 打包为独立可执行文件
- Nuitka — 编译为 C 代码,更强代码保护
- SemVer — 语义化版本管理
- devpi — 私有 PyPI 服务器
- Trusted Publishing — PyPI 官方推荐的 CI/CD 发布方式
常见误区
- 还在用 setup.py install:已废弃,使用 pip install .
- 不提供 wheel:只有 sdist 会导致用户安装慢且可能构建失败
- 硬编码版本号:应使用 setuptools_scm 从 Git tag 自动获取
- PyInstaller 万能:部分动态导入的库需要手动指定 hidden-import
- 忽略 .pyc 缓存:分发时排除 pycache,减小包体积
- 不签名的包:生产环境应使用 Trusted Publishing 或 GPG 签名
进阶路线
- 入门:pyproject.toml 配置、pip install -e . 开发模式
- 进阶:构建 wheel、发布到 PyPI、CLI 工具打包
- 高级:PyInstaller/Nuitka 可执行文件、devpi 私有仓库、CI/CD 发布
- 专家:C 扩展打包、多平台 wheel 构建、Namespace Packages
适用场景
- 开发可复用的 Python 库
- 构建 CLI 命令行工具
- 将 Python 应用分发给非技术用户
- 企业内部包管理
- 开源项目发布
落地建议
- 第一步:为项目创建 pyproject.toml,使用 pip install -e . 开发
- 第二步:配置 CLI 入口点,本地测试命令行工具
- 第三步:使用 python -m build 构建并检查 wheel
- 第四步:在 CI 中配置自动测试和发布
- 第五步:发布到 PyPI 或内部 devpi
- 持续:每次发版更新 CHANGELOG、打 Git tag
排错清单
复盘问题
- 上次发布是否有问题?用户反馈什么?
- wheel 和 sdist 的下载比例是多少?
- 当前支持的 Python 版本是否合理?是否需要放弃旧版本?
- 依赖是否有安全漏洞?定期运行 pip-audit
- 包的安装时间是否合理?是否有不必要的依赖?
- 版本号是否遵循 SemVer?用户是否能预期升级影响?
