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
52 changes: 52 additions & 0 deletions docs/experimental/lazy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Lazy Provider

The `LazyProvider` enables you to reference other providers without explicitly
importing them into your module.

This can be helpful if you have a circular dependency between providers in
multiple containers.


## Creating a Lazy Provider

=== "Single import string"
```python
from that_depends.experimental import LazyProvider

lazy_p = LazyProvider("full.import.string.including.attributes")
```
=== "Separate module and provider"
```python
from that_depends.experimental import LazyProvider

lazy_p = LazyProvider(module_string="my.module", provider_string="attribute.path")
```


## Usage

You can use the lazy provider in exactly the same way as you would use the referenced provider.

```python
# first_container.py
from that_depends import BaseContainer, providers, ContextScopes

def my_creator():
yield 42

class FirstContainer(BaseContainer):
value_provider = providers.ContextResource(my_creator).with_config(scope=ContextScopes.APP)
```

You can lazily import this provider:
```python
# second_container.py
from that_depends.experimental import LazyProvider
from that_depends import BaseContainer, providers
class SecondContainer(BaseContainer):
lazy_value = LazyProvider("first_container.FirstContainer.value_provider")


with SecondContainer.lazy_value.context_sync(force=True):
SecondContainer.lazy_value.resolve_sync() # 42
```
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ nav:
- Selector: providers/selector.md
- Singletons: providers/singleton.md
- State: providers/state.md
- Experimental Features:
- Lazy Provider: experimental/lazy.md

- Integrations:
- FastAPI: integrations/fastapi.md
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ fastapi = [
"fastapi",
]
faststream = [
"faststream"
"faststream<0.6.0"
]

[project.urls]
Expand Down
Empty file added tests/experimental/__init__.py
Empty file.
15 changes: 15 additions & 0 deletions tests/experimental/test_container_1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from tests.experimental.test_container_2 import Container2
from that_depends import BaseContainer, providers
from that_depends.experimental import LazyProvider


class Container1(BaseContainer):
"""Test Container 1."""

alias = "container_1"
obj_1 = providers.Object(1)
obj_2 = LazyProvider(module_string="tests.experimental.test_container_2", provider_string="Container2.obj_2")


def test_lazy_provider_resolution_sync() -> None:
assert Container2.obj_2.resolve_sync() == 2 # noqa: PLR2004
145 changes: 145 additions & 0 deletions tests/experimental/test_container_2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import random
from collections.abc import AsyncIterator, Iterator

import pytest
import typing_extensions

from that_depends import BaseContainer, ContextScopes, container_context, providers
from that_depends.experimental import LazyProvider


class _RandomWrapper:
def __init__(self) -> None:
self.value = random.random()

@typing_extensions.override
def __eq__(self, other: object) -> bool:
if isinstance(other, _RandomWrapper):
return self.value == other.value
return False # pragma: nocover

def __hash__(self) -> int:
return 0 # pragma: nocover


async def _async_creator() -> AsyncIterator[float]:
yield random.random()


def _sync_creator() -> Iterator[_RandomWrapper]:
yield _RandomWrapper()


class Container2(BaseContainer):
"""Test Container 2."""

alias = "container_2"
default_scope = ContextScopes.APP
obj_1 = LazyProvider("tests.experimental.test_container_1.Container1.obj_1")
obj_2 = providers.Object(2)
async_context_provider = providers.ContextResource(_async_creator)
sync_context_provider = providers.ContextResource(_sync_creator)
singleton_provider = providers.Singleton(lambda: random.random())


async def test_lazy_provider_resolution_async() -> None:
assert await Container2.obj_1.resolve() == 1


def test_lazy_provider_override_sync() -> None:
override_value = 42
Container2.obj_1.override_sync(override_value)
assert Container2.obj_1.resolve_sync() == override_value
Container2.obj_1.reset_override_sync()
assert Container2.obj_1.resolve_sync() == 1


async def test_lazy_provider_override_async() -> None:
override_value = 42
await Container2.obj_1.override(override_value)
assert await Container2.obj_1.resolve() == override_value
await Container2.obj_1.reset_override()
assert await Container2.obj_1.resolve() == 1


def test_lazy_provider_invalid_state() -> None:
lazy_provider = LazyProvider(
module_string="tests.experimental.test_container_2", provider_string="Container2.sync_context_provider"
)
lazy_provider._module_string = None
with pytest.raises(RuntimeError):
lazy_provider.resolve_sync()


