Python发布包

wheel and egg diff

Wheel和Egg都是python的打包格式,为了不需要构建和编译就可以安装。

Egg格式是由setuptools在2004年引入,而Wheel格式是由PEP427在2012年定义。Wheel目前被认为是Python构建二进制打包的标准

主要的不同点:

  • Wheel有一个官方的PEP427来定义,而Egg没有PEP定义
  • Wheel是一种分发格式,即打包格式。而Egg既是一种分发格式,也是一种 运行时安装的格式,并且是可以被import的

  • Wheel文件不会包含.pyc文件,只有Python文件,兼容py2和py3,更加通用类似sdist
  • Wheel使用和PEP376兼容的.dist-info目录,而Egg使用.egg-info目录。
  • Wheel有着更丰富的命名规则
  • Wheel是有版本的,每个Wheel文件都包含wheel规格的版本和打包它的实现。
  • Wheel内部由sysconfig path type组织,因此更容易转换为其他格式。

setup.py

发布使用setuptools工具,需要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
36
37
38
39
40
41
42
43
44
45
import setuptools

TESTS_REQUIRES = [
  'pytest',
  'pytest-tornasync',
  'mypy'
]

with open('requirements.txt') as f:
  REQUIRES = f.readlines()

setuptools.setup(
  name='kubeflow-training',  # 包名称
  version='1.3.0', # 包版本
  author="Kubeflow Authors", # 作者名字
  author_email='hejinchi@cn.ibm.com', # 作者邮箱
  license="Apache License Version 2.0", # 维护者名字
  url="https://github.com/kubeflow/training-operator/sdk/python", # 主页地址
  description="Training Operator Python SDK", # 简短描述
  long_description="Training Operator Python SDK", # 详细描述
  packages=setuptools.find_packages(  # 该库包含的 Python 包(源码目录)
    include=("kubeflow*")),
  package_data={},
  include_package_data=False,
  zip_safe=False,
  classifiers=[ # pypi 侧栏展示数据
    'Intended Audience :: Developers',
    'Intended Audience :: Education',
    'Intended Audience :: Science/Research',
    'Programming Language :: Python :: 3',
    'Programming Language :: Python :: 3.6',
    'Programming Language :: Python :: 3.7',
    "License :: OSI Approved :: Apache Software License",
    "Operating System :: OS Independent",
    'Topic :: Scientific/Engineering',
    'Topic :: Scientific/Engineering :: Artificial Intelligence',
    'Topic :: Software Development',
    'Topic :: Software Development :: Libraries',
    'Topic :: Software Development :: Libraries :: Python Modules',
  ],
  install_requires=REQUIRES, # 该库依赖,安装该库之前安装
  tests_require=TESTS_REQUIRES,
  extras_require={'test': TESTS_REQUIRES} # 可选依赖库,安装该库不会自动安装
)

markdown描述

1
2
3
4
5
6
7
8
9
10
11
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',
    # ...
)

依赖

  • install_requires:依赖的其他库列表,安装该库之前也会安装
  • extras_require:其他的可选依赖库,安装该库不会自动安装
  • setup_requires:构建依赖的库,不会安装到解释器库,安装到本地临时目录
  • python_requires:Python 版本依赖
  • use_2to3:布尔值,True 则自动将 Python2 的代码转换为 Python3
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
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 版本才需要的,可以这样指定

1
2
3
4
5
6
setup(
    ...
    install_requires=[
        "enum34;python_version<'3.4'",
    ]
)

特定操作系统依赖

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

1
2
3
4
5
6
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 工具函数即可。

1
2
3
4
5
6
7
from setuptools import setup
from setuptools import find_packages

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

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

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

1
2
3
4
5
6
my_project
|- src
    |- my_package
       |- __init__.py
       |- main.py
setup.py

可以如下配置

1
2
3
4
5
6
7
8
from setuptools import setup
from setuptools import find_packages

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

引入其他的数据文件

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

1
2
3
4
5
6
7
8
9
setup(
    ...
    package_data={
        # 引入任何包下面的 *.txt、*.rst 文件
        "": ["*.txt", "*.rst"],
        # 引入 hello 包下面的 *.msg 文件
        "hello": ["*.msg"],
    },
)

通过 MANIFEST.in 文件配置

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

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

1
2
3
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 方便的配置。

1
2
3
4
5
6
setup(
    ...
    entry_points={
        "console_scripts": ["flask = flask.cli:main"]
    },
)

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

1
2
3
4
5
6
7
8
9
10
11
12
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 中定义插件入口。

1
2
3
4
5
6
7
8
setup(
    ...
    entry_points={
        "console_scripts": [
            "foo = my_package.some_module:main_func",
        ],
    }
)

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

1
2
3
4
5
6
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 就是利用这个机制实现自定义命令扩展的。

1
2
3
4
5
6
7
8
setup(
    ...
    entry_points={
        'flask.commands': [
            'test=my_package.commands:cli'
        ],
    },
)

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

1
2
3
4
5
6
7
8
9
10
11
12
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

打包

tar

1
python setup.py sdist

wheel

1
2
3
4
5
pip install wheel
python setup.py bdist_wheel

# 安装
pip install <path-to-package>

发布

注册pipy,获得用户名和密码

1
2
3
4
# 上传tar
python setup.py sdist upload
# 上传whl
python setup.py bdist_wheel upload

常见~/.pypirc文件

1
2
3
[pypi]
username=your_username
password=your_password