首页 > 编程笔记 > Python笔记 阅读:1,838

如何创建Python程序包,Python程序包结构详解(超级详细)

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

setup.py脚本文件

对于一个需要被分发的包来说,其根目录包含一个 setup.py 脚本,它定义了 distutils 模块中描述的所有元数据,并将其合并为标准的 setup() 函数调用的参数。

虽然 distutils 是一个标准库模块,但建议读者使用 setuptools 包来代替,因为它对标准的 distutils 做了一些改进。

因此,这个文件的最少内容如下:
from setuptools import setup
setup(
    name='mypackage',
)
其中,name 给出了包的全名。

另外,该脚本提供了一些命令,可以用 --help -commands 选项列出这些命令:

$Python setup.py --help-commands
tandard commands:
  build             build everything needed to install
  clean             clean up temporary files from 'build' command
  install           install everything from build directory
  sdist             create a source distribution (tarball, zip file)
  register          register the distribution with the PyP
  bdist             create a built (binary) distribution
  check             perform some checks on the package
  upload            upload binary package to PyPI

Extra commands:
  develop           install package in 'development mode'
  alias             define a shortcut to invoke one or more commands
  test              run unit t#sts after in-place build
  bdist_wheel       create a wheel distribution

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

实际的命令列表更长,而且会根据 setuptools 的可用扩展而变化。这里所列举的都是相对来说比较重要的,且和本节相关的命令。

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 命令的默认值,代码如下:

[global]
quiet=1

[sdist]
formats=zip,tar

[bdist_wheel】
universal=1

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

注意,这只是为了便于说明,默认阻止每个命令的输出可能并不是一个合理的选择。

MANIFEST.in

使用 sdist 命令构建发行版时,distutils 将浏览包的目录,查找需要包含在存档中的文件。distutils 将包含: 匹配 glob 模式 test/test*.py 的文件包括:README、README.txt、setup.py 和 setup.cfg。

此外,如果你的包是由 subversion 或 CVS 管理,那么 sdist 将浏览诸如 .svn 之类的文件夹,查找需要包含的文件。利用扩展也可以与其他版本控制系统集成。sdist 将构建—个 MANIFEST 文件,列出所有文件并将它们包含在存档中。

假设你不使用这些版本控制系统,并且需要包含更多的文件,那么在与 setup.py 相同的目录中,可以为 MANIFEST 文件定义一个名为 MANIFEST.in 的模板,在其中可以指定 sdist 要包含哪些文件。

这个模板的每一行都定义一条包含或排除规则,例如:

include HISTORY.txt
include README.txt
include CHANGES.txt
include CONTRIBUTORS.txt
include LICENSE
recursive-include *.txt *.py

MANIFEST.in 命令的完整列表可以在 distutils 官方文档中找到。

最重要的元数据

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

trove分类器

PyPI 和 distutils 为应用程序分类提供了一种解决方案,就是使用一套 trove 分类器。所有 trove 分类器都形成一个树状结构,每个分类器都是字符 串形式,其中用 :: 字符串分隔每个命名空间。分类器列表在包定义中是作为 setup() 函数的 classifiers 参数。

下面是 PyPI上某个项目的分类器列表示例(这里是 solrq 项目):
from setuptools import setup
setup(
    name="solrq",
    # (...)
    classifiers=[
        'Development Status :: 4 - Beta',
        'Intended Audience :: Developers',
        'License :: OSI Approved :: BSD License',
        'Operating System :: OS Independent',
        'Programming Language :: Python',
        'Programming Language :: Python :: 2',
        'Programming Language :: Python :: 2.6',
        'Programming Language :: Python :: 2.7',
        'Programming Language :: Python :: 3',
        'Programming Language :: Python :: 3.2',
        'Programming Language :: Python :: 3.3',
        'Programming Language :: Python :: 3.4',
        'Programming Language :: Python :: Implementation :: PyPy',
        'Topic :: Internet :: WWW/HTTP :: Indexing/Search',
    ],
)
它们在包定义中是完全可选的,但可以对 setup() 接口中可用的基本元数据提供有用的扩展。

此外,trove 分类器还可以提供支持的 Python 版本或系统、项目的开发阶段或发布代码所使用的许可证等信息。许多 PyPI 用户按类别对可用的包进行搜索和浏览,因此正确的分类可以让 Python 包找到目标客户。

trove分类器在整个打包生态系统中发挥重要作用,不应该被忽略。没有一个组织来验证包的分类,所以我们有责任为自己的包提供正确的分类器,并且不要为整个包索引带来混乱。

在编写本教程时,PyPI 上共有 608 个可用的分类器,分为以下 9 类:
  1. 开发状态(DevelopmentStatus)
  2. 环境(Environment)
  3. 框架(Framework)
  4. 目标受众(IntendedAudience)
  5. 许可证(License)
  6. 自然语言(Natural Language)
  7. 操作系统(Operating System)
  8. 编程语言(Programming Language)
  9. 话题(Topic)

由于不时会添加新的分类器,所以在阅读本教程时,这些数字可能会有所不同。当前可用的 trove 分类器的完整列表可以用 setup.py register --list-classifiers 命令来查看。

常见模式

对于没有经验的开发者来说,创建一个用于分发的包可能是一项乏味的任务。如果不考虎元数据可能在项目其他部分找到的事实,setuptools 或 distuitls 在 setup() 函数调用中接受的大多数元数据都可以手动输入,代码如下:
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',
        'etc1',
    ]
)
这么做当然可行,但从长远来看很难维护,并且未来可能会出现错误和不一致。setuptools 和 distuitls 都不能从项目源代码中自动提取各种元数据信息,因此需要自己提供这些佶息。

