importlib.metadata – 访问包元数据

在 3.8 版本中添加。

在 3.10 版本中更改: importlib.metadata 不再是临时的。

源代码: Lib/importlib/metadata/__init__.py

importlib.metadata 是一个库,它提供对已安装的分发包的元数据访问,例如它的入口点或其顶级名称(导入包、模块,如果有的话)。这个库部分基于 Python 的导入系统构建,旨在替代 pkg_resources入口点 API元数据 API 中的类似功能。与 importlib.resources 一起,此包可以消除使用较旧且效率较低的 pkg_resources 包的需要。

importlib.metadata 操作通过诸如 pip 之类的工具安装到 Python 的 site-packages 目录中的第三方分发包。具体来说,它适用于具有可发现的 dist-infoegg-info 目录以及由 核心元数据规范定义的元数据的分发。

重要

这些不一定与可以在 Python 代码中导入的顶级导入包名称等效或 1:1 对应。一个分发包可以包含多个导入包(和单个模块),如果一个顶级导入包是一个命名空间包,则它可能映射到多个分发包。你可以使用 packages_distributions() 来获取它们之间的映射。

默认情况下,分发元数据可以存在于文件系统中或 sys.path 上的 zip 存档中。通过扩展机制,元数据几乎可以存在于任何地方。

另请参阅

https://importlib-metadata.readthedocs.io/

importlib_metadata 的文档,它提供了 importlib.metadata 的向后移植。这包括该模块的类和函数的API 参考,以及 pkg_resources 的现有用户的迁移指南

概述

假设你想获取你使用 pip 安装的分发包的版本字符串。我们首先创建一个虚拟环境并在其中安装一些东西

$ python -m venv example
$ source example/bin/activate
(example) $ python -m pip install wheel

你可以通过运行以下命令来获取 wheel 的版本字符串

(example) $ python
>>> from importlib.metadata import version  
>>> version('wheel')  
'0.32.3'

你还可以获取可通过 EntryPoint 的属性(通常是“group”或“name”)选择的入口点集合,例如 console_scriptsdistutils.commands 等。每个组都包含一个 EntryPoint 对象集合。

你可以获取分发的元数据

