importlib.metadata – 訪問包元資料

在 3.8 版本加入。

在 3.10 版本發生變更: importlib.metadata 不再是臨時性質的。

原始碼: Lib/importlib/metadata/__init__.py

importlib.metadata 是一個庫,它提供了對已安裝分發包的元資料的訪問,例如其入口點或其頂層名稱(匯入包、模組,如果有的話)。這個庫部分建立在 Python 的匯入系統之上,旨在取代 pkg_resourcesentry point APImetadata API 的類似功能。與 importlib.resources 一起,這個包可以讓你不再需要使用陳舊且效率較低的 pkg_resources 包。

importlib.metadata 操作的是透過 pip 等工具安裝到 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_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']

你還可以獲取分發包的版本號、列出其組成檔案,並獲取該分發包的依賴項列表。

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 物件的集合,為方便起見,它還帶有 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 屬性,它會根據 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 的物件,帶有額外的 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 物件,儘管這些值在匯入時會被忽略。

實現自定義提供程式

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.pathpath 屬性預設為 sys.path,並且是搜尋中要考慮的匯入路徑集合。一個 DatabaseImporter 可能無需關心搜尋路徑即可執行。假設匯入器不做任何分割槽,那麼“路徑”將是無關緊要的。為了說明 path 的目的,該示例需要展示一個更復雜的 DatabaseImporter,其行為會根據 sys.path/PYTHONPATH 而變化。在這種情況下,find_distributions 應該遵循 context.path,並且只產生與該路徑相關的 Distributions。

然後,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 還可以提供其他元資料檔案,例如 RECORDDistribution.files 所需),或者重寫 Distribution.files 的實現。請參閱原始碼以獲取更多靈感。