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 程式碼中匯入的頂層匯入包名稱。一個分發包可以包含多個匯入包(以及單個模組),而一個頂層匯入包如果是名稱空間包,則可能對映到多個分發包。你可以使用 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']
你還可以獲取分發包的版本號、列出其組成檔案,並獲取該分發包的依賴項列表。
- 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
物件的集合,為方便起見,它還帶有 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
屬性,它會根據 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
的物件,帶有額外的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
物件,儘管這些值在匯入時會被忽略。
實現自定義提供程式¶
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.path
。path
屬性預設為 sys.path
,並且是搜尋中要考慮的匯入路徑集合。一個 DatabaseImporter
可能無需關心搜尋路徑即可執行。假設匯入器不做任何分割槽,那麼“路徑”將是無關緊要的。為了說明 path
的目的,該示例需要展示一個更復雜的 DatabaseImporter
,其行為會根據 sys.path
/PYTHONPATH
而變化。在這種情況下,find_distributions
應該遵循 context.path
,並且只產生與該路徑相關的 Distribution
s。
然後,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
還可以提供其他元資料檔案,例如 RECORD
(Distribution.files
所需),或者重寫 Distribution.files
的實現。請參閱原始碼以獲取更多靈感。