Skip to content

创建一个包

https://pypi.org/

https://python-packaging-zh.readthedocs.io/zh_CN/latest/index.html

setup.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from setuptools import setup


setup(
    name="mystrategy",
    version="0.10",
    packages=[
        "mystrategy",
    ],
    py_modules="mystrategy",
    entry_points={},
    install_requires=[]
)

安装:pip install -e .

Python 打包一开始可能有些难以理解。其主要原因是不了解创建 Python 包的正确工具。
不管怎样,一旦创建了第一个包,就会会发现它并不像看起来那么难。
此外,熟悉正确且先进的打包工具也很有帮助。

你即使对将代码开源分发不感兴趣,但也应该知道如何创建包。
知道如何创建自己的包,可以让你深入了解打包生态系统,并且有助于你使用 PyPI 上可用的第三方代码。

此外,将你的闭源项目或其组件变成源代码发行包,有助于你在不同的环境中部署代码。

Python 打包工具的混乱状态

Python 打包曾经在很长一段时间内处于混乱不堪的状态,人们花了很多年才使得这一主题重新变得有组织。
一切都从 1998 年引入的distutils包开始,随后在 2003 年setuptools对其进行改进。
这两个项目开启了一段漫长而又纠结的故事,故事包括派生(fork)、替代项目与完全重新编写,都想要彻底修复 Python 的打包生态系统。
不幸的是,大部分尝试都没有成功。效果恰恰相反。
每个想要取代setuptoolsdistutils的新项目只是给打包工具十分混乱的状态添乱而已。
有些派生被合并回它们的祖先中(例如 setuptools 派生的 distribute),但有些则被弃用(distutils2)

