importlib.metadata
– 访问包元数据¶
在 3.8 版本加入。
在 3.10 版本发生变更: importlib.metadata
不再是临时性质的。
源代码: Lib/importlib/metadata/__init__.py
importlib.metadata
是一个库,它提供了对已安装分发包的元数据的访问,例如其入口点或其顶层名称(导入包、模块,如果有的话)。这个库部分建立在 Python 的导入系统之上,旨在取代 pkg_resources
中 entry point API 和 metadata API 的类似功能。与 importlib.resources
一起,这个包可以让你不再需要使用陈旧且效率较低的 pkg_resources
包。
importlib.metadata
操作的是通过 pip 等工具安装到 Python 的 site-packages
目录中的第三方分发包。具体来说,它适用于具有可发现的 dist-info
或 egg-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
等。每个组都包含一个 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¶
ModuleNotFoundError
的子类,当查询当前 Python 环境中未安装的分发包时,本模块中的几个函数会引发此异常。
函数式 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
对象的集合,为方便起见,它还带有 names
和 groups
属性
>>> 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>
group
和 name
是包作者定义的任意值,通常客户端会希望解析特定组的所有入口点。阅读 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
属性,它会根据 PEP 566 以 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)¶
返回指定分发包中包含的完整文件集。
如果指定的发行包未在当前 Python 环境中安装,则会引发
PackageNotFoundError
。如果找到了分发包,但报告与该分发包关联的文件的安装数据库记录缺失,则返回
None
。
- class importlib.metadata.PackagePath¶
一个派生自
pathlib.PurePath
的对象,带有额外的dist
、size
和hash
属性,这些属性对应于该分发包安装元数据中关于该文件的信息。
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')
在列出文件的元数据文件(RECORD
或 SOURCES.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
对象,尽管这些值在导入时会被忽略。
实现自定义提供程序¶
importlib.metadata
涉及两个 API 层面,一个用于消费者,另一个用于提供者。大多数用户是消费者,消费由包提供的元数据。但是,也存在其他用例,用户希望通过其他机制(例如,与自定义导入器一起)公开元数据。这样的用例需要一个自定义提供者。
由于分发包的元数据不能通过 sys.path
搜索或包加载器直接获得,因此分发包的元数据是通过导入系统的查找器找到的。为了找到一个分发包的元数据,importlib.metadata
会查询 sys.meta_path
上的元路径查找器列表。
该实现已将钩子集成到 PathFinder
中,为在文件系统上找到的分发包提供元数据。
抽象类 importlib.abc.MetaPathFinder
定义了 Python 导入系统期望查找器遵循的接口。importlib.metadata
扩展了此协议,它会在 sys.meta_path
的查找器上寻找一个可选的 find_distributions
可调用对象,并将这个扩展接口呈现为 DistributionFinder
抽象基类,该基类定义了以下抽象方法:
@abc.abstractmethod
def find_distributions(context=DistributionFinder.Context()) -> Iterable[Distribution]:
"""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
在数据库中,那么对于 Context(name='requests')
或 Context(name=None)
,find_distributions
将产生一个 DatabaseDistribution
。
为了简单起见,此示例忽略了 context.path
。path
属性默认为 sys.path
,并且是搜索中要考虑的导入路径集合。一个 DatabaseImporter
可能无需关心搜索路径即可运行。假设导入器不做任何分区,那么“路径”将是无关紧要的。为了说明 path
的目的,该示例需要展示一个更复杂的 DatabaseImporter
,其行为会根据 sys.path
/PYTHONPATH
而变化。在这种情况下,find_distributions
应该遵循 context.path
,并且只产生与该路径相关的 Distribution
s。
然后,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")
假设 record
提供了适当的 .name
、.version
和 .entry_points
属性,这个基本实现应该能为 DatabaseImporter
提供的包提供元数据和入口点。
DatabaseDistribution
还可以提供其他元数据文件,例如 RECORD
(Distribution.files
所需),或者重写 Distribution.files
的实现。请参阅源代码以获取更多灵感。