python

关注公众号 jb51net

关闭
首页 > 脚本专栏 > python > Python项目打包与发布

Python项目打包与发布的三大工具(setuptools、Poetry、Flit)使用完全指南

作者:铭渊老黄

这篇文章全面介绍了Python打包与发布的三大工具,包括setuptools、Poetry和Flit,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下

引言:当你的代码值得被世界看见

每个 Python 开发者都有这样的时刻——你写了一个解决实际问题的工具,同事们用了都说好,你开始思考:能不能让更多人用上它?

这就是打包与发布的起点。

但随之而来的困惑往往让人望而却步:setup.pypyproject.tomlMANIFEST.inwheelsdist……这些概念是什么?setuptoolsPoetryFlit 又该选哪个?如何把包发布到 PyPI?

我做了十几年 Python 开发,亲历了从手写 setup.py 到现代 pyproject.toml 的整个演变。今天这篇文章,就是我希望当年入门时就能读到的那篇指南——带你从零到一,真正搞清楚 Python 打包这件事。

第一章:先理解"打包"究竟是什么

在动手之前,先建立清晰的概念模型。

1.1 为什么需要打包

你的项目目录

├── mylib/
│   ├── __init__.py
│   ├── core.py
│   └── utils.py
├── tests/
├── README.md
└── requirements.txt

别人想用你的库,直接复制文件夹?显然不够优雅。打包解决了这些问题:

1.2 两种分发格式

发布产物

├── mylib-1.0.0.tar.gz      ← sdist(源码发行包)
└── mylib-1.0.0-py3-none-any.whl  ← wheel(二进制发行包)

1.3 配置文件的演变

Python 打包配置的历史时间线

2000s    setup.py(纯 Python 脚本,灵活但混乱)
   │
2012     setup.cfg(声明式配置,分离逻辑与元数据)
   │
2016     PEP 518 提出 pyproject.toml(统一的配置标准)
   │
2021+    pyproject.toml 成为主流,各工具纷纷支持

现在的最佳实践:使用 pyproject.toml 作为主配置文件,告别 setup.py

第二章:三大工具横向对比

2.1 Setuptools——老牌权威

适用场景:复杂项目、需要编译扩展(C/C++ 扩展)、需要高度自定义构建过程

# pyproject.toml(现代 setuptools 写法)
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.backends.legacy:build"

[project]
name = "myawesome-lib"
version = "1.0.0"
description = "一个很棒的 Python 库"
readme = "README.md"
license = {text = "MIT"}
authors = [
    {name = "Your Name", email = "you@example.com"}
]
requires-python = ">=3.8"
dependencies = [
    "requests>=2.28.0",
    "click>=8.0",
]

[project.optional-dependencies]
dev = ["pytest>=7.0", "black", "mypy"]
docs = ["sphinx", "sphinx-rtd-theme"]

[project.scripts]
mylib-cli = "mylib.cli:main"

[project.urls]
Homepage = "https://github.com/yourname/mylib"
Documentation = "https://mylib.readthedocs.io"

对应的项目结构:

myawesome-lib/
├── pyproject.toml
├── README.md
├── LICENSE
├── src/
│   └── mylib/          ← 推荐 src 布局
│       ├── __init__.py
│       ├── core.py
│       └── cli.py
└── tests/
    └── test_core.py

为什么推荐 src 布局?它能防止你在开发时意外导入本地未安装版本,让测试更加可靠。

优点:生态最成熟,文档丰富,支持所有复杂场景

缺点:配置项繁多,学习曲线稍陡,依赖管理需配合 pip-tools 等工具

2.2 Poetry——现代全能选手

适用场景:日常 Python 项目,既要管理依赖又要打包发布,追求开发体验

Poetry 是目前我最推荐的工具,原因很简单:它把虚拟环境管理、依赖管理、打包发布整合成了一套流畅的工作流。

# 创建新项目
poetry new myawesome-lib

# 目录结构自动生成
myawesome-lib/
├── pyproject.toml
├── README.md
├── myawesome_lib/
│   └── __init__.py
└── tests/
    └── __init__.py

生成的 pyproject.toml

