You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
- resolve the dependency only once and cache the resolved instance for future injections;
3
-
- unlike `Singleton` has finalization logic;
4
-
- generator or async generator can be used;
5
-
- context manager derived from `typing.ContextManager` or `typing.AsyncContextManager` can be used;
1
+
# Resource Provider
6
2
7
-
## How it works
8
-
```python
9
-
import typing
3
+
A **Resource** is a special provider that:
4
+
5
+
-**Resolves** its dependency only **once** and **caches** the resolved instance for future injections.
6
+
-**Includes** teardown (finalization) logic, unlike a plain `Singleton`.
7
+
-**Supports** generator or async generator functions for creation (allowing a `yield` plus teardown in `finally`).
8
+
-**Also** allows usage of classes that implement standard Python context managers (`typing.ContextManager` or `typing.AsyncContextManager`), but *does not* automatically integrate with `container_context`.
9
+
10
+
This makes `Resource` ideal for dependencies that need:
11
+
12
+
1. A **single creation** step,
13
+
2. A **single finalization** step,
14
+
3.**Thread/async safety**—all consumers receive the same resource object, and concurrency is handled.
15
+
16
+
---
10
17
11
-
from that_depends import BaseContainer, providers
18
+
## How It Works
12
19
20
+
### Defining a Sync or Async Resource
21
+
22
+
You can define your creation logic as either a **generator** or a **context manager** class (sync or async).
`Resource` is safe to use in threading and asyncio concurrency:
85
+
-**`sync_resolve()`** or **`async_resolve()`**: Creates (if needed) and returns the resource instance.
86
+
-**`sync_tear_down()`** or **`tear_down()`**: Closes/cleans up the resource (triggering your `finally` block or exiting the context manager) and resets the cached instance to `None`. A subsequent resolve call will then recreate it.
87
+
88
+
---
89
+
90
+
## Concurrency Safety
91
+
92
+
`Resource` is **safe** to use under **threading** and **asyncio** concurrency. Internally, a lock ensures only one resource instance is created per container:
93
+
94
+
- Multiple threads calling `sync_resolve()` simultaneously will produce a **single** instance for that container.
95
+
- Multiple coroutines calling `async_resolve()` simultaneously will likewise produce **only one** instance for that container in an async environment.
96
+
37
97
```python
38
-
# calling async_resolve concurrently in different coroutines will create only one instance
98
+
# Even if multiple coroutines call async_resolve in parallel,
99
+
# only one instance is created at a time:
39
100
await MyContainer.async_resource.async_resolve()
40
101
41
-
# calling sync_resolve concurrently in different threads will create only one instance
If your resource is a standard **context manager** or **async context manager** class, `Resource` will handle entering and exiting it under the hood. For example:
- resolve the dependency only once and cache the resolved instance for future injections;
1
+
# Singleton Provider
2
+
3
+
A **Singleton** provider creates its instance once and caches it for all future injections or resolutions. When the instance is first requested (via `sync_resolve()` or `async_resolve()`), the underlying factory is called. On subsequent calls, the cached instance is returned without calling the factory again.
4
+
5
+
## How it Works
3
6
4
-
## How it works
5
7
```python
6
8
import random
7
9
@@ -18,68 +20,109 @@ class MyContainer(BaseContainer):
18
20
singleton = providers.Singleton(some_function)
19
21
20
22
21
-
# provider will call `some_func` and cache the return value
22
-
MyContainer.singleton.sync_resolve() # 0.3
23
-
# provider with return the cached value
24
-
MyContainer.singleton.sync_resolve() # 0.3
23
+
# The provider will call `some_function` once and cache the return value
`Singleton` is safe to use in threading and asyncio concurrency:
39
+
### Teardown Support
40
+
If you need to reset the singleton (for example, in tests or at application shutdown), you can call:
41
+
```python
42
+
await MyContainer.singleton.tear_down()
43
+
```
44
+
This clears the cached instance, causing a new one to be created the next time `sync_resolve()` or `async_resolve()` is called.
45
+
*(If you only ever use synchronous resolution, you can call `MyContainer.singleton.sync_tear_down()` instead.)*
46
+
47
+
---
48
+
49
+
## Concurrency Safety
50
+
51
+
`Singleton` is **thread-safe** and **async-safe**:
52
+
53
+
1.**Async Concurrency**
54
+
If multiple coroutines call `async_resolve()` concurrently, the factory function is guaranteed to be called only once. All callers receive the same cached instance.
55
+
56
+
2.**Thread Concurrency**
57
+
If multiple threads call `sync_resolve()` at the same time, the factory is only called once. All threads receive the same cached instance.
58
+
37
59
```python
38
-
# calling async_resolve concurrently in different coroutines will create only one instance
39
-
await MyContainer.singleton.async_resolve()
60
+
import threading
61
+
import asyncio
62
+
63
+
# In async code:
64
+
asyncdefmain():
65
+
# calling async_resolve concurrently in different coroutines
66
+
results =await asyncio.gather(
67
+
MyContainer.singleton.async_resolve(),
68
+
MyContainer.singleton.async_resolve(),
69
+
)
70
+
# Both results point to the same instance
40
71
41
-
# calling sync_resolve concurrently in different threads will create only one instance
42
-
MyContainer.singleton.sync_resolve()
72
+
# In threaded code:
73
+
defthread_task():
74
+
instance = MyContainer.singleton.sync_resolve()
75
+
...
76
+
77
+
threads = [threading.Thread(target=thread_task) for _ inrange(5)]
78
+
for t in threads:
79
+
t.start()
43
80
```
44
-
## ThreadLocalSingleton
45
81
46
-
For cases when you need to have a separate instance for each thread, you can use `ThreadLocalSingleton` provider. It will create a new instance for each thread and cache it for future injections in the same thread.
82
+
---
83
+
84
+
## ThreadLocalSingleton Provider
85
+
86
+
If you want each *thread* to have its own, separately cached instance, use **ThreadLocalSingleton**. This provider creates a new instance per thread and reuses that instance on subsequent calls *within the same thread*.
47
87
48
88
```python
49
-
from that_depends.providers import ThreadLocalSingleton
50
-
import threading
51
89
import random
90
+
import threading
91
+
from that_depends.providers import ThreadLocalSingleton
92
+
52
93
53
-
# Define a factory function
54
94
deffactory() -> int:
95
+
"""Return a random int between 1 and 100."""
55
96
return random.randint(1, 100)
56
97
57
-
# Create a ThreadLocalSingleton instance
98
+
99
+
# ThreadLocalSingleton caches an instance per thread
58
100
singleton = ThreadLocalSingleton(factory)
59
101
60
-
#Same thread, same instance
61
-
instance1 = singleton.sync_resolve() # 56
62
-
instance2 = singleton.sync_resolve() # 56
102
+
#In a single thread:
103
+
instance1 = singleton.sync_resolve() # e.g. 56
104
+
instance2 = singleton.sync_resolve() # 56 (cached in the same thread)
63
105
64
-
#Example usage in multiple threads
106
+
#In multiple threads:
65
107
defthread_task():
66
-
instance = singleton.sync_resolve()
67
-
return instance
68
-
69
-
# Create and start threads
70
-
threads = [threading.Thread(target=thread_task) for i inrange(2)]
71
-
for thread in threads:
72
-
thread.start()
73
-
for thread in threads:
74
-
results = thread.join()
75
-
76
-
# Results will be different for each thread
77
-
print(results) # [56, 78]
108
+
return singleton.sync_resolve()
109
+
110
+
thread1 = threading.Thread(target=thread_task)
111
+
thread2 = threading.Thread(target=thread_task)
112
+
thread1.start()
113
+
thread2.start()
114
+
115
+
# thread1 and thread2 each get a different cached value
78
116
```
79
117
118
+
You can still use `.async_resolve()` with `ThreadLocalSingleton`, which will also maintain isolation per thread. However, note that this does *not* isolate instances per asynchronous Task – only per OS thread.
119
+
120
+
---
80
121
81
122
## Example with `pydantic-settings`
82
-
Let's say we are storing our application configuration using [pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/):
123
+
124
+
Consider a scenario where your application configuration is defined via [**pydantic-settings**](https://docs.pydantic.dev/latest/concepts/pydantic_settings/). Often, you only want to parse this configuration (e.g., from environment variables) once, then reuse it throughout the application.
125
+
83
126
```python
84
127
from pydantic_settings import BaseSettings
85
128
from pydantic import BaseModel
@@ -96,41 +139,51 @@ class Settings(BaseSettings):
96
139
db: DatabaseConfig = DatabaseConfig()
97
140
```
98
141
99
-
Because we do not want to resolve the configuration each time it is used in our application, we provide it using the `Singleton` provider.
142
+
### Defining the Container
143
+
144
+
Below, we define a container with a **Singleton** provider for our settings. We also define a separate async factory that connects to the database using those settings.
0 commit comments