Python打包
最近感兴趣想将开发的项目转成Package,研究了一下相关文章,并且自己跑通了,走了一下弯路,这里记录一下如何打包一个简单的Python项目,展示如何添加必要的文件和结构来创建包,如何构建包,以及如何将其上传到Python包索引(PyPI)。
pyproject简介
之前python因为规范太宽松,导致每个项目的结构五花八门,且打包方式非常繁琐;就比如 setuptools 打包方式要写各种配置,实在难学。但是!Python从PEP 518开始引入使用pyproject.toml管理项目元数据的方案,且该规范目前已在很多开源项目中得以支持!
pyproject.toml 是在 PEP 518 中提出并在 PEP 621 中扩展的新配置文件 。目的是管理构建依赖,同时也可以存储 Python 项目的任何工具配置。
使用pyproject的目的:
- 在一个 Python 项目中,我们需要管理
requirements.txt
,flake8
等等的配置文件,当一个项目中使用的工具越多,根目录就越杂乱,管理成本越高,对新人也就越不友好 - 将诸多工具的配置集中到 pyproject.toml 统一管理,将小而零散的开发工具配置提取并放到同一个地方,便于了解项目构建、开发流程等信息
项目结构
假设我们软件包的名字是myutils,那么整个项目的目录结构在推荐的风格下看起来应该像这样:
.
├── LICENSE
├── pyproject.toml
├── README.md
├── src
│ └── hatch_demo # src 下面是包名,包下面是业务代码
│ ├── core.py
│ └── __init__.py
└── tests
└── __init__.py
3 directories, 6 files
选择构建后端
像pip和build这样的工具实际上不会将源代码转换为分发包(如轮子);该工作由构建后端执行。构建后端决定您的项目将如何指定其配置,包括元数据(有关项目的信息,例如,PyPI上显示的名称和标签)和输入文件。构建后端具有不同级别的功能,例如它们是否支持构建扩展模块,应该选择适合需求和偏好的一个。
这里可以从许多后端中进行选择;本教程默认使用Hatchling,但它将与支持元数据的setuptools、Flight、PDM和其他支持[project]
表的方法相同。
pyproject.toml
告诉构建前端工具,如pip和build,为项目使用哪个后端。以下是一些常见构建后端的示例,但请查看后端自己的留档以获取更多详细信息。
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[build-system]
requires = ["flit_core>=3.4"]
build-backend = "flit_core.buildapi"
[build-system]
requires = ["pdm-backend"]
build-backend = "pdm.backend"
最小化示例
后面使用 hatch
后端进行构建,详细配置请参阅 hatch 官方文档:https://hatch.pypa.io/latest/config/metadata/
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "demo" # 软件包名
version = "0.0.1" # 软件版本
简单示例
下面小实践一下,目录结构如下:
.
├── myutils
│ ├── example.py
│ └── __init__.py
└── pyproject.toml
1 directory, 3 files
pyproject.toml
文件内容:
[build-system]
build-backend = "hatchling.build"
requires = [
"hatchling>=1.21",
"importlib_metadata>=4.8.3",
"versioningit>=2.3",
]
[project]
name = "myutils"
license = "Apache-2.0"
requires-python = ">=3.9"
classifiers = [
"Private :: Do Not Upload",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
version = "0.1.0"
[tool.hatch.build.targets.wheel]
packages = ["myutils"]
# 如果需要把.pyc编译的文件打包,需要使用此配置(注意:此方式打包安装后要到python包目录查看确认一下)
# force-include 是强制包含;这里就是强制把myutils目录打入whl包的根目录内
#[tool.hatch.build.targets.wheel.force-include]
#"./myutils" = "/myutils"
example.py文件内容,简单写一个add函数:
def add(x: int, y: int) -> int:
return x + y
文件准备完成后,即可打包使用。
安装 build 依赖并用 build 来打包
# 安装依赖 pip install build # 执行打包 python -m build # 或者 pyproject-build . # 默认生成 .tar.gz 和 .whl 文件 pyproject-build -w . # -w参数是只保留whl包 pyproject-build -s . # -s参数是只保留.tar.gz源码包
打包成功后会生成一个dist目录,里面存放有whl包
pip install dist/myutils-0.1.0-py3-none-any.whl
测试使用
>>> from myutils import example >>> example.add(1,3) 4
也可以上传到pypi
将打好的包上传到Python包索引,可供其它人安装。需要做的第一件事是在TestPyPI上注册一个帐户,这是一个用于测试和实验的包索引的单独实例。对于像本教程这样不一定想上传到真实索引的东西,这样的测试环境是非常好的。 注意:这不是永久存储。Test系统偶尔会删除包和帐户。
如何使用TestPyPI,请参阅使用TestPyPI。
上传之前需要先下载 twine
pip install --upgrade twine
上传软件包:
# 上传dist下的所有存档到 testpypi 源: python3 -m twine upload --repository testpypi dist/* # 上传单个软件包到 pypi twine upload dist/myutils-0.1.0-py3-none-any.whl
下载上传的软件包:
python3 -m pip install --index-url https://test.pypi.org/simple/ --no-deps myutils
当准备好将真实包上传到Python包索引时,可以像本教程中一样执行相同的操作,但有以下重要区别:
- 为包选择一个难忘且独特的名称;
- 在https://pypi.org上注册一个帐户,这是两个独立的服务器,测试服务器的登录详细信息不与主服务器共享;
- 使用
twine上传dist/*
上传自己的包,并输入正式PyPI环境上注册的帐户的凭据。在生产环境中上传包,不需要指定--repository
;默认情况下,包将上传到https://pypi.org/; - 使用
python3 -m pip install [your-package]
从真正的PyPI安装包;
复杂示例
接下是打包一个django app:
目录结构如下:
Two
├── app01
│ └── ......
├── app02 # 要打包的app
│ ├── admin.py
│ ├── apps.py
│ ├── __init__.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ └── __init__.py
│ ├── models.py
│ ├── README.md
│ ├── ser.py
│ ├── templates
│ │ └── test.html
│ ├── tests.py
│ ├── urls.py
│ └── views.py
├── db.sqlite3
├── dist
│ ├── django_app02-0.1.0-py3-none-any.whl
│ └── django_app02-0.1.0.tar.gz
├── manage.py
├── pyproject.toml
├── templates
├── Two
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
└── utils
├── orm_test.py
└── schema.py
根据上面目录结构可知这是一个名为Two
的django程序,里面有app01
, app02
两个子app,这也是我们常用的目录结构。接下来打包app02
。
编写
pyproject.toml
文件(环境准备看上面):[build-system] build-backend = "hatchling.build" requires = [ "hatchling>=1.21", "importlib_metadata>=4.8.3", "versioningit>=2.3", ] [project] name = "django-app02" license = "Apache-2.0" requires-python = ">=3.6" classifiers = [ "Private :: Do Not Upload", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "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", ] version = "0.1.0" dependencies = [ "djangorestframework~=3.12.4" # 用到的依赖,在安装该软件包时会自动关联安装restframework ] # 如果需要压缩包,则需要取消此注释 #[tool.hatch.build.targets.sdist] #only-include = ["app02"] # 对app02目录下内容打包 [tool.hatch.build.targets.wheel] packages = ["app02"] # 对app02目录下内容打包 # 使用ruff检查规范 [tool.ruff] include = [ "app02/*.py", ] exclude = [ "app02/*/migrations/*.py", ] line-length = 120 target-version = "py39" output-format = "full" show-fixes = true [tool.ruff.format] preview = true [tool.ruff.lint] preview = true select = [ "ALL", ] ignore = [ "D", "ANN", "COM812", "ISC001", "FA100", "FA102", "N818", "TID252", "RUF012", "DJ001", "DJ008", ]
执行打包命令:
pyproject-build -w .
安装:
pip install dist/django_app02-0.1.0
测试使用:
创建新的django项目
django-admin startproject myproject
修改
myproject/settings.py
文件,添加restframework
和app02
两个appINSTALLED_APPS = [ ...... 'rest_framework', 'app02.apps.App02Config' ]
把app02路由添加到
myproject/urls.py
中:urlpatterns = [ ...... path("app02/", include('app02.urls')), ]
启动测试:
python manage.py runserver 0.0.0.0:8080
启动后访问app02即可。
额外补充:有没有发现在settings配置中,rest_framework配置只需要写个名字就行了,但是我们的app02还需要写那么多,那如何做到呢?这需要我们修改
app02/__init__.py
文件(可以参考rest源码),修改如下即可:import django __title__ = 'app02' __version__ = '0.0.1' __author__ = 'wuye' VERSION = __version__ django.VERSION >=(2, 2) and django.VERSION <= (2, 3) # 限定了django版本为2.2.* default_app_config = 'app02.apps.App02Config' # app的配置类
[参考附录]
- build 包文档说明:https://build.pypa.io/en/stable/mission.html
- hatch 官方文档:https://hatch.pypa.io/latest/config/metadata/
- pep-0517:https://peps.python.org/pep-0517/
- pep-0518:https://peps.python.org/pep-0518/
- toml 格式说明:https://toml.io/en/