async def test_lazy_provider_context_resource_async() -> None:
lazy_provider = LazyProvider("tests.experimental.test_container_2.Container2.async_context_provider")
async with lazy_provider.context_async(force=True):
assert await lazy_provider.resolve() == await Container2.async_context_provider.resolve()
async with Container2.async_context_provider.context_async(force=True):
assert await lazy_provider.resolve() == await Container2.async_context_provider.resolve()

with pytest.raises(RuntimeError):
await lazy_provider.resolve()

async with container_context(Container2, scope=ContextScopes.APP):
assert await lazy_provider.resolve() == await Container2.async_context_provider.resolve()

assert lazy_provider.get_scope() == ContextScopes.APP

assert lazy_provider.supports_context_sync() is False


def test_lazy_provider_context_resource_sync() -> None:
lazy_provider = LazyProvider("tests.experimental.test_container_2.Container2.sync_context_provider")
with lazy_provider.context_sync(force=True):
assert lazy_provider.resolve_sync() == Container2.sync_context_provider.resolve_sync()
with Container2.sync_context_provider.context_sync(force=True):
assert lazy_provider.resolve_sync() == Container2.sync_context_provider.resolve_sync()

with pytest.raises(RuntimeError):
lazy_provider.resolve_sync()

with container_context(Container2, scope=ContextScopes.APP):
assert lazy_provider.resolve_sync() == Container2.sync_context_provider.resolve_sync()

assert lazy_provider.get_scope() == ContextScopes.APP

assert lazy_provider.supports_context_sync() is True


async def test_lazy_provider_tear_down_async() -> None:
lazy_provider = LazyProvider("tests.experimental.test_container_2.Container2.singleton_provider")
assert lazy_provider.resolve_sync() == Container2.singleton_provider.resolve_sync()

await lazy_provider.tear_down()

assert await lazy_provider.resolve() == Container2.singleton_provider.resolve_sync()


def test_lazy_provider_tear_down_sync() -> None:
lazy_provider = LazyProvider("tests.experimental.test_container_2.Container2.singleton_provider")
assert lazy_provider.resolve_sync() == Container2.singleton_provider.resolve_sync()

lazy_provider.tear_down_sync()

assert lazy_provider.resolve_sync() == Container2.singleton_provider.resolve_sync()


async def test_lazy_provider_not_implemented() -> None:
lazy_provider = Container2.obj_1
with pytest.raises(NotImplementedError):
lazy_provider.get_scope()
with pytest.raises(NotImplementedError):
lazy_provider.context_sync()
with pytest.raises(NotImplementedError):
lazy_provider.context_async()
with pytest.raises(NotImplementedError):
lazy_provider.tear_down_sync()
with pytest.raises(NotImplementedError):
await lazy_provider.tear_down()


def test_lazy_provider_attr_getter() -> None:
lazy_provider = LazyProvider("tests.experimental.test_container_2.Container2.sync_context_provider")
with lazy_provider.context_sync(force=True):
assert isinstance(lazy_provider.value.resolve_sync(), float)
30 changes: 30 additions & 0 deletions tests/experimental/test_lazy_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import pytest

from that_depends.experimental import LazyProvider


def test_lazy_provider_incorrect_initialization() -> None:
with pytest.raises(
ValueError,
match=r"You must provide either import_string "
"OR both module_string AND provider_string, but not both or neither.",
):
LazyProvider(module_string="3213") # type: ignore[call-overload]

with pytest.raises(ValueError, match=r"Invalid import_string ''"):
LazyProvider("")

with pytest.raises(ValueError, match=r"Invalid provider_string ''"):
LazyProvider(module_string="some.module", provider_string="")

with pytest.raises(ValueError, match=r"Invalid module_string '.'"):
LazyProvider(module_string=".", provider_string="SomeProvider")

with pytest.raises(ValueError, match=r"Invalid import_string 'import.'"):
LazyProvider("import.")


def test_lazy_provider_incorrect_import_string() -> None:
p = LazyProvider("some.random.path")
with pytest.raises(ImportError):
p.resolve_sync()
8 changes: 8 additions & 0 deletions that_depends/experimental/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""Experimental features."""

from that_depends.experimental.providers import LazyProvider


__all__ = [
"LazyProvider",
]
Loading