Python包发布指南

前言

一门编程语言的强大,有一点在于社区是否活跃,相关库是否够多。主流的编程语言都有非常强大的包管理工具和便捷的库下载方式,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 上的信息对应关系如下。

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

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

image

这整一块都是 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 包了。

来自知乎专栏

参考

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,185评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,652评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,524评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,339评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,387评论 6 391
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,287评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,130评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,985评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,420评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,617评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,779评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,477评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,088评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,716评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,857评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,876评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,700评论 2 354