zl程序教程

您现在的位置是:首页 >  Python

当前栏目

Python 新规范 pyproject.toml 完全解析

2023-03-07 09:46:28 时间

背景

这些年整个 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 就直说了,我想涨粉帮忙点下关注,我技术文章的质量还可以关注应该不亏的。

“在看” + “分享” + “点赞” + “收藏” 也是我继续写下去的动力。谢谢!!!