importlib.metadata – Доступ к метаданным пакета

Added in version 3.8.

Изменено в версии 3.10: importlib.metadata больше не является предварительным.

Источник: Lib/importlib/metadata/__init__.py

importlib.metadata - это библиотека, предоставляющая доступ к метаданным установленного Distribution Package, таким как точки входа или имена верхнего уровня (Import Packages, модули, если таковые имеются). Построенная частично на системе импорта Python, эта библиотека призвана заменить аналогичную функциональность в entry point API и metadata API из pkg_resources. Вместе с importlib.resources этот пакет может избавить от необходимости использовать более старый и менее эффективный пакет pkg_resources.

importlib.metadata работает со сторонними дистрибутивными пакетами, установленными в каталог site-packages Python с помощью таких инструментов, как pip. В частности, он работает с дистрибутивами с обнаруживаемыми каталогами dist-info или egg-info и метаданными, определенными Core metadata specifications.

Важно

Они не обязательно эквивалентны или соответствуют 1:1 именам импортных пакетов верхнего уровня, которые могут быть импортированы в код Python. Один распределительный пакет может содержать несколько импортных пакетов (и отдельных модулей), а один импортный пакет верхнего уровня может быть сопоставлен с несколькими распределительными пакетами, если он является пакетом пространства имен. Вы можете использовать packages_distributions(), чтобы получить сопоставление между ними.

По умолчанию метаданные дистрибутива могут находиться в файловой системе или в zip-архивах на sys.path. С помощью механизма расширения метаданные могут находиться практически в любом месте.

См.также

https://importlib-metadata.readthedocs.io/

Документация для importlib_metadata, который является бэкпортом importlib.metadata. Она включает API reference для классов и функций этого модуля, а также migration guide для существующих пользователей pkg_resources.

Обзор

Допустим, вы хотите получить строку версии для Distribution Package, который вы установили с помощью 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'

Вы также можете получить коллекцию точек входа, выбираемых по свойствам точки входа (обычно „group“ или „name“), таким как console_scripts, distutils.commands и другие. Каждая группа содержит коллекцию объектов EntryPoint.

Вы можете получить metadata for a distribution:

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

Вы также можете получить distribution’s version number, перечислить его constituent files и получить список Требования к распределению дистрибутива.

Функциональный API

Этот пакет предоставляет следующие функциональные возможности через свой публичный API.

Точки входа

Функция entry_points() возвращает коллекцию точек входа. Точки входа представлены экземплярами EntryPoint; каждый EntryPoint имеет атрибуты .name, .group и .value и метод .load() для определения значения. Существуют также атрибуты .module, .attr и .extras для получения компонентов атрибута .value.

Запросите все точки входа:

>>> 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 являются произвольными, определяемыми автором пакета, и обычно клиент хочет разрешить все точки входа для определенной группы. Прочитайте the setuptools docs для получения дополнительной информации о точках входа, их определении и использовании.

Изменено в версии 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__()).

Метаданные о распространении

Каждый Distribution Package содержит некоторые метаданные, которые можно извлечь с помощью функции metadata():

>>> wheel_metadata = metadata('wheel')  

Ключи возвращаемой структуры данных, a PackageMetadata, называют ключевые слова метаданных, а значения возвращаются без разбора из метаданных дистрибутива:

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

Примечание

Фактический тип объекта, возвращаемого metadata(), является деталью реализации и должен быть доступен только через интерфейс, описанный PackageMetadata protocol.

Изменено в версии 3.10: Теперь символ Description включается в метаданные при представлении через полезную нагрузку. Символы продолжения строки были удалены.

Был добавлен атрибут json.

Версии распространения

Функция version() - это самый быстрый способ получить номер версии Distribution Package в виде строки:

>>> version('wheel')  
'0.32.3'

Файлы распространения

Вы также можете получить полный набор файлов, содержащихся в дистрибутиве. Функция files() принимает имя Distribution Package и возвращает все файлы, установленные этим дистрибутивом. Каждый возвращаемый файловый объект - это PackagePath, производный объект pathlib.PurePath с дополнительными dist, size и hash свойствами, указанными в метаданных. Например:

>>> 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 или иным образом защититься от этого условия, если неизвестно, что в целевом дистрибутиве присутствуют метаданные.

Требования к распределению

Чтобы получить полный набор требований для Distribution Package, используйте функцию requires():

>>> requires('wheel')  
["pytest (>=3.0.0) ; extra == 'test'", "pytest-cov ; extra == 'test'"]

Сопоставление импорта с дистрибутивными пакетами

Удобный метод для определения Distribution Package имени (или имен, в случае пакета пространства имен), которое предоставляет каждый импортируемый модуль Python верхнего уровня или Import Package:

>>> packages_distributions()
{'importlib_metadata': ['importlib-metadata'], 'yaml': ['PyYAML'], 'jaraco': ['jaraco.classes', 'jaraco.functools'], ...}

Некоторые редактируемые установки - do not supply top-level names, и поэтому эта функция не работает с такими установками.

Added in version 3.10.

Распределения

Хотя приведенный выше API является наиболее распространенным и удобным вариантом использования, вы можете получить всю эту информацию из класса Distribution. Класс Distribution - это абстрактный объект, который представляет метаданные для Distribution Package в Python. Вы можете получить экземпляр Distribution:

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

Полный набор доступных метаданных здесь не описан. Дополнительные сведения см. в разделе Core metadata specifications.

Added in version 3.13: Было добавлено свойство .origin.

Открытие дистрибуции

По умолчанию этот пакет обеспечивает встроенную поддержку поиска метаданных для файловой системы и zip-файлов Distribution Package. Этот поиск метаданных по умолчанию принимает значение sys.path, но немного отличается в интерпретации этих значений от того, как это делают другие механизмы импорта. В частности:

  • importlib.metadata не уважает объекты bytes на sys.path.

  • importlib.metadata будет случайно учитывать объекты pathlib.Path на sys.path, хотя такие значения будут игнорироваться при импорте.

Расширение алгоритма поиска

Поскольку метаданные Distribution Package недоступны ни через поиск sys.path, ни через загрузчики пакетов напрямую, метаданные дистрибутива находятся через систему импорта finders. Чтобы найти метаданные дистрибутива, importlib.metadata запрашивает список meta path finders на sys.meta_path.

По умолчанию importlib.metadata устанавливает программу поиска дистрибутивов, найденных в файловой системе. Этот искатель не находит дистрибутивы, но может найти их метаданные.

Абстрактный класс importlib.abc.MetaPathFinder определяет интерфейс, ожидаемый от finders системой импорта Python. importlib.metadata расширяет этот протокол, ища необязательный find_distributions вызываемый на finders из sys.meta_path, и представляет этот расширенный интерфейс в виде 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 и реализовать абстрактные методы. Затем из пользовательского поисковика верните экземпляры этого производного Distribution в методе find_distributions().

Пример

Рассмотрим, например, пользовательский поисковик, который загружает модули 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 выдаст DatabaseDistribution для Context(name='requests') или Context(name=None).

Для простоты в этом примере игнорируется context.path. Атрибут path по умолчанию равен sys.path и представляет собой набор путей импорта, которые будут учитываться при поиске. Атрибут DatabaseImporter потенциально может работать без учета пути поиска. Если предположить, что импортер не делает разбиения, то «путь» не будет иметь значения. Чтобы проиллюстрировать назначение 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. Смотрите источник для большего вдохновения.