importlib.metadata -- 访问软件包元数据

Added in version 3.8.

在 3.10 版本发生变更: importlib.metadata 不再是暂定的。

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

importlib.metadata 是一个提供对已安装的 分发包 的元数据的访问的库,如其入口点或其顶层名称 (导入包,模块等,如果有的话)。 这个库部分构建于 Python 的导入系统之上,其目标是取代 pkg_resourcesentry point APImetadata API 中的类似功能。 配合 importlib.resources,这个包让使用较老旧且低效率的 pkg_resources 包不再必要。

importlib.metadatapip 等工具安装到 Python 的 site-packages 目录的第三方 分发包 进行操作。 具体来说,适用的分发包应带有可发现的 dist-infoegg-info 目录,以及 核心元数据规范说明 定义的元数据。

重要

它们 不一定 等同或 1:1 对应于可在 Python 代码中导入的顶层 导入包 名称。一个 分发包 可以包含多个 导入包 (和单个模块),如果是命名空间包,一个顶层 导入包 可以映射到多个 分发包。您可以使用 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_scripts, distutils.commands 等等。 每个 group 都包含一个 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']

你也可以获得 分发包的版本号,列出它的 构成文件,并且得到分发包的 分发包的依赖 列表。

exception importlib.metadata.PackageNotFoundError

当查询未在当前 Python 环境中安装的分发包时由此模块的某些函数所引发的 ModuleNotFoundError 子类。

函数式 API

这个包的公开 API 提供了以下功能。

入口点

importlib.metadata.entry_points(**select_params)

返回一个描述当前环境的入口点的 EntryPoints 实例。 所给出的任何关键字形参都将被传给 select() 方法以与单独的入口点定义的属性进行比较。

Note: to query for entry points based on EntryPoint.dist attribute, use Distribution.entry_points() instead (as different Distribution instances do not currently compare equal, even if they have the same attributes)

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 对象,即由带 namesgroups 属性的所有 EntryPoint 对象组成的多项集以方便使用:

>>> 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() 会传递关键字参数来选择:

>>> 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 是由软件包作者定义的任意值并且通常来说客户端会想要解析特定 group 的所有入口点。 请参阅 setuptools 文档 了解有关入口点及其定义和用法的详情。

在 3.12 版本发生变更: "selectable" 入口点是在 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 也还提供了一个按照 PEP 566 将所有元数据以 JSON 兼容形式返回的 json:

>>> 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)

Return the full set of files contained within the named distribution package as PackagePath instances.

如果指定的分发包未在当前 Python 环境中安装则会引发 PackageNotFoundError

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

class importlib.metadata.PackagePath

A pathlib.PurePath derived object with additional dist, size, and hash properties corresponding to the distribution package's installation metadata for that file, also:

locate()

If possible, return the concrete SimplePath allowing to access data, or raise a NotImplementedError otherwise.

class importlib.metadata.SimplePath

A protocol representing a minimal subset of pathlib.Path that allows to check if it exists(), to traverse using joinpath() and parent, and to retrieve data using read_text() and read_bytes().

The files() function takes a Distribution Package name and returns all of the files installed by this distribution. For example:

>>> 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'], ...}

某些可编辑的安装 没有提供最高层级名称,因而此函数不适用于这样的安装。

Added in version 3.10.

分发包对象

While the module level API described above is the most common and convenient usage, all that information is accessible from the Distribution class. Distribution is an abstract object that represents the metadata for a Python Distribution Package. Get the concrete Distribution subclass instance for an installed distribution package by calling the distribution() function:

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

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

如果指定的分发包未在当前 Python 环境中安装则会引发 PackageNotFoundError

Thus, an alternative way to get e.g. the version number is through the Distribution.version attribute:

>>> dist.version
'0.32.3'

The same applies for entry_points() and files().

class importlib.metadata.Distribution

一个已安装分发包的详情。

注意:目前不同的 Distribution 实例在比较时不会相等,即使它们是关联到相同的已安装分发包因而具有相同的属性。

static at(path)
classmethod from_name(name)

Return a Distribution instance at the given path or with the given name.

classmethod discover(*, context=None, **kwargs)

Returns an iterable of Distribution instances for all packages (see distribution-discovery).

The optional argument context is a DistributionFinder.Context instance, used to modify the search for distributions. Alternatively, kwargs may contain keyword arguments for constructing a new DistributionFinder.Context.

metadata: PackageMetadata

There are all kinds of additional metadata available on Distribution instances as a PackageMetadata instance:

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

此处并未描述可用元数据的完整集合。 更多详情参见 PyPA 核心元数据规格说明

name: str
requires: list[str]
version: str

A few metadata fields are also available as shortcut properties.

Added in version 3.10: The name shortcut was added.