幸运的是,这种状态正在逐步改变。
成立了一个叫做 Python Packaging Authority(PyPA)的组织,将秩序和组织性带回打包生态系统中。
PyPA 维护的 Python 打包用户指南(Python Packaging User Guide,https://packaging.python.org/)是关于最新打包工具和最佳实践的权威信息来源。

由于PyPA的存在,Python打包的现状

PyPA 除了提供一份权威的打包指南之外,还维护着打包项目与新的官方打包的标准化过程。
请参阅:https://github.com/pypa

这些项目中国呢最有名的是:

  • pip
  • virtualenv
  • twine
  • warehouse

注意,大部分项目都是在这个组织之外开始的,只是作为一个成熟且广泛使用的解决方案迁移到 PyPA 的赞助下。

工具推荐

在 Python 打包用户指南中,推荐包的创建与分发的工具如下:

  • 使用 setuptools 来定义项目并创建源代码发行版(source distributions)
  • 使用 wheel 而不是 egg 来创建构建发行版(built distributions)
  • 使用 twine 向 PyPI 上传包的发行版

项目配置

很显然,组织大型应用的代码的最简单方法是将其分成几个包。
这使得代码更加简单,也更容易理解、维护和修改。
这样也使每个包的可复用性最大化。它们的作用就像组件一样。

setup.py

对于一个需要被分发的包来说,其根目录包含一个 setup.py 脚本。
它定义了 distutils 模块中描述的所有元数据,并将其合并为标准的 setup() 函数调用的参数。
虽然 distutils 是一个标准库模块,但建议你使用 setuptools 包来代替,它对标准的 distuils 做了一些改进。

因为,这个文件的最少内容如下:

1
2
3
4
5
from setuptools import setup

setup(
    name="mypackage",
)

name 给出了包的全名。
该脚本提供了一些命令,你可以用--help-commands选项列出以下这些命令:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
python setup.py --help-commands
Standard commands:
  build             build everything needed to install
  build_py          "build" pure Python modules (copy to build directory)
  build_ext         build C/C++ and Cython extensions (compile/link to build directory)
  build_clib        build C/C++ libraries used by Python extensions
  build_scripts     "build" scripts (copy and fixup #! line)
  clean             clean up temporary files from 'build' command
  install           install everything from build directory
  install_lib       install all Python modules (extensions and pure Python)
  install_headers   install C/C++ header files
  install_scripts   install scripts (Python or otherwise)
  install_data      install data files
  sdist             create a source distribution (tarball, zip file, etc.)
  register          register the distribution with the Python package index
  bdist             create a built (binary) distribution
  bdist_dumb        create a "dumb" built distribution
  bdist_rpm         create an RPM distribution
  bdist_wininst     create an executable installer for MS Windows
  check             perform some checks on the package
  upload            upload binary package to PyPI

Extra commands:
  bdist_wheel       create a wheel distribution
  build_sphinx      Build Sphinx documentation
  alias             define a shortcut to invoke one or more commands
  bdist_egg         create an "egg" distribution
  develop           install package in 'development mode'
  dist_info         create a .dist-info directory
  easy_install      Find/get/install Python packages
  egg_info          create a distribution's .egg-info directory
  install_egg_info  Install an .egg-info directory for the package
  rotate            delete older distributions, keeping N newest files
  saveopts          save supplied options to setup.cfg or other config file
  setopt            set an option in setup.cfg or another config file
  test              run unit tests after in-place build (deprecated)
  upload_docs       Upload documentation to PyPI
  ptr               run unit tests after in-place build (deprecated)
  pytest            run unit tests after in-place build (deprecated)
  nosetests         Run unit tests using nosetests
  isort             Run isort on modules registered in setuptools
  flake8            Run Flake8 on modules registered in setup.py
  compile_catalog   compile message catalogs to binary MO files
  extract_messages  extract localizable strings from the project code
  init_catalog      create a new catalog based on a POT file
  update_catalog    update message catalogs from a POT file

usage: setup.py [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...]
   or: setup.py --help [cmd1 cmd2 ...]
   or: setup.py --help-commands
   or: setup.py cmd --help

Standard commands(标准命令)是 distutils 提供的内置明林,而Extra commands(额外命令)则是由诸如 setuptools 这样的第三方包或任何其他定义并注册一个新命令的包所创建的。
由另一个包注册的一个额外命令就是 wheel 包提供的 bdist_wheel

setup.cfg

setup.cfg文件包含setup.py脚本命令的默认选项。
如果构建和分发包的过程更加复杂,并且需要向setup.py命令中传入许多可选参数,那么这个文件非常有用。
你可以按照项目将这些默认参数保存在代码中。
这将使你的分发流程独立于项目之外,也能够让包的构建方式与向用户和其他团队成员的分发变得透明。

setup.cfg文件的语法与内置configparser模块提供的语法相同,因此它类似于常见的 Microsoft Windows INI 文件。
下面是安装配置文件的示例,提供了 global、sdist 和 bdist_wheel 命令的默认值,代码如下:

1
2
3
4
5
6
7
8
[global]
quiet=1

[sdist]
formats=zip,tar

[bdist_wheel]
universal=1

这个配置示例可以确保源代码发行版总是以两种格式创建(ZIP 和 TAR),并且构建 wheel 发行版将被创建为通用 wheel(与 Python 版本无关)。
此外,由于全局 quiet 开关,每个命令的大部分输出都将被阻止。
注意,这只是为了便于说明,默认阻止每个命令的输出可能并不是一个合理的选择。

MANIFEST.in

使用 sdist 命令构建发行版时,distutils 将浏览包的目录,查找需要包含在存档中的文件。
distutils 将包含:

  • py_modules、packages 和 scripts 选项隐含的所有 Python 源文件
  • ext_modules 选项列出的所有 C 源文件

最重要的元数据

除了被分发包的名称和版本之外,setup 可以接收的最重要的参数包括。

  • description: 包含描述包的几句话。
  • long_description: 包含完整说明,可以使用 reStructuredText 格式
  • keywords: 定义包的关键字列表
  • author: 作者的姓名和组织
  • author_email 联系人电子邮件地址
  • url: 项目的 URL。
  • license: 许可证(GPL、LGPL 等)
  • packages: 包中所有名称的列表;setuptools 提供了一个名为find_packages的小函数来计算它
  • namespace_packages: 命令空间包的列表

常见模式

对于没有经验的开发者来说,创建一个用于分发的包可能是一项乏味的任务。
如果不考虑元数据可能在项目其他部分找到的事实,setuptools 或 distuitls 在 setup() 函数调用中接受的大多数元数据都可以手动输入,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from setuptools import setup

setup(
    name="myproject",
    version="0.0.1",
    description="mypackage project short description",
    long_description="""
        Longer description of mypackage project
        possibly with some documentation and/or
        usage examples
    """,
    install_requires=[
        "dependency1",
        "dependency2",
        "etc",
    ]
)

这么做当然可行,但从长远来看很难维护,并且未来可能会出现错误和不一致。
setuptools 和 distuitls 都不能从项目源代码中自动提取各种元数据信息,因此你需要自己提供这些信息。
在 Python 社区中有一些常见模式剋解决最常见的问题,例如依赖管理、包含版本/自述文件等。
至少应该知道其中一些模式,因为它们非常流行,已经被看做一种打包惯例(packaging idioms)

(1) 自动包含包中的版本字符串

PEP 440(版本标识和依赖规范,Version Identification and Dependency Specifiaction)文档规定了版本和依赖规范的标准。
这是一份很长的文档,包含已接受的版本规范方案和 Python 打包工具中应该如何做版本匹配和比较。
如果你正在使用或打算使用一种复杂的项目版本编号方案,那么一定要阅读这份文档。
如果你使用的是一种简单方案,其中包含用点分开的一个、两个、三个或更多的数字,那么可以不必阅读 PEP 440。

目前来看,管理兼容性未来变化的最佳方法,就是正确使用语义化版本(Semantic Versioning semver)的版本号。
它是一个广为接受的标准,用仅包含 3 个数字的版本标识符来标记代码的变化范围。
它还给出了如何处理弃用的方法建议。下面是摘录 semver 官网的摘要。

版本格式:主版本号.次版本号.修订号,版本号递增规则如下:

  • 主版本号(MAJOR): 当你做了不兼容的 API 修改
  • 次版本号(MINOR): 当你做了向后兼容的功能性新增。
  • 修订号(PATCH): 当你做了向后兼容的问题修正。

先行版本号及版本编译信息可以加到“主版本号.次版本号.修改号”的后面,作为延伸。

另一个问题是将包或模块的版本标识符包含在什么位置。
PEP 396(模块版本号,Module Version Numbers)正好解决了这个问题。
注意,这份文档只是信息性的(informational),并且状态为延期(deferred),所以它并不是标准路径(standards track)的一部分。
不管怎样,它描述的内容现在似乎成了事实上的标准。
根据 PEP 396,如果一个包或模块要指定一个版本,那么应该将其包含在包的根目录(__init__.py)或模块文件的__version__属性中。
另一个事实上的标准是,也要将包括版本元组的 VERSION 属性包含其中。
这有助于用户编写兼容代码,因此如果版本方案足够简单的话,这样的版本元组容易比较。

因此,PyPI 上的很多包都遵循这两个标准。
它们的__init__.py文件包含如下所示的版本属性,如下所示:

1
2
3
4
# 用元组表示版本,可以简单比较
VERSION = (0, 1, 1)
# 利用元组创建字符串,以避免出现不一致
__version__ = ".".join([str(x) for x in VERSION])

延期的 PEP 396 的另一个建议是,在 distutils 的 setup() 函数中提供的版本应该从__version__派生,反之亦然。
Python 打包用户指南为单一来源的项目版本提供了多种模式,每一种都有自己的优点和局限性。
我个人最喜欢相当长的,并没有包含在 PyPA 的指南中,但它的优点是仅限制 setup.py 脚本的复杂度。
这个样板假定,版本标识符由包的__init__模块的VERSION属性给出,并且提取出这一数据包含在 setup() 调用中。
下面是某个虚构的包的 setup.py 脚本中的片段,其中使用了以下的方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from setuptools import setup
import os

def get_version(version_tuple):
    # additional handling of a,b,rc tags, this can
    # be simpler depending on your versioning scheme
    if not isinstance(version_tuple[-1], int):
        return '.'.join(
            map(str, version_tuple[:-1])
        ) + version_tuple[-1]

    return '.'.join(map(str, version_tuple))

# path to the packages __init__ module in project
# source tree
init = os.path.join(
    os.path.dirname(__file__), 'src', 'some_package',
    '__init__.py'
)

version_line = list(
    filter(lambda l: l.startswith('VERSION'), open(init))
)[0]

# VERSION is a tuple so we need to eval its line of code.
# We could simply import it from the package but we
# cannot be sure that this package is importable before
# finishing its installation
VERSION = get_version(eval(version_line.split('=')[-1]))

setup(
    name='some-package',
    version=VERSION,
    # ...
)

(2) 管理依赖

许多项目需要安装和/或使用一些外部包。
如果依赖列表很长的话,就会出现一个问题:如何管理依赖?
在大多数情况下答案很简单。不要过度设计(over-engineer)问题。
保持简单,并在 setup.py 脚本中明确提供依赖列表,代码如下:

1
2
3
4
5
6
7
from setuptools import setup

setup(
    name="some-package",
    install_requires=["falcon", "requests", "deloream"]
    # ...
)

有些 Python 开发者喜欢使用 requirements.txt 文件来追踪包的依赖列表。
在某些情况下,你可能会找到这么做的原因,但在大多数情况下,这是项目代码没有正确打包的时代遗留问题。
无论如何,即使像 Celery 这样著名的项目也仍然坚持使用这一约定。
因此,如果你不愿意改变习惯或者不知何故被迫使用 requirements.txt 文件,那么至少要将其做对。
下面是从 requirements.txt 文件读取依赖列表的常见做法之一:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from setuptools import setup
import os

def strip_comments(l):
    return l.split('#', 1)[0].strip()

def reqs(*f):
    return list(filter(None, [strip_comments(l) for l in open(
        os.path.join(os.getcwd(), *f)).readlines()]))

setup(
    name='some-package',
    install_requires=reqs('requirements.txt')
    # ...
)

自定义 setup 命令

添加非代码的其他文件

https://python-packaging-zh.readthedocs.io/zh_CN/latest/non-code-files.html

通常我们的包都需要一下不是python代码的文件, 例如图片, 数据, 文档等等. 为了让setuptools正确处理这些文件, 我们需要特别定义一下这些文件.

我们需要在 MANIFEST.in 中指定这些文件, MANIFEST.in 提供了一个文件清单, 使用相对路径或是绝对路径指出打包时需要包含的特殊文件.:

1
2
3
include README.rst
include docs/*.txt
include funniest/data.json

为了让在安装的时候这些特殊文件能被复制到 site-packages 下的文件夹中, 需要在setup()添加参数include_package_data=True.

添加在安装包中的文件(例如计算需要的数据文件)应该放在Python模块的文件夹里面(例如 funniest/funniest/data.json ). 在加载这些文件的时候, 使用相对路径再加上 __file__ 变量.

上传包

https://packaging.python.org/tutorials/packaging-projects/

1
2
pip install --upgrade setuptools wheel
python setup.py sdist bdist_wheel

This command should output a lot of text and once completed should generate two files in the dist directory:

1
2
3
dist/
  example_pkg_YOUR_USERNAME_HERE-0.0.1-py3-none-any.whl
  example_pkg_YOUR_USERNAME_HERE-0.0.1.tar.gz
1
2
3
4
5
pip install --upgrade twine
twine --version

twine version 3.2.0 (pkginfo: 1.5.0.1, requests: 2.24.0, setuptools: 50.3.2,
requests-toolbelt: 0.9.1, tqdm: 4.51.0)

执行twine upload dist/*

然后输入 pypi 的账号密码

更新包

首先必须要更新项目版本号

1
2
3
rm -rf dist
python setup.py sdist bdist_wheel
twine upload  dist/*