前言
一门编程语言的强大,有一点在于社区是否活跃,相关库是否够多。主流的编程语言都有非常强大的包管理工具和便捷的库下载方式,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 侧栏对应 author
,author_email
,maintainer
,maintainer_email
,license
,keywords
, 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
如果我们需要包被全世界的同好通过 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 包了。
来自知乎专栏。
参考
- Python Packing User Guide
- setuptools
- pypi classifiers
- MANIFEST.in
- setup.cfg
- twine