Python项目打包与发布:PyInstaller打包exe,pyproject.toml发布到PyPI
将Python项目打包可以方便地分发和部署。本篇介绍如何将Python项目打包为可安装的包、可执行文件,以及发布到PyPI。
1. 项目结构
my_package/
├── pyproject.toml # 项目配置(推荐)
├── setup.py # 传统安装脚本(可选)
├── setup.cfg # 配置文件(可选)
├── README.md # 项目说明
├── LICENSE # 许可证
├── CHANGELOG.md # 更新日志
├── requirements.txt # 依赖列表
│
├── src/ # 源代码目录
│ └── my_package/
│ ├── __init__.py
│ ├── main.py
│ ├── utils.py
│ └── cli.py
│
├── tests/ # 测试目录
│ ├── __init__.py
│ └── test_main.py
│
└── docs/ # 文档目录
└── index.md
# src/my_package/__init__.py
"""My Package - 一个示例Python包"""
__version__ = "0.1.0"
__author__ = "Your Name"
from .main import main_function
from .utils import helper_function
__all__ = ['main_function', 'helper_function']
2. pyproject.toml配置
# pyproject.toml - 现代Python项目配置文件
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "my-package"
version = "0.1.0"
description = "一个示例Python包"
readme = "README.md"
license = {text = "MIT"}
requires-python = ">=3.8"
authors = [
{name = "Your Name", email = "[email protected]"}
]
keywords = ["example", "package", "python"]
classifiers = [
"Development Status :: 3 - Alpha",
"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",
"click>=8.0.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"black>=23.0.0",
"flake8>=6.0.0",
"mypy>=1.0.0",
]
docs = [
"sphinx>=5.0.0",
"sphinx-rtd-theme>=1.0.0",
]
# 命令行入口点
[project.scripts]
my-cli = "my_package.cli:main"
# GUI入口点
[project.gui-scripts]
my-gui = "my_package.gui:main"
# 插件入口点
[project.entry-points."my_package.plugins"]
plugin1 = "my_package.plugins:Plugin1"
[project.urls]
Homepage = "https://github.com/username/my-package"
Documentation = "https://my-package.readthedocs.io"
Repository = "https://github.com/username/my-package"
Issues = "https://github.com/username/my-package/issues"
# setuptools配置
[tool.setuptools]
package-dir = {"" = "src"}
[tool.setuptools.packages.find]
where = ["src"]
# 包含数据文件
[tool.setuptools.package-data]
my_package = ["data/*.json", "templates/*.html"]
# 工具配置
[tool.black]
line-length = 100
target-version = ['py38', 'py39', 'py310', 'py311']
[tool.isort]
profile = "black"
line_length = 100
[tool.mypy]
python_version = "3.10"
warn_return_any = true
warn_unused_ignores = true
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = "test_*.py"
3. 构建包
# 安装构建工具
pip install build twine
# 构建包
python -m build
# 构建结果在 dist/ 目录
# dist/
# ├── my_package-0.1.0-py3-none-any.whl # wheel包
# └── my_package-0.1.0.tar.gz # 源码包
# 本地安装测试
pip install dist/my_package-0.1.0-py3-none-any.whl
# 开发模式安装(可编辑)
pip install -e .
# 带可选依赖安装
pip install -e ".[dev]"
pip install -e ".[dev,docs]"
# 传统 setup.py(如果需要)
from setuptools import setup, find_packages
setup(
name="my-package",
version="0.1.0",
packages=find_packages(where="src"),
package_dir={"": "src"},
install_requires=[
"requests>=2.28.0",
"click>=8.0.0",
],
entry_points={
"console_scripts": [
"my-cli=my_package.cli:main",
],
},
python_requires=">=3.8",
)
4. 发布到PyPI
# 1. 注册PyPI账号
# https://pypi.org/account/register/
# 2. 创建API Token
# https://pypi.org/manage/account/token/
# 3. 配置认证
# 创建 ~/.pypirc 文件
# ~/.pypirc
[distutils]
index-servers =
pypi
testpypi
[pypi]
username = __token__
password = pypi-AgEIcHlwaS5vcmc...
[testpypi]
repository = https://test.pypi.org/legacy/
username = __token__
password = pypi-AgENdGVzdC5weXBp...
# 4. 上传到TestPyPI(测试)
python -m twine upload --repository testpypi dist/*
# 从TestPyPI安装测试
pip install --index-url https://test.pypi.org/simple/ my-package
# 5. 上传到PyPI(正式)
python -m twine upload dist/*
# 6. 安装验证
pip install my-package
# 自动化发布脚本
# scripts/release.py
import subprocess
import sys
from pathlib import Path
def run(cmd):
print(f"运行: {cmd}")
result = subprocess.run(cmd, shell=True)
if result.returncode != 0:
sys.exit(1)
def release():
# 清理旧构建
for path in Path("dist").glob("*"):
path.unlink()
# 运行测试
run("pytest")
# 构建
run("python -m build")
# 上传
run("python -m twine upload dist/*")
print("发布完成!")
if __name__ == "__main__":
release()
5. 打包为可执行文件
PyInstaller
pip install pyinstaller
# 基本打包(单文件)
pyinstaller --onefile main.py
# 带图标
pyinstaller --onefile --icon=app.ico main.py
# 无控制台窗口(GUI程序)
pyinstaller --onefile --noconsole --icon=app.ico main.py
# 指定名称
pyinstaller --onefile --name myapp main.py
# 添加数据文件
pyinstaller --onefile --add-data "data;data" main.py
# 使用spec文件
pyinstaller myapp.spec
# myapp.spec - PyInstaller配置文件
# -*- mode: python ; coding: utf-8 -*-
block_cipher = None
a = Analysis(
['src/main.py'],
pathex=[],
binaries=[],
datas=[
('data/*.json', 'data'),
('templates/*.html', 'templates'),
],
hiddenimports=['pkg_resources.py2_warn'],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='MyApp',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False, # GUI程序设为False
disable_windowed_traceback=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon='app.ico',
)
# 处理数据文件路径
import sys
from pathlib import Path
def get_resource_path(relative_path):
"""获取资源文件路径(兼容PyInstaller)"""
if hasattr(sys, '_MEIPASS'):
# PyInstaller打包后的路径
base_path = Path(sys._MEIPASS)
else:
# 开发环境路径
base_path = Path(__file__).parent
return base_path / relative_path
# 使用
config_path = get_resource_path('data/config.json')
Nuitka(编译为C)
pip install nuitka
# 基本编译
nuitka --standalone --onefile main.py
# Windows GUI程序
nuitka --standalone --onefile --windows-disable-console main.py
# 带图标
nuitka --standalone --onefile --windows-icon-from-ico=app.ico main.py
6. 创建安装脚本
# install.py - Windows安装脚本
import os
import sys
import subprocess
from pathlib import Path
def install():
"""安装程序"""
print("=" * 50)
print("开始安装...")
print("=" * 50)
# 检查Python版本
if sys.version_info < (3, 8):
print("错误: 需要Python 3.8或更高版本")
sys.exit(1)
# 创建虚拟环境
venv_path = Path("venv")
if not venv_path.exists():
print("创建虚拟环境...")
subprocess.run([sys.executable, "-m", "venv", "venv"])
# 确定pip路径
if os.name == 'nt':
pip_path = venv_path / "Scripts" / "pip.exe"
else:
pip_path = venv_path / "bin" / "pip"
# 安装依赖
print("安装依赖...")
subprocess.run([str(pip_path), "install", "-r", "requirements.txt"])
# 安装包
print("安装程序...")
subprocess.run([str(pip_path), "install", "-e", "."])
print("=" * 50)
print("安装完成!")
print("=" * 50)
# 显示使用说明
if os.name == 'nt':
activate = r"venv\Scripts\activate"
else:
activate = "source venv/bin/activate"
print(f"\n激活虚拟环境: {activate}")
print("运行程序: my-cli --help")
if __name__ == "__main__":
install()
@echo off
REM install.bat - Windows批处理安装脚本
echo ========================================
echo 安装程序
echo ========================================
REM 检查Python
python --version >nul 2>&1
if errorlevel 1 (
echo 错误: 未找到Python
pause
exit /b 1
)
REM 创建虚拟环境
if not exist venv (
echo 创建虚拟环境...
python -m venv venv
)
REM 激活虚拟环境并安装
call venv\Scripts\activate
pip install -r requirements.txt
pip install -e .
echo ========================================
echo 安装完成!
echo ========================================
pause
7. Docker打包
# Dockerfile
FROM python:3.11-slim
# 设置工作目录
WORKDIR /app
# 复制依赖文件
COPY requirements.txt .
# 安装依赖
RUN pip install --no-cache-dir -r requirements.txt
# 复制源代码
COPY src/ ./src/
COPY pyproject.toml .
# 安装包
RUN pip install --no-cache-dir .
# 设置环境变量
ENV PYTHONUNBUFFERED=1
# 暴露端口(如果是Web应用)
EXPOSE 8000
# 运行命令
CMD ["my-cli", "run"]
# Dockerfile.multi-stage - 多阶段构建(更小的镜像)
# 构建阶段
FROM python:3.11-slim as builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --user -r requirements.txt
COPY src/ ./src/
COPY pyproject.toml .
RUN pip install --no-cache-dir --user .
# 运行阶段
FROM python:3.11-slim
WORKDIR /app
# 从构建阶段复制安装的包
COPY --from=builder /root/.local /root/.local
ENV PATH=/root/.local/bin:$PATH
# 复制必要文件
COPY --from=builder /app/src ./src
CMD ["my-cli", "run"]
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "8000:8000"
volumes:
- ./data:/app/data
environment:
- DEBUG=false
- DATABASE_URL=sqlite:///data/db.sqlite
restart: unless-stopped
# Docker命令
docker build -t my-app .
docker run -p 8000:8000 my-app
docker-compose up -d
8. 常见问题
❌ 问题1:找不到模块
# 确保__init__.py存在
# 确保包结构正确
# 检查MANIFEST.in(如果使用)
# MANIFEST.in
include README.md
include LICENSE
recursive-include src *.py
recursive-include data *
❌ 问题2:数据文件未包含
# pyproject.toml
[tool.setuptools.package-data]
my_package = ["data/*.json", "templates/*.html"]
# 或使用MANIFEST.in
❌ 问题3:PyInstaller打包后找不到文件
# 使用get_resource_path函数
# 在spec文件中添加datas
❌ 问题4:版本号管理
# 使用单一来源的版本号
# src/my_package/__init__.py
__version__ = "0.1.0"
# pyproject.toml中动态读取
[project]
dynamic = ["version"]
[tool.setuptools.dynamic]
version = {attr = "my_package.__version__"}
9. 总结
🔑 核心要点
| 任务 | 工具/方法 |
|---|---|
| 项目配置 | pyproject.toml |
| 构建包 | python -m build |
| 发布PyPI | twine upload |
| 可执行文件 | PyInstaller, Nuitka |
| 容器化 | Docker |
✅ 学习检查清单
- 了解项目结构规范
- 会配置pyproject.toml
- 能构建和发布Python包
- 能使用PyInstaller打包
- 了解Docker打包
📖 下一步学习
掌握了项目打包后,让我们学习学习资源与进阶路线:
常见问题 FAQ
💬 PyInstaller打包的exe太大怎么办?
默认打包会包含整个Python环境。解决:1)用虚拟环境只安装必要依赖;2)用--exclude-module排除不需要的模块;3)用UPX压缩。通常能从200MB降到50MB以下。
💬 打包的exe在别人电脑上运行报错?
常见原因:缺少运行时依赖(如VC++ Redistributable)、路径问题、缺少数据文件。用--add-data打包数据文件,测试时在干净的虚拟机上验证。
� 系列导航
- 上一篇:31 - Python常用第三方库
- 当前:32 - Python项目打包与发布
- 下一篇:33 - Python学习资源与进阶路线