Python包发布指南

409 阅读8分钟

前言

一门编程语言的强大,有一点在于社区是否活跃,相关库是否够多。主流的编程语言都有非常强大的包管理工具和便捷的库下载方式,Python 就有 pip 工具,一行命令就可以下载所需要的依赖库。

pip install requests

俗话说得好:轮子用的好,头发不会少。但是当我们开发到一定的阶段,还是经常会发现没有趁手的库可以用,或者一些业务代码过于冗余,需要提取抽象,那就可以自己开发依赖库。一来减少重复劳动,避免代码的复制粘贴,二来贡献开源社区,也是给需要的人做贡献。

下面就谈谈如何从头开始构建自己的 Python 包。

打包

Python 包需要发布,第一步就是打包。好比货物要出售,就需要一套标准化的包装流程,保证货物交付的可靠性。

相比与手动的代码复制方式,打包有如下好处:

  • 代码包不需要手动复制
  • 版本管理,避免复制代码混乱
  • 可使用 pip 工具直接安装

所以 Python 有一套非常完善的打包工具 setuptools,使用也是非常简单,我们从一个项目入手。

假设我们的项目目录如下:

my_project
|- my_package
   |- __init__.py
   |- main.py

my_project 是我们的项目根目录,my_package 是我们的包根目录,下面只有一个模块 main.py

要使用 setuptools,需要创建一个 setup.py 打包配置文件,放在项目根目录下。

内容如下

from setuptools import setup
from setuptools import find_packages


VERSION = '0.1.0'

setup(
    name='Flask-Board',  # package name
    version=VERSION,  # package version
    description='my package',  # package description
    packages=find_packages(),
    zip_safe=False,
)

通过添加这么一个简单的配置文件,我们的项目就可以变身称为一个 Python 包了。

执行构建

python setup.py build

会将包的内容构建到 build 文件夹下。

执行安装

python setup.py install

会将包直接安装到当前解释器的 site-packages 下,安装完成后即可以使用 pip list 命令查看到。

Python 库的打包就这么简单?不过实际情况下我们需要更多的配置,下面我们来看看主要的配置方式。

配置

下面我将主要配置分为几类,详细讲解,基本可以涵盖大部分使用场景,可作为快速指南使用。

基本信息

  • name:包名称
  • version:包版本
  • url:主页地址
  • project_urls:包相关网页地址,字典格式,对应关系见下图
  • author:作者名字
  • author_email:作者邮箱
  • maintainer:维护者名字
  • maintainer_email:维护者邮箱
  • classifiers:分类信息
  • license:使用的开源许可
  • description:简短描述
  • long_description:详细描述
  • long_description_content_type:详细描述的格式
  • keywords:关键词
  • platforms:支持的操作系统

pypi.org 上的信息对应关系如下。

  • name: 1
  • version: 2
  • description: 3
  • long_description: 4
  • url 和 project_urls: 5

Meta 侧栏对应 authorauthor_emailmaintainermaintainer_emaillicensekeywords, python_requires(下面依赖配置中)等信息。

这整一块都是 classifiers 信息。

常用场景

URL

项目前期比较简单,只有 github 地址,一般只配置 url,对应页面只显示 Homepage。

项目完善后,可能有独立的主页,Github 代码页,文档页等。url 可以配置项目的主页,project_urls 配置其他页面,如下所示。

project_urls={
    "Documentation": "https://flask.palletsprojects.com/",
    "Code": "https://github.com/pallets/flask",
    "Issue tracker": "https://github.com/pallets/flask/issues",
}

详细描述配置

项目的详细描述往往很长,可以使用一个单独的文件描述,pypi 默认使用 rst 格式渲染。

with open('README.rst') as f:
    LONG_DESCRIPTION = f.read()

setup(
    name='my-package',
    version='0.1.0',
    description='short description',
    long_description=LONG_DESCRIPTION,
    # ...
)

不过,因为 Github 默认使用 README.md 文件作为项目的详细描述,我们也可以重复利用,markdown 的语法更简单。

with open('README.md') as f:
    LONG_DESCRIPTION = f.read()

setup(
    name='my-package',
    version='0.1.0',
    description='short description',
    long_description=LONG_DESCRIPTION,
    long_description_content_type='text/markdown',
    # ...
)

long_description_content_type 配置可以指定 long_description 的渲染格式,支持的值是:

  • text/plain
  • text/x-rst
  • text/markdown

分类信息

classifiers 配置主要用来帮助 pypi 更好的分类和索引包,同时告诉其他人包相关特点。双冒号前面是分类的名称,后面是分类的值,包含了包的各个方面,视情况填写就行。这里可以看到所有的分类列表。

