importlib.metadata – 訪問包元資料

在 3.8 版本中新增。

在 3.10 版本中更改: importlib.metadata 不再是臨時的。

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

importlib.metadata 是一個庫,它提供對已安裝的分發包的元資料訪問,例如它的入口點或其頂級名稱(匯入包、模組,如果有的話)。這個庫部分基於 Python 的匯入系統構建,旨在替代 pkg_resources入口點 API元資料 API 中的類似功能。與 importlib.resources 一起,此包可以消除使用較舊且效率較低的 pkg_resources 包的需要。

importlib.metadata 操作透過諸如 pip 之類的工具安裝到 Python 的 site-packages 目錄中的第三方分發包。具體來說,它適用於具有可發現的 dist-infoegg-info 目錄以及由 核心元資料規範定義的元資料的分發。

重要

這些不一定與可以在 Python 程式碼中匯入的頂級匯入包名稱等效或 1:1 對應。一個分發包可以包含多個匯入包(和單個模組),如果一個頂級匯入包是一個名稱空間包,則它可能對映到多個分發包。你可以使用 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']

你還可以獲取分發的版本號,列出其組成檔案,並獲取分發的分發要求列表。

異常 importlib.metadata.PackageNotFoundError

當查詢未安裝在當前 Python 環境中的分發包時,此模組中的幾個函式引發的 ModuleNotFoundError 的子類。

函式式 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 屬性,該屬性以 JSON 相容的形式返回所有元資料,符合 PEP 566

>>> 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 物件,即使這些值會被匯入忽略。

擴充套件搜尋演算法

由於 發行包 元資料無法透過 sys.path 搜尋或包載入器直接獲得,因此透過匯入系統查詢器 來查詢發行版的元資料。為了找到發行包的元資料,importlib.metadata 查詢 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 的例項。

示例

例如,考慮一個從資料庫載入 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,則 find_distributions 將為 Context(name='requests')Context(name=None) 生成一個 DatabaseDistribution

為了簡單起見,此示例忽略了 context.pathpath 屬性預設為 sys.path,並且是要在搜尋中考慮的匯入路徑集。DatabaseImporter 可能會在不考慮搜尋路徑的情況下執行。假設匯入器不進行任何分割槽,“路徑”將是無關緊要的。為了說明 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):
        """
        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 還可以提供其他元資料檔案,如 RECORDDistribution.files 所需)或覆蓋 Distribution.files 的實現。請參閱原始碼以獲取更多靈感。