Параллельный импорт между моделями SQLAlchemy

У меня был модуль, описывающий модели данных:

Основные классы:
- ActionType: Таблица-Справочник отслеживаемых действий
- AuditLog: Логирование действий пользователей.
- Assembly: Описание сборочных чертежей.
- Part: Описание деталей.
- AssemblyPart: Связь между сборками и деталями.
- AssemblyAssembly: Связь между сборками и другими сборками.
- Material: Таблица-Справочник материалов.
- Thick: Таблица-Справочник толщин материалов.
- ProgramX5: Программы обработки для станков Х5.
- PartProgramX5: Связь между деталями и программами обработки.
- Role: Таблица-Справочник ролей пользователей.
- User: Пользователи системы.

Учитывая, что это минимальная структура, я решил сразу разделить его на 3 части:

- модуль с основными моделями (Сборки, детали, пользователи)
- модуль со справочниками (роли, материалы и т.д.)
- модуль связей (все модели, организующие связь многие-ко-многим)

И тут я столкнулся с перекрестным импортом между справочниками / связями и основными моделями.

Как это решается? Интересует best practices декларативного стиля SQLAlchemy.


Ответы (1 шт):

Автор решения: Alexander Lonberg

Скорее вопрос о "Круговом" а не "Параллельном" импорте.

Если это так, то: один модуль(файл) A не может завершить инициализацию, так как требует зависимый модуль B, который, в свою очередь, требует A. Такое невозможно и попытка решить нерешаемое не принесет успехов.

TYPE_CHECKING

Если речь о импорте типов(как аннотаций), то это решает TYPE_CHECKING.

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from my_module import MyType

def foo(instance: "MyType"):
    data = instance.get_data()

Такой тип нельзя использовать явно, например создать экземпляр класса ins = MyType(), но это не мешает вызывать его методы и использовать в аннотациях. Попросту говоря - строки в TYPE_CHECKING интерпретатор проигнорирует, но они видны статическим анализаторам.

Что касается библиотек SQLModel/SQLAlchemy, используется тот же TYPE_CHECKING, и так же окружаем ссылку на символ в скобки, тык SQLModel:

team: Optional["SomeClass"] = Relationship(back_populates="xyz")

... точно так же как это происходит с именем класса, когда он полностью не определен:

class SomeClass:
    def try_get(self) -> "None | SomeClass":...

Protocol

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

// types.py
from typing import Protocol

class LoggerProtocol(Protocol):
    def error(self, msg: str) -> None:...

... после реализовать такой класс-протокол LoggerImpl в любом месте(наследовать его необязательно), но импортировать в зависимые модули именно протокол:

from .types import LoggerProtocol

class MyModule:
    def __init__(self, logger: LoggerProtocol):...

Конечно, такой подход требует возможности создать LoggerImpl и передать его методам зависимого модуля:

logger = LoggerImpl()
my_module = MyModule(logger)

Протоколы работают так же как типы TypeScript: структурная типизация - главное чтобы были все свойства и методы, а что передается в качестве этого типа не имеет значения.

Грязные методы

<pre>(Вместо спойлера которого тут нет)
На самом деле любую зависимость можно получить налету используя нечто подобное:

```py
if TYPE_CHECKING:
    from .my_module import SomeClass

# так
def create_instance(class_name: str, *args: Any, **kwargs: Any) -> Any:
    module = importlib.import_module("my_project.my_module")
    cls = getattr(module, class_name)
    return cls(*args, **kwargs)

# ... или так
def createSomeClass() -> "SomeClass":
    from .my_module import SomeClass
    return SomeClass()
```

... но это хорошо для условных `if sys.platform == "win32"`, а не круговых зависимостей. 
Такие фокусы с круговыми зависимостями приведут к повторному выполнению кода модуля.
</pre>

PS

Чаще всего таких подходов TYPE_CHECKING/Interface-Protocol достаточно чтобы разрешить круговые зависимости, а что такое best practices в SQLAlchemy я не знаю.

PPS: Иногда проще оставить 1000_строк в одном файле не выдумывая лишних имен файлов и не решая проблем с круговыми зависимостями.

→ Ссылка