Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 376 – 已安装 Python 发行版的数据库

作者:
Tarek Ziadé <tarek at ziade.org>
状态:
最终版
类型:
标准跟踪
主题:
打包
创建日期:
2009年2月22日
Python 版本:
2.7, 3.2
发布历史:
2009年6月22日

目录

重要

本 PEP 是一份历史文档。最新且权威的规范,核心元数据规范,维护在 PyPA 规范页面上。

×

有关如何提出更改的建议,请参阅PyPA 规范更新流程

摘要

此 PEP 的目标是提供一个标准基础设施来管理系统上安装的项目发行版,以便所有安装或移除项目的工具都能互操作。

为了实现这一目标,本 PEP 提出了一个描述系统上已安装发行版的新格式。它还描述了标准库的参考实现。

过去曾尝试创建安装数据库(参见 PEP 262)。

结合 PEP 345,当前提案取代了 PEP 262

注意:实施计划并未如预期进行,因此对于本 PEP 而言,应将其视为仅供参考。

基本原理

目前 Python 中发行版安装方式存在两个问题

  • 方式太多,导致互操作性困难。
  • 没有 API 可以获取已安装发行版的信息。

发行版如何安装

目前,当一个发行版安装到 Python 中时,每个元素都可以安装到不同的目录中。

例如,Distutils 将纯 Python 代码安装在 purelib 目录中,对于类 Unix 系统和 Mac OS X,该目录是 lib/python2.6/site-packages,而在 Windows 下,则是 Python 安装目录下的 Lib\site-packages

此外,Distutils install 命令的 install_egg_info 子命令会将项目的 .egg-info 文件添加到 purelib 目录中。

例如,对于包含一个包、一个额外模块和可执行脚本的 docutils 发行版,有三个元素安装在 site-packages

  • docutils: docutils 包。
  • roman.py: docutils 使用的额外模块。
  • docutils-0.5-py2.6.egg-info:一个文件,包含 PEP 314 中描述的发行版元数据。此文件对应于 sdist 命令构建的名为 PKG-INFO 的文件。

一些可执行脚本,例如 rst2html.py,也会添加到 Python 安装的 bin 目录中。

另一个名为 setuptools [1] 的项目有两种其他的发行版安装格式,称为 EggFormats [4]

  • 一个自包含的 .egg 目录,其中包含所有发行版文件和发行版元数据,位于一个名为 EGG-INFO 的子目录中的 PKG-INFO 文件中。setuptools 在该目录中创建其他文件,这些文件可被视为补充元数据。
  • 一个安装在 site-packages 中的 .egg-info 目录,其中包含与 .egg 格式中 EGG-INFO 相同的文件。

如果您在 setup.py 文件中使用 setuptools.setup 函数而非 distutils.core.setup 函数安装发行版,则会自动使用第一种格式。

setuptools 还会将对发行版的引用添加到 easy-install.pth 文件中。

最后,setuptools 项目提供了一个名为 easy_install [2] 的可执行脚本,该脚本将所有发行版(包括基于 distutils 的发行版)安装到自包含的 .egg 目录中。

如果您想为您的发行版拥有独立的 .egg-info 目录,例如第二种 setuptools 格式,您必须在处理基于 setuptools 的发行版或使用 easy_install 脚本时强制执行。您可以通过使用 --single-version-externally-managed 选项 --root 选项来强制执行。这将使 setuptools 项目像 distutils 一样安装项目。

此选项由以下组件使用

  • pip [3] 安装程序
  • Fedora 打包者 [5]
  • Debian 打包者 [6]

卸载信息

Distutils 不提供 uninstall 命令。如果您想卸载一个发行版,您必须成为高级用户,移除已安装的各种元素,然后查看 .pth 文件以在必要时进行清理。

而且,这个过程因您用于安装发行版的工具以及发行版的 setup.py 是使用 Distutils 还是 Setuptools 而异。

在某些情况下,您可能无法确定是否已删除所有内容,或者是否因删除了在多个发行版之间共享的文件而损坏了另一个发行版。