classifiers=[
    "Development Status :: 5 - Production/Stable",
    "Environment :: Web Environment",
    "Framework :: Flask",
    "Intended Audience :: Developers",
    "License :: OSI Approved :: BSD License",
    "Operating System :: OS Independent",
    "Programming Language :: Python",
    "Programming Language :: Python :: 2",
    "Programming Language :: Python :: 2.7",
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.5",
    "Programming Language :: Python :: 3.6",
    "Programming Language :: Python :: 3.7",
    "Programming Language :: Python :: 3.8",
    "Programming Language :: Python :: Implementation :: CPython",
    "Programming Language :: Python :: Implementation :: PyPy",
    "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
    "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
    "Topic :: Software Development :: Libraries :: Application Frameworks",
    "Topic :: Software Development :: Libraries :: Python Modules",
]

依赖信息

  • install_requires:依赖的其他库列表,安装该库之前也会安装
  • extras_require:其他的可选依赖库,安装该库不会自动安装
  • setup_requires:构建依赖的库,不会安装到解释器库,安装到本地临时目录
  • python_requires:Python 版本依赖
  • use_2to3:布尔值,True 则自动将 Python2 的代码转换为 Python3

这些主要是配置依赖信息,常用的主要就是 install_requires,配置该库依赖的其他库。

setup(
    ...
    python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*",
    install_requires=[
        "Werkzeug>=0.15",
        "Jinja2>=2.10.1",
        "itsdangerous>=0.24",
        "click>=5.1",
    ],
    extras_require={
        "dotenv": ["python-dotenv"],
        "dev": [
            "pytest",
            "coverage",
            "tox",
            "sphinx",
            "pallets-sphinx-themes",
            "sphinxcontrib-log-cabinet",
            "sphinx-issues",
        ],
        "docs": [
            "sphinx",
            "pallets-sphinx-themes",
            "sphinxcontrib-log-cabinet",
            "sphinx-issues",
        ],
    },
)

常用场景

特定 Python 版本依赖

如果一些依赖是只有某些 Python 版本才需要的,可以这样指定

setup(
    ...
    install_requires=[
        "enum34;python_version<'3.4'",
    ]
)

特定操作系统依赖

如果一些依赖是特定操作系统才需要安装的,可以这样指定

setup(
    ...
    install_requires=[
        "pywin32 >= 1.0;platform_system=='Windows'"
    ]
)

功能管理

  • packages:该库包含的 Python 包
  • package_dir:字典配置包的目录
  • package_data:配置包的其他数据文件
  • include_package_data:布尔值,为 True 则根据 MANIFEST.in 文件自动引入数据文件
  • exclude_package_data:字典配置需要移除的数据文件
  • zip_safe:布尔值,表明这个库能否安全的使用 zip 安装和执行
  • entry_points:库的入口点配置,可用来做命令行工具和插件

这些配置主要用来指定那些文件需要打包,哪些不需要,以及打包的行为等。

常用场景

包文件配置

setuptools 自动搜索包文件,使用 find_packages 工具函数即可。

from setuptools import setup
from setuptools import find_packages

setup(
    ...
    packages=find_packages(),
)

会自动引入当前目录下的所有 Python 包(即包含 __init__.py 的文件夹),只会自动引入 py 文件,不会引入所有的文件。

如果所有的包需要统一放置在一个独立的目录下,例如 src,如下所示的目录结构

my_project
|- src
    |- my_package
       |- __init__.py
       |- main.py
setup.py

可以如下配置

from setuptools import setup
from setuptools import find_packages

setup(
    ...
    packages=find_packages("src"),
    package_dir={"": "src"},
)

引入其他的数据文件

默认只会引入满足条件文件(例如 py),如果需要引入其他的文件,例如 txt 等文件,需要配置导入数据文件。

setup(
    ...
    package_data={
        # 引入任何包下面的 *.txt、*.rst 文件
        "": ["*.txt", "*.rst"],
        # 引入 hello 包下面的 *.msg 文件
        "hello": ["*.msg"],
    },
)

通过 MANIFEST.in 文件配置

setup(
    include_package_data=True,
    # 不引入 README.txt 文件
    exclude_package_data={"": ["README.txt"]},
)

MANIFEST.in 文件位于 setup.py 同级的项目根目录上,内容类似下面。

include CHANGES.rst
graft docs
prune docs/_build

