Skip to content

Commit d17ba52

Browse files
committed
feat: LazyProvider
1 parent 170e23e commit d17ba52

File tree

11 files changed

+434
-4
lines changed

11 files changed

+434
-4
lines changed

docs/experimental/lazy.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Lazy Provider
2+
3+
The `LazyProvider` enables you to reference other providers without explicitly
4+
importing them into your module.
5+
6+
This can be helpful if you have a circular dependency between providers in
7+
multiple containers.
8+
9+
10+
## Creating a Lazy Provider
11+
12+
=== "Single import string"
13+
```python
14+
from that_depends.experimental import LazyProvider
15+
16+
lazy_p = LazyProvider("full.import.string.including.attributes")
17+
```
18+
=== "Separate module and provider"
19+
```python
20+
from that_depends.experimental import LazyProvider
21+
22+
lazy_p = LazyProvider(module_string="my.module", provider_string="attribute.path")
23+
```
24+
25+
26+
## Usage
27+
28+
You can use the lazy provider in exactly the same way as you would use the referenced provider.
29+
30+
```python
31+
# first_container.py
32+
from that_depends import BaseContainer, providers, ContextScopes
33+
34+
def my_creator():
35+
yield 42
36+
37+
class FirstContainer(BaseContainer):
38+
value_provider = providers.ContextResource(my_creator).with_config(scope=ContextScopes.APP)
39+
```
40+
41+
You can lazily import this provider:
42+
```python
43+
# second_container.py
44+
from that_depends.experimental import LazyProvider
45+
from that_depends import BaseContainer, providers
46+
class SecondContainer(BaseContainer):
47+
lazy_value = LazyProvider("first_container.FirstContainer.value_provider")
48+
49+
50+
with SecondContainer.lazy_value.context_sync(force=True):
51+
SecondContainer.lazy_value.resolve_sync() # 42
52+
```

mkdocs.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ nav:
2323
- Selector: providers/selector.md
2424
- Singletons: providers/singleton.md
2525
- State: providers/state.md
26+
- Experimental Features:
27+
- Lazy Provider: experimental/lazy.md
2628

2729
- Integrations:
2830
- FastAPI: integrations/fastapi.md

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ fastapi = [
2424
"fastapi",
2525
]
2626
faststream = [
27-
"faststream"
27+
"faststream<0.6.0"
2828
]
2929

3030
[project.urls]

tests/experimental/__init__.py