但有一个共同的行为:当你安装一个发行版时,文件会被复制到你的系统中。并且可以跟踪这些文件以便以后删除。

此外,Pip 项目最近获得了 uninstall 功能。它使用 install 命令的 record 选项记录所有已安装的文件。

此 PEP 提议的内容

为了解决这些问题,本 PEP 提出了几项更改

  • 一种新的 .dist-info 结构,使用目录,灵感来自 setuptoolsEggFormats 标准的一种格式。
  • pkgutil 中新的 API,能够查询已安装发行版的信息。
  • Distutils 中的卸载函数和卸载脚本。

每个已安装发行版一个 .dist-info 目录

本 PEP 提出了一个安装格式,灵感来自 EggFormats 标准中的一个选项,即使用位于 site-packages 目录中的独立目录。

这个独特的目录命名如下

name + '-' + version + '.dist-info'

.dist-info 目录可以包含这些文件

  • METADATA:包含元数据,如 PEP 345PEP 314PEP 241 中所述。
  • RECORD:记录已安装文件的列表
  • INSTALLER:记录用于安装项目的工具名称
  • REQUESTED:此文件的存在表示项目安装是明确请求的(即,不是作为依赖项安装的)。

METADATA、RECORD 和 INSTALLER 文件是强制性的,而 REQUESTED 可能缺失。

此提案不会影响 Python 本身,因为除了 Distutils 之外,标准库中尚未在任何地方使用元数据文件。

它将影响 setuptoolspip 项目,但是,鉴于它们已经使用包含 PKG-INFO 文件的目录,因此更改不会产生深远影响。

RECORD

当使用 install 命令安装源发行版时,在安装时会在 .dist-info 目录中添加一个 RECORD 文件。请注意,当安装使用 bdist 命令或基于 bdist 命令创建的二进制发行版时,RECORD 文件也将被安装,因为这些命令使用 install 命令来创建二进制发行版。

RECORD 文件保存已安装文件的列表。这些文件对应于 install 命令的 record 选项所列出的文件,并将默认生成。这允许实现卸载功能,如本 PEP 后文所述。install 命令还提供了一个选项来阻止写入 RECORD 文件,创建系统包时应使用此选项。

第三方安装工具也不应在未提示或警告的情况下覆盖或删除不在 RECORD 文件中的文件。

此 RECORD 文件受 PEP 262 FILES 的启发。

