"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 代码中导入的最高层级
  *导入包* 名称。 一个 *分发包* 可以包含多个 *导入包* (和单独模块)，而
  一个最高层级 *导入包* 如果是命名空间包则可以映射到多个 *分发包*。 你
  可以使用 package_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']

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


函数式 API
==========

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


入口点
------

"entry_points()" 函数返回入口点的字典。入口点表现为 "EntryPoint" 的实
例；每个 "EntryPoint" 对象都有 ".name" ，".group" 与 ".value" 属性，用
于解析值的 ".load()" 方法， 来自 ".value" 属性的对应部分的 ".module",
".attr" 与 ".extras" 属性。

查询所有的入口点：

   >>> eps = entry_points()  

"entry_points()" 函数返回一个 "EntryPoints" 对象，即由带有 "names" 和
"groups" 属性的全部 "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" 传递关键字参数 "group" 以实现相同的效果:

   >>> 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" 是由包作者定义的任意值并且通常来说客户端会想要解析特
定 group 的所有入口点。 请参阅 the setuptools docs 了解有关入口点，其
定义和用法的更多信息。

*兼容性说明*

The "selectable" entry points were introduced in "importlib_metadata"
3.6 and Python 3.10. Prior to those changes, "entry_points" accepted
no parameters and always returned a dictionary of entry points, keyed
by group. For compatibility, if no parameters are passed to
entry_points, a "SelectableGroups" object is returned, implementing
that dict interface. In the future, calling "entry_points" with no
parameters will return an "EntryPoints" object. Users should rely on
the selection interface to retrieve entry points by group.


分发的元数据
------------

每个 分发包 都包括一些元数据，你可以使用 "metadata()" 函数来获取:

   >>> wheel_metadata = metadata('wheel')  

返回的数据结构 "PackageMetadata" 的键代表元数据的关键字，而值从分发的
元数据中不被解析地返回：

   >>> 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.*'

备注:

  "metadata()" 所返回的对象的实际类型是一个实现细节并且应当只能通过
  PackageMetadata 协议 所描述的接口来访问。

在 3.10 版本发生变更: 当有效载荷中包含时，"Description" 以去除续行符的
形式被包含于元数据中。添加了 "json" 属性。


分发包的版本
------------

"version()" 函数可以最快捷地以字符串形式获取一个 分发包 的版本号:

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


分发包的文件
------------

你还可以获取包含在分发包内的全部文件的集合。 "files()" 函数接受一个 分
发包 名称并返回此分发包所安装的全部文件。 每个返回的文件对象都是一个
"PackagePath"，即带有由元数据指明的额外 "dist", "size" 和 "hash" 特征
属性的派生自 "pathlib.PurePath" 的对象。 例如:

   >>> 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 中，或者用其他方法来应对目标分发元数据存在性未知
的情况。


分发包的依赖
------------

要获取一个 分发包 的完整需求集合，请使用 "requires()" 函数:

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


将导入映射到分发包
------------------

解析每个提供可导入的最高层级 Python 模块或 导入包 对应的 分发包 名称（
对于命名空间包可能有多个名称）的快捷方法:

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

在 3.10 版本加入.


分发包对象
==========

以上 API 是最常见且便捷的用法，但你也可以通过 "Distribution" 类来获得
所有信息。 "Distribution" 是一个代表 Python 分发包 元数据的抽象对象。
你可以这样获取 "Distribution" 实例:

   >>> from importlib.metadata import distribution  
   >>> dist = distribution('wheel')  

因此，可以通过 "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'

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


分发包的发现
============

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

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

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


扩展搜索算法
============

Because Distribution Package metadata is not available through
"sys.path" searches, or package loaders directly, the metadata for a
distribution is found through import system finders.  To find a
distribution package's metadata, "importlib.metadata" queries the list
of *meta path finders* on "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" 实例。