Whitespace-only changes.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from tests.experimental.test_container_2 import Container2
2+
from that_depends import BaseContainer, providers
3+
from that_depends.experimental import LazyProvider
4+
5+
6+
class Container1(BaseContainer):
7+
"""Test Container 1."""
8+
9+
alias = "container_1"
10+
obj_1 = providers.Object(1)
11+
obj_2 = LazyProvider(module_string="tests.experimental.test_container_2", provider_string="Container2.obj_2")
12+
13+
14+
def test_lazy_provider_resolution_sync() -> None:
15+
assert Container2.obj_2.resolve_sync() == 2 # noqa: PLR2004
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import random
2+
from collections.abc import AsyncIterator, Iterator
3+
4+
import pytest
5+
import typing_extensions
6+
7+
from that_depends import BaseContainer, ContextScopes, container_context, providers
8+
from that_depends.experimental import LazyProvider
9+
10+
11+
class _RandomWrapper:
12+
def __init__(self) -> None:
13+
self.value = random.random()
14+
15+
@typing_extensions.override
16+
def __eq__(self, other: object) -> bool:
17+
if isinstance(other, _RandomWrapper):
18+
return self.value == other.value
19+
return False # pragma: nocover
20+
21+
def __hash__(self) -> int:
22+
return 0 # pragma: nocover
23+
24+
25+
async def _async_creator() -> AsyncIterator[float]:
26+
yield random.random()
27+
28+
29+
def _sync_creator() -> Iterator[_RandomWrapper]:
30+
yield _RandomWrapper()
31+
32+
33+
class Container2(BaseContainer):
34+
"""Test Container 2."""
35+
36+
alias = "container_2"
37+
default_scope = ContextScopes.APP
38+
obj_1 = LazyProvider("tests.experimental.test_container_1.Container1.obj_1")
39+
obj_2 = providers.Object(2)
40+
async_context_provider = providers.ContextResource(_async_creator)
41+
sync_context_provider = providers.ContextResource(_sync_creator)
42+
singleton_provider = providers.Singleton(lambda: random.random())
43+
44+
45+
async def test_lazy_provider_resolution_async() -> None:
46+
assert await Container2.obj_1.resolve() == 1
47+
48+
49+
def test_lazy_provider_override_sync() -> None:
50+
override_value = 42
51+
Container2.obj_1.override_sync(override_value)
52+
assert Container2.obj_1.resolve_sync() == override_value
53+
Container2.obj_1.reset_override_sync()
54+
assert Container2.obj_1.resolve_sync() == 1
55+
56+
57+
async def test_lazy_provider_override_async() -> None:
58+
override_value = 42
59+
await Container2.obj_1.override(override_value)
60+
assert await Container2.obj_1.resolve() == override_value
61+
await Container2.obj_1.reset_override()
62+
assert await Container2.obj_1.resolve() == 1
63+
64+
65+
def test_lazy_provider_invalid_state() -> None:
66+
lazy_provider = LazyProvider(
67+
module_string="tests.experimental.test_container_2", provider_string="Container2.sync_context_provider"
68+
)
69+
lazy_provider._module_string = None
70+
with pytest.raises(RuntimeError):
71+
lazy_provider.resolve_sync()
72+
73+
74+
async def test_lazy_provider_context_resource_async() -> None:
75+
lazy_provider = LazyProvider("tests.experimental.test_container_2.Container2.async_context_provider")
76+
async with lazy_provider.context_async(force=True):
77+
assert await lazy_provider.resolve() == await Container2.async_context_provider.resolve()
78+
async with Container2.async_context_provider.context_async(force=True):
79+
assert await lazy_provider.resolve() == await Container2.async_context_provider.resolve()
80+
81+
with pytest.raises(RuntimeError):
82+
await lazy_provider.resolve()
83+
84+
async with container_context(Container2, scope=ContextScopes.APP):
85+
assert await lazy_provider.resolve() == await Container2.async_context_provider.resolve()
86+
87+
assert lazy_provider.get_scope() == ContextScopes.APP
88+
89+
assert lazy_provider.supports_context_sync() is False
90+
91+
92+
def test_lazy_provider_context_resource_sync() -> None:
93+
lazy_provider = LazyProvider("tests.experimental.test_container_2.Container2.sync_context_provider")
94+
with lazy_provider.context_sync(force=True):
95+
assert lazy_provider.resolve_sync() == Container2.sync_context_provider.resolve_sync()
96+
with Container2.sync_context_provider.context_sync(force=True):
97+
assert lazy_provider.resolve_sync() == Container2.sync_context_provider.resolve_sync()
98+
99+
with pytest.raises(RuntimeError):
100+
lazy_provider.resolve_sync()
101+
102+
with container_context(Container2, scope=ContextScopes.APP):
103+
assert lazy_provider.resolve_sync() == Container2.sync_context_provider.resolve_sync()
104+
105+
assert lazy_provider.get_scope() == ContextScopes.APP
106+
107+
assert lazy_provider.supports_context_sync() is True
108+
109+
110+
async def test_lazy_provider_tear_down_async() -> None:
111+
lazy_provider = LazyProvider("tests.experimental.test_container_2.Container2.singleton_provider")
112+
assert lazy_provider.resolve_sync() == Container2.singleton_provider.resolve_sync()
113+
114+
await lazy_provider.tear_down()
115+
116+
assert await lazy_provider.resolve() == Container2.singleton_provider.resolve_sync()
117+
118+
119+
def test_lazy_provider_tear_down_sync() -> None:
120+
lazy_provider = LazyProvider("tests.experimental.test_container_2.Container2.singleton_provider")
121+
assert lazy_provider.resolve_sync() == Container2.singleton_provider.resolve_sync()
122+
123+
lazy_provider.tear_down_sync()
124+
125+
assert lazy_provider.resolve_sync() == Container2.singleton_provider.resolve_sync()
126+
127+
128+
async def test_lazy_provider_not_implemented() -> None:
129+
lazy_provider = Container2.obj_1
130+
with pytest.raises(NotImplementedError):
131+
lazy_provider.get_scope()
132+
with pytest.raises(NotImplementedError):
133+
lazy_provider.context_sync()
134+
with pytest.raises(NotImplementedError):
135+
lazy_provider.context_async()
136+
with pytest.raises(NotImplementedError):
137+
lazy_provider.tear_down_sync()
138+
with pytest.raises(NotImplementedError):
139+
await lazy_provider.tear_down()
140+
141+
142+
def test_lazy_provider_attr_getter() -> None:
143+
lazy_provider = LazyProvider("tests.experimental.test_container_2.Container2.sync_context_provider")
144+
with lazy_provider.context_sync(force=True):
145+
assert isinstance(lazy_provider.value.resolve_sync(), float)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import pytest
2+
3+
from that_depends.experimental import LazyProvider
4+
5+
6+
def test_lazy_provider_incorrect_initialization() -> None:
7+
with pytest.raises(
8+
ValueError,
9+
match=r"You must provide either import_string "
10+
"OR both module_string AND provider_string, but not both or neither.",
11+
):
12+
LazyProvider(module_string="3213") # type: ignore[call-overload]
13+
14+
with pytest.raises(ValueError, match=r"Invalid import_string ''"):
15+
LazyProvider("")
16+
17+
with pytest.raises(ValueError, match=r"Invalid provider_string ''"):
18+
LazyProvider(module_string="some.module", provider_string="")
19+
20+
with pytest.raises(ValueError, match=r"Invalid module_string '.'"):
21+
LazyProvider(module_string=".", provider_string="SomeProvider")
22+
23+
with pytest.raises(ValueError, match=r"Invalid import_string 'import.'"):
24+
LazyProvider("import.")
25+
26+
27+
def test_lazy_provider_incorrect_import_string() -> None:
28+
p = LazyProvider("some.random.path")
29+
with pytest.raises(ImportError):
30+
p.resolve_sync()
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"""Experimental features."""
2+
3+
from that_depends.experimental.providers import LazyProvider
4+
5+
6+
__all__ = [
7+
"LazyProvider",
8+
]

0 commit comments

Comments
 (0)