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 такие импорты не разрешит: статический анализ намеренно ограничен тем, что доступно в файловой системе.