Плагины Python без списка импортов: обнаружение через entry points
Утилита поддерживает несколько экспортёров. Пока они живут в одном пакете, словаря из трёх функций достаточно. Но внешнее расширение не может дописать этот словарь без изменения основного проекта. Возникает желание сканировать каталоги и импортировать всё, что похоже на плагин. У такого решения быстро появляются неявные правила имён и порядка загрузки.
Entry points дают установленному дистрибутиву способ объявить расширение в метаданных. Основная программа выбирает собственную группу, например ledger.exporters, и читает записи через importlib.metadata.entry_points. Каждая запись содержит имя и ссылку на объект. Загрузка объекта выполняется явно через load, поэтому обнаружение можно отделить от запуска кода расширения.
Расширение в метаданных
В pyproject.toml расширения добавьте секцию [project.entry-points."ledger.exporters"] со строкой csv = "ledger_csv:export". Функция export должна быть доступна в устанавливаемом модуле ledger_csv. После установки этого дистрибутива основной пакет увидит запись независимо от расположения исходников.
from importlib.metadata import entry_points
from typing import Any
def load_exporter(name: str) -> Any:
matches = [
ep for ep in entry_points(group="ledger.exporters")
if ep.name == name
]
if not matches:
raise LookupError(f"unknown exporter: {name}")
if len(matches) != 1:
raise RuntimeError(f"duplicate exporter: {name}")
return matches[0].load()
print(sorted(ep.name for ep in entry_points(group="ledger.exporters")))
Конфликты имён и загрузка
Фрагмент запускается на Python 3.12 без дополнительных библиотек. Без установленных расширений он напечатает пустой список. Чтобы проверить загрузку, установите пакет с указанной секцией, вызовите load_exporter("csv") и передайте результату ожидаемые аргументы. Метаданные отражают установленное окружение: правка pyproject без повторной установки не обязана изменить обнаруживаемые записи.
Два дистрибутива могут объявить одинаковое имя в одной группе. Преобразование записей прямо в dict молча выберет одну из них. В примере конфликт становится отдельной ошибкой до импорта расширения. Это особенно важно для воспроизводимого выбора: порядок обхода метаданных не должен определять, какой экспортёр отправит данные.
Загрузка плагина выполняет обычный импорт и может завершиться ошибкой зависимости. Не скрывайте такой отказ сообщением «плагин не найден»: запись существует, но реализация не загрузилась. Сохраните исходное исключение и сообщите имя выбранного расширения. Проверка наличия записи и проверка совместимости интерфейса — разные этапы.
Entry points подходят для доверенных установленных расширений, а не для исполнения произвольного пользовательского кода в изоляции. Определите маленький контракт export, версию совместимости и проверку результата. Затем сделайте интеграционный тест в чистом окружении: установка основного пакета и плагина, обнаружение имени, загрузка, один полезный вызов. Так проверяется механизм расширения целиком, включая метаданные поставки.
Следующий шаг — перенести этот приём из отдельного файла в поддерживаемое приложение. В курсе «Python для продвинутых: ООП, типизация и тестирование» структура пакетов, объектная модель, mypy и pytest соединяются в типизированном трекере задач с хранением данных и логированием. Можно отработать связь между интерфейсом, реализацией и регрессионным тестом, а затем применить её к своему проекту.
Посмотреть программу и практику курса.
Документация: официальный источник по теме.
Другие задачи по теме: Типы, тесты и поведение программы.
Продолжить на практике
- Python для продвинутых: ООП, типизация и тестирование ↗
Практический курс-мост от базового Python к разработке поддерживаемых приложений. Вы разберётесь со структурой пакетов, объектной моделью, современной типизацией и автоматическими тестами, а затем соедините эти инструменты в небольшом приложении с хранением данных и логированием.
