2025, Sep 27 15:17

Абсолютные импорты proj.foo.* в Python: Pyright, PEP 420 и симлинки

Как заставить работать абсолютные импорты proj.foo.* в Python в монорепо и отдельном репо: Pyright в VS Code, редактируемая установка, PEP 420 и симлинки.

Когда подпакет одновременно существует в большом приватном монорепозитории и в отдельном самостоятельном репозитории, абсолютные импорты вроде proj.foo.bar должны одинаково работать в обеих средах. Запустить это в рантайме Python обычно удаётся с помощью редактируемой установки, но статические анализаторы — например, Pyright в VS Code — будут ругаться, если структура на диске не выглядит как proj/foo/...

Предпосылки задачи

Предположим, что в разработческом репозитории код лежит под src/proj/foo, но стажёр может клонировать только автономный репозиторий, где есть лишь поддерево foo. Внутри этого отдельного дерева модули продолжают импортировать по полному пространству имён proj.foo.bar, и у вас нет возможности перемещать файлы, добавлять фиктивные заглушки или менять эти импорты. Цель — добиться, чтобы и интерпретатор, и Pyright разрешали proj.foo.* так, словно отдельная папка является частью исходного родительского пакета.

Минимальный пример

В автономном клоне представим такую структуру:

~/foo-repo/
  └── bar/
      ├── __init__.py
      ├── types.py
      └── jim.py

Вот код пакета, который опирается на пространство имён proj.foo.*. Имена изменены, но поведение идентично.

~/foo-repo/bar/__init__.py

from proj.foo.bar.types import Payload

__all__ = ["Payload"]

~/foo-repo/bar/types.py

class Payload:
    pass

~/foo-repo/bar/jim.py

import proj.foo.bar as foopkg

print(foopkg.Payload)

Если запустить python bar/jim.py в свежем виртуальном окружении, ни рантайм, ни Pyright не найдут proj.foo.*, потому что в автономном репозитории нет структуры каталогов вида proj/foo.

Почему это не работает

Setuptools умеет сопоставлять имена пакетов произвольным исходным каталогам через package-dir и редактируемые установки. Такой трюк устраивает интерпретатор, потому что импорт-хук, который даёт editable-установка, перенаправляет proj.foo к вашим исходникам. Но Pyright не выполняет импорт-хуки и не смотрит на метаданные упаковки. Он разрешает импорты напрямую по структуре файловой системы (или по стабу) и ожидает увидеть реальные каталоги, соответствующие точечной записи пакета на sys.path. Если нигде нет физической иерархии proj/foo, по которой Pyright может пройтись, то proj.foo.* в IDE не разрешится, даже если код в рантайме запускается.

Два рабочих минимальных пути

Сначала нужно обеспечить работу пакета на месте для интерпретатора. Затем — предоставить Pyright вид каталога, который выглядит как proj/foo/…

Маршрут с редактируемой установкой ниже аккуратно решает задачу для интерпретатора. Чтобы удовлетворить Pyright, далее материализуйте ожидаемую форму пространства имён с помощью символической ссылки, которую не добавляйте в систему контроля версий.

Вариант 1: редактируемая установка с явным сопоставлением (PEP 660)

Поместите это в ~/foo-repo/pyproject.toml. Файл объявляет пакет в пространстве имён proj.foo и сопоставляет его с вашей текущей раскладкой, оставаясь редактируемым.

[project]
name = "proj.foo"
version = "0.0.0"
requires-python = ">=3.11"

[build-system]
requires = ["setuptools>=70", "wheel", "editables>=0.5"]
build-backend = "setuptools.build_meta"

[tool.setuptools]
packages = ["proj.foo", "proj.foo.bar"]
package-dir = { "proj.foo" = ".", "proj.foo.bar" = "bar" }
include-package-data = true

Затем создайте и активируйте окружение и запустите модуль через -m — так вы избегаете особенностей путей к скриптам и используете семантику импорта пакетов.

uv sync
source .venv/bin/activate
python -m proj.foo.bar.jim

На этом этапе рантайм работает, потому что редактируемая установка предоставляет нужный импорт-хук. Но одного этого для Pyright недостаточно: он не выполняет такие хуки и всё ещё не разрешит proj.foo.* только по этим метаданным.

Вариант 2: дать Pyright ожидаемую форму каталога (PEP 420 + символическая ссылка)

Сделайте на диске реальный каталог proj/foo, который указывает на ваш код, не двигая файлы и не добавляя заглушки. Создайте макет неявного пакета-пространства имён, добавив каталоги и симлинк. Держите эту структуру вне контроля версий, чтобы не менять канонический вид репозитория.

cd ~/foo-repo
mkdir -p proj/foo
ln -s ../../bar proj/foo/bar

Добавьте proj/ в .gitignore, чтобы эти вспомогательные пути никогда не покидали локальную копию. Теперь Pyright видит на диске proj/foo/bar и может разрешать proj.foo.bar и его подмодули, а интерпретатор продолжает загружать тот же исходный код.

Почему одних метаданных недостаточно

Pyright и аналогично Pylance разрешают импорты по реальным каталогам и файлам на путях поиска интерпретатора. Они не запускают бекенды сборки, не выполняют импорт-хуки и не читают упаковочные метаданные вроде package-dir для поиска исходников. В случае редактируемых установок анализатор индексирует .pth-файлы, которые указывают на реальные каталоги. Пакеты-пространства имён также требуют, чтобы каждый сегмент точечного имени присутствовал как каталог, доступный через sys.path. Если физической иерархии proj/foo нет, статическому резолверу просто нечего обходить, и proj.foo.* остаётся неизвестным для IDE, даже когда импорты в рантайме работают.

Зачем это нужно

В сценариях с раздельными репозиториями или поддеревьями скорость разработки зависит от единого пути импорта, одинаково работающего и в отрыве, и в монорепо. Полагаться только на трюки упаковки — значит замаскировать проблемы локально и оставить IDE «слепой» к вашим модулям. Создание реального представления пространства имён на диске даёт корректное поведение в рантайме и надёжный статический анализ, не меняя импорты в исходниках и не двигая файлы.

Итоги

Используйте редактируемую установку, чтобы сохранить корректное сопоставление для интерпретатора, и предпочитайте запуск через python -m для модулей пакета. А если нужно, чтобы Pyright и VS Code полноценно разрешали proj.foo.*, добавьте каталог proj/foo и сделайте в нём симлинк на bar, игнорируя эту подсобную обвязку в системе контроля версий. Если вспомогательные действия с файловой системой недопустимы, придётся смириться с тем, что Pyright такие импорты не разрешит: статический анализ намеренно ограничен тем, что доступно в файловой системе.

Статья основана на вопросе с StackOverflow от Phrogz и ответе Mag_Amine.