Python 打包实战精要:基于 pyproject.toml 的现代分发、CLI 与 PyPI 发布全流程
【免费下载链接】agentsMulti-harness agentic plugin marketplace for Claude Code, Codex, Cursor, OpenCode, GitHub Copilot, and Google Antigravity项目地址: https://gitcode.com/GitHub_Trending/agents24/agents
本文是 python-packaging 技能 的详细模式参考(references/details.md)的技术解读,面向需要将 Python 库、CLI 工具发布到 PyPI 或私有索引的开发者。读完本文,你将掌握一套可直接复制的"全功能 pyproject.toml"配置、动态版本管理、Click/argparse 两种 CLI 注册方式,以及从本地构建到 GitHub Actions 自动化发布的完整链路,并看到这些模式在当前仓库真实工程(如 plugin-eval、yt-design-extractor)中的落地形态。
文档定位:从导航到细节的第三层
该技能采用"导航 → 快速上手 → 细节参考"的三级结构:SKILL.md负责给出包结构、构建后端(setuptools/hatchling/flit/poetry)、PEP 517/518/621/660 等核心概念与最小化示例;references/details.md则是"详细模式与完整示例"(Detailed patterns and worked examples)的承载文件,编号从 Pattern 4 继续向后延伸,覆盖完整配置、动态版本、CLI、构建与发布四大板块。当导航层信息不足以支撑实际落地时,就应读取该细节文档。本文即以此为骨架展开。
Pattern 4:一份可直接落地的全功能 pyproject.toml
details.md给出了从构建系统、项目元数据到工具链配置一应俱全的完整示例。下面按区块逐段拆解其作用与含义。
build-system:声明构建后端
[build-system] requires = ["setuptools>=61.0", "wheel"] build-backend = "setuptools.build_meta"这是 PEP 517/518 所要求的最小声明:requires列出构建时需要的隔离依赖(此处要求 setuptools 不低于 61.0,这是支持 pyproject.toml 元数据的关键版本门槛),build-backend指定由setuptools.build_meta负责实际构建。wheel的加入确保能同时产出 wheel 与源码分发包。
project:PEP 621 标准元数据
[project] name = "my-awesome-package" version = "1.0.0" description = "An awesome Python package" readme = "README.md" requires-python = ">=3.8" license = {text = "MIT"} authors = [ {name = "Your Name", email = "you@example.com"}, ] maintainers = [ {name = "Maintainer Name", email = "maintainer@example.com"}, ] keywords = ["example", "package", "awesome"] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", ] dependencies = [ "requests>=2.28.0,<3.0.0", "click>=8.0.0", "pydantic>=2.0.0", ]要点说明:
readme支持字符串(文件名)或 TOML 表(指定 content-type 与文本内容);license的{text = "MIT"}写法适用于未使用 SPDX 表达式作为license字符串的 setuptools 版本。classifiers是 PyPI 页面筛选与展示的核心元数据,务必同时声明License :: OSI Approved :: ...和各Programming Language :: Python :: 3.x条目,并保持与requires-python一致。dependencies使用 PEP 508 规范说明符。示例中的requests>=2.28.0,<3.0.0是典型的"兼容范围"写法:既排除过旧版本,又用上限挡住未来不兼容的大版本。
optional-dependencies:特性开关
[project.optional-dependencies] dev = [ "pytest>=7.0.0", "pytest-cov>=4.0.0", "black>=23.0.0", "ruff>=0.1.0", "mypy>=1.0.0", ] docs = [ "sphinx>=5.0.0", "sphinx-rtd-theme>=1.0.0", ] all = [ "my-awesome-package[dev,docs]", ]dev/docs分组可让用户按需安装(pip install -e ".[dev]");all分组通过自引用语法my-awesome-package[dev,docs]一次性聚合全部可选依赖,是发布时常用的"全家桶"入口。这一模式在当前仓库的 plugins/plugin-eval/pyproject.toml 中同样可见——它将llm(claude-agent-sdk)、api(anthropic)、dev(pytest/ruff/ty)分别分组,形成依赖分层。
urls 与 scripts:项目链接与命令行入口
[project.urls] Homepage = "https://github.com/username/my-awesome-package" Documentation = "https://my-awesome-package.readthedocs.io" Repository = "https://github.com/username/my-awesome-package" "Bug Tracker" = "https://github.com/username/my-awesome-package/issues" Changelog = "https://github.com/username/my-awesome-package/blob/main/CHANGELOG.md" [project.scripts] my-cli = "my_package.cli:main" awesome-tool = "my_package.tools:run" [project.entry-points."my_package.plugins"] plugin1 = "my_package.plugins:plugin1"[project.scripts]会在安装时自动生成可执行命令(如my-cli指向my_package.cli:main),这是发布 CLI 工具的标准做法;[project.entry-points]则允许第三方通过"插件发现"机制向你的包注册扩展点。当前仓库中 plugin-eval 的 pyproject.toml 即注册了plugin-eval = "plugin_eval.cli:app"这一脚本入口,指向 cli.py 中由 typer 创建的应用对象。
tool.setuptools:包发现与数据文件
[tool.setuptools] package-dir = {"" = "src"} zip-safe = false [tool.setuptools.packages.find] where = ["src"] include = ["my_package*"] exclude = ["tests*"] [tool.setuptools.package-data] my_package = ["py.typed", "*.pyi", "data/*.json"]package-dir = {"" = "src"}将包根目录指向src/,配合packages.find.where = ["src"]实现 src 布局(source layout)自动发现;include/exclude控制匹配范围。zip-safe = false显式声明包不做 zip 导入,避免资源文件在 zip 环境下失效。package-data声明随包安装的非 Python 文件:py.typed是 PEP 561 类型标记,*.pyi是存根文件,data/*.json是运行时数据。这些资源在运行时可通过importlib.resources读取(见后文高级模式)。
tool.black / tool.ruff / tool.mypy / tool.pytest / tool.coverage:工具链统一配置
[tool.black] line-length = 100 target-version = ["py38", "py39", "py310", "py311"] include = '\.pyi?$' [tool.ruff] line-length = 100 target-version = "py38" [tool.ruff.lint] select = ["E", "F", "I", "N", "W", "UP"] [tool.mypy] python_version = "3.8" warn_return_any = true warn_unused_configs = true disallow_untyped_defs = true [tool.pytest.ini_options] testpaths = ["tests"] python_files = ["test_*.py"] addopts = "-v --cov=my_package --cov-report=term-missing" [tool.coverage.run] source = ["src"] omit = ["*/tests/*"] [tool.coverage.report] exclude_lines = [ "pragma: no cover", "def __repr__", "raise AssertionError", "raise NotImplementedError", ]将格式化、lint、类型检查、测试与覆盖率配置全部收拢进 pyproject.toml,实现了"单一配置源"。当前仓库的做法与此一致:plugin-eval 在 pyproject.toml 中配置了[tool.ruff](line-length 100)、[tool.ruff.lint]、[tool.ty]与[tool.pytest.ini_options],并且仓库 Makefile 中的lint/test目标均以uv run --extra dev方式在该项目目录内执行,确保使用锁定的工具版本。
Pattern 5:动态版本管理
手工维护version = "1.0.0"容易与 git tag 脱节,动态版本是发布自动化的重要一环:
[build-system] requires = ["setuptools>=61.0", "setuptools-scm>=8.0"] build-backend = "setuptools.build_meta" [project] name = "my-package" dynamic = ["version"] description = "Package with dynamic version" [tool.setuptools.dynamic] version = {attr = "my_package.__version__"} # Or use setuptools-scm for git-based versioning [tool.setuptools_scm] write_to = "src/my_package/_version.py"两种方式二选一:
属性读取:
[tool.setuptools.dynamic] version = {attr = "my_package.__version__"}从源码读取版本字符串,配合:# src/my_package/__init__.py __version__ = "1.0.0" # Or with setuptools-scm from importlib.metadata import version __version__ = version("my-package")git 派生(setuptools-scm):
[tool.setuptools_scm]根据 git tag 自动推导版本并写入_version.py,彻底消除手工同步。注意使用dynamic = ["version"]后就不能再在[project]中写静态version。
CLI 模式:Click 与 argparse 双方案
Pattern 6:基于 Click 的分组命令
# src/my_package/cli.py import click @click.group() @click.version_option() def cli(): """My awesome CLI tool.""" pass @cli.command() @click.argument("name") @click.option("--greeting", default="Hello", help="Greeting to use") def greet(name: str, greeting: str): """Greet someone.""" click.echo(f"{greeting}, {name}!") @cli.command() @click.option("--count", default=1, help="Number of times to repeat") def repeat(count: int): """Repeat a message.""" for i in range(count): click.echo(f"Message {i + 1}") def main(): """Entry point for CLI.""" cli() if __name__ == "__main__": main()在 pyproject.toml 中注册并测试:
[project.scripts] my-tool = "my_package.cli:main"pip install -e . my-tool greet World my-tool greet Alice --greeting="Hi" my-tool repeat --count=3@click.group()将cli变为命令组,子命令通过@cli.command()挂载;@click.version_option()自动提供--version。pip install -e .(PEP 660 可编辑安装)后即可直接以my-tool命令名调用。
Pattern 7:基于 argparse 的子命令解析
# src/my_package/cli.py import argparse import sys def main(): """Main CLI entry point.""" parser = argparse.ArgumentParser( description="My awesome tool", prog="my-tool" ) parser.add_argument( "--version", action="version", version="%(prog)s 1.0.0" ) subparsers = parser.add_subparsers(dest="command", help="Commands") # Add subcommand process_parser = subparsers.add_parser("process", help="Process data") process_parser.add_argument("input_file", help="Input file path") process_parser.add_argument( "--output", "-o", default="output.txt", help="Output file path" ) args = parser.parse_args() if args.command == "process": process_data(args.input_file, args.output) else: parser.print_help() sys.exit(1) def process_data(input_file: str, output_file: str): """Process data from input to output.""" print(f"Processing {input_file} -> {output_file}") if __name__ == "__main__": main()argparse 是标准库方案,零第三方依赖。核心手法是add_subparsers(dest="command")声明子命令,action="version"提供--version,未匹配命令时print_help()并sys.exit(1)。Click 适合命令树复杂、需要丰富交互体验的场景,argparse 适合追求最小依赖的纯标准库工具——选择取决于项目约束。
构建与发布全流程
Pattern 8:本地构建
# Install build tools pip install build twine # Build distribution python -m build # This creates: # dist/ # my-package-1.0.0.tar.gz (source distribution) # my_package-1.0.0-py3-none-any.whl (wheel) # Check the distribution twine check dist/*python -m build会同时产出 sdist(.tar.gz,源码分发包)与 wheel(.whl,平台无关的纯 Python 轮子,文件名中的py3-none-any即"Python 3、无 ABI、任意平台")。twine check在发布前校验分发包元数据(README 渲染、长描述格式、缺失字段等)是否合规。
Pattern 9:发布到 PyPI
# Install publishing tools pip install twine # Test on TestPyPI first twine upload --repository testpypi dist/* # Install from TestPyPI to test pip install --index-url https://test.pypi.org/simple/ my-package # If all good, publish to PyPI twine upload dist/*规范流程是"先在 TestPyPI 试上传、试安装,确认无误后再正式发布"。推荐使用 API token 而非账号密码,配置~/.pypirc:
# Create ~/.pypirc [distutils] index-servers = pypi testpypi [pypi] username = __token__ password = pypi-...your-token... [testpypi] username = __token__ password = pypi-...your-test-token...Pattern 10:GitHub Actions 自动化发布
# .github/workflows/publish.yml name: Publish to PyPI on: release: types: [created] jobs: publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: python-version: "3.11" - name: Install dependencies run: | pip install build twine - name: Build package run: python -m build - name: Check package run: twine check dist/* - name: Publish to PyPI env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} run: twine upload dist/*工作流以release事件(types: [created])为触发器,串起"checkout → 装构建工具 → 构建 → twine check → 用PYPI_API_TOKEN密钥上传"五步。TWINE_USERNAME: __token__配合仓库 Secrets 中的PYPI_API_TOKEN,避免了在 CI 日志中泄露凭据。
仓库实践印证:真实的 pyproject.toml 与 CLI 落地
上述模式并非纸上谈兵,当前仓库内部就有多个可对照的真实工程:
- plugin-eval(pyproject.toml):采用
hatchling构建后端与packages = ["src/plugin_eval"]的 src 布局;[project.scripts]注册plugin-eval = "plugin_eval.cli:app";可选依赖按llm/api/dev分组;[tool.pytest.ini_options]与[tool.ruff]集中配置。其 CLI 在 cli.py 中基于 typer(Click 的上层封装)实现score/certify/init等命令,与 details.md 中"用[project.scripts]注册入口点"的思路完全一致——这正是my-tool = "my_package.cli:main"模式的工程级印证。 - yt-design-extractor(pyproject.toml):展示了
[project.optional-dependencies]控制重依赖的实践——easyocr分组携带 torch/torchvision(约 2GB),注释明确提示用uv sync --extra easyocr按需安装;[tool.uv] package = false声明其为"仅依赖工程",脚本通过uv run yt-design-extractor.py直接执行而非安装为包。 - Makefile(Makefile):仓库约定所有 Python 工具链统一走
uv,lint/test目标都以uv run --extra dev在 plugin-eval 项目目录内执行,保证 ruff/ty 版本与 CI 锁定一致——呼应了 Pattern 4 中"工具链配置入 pyproject.toml"的价值。
延伸阅读:高级模式索引
details.md末尾将更进阶的主题指向 references/advanced-patterns.md,其中覆盖:
- Pattern 11数据文件打包:
[tool.setuptools.package-data]声明data/*.json、templates/*.html等资源,运行时用importlib.resources.files("my_package").joinpath(...)读取(Python 3.9+); - Pattern 12命名空间包:多仓库共享
company/命名空间,命名空间目录不写__init__.py,各包用include = ["company.core*"]限定; - Pattern 13C 扩展:pyproject.toml 的
[tool.setuptools] ext-modules或传统setup.py的Extension; - Pattern 14-15语义化版本与 setuptools-scm 的 git 派生版本(
1.0.1.dev3+g1234567); - Pattern 16-17
pip install -e .可编辑安装与隔离环境安装验证; - Pattern 18-20README 模板、cibuildwheel 多架构 wheel、私有索引(
--index-url/twine upload --repository-url); - 以及
.gitignore、MANIFEST.in模板与发布前的逐项检查清单(测试、文档、版本号、CHANGELOG、License、TestPyPI 先行、git tag 等)。
至此,从"全功能 pyproject.toml"到"动态版本 + CLI + 构建发布 + 自动化 CI",再到仓库内真实工程的对照印证,你已经拥有了一套可完整覆盖 Python 库/工具打包发布全生命周期的操作指南。
【免费下载链接】agentsMulti-harness agentic plugin marketplace for Claude Code, Codex, Cursor, OpenCode, GitHub Copilot, and Google Antigravity项目地址: https://gitcode.com/GitHub_Trending/agents24/agents
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考