Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions docs/providers/state.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# State

The `State` provider stores a value as part of a context.

It is useful when you want to pass a value into your Container that other providers depend on.


## Creating a state provider

The `State` provider does not accept any arguments when it created.
```python
from that_depends import BaseContainer, providers
class Container(BaseContainer):
my_state: providers.State[int] = providers.State()
```

## Initializing state

=== "async"
```python

with Container.my_state.init(42):
print(await Container.my_state.resolve()) # 42

```

=== "sync"
```python
with Container.my_state.init(42):
print(Container.my_state.resolve_sync()) # 42

```

> Note: If you try to resolve a `State` provider without initializing it first it will raise an `StateNotInitializedError`.


## Nested state

The `State` provider will always resolve the last initialize value.

```python
with Container.my_state.init(1):
print(Container.my_state.resolve_sync()) # 1

with Container.my_state.init(2):
print(Container.my_state.resolve_sync()) # 2

print(Container.my_state.resolve_sync()) # 1
```
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ nav:
- Resources: providers/resources.md
- Selector: providers/selector.md
- Singletons: providers/singleton.md
- State: providers/state.md

- Integrations:
- FastAPI: integrations/fastapi.md
- FastStream: integrations/faststream.md
Expand Down
79 changes: 79 additions & 0 deletions tests/providers/test_state.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import asyncio
import random

import pytest

from that_depends import BaseContainer
from that_depends.exceptions import StateNotInitializedError
from that_depends.providers import AsyncFactory, Factory, State


async def test_state_raises_if_not_set_async() -> None:
class _Container(BaseContainer):
state: State[str] = State()

with pytest.raises(StateNotInitializedError):
await _Container.state.resolve()


def test_state_raises_if_not_set_sync() -> None:
class _Container(BaseContainer):
state: State[str] = State()

with pytest.raises(StateNotInitializedError):
_Container.state.resolve_sync()


async def test_state_set_resolves_correctly_async() -> None:
class _Container(BaseContainer):
state: State[str] = State()

state_value = "test_value"

with _Container.state.init(state_value) as value:
assert value == state_value
assert await _Container.state.resolve() == state_value


def test_state_set_resolves_correctly_sync() -> None:
class _Container(BaseContainer):
state: State[str] = State()

state_value = "test_value"

with _Container.state.init(state_value) as value:
assert value == state_value
assert _Container.state.resolve_sync() == state_value


async def test_state_correctly_manages_its_context() -> None:
async def _async_creator(x: int) -> int:
await asyncio.sleep(random.random())
return x

class _Container(BaseContainer):
state: State[int] = State()
dependent = Factory(lambda x: x, state.cast)
async_dependent = AsyncFactory(_async_creator, state.cast)

async def _main(x: int) -> None:
with _Container.state.init(x):
dependent_value = await _Container.dependent.resolve()
assert dependent_value == x
async_dependent_value = await _Container.async_dependent.resolve()
assert async_dependent_value == x
new_x = x + random.randint(1, 1000)
with _Container.state.init(new_x):
dependent_value = await _Container.dependent.resolve()
assert dependent_value == new_x
async_dependent_value = await _Container.async_dependent.resolve()
assert async_dependent_value == new_x

dependent_value = await _Container.dependent.resolve()
assert dependent_value == x
async_dependent_value = await _Container.async_dependent.resolve()
assert async_dependent_value == x

tasks = [_main(x) for x in range(10)]

await asyncio.gather(*tasks)
17 changes: 17 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from that_depends.utils import UNSET, Unset, is_set


def test_is_set_with_unset() -> None:
assert not is_set(UNSET)


def test_is_set_with_value() -> None:
assert is_set(42)
assert is_set("hello")
assert is_set([1, 2, 3])
assert is_set(None)


def test_unset_is_singleton() -> None:
assert isinstance(UNSET, Unset)
assert UNSET is Unset()
4 changes: 4 additions & 0 deletions that_depends/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
class TypeNotBoundError(Exception):
"""Exception raised when a type is not bound to a provider."""


class StateNotInitializedError(Exception):
"""Exception raised when the state is not initialized."""
2 changes: 2 additions & 0 deletions that_depends/providers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from that_depends.providers.resources import Resource
from that_depends.providers.selector import Selector
from that_depends.providers.singleton import AsyncSingleton, Singleton
from that_depends.providers.state import State


__all__ = [
Expand All @@ -29,6 +30,7 @@
"Resource",
"Selector",
"Singleton",
"State",
"ThreadLocalSingleton",
"container_context",
]
55 changes: 55 additions & 0 deletions that_depends/providers/state.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import typing
import uuid
from contextlib import contextmanager
from contextvars import ContextVar
from typing import TypeVar

from typing_extensions import override

from that_depends.exceptions import StateNotInitializedError
from that_depends.providers import AbstractProvider
from that_depends.utils import UNSET, Unset, is_set


T = TypeVar("T", covariant=False)

_STATE_UNSET_ERROR_MESSAGE: typing.Final[str] = (
"State has not been initialized.\n"
"Please use `with provider.init(state):` to initialize the state before resolving it."
)


class State(AbstractProvider[T]):
"""Provides a value that can be passed into the provider at runtime."""

def __init__(self) -> None:
"""Create a state provider."""
super().__init__()

self._state: ContextVar[T | Unset] = ContextVar(f"STATE_{uuid.uuid4()}", default=UNSET)

@contextmanager
def init(self, state: T) -> typing.Iterator[T]:
"""Set the state provider's value.

Args:
state: value to store.

Returns:
Iterator[T_co]: A context manager that yields the state value.

"""
token = self._state.set(state)
yield state
self._state.reset(token)

@override
async def resolve(self) -> T:
return self.resolve_sync()

@override
def resolve_sync(self) -> T:
value = self._state.get()
if is_set(value):
return value
raise StateNotInitializedError(_STATE_UNSET_ERROR_MESSAGE)
25 changes: 25 additions & 0 deletions that_depends/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from typing import Any, ClassVar, TypeGuard, TypeVar


T = TypeVar("T")


class _Singleton(type):
_instances: ClassVar[dict[type, Any]] = {}

def __call__(cls, *args: Any, **kwargs: Any) -> Any: # noqa: ANN401
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]


class Unset(metaclass=_Singleton):
"""Represents an unset value."""


UNSET = Unset()


def is_set(value: T | Unset) -> TypeGuard[T]:
"""Check if a value is set (not UNSET)."""
return value is not UNSET