PEP 376 – 已安装 Python 发行版的数据库
- 作者:
- Tarek Ziadé <tarek at ziade.org>
- 状态:
- 最终
- 类型:
- 标准轨迹
- 主题:
- 打包
- 创建:
- 2009-02-22
- Python 版本:
- 2.7, 3.2
- 历史记录:
- 2009-06-22
摘要
本 PEP 的目标是提供一个标准基础设施来管理系统上安装的项目发行版,这样所有安装或删除项目的工具都能够互操作。
为了实现此目标,本 PEP 提出了一个新的格式来描述系统上安装的发行版。它还描述了标准库的参考实现。
过去曾尝试创建安装数据库(参见PEP 262)。
注意:实施计划没有按预期进行,因此对于本 PEP 而言,它应该被视为仅供参考。
基本原理
目前 Python 中的发行版安装方式存在两个问题
- 安装方式太多,这使得互操作性变得困难。
- 没有 API 可以获取已安装发行版的信息。
发行版如何安装
现在,当发行版在 Python 中安装时,每个元素都可以在不同的目录中安装。
例如,Distutils
将纯 Python 代码安装在 purelib
目录中,对于类 Unix 系统和 Mac OS X,该目录为 lib/python2.6/site-packages
,对于 Windows,该目录为 Lib\site-packages
,位于 Python 的安装目录下。
此外,Distutils install
命令的 install_egg_info
子命令会在 purelib
目录中为项目添加一个 .egg-info
文件。
例如,对于 docutils
发行版,它包含一个包、一个额外的模块和可执行脚本,三个元素安装在 site-packages
中
docutils
:docutils
包。roman.py
:docutils
使用的额外模块。docutils-0.5-py2.6.egg-info
:包含发行版元数据的文件,如PEP 314中所述。此文件对应于名为PKG-INFO
的文件,由sdist
命令构建。
一些可执行脚本,例如 rst2html.py
,也会添加到 Python 安装的 bin
目录中。
另一个名为 setuptools
[3] 的项目还有另外两种格式来安装发行版,称为 EggFormats
[6]
- 一个自包含的
.egg
目录,其中包含所有发行版文件和发行版元数据,这些元数据存储在一个名为PKG-INFO
的文件中,该文件位于名为EGG-INFO
的子目录中。setuptools
在该目录中创建了其他文件,这些文件可以被视为补充元数据。 - 一个
.egg-info
目录,安装在site-packages
中,其中包含与.egg
格式中的EGG-INFO
相同的文件。
当您安装使用 setuptools.setup
函数(而不是 distutils.core.setup
函数)在其 setup.py 文件中设置的发行版时,会自动使用第一种格式。
setuptools
还会在 easy-install.pth
文件中添加对发行版的引用。
最后,setuptools
项目提供了一个名为 easy_install
[4] 的可执行脚本,该脚本安装所有发行版,包括基于 distutils 的发行版,这些发行版位于自包含的 .egg
目录中。
如果您希望为您的发行版创建独立的 .egg-info
目录,例如第二种 setuptools
格式,您必须在使用基于 setuptools 的发行版或 easy_install
脚本时强制执行此操作。您可以通过使用 --single-version-externally-managed
选项 **或** --root
选项来强制执行此操作。这将使 setuptools
项目像 distutils 一样安装项目。
此选项由以下项目使用
卸载信息
Distutils 没有提供 uninstall
命令。如果您要卸载发行版,您必须成为超级用户,并删除安装的各种元素,然后检查 .pth
文件,如有必要则清除它们。
该过程因您用来安装发行版的工具以及发行版的 setup.py
使用的是 Distutils 还是 Setuptools 而有所不同。
在某些情况下,您可能无法确定您是否已删除所有内容,或者您是否在删除多个发行版共享的文件时破坏了其他发行版。
但是有一个共同的行为:当您安装发行版时,文件会被复制到您的系统中。并且可以跟踪这些文件,以便以后删除。
此外,Pip 项目最近获得了 uninstall
功能。它使用 install
命令的 record
选项记录所有已安装的文件。
本 PEP 的提案
为了解决这些问题,本 PEP 提出了以下几点更改
- 一个新的
.dist-info
结构,使用目录,灵感来自setuptools
的EggFormats
标准的一种格式。 - 在
pkgutil
中添加新的 API,以便能够查询已安装发行版的信息。 - 在 Distutils 中添加卸载函数和卸载脚本。
每个已安装发行版一个 .dist-info 目录
本 PEP 提出了一个安装格式,该格式的灵感来自于 EggFormats
标准中的一个选项,该选项使用位于 site-packages 目录中的一个单独目录。
此单独目录的命名方式如下
name + '-' + version + '.dist-info'
此 .dist-info
目录可以包含以下文件
METADATA
:包含元数据,如PEP 345、PEP 314 和PEP 241 中所述。RECORD
:记录已安装文件的列表INSTALLER
:记录用于安装项目的工具的名称REQUESTED
:该文件的出现表示项目安装是显式请求的(即不是作为依赖项安装的)。
METADATA、RECORD 和 INSTALLER 文件是必需的,而 REQUESTED 可以缺失。
此提案不会影响 Python 本身,因为元数据文件除了 Distutils 之外,尚未在标准库中的任何地方使用。
它将影响 setuptools
和 pip
项目,但是,考虑到它们已经使用包含 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
。 - 系统上的任何其他路径。
每个记录由三个元素组成:
- 文件的**路径**
- 相对于**基本位置**的“/”分隔路径,如果文件位于**基本位置**下。
- 相对于**基本位置**的“/”分隔路径,如果文件位于**安装前缀**下,并且**基本位置**是**安装前缀**的子路径。
- 使用本地平台分隔符的绝对路径。
- 文件内容的哈希值。请注意,生成的
pyc
和pyo
文件没有哈希值,因为它们是根据py
文件自动生成的。因此,检查相应py
文件的哈希值足以确定文件及其关联的pyc
或pyo
文件是否已更改。哈希值要么为空字符串,要么是在
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
文件不能包含自身哈希值,这里只是提到了它。
在 /etc/myapp
中安装 config.ini
文件的项目将按以下方式添加:
/etc/myapp/config.ini,md5=gLfd6IANquzGLhOkW4Mfgg,9544
对于 Windows 平台,驱动器号将添加到绝对路径中,因此复制到 c:MyApp 中的文件将是:
c:\etc\myapp\config.ini,md5=gLfd6IANquzGLhOkW4Mfgg,9544
INSTALLER
该 install
命令有一个名为 installer
的新选项。此选项是用于调用安装的工具的名称。这是一个规范化的 lowercase 字符串,与 [a-z0-9_\-\.]
匹配。
$ python setup.py install –installer=pkg-system
如果没有提供,则默认值为 distutils
。
当安装发行版时,INSTALLER 文件将使用此值在 .dist-info
目录中生成,以跟踪**谁**安装了发行版。该文件是一个单行文本文件。
REQUESTED
某些安装工具会自动检测未满足的依赖项并安装它们。在这些情况下,跟踪哪些发行版纯粹是作为依赖项安装的很有用,这样,如果其依赖发行版稍后被卸载,用户就会收到有关孤立依赖项的警报。
如果发行版是通过直接用户请求(通常情况)安装的,则 REQUESTED 文件将被添加到已安装发行版的 .dist-info 目录中。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
结尾的目录。返回一个Distribution
,对应于包含与name
匹配的 METADATA 的.dist-info
目录的name
元数据。此函数仅返回第一个找到的结果,因为预计不会出现多个值。如果未找到目录,则返回 None。
get_file_users(path)
->Distribution
实例的迭代器。迭代所有发行版,以找出哪些发行版使用
path
。path
可以是本地绝对路径或相对的“/”分隔路径。本地绝对路径是绝对路径,其中“/”的出现已由
os.sep
给出的系统分隔符替换。
Distribution 类
创建一个名为 Distribution
的新类,其中包含传递给构造函数的 .dist-info
目录的路径。它在实例化时读取包含在 METADATA
中的元数据。
Distribution(path)
-> 实例
为给定的path
创建一个Distribution
实例。
Distribution
提供以下属性:
name
:发行版的名称。metadata
:一个DistributionMetadata
实例,使用发行版的 METADATA 文件加载。requested
:一个布尔值,指示 REQUESTED 元数据文件是否存在(换句话说,发行版是否通过用户请求安装)。
以及以下方法:
get_installed_files(local=False)
-> (path, hash, size) 的迭代器迭代
RECORD
条目,并为每行返回一个元组(path, hash, size)
。如果local
为True
,则路径将转换为本地绝对路径。否则,将返回RECORD
中的原始值。本地绝对路径是绝对路径,其中“/”的出现已由
os.sep
给出的系统分隔符替换。uses(path)
-> 布尔值如果
RECORD
中列出了path
,则返回True
。path
可以是本地绝对路径或相对的“/”分隔路径。get_distinfo_file(path, binary=False)
-> 文件对象返回位于.dist-info
目录下的文件。返回一个
file
实例,指向path
指示的文件。path
必须是相对于.dist-info
目录的“/”分隔路径或绝对路径。如果
path
是一个绝对路径,并且没有以.dist-info
目录路径开头,则会引发DistutilsError
。如果
binary
为True
,则以只读二进制模式 (rb
) 打开文件,否则以只读模式 (r
) 打开文件。get_distinfo_files(local=False)
-> 路径的迭代器迭代
RECORD
条目,如果路径指向位于.dist-info
目录或其子目录中的文件,则为每行返回路径。如果
local
为True
,则每个路径将转换为本地绝对路径。否则,将返回RECORD
中的原始值。
请注意,API 组织为五个类,它们与目录和 Zip 文件一起工作(因此它与包含在 Zip 文件中的文件一起工作,有关更多详细信息,请参阅 PEP 273)。这些类在原型实现的文档中进行了描述,供感兴趣的读者参考 [9].
示例
让我们在 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 的第三方程序在安装时对系统执行额外步骤时,它必须在卸载时撤消这些步骤。这很有用。
添加卸载脚本
一个 uninstall
脚本已添加到 Distutils2 中,使用方法如下
$ python -m distutils2.uninstall projectname
请注意,该脚本不会控制删除发行版是否会破坏另一个发行版。尽管它确保它删除的所有文件都没有被其他发行版使用,但它通过使用 uninstall 函数来实现这一点。
另请注意,此卸载脚本不会关注 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
最后修改时间:2023-09-09 17:39:29 GMT