有如下几种语法

  • include pat1 pat2 ...:引入所有匹配后面正则表达式的文件
  • exclude pat1 pat2 ...:不引入所有匹配后面正则表达式的文件
  • recursive-include dir-pattern pat1 pat2 ...:递归引入匹配 dir-pattern 目录下匹配后面正则表达式的文件
  • recursive-exclude dir-pattern pat1 pat2 ...:递归不引入匹配 dir-pattern 目录下匹配后面正则表达式的文件
  • global-include pat1 pat2 ...:引入源码树中所有匹配后面正则表达式的文件,无论文件在哪里
  • global-exclude pat1 pat2 ...:不引入源码树中所有匹配后面正则表达式的文件,无论文件在哪里
  • graft dir-pattern:引入匹配 dir-pattern 正则表达式的目录下的所有文件
  • prune dir-pattern:不引入匹配 dir-pattern 正则表达式的目录下的所有文件

添加命令

如果需要用户安装库之后添加一些命令,例如 flask 安装之后添加了 flask 命令,可以使用 entry_points 方便的配置。

setup(
    ...
    entry_points={
        "console_scripts": ["flask = flask.cli:main"]
    },
)

console_scripts 键用来配置命令行的命令,等号前面的 flask 是命令的名称,等号后面是模块名:方法名

setup(
    ...
    entry_points={
        "console_scripts": [
            "foo = my_package.some_module:main_func",
            "bar = other_module:some_func",
        ],
        "gui_scripts": [
            "baz = my_package_gui:start_func",
        ]
    }
)

自动发现插件

entry_points 还可以用开开发插件,在无需修改其他库的情况下,插入额外的功能。

插件库在 setup.py 中的 entry_points 中定义插件入口。

setup(
    ...
    entry_points={
        "console_scripts": [
            "foo = my_package.some_module:main_func",
        ],
    }
)

而主体库可以通过 pkg_resources 遍历获取同一组的 entry_points

from pkg_resources import iter_entry_points

group = 'console_scripts'
for entry_point in iter_entry_points(group):
    fun = entry_point.load()
    print(fun)

这里的 fun 就是所有定义在 entry_points 上的类或者方法。

这样就可以在主体类不变更的情况下,轻松实现插件的插入,Flask 就是利用这个机制实现自定义命令扩展的。

setup(
    ...
    entry_points={
        'flask.commands': [
            'test=my_package.commands:cli'
        ],
    },
)

而对应 Flask 库中有如下代码自动载入命令。

def _load_plugin_commands(self):
    if self._loaded_plugin_commands:
        return
    try:
        import pkg_resources
    except ImportError:
        self._loaded_plugin_commands = True
        return

    for ep in pkg_resources.iter_entry_points("flask.commands"):
        self.add_command(ep.load(), ep.name)
    self._loaded_plugin_commands = True

配置文件

setuptools 同时还支持配置文件来配置,在 setup.py 文件同级的项目根目录下创建 setup.cfg 文件。

配置内容同上,只是按照 cfg 配置文件的格式,加上一些分块,同时支持一些特殊的语法。相对于 setup.py 中配置,更利于阅读和管理,但是缺少了灵活性。

[metadata]
name = my_package
version = attr: src.VERSION
description = My package description
long_description = file: README.rst, CHANGELOG.rst, LICENSE.rst
keywords = one, two
license = BSD 3-Clause License
classifiers =
    Framework :: Django
    License :: OSI Approved :: BSD License
    Programming Language :: Python :: 3
    Programming Language :: Python :: 3.5

[options]
zip_safe = False
include_package_data = True
packages = find:
install_requires =
  requests
  importlib; python_version == "2.6"

详细配置可以参考 setup.cfg 格式

发布

打包配置完成后就是发布我们的库了。

打包成 tar 包

python setup.py sdist

安装 wheel 库后可以打包成 whl 包

安装 wheel

pip install wheel

打包 whl

python setup.py bdist_wheel

打包完后的包可以直接通过 pip 安装

pip install <path-to-package>

如果我们需要包被全世界的同好通过 pip install 直接安装的话,需要将包上传到 pypi 网站。首先注册 pypi,获得用户名和密码。

上传 tar 包

python setup.py sdist upload

上传 whl 包

python setup.py bdist_wheel upload

如果要更安全和方便地上传包就使用 twine 上传。

安装 twine

pip install twine

上传所有包

twine upload dist/*

如果嫌每次输入用户名和密码麻烦可以配置到文件中。

编辑用户目录下的 .pypirc 文件,输入

[pypi]
username=your_username
password=your_password

好了,我们就可以尽情发布我们开发的 Python 包了。

参考