origin

For editable packages, an origin property may present PEP 610 metadata (for non-editable packages, origin is None):

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

The origin object follows the Direct URL Data Structure.

Added in version 3.13.

entry_points: EntryPoints

The entry points provided by this distribution package.

files: list[PackagePath] | None

All files contained in this distribution package. Like files(), this returns None if there are no records.

The following two abstract methods need to be implemented when implementing-custom-providers:

locate_file(path)

Like PackagePath.locate(), return a SimplePath for the given path. Takes a os.PathLike or a str.

read_text(filename)

A shortcut for distribution.locate_file(filename).read_text().

分发包的发现

在默认情况下,这个包针对文件系统和 zip 文件 分发包 的元数据发现提供了内置支持。 这个元数据查找器的搜索目标默认为 sys.path,但它对来自其他导入机制行为方式的解读会略有变化。 特别地:

  • importlib.metadata 不会识别 sys.path 上的 bytes 对象。

  • importlib.metadata 将顺带识别 sys.path 上的 pathlib.Path 对象,即使这些值会被导入操作所忽略。

class importlib.metadata.DistributionFinder

A MetaPathFinder subclass capable of discovering installed distributions.

Custom providers should implement this interface in order to supply metadata.

class Context(**kwargs)

A Context gives a custom provider a means to solicit additional details from the callers of distribution discovery functions like distributions() or Distribution.discover() beyond .name and .path when searching for distributions.

For example, a provider could expose suites of packages in either a "public" or "private" realm. A caller of distribution discovery functions may wish to query only for distributions in a particular realm and could call distributions(realm="private") to signal to the custom provider to only include distributions from that realm.

Each DistributionFinder must expect any parameters and should attempt to honor the canonical parameters defined below when appropriate.

See the section on 实现自定义 Provider for more details.

name

Specific name for which a distribution finder should match.

A .name of None matches all distributions.

path

A property providing the sequence of directory paths that a distribution finder should search.

Typically refers to Python installed package paths such as "site-packages" directories and defaults to sys.path.

importlib.metadata.distributions(**kwargs)

Returns an iterable of Distribution instances for all packages.

The kwargs argument may contain either a keyword argument context, a DistributionFinder.Context instance, or pass keyword arguments for constructing a new DistributionFinder.Context. The DistributionFinder.Context is used to modify the search for distributions.

实现自定义 Provider

importlib.metadata 会处理两种 API 表层,一个用于 消费方 而另一个用于 供给方。 大部分用户属于消费方,他们将消费由软件包提供的元数据。 不过也存在其他的用例,其中的用户会想要通过某些其他机制来暴露元数据,比如配合自定义的导入器。 这样的用例就需要 自定义供给方

由于 分发包 元数据不能通过 sys.path 搜索或是直接通过包加载器来获得,一个分发包的元数据是通过导入系统的 查找器 来找到的。 要找到分发包的元数据,importlib.metadata 会在 sys.meta_path 上查询 元路径查找器 的列表。

该实现具有集成到 PathFinder 中的钩子,为在文件系统中找到的分发包提供元数据。

The abstract class importlib.abc.MetaPathFinder defines the interface expected of finders by Python's import system. importlib.metadata extends this protocol by looking for an optional find_distributions callable on the finders from sys.meta_path and presents this extended interface as the DistributionFinder abstract base class, which defines this abstract method:

@abc.abstractmethod
def find_distributions(context=DistributionFinder.Context()) -> Iterable[Distribution]:
    """返回一个由所有能够为指定 ``context`` 加载包的元数据的
    Distribution 实例组成的可迭代对象。
    """

The DistributionFinder.Context object provides path and name properties indicating the path to search and name to match and may supply other relevant context sought by the consumer.

In practice, to support finding distribution package metadata in locations other than the file system, subclass Distribution and implement the abstract methods. Then from a custom finder, return instances of this derived Distribution in the find_distributions() method.

示例

设想一个从数据库中加载 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(...)))

That importer now presumably provides importable modules from a database, but it provides no metadata or entry points. For this custom importer to provide metadata, it would also need to implement 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.0find_distributions 就会为 Context(name='requests')Context(name=None) 产生 DatabaseDistribution

为简单起见,本例忽略了 context.pathpath 属性默认为 sys.path,是搜索中考虑的导入路径集。一个 DatabaseImporter 可以在不考虑搜索路径的情况下运作。假设导入器不进行分区,那么 "path" 就无关紧要了。为了说明 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):
        """
        读取当前分发包的某个文件,如 "METADATA"。
        """
        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 属性。

The DatabaseDistribution may also provide other metadata files, like RECORD (required for Distribution.files) or override the implementation of Distribution.files. See the source for more inspiration.