[tool.poetry]
name = "myawesome-lib"
version = "0.1.0"
description = "一个很棒的 Python 库"
authors = ["Your Name <you@example.com>"]
readme = "README.md"
license = "MIT"
homepage = "https://github.com/yourname/myawesome-lib"
repository = "https://github.com/yourname/myawesome-lib"
keywords = ["python", "library"]
classifiers = [
    "Development Status :: 3 - Alpha",
    "Intended Audience :: Developers",
]

[tool.poetry.dependencies]
python = "^3.8"
requests = "^2.28.0"
click = "^8.0"

[tool.poetry.group.dev.dependencies]
pytest = "^7.0"
black = "^23.0"
mypy = "^1.0"

[tool.poetry.scripts]
mylib-cli = "myawesome_lib.cli:main"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

核心工作流

# 添加依赖(自动更新 pyproject.toml 和 poetry.lock)
poetry add requests

# 添加开发依赖
poetry add --group dev pytest black

# 安装所有依赖(包含 lock 文件确保版本一致)
poetry install

# 运行项目内命令
poetry run python main.py
poetry run pytest

# 构建发行包
poetry build
# 输出:dist/myawesome_lib-0.1.0.tar.gz
#       dist/myawesome_lib-0.1.0-py3-none-any.whl

# 发布到 PyPI(一键完成)
poetry publish

依赖版本控制语义

[tool.poetry.dependencies]
requests = "^2.28.0"   # 兼容 2.x,不升级到 3.x
click = "~8.0.0"       # 只允许补丁版本升级
numpy = ">=1.20,<2.0"  # 区间限定
flask = "*"             # 任意版本(不推荐)

优点:一体化体验、lock 文件保证可复现、依赖解析强大、发布流程简洁

缺点:不支持 C 扩展编译、相对较重(但绝大多数项目不需要编译扩展)

2.3 Flit——极简主义者的选择

适用场景:纯 Python 库、结构简单、希望配置尽量少

# pyproject.toml
[build-system]
requires = ["flit_core>=3.4"]
build-backend = "flit_core.buildapi"

[project]
name = "mylib"
dynamic = ["version", "description"]  # 从代码中读取
readme = "README.md"
license = {file = "LICENSE"}
authors = [{name = "Your Name", email = "you@example.com"}]
requires-python = ">=3.8"
dependencies = ["requests"]

版本号和描述直接从模块文件中读取:

# mylib/__init__.py
"""一个极简的 Python 库。"""  # 这是包的 description

__version__ = "1.0.0"         # 这是版本号
# 构建
flit build

# 发布
flit publish

优点:配置极少,上手快,适合小型纯 Python 包

缺点:功能有限,不做依赖管理,复杂项目力不从心

2.4 三者对比速查表

功能维度setuptools PoetryFlit
虚拟环境管理 ✓
依赖管理需配合工具 ✓✗(仅声明)
lock 文件 ✓
C 扩展支持 ✓
打包发布 ✓ ✓ ✓
配置复杂度 中
生态成熟度最高 中
推荐场景  复杂/C扩展日常项目极简小包

我的选择建议

第三章:实战——从零发布一个真实的包

3.1 项目实战:发布一个命令行文件统计工具

我们以 Poetry 为例,完整走遍发布流程。

需求:统计目录下各类型文件的数量和大小,支持命令行调用。

# 初始化项目
poetry new filestats
cd filestats

实现核心功能

# filestats/core.py
from pathlib import Path
from collections import defaultdict
from dataclasses import dataclass

@dataclass
class FileStats:
    count: int = 0
    total_size: int = 0
    
    @property
    def size_mb(self) -> float:
        return self.total_size / (1024 * 1024)

def analyze_directory(
    path: str | Path,
    recursive: bool = True
) -> dict[str, FileStats]:
    """分析目录中各类型文件的统计信息"""
    root = Path(path)
    if not root.exists():
        raise FileNotFoundError(f"路径不存在:{path}")
    
    stats: dict[str, FileStats] = defaultdict(FileStats)
    
    pattern = "**/*" if recursive else "*"
    for file_path in root.glob(pattern):
        if not file_path.is_file():
            continue
        
        suffix = file_path.suffix.lower() or "(无扩展名)"
        stats[suffix].count += 1
        stats[suffix].total_size += file_path.stat().st_size
    
    return dict(sorted(stats.items(), key=lambda x: x[1].count, reverse=True))
