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

Added in version 3.8.

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

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

"importlib.metadata" is a library that provides access to the metadata
of an installed Distribution Package, such as its entry points or its
top-level names (Import Packages, modules, if any). Built in part on
Python's import system, this library intends to replace similar
functionality in the entry point API and metadata API of
"pkg_resources". Along with "importlib.resources", this package can
eliminate the need to use the older and less efficient "pkg_resources"
package.

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

重要:

  它们 *不一定* 等同或 1:1 对应于可在 Python 代码中导入的顶层 *导入包*
  名称。一个 *分发包* 可以包含多个 *导入包* (和单个模块)，如果是命名空
  间包，一个顶层 *导入包* 可以映射到多个 *分发包*。您可以使用
  packages_distributions() 来获取它们之间的映射。

By default, distribution metadata can live on the file system or in
zip archives on "sys.path". Through an extension mechanism, the
metadata can live almost anywhere.

参见:

  https://importlib-metadata.readthedocs.io/
     "importlib_metadata" 的文档，它向下移植了 "importlib.metadata"。
     它包含该模块的类和函数的 API 参考，以及针对 "pkg_resources" 现有
     用户的 迁移指南。


概述
====

Let's say you wanted to get the version string for a Distribution
Package you've installed using "pip". We start by creating a virtual
environment and installing something into it:

   $ 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'

You can also get a collection of entry points selectable by properties
of the EntryPoint (typically 'group' or 'name'), such as
"console_scripts", "distutils.commands" and others. Each group
contains a collection of EntryPoint objects.

你可以获得 分发的元数据：

   >>> 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 提供了以下功能。


入口点
------

The "entry_points()" function returns a collection of entry points.
Entry points are represented by "EntryPoint" instances; each
"EntryPoint" has a ".name", ".group", and ".value" attributes and a
".load()" method to resolve the value. There are also ".module",
".attr", and ".extras" attributes for getting the components of the
".value" attribute.

查询所有的入口点：

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

The "group" and "name" are arbitrary values defined by the package
author and usually a client will wish to resolve all entry points for
a particular group. Read the setuptools docs for more information on
entry points, their definition, and usage.

在 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__()"）。


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

每个 分发包 都包括一些元数据，你可以使用 "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'


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

You can also get the full set of files contained within a
distribution. The "files()" function takes a Distribution Package name
and returns all of the files installed by this distribution. Each file
object returned is a "PackagePath", a "pathlib.PurePath" derived
object with additional "dist", "size", and "hash" properties as
indicated by the metadata. 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')

当列出包含文件的元数据文件（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'], ...}

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

Added in version 3.10.


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

While the above API is the most common and convenient usage, you can
get all of that information from the "Distribution" class. A
"Distribution" is an abstract object that represents the metadata for
a Python Distribution Package. You can get the "Distribution"
instance:

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

对于可编辑包，"origin" 属性可能表示 **PEP 610** 元数据：

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

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

Added in version 3.13: 增加了 ".origin" 特征属性。


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

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

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

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


Implementing Custom Providers
=============================

"importlib.metadata" address two API surfaces, one for *consumers* and
another for *providers*. Most users are consumers, consuming metadata
provided by the packages. There are other use-cases, however, where
users wish to expose metadata through some other mechanism, such as
alongside a custom importer. Such a use case calls for a *custom
provider*.

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".

The implementation has hooks integrated into the "PathFinder", serving
metadata for distribution packages found on the file system.

抽象基类 "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``.
       """

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.


示例
----

Imagine a custom finder that loads Python modules from a database:

   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.path"。"path" 属性默认为 "sys.path"，
是搜索中考虑的导入路径集。一个 "DatabaseImporter" 可以在不考虑搜索路径
的情况下运作。假设导入器不进行分区，那么 "path" 就无关紧要了。为了说明
"path" 的作用，示例需要展示一个更复杂的 "DatabaseImporter"，它的行为随
"sys.path"/"PYTHONPATH" 而变化。在这种情况下，"find_distributions" 应
该尊重 "context.path"，并且只产生与该路径相关的 "Distribution"。

那么，"DatabaseDistribution" 看起来就像是这样：

   class DatabaseDistribution(importlib.metadata.Distributon):
       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" 还可能提供其他元数据文件，比如 "RECORD" (对
"Distribution.files" 来说需要) 或重写 "Distribution.files" 的实现。 请
参看源代码深入了解。
