Package: classic-container
Classic ContainerБиблиотека представляет реализацию IoC контейнера.Оглавление:ВведениеContainerregisteradd_settingsresetresolveSettingsinitfactoryscopeinstanceВведениеПри написании приложение со слабосвязанной архитектурой,
приятным, но необязательным элементом является DI контейнерДанная библиотека реализует контейнер для библиотек семейства пакетов classic.
Она призвана ускорить работу над приложением упрощая запуск приложения и
разрешение зависимостей.Для понимания зачем это нужен DI и в каких случаях он применим,
рекомендуется к прочтению книга:"Внедрение зависимостей на платформе .NET" Авторы: Вильчинский Н., Симан МаркПример простого приложения:fromabcimportABC,abstractmethodclassInterfaceRepo(ABC):@abstractmethoddefget_by_id(self):...classAccounts(InterfaceRepo):defget_by_id(self):print('Обращение к хранилищу данных')classAccount:# Dependency Injectiondef__init__(self,repo:InterfaceRepo):self.repo=repo# Ручное разрешение зависимостейimpl=Accounts()service=Account(repo=impl)# Разрешение зависимостей через контейнерfromclassic.containerimportcontainercontainer.register(Accounts,Account)container.build(Account)При малых объемах кода ручное внедрение зависимостей выглядит достаточно
лаконично. Сборка же через контейнер на оборот, выглядит громоздко.
При увеличении объемов кода, разница будет в пользу контейнера.Поиск ошибокПри возникновении ошибки бывает сложно понять, что в контейнере пошло не так.Во время разработки рекомендуется устанавливать пакет вdebugварианте:pip install classic-container[debug]Это подтянет пакетtraceback_with_variables:https://pypi.org/project/traceback-with-variables/Для использования вовремя отладки нужно импортировать в композите с ошибкой.Пример:fromtraceback_with_variablesimportactivate_by_importПример трейса:File"home/classic/container/builder.py",line105,inbuildinstance=factory(**factory_kwargs)self=<classic.container.builder.Builderobjectat0x7fb37c9a68f0>target=<class'__main__.Interface'>cached=Nonetarget_settings=<container.Settings(scope=SINGLETON)>target_settings_layer=<classic.container.builder.Builderobjectat0x7fb37c9a68f0>factory=<class'__main__.ErrorImplementation'>factory_settings=<container.Settings(scope=SINGLETON,init={'some_str':[1,2,3]})>factory_kwargs={'some_str':[1,2,3]}signature=<Signature(some_str:str)>parameter=<Parameter"some_str: str">В трейслоге вас интересует:targetзапрошенный классtarget_settingsнастройки запрошенного классаfactoryфабрика построения классаfactory_settingsнастройки фабрикиfactory_kwargsподаваемые параметры фабрикиsignatureсигнатура фабрикиhttps://docs.python.org/3/library/inspect.html#introspecting-callables-with-the-signature-objectparameterпоследний параметр опрошенный в цикле, для которого был вызванbuildContainerПредоставляет четыре метода -register,add_settings,resetиresolve.registerнужен для регистрации классов, интерфейсов, функций и даже модулей.add_settingsдобавляет или обновляет настройки контейнера.
Ключом является класс, значение - настройки.resetудаляет добавленные настройки контейнера и ссылки на инстансы уже
созданных классовresolveпринимает какой-либо класс (интерфейс), и возвращает инстанс
указанного интерфейс с разрешенными зависимостями.Ради удобства был сделан дефолтный контейнер, чтобы не инстанцировать каждый
раз в ручную. Конечно, вы можете создавать свои инстансы контейнера,
когда вам нужно.# Импортирование дефолтного контейнераfromclassic.containerimportcontainer# Самостоятельное создание контейнераfromclassic.containerimportContainermanual_container=Container()В примере выше модульcontainerпредоставляет возможность использовать
методы классаContainer.Дальше по каждому методу подробней:registerПри обращении принимает список компонентов. Определяет тип, заносит в реестр,
каждому типу сопоставляет список фабрик, способных построить указанный тип.Существует только один способ зарегистрировать компоненты:importosfromclassic.containerimportcontainerdefsome_factory()->SomeClass:passcontainer.register(os,some_factory,Composition)Элементами списка могут быть: абстрактные классы, классы,
фабрики (функции, возвращающие один инстанс любого класса) и модули.Абстрактные классы регистрируется в реестре только как ключи, без фабрик.Нормальные классы регистрируются как ключ и соответсвующая ему
фабрика - конструктор самого класса.Фабрики регистрируются сложнее, ключом будет являться результат из
аннотации функции, а значением сама фабрика. Пример:defsome_factory()->SomeClass:# будет зарегистрировано как SomeClass: [some_factory]passМодули не регистрируются напрямую. Регистратор рекурсивно обходит
указанный модуль и все его дочерние модули, и регистрирует в реестре
все классы и фабрики из каждого модуля.importos# будут зарегистрированы os и os.path но не syscontainer.register(os)Методregisterу контейнера является классомRegistrator, реализация
контейнера не подразумевает прямого оперирования объектом из вне.add_settingsДобавляет или обновляет настройки контейнера. Ключом является класс,
значение - настройки.
В роле настроек выступает объект классаSettings:initиспользуется для передачи простых объектов (чисел, строк).factoryописывает способ создания объекта (фабрика, класс, абстрактный класс).scopeрегулирует жизненны цикл объекта, возможные значения указанны в константах текущего пакета:
SINGLETON, TRANSIENTinstanceсоздания настроек с готовым объектом при разрешении
зависимостейfromabcimportABC,abstractmethodfromclassic.containerimportcontainerclassInterface(ABC):@abstractmethoddefmethod(self):...classImplementation(Interface):defmethod(self):return1classComposition:def__init__(self,impl:Interface):self.impl=implclassNextLevelComposition:def__init__(self,obj:Composition):self.obj=objdefcomposition_factory(obj:Interface)->Composition:returnComposition(obj)container.register(composition_factory,Implementation,Composition,NextLevelComposition)container.add_settings({Interface:container.factory(Implementation),Composition:container.factory(composition_factory)})resolved=container.build(NextLevelComposition)Подробное описание в классе SettingsresetУдаляет добавленные настройки контейнера и ссылки на инстансы уже
созданных классов. Подразумевается использование в тестировании.fromdataclassesimportdataclassfromclassic.containerimportcontainerclassSomeCls:pass@dataclassclassAnotherCls:some:SomeClsresult_1=container.build(AnotherCls)container.reset()result_2=container.build(AnotherCls)result_1isnotresult_2resolveРазрешает зависимости для указанной реализации, создает и возвращает инстанс класса.Рекурсивно обходит дерево зависимостей, начиная с указанного класса.
На каждый шаг рекурсии для указанного класса ищется фабрика в реестре.
Далее для найденной фабрики собираются аргументы,
чтобы вызвать фабрику и построить объект.
При этом:пропускаются аргументы простых типов, аргументы без аннотаций
и функции;подставляются значения из init для аргументов,
указанных в этом же init;для аргументов, проаннотированных классами, повторяется рекурсия.В процессе разрешения могут возникать ситуации, когда:для интерфейса (абстрактного класса) не нашлось реализации;для класса нашлось больше 1 фабрики
и ни одна не указана в настройках для этого класса;фабрика для аргумента вернула None
и для аргумента не указан значение по умолчанию;при вызове фабрики не был указан обязательный аргумент.
Во всех этих случаях контейнер выкидывает ResolutionError.Все ошибки состоят из двух частей. Первая часть уникальна для ошибки
и объясняет причину, во второй части описано,
что и в каком порядке пытался построить контейнер.
Она состоит из строк, по три элемента в каждой:Target: полное имя класса (some.module.SomeClass);Factory: полное имя фабрики (another.module.SomeFactory);Arg: имя аргумента фабрики.Пример:fromabcimportABC,abstractmethodfromclassic.containerimportcontainerclassInterface(ABC):@abstractmethoddefmethod(self):...classImplementation(Interface):def__init__(self):raiseNotImplementeddefmethod(self):return1classComposition:def__init__(self,impl:Interface):self.impl=implclassSomeClass:def__init__(self,obj:Composition):self.obj=objcontainer.register(Interface,Implementation,SomeClass,Composition)container.build(SomeClass)classic.container.exceptions.ResolutionError:Class\<class'example.Interface'> do not have registered implementations.Resolvechain:Target:app.SomeClass,Factory:app.SomeClass,Arg:objTarget:app.Composition,Factory:app.Composition,Arg:implTarget:app.Interface,Factory:app.Implementation,Arg:-Методresolveу контейнера является классомResolver, реализация
контейнера не подразумевает прямого оперирования объектом из вне.SettingsКласс хранит настройки resolv-а контейнера.Используется для хранения способа создания объекта или самого объекта
при разрешении зависимостей.initПозволяет установить значения аргументов для фабрики при построении объекта.
Самое частое использование - передача простых объектов (чисел, строк).fromclassic.containerimportContainer,Settings,initclassSomeClass:def__init__(self,some_value:int):# Для int будет неудобно указывать фабрику,# так как много у каких классов может быть параметр типа int# (справедливо для любого простого типа), поэтому# библиотека оставляет возможность# указать параметр через initself.some_value=some_valuecontainer=Container()container.register(SomeClass)# Длинный способ через конструкторcontainer.add_settings({SomeClass:Settings(init=dict(some_value=2))})# Вызов "цепочкой"container.add_settings({SomeClass:Settings().init(some_value=2)})# А можно через алиасcontainer.add_settings({SomeClass:init(some_value=2)})factoryПозволяет явно передать способ создания компонента системы.
Значением может являться любой вызываемый объект возвращающий инстанс любого
объекта, и имеющий аннотацию типов.fromabcimportABC,abstractmethodfromclassic.containerimportContainer,Settings,factoryclassInterface(ABC):@abstractmethoddefmethod(self):...classImplementation(Interface):defmethod(self):return1classSomeClass:def__init__(self,impl:Interface):self.impl=impldefcomposition_factory(obj:Interface)->SomeClass:returnSomeClass(obj)container=Container()container.register(Implementation,SomeClass,composition_factory)# Длинный способ через конструкторcontainer.add_settings({SomeClass:Settings(factory=composition_factory)})# Вызов "цепочкой"container.add_settings({SomeClass:Settings().factory(factory=composition_factory)})# А можно через алиасcontainer.add_settings({SomeClass:factory(composition_factory)})scopeДанная настройка регулирует жизненны цикл объекта, который может быть
SINGLETON и TRANSIENT.При значении SINGLETON контейнер создаст объект только один раз,
все последующие запросы будут использовать тот же самый объект.
Является значением по умолчанию.При TRANSIENT контейнер будет создавать новый объект при каждом resolve.Для каждого класса настройка scope добавляется отдельно!fromabcimportABC,abstractmethodfromclassic.containerimportContainer,Settings,TRANSIENT,scopeclassInterface(ABC):@abstractmethoddefmethod(self):...classImplementation(Interface):defmethod(self):return1container=Container()container.register(Interface,Implementation)# Длинный способ через конструкторcontainer.add_settings({Implementation:Settings(scope=TRANSIENT)})# Вызов "цепочкой"container.add_settings({Implementation:Settings().scope(name=TRANSIENT)})# А можно через алиасcontainer.add_settings({Implementation:scope(TRANSIENT)})instanceНастройка позволяет подать готовый инстанс класса.Подразумевается основное использование при потребности подачи
в разные классы готовых объектов, но настроенных по-разному.Класс сделан для удобства, тоже самое можно сделать через фабрики.fromabcimportABC,abstractmethodfromclassic.containerimportContainer,Settings,instanceclassInterface(ABC):some_value:int@abstractmethoddefmethod(self):...classImplementation(Interface):def__init__(self,some_value):self.some_value=some_valuedefmethod(self):return1classSomeClass:def__init__(self,impl:Interface):self.impl=implcontainer=Container()container.register(Interface,Implementation,SomeClass,)impl=Implementation(1)# Длинный способ через конструкторcontainer.add_settings({SomeClass:Settings(instance=impl)})# Вызов "цепочкой"container.add_settings({SomeClass:Settings().instance(instance=impl)})# А можно через алиасcontainer.add_settings({SomeClass:instance(impl)})