Шаблон есть в репозитории, но пакет его не читает: importlib.resources

Локально сервис читает шаблон по относительному пути templates/report.txt. После установки пакета рабочий каталог меняется, и файл перестаёт находиться. Попытка построить путь от __file__ улучшает ситуацию только до появления загрузчика, который хранит пакет внутри ZIP.

Ресурс принадлежит пакету

Рабочий каталог процесса отвечает за относительные пути запуска. Он может быть каталогом сервиса, домашней директорией пользователя или временной папкой теста. Установленные вместе с библиотекой шаблоны и небольшие схемы принадлежат самому пакету. Для доступа к ним лучше исходить из имени пакета, а не из текущего расположения процесса.

importlib.resources.files возвращает объект для обхода ресурсов. Его интерфейс похож на pathlib: можно соединить части имени и прочитать текст. Однако это не обещание физического файла на диске. Пакет может быть загружен из архива, а ресурс — прочитан через импортную систему. Поэтому не передавайте полученный объект произвольному API, который требует настоящий путь.

Если внешняя библиотека принимает только путь, используйте as_file как контекстный менеджер и выполняйте вызов внутри with. При необходимости ресурс будет предоставлен через временный файл. Нельзя сохранять такой путь в глобальную переменную и использовать после выхода: срок жизни материализованного файла ограничен контекстом. Для собственного парсера проще сразу передать текст или байты.

Проверить пакет без обычной директории

Самодостаточный пример создаёт ZIP с минимальным пакетом и шаблоном. Архив добавляется в sys.path только на время проверки. Пакет действительно импортируется, а ресурс читается по его имени. Такой тест обнаруживает предположение, что все данные рядом с Python-модулем обязательно доступны обычным путём в файловой системе.

После проверки удаляется запись импорта и восстанавливается sys.path. Это важно для тестового процесса, который выполняет несколько сценариев подряд. В обычном приложении установленный пакет уже находится в путях поиска, поэтому манипуляции sys.path не нужны. Остаются вызов files и чтение нужного ресурса. Имя пакета в примере специально уникальное, чтобы не столкнуться с установленной библиотекой.

from importlib import import_module, invalidate_caches
from importlib.resources import files, as_file
from pathlib import Path
from tempfile import TemporaryDirectory
from zipfile import ZipFile
import sys

with TemporaryDirectory() as folder:
    archive = Path(folder) / "demo.zip"
    with ZipFile(archive, "w") as bundle:
        bundle.writestr("latorn_resource_demo/__init__.py", "")
        bundle.writestr("latorn_resource_demo/templates/report.txt", "Итог: {total}")
    sys.path.insert(0, str(archive))
    invalidate_caches()
    try:
        package = import_module("latorn_resource_demo")
        resource = files(package).joinpath("templates", "report.txt")
        rendered = resource.read_text(encoding="utf-8").format(total=sum([12, 18]))
        assert rendered == "Итог: 30"
        with as_file(resource) as real_path:
            assert real_path.is_file()
            assert real_path.read_text(encoding="utf-8") == "Итог: {total}"
        print(rendered)
        resource.root.close()
    finally:
        sys.path.pop(0)
        sys.modules.pop("latorn_resource_demo", None)
        invalidate_caches()

Чтение и сборка — разные этапы

Правильный вызов files не добавит забытый шаблон в дистрибутив. Сборщик должен включать данные пакета в wheel согласно своим настройкам. После изменения конфигурации соберите wheel, установите его в отдельное окружение и проверьте чтение ресурса оттуда. Editable install может скрывать проблему: он продолжает видеть файлы рабочего дерева, которых в собранном артефакте уже нет.

Сначала посмотрите содержимое wheel как ZIP-архива. Если шаблон отсутствует, исправляйте настройки сборки. Если присутствует, проверьте имя пакета и путь внутри него. Эти два наблюдения разделяют задачи и экономят время: изменение текущего каталога не исправляет отсутствующий файл в артефакте, а переупаковка не лечит обращение к неправильному имени ресурса.

Ресурсы подходят для поставляемых вместе с версией программы данных. Пользовательские настройки, загруженные отчёты и изменяемый кэш лучше хранить отдельно по явному пути. Запись внутрь установленного пакета зависит от разрешений и способа установки. Для теста релиза полезно запускать чтение из каталога, где нет исходников: тогда случайная привязка к репозиторию станет видимой.

В демонстрации возвращаемый объект является zipfile.Path; его корневой архив явно закрывается после чтения. Это нужно, чтобы Windows позволила удалить временный ZIP при выходе из TemporaryDirectory. В библиотечной функции ресурс обычно читается из установленного пакета, и такой тестовый архив вручную не создаётся. Не переносите специфическую очистку ZIP в универсальный загрузчик ресурсов: у другого импортного загрузчика устройство объекта может отличаться. Лучше держать создание тестового окружения рядом с проверкой, а функцию чтения оставить зависимой только от публичного интерфейса ресурса. При интеграции с внешним API сохраните тот же принцип: получение настоящего пути через as_file и использование этого пути должны находиться в одной области жизни, без отложенных фоновых обращений после выхода из with.

Следующий шаг в практике

В бесплатном уроке о pytest закрепите проверку результата, затем превратите чтение шаблона в тест установленного пакета. Откройте программу продвинутого Python: в ней есть pyproject.toml, editable install и структура пакета. Для дальнейшей практики со стандартной библиотекой доступен «Python для профессионалов».

Продолжить на практике

  • Python для продвинутых: ООП, типизация и тестирование ↗

    Практический курс-мост от базового Python к разработке поддерживаемых приложений. Вы разберётесь со структурой пакетов, объектной моделью, современной типизацией и автоматическими тестами, а затем соедините эти инструменты в небольшом приложении с хранением данных и логированием.

  • Python для профессионалов ↗

    Python для профессионалов — курс для людей, стремящихся углубить свои знания языка. Вы изучите итераторы, генераторы, декораторы, современные возможности Python (3.8+), освоите метапрограммирование, асинхронность, внутреннее устройство интерпретатора, инструменты оптимизации и профилирования, а также получите уверенность в использовании продвинутых возможностей стандартной библиотеки.