背景
这些年整个 Python 社区都在向更加优雅的代码风格大踏步地前进。之前写过一篇文章 用好 Python 标准库!少写几百行,介绍了如何在类这个层面让代码更加简洁,今天我想讲一下 pyproject.toml ;是怎么把这种简洁推向更高的层次,做到工程级别的简洁。
要讲清楚这个我们要追溯到 pyproject.toml 没有出现之前;为了提起大家的兴趣,还是先来看一下 pyproject.toml 目前有多流行。我在 github 上看一下 Python 生态顶级项目引入 pyproject.toml 的情况。
1. Django 这个 Python 生态的顶级项目在 5 个月之前开始使用 pyproject.toml
2. Pytest 这个 Python 生态测试框架的领头羊在 4 个月之前开始使用 pyproject.toml
3. SciPy 这机器学习的库也在 3 周前切到了 pyproject.toml
例子就不多举了,这么多牛逼的软件都同时转向,不是没有理由的。接下来我们看一下没有它之前世界。
PyPI 的旧时代
Python 生态的强大之处主要体现在大量的软件包,比如写网站我们可以用 Django ,爬虫我们可以用 requests ,MySQL 数据库管理我们可以用 dbm-agent 。
其实想要做有软件包可用,宏观上要完成如下 3 步。
1: 包的作者无私的上传软件包到 PyPI
twine upload dist/dbm-agent-8.31.1.tar.gz
2: 包的使用者下载安装包并安装
pip install dbm-agent
3: 在业务代码中引入第三方软件包
import dbma
以前 Python 整个生态在第 2 步和第 3 步做的比较友好,第一步做的差强人意。就以我在 PyPI 上维护的 dbm-agent 包为例子,讲一下旧时代的坑。
在 PyPI 上打开任何一个软件包的主页,我们都能在页面看到包的 “版本”,“安装命令”,“介绍” 这些元数据信息
之所以在 PyPI 上能看到这些信息是因为,开发者在项目的 setup.py 文件里一个个的填写了这些信息。以 dbm-agent 这个包的 setup.py 配置为例子,元数据本质上就是传给 setup 函数传递的实参。
import os
import re
from setuptools import setup
def get_version():
"""
动态获取 dbm-agent 的版本号
"""
project_dir_name = os.path.dirname(__file__)
version_file_path = os.path.join(project_dir_name,"dbma/unix/version.py")
with open(version_file_path) as version_file_obj:
content = version_file_obj.read()
g = {}
exec(content,g,g)
return g['VERSION']
agent_version = get_version()
setup(name='dbm-agent',
version=agent_version,
description='dbm-agent 数据库管理中心客户端程序',
author="Neeky",
author_email="neeky@live.com",
maintainer='Neeky',
maintainer_email='neeky@live.com',
scripts=['bin/dbm-agent', 'bin/dbma-cli-single-instance', 'bin/dbma-cli-install-mysqlsh',
'bin/dbma-cli-build-slave', 'bin/dbma-cli-build-mgr', 'bin/dbma-cli-clone-instance',
'bin/dbm-monitor-gateway', 'bin/dbma-cli-zabbix-agent', 'bin/dbma-cli-mysql-monitor-item',
'bin/dbma-cli-backup-instance', 'bin/dbma-cli-install-backuptool', 'bin/dbm-backup-proxy'],
packages=['dbma','dbma/unix', 'dbma/core', 'dbma/core/views', 'dbma/loggers', 'dbma/installsoftwares', 'dbma/bil'],
package_data={'dbma': ['static/cnfs/*', 'static/sql-scripts/*']},
url='https://github.com/Neeky/dbm-agent',
install_requires=['Jinja2>=2.10.1', 'mysql-connector-python>=8.0.31',
'psutil>=5.6.6', 'requests>=2.22.0', 'distro>=1.4.0',
'aiohttp==3.8.1', 'cchardet==2.1.7', 'aiodns==3.0.0'],
python_requires='>=3.6.*',
classifiers=[
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'Operating System :: POSIX',
'Operating System :: MacOS :: MacOS X',
'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']
)
看到了吧,所有程序的配置都是以 Python 代码的形式来体现的。只要是代码在没有强制规范的情况下一万个人就有一万种写法,我们现在看一下 Django 的 setup.py 文件是怎么个样子。
import os
import site
import sys
from distutils.sysconfig import get_python_lib
from setuptools import setup
# Allow editable install into user site directory.
# See https://github.com/pypa/pip/issues/7953.
site.ENABLE_USER_SITE = "--user" in sys.argv[1:]
# Warn if we are installing over top of an existing installation. This can
# cause issues where files that were deleted from a more recent Django are
# still present in site-packages. See #18115.
overlay_warning = False
if "install" in sys.argv:
lib_paths = [get_python_lib()]
if lib_paths[0].startswith("/usr/lib/"):
# We have to try also with an explicit prefix of /usr/local in order to
# catch Debian's custom user site-packages directory.
lib_paths.append(get_python_lib(prefix="/usr/local"))
for lib_path in lib_paths:
existing_path = os.path.abspath(os.path.join(lib_path, "django"))
if os.path.exists(existing_path):
# We note the need for the warning here, but present it after the
# command is run, so it's more likely to be seen.
overlay_warning = True
break
setup()
可以看到不同的项目,它们的 setup.py 文件真的是一点都不像。原因就是因为以前的规范比较松散,可以理解成只规定了要调用 setuptools.setup 这个函数。
用代码来体现配置的问题还不只是这个,CI/CD 软件要去检查 setup 函数传了什么参数,更加要命的是,如果没有传参数的情况下,还要配置 CI/CD 他们去哪里文件解析参数。
总的来讲用代码来体现软件项目的配置信息,对开发者和 CI/CD 都不太友好。比较现代的方案是通过配置文件来声明配置,pyproject.toml 正是这么一个产物。
pyproject.toml 实践
之前用 setup 的时候不就是因为规范太松散了,每个项目的结构都五花八门。现在好了,pyproject.toml 它在 Python 项目的结构上都有一个推荐风式了。假设我们软件包的名字是 npts ,那么整个项目的目录结构在推荐的风格下看起来应该像这样。
tree ./
./
├── LICENSE
├── README.md
├── pyproject.toml
├── src
│ └── npts # src 下面是包名,包下面是业务代码
│ ├── __init__.py
│ └── core.py
└── tests
3 directories, 5 files
简单地在 src/npts/core.py 加一个函数,模拟我们的业务逻辑。
# -*- coding: utf8 -*-
def hello(name: str = "world"):
return f"hello {name} ."
我们先来看一下最小化配置的情况下 pyproject.toml 是多么的简洁,就能完成打包。原本几十行的代码现在几行就行了。
[project]
name = "npts"
version = "0.0.1"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
别看它除去空行之外只有 6 行,但是已经完全够用来演示了。接下来我们一起来走一个完整的个“打包”,“上传”到 PyPI 的打包上传流程。文件内容这里我也不打算解释了,只要我们把 [build-system] 看成是固定套路其它的都一目了然。
1. 安装 build 依赖并用 build 来打包
# 安装依赖
python3 -m pip install --upgrade build
# 打包
python3 -m build
...
...
Successfully built npts-0.0.1.tar.gz and npts-0.0.1-py2.py3-none-any.whl
2. 把打包好的软件包上传到 PyPI
twine upload dist/npts-0.0.1-py3-none-any.whl
Uploading distributions to https://upload.pypi.org/legacy/
Enter your username: neeky
Enter your password:
Uploading npts-0.0.1-py3-none-any.whl
100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 5.81k/5.81k [00:02<00:00, 2.23kB/s]
View at:
https://pypi.org/project/npts/0.0.1/
3. 下载安装软件包
pip3 install npts
Looking in indexes: https://mirrors.cloud.tencent.com/pypi/simple
...
Successfully installed npts-0.0.1
4. 测试
In [1]: from npts import core
In [2]: core.hello("world")
Out[2]: 'hello world .'
有了 pyproject.toml 之后软件包的发行是相当方便了,再见 setup.py 。
pyproject.toml 高级
其实 pyproject.toml 还有一些其它的配置项,不过数量上也不多;文章里面为了简单只列举了最少要声明的项。我这里还有一份相对完整的可以给大家参考。
[project]
name = "npts"
version = "0.0.3"
description = "neeky's perf tools"
requires-python = ">=3.8"
dependencies = [
"Django==4.1.2",
]
authors = [
{ name="LeXing Jiang", email="1721900707@qq.com" },
{ name="neeky", email="neeky@live.com" }
]
readme = "README.md"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
[project.scripts]
npt-cli-hello = "npt.core:hello"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
最后
私信回复 “npts” 获取源代码 !!!
都到这里了,是时候图穷匕见了!我这人比较 real 就直说了,我想涨粉帮忙点下关注,我技术文章的质量还可以关注应该不亏的。
“在看” + “分享” + “点赞” + “收藏” 也是我继续写下去的动力。谢谢!!!