omlish 0.0.0.dev493__py3-none-any.whl → 0.0.0.dev506__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of omlish might be problematic. Click here for more details.
- omlish/CODESTYLE.md +345 -0
- omlish/README.md +2 -2
- omlish/__about__.py +6 -4
- omlish/_check.cc +209 -0
- omlish/check.py +11 -0
- omlish/dataclasses/__init__.py +4 -0
- omlish/dataclasses/impl/concerns/frozen.py +4 -1
- omlish/dataclasses/tools/replace.py +27 -0
- omlish/dispatch/functions.py +1 -1
- omlish/formats/json/stream/lexing.py +13 -5
- omlish/formats/json/stream/parsing.py +1 -1
- omlish/inject/README.md +430 -0
- omlish/inject/__init__.py +1 -0
- omlish/inject/_dataclasses.py +64 -64
- omlish/inject/eagers.py +0 -4
- omlish/inject/elements.py +4 -0
- omlish/inject/helpers/late.py +1 -1
- omlish/inject/helpers/managed.py +27 -24
- omlish/inject/impl/injector.py +7 -22
- omlish/inject/impl/inspect.py +0 -8
- omlish/inject/impl/origins.py +1 -0
- omlish/inject/impl/privates.py +2 -6
- omlish/inject/impl/providers.py +0 -4
- omlish/inject/impl/scopes.py +14 -18
- omlish/inject/inspect.py +9 -0
- omlish/inject/multis.py +0 -3
- omlish/inject/scopes.py +7 -5
- omlish/io/buffers.py +35 -8
- omlish/lang/__init__.py +8 -0
- omlish/lang/classes/simple.py +2 -1
- omlish/lang/iterables.py +6 -0
- omlish/lang/objects.py +13 -0
- omlish/lang/outcomes.py +1 -1
- omlish/lang/recursion.py +1 -1
- omlish/lang/sequences.py +33 -0
- omlish/lifecycles/_dataclasses.py +18 -18
- omlish/lifecycles/injection.py +4 -4
- omlish/lite/maybes.py +7 -0
- omlish/lite/typing.py +15 -0
- omlish/logs/all.py +11 -0
- omlish/logs/base.py +3 -3
- omlish/logs/bisync.py +99 -0
- omlish/marshal/_dataclasses.py +32 -32
- omlish/specs/jmespath/_dataclasses.py +38 -38
- omlish/specs/jsonschema/keywords/_dataclasses.py +24 -24
- omlish/typedvalues/_collection.cc +500 -0
- omlish/typedvalues/collection.py +159 -62
- omlish/typedvalues/generic.py +5 -4
- omlish/typedvalues/values.py +6 -0
- {omlish-0.0.0.dev493.dist-info → omlish-0.0.0.dev506.dist-info}/METADATA +9 -7
- {omlish-0.0.0.dev493.dist-info → omlish-0.0.0.dev506.dist-info}/RECORD +55 -50
- {omlish-0.0.0.dev493.dist-info → omlish-0.0.0.dev506.dist-info}/WHEEL +0 -0
- {omlish-0.0.0.dev493.dist-info → omlish-0.0.0.dev506.dist-info}/entry_points.txt +0 -0
- {omlish-0.0.0.dev493.dist-info → omlish-0.0.0.dev506.dist-info}/licenses/LICENSE +0 -0
- {omlish-0.0.0.dev493.dist-info → omlish-0.0.0.dev506.dist-info}/top_level.txt +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import dataclasses as dc
|
|
2
|
+
import operator
|
|
2
3
|
import typing as ta
|
|
3
4
|
|
|
4
5
|
|
|
@@ -15,3 +16,29 @@ def deep_replace(o: T, *args: str | ta.Callable[[ta.Any], ta.Mapping[str, ta.Any
|
|
|
15
16
|
return dc.replace(o, **args[0](o)) # type: ignore
|
|
16
17
|
else:
|
|
17
18
|
return dc.replace(o, **{args[0]: deep_replace(getattr(o, args[0]), *args[1:])}) # type: ignore
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
##
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def replace_if(
|
|
25
|
+
o: T,
|
|
26
|
+
fn: ta.Callable[[ta.Any, ta.Any], bool] = operator.eq,
|
|
27
|
+
/,
|
|
28
|
+
**kwargs: ta.Any,
|
|
29
|
+
) -> T:
|
|
30
|
+
dct: dict[str, ta.Any] = {}
|
|
31
|
+
for k, v in kwargs.items():
|
|
32
|
+
if fn(getattr(o, k), v):
|
|
33
|
+
dct[k] = v # noqa
|
|
34
|
+
if not dct:
|
|
35
|
+
return o
|
|
36
|
+
return dc.replace(o, **dct) # type: ignore
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def replace_ne(o: T, **kwargs: ta.Any) -> T:
|
|
40
|
+
return replace_if(o, operator.ne, **kwargs)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def replace_is_not(o: T, **kwargs: ta.Any) -> T:
|
|
44
|
+
return replace_if(o, operator.is_not, **kwargs)
|
omlish/dispatch/functions.py
CHANGED
|
@@ -43,7 +43,7 @@ def function(func): # noqa
|
|
|
43
43
|
wrapper.dispatch = disp.dispatch # type: ignore
|
|
44
44
|
|
|
45
45
|
else:
|
|
46
|
-
from x.c._dispatch import function_wrapper #
|
|
46
|
+
from x.c._dispatch import function_wrapper # type: ignore
|
|
47
47
|
wrapper = function_wrapper(
|
|
48
48
|
disp.dispatch,
|
|
49
49
|
**{k: getattr(func, k) for k in functools.WRAPPER_ASSIGNMENTS if hasattr(func, k)},
|
|
@@ -338,7 +338,6 @@ class JsonStreamLexer(GenMachine[str, Token]):
|
|
|
338
338
|
self._line = line
|
|
339
339
|
self._col = col
|
|
340
340
|
|
|
341
|
-
last = None
|
|
342
341
|
while True:
|
|
343
342
|
c: str | None = None
|
|
344
343
|
|
|
@@ -358,7 +357,7 @@ class JsonStreamLexer(GenMachine[str, Token]):
|
|
|
358
357
|
ofs += skip_to - char_in_str_pos
|
|
359
358
|
if (np := char_in_str.rfind('\n', char_in_str_pos, skip_to)) >= 0:
|
|
360
359
|
line += char_in_str.count('\n', char_in_str_pos, skip_to)
|
|
361
|
-
col = np -
|
|
360
|
+
col = skip_to - np - 1
|
|
362
361
|
else:
|
|
363
362
|
col += skip_to - char_in_str_pos
|
|
364
363
|
buf.write(char_in_str[char_in_str_pos:skip_to])
|
|
@@ -406,9 +405,18 @@ class JsonStreamLexer(GenMachine[str, Token]):
|
|
|
406
405
|
self._raise(f'Unterminated string literal: {buf.getvalue()}')
|
|
407
406
|
|
|
408
407
|
buf.write(c)
|
|
409
|
-
if c == q
|
|
410
|
-
|
|
411
|
-
|
|
408
|
+
if c == q:
|
|
409
|
+
# Count consecutive backslashes before this quote
|
|
410
|
+
backslash_count = 0
|
|
411
|
+
buf_val = buf.getvalue()
|
|
412
|
+
check_pos = len(buf_val) - 2 # -2 because we just wrote the quote
|
|
413
|
+
while check_pos >= 0 and buf_val[check_pos] == '\\':
|
|
414
|
+
backslash_count += 1
|
|
415
|
+
check_pos -= 1
|
|
416
|
+
|
|
417
|
+
# Quote is escaped only if preceded by odd number of backslashes
|
|
418
|
+
if backslash_count % 2 == 0:
|
|
419
|
+
break
|
|
412
420
|
|
|
413
421
|
restore_state()
|
|
414
422
|
|
|
@@ -215,7 +215,7 @@ class JsonStreamParser(GenMachine[Token, Event]):
|
|
|
215
215
|
except GeneratorExit:
|
|
216
216
|
raise JsonStreamParseError('Expected object body') from None
|
|
217
217
|
|
|
218
|
-
if tok.kind == 'STRING' or (self.
|
|
218
|
+
if tok.kind == 'STRING' or (self._allow_extended_idents and tok.kind == 'IDENT'):
|
|
219
219
|
k = tok.value
|
|
220
220
|
|
|
221
221
|
try:
|
omlish/inject/README.md
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
# omlish.inject
|
|
2
|
+
|
|
3
|
+
A Guice-inspired dependency injection system for Python with first-class async support and a focus on type safety.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
`omlish.inject` provides constructor-based dependency injection with explicit, declarative configuration. Like Guice, it
|
|
8
|
+
emphasizes immutability, composition over inheritance, and pure constructor injection. Unlike Guice, it embraces
|
|
9
|
+
Python's native type system, provides native async/await support, and uses a functional elements-based configuration
|
|
10
|
+
model.
|
|
11
|
+
|
|
12
|
+
The system is built around three core concepts:
|
|
13
|
+
- **Keys** identify dependencies by type and optional tag
|
|
14
|
+
- **Bindings** associate keys with providers
|
|
15
|
+
- **Injectors** resolve and provide instances
|
|
16
|
+
|
|
17
|
+
## Core API
|
|
18
|
+
|
|
19
|
+
### Keys
|
|
20
|
+
|
|
21
|
+
A `Key` identifies a dependency by its type and an optional tag for disambiguation:
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from omlish import inject as inj
|
|
25
|
+
|
|
26
|
+
# Simple key by type
|
|
27
|
+
int_key = inj.Key(int)
|
|
28
|
+
|
|
29
|
+
# Tagged key for multiple bindings of the same type
|
|
30
|
+
db_conn_key = inj.Key(DbConnection, tag='primary')
|
|
31
|
+
cache_conn_key = inj.Key(DbConnection, tag='cache')
|
|
32
|
+
|
|
33
|
+
# Convert any type to a key
|
|
34
|
+
key = inj.as_key(MyService)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Bindings
|
|
38
|
+
|
|
39
|
+
Bindings connect keys to providers. The `bind()` function provides a concise API for most use cases:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
# Bind a constant
|
|
43
|
+
inj.bind(420)
|
|
44
|
+
|
|
45
|
+
# Bind a type to its constructor
|
|
46
|
+
inj.bind(UserService)
|
|
47
|
+
|
|
48
|
+
# Bind to a factory function
|
|
49
|
+
def make_conn(cfg: Config) -> Connection:
|
|
50
|
+
return create_connection(cfg)
|
|
51
|
+
inj.bind(make_conn)
|
|
52
|
+
|
|
53
|
+
# Bind to another key (linking)
|
|
54
|
+
inj.bind(Service, to_key=ServiceImpl)
|
|
55
|
+
|
|
56
|
+
# Tagged binding
|
|
57
|
+
inj.bind(420, tag='port')
|
|
58
|
+
|
|
59
|
+
# Scoped binding
|
|
60
|
+
inj.bind(Database, singleton=True)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Providers
|
|
64
|
+
|
|
65
|
+
Providers define how to construct instances. The system includes several built-in provider types:
|
|
66
|
+
|
|
67
|
+
- `ConstProvider` - returns a constant value
|
|
68
|
+
- `CtorProvider` - calls a constructor
|
|
69
|
+
- `FnProvider` - calls a function
|
|
70
|
+
- `AsyncFnProvider` - calls an async function
|
|
71
|
+
- `LinkProvider` - delegates to another key
|
|
72
|
+
|
|
73
|
+
Providers are typically created implicitly through `bind()`, but can be used directly for advanced cases.
|
|
74
|
+
|
|
75
|
+
### Injectors
|
|
76
|
+
|
|
77
|
+
Injectors resolve dependencies and provide instances:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
# Create an injector
|
|
81
|
+
injector = inj.create_injector(
|
|
82
|
+
inj.bind(420),
|
|
83
|
+
inj.bind(str, to_fn=inj.KwargsTarget.of(lambda i: f'Port: {i}', i=int)),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Provide instances
|
|
87
|
+
port = injector[int] # 420
|
|
88
|
+
msg = injector.provide(str) # 'Port: 420'
|
|
89
|
+
|
|
90
|
+
# Check if a key is bound
|
|
91
|
+
maybe_val = injector.try_provide(SomeService)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
For async code, use `AsyncInjector`:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
async_injector = await inj.create_async_injector(
|
|
98
|
+
inj.bind(async_factory),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
service = await async_injector[MyService]
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Elements
|
|
105
|
+
|
|
106
|
+
`Elements` are the building blocks of injector configuration. Most binding functions return `Element` or `Elements`:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
# Combine multiple elements
|
|
110
|
+
config = inj.as_elements(
|
|
111
|
+
inj.bind(DatabaseConfig(host='localhost')),
|
|
112
|
+
inj.bind(Database, singleton=True),
|
|
113
|
+
inj.bind(UserRepository),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Elements can be reused
|
|
117
|
+
injector1 = inj.create_injector(config)
|
|
118
|
+
injector2 = inj.create_injector(config)
|
|
119
|
+
|
|
120
|
+
# Collect elements once for efficiency
|
|
121
|
+
collected = inj.collect_elements(config)
|
|
122
|
+
injector3 = inj.create_injector(collected)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Scopes
|
|
126
|
+
|
|
127
|
+
Scopes control instance lifecycle:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
# Singleton scope - one instance per injector
|
|
131
|
+
inj.bind(Database, singleton=True)
|
|
132
|
+
inj.bind(Cache, in_='singleton')
|
|
133
|
+
|
|
134
|
+
# Thread scope - one instance per thread
|
|
135
|
+
inj.bind(RequestContext, in_='thread')
|
|
136
|
+
|
|
137
|
+
# Seeded scope - manually seeded instances
|
|
138
|
+
request_scope = inj.SeededScope('request')
|
|
139
|
+
inj.bind_scope(request_scope)
|
|
140
|
+
inj.bind(UserId, in_=request_scope)
|
|
141
|
+
inj.bind_scope_seed(RequestContext, request_scope)
|
|
142
|
+
|
|
143
|
+
with inj.enter_seeded_scope(injector, request_scope, {
|
|
144
|
+
inj.as_key(RequestContext): ctx,
|
|
145
|
+
}):
|
|
146
|
+
user_id = injector[UserId]
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Differences from Guice
|
|
150
|
+
|
|
151
|
+
While inspired by Guice, `omlish.inject` differs in several key ways:
|
|
152
|
+
|
|
153
|
+
### Type System
|
|
154
|
+
- Uses Python's native `typing` module instead of Java generics
|
|
155
|
+
- No annotation-based injection (no `@Inject` decorator)
|
|
156
|
+
- Type annotations on constructors/functions drive automatic injection
|
|
157
|
+
|
|
158
|
+
### Async Support
|
|
159
|
+
- Native `AsyncInjector` for async dependency graphs
|
|
160
|
+
- `AsyncFnProvider` for async factories
|
|
161
|
+
- Async-aware scopes and lifecycle management
|
|
162
|
+
|
|
163
|
+
### Configuration Model
|
|
164
|
+
- `Elements`-based functional composition instead of module classes
|
|
165
|
+
- Functions return `Elements` rather than imperative `configure()` methods
|
|
166
|
+
- More flexible composition and reuse
|
|
167
|
+
|
|
168
|
+
### Dependency Resolution
|
|
169
|
+
- Automatic kwarg inspection via `KwargsTarget`
|
|
170
|
+
- Supports optional dependencies (parameters with defaults)
|
|
171
|
+
- Explicit async/sync separation
|
|
172
|
+
|
|
173
|
+
### Python Idioms
|
|
174
|
+
- `__getitem__` syntax for common case: `injector[MyService]`
|
|
175
|
+
- Context managers for lifecycle and scoped execution
|
|
176
|
+
- Dataclasses as natural config objects
|
|
177
|
+
|
|
178
|
+
## Common Idioms
|
|
179
|
+
|
|
180
|
+
### Hierarchical Package-Level Modules
|
|
181
|
+
|
|
182
|
+
Organize bindings into composable, package-level functions that mirror your application structure:
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
# app/services/inject.py
|
|
186
|
+
def bind_services(cfg: ServicesConfig) -> inj.Elements:
|
|
187
|
+
return inj.as_elements(
|
|
188
|
+
inj.bind(UserService, singleton=True),
|
|
189
|
+
inj.bind(AuthService, singleton=True),
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# app/database/inject.py
|
|
193
|
+
def bind_database(cfg: DatabaseConfig) -> inj.Elements:
|
|
194
|
+
return inj.as_elements(
|
|
195
|
+
inj.bind(cfg),
|
|
196
|
+
inj.bind(Database, singleton=True),
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# app/inject.py
|
|
200
|
+
def bind_app(cfg: AppConfig) -> inj.Elements:
|
|
201
|
+
return inj.as_elements(
|
|
202
|
+
bind_database(cfg.database),
|
|
203
|
+
bind_services(cfg.services),
|
|
204
|
+
)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
This pattern enables:
|
|
208
|
+
- Clear separation of concerns
|
|
209
|
+
- Easy testing of subsystems
|
|
210
|
+
- Configuration composition
|
|
211
|
+
- Lazy imports (using `lang.auto_proxy_import`)
|
|
212
|
+
|
|
213
|
+
### Multi-Bindings
|
|
214
|
+
|
|
215
|
+
Collect multiple bindings into sets or maps:
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
# Set bindings
|
|
219
|
+
inj.set_binder[Plugin]().bind(
|
|
220
|
+
inj.Key(Plugin, tag='auth'),
|
|
221
|
+
inj.Key(Plugin, tag='logging'),
|
|
222
|
+
)
|
|
223
|
+
plugins = injector[ta.AbstractSet[Plugin]]
|
|
224
|
+
|
|
225
|
+
# Map bindings
|
|
226
|
+
inj.map_binder[str, Handler]().bind('GET', GetHandler)
|
|
227
|
+
inj.map_binder[str, Handler]().bind('POST', PostHandler)
|
|
228
|
+
handlers = injector[ta.Mapping[str, Handler]]
|
|
229
|
+
|
|
230
|
+
# Helper for const entries
|
|
231
|
+
inj.bind_set_entry_const(ta.AbstractSet[str], 'value1')
|
|
232
|
+
inj.bind_map_entry_const(ta.Mapping[str, int], 'key', 42)
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Private Modules
|
|
236
|
+
|
|
237
|
+
Encapsulate internal bindings while exposing only specific keys:
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
inj.private(
|
|
241
|
+
inj.bind('jdbc:postgresql://internal-db'),
|
|
242
|
+
inj.bind(DbConnection, singleton=True),
|
|
243
|
+
inj.bind(UserRepository, expose=True),
|
|
244
|
+
)
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Only `UserRepository` is visible to the parent injector; the connection string and `DbConnection` remain private.
|
|
248
|
+
|
|
249
|
+
### Overrides
|
|
250
|
+
|
|
251
|
+
Replace bindings for testing or configuration:
|
|
252
|
+
|
|
253
|
+
```python
|
|
254
|
+
production = inj.bind(Database, to_ctor=PostgresDatabase)
|
|
255
|
+
|
|
256
|
+
testing = inj.override(
|
|
257
|
+
production,
|
|
258
|
+
inj.bind(Database, to_ctor=InMemoryDatabase),
|
|
259
|
+
)
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Late Bindings
|
|
263
|
+
|
|
264
|
+
Break circular dependencies by injecting factories:
|
|
265
|
+
|
|
266
|
+
```python
|
|
267
|
+
class ServiceA:
|
|
268
|
+
def __init__(self, b: 'ServiceB') -> None: ...
|
|
269
|
+
|
|
270
|
+
class ServiceB:
|
|
271
|
+
def __init__(self, c: 'ServiceC') -> None: ...
|
|
272
|
+
|
|
273
|
+
class ServiceC:
|
|
274
|
+
def __init__(self, a: inj.Late[ServiceA]) -> None:
|
|
275
|
+
self.get_a = a # Callable that returns ServiceA
|
|
276
|
+
|
|
277
|
+
inj.create_injector(
|
|
278
|
+
inj.bind(ServiceA, singleton=True),
|
|
279
|
+
inj.bind(ServiceB, singleton=True),
|
|
280
|
+
inj.bind(ServiceC, singleton=True),
|
|
281
|
+
inj.bind_late(ServiceA),
|
|
282
|
+
)
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Managed Providers
|
|
286
|
+
|
|
287
|
+
Integrate with context managers for lifecycle management:
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
# Automatic context manager handling
|
|
291
|
+
with inj.create_managed_injector(
|
|
292
|
+
inj.bind(
|
|
293
|
+
Database,
|
|
294
|
+
singleton=True,
|
|
295
|
+
to_fn=inj.make_managed_provider(Database),
|
|
296
|
+
),
|
|
297
|
+
) as injector:
|
|
298
|
+
db = injector[Database]
|
|
299
|
+
# Database.__enter__ called automatically
|
|
300
|
+
# Database.__exit__ called on scope exit
|
|
301
|
+
|
|
302
|
+
# Wrap with custom lifecycle
|
|
303
|
+
inj.bind(
|
|
304
|
+
Resource,
|
|
305
|
+
to_fn=inj.make_managed_provider(
|
|
306
|
+
create_resource,
|
|
307
|
+
contextlib.closing,
|
|
308
|
+
),
|
|
309
|
+
)
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### KwargsTarget
|
|
313
|
+
|
|
314
|
+
Explicitly control function parameter injection:
|
|
315
|
+
|
|
316
|
+
```python
|
|
317
|
+
# Manual kwarg specification
|
|
318
|
+
target = inj.KwargsTarget.of(
|
|
319
|
+
my_function,
|
|
320
|
+
db=DatabaseKey,
|
|
321
|
+
cache=(CacheKey, True), # has_default=True
|
|
322
|
+
)
|
|
323
|
+
inj.bind(MyService, to_fn=target)
|
|
324
|
+
|
|
325
|
+
# Decorator syntax
|
|
326
|
+
@inj.target(db=Database, cache=Cache)
|
|
327
|
+
def create_service(db, cache):
|
|
328
|
+
return Service(db, cache)
|
|
329
|
+
|
|
330
|
+
inj.bind(Service, to_fn=create_service)
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### Wrapper Binder Helper
|
|
334
|
+
|
|
335
|
+
Build decorator chains of providers:
|
|
336
|
+
|
|
337
|
+
```python
|
|
338
|
+
stack = inj.wrapper_binder_helper(Service)
|
|
339
|
+
|
|
340
|
+
# Each layer wraps the previous
|
|
341
|
+
stack.push_bind(to_ctor=LoggingServiceWrapper)
|
|
342
|
+
stack.push_bind(to_ctor=RetryServiceWrapper)
|
|
343
|
+
stack.push_bind(to_ctor=MetricsServiceWrapper)
|
|
344
|
+
|
|
345
|
+
# Final binding gets the fully-wrapped stack
|
|
346
|
+
inj.bind(Service, to_key=stack.top)
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### Items Binder Helper
|
|
350
|
+
|
|
351
|
+
Collect and aggregate items across multiple bindings:
|
|
352
|
+
|
|
353
|
+
```python
|
|
354
|
+
Plugins = ta.NewType('Plugins', ta.Sequence[Plugin])
|
|
355
|
+
helper = inj.items_binder_helper[Plugin](Plugins)
|
|
356
|
+
|
|
357
|
+
inj.as_elements(
|
|
358
|
+
helper.bind_items_provider(),
|
|
359
|
+
helper.bind_item_consts(plugin1, plugin2),
|
|
360
|
+
helper.bind_item(to_ctor=DynamicPlugin),
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
plugins = injector[Plugins] # All collected plugins
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Eager Bindings
|
|
367
|
+
|
|
368
|
+
Force instantiation at injector creation:
|
|
369
|
+
|
|
370
|
+
```python
|
|
371
|
+
# Start background tasks immediately
|
|
372
|
+
inj.bind(BackgroundWorker, singleton=True, eager=True)
|
|
373
|
+
|
|
374
|
+
# Control initialization order with priority
|
|
375
|
+
inj.bind(Database, singleton=True, eager=1)
|
|
376
|
+
inj.bind(Cache, singleton=True, eager=2)
|
|
377
|
+
inj.bind(Service, singleton=True, eager=3)
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### Provision Listeners
|
|
381
|
+
|
|
382
|
+
Intercept instance provision for logging, metrics, or instrumentation:
|
|
383
|
+
|
|
384
|
+
```python
|
|
385
|
+
async def log_provisions(
|
|
386
|
+
injector: inj.AsyncInjector,
|
|
387
|
+
key: inj.Key,
|
|
388
|
+
binding: inj.Binding | None,
|
|
389
|
+
provide: ta.Callable[[], ta.Awaitable[ta.Any]],
|
|
390
|
+
) -> ta.Any:
|
|
391
|
+
start = time.time()
|
|
392
|
+
result = await provide()
|
|
393
|
+
duration = time.time() - start
|
|
394
|
+
log.info(f'Provided {key} in {duration:.3f}s')
|
|
395
|
+
return result
|
|
396
|
+
|
|
397
|
+
inj.bind_provision_listener(log_provisions)
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
## Error Handling
|
|
401
|
+
|
|
402
|
+
The system provides clear error types for common issues:
|
|
403
|
+
|
|
404
|
+
- `UnboundKeyError` - no binding exists for a requested key
|
|
405
|
+
- `ConflictingKeyError` - multiple conflicting bindings for the same key
|
|
406
|
+
- `CyclicDependencyError` - circular dependency detected
|
|
407
|
+
- `ScopeError` - scope-related errors (not open, already open)
|
|
408
|
+
|
|
409
|
+
## Advanced Topics
|
|
410
|
+
|
|
411
|
+
### MaysyncInjector
|
|
412
|
+
|
|
413
|
+
For code that supports both sync and async execution:
|
|
414
|
+
|
|
415
|
+
```python
|
|
416
|
+
maysync_injector = inj.create_maysync_injector(...)
|
|
417
|
+
# Can be used in both sync and async contexts
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### Custom Scopes
|
|
421
|
+
|
|
422
|
+
Define application-specific scopes by implementing the `Scope` interface and registering with `bind_scope()`.
|
|
423
|
+
|
|
424
|
+
### Origins
|
|
425
|
+
|
|
426
|
+
Track binding sources for debugging with the `HasOrigins` interface and `Origin` metadata.
|
|
427
|
+
|
|
428
|
+
### Inspection
|
|
429
|
+
|
|
430
|
+
Use `inj.tag()` and `inj.build_kwargs_target()` for runtime parameter inspection and custom injection logic.
|