在 Python 社区中,有一些常见模式可以解决最常见的问题。例如依赖管理、包含版本/自述文件等。至少应该知道其中一些模式,因为它们非常流行,己经被看作一种打包惯例。

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

PEP440(版本标识和依赖规范)文档规定了版本和依赖规范的标准。这是一份很长的文档,包含已接受的版本规范方案和 Python 打包工具中应该如何做版本匹配和比较。

如果你正在使用或打算使用一种复杂的项目版本编号方案,那么一定要阅读这份文档;如果你使用的是一种简单方案,其中包含用点分开的一个、两个、三个或更多的数字,那么可以不必阅读 PEP 440。

另一个问题是将包或模块的版本标识符包含在什么位罝。PEP 396(模块版本号)正好解决这个问题。注意,这份文档只是信息性的,并且状态为延期,所以它并不是标准路径的一部分。不管怎样,它描述的内容现在似乎成了事实上的标准。

根据 PEP 396,如果一个包或模块要指定一个版本,那么应该将其包含在包的根目录(__init__.py)或模块文件的 __version__ 属性中。另一个事实上的标准是,也要将包括版本元组的 VERSION 属性包含其中,这有助于用户编写兼容代码,因为如果版本方案足够简单的话,这样的版本元组很容易比较。

因此,PyPI 上的很多包都进循这两个标准。它们的 __init__.py 文件包含如下所示的版本属性,如下所示:
#用元组表示版本,可以简单比较
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 脚本中的片段,其中使用了以下这种方法:
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) README文件

Python 包索引可以在 PyPI 门户的包页面中显示一个项目的 readme 或者 long_description 的值。你可以用 reStructuredText 标记来编写这个说明,它在上传时会转换为 HTML 格式。

不幸的是,目前 PyPI 上的文档标记只能使用 reStructuredText,这在短期内也不太可能改变。更有可能的是,如果 warehouse 项目完全取代了当前的 PyPI 实现,那么将会支持其他标记语言。但是,我们仍然不知道 warehouse 的最终发布时间。

但是,许多开发者想要使用不同的标记语言,原因有很多。最常见的选择是 Markdown。它是 GitHub 上默认的标记语言,目前大多数开源的 Python 开发都是在 GitHub 上。

因此,GitHub 和 Markdown 的粉丝通常要么忽略这个问题,要么就提供两份独立的文档文本。提供给 PyPI 的说明要么是项目 GitHub 页面上说明的简短版本,要么是在 PyPI 上无法正常显示的普通的无格式 Markdown。

如果你想使用除了 rcStructurcdText 之外的标记语言来编写项目的 README,你仍然可以用可读的形式将它作为 PyPI 页面上的项目说明。诀窍是在将包上传到 Python 包索引时使用 pypandoc 包将你使用的其他脚本语言转换成 reStructuredText,同时准备 readme 文件的简单内容作为备用(fallback)也很重要,这样即使用户没有安装 pypandoc,安装也不会失败,代码如下:
try:
    from pypandoc import convert
    def read_md(f):
        return convert(f, 'rst')
except ImportError:
    convert = None
    print(
        "warning: pypandoc module not found, could not convert Markdown to RST"
    )

    def readjnd(f):
        return open(f, 'r').read() #noqa
README = os.path.join (os.path.dirname(__file__), 'README.md')

setup(
    name='some-package',
    long_description=read_md(README),
    #...
)

3) 管理依赖

许多项目需要安装和(或)使用一些外部包。如果依赖列表很长的话,就会出现一个问題,即如何管理依赖?在大多数情况下答案很简单,不要过度设计问题。保持简单,并在 setup.py 脚本中明确提供依赖列表,代码如下:
from setuptools import setup
setup(
    name = 'some-package',
    install_requires=['falcon', 'requests', 'delorean']
    #...
)
有些 Python 开发者喜欢使用 requirements.txt 文件来追踪包的依赖列表。在某些情况下,你可能会找到这么做的原因,但在大多数情况下,这是项目代码没有正确打包的时代遗留的问题。

无论如何,即使像 Celery 这样著名的项目也仍然坚持使用这一约定。因此,如果你不愿意改变习惯或者不知何故被迫使用 requirements.txt 文件,那么至少要将其做对。下面是从 requirements.txt 文件读取依赖列表的常见做法之一:
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')
    #...
)

所有教程

优秀文章