Skip to main content

Provides container with dependencies resolving

Project description

Classic Container

Библиотека представляет реализацию IoC контейнера.

Оглавление:

Введение

При написании приложение со слабосвязанной архитектурой, приятным, но необязательным элементом является DI контейнер

Данная библиотека реализует контейнер для библиотек семейства пакетов classic. Она призвана ускорить работу над приложением упрощая запуск приложения и разрешение зависимостей.

Для понимания зачем это нужен DI и в каких случаях он применим, рекомендуется к прочтению книга:

"Внедрение зависимостей на платформе .NET" Авторы: Вильчинский Н., Симан Марк

Пример простого приложения:

from abc import ABC, abstractmethod


class InterfaceRepo(ABC):

    @abstractmethod
    def get_by_id(self): ...


class Accounts(InterfaceRepo):

    def get_by_id(self):
        print('Обращение к хранилищу данных')


class Account:

    # Dependency Injection
    def __init__(self, repo: InterfaceRepo):
        self.repo = repo


# Ручное разрешение зависимостей 
impl = Accounts()
service = Account(repo=impl)

# Разрешение зависимостей через контейнер
from classic.container import container

container.register(Accounts, Account)
container.build(Account)

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

Поиск ошибок

При возникновении ошибки бывает сложно понять, что в контейнере пошло не так.

Во время разработки рекомендуется устанавливать пакет в debug варианте: pip install classic-container[debug]

Это подтянет пакет traceback_with_variables: https://pypi.org/project/traceback-with-variables/

Для использования вовремя отладки нужно импортировать в композите с ошибкой.

Пример:

from traceback_with_variables import activate_by_import

Пример трейса:

 File "home/classic/container/builder.py", line 105, in build
    instance = factory(**factory_kwargs)
      self = <classic.container.builder.Builder object at 0x7fb37c9a68f0>
      target = <class '__main__.Interface'>
      cached = None
      target_settings = <container.Settings(scope=SINGLETON)>
      target_settings_layer = <classic.container.builder.Builder object at 0x7fb37c9a68f0>
      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-object
  • parameter последний параметр опрошенный в цикле, для которого был вызван build

Container

Предоставляет четыре метода - register, add_settings, reset и resolve.

  • register нужен для регистрации классов, интерфейсов, функций и даже модулей.

  • add_settings добавляет или обновляет настройки контейнера. Ключом является класс, значение - настройки.

  • reset удаляет добавленные настройки контейнера и ссылки на инстансы уже созданных классов

  • resolve принимает какой-либо класс (интерфейс), и возвращает инстанс указанного интерфейс с разрешенными зависимостями.

Ради удобства был сделан дефолтный контейнер, чтобы не инстанцировать каждый раз в ручную. Конечно, вы можете создавать свои инстансы контейнера, когда вам нужно.

# Импортирование дефолтного контейнера
from classic.container import container

# Самостоятельное создание контейнера
from classic.container import Container

manual_container = Container()

В примере выше модуль container предоставляет возможность использовать методы класса Container.

Дальше по каждому методу подробней:

register

При обращении принимает список компонентов. Определяет тип, заносит в реестр, каждому типу сопоставляет список фабрик, способных построить указанный тип.

Существует только один способ зарегистрировать компоненты:

import os
from classic.container import container

def some_factory() -> SomeClass:
    pass

container.register(os, some_factory, Composition)

Элементами списка могут быть: абстрактные классы, классы, фабрики (функции, возвращающие один инстанс любого класса) и модули.

  • Абстрактные классы регистрируется в реестре только как ключи, без фабрик.
  • Нормальные классы регистрируются как ключ и соответсвующая ему фабрика - конструктор самого класса.
  • Фабрики регистрируются сложнее, ключом будет являться результат из аннотации функции, а значением сама фабрика. Пример:
def some_factory() -> SomeClass:
    # будет зарегистрировано как SomeClass: [some_factory]
    pass
  • Модули не регистрируются напрямую. Регистратор рекурсивно обходит указанный модуль и все его дочерние модули, и регистрирует в реестре все классы и фабрики из каждого модуля.
import os
# будут зарегистрированы os и os.path но не sys
container.register(os)

Метод register у контейнера является классом Registrator, реализация контейнера не подразумевает прямого оперирования объектом из вне.

add_settings

Добавляет или обновляет настройки контейнера. Ключом является класс, значение - настройки. В роле настроек выступает объект класса Settings:

  • init используется для передачи простых объектов (чисел, строк).
  • factory описывает способ создания объекта (фабрика, класс, абстрактный класс).
  • scope регулирует жизненны цикл объекта, возможные значения указанны в константах текущего пакета: SINGLETON, TRANSIENT
  • instance создания настроек с готовым объектом при разрешении зависимостей
from abc import ABC, abstractmethod
from classic.container import container


class Interface(ABC):

    @abstractmethod
    def method(self): ...


class Implementation(Interface):

    def method(self):
        return 1