# filestats/cli.py
import click
from pathlib import Path
from .core import analyze_directory

@click.command()
@click.argument("path", default=".", type=click.Path(exists=True))
@click.option("--no-recursive", is_flag=True, help="不递归子目录")
@click.option("--top", default=10, help="显示前 N 种文件类型")
def main(path: str, no_recursive: bool, top: int) -> None:
    """统计目录中各类型文件的数量和大小"""
    try:
        stats = analyze_directory(path, recursive=not no_recursive)
    except FileNotFoundError as e:
        click.echo(f"错误:{e}", err=True)
        raise click.Abort()
    
    click.echo(f"\n📁 分析路径:{Path(path).resolve()}")
    click.echo(f"{'扩展名':<15} {'数量':>8} {'大小(MB)':>12}")
    click.echo("-" * 38)
    
    for ext, stat in list(stats.items())[:top]:
        click.echo(f"{ext:<15} {stat.count:>8} {stat.size_mb:>11.2f}")
    
    total_files = sum(s.count for s in stats.values())
    click.echo(f"\n共计 {total_files} 个文件,{len(stats)} 种类型")

注册 CLI 入口

[tool.poetry.scripts]
filestats = "filestats.cli:main"

本地测试

poetry install
poetry run filestats /path/to/any/directory --top 5

输出示例:

📁 分析路径:/Users/dev/myproject
扩展名          数量         大小(MB)
──────────────────────────────────────
.py              342         1.25
.json             89         0.43
.md               31         0.18
.txt              18         0.08
.yml              12         0.02

共计 492 个文件,5 种类型

3.2 编写测试(发布前必做)

# tests/test_core.py
import pytest
from pathlib import Path
import tempfile
from filestats.core import analyze_directory, FileStats

@pytest.fixture
def temp_dir(tmp_path: Path) -> Path:
    """创建测试用临时目录"""
    (tmp_path / "a.py").write_text("print('hello')")
    (tmp_path / "b.py").write_text("x = 1")
    (tmp_path / "readme.md").write_text("# Test")
    subdir = tmp_path / "sub"
    subdir.mkdir()
    (subdir / "c.py").write_text("pass")
    return tmp_path

def test_analyze_directory_counts(temp_dir: Path) -> None:
    stats = analyze_directory(temp_dir)
    assert stats[".py"].count == 3
    assert stats[".md"].count == 1

def test_analyze_directory_non_recursive(temp_dir: Path) -> None:
    stats = analyze_directory(temp_dir, recursive=False)
    assert stats[".py"].count == 2  # 不含子目录

def test_invalid_path() -> None:
    with pytest.raises(FileNotFoundError):
        analyze_directory("/nonexistent/path")
poetry run pytest --cov=filestats

第四章:发布到 PyPI 的完整流程

4.1 准备工作

第一步:注册账号

第二步:配置 API Token(推荐,比密码更安全)

在 PyPI 账户设置中创建 API Token,然后配置 Poetry:

poetry config pypi-token.pypi pypi-AgEIcHlwaS5vcmcXXXXXXXXX
poetry config pypi-token.testpypi pypi-AgEIcHlwaS5vcmcYYYYYYYYY

4.2 构建前的 Checklist

发布前检查清单

版本号规范(SemVer)

1.0.0
│ │ └── 补丁版本:bug 修复,向后兼容
│ └──── 次版本:新功能,向后兼容
└────── 主版本:破坏性变更

# Poetry 提供便捷的版本管理命令
poetry version patch   # 1.0.0 → 1.0.1
poetry version minor   # 1.0.0 → 1.1.0
poetry version major   # 1.0.0 → 2.0.0

4.3 发布流程

# 步骤一:构建发行包
poetry build

# 验证构建结果
ls dist/
# filestats-0.1.0.tar.gz
# filestats-0.1.0-py3-none-any.whl