>>> list(metadata('wheel'))  
['Metadata-Version', 'Name', 'Version', 'Summary', 'Home-page', 'Author', 'Author-email', 'Maintainer', 'Maintainer-email', 'License', 'Project-URL', 'Project-URL', 'Project-URL', 'Keywords', 'Platform', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Requires-Python', 'Provides-Extra', 'Requires-Dist', 'Requires-Dist']

你还可以获取分发的版本号,列出其组成文件,并获取分发的分发要求列表。

异常 importlib.metadata.PackageNotFoundError

当查询未安装在当前 Python 环境中的分发包时,此模块中的几个函数引发的 ModuleNotFoundError 的子类。

函数式 API

此包通过其公共 API 提供以下功能。

入口点

importlib.metadata.entry_points(**select_params)

返回一个 EntryPoints 实例,该实例描述当前环境的入口点。任何给定的关键字参数都会传递给 select() 方法,以便与各个入口点定义的属性进行比较。

注意:目前无法根据其 EntryPoint.dist 属性查询入口点(因为不同的 Distribution 实例目前不相等比较,即使它们具有相同的属性)

class importlib.metadata.EntryPoints

已安装入口点集合的详细信息。

还提供一个 .groups 属性,报告所有已识别的入口点组,以及一个 .names 属性,报告所有已识别的入口点名称。

class importlib.metadata.EntryPoint

已安装入口点的详细信息。

每个 EntryPoint 实例都有 .name.group.value 属性以及一个 .load() 方法来解析值。还有 .module.attr.extras 属性用于获取 .value 属性的组件,以及 .dist 用于获取有关提供入口点的分发包的信息。

查询所有入口点

>>> eps = entry_points()  

entry_points() 函数返回一个 EntryPoints 对象,它是所有 EntryPoint 对象的集合,为了方便起见,它具有 namesgroups 属性

>>> sorted(eps.groups)  
['console_scripts', 'distutils.commands', 'distutils.setup_keywords', 'egg_info.writers', 'setuptools.installation']

EntryPoints 有一个 select() 方法来选择与特定属性匹配的入口点。选择 console_scripts 组中的入口点

>>> scripts = eps.select(group='console_scripts')  

等效地,因为 entry_points() 将关键字参数传递给 select

>>> scripts = entry_points(group='console_scripts')  

挑出一个名为“wheel”的特定脚本(在 wheel 项目中找到)

>>> 'wheel' in scripts.names  
True
>>> wheel = scripts['wheel']  

等效地,在选择期间查询该入口点

>>> (wheel,) = entry_points(group='console_scripts', name='wheel')  
>>> (wheel,) = entry_points().select(group='console_scripts', name='wheel')  

检查解析的入口点

>>> wheel  
EntryPoint(name='wheel', value='wheel.cli:main', group='console_scripts')
>>> wheel.module  
'wheel.cli'
>>> wheel.attr  
'main'
>>> wheel.extras  
[]
>>> main = wheel.load()  
>>> main  
<function main at 0x103528488>

groupname 是由软件包作者定义的任意值,通常客户端希望解析特定组的所有入口点。请阅读 setuptools 文档,了解关于入口点、其定义和用法的更多信息。

3.12 版本更改: “可选择的”入口点是在 importlib_metadata 3.6 和 Python 3.10 中引入的。在这些更改之前,entry_points 不接受任何参数,并且总是返回一个以组为键的入口点字典。在 importlib_metadata 5.0 和 Python 3.12 中,entry_points 总是返回一个 EntryPoints 对象。请参阅 backports.entry_points_selectable 以了解兼容性选项。

3.13 版本更改: EntryPoint 对象不再呈现类似元组的接口(__getitem__())。

分发元数据

importlib.metadata.metadata(distribution_name)

返回与命名分发包对应的分发元数据,作为 PackageMetadata 实例。

如果命名的分发包未安装在当前 Python 环境中,则引发 PackageNotFoundError 异常。

class importlib.metadata.PackageMetadata

一个 PackageMetadata 协议的具体实现。

除了提供定义的协议方法和属性外,对实例进行下标操作等同于调用 get() 方法。

每个 分发包 都包含一些元数据,您可以使用 metadata() 函数提取这些元数据。

>>> wheel_metadata = metadata('wheel')  

返回的数据结构的键名是元数据关键字,值是从分发元数据返回的未解析的值。

>>> wheel_metadata['Requires-Python']  
'>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*'

PackageMetadata 还提供一个 json 属性,该属性以 JSON 兼容的形式返回所有元数据,符合 PEP 566

>>> wheel_metadata.json['requires_python']
'>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*'

此处未描述完整的可用元数据。有关更多详细信息,请参阅 PyPA 核心元数据规范

3.10 版本更改: 现在通过有效负载呈现元数据时,Description 也包含在元数据中。行继续字符已被删除。

添加了 json 属性。

分发版本

importlib.metadata.version(distribution_name)

返回指定分发包的已安装分发包版本。

如果命名的分发包未安装在当前 Python 环境中,则引发 PackageNotFoundError 异常。

version() 函数是获取 分发包 版本号的最快方法,以字符串形式返回。

>>> version('wheel')  
'0.32.3'

分发文件

importlib.metadata.files(distribution_name)

返回指定分发包中包含的所有文件。

如果命名的分发包未安装在当前 Python 环境中,则引发 PackageNotFoundError 异常。

如果找到了分发包,但安装数据库缺少报告与分发包关联的文件记录,则返回 None

class importlib.metadata.PackagePath

一个 pathlib.PurePath 派生对象,具有额外的 distsizehash 属性,这些属性对应于该文件的分发包安装元数据。

files() 函数接受一个 分发包 名称,并返回此分发包安装的所有文件。每个文件都报告为 PackagePath 实例。例如

>>> util = [p for p in files('wheel') if 'util.py' in str(p)][0]  
>>> util  
PackagePath('wheel/util.py')
>>> util.size  
859
>>> util.dist  
<importlib.metadata._hooks.PathDistribution object at 0x101e0cef0>
>>> util.hash  
<FileHash mode: sha256 value: bYkw5oMccfazVCoYQwKkkemoVyMAFoR34mmKBx8R1NI>

获取文件后,您还可以读取其内容

>>> print(util.read_text())  
import base64
import sys
...
def as_bytes(s):
    if isinstance(s, text_type):
        return s.encode('utf-8')
    return s

您还可以使用 locate() 方法获取文件的绝对路径

>>> util.locate()  
PosixPath('/home/gustav/example/lib/site-packages/wheel/util.py')

如果缺少列出文件的元数据文件(RECORDSOURCES.txt),files() 将返回 None。如果已知目标分发包不存在元数据,调用者可能希望将 files() 的调用包装在 always_iterable 中,或者以其他方式防止这种情况。

分发要求

importlib.metadata.requires(distribution_name)

返回指定分发包的声明依赖项说明符。

如果命名的分发包未安装在当前 Python 环境中,则引发 PackageNotFoundError 异常。

要获取 分发包 的完整要求集,请使用 requires() 函数。

>>> requires('wheel')  
["pytest (>=3.0.0) ; extra == 'test'", "pytest-cov ; extra == 'test'"]

将导入映射到分发包

importlib.metadata.packages_distributions()

返回通过 sys.meta_path 找到的顶级模块和导入包名称到提供相应文件的分发包名称(如果有)的映射。

为了允许命名空间包(其成员可能由多个分发包提供),每个顶级导入名称都映射到分发名称列表,而不是直接映射到单个名称。

一种方便的方法,用于解析提供每个可导入的顶级 Python 模块或 导入包分发包 名称(或多个名称,在命名空间包的情况下)。

>>> packages_distributions()
{'importlib_metadata': ['importlib-metadata'], 'yaml': ['PyYAML'], 'jaraco': ['jaraco.classes', 'jaraco.functools'], ...}

一些可编辑的安装,不提供顶级名称,因此此函数对于此类安装不可靠。

在 3.10 版本中添加。

分发

importlib.metadata.distribution(distribution_name)

返回描述指定分发包的 Distribution 实例。

如果命名的分发包未安装在当前 Python 环境中,则引发 PackageNotFoundError 异常。

class importlib.metadata.Distribution

已安装分发包的详细信息。

注意:即使不同的 Distribution 实例与同一个已安装的发行版相关,并因此具有相同的属性,它们目前也不相等。

虽然上面描述的模块级 API 是最常用和最方便的用法,但你可以从 Distribution 类获取所有这些信息。Distribution 是一个抽象对象,表示 Python 发行包的元数据。你可以通过调用 distribution() 函数获取已安装的发行包的具体的 Distribution 子类实例。

>>> from importlib.metadata import distribution  
>>> dist = distribution('wheel')  
>>> type(dist)  
<class 'importlib.metadata.PathDistribution'>

因此,获取版本号的另一种方法是通过 Distribution 实例。

>>> dist.version  
'0.32.3'

Distribution 实例上还有各种其他可用的元数据。

>>> dist.metadata['Requires-Python']  
'>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*'
>>> dist.metadata['License']  
'MIT'

对于可编辑的包,origin 属性可能会显示 PEP 610 元数据。

>>> dist.origin.url
'file:///path/to/wheel-0.32.3.editable-py3-none-any.whl'

此处未描述完整的可用元数据。有关更多详细信息,请参阅 PyPA 核心元数据规范

3.13 版本新增: 添加了 .origin 属性。

发行版发现

默认情况下,此包提供内置支持,用于发现文件系统和 ZIP 文件发行包的元数据。此元数据查找器搜索默认使用 sys.path,但它对这些值的解释方式与其它导入机制略有不同。特别是

  • importlib.metadata 不会接受 sys.path 上的 bytes 对象。

  • importlib.metadata 会意外地接受 sys.path 上的 pathlib.Path 对象,即使这些值会被导入忽略。

扩展搜索算法

由于 发行包 元数据无法通过 sys.path 搜索或包加载器直接获得,因此通过导入系统查找器 来查找发行版的元数据。为了找到发行包的元数据,importlib.metadata 查询 sys.meta_path 上的元路径查找器列表。

默认情况下,importlib.metadata 会为在文件系统中找到的发行包安装一个查找器。此查找器实际上不会找到任何发行版,但它可以找到它们的元数据。

抽象类 importlib.abc.MetaPathFinder 定义了 Python 导入系统对查找器的预期接口。importlib.metadata 通过在 sys.meta_path 中的查找器上查找可选的 find_distributions 可调用对象来扩展此协议,并将此扩展接口呈现为 DistributionFinder 抽象基类,该基类定义了这个抽象方法。

@abc.abstractmethod
def find_distributions(context=DistributionFinder.Context()):
    """Return an iterable of all Distribution instances capable of
    loading the metadata for packages for the indicated ``context``.
    """

DistributionFinder.Context 对象提供 .path.name 属性,分别指示要搜索的路径和要匹配的名称,并且可以提供其他相关上下文。

实际意义是,要支持在文件系统以外的位置查找发行包元数据,需要子类化 Distribution 并实现抽象方法。然后,从自定义查找器中,在 find_distributions() 方法中返回此派生的 Distribution 的实例。

示例

例如,考虑一个从数据库加载 Python 模块的自定义查找器

class DatabaseImporter(importlib.abc.MetaPathFinder):
    def __init__(self, db):
        self.db = db

    def find_spec(self, fullname, target=None) -> ModuleSpec:
        return self.db.spec_from_name(fullname)

sys.meta_path.append(DatabaseImporter(connect_db(...)))

现在,该导入器大概会从数据库中提供可导入的模块,但它不提供元数据或入口点。要使此自定义导入器提供元数据,它还需要实现 DistributionFinder

from importlib.metadata import DistributionFinder

class DatabaseImporter(DistributionFinder):
    ...

    def find_distributions(self, context=DistributionFinder.Context()):
        query = dict(name=context.name) if context.name else {}
        for dist_record in self.db.query_distributions(query):
            yield DatabaseDistribution(dist_record)

这样,query_distributions 将返回数据库提供的每个与查询匹配的发行版的记录。例如,如果数据库中存在 requests-1.0,则 find_distributions 将为 Context(name='requests')Context(name=None) 生成一个 DatabaseDistribution

为了简单起见,此示例忽略了 context.pathpath 属性默认为 sys.path,并且是要在搜索中考虑的导入路径集。DatabaseImporter 可能会在不考虑搜索路径的情况下运行。假设导入器不进行任何分区,“路径”将是无关紧要的。为了说明 path 的用途,该示例需要说明一个更复杂的 DatabaseImporter,其行为会因 sys.path/PYTHONPATH 而异。在这种情况下,find_distributions 应该遵守 context.path,并且只生成与该路径相关的 Distribution

然后,DatabaseDistribution 看起来应该像这样

class DatabaseDistribution(importlib.metadata.Distribution):
    def __init__(self, record):
        self.record = record

    def read_text(self, filename):
        """
        Read a file like "METADATA" for the current distribution.
        """
        if filename == "METADATA":
            return f"""Name: {self.record.name}
Version: {self.record.version}
"""
        if filename == "entry_points.txt":
            return "\n".join(
              f"""[{ep.group}]\n{ep.name}={ep.value}"""
              for ep in self.record.entry_points)

    def locate_file(self, path):
        raise RuntimeError("This distribution has no file system")

这个基本实现应该为 DatabaseImporter 提供的包提供元数据和入口点,假设 record 提供了合适的 .name.version.entry_points 属性。

DatabaseDistribution 还可以提供其他元数据文件,如 RECORDDistribution.files 所需)或覆盖 Distribution.files 的实现。请参阅源代码以获取更多灵感。