RECORD 文件是一个 CSV 文件,由记录组成,每行一个已安装文件。使用 csv 模块读取文件,选项如下

  • 字段分隔符:,
  • 引用字符:"
  • 行终止符:os.linesep(因此为 \r\n\n

安装发行版时,文件可以安装在以下位置:

  • 基本位置:由 --install-lib 选项定义的路径,默认为 site-packages 目录。
  • 安装前缀:由 --prefix 选项定义的路径,默认为 sys.prefix
  • 系统上的任何其他路径。

每条记录由三个元素组成

  • 文件的路径
    • 如果文件在 base location 下,则为相对于 base location 的 ‘/’ 分隔路径。
    • 如果文件在 安装前缀 下,并且 基本位置安装前缀 的子路径,则为相对于 基本位置 的 ‘/’ 分隔路径。
    • 使用本地平台分隔符的绝对路径
  • 文件内容的哈希值。请注意,pycpyo 生成的文件没有任何哈希值,因为它们是从 py 文件自动生成的。因此,检查相应 py 文件的哈希值足以判断该文件及其相关的 pycpyo 文件是否已更改。

    哈希值可以是空字符串,也可以是 hashlib.algorithms_guaranteed 中命名的哈希算法,后跟等号 =,再后跟摘要的 urlsafe-base64-nopad 编码(base64.urlsafe_b64encode(digest) 并移除尾随的 =)。

  • 文件大小(字节)

使用 csv 模块生成此文件,因此字段分隔符为“,”。字段中发现的任何“,”字符都由 csv 自动转义。

读取文件时,使用 U 选项,从而激活通用换行支持(参见 PEP 278),避免了在不同换行符的平台上读取文件时出现问题。

这是一个 RECORD 文件示例(节选)

lib/python2.6/site-packages/docutils/__init__.py,md5=nWt-Dge1eug4iAgqLS_uWg,9544
lib/python2.6/site-packages/docutils/__init__.pyc,,
lib/python2.6/site-packages/docutils/core.py,md5=X90C_JLIcC78PL74iuhPnA,66188
lib/python2.6/site-packages/docutils/core.pyc,,
lib/python2.6/site-packages/roman.py,md5=7YhfNczihNjOY0FXlupwBg,234
lib/python2.6/site-packages/roman.pyc,,
/usr/local/bin/rst2html.py,md5=g22D3amDLJP-FhBzCi7EvA,234
/usr/local/bin/rst2html.pyc,,
python2.6/site-packages/docutils-0.5.dist-info/METADATA,md5=ovJyUNzXdArGfmVyb0onyA,195
lib/python2.6/site-packages/docutils-0.5.dist-info/RECORD,,

注意,RECORD 文件不能包含自身的哈希值,此处仅提及。

一个将 config.ini 文件安装到 /etc/myapp 的项目将像这样添加

/etc/myapp/config.ini,md5=gLfd6IANquzGLhOkW4Mfgg,9544

对于 Windows 平台,绝对路径会添加驱动器号,因此复制到 c:MyApp 的文件将是

c:\etc\myapp\config.ini,md5=gLfd6IANquzGLhOkW4Mfgg,9544

INSTALLER

install 命令有一个新选项,名为 installer。此选项是用于调用安装的工具名称。它是一个规范化的小写字符串,匹配 [a-z0-9_\-\.]

$ python setup.py install –installer=pkg-system

如果未提供,则默认为 distutils

安装发行版时,会在 .dist-info 目录中生成带有此值的 INSTALLER 文件,以跟踪 安装了该发行版。该文件是一个单行文本文件。

REQUESTED

有些安装工具会自动检测未满足的依赖项并安装它们。在这种情况下,跟踪哪些发行版纯粹作为依赖项安装会很有用,这样如果其依赖的发行版以后被卸载,用户就可以被提醒孤立的依赖项。

如果一个发行版是应用户直接请求安装的(常见情况),则会在已安装发行版的 .dist-info 目录中添加一个 REQUESTED 文件。REQUESTED 文件可以是空的,也可以包含一行以“#”字符开头的注释标记。

如果安装工具自动安装一个发行版作为另一个发行版的依赖项,则不应创建 REQUESTED 文件。

distutils 的 install 命令默认创建 REQUESTED 文件。它接受 --requested--no-requested 选项,以明确指定是否创建该文件。

如果一个已作为依赖项安装在系统上的发行版后来按名称安装,distutils install 命令将在现有安装的 .dist-info 目录中创建 REQUESTED 文件。

实现细节

注意:本节不具规范性。最终,此 PEP 由第三方库和工具实现,而非标准库。

pkgutil 中的新函数和类

为了使用 .dist-info 目录内容,我们需要在标准库中添加一套 API。放置这些 API 的最佳位置是 pkgutil

函数

pkgutil 模块中新增的函数有

  • distinfo_dirname(name, version) -> 目录名
    name 通过将任何非字母数字字符串替换为单个 ‘-‘ 转换为标准发行版名称。

    version 被转换为标准版本字符串。空格变为点,所有其他非字母数字字符(点除外)变为破折号,多个破折号连续的缩减为单个破折号。

    然后将这两个属性都转换为文件名转义形式,即除了“dist-info”中的破折号以及名称和版本号之间的破折号之外,所有“-”字符都替换为“_”。

  • get_distributions() -> Distribution 实例的迭代器。

    提供一个迭代器,用于在 sys.path 中查找 .dist-info 目录,并为每个目录返回 Distribution 实例。

  • get_distribution(name) -> Distribution 或 None。
  • obsoletes_distribution(name, version=None) -> Distribution 实例的迭代器。

    迭代所有发行版以查找哪些发行版 淘汰 name。如果提供了 version,它将用于过滤结果。

  • provides_distribution(name, version=None) -> Distribution 实例的迭代器。

    遍历所有发行版,查找哪些发行版“提供”name。如果提供了 version,它将用于过滤结果。扫描 sys.path 中的所有元素,并查找所有以 .dist-info 结尾的目录。返回与包含匹配 name 元数据的 METADATA 的 .dist-info 目录对应的 Distribution

    此函数仅返回第一个结果,因为预计不会有多个值。如果未找到目录,则返回 None。

  • get_file_users(path) -> Distribution 实例的迭代器。

    迭代所有发行版以找出哪些发行版使用了 pathpath 可以是本地绝对路径或相对的 ‘/’ 分隔路径。

    本地绝对路径是一个绝对路径,其中 ‘/’ 的出现已被 os.sep 给出的系统分隔符替换。

Distribution 类

一个名为 Distribution 的新类被创建,其构造函数提供了 .dist-info 目录的路径。当它实例化时,它会读取 METADATA 中包含的元数据。

Distribution(path) -> 实例

为给定 path 创建一个 Distribution 实例。

Distribution 提供以下属性

  • name:发行版的名称。
  • metadata:一个已加载发行版 METADATA 文件的 DistributionMetadata 实例。
  • requested:一个布尔值,表示 REQUESTED 元数据文件是否存在(换句话说,发行版是否由用户请求安装)。

以及以下方法

  • get_installed_files(local=False) -> (path, hash, size) 的迭代器

    遍历 RECORD 条目,并为每行返回一个元组 (path, hash, size)。如果 localTrue,则路径将转换为本地绝对路径。否则返回 RECORD 中的原始值。

    本地绝对路径是一个绝对路径,其中 ‘/’ 的出现已被 os.sep 给出的系统分隔符替换。

  • uses(path) -> 布尔值

    如果 pathRECORD 中列出,则返回 Truepath 可以是本地绝对路径,也可以是相对的 ‘/’ 分隔路径。

  • get_distinfo_file(path, binary=False) -> 文件对象
    返回位于 .dist-info 目录下的文件。

    返回由 path 指向的文件 file 实例。

    path 必须是相对于 .dist-info 目录的 ‘/’ 分隔路径,或是一个绝对路径。

    如果 path 是绝对路径且不以 .dist-info 目录路径开头,则会引发 DistutilsError

    如果 binaryTrue,则以只读二进制模式 (rb) 打开文件;否则以只读模式 (r) 打开。

  • get_distinfo_files(local=False) -> 路径迭代器

    遍历 RECORD 条目,并为每行返回路径,如果该路径指向 .dist-info 目录或其子目录中的文件。

    如果 localTrue,则每个路径都会转换为本地绝对路径。否则返回 RECORD 中的原始值。

请注意,API 组织成五个类,这些类处理目录和 Zip 文件(因此它适用于 Zip 文件中包含的文件,有关更多详细信息,请参阅 PEP 273)。这些类在原型实现的文档中为感兴趣的读者进行了描述 [7]

示例

让我们用我们的 docutils 示例来使用一些新的 API

>>> from pkgutil import get_distribution, get_file_users, distinfo_dirname
>>> dist = get_distribution('docutils')
>>> dist.name
'docutils'
>>> dist.metadata.version
'0.5'

>>> distinfo_dirname('docutils', '0.5')
'docutils-0.5.dist-info'

>>> distinfo_dirname('python-ldap', '2.5')
'python_ldap-2.5.dist-info'

>>> distinfo_dirname('python-ldap', '2.5 a---5')
'python_ldap-2.5.a_5.dist-info'

>>> for path, hash, size in dist.get_installed_files()::
...     print '%s %s %d' % (path, hash, size)
...
python2.6/site-packages/docutils/__init__.py,b690274f621402dda63bf11ba5373bf2,9544
python2.6/site-packages/docutils/core.py,9c4b84aff68aa55f2e9bf70481b94333,66188
python2.6/site-packages/roman.py,a4b84aff68aa55f2e9bf70481b943D3,234
/usr/local/bin/rst2html.py,a4b84aff68aa55f2e9bf70481b943D3,234
python2.6/site-packages/docutils-0.5.dist-info/METADATA,6fe57de576d749536082d8e205b77748,195
python2.6/site-packages/docutils-0.5.dist-info/RECORD

>>> dist.uses('docutils/core.py')
True

>>> dist.uses('/usr/local/bin/rst2html.py')
True

>>> dist.get_distinfo_file('METADATA')
<open file at ...>

>>> dist.requested
True

Distutils 中的新函数

Distutils 已经提供了一种非常基本的安装发行版的方式,即在发行版的 setup.py 脚本上运行 install 命令。

Distutils2 将提供一个非常基本的 uninstall 函数,该函数被添加到 distutils2.util 中,并以要卸载的发行版名称作为其参数。uninstall 使用前面描述的 API,并移除所有唯一文件,只要它们的哈希值没有改变。然后它会移除留下的空目录。

uninstall 返回已卸载文件的列表

>>> from distutils2.util import uninstall
>>> uninstall('docutils')
['/opt/local/lib/python2.6/site-packages/docutils/core.py',
 ...
 '/opt/local/lib/python2.6/site-packages/docutils/__init__.py']

如果未找到发行版,则会引发 DistutilsUninstallError

过滤

为了使其成为希望控制 uninstall 工作方式的第三方项目的参考 API,可以使用第二个可调用参数。它为每个被删除的文件调用。如果可调用返回 True,则文件被删除。如果返回 False,则文件保留。

示例

>>> def _remove_and_log(path):
...     logging.info('Removing %s' % path)
...     return True
...
>>> uninstall('docutils', _remove_and_log)

>>> def _dry_run(path):
...     logging.info('Removing %s (dry run)' % path)
...     return False
...
>>> uninstall('docutils', _dry_run)

当然,第三方工具可以使用更底层的 pkgutil API 来实现自己的卸载功能。

安装程序标记

如本 PEP 前文所述,install 命令会在 .dist-info 目录中添加一个 INSTALLER 文件,其中包含安装程序的名称。

为了避免删除由另一个打包系统安装的发行版,uninstall 函数接受一个额外的参数 installer,默认为 distutils2

调用时,uninstall 会检查 INSTALLER 文件是否与此参数匹配。如果不匹配,它会引发 DistutilsUninstallError

>>> uninstall('docutils')
Traceback (most recent call last):
...
DistutilsUninstallError: docutils was installed by 'cool-pkg-manager'

>>> uninstall('docutils', installer='cool-pkg-manager')

这允许第三方应用程序使用 uninstall 函数,并强烈建议任何其他程序不要移除其先前安装的发行版。当一个依赖于 Distutils API 的第三方程序在安装时执行额外步骤,并且必须在卸载时撤销这些步骤时,这很有用。

添加卸载脚本

Distutils2 中新增了一个 uninstall 脚本,其用法如下

$ python -m distutils2.uninstall projectname

注意,脚本不控制移除一个发行版是否会破坏另一个发行版。尽管它通过使用卸载函数确保它移除的所有文件都没有被其他发行版使用。

另请注意,此卸载脚本不关注 REQUESTED 元数据;它仅供外部工具使用,以提供更高级的依赖管理。

向后兼容性和路线图

这些更改不会引入任何兼容性问题,因为它们将在以下方面实现

  • pkgutil 中的新函数
  • distutils2

该计划是将本 PEP 中概述的功能包含在 Python 3.2 的 pkgutil 和 Distutils2 中。

Distutils2 也将包含新的 pgkutil 的反向移植,并且可以在 2.4 版本及更高版本中使用。

使用现有、标准化前格式安装的发行版不具备新 API 所需的元数据,因此将被忽略。当然,第三方工具可以继续支持以前的格式以及新格式,以简化过渡。

参考资料

致谢

Jim Fulton、Ian Bicking、Phillip Eby、Rafael Villar Burke 以及 Pycon 和 Distutils-SIG 的许多人。


来源: https://github.com/python/peps/blob/main/peps/pep-0376.rst

最后修改: 2024-12-15 20:57:13 GMT