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-info
或 egg-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_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']
你還可以獲取分發的版本號,列出其組成檔案,並獲取分發的分發要求列表。
- 異常 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
物件的集合,為了方便起見,它具有 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
屬性,該屬性以 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
派生物件,具有額外的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
物件,即使這些值會被匯入忽略。
擴充套件搜尋演算法¶
由於 發行包 元資料無法透過 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.path
。path
屬性預設為 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
還可以提供其他元資料檔案,如 RECORD
(Distribution.files
所需)或覆蓋 Distribution.files
的實現。請參閱原始碼以獲取更多靈感。