class Composition:

    def __init__(self, impl: Interface):
        self.impl = impl


class NextLevelComposition:

    def __init__(self, obj: Composition):
        self.obj = obj


def composition_factory(obj: Interface) -> Composition:
    return Composition(obj)


container.register(
    composition_factory, Implementation, Composition, NextLevelComposition
)

container.add_settings({
    Interface: container.factory(Implementation),
    Composition: container.factory(composition_factory)
})

resolved = container.build(NextLevelComposition)

Подробное описание в классе Settings

reset

Удаляет добавленные настройки контейнера и ссылки на инстансы уже созданных классов. Подразумевается использование в тестировании.

from dataclasses import dataclass
from classic.container import container


class SomeCls:
    pass


@dataclass
class AnotherCls:
    some: SomeCls


result_1 = container.build(AnotherCls)
container.reset()
result_2 = container.build(AnotherCls)

result_1 is not result_2

resolve

Разрешает зависимости для указанной реализации, создает и возвращает инстанс класса.

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

  • пропускаются аргументы простых типов, аргументы без аннотаций и функции;
  • подставляются значения из init для аргументов, указанных в этом же init;
  • для аргументов, проаннотированных классами, повторяется рекурсия.

В процессе разрешения могут возникать ситуации, когда:

  • для интерфейса (абстрактного класса) не нашлось реализации;
  • для класса нашлось больше 1 фабрики и ни одна не указана в настройках для этого класса;
  • фабрика для аргумента вернула None и для аргумента не указан значение по умолчанию;
  • при вызове фабрики не был указан обязательный аргумент. Во всех этих случаях контейнер выкидывает ResolutionError.

Все ошибки состоят из двух частей. Первая часть уникальна для ошибки и объясняет причину, во второй части описано, что и в каком порядке пытался построить контейнер. Она состоит из строк, по три элемента в каждой:

  • Target: полное имя класса (some.module.SomeClass);
  • Factory: полное имя фабрики (another.module.SomeFactory);
  • Arg: имя аргумента фабрики.

Пример:

from abc import ABC, abstractmethod
from classic.container import container


class Interface(ABC):

    @abstractmethod
    def method(self): ...


class Implementation(Interface):

    def __init__(self):
        raise NotImplemented

    def method(self):
        return 1


class Composition:

    def __init__(self, impl: Interface):
        self.impl = impl


class SomeClass:

    def __init__(self, obj: Composition):
        self.obj = obj


container.register(Interface, Implementation, SomeClass, Composition)
container.build(SomeClass)
classic.container.exceptions.ResolutionError: Class \
<class 'example.Interface'> do not have registered implementations.
Resolve chain:
Target: app.SomeClass, Factory: app.SomeClass, Arg: obj
Target: app.Composition, Factory: app.Composition, Arg: impl
Target: app.Interface, Factory: app.Implementation, Arg: -

Метод resolve у контейнера является классом Resolver, реализация контейнера не подразумевает прямого оперирования объектом из вне.

Settings

Класс хранит настройки resolv-а контейнера.

Используется для хранения способа создания объекта или самого объекта при разрешении зависимостей.

init

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

from classic.container import Container, Settings, init

class SomeClass:
    def __init__(self, some_value: int):
        # Для int будет неудобно указывать фабрику,
        # так как много у каких классов может быть параметр типа int
        # (справедливо для любого простого типа), поэтому
        # библиотека оставляет возможность
        # указать параметр через init
        self.some_value = some_value

container = 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

Позволяет явно передать способ создания компонента системы. Значением может являться любой вызываемый объект возвращающий инстанс любого объекта, и имеющий аннотацию типов.

from abc import ABC, abstractmethod
from classic.container import Container, Settings, factory

class Interface(ABC):

    @abstractmethod
    def method(self): ...

class Implementation(Interface):

    def method(self):
        return 1

class SomeClass:

    def __init__(self, impl: Interface):
        self.impl = impl

def composition_factory(obj: Interface) -> SomeClass:
    return SomeClass(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 добавляется отдельно!

from abc import ABC, abstractmethod
from classic.container import Container, Settings, TRANSIENT, scope

class Interface(ABC):

    @abstractmethod
    def method(self): ...

class Implementation(Interface):

    def method(self):
        return 1

container = 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

Настройка позволяет подать готовый инстанс класса.

Подразумевается основное использование при потребности подачи в разные классы готовых объектов, но настроенных по-разному.

Класс сделан для удобства, тоже самое можно сделать через фабрики.

from abc import ABC, abstractmethod
from classic.container import Container, Settings, instance

class Interface(ABC):
    some_value: int

    @abstractmethod
    def method(self): ...

class Implementation(Interface):

    def __init__(self, some_value):
        self.some_value = some_value

    def method(self):
        return 1

class SomeClass:

    def __init__(self, impl: Interface):
        self.impl = impl

container = 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)})

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

classic-container-0.6.0.tar.gz (21.6 kB view hashes)

Uploaded Source

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page