# 步骤二:先发布到 TestPyPI 验证
poetry publish --repository testpypi

# 步骤三:在新环境中测试安装
pip install --index-url https://test.pypi.org/simple/ filestats
filestats --help  # 验证功能正常

# 步骤四:确认无误后发布正式版
poetry publish

自动化发布——GitHub Actions

# .github/workflows/publish.yml
name: Publish to PyPI

on:
  push:
    tags:
      - "v*"  # 推送 tag 时触发

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 Poetry
        run: pip install poetry
      
      - name: Install dependencies
        run: poetry install
      
      - name: Run tests
        run: poetry run pytest
      
      - name: Build and publish
        env:
          POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }}
        run: |
          poetry build
          poetry publish

发布一个新版本只需要:

poetry version patch
git add pyproject.toml
git commit -m "chore: bump version to $(poetry version -s)"
git tag v$(poetry version -s)
git push && git push --tags
# GitHub Actions 自动完成后续发布

第五章:最佳实践与常见陷阱

5.1 README 是你包的门面

一个好的 README 应该包含:

# filestats

[![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fbadge.fury.io%2Fpy%2Ffilestats.svg&pos_id=img-jHHE1z5y-1772663561917)](https://pypi.org/project/filestats/)
[![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=https%3A%2F%2Fimg.shields.io%2Fbadge%2Fpython-3.8%2B-blue.svg&pos_id=img-8fTVZX00-1772663561919)](https://www.python.org/)

> 快速统计目录中各类型文件的工具

## 安装

\```bash
pip install filestats
\```

## 快速开始

\```bash
# 统计当前目录
filestats .

# 不递归,只显示前 5 种类型
filestats /path/to/dir --no-recursive --top 5
\```

## 作为库使用

\```python
from filestats import analyze_directory
stats = analyze_directory("/path/to/dir")
\```

5.2 常见陷阱与解决方案

陷阱一:包名冲突

在 PyPI 搜索确认名称唯一性。包名用连字符(my-lib),但 Python 导入用下划线(import my_lib),Poetry 会自动处理这个映射。

陷阱二:忘记包含必要文件

# pyproject.toml 中明确包含
[tool.setuptools.package-data]
mylib = ["*.json", "*.yaml", "data/*"]

使用 Poetry 时:

[tool.poetry]
include = ["filestats/data/*.json"]
exclude = ["filestats/tests/*"]

陷阱三:依赖版本过于宽松或过于严格

# ❌ 过于宽松——可能引入不兼容版本
requests = "*"

# ❌ 过于严格——妨碍用户使用其他兼容版本
requests = "2.28.1"

# ✓ 推荐:允许兼容的次版本更新
requests = "^2.28.0"

第六章:前沿展望

6.1 uv——下一代包管理器

Astral 团队(ruff 的作者)推出的 uv 正在快速崛起。用 Rust 编写,速度比 pip 快 10-100 倍:

# 安装 uv
curl -LsSf https://astral.sh/uv/install.sh | sh

# 极速安装依赖
uv pip install requests  # 比 pip 快 10 倍以上

# 未来:uv 可能统一包管理与项目管理
uv init myproject
uv add requests
uv run python main.py

6.2 可信发布(Trusted Publishing)

PyPI 正在推广基于 OIDC 的可信发布,无需存储 Token:

# 在 PyPI 项目设置中配置 GitHub Actions 可信发布后
- name: Publish to PyPI
  uses: pypa/gh-action-pypi-publish@release/v1
  # 不需要任何密钥!OIDC 自动认证

总结

Python 打包生态从混乱走向统一,pyproject.toml 是这场变革的核心。三大工具的选择逻辑清晰:

最重要的一步,永远是迈出第一步。选一个你感兴趣的工具,把你手边的项目打个包,发布到 TestPyPI 试试——这个过程远比想象中简单,而那种"我的代码可以被全世界 pip install"的成就感,真的会上瘾。

以上就是Python项目打包与发布的三大工具(setuptools、Poetry、Flit)使用完全指南的详细内容,更多关于Python项目打包与发布的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文