Skip to main content

A small dependency injection library

Project description

futile_di

pip install futile-di-azya0

Описание

futile_di - маленькая библиотека для инъекции зависимостей, не привязанной к реализации конкретного фреймворка (вроде Depends из FastAPI).

Допустим у нас есть генератор для контроля цикла жизни сессии:

def get_session():
    with create_session() as session:
        yield session

Тогда, чтобы получить сессию нам нужно:

  • Создавать сильную зависимость
  • Хранить созданный генератор на стеке функции-консьюмера
  • Вручную вызывать next(...) для получения сессии
def get_user(generator=get_session()):
    session = next(generator)

    ...

Вместо этого можно использовать механизм инъекции зависимости, который будет делать это "под капотом":

from futile_di_azya0 import Depends, inject


def get_session() -> Session:
    with create_session() as session:
        yield session

@inject
def get_user(session: Session = Depends(get_session)):
    ...

На данный момент @inject и Depends поддерживают:

  • синхронные функции
  • асинхронные функции
  • генераторы
  • асинхронные генераторы

Реализация

inject - декоратор для синхронной/асинхронной функции. Он ищет класс Depends (main) как среди параметров функции по умолчанию, так и среди переданных значений. Далее для каждого main он собирает стек вложенных Depends, чтобы получить значение для текущего (main), параллельно сохраняя контекст до конца выполнения функции:

При анализе вложенных main Depends ищутся только среди параметров по умолчанию

Поддерживаемые типы для обёртки в Depends:

class DependsType(Enum):
    VALUE = 0,
    SYNC = 1,
    ASYNC = 2,
    GENERATOR = 3,
    ASYNC_GENERATOR = 4,

Если это VALUE, то он просто возвращает значение. Если это SYNC или ASYNC, то он вызывает их, получает значение и возвращает его. Если это GENERATOR или ASYNC_GENERATOR, то он получает первое значение, сохраняет генераторы до конца выполнения функции, а потом вызывает ещё один next(...)/await anext(...), для того, чтобы генератор мог завершить свой контекст.

В конечном итоге генераторы удаляются, чтобы контекст мог закрыться

Пример

class Context(AbstractAsyncContextManager):
    def __init__(self):
        super().__init__()

        self.is_open = False
    
    async def __aenter__(self) -> AbstractContextManager:
        await asyncio.sleep(0.01)
        
        self.is_open = True

        return self

    async def __aexit__(self, exc_type, exc_value, traceback):
        await asyncio.sleep(0.01)

        self.is_open = False

        return await super().__aexit__(exc_type, exc_value, traceback)


context = Context()

async def one_generator() -> AsyncGenerator[int]:
    assert not context.is_open
    
    async with context:
        assert context.is_open

        await asyncio.sleep(0.01)

        yield 1

class SomeClass:
    def __init__(self, value: int):
        self.value = value
    
    def get(self) -> int:
        return self.value

def get_instance(value: int = Depends(one_generator)) -> SomeClass:
    assert context.is_open
    
    return SomeClass(value)

@inject
async def some_function(arg: SomeClass = Depends(get_instance)) -> int:
    await asyncio.sleep(0.01)

    assert context.is_open
    
    return arg.get() + 1

assert asyncio.run(some_function()) == 2
assert not context.is_open

Допустим, у нас есть некоторый контекст асинхронный Context. Мы хотим, чтобы:

  • Зависимость с этим контекстом имела некоторую глубину вложенности
  • Контекст сохранялся до конца выполнения функции, которую мы обернули в @inject
  • Асинхронные вложенные зависимости могли зависеть от синхронных вложенных и наоборот (для поддержки асинхронных зависимостей оборачеваемая в @inject исходная функция обязана быть асинхронной)

Этот код был взят из теста к библиотеке. Он показывает работоспособность всех вышеперечисленных запросов.

Пример для тестов aiohttp

def self_mock_generator() -> Generator[aioresponses]:
    with aioresponses() as mock:
        mock.get(
            "https://api.example.com/status",
            payload={"status": True},
            status=200
        )

        yield mock

@inject
def test_get_status_self_injection(mock: aioresponses = Depends(self_mock_generator)):
    async def get_status(session: ClientSession = Depends(get_session)) -> AsyncGenerator[ClientResponse]:
        async with session.get("https://api.example.com/status") as response:
            yield response
    
    @inject
    async def enpoint_example(response: ClientResponse = Depends(get_status)) -> bool:
        await asyncio.sleep(0.01)
        
        return (await response.json())["status"]
    
    assert asyncio.run(enpoint_example())

В этом тесте вместо привычной pytest.fixture используется инъекция из этой библиотеки, чтобы открыть контекст мока и поддерживать его, пока тест не законится. Далее идёт тест для сохранения асинхронного контекста session и асинхронного контекста response

Тесты

Для futile_di было написано несколько тестов. Запустить их можно через библиотеку pytest из корневой директории командой:

pytest

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

futile_di_azya0-0.2.2.tar.gz (10.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

futile_di_azya0-0.2.2-py3-none-any.whl (11.2 kB view details)

Uploaded Python 3

File details

Details for the file futile_di_azya0-0.2.2.tar.gz.

File metadata

  • Download URL: futile_di_azya0-0.2.2.tar.gz
  • Upload date:
  • Size: 10.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for futile_di_azya0-0.2.2.tar.gz
Algorithm Hash digest
SHA256 4a2db216d2e1fdd667c58d0627db1e438bfa90896f29cd7062f2fc1c1d8b34a9
MD5 fa82d69fde5fb289493b5e74371eb9f1
BLAKE2b-256 e37b20c73cf522a96d814ce7f07a518d4e3e7aa51faf0886b4d93ec53fac851b

See more details on using hashes here.

File details

Details for the file futile_di_azya0-0.2.2-py3-none-any.whl.

File metadata

File hashes

Hashes for futile_di_azya0-0.2.2-py3-none-any.whl
Algorithm Hash digest
SHA256 cf9f30addc8deb00ac878044f8a848f61abe698703e860ce954cb2de0a9e92f9
MD5 a44b19a90f053e906f154fb24f090921
BLAKE2b-256 63807c065c47dc5dab01c7a66cc0d681e07f9dd9d5a487e9e5b60dedb8b1e298

See more details on using hashes here.

Supported by

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