美文网首页趣说编程
Python包发布指南

Python包发布指南

作者: 火眼0狻猊 | 来源:发表于2020-05-10 11:43 被阅读0次

    前言

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

    来自知乎专栏

    参考

    相关文章

      网友评论

        本文标题:Python包发布指南

        本文链接:https://www.haomeiwen.com/subject/cyzdnhtx.html