anydi 0.55.0__py3-none-any.whl → 0.56.0__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.
- anydi/_container.py +14 -9
- anydi/_provider.py +1 -0
- anydi/_resolver.py +236 -125
- anydi/testing.py +34 -113
- anydi-0.56.0.dist-info/METADATA +267 -0
- {anydi-0.55.0.dist-info → anydi-0.56.0.dist-info}/RECORD +8 -8
- anydi-0.55.0.dist-info/METADATA +0 -193
- {anydi-0.55.0.dist-info → anydi-0.56.0.dist-info}/WHEEL +0 -0
- {anydi-0.55.0.dist-info → anydi-0.56.0.dist-info}/entry_points.txt +0 -0
anydi/_container.py
CHANGED
|
@@ -339,6 +339,7 @@ class Container:
|
|
|
339
339
|
default=default,
|
|
340
340
|
has_default=has_default,
|
|
341
341
|
provider=sub_provider,
|
|
342
|
+
shared_scope=sub_provider.scope == scope and scope != "transient",
|
|
342
343
|
)
|
|
343
344
|
)
|
|
344
345
|
|
|
@@ -389,10 +390,10 @@ class Container:
|
|
|
389
390
|
@staticmethod
|
|
390
391
|
def _validate_provider_scope(scope: Scope, name: str, is_resource: bool) -> None:
|
|
391
392
|
"""Validate the provider scope."""
|
|
392
|
-
if scope not in
|
|
393
|
+
if scope not in ALLOWED_SCOPES:
|
|
393
394
|
raise ValueError(
|
|
394
395
|
f"The provider `{name}` scope is invalid. Only the following "
|
|
395
|
-
f"scopes are supported: {', '.join(
|
|
396
|
+
f"scopes are supported: {', '.join(ALLOWED_SCOPES)}. "
|
|
396
397
|
"Please use one of the supported scopes when registering a provider."
|
|
397
398
|
)
|
|
398
399
|
if scope == "transient" and is_resource:
|
|
@@ -670,13 +671,17 @@ class Container:
|
|
|
670
671
|
) -> None:
|
|
671
672
|
self._scanner.scan(packages=packages, tags=tags)
|
|
672
673
|
|
|
673
|
-
# == Testing ==
|
|
674
|
+
# == Testing / Override Support ==
|
|
674
675
|
|
|
675
676
|
@contextlib.contextmanager
|
|
676
677
|
def override(self, interface: Any, instance: Any) -> Iterator[None]:
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
)
|
|
678
|
+
"""Override a dependency with a specific instance for testing."""
|
|
679
|
+
if not self.has_provider_for(interface):
|
|
680
|
+
raise LookupError(
|
|
681
|
+
f"The provider interface `{type_repr(interface)}` not registered."
|
|
682
|
+
)
|
|
683
|
+
self._resolver.add_override(interface, instance)
|
|
684
|
+
try:
|
|
685
|
+
yield
|
|
686
|
+
finally:
|
|
687
|
+
self._resolver.remove_override(interface)
|
anydi/_provider.py
CHANGED
anydi/_resolver.py
CHANGED
|
@@ -6,6 +6,7 @@ import contextlib
|
|
|
6
6
|
from typing import TYPE_CHECKING, Any, NamedTuple
|
|
7
7
|
|
|
8
8
|
import anyio.to_thread
|
|
9
|
+
import wrapt # type: ignore
|
|
9
10
|
from typing_extensions import type_repr
|
|
10
11
|
|
|
11
12
|
from ._provider import Provider
|
|
@@ -15,6 +16,18 @@ if TYPE_CHECKING:
|
|
|
15
16
|
from ._container import Container
|
|
16
17
|
|
|
17
18
|
|
|
19
|
+
class InstanceProxy(wrapt.ObjectProxy): # type: ignore
|
|
20
|
+
"""Proxy for dependency instances to enable override support."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, wrapped: Any, *, interface: type[Any]) -> None:
|
|
23
|
+
super().__init__(wrapped) # type: ignore
|
|
24
|
+
self._self_interface = interface
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def interface(self) -> type[Any]:
|
|
28
|
+
return self._self_interface
|
|
29
|
+
|
|
30
|
+
|
|
18
31
|
class CompiledResolver(NamedTuple):
|
|
19
32
|
resolve: Any
|
|
20
33
|
create: Any
|
|
@@ -24,32 +37,46 @@ class Resolver:
|
|
|
24
37
|
def __init__(self, container: Container) -> None:
|
|
25
38
|
self._container = container
|
|
26
39
|
self._unresolved_interfaces: set[Any] = set()
|
|
40
|
+
# Normal caches (fast path, no override checks)
|
|
27
41
|
self._cache: dict[Any, CompiledResolver] = {}
|
|
28
42
|
self._async_cache: dict[Any, CompiledResolver] = {}
|
|
43
|
+
# Override caches (with override support)
|
|
44
|
+
self._override_cache: dict[Any, CompiledResolver] = {}
|
|
45
|
+
self._async_override_cache: dict[Any, CompiledResolver] = {}
|
|
46
|
+
# Override instances storage
|
|
47
|
+
self._override_instances: dict[Any, Any] = {}
|
|
29
48
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
self.
|
|
38
|
-
|
|
39
|
-
|
|
49
|
+
@property
|
|
50
|
+
def override_mode(self) -> bool:
|
|
51
|
+
"""Check if override mode is enabled."""
|
|
52
|
+
return bool(self._override_instances)
|
|
53
|
+
|
|
54
|
+
def add_override(self, interface: Any, instance: Any) -> None:
|
|
55
|
+
"""Add an override instance for an interface."""
|
|
56
|
+
self._override_instances[interface] = instance
|
|
57
|
+
|
|
58
|
+
def remove_override(self, interface: Any) -> None:
|
|
59
|
+
"""Remove an override instance for an interface."""
|
|
60
|
+
self._override_instances.pop(interface, None)
|
|
40
61
|
|
|
41
62
|
def add_unresolved(self, interface: Any) -> None:
|
|
42
63
|
self._unresolved_interfaces.add(interface)
|
|
43
64
|
|
|
44
65
|
def get_cached(self, interface: Any, *, is_async: bool) -> CompiledResolver | None:
|
|
45
66
|
"""Get cached resolver if it exists."""
|
|
46
|
-
|
|
67
|
+
if self.override_mode:
|
|
68
|
+
cache = self._async_override_cache if is_async else self._override_cache
|
|
69
|
+
else:
|
|
70
|
+
cache = self._async_cache if is_async else self._cache
|
|
47
71
|
return cache.get(interface)
|
|
48
72
|
|
|
49
73
|
def compile(self, provider: Provider, *, is_async: bool) -> CompiledResolver:
|
|
50
74
|
"""Compile an optimized resolver function for the given provider."""
|
|
51
|
-
# Select the appropriate cache based on sync/async mode
|
|
52
|
-
|
|
75
|
+
# Select the appropriate cache based on sync/async mode and override mode
|
|
76
|
+
if self.override_mode:
|
|
77
|
+
cache = self._async_override_cache if is_async else self._override_cache
|
|
78
|
+
else:
|
|
79
|
+
cache = self._async_cache if is_async else self._cache
|
|
53
80
|
|
|
54
81
|
# Check if already compiled in cache
|
|
55
82
|
if provider.interface in cache:
|
|
@@ -61,35 +88,79 @@ class Resolver:
|
|
|
61
88
|
self.compile(p.provider, is_async=is_async)
|
|
62
89
|
|
|
63
90
|
# Compile the resolver and creator functions
|
|
64
|
-
compiled = self._compile_resolver(
|
|
91
|
+
compiled = self._compile_resolver(
|
|
92
|
+
provider, is_async=is_async, with_override=self.override_mode
|
|
93
|
+
)
|
|
65
94
|
|
|
66
95
|
# Store the compiled functions in the cache
|
|
67
96
|
cache[provider.interface] = compiled
|
|
68
97
|
|
|
69
98
|
return compiled
|
|
70
99
|
|
|
100
|
+
def _add_override_check(
|
|
101
|
+
self, lines: list[str], *, include_not_set: bool = False
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Add override checking code to generated resolver."""
|
|
104
|
+
lines.append(" override_mode = resolver.override_mode")
|
|
105
|
+
lines.append(" if override_mode:")
|
|
106
|
+
if include_not_set:
|
|
107
|
+
lines.append(" NOT_SET_ = _NOT_SET")
|
|
108
|
+
lines.append(" override = resolver._get_override_for(_interface)")
|
|
109
|
+
lines.append(" if override is not NOT_SET_:")
|
|
110
|
+
lines.append(" return override")
|
|
111
|
+
|
|
112
|
+
def _add_create_call(
|
|
113
|
+
self,
|
|
114
|
+
lines: list[str],
|
|
115
|
+
*,
|
|
116
|
+
is_async: bool,
|
|
117
|
+
with_override: bool,
|
|
118
|
+
context: str,
|
|
119
|
+
store: bool,
|
|
120
|
+
defaults: str = "None",
|
|
121
|
+
indent: str = " ",
|
|
122
|
+
) -> None:
|
|
123
|
+
"""Add _create_instance call to generated resolver."""
|
|
124
|
+
override_arg = "override_mode" if with_override else "False"
|
|
125
|
+
context_arg = context if context else "None"
|
|
126
|
+
store_arg = "True" if store else "False"
|
|
127
|
+
|
|
128
|
+
if is_async:
|
|
129
|
+
lines.append(
|
|
130
|
+
f"{indent}return await _create_instance("
|
|
131
|
+
f"container, {context_arg}, {store_arg}, {defaults}, {override_arg})"
|
|
132
|
+
)
|
|
133
|
+
else:
|
|
134
|
+
lines.append(
|
|
135
|
+
f"{indent}return _create_instance("
|
|
136
|
+
f"container, {context_arg}, {store_arg}, {defaults}, {override_arg})"
|
|
137
|
+
)
|
|
138
|
+
|
|
71
139
|
def _compile_resolver( # noqa: C901
|
|
72
|
-
self, provider: Provider, *, is_async: bool
|
|
140
|
+
self, provider: Provider, *, is_async: bool, with_override: bool = False
|
|
73
141
|
) -> CompiledResolver:
|
|
74
142
|
"""Compile optimized resolver functions for the given provider."""
|
|
75
|
-
has_override_support = self._has_override_support
|
|
76
|
-
wrap_dependencies = self._wrap_dependencies
|
|
77
|
-
wrap_instance = self._wrap_instance
|
|
78
143
|
num_params = len(provider.parameters)
|
|
79
144
|
param_resolvers: list[Any] = [None] * num_params
|
|
80
145
|
param_annotations: list[Any] = [None] * num_params
|
|
81
146
|
param_defaults: list[Any] = [None] * num_params
|
|
82
147
|
param_has_default: list[bool] = [False] * num_params
|
|
83
148
|
param_names: list[str] = [""] * num_params
|
|
149
|
+
param_shared_scopes: list[bool] = [False] * num_params
|
|
84
150
|
unresolved_messages: list[str] = [""] * num_params
|
|
85
151
|
|
|
86
|
-
cache =
|
|
152
|
+
cache = (
|
|
153
|
+
(self._async_override_cache if is_async else self._override_cache)
|
|
154
|
+
if with_override
|
|
155
|
+
else (self._async_cache if is_async else self._cache)
|
|
156
|
+
)
|
|
87
157
|
|
|
88
158
|
for idx, p in enumerate(provider.parameters):
|
|
89
159
|
param_annotations[idx] = p.annotation
|
|
90
160
|
param_defaults[idx] = p.default
|
|
91
161
|
param_has_default[idx] = p.has_default
|
|
92
162
|
param_names[idx] = p.name
|
|
163
|
+
param_shared_scopes[idx] = p.shared_scope
|
|
93
164
|
|
|
94
165
|
if p.provider is not None:
|
|
95
166
|
compiled = cache.get(p.provider.interface)
|
|
@@ -98,13 +169,13 @@ class Resolver:
|
|
|
98
169
|
cache[p.provider.interface] = compiled
|
|
99
170
|
param_resolvers[idx] = compiled.resolve
|
|
100
171
|
|
|
101
|
-
|
|
172
|
+
unresolved_message = (
|
|
102
173
|
f"You are attempting to get the parameter `{p.name}` with the "
|
|
103
174
|
f"annotation `{type_repr(p.annotation)}` as a dependency into "
|
|
104
175
|
f"`{type_repr(provider.call)}` which is not registered or set in the "
|
|
105
176
|
"scoped context."
|
|
106
177
|
)
|
|
107
|
-
unresolved_messages[idx] =
|
|
178
|
+
unresolved_messages[idx] = unresolved_message
|
|
108
179
|
|
|
109
180
|
scope = provider.scope
|
|
110
181
|
is_generator = provider.is_generator
|
|
@@ -115,11 +186,13 @@ class Resolver:
|
|
|
115
186
|
create_lines: list[str] = []
|
|
116
187
|
if is_async:
|
|
117
188
|
create_lines.append(
|
|
118
|
-
"async def _create_instance(
|
|
189
|
+
"async def _create_instance("
|
|
190
|
+
"container, context, store, defaults, override_mode):"
|
|
119
191
|
)
|
|
120
192
|
else:
|
|
121
193
|
create_lines.append(
|
|
122
|
-
"def _create_instance(
|
|
194
|
+
"def _create_instance("
|
|
195
|
+
"container, context, store, defaults, override_mode):"
|
|
123
196
|
)
|
|
124
197
|
|
|
125
198
|
if no_params:
|
|
@@ -168,8 +241,10 @@ class Resolver:
|
|
|
168
241
|
create_lines.append(
|
|
169
242
|
f" raise LookupError(_unresolved_messages[{idx}])"
|
|
170
243
|
)
|
|
171
|
-
create_lines.append(
|
|
172
|
-
|
|
244
|
+
create_lines.append(
|
|
245
|
+
f" _dep_resolver = _param_resolvers[{idx}]"
|
|
246
|
+
)
|
|
247
|
+
create_lines.append(" if _dep_resolver is None:")
|
|
173
248
|
create_lines.append(" try:")
|
|
174
249
|
if is_async:
|
|
175
250
|
create_lines.append(
|
|
@@ -191,7 +266,8 @@ class Resolver:
|
|
|
191
266
|
)
|
|
192
267
|
create_lines.append(
|
|
193
268
|
f" arg_{idx} = "
|
|
194
|
-
f"await compiled[0](container,
|
|
269
|
+
f"await compiled[0](container, "
|
|
270
|
+
f"context if _param_shared_scopes[{idx}] else None)"
|
|
195
271
|
)
|
|
196
272
|
else:
|
|
197
273
|
create_lines.append(
|
|
@@ -212,7 +288,8 @@ class Resolver:
|
|
|
212
288
|
)
|
|
213
289
|
create_lines.append(
|
|
214
290
|
f" arg_{idx} = "
|
|
215
|
-
f"compiled[0](container,
|
|
291
|
+
f"compiled[0](container, "
|
|
292
|
+
f"context if _param_shared_scopes[{idx}] else None)"
|
|
216
293
|
)
|
|
217
294
|
create_lines.append(" except LookupError:")
|
|
218
295
|
create_lines.append(
|
|
@@ -226,18 +303,23 @@ class Resolver:
|
|
|
226
303
|
create_lines.append(" else:")
|
|
227
304
|
if is_async:
|
|
228
305
|
create_lines.append(
|
|
229
|
-
f" arg_{idx} = await
|
|
230
|
-
f"container,
|
|
306
|
+
f" arg_{idx} = await _dep_resolver("
|
|
307
|
+
f"container, "
|
|
308
|
+
f"context if _param_shared_scopes[{idx}] else None)"
|
|
231
309
|
)
|
|
232
310
|
else:
|
|
233
311
|
create_lines.append(
|
|
234
|
-
f" arg_{idx} =
|
|
312
|
+
f" arg_{idx} = _dep_resolver("
|
|
313
|
+
f"container, "
|
|
314
|
+
f"context if _param_shared_scopes[{idx}] else None)"
|
|
235
315
|
)
|
|
236
316
|
create_lines.append(" else:")
|
|
237
317
|
create_lines.append(f" arg_{idx} = cached")
|
|
238
|
-
if
|
|
318
|
+
# Wrap dependencies if in override mode (only for override version)
|
|
319
|
+
if with_override:
|
|
320
|
+
create_lines.append(" if override_mode:")
|
|
239
321
|
create_lines.append(
|
|
240
|
-
f"
|
|
322
|
+
f" arg_{idx} = resolver._wrap_for_override("
|
|
241
323
|
f"_param_annotations[{idx}], arg_{idx})"
|
|
242
324
|
)
|
|
243
325
|
|
|
@@ -373,9 +455,11 @@ class Resolver:
|
|
|
373
455
|
create_lines.append(" if context is not None and store:")
|
|
374
456
|
create_lines.append(" context.set(_interface, inst)")
|
|
375
457
|
|
|
376
|
-
if
|
|
458
|
+
# Wrap instance if in override mode (only for override version)
|
|
459
|
+
if with_override:
|
|
460
|
+
create_lines.append(" if override_mode:")
|
|
377
461
|
create_lines.append(
|
|
378
|
-
"
|
|
462
|
+
" inst = resolver._post_resolve_override(_interface, inst)"
|
|
379
463
|
)
|
|
380
464
|
create_lines.append(" return inst")
|
|
381
465
|
|
|
@@ -386,7 +470,7 @@ class Resolver:
|
|
|
386
470
|
resolver_lines.append("def _resolver(container, context=None):")
|
|
387
471
|
|
|
388
472
|
# Only define NOT_SET_ if we actually need it
|
|
389
|
-
needs_not_set =
|
|
473
|
+
needs_not_set = scope in ("singleton", "request")
|
|
390
474
|
if needs_not_set:
|
|
391
475
|
resolver_lines.append(" NOT_SET_ = _NOT_SET")
|
|
392
476
|
|
|
@@ -399,22 +483,14 @@ class Resolver:
|
|
|
399
483
|
else:
|
|
400
484
|
resolver_lines.append(" context = None")
|
|
401
485
|
|
|
402
|
-
if has_override_support:
|
|
403
|
-
resolver_lines.append(
|
|
404
|
-
" override = container._hook_override_for(_interface)"
|
|
405
|
-
)
|
|
406
|
-
resolver_lines.append(" if override is not NOT_SET_:")
|
|
407
|
-
resolver_lines.append(" return override")
|
|
408
|
-
|
|
409
486
|
if scope == "singleton":
|
|
487
|
+
if with_override:
|
|
488
|
+
self._add_override_check(resolver_lines)
|
|
489
|
+
|
|
490
|
+
# Fast path: check cached instance
|
|
410
491
|
resolver_lines.append(" inst = context.get(_interface)")
|
|
411
492
|
resolver_lines.append(" if inst is not NOT_SET_:")
|
|
412
|
-
|
|
413
|
-
resolver_lines.append(
|
|
414
|
-
" return container._hook_post_resolve(_provider, inst)"
|
|
415
|
-
)
|
|
416
|
-
else:
|
|
417
|
-
resolver_lines.append(" return inst")
|
|
493
|
+
resolver_lines.append(" return inst")
|
|
418
494
|
|
|
419
495
|
if is_async:
|
|
420
496
|
resolver_lines.append(" async with context.alock():")
|
|
@@ -422,47 +498,43 @@ class Resolver:
|
|
|
422
498
|
resolver_lines.append(" with context.lock():")
|
|
423
499
|
resolver_lines.append(" inst = context.get(_interface)")
|
|
424
500
|
resolver_lines.append(" if inst is not NOT_SET_:")
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
"_create_instance(container, context, True, None)"
|
|
435
|
-
)
|
|
436
|
-
else:
|
|
437
|
-
resolver_lines.append(
|
|
438
|
-
" return _create_instance(container, context, True, None)"
|
|
439
|
-
)
|
|
501
|
+
resolver_lines.append(" return inst")
|
|
502
|
+
self._add_create_call(
|
|
503
|
+
resolver_lines,
|
|
504
|
+
is_async=is_async,
|
|
505
|
+
with_override=with_override,
|
|
506
|
+
context="context",
|
|
507
|
+
store=True,
|
|
508
|
+
indent=" ",
|
|
509
|
+
)
|
|
440
510
|
elif scope == "request":
|
|
511
|
+
if with_override:
|
|
512
|
+
self._add_override_check(resolver_lines)
|
|
513
|
+
|
|
514
|
+
# Fast path: check cached instance
|
|
441
515
|
resolver_lines.append(" inst = context.get(_interface)")
|
|
442
516
|
resolver_lines.append(" if inst is not NOT_SET_:")
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
)
|
|
453
|
-
else:
|
|
454
|
-
resolver_lines.append(
|
|
455
|
-
" return _create_instance(container, context, True, None)"
|
|
456
|
-
)
|
|
517
|
+
resolver_lines.append(" return inst")
|
|
518
|
+
|
|
519
|
+
self._add_create_call(
|
|
520
|
+
resolver_lines,
|
|
521
|
+
is_async=is_async,
|
|
522
|
+
with_override=with_override,
|
|
523
|
+
context="context",
|
|
524
|
+
store=True,
|
|
525
|
+
)
|
|
457
526
|
else:
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
resolver_lines
|
|
464
|
-
|
|
465
|
-
|
|
527
|
+
# Transient scope
|
|
528
|
+
if with_override:
|
|
529
|
+
self._add_override_check(resolver_lines, include_not_set=True)
|
|
530
|
+
|
|
531
|
+
self._add_create_call(
|
|
532
|
+
resolver_lines,
|
|
533
|
+
is_async=is_async,
|
|
534
|
+
with_override=with_override,
|
|
535
|
+
context="",
|
|
536
|
+
store=False,
|
|
537
|
+
)
|
|
466
538
|
|
|
467
539
|
create_resolver_lines: list[str] = []
|
|
468
540
|
if is_async:
|
|
@@ -474,9 +546,9 @@ class Resolver:
|
|
|
474
546
|
"def _resolver_create(container, defaults=None):"
|
|
475
547
|
)
|
|
476
548
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
create_resolver_lines.append("
|
|
549
|
+
if with_override:
|
|
550
|
+
# Cache override mode check
|
|
551
|
+
create_resolver_lines.append(" override_mode = resolver.override_mode")
|
|
480
552
|
|
|
481
553
|
if scope == "singleton":
|
|
482
554
|
create_resolver_lines.append(" context = container._singleton_context")
|
|
@@ -487,43 +559,20 @@ class Resolver:
|
|
|
487
559
|
else:
|
|
488
560
|
create_resolver_lines.append(" context = None")
|
|
489
561
|
|
|
490
|
-
if
|
|
491
|
-
|
|
492
|
-
" override = container._hook_override_for(_interface)"
|
|
493
|
-
)
|
|
494
|
-
create_resolver_lines.append(" if override is not NOT_SET_:")
|
|
495
|
-
create_resolver_lines.append(" return override")
|
|
562
|
+
if with_override:
|
|
563
|
+
self._add_override_check(create_resolver_lines, include_not_set=True)
|
|
496
564
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
if is_async:
|
|
509
|
-
create_resolver_lines.append(
|
|
510
|
-
" return await "
|
|
511
|
-
"_create_instance(container, context, False, defaults)"
|
|
512
|
-
)
|
|
513
|
-
else:
|
|
514
|
-
create_resolver_lines.append(
|
|
515
|
-
" return _create_instance(container, context, False, defaults)"
|
|
516
|
-
)
|
|
517
|
-
else:
|
|
518
|
-
if is_async:
|
|
519
|
-
create_resolver_lines.append(
|
|
520
|
-
" return await "
|
|
521
|
-
"_create_instance(container, None, False, defaults)"
|
|
522
|
-
)
|
|
523
|
-
else:
|
|
524
|
-
create_resolver_lines.append(
|
|
525
|
-
" return _create_instance(container, None, False, defaults)"
|
|
526
|
-
)
|
|
565
|
+
# Determine context for create call
|
|
566
|
+
context_arg = "context" if scope in ("singleton", "request") else ""
|
|
567
|
+
|
|
568
|
+
self._add_create_call(
|
|
569
|
+
create_resolver_lines,
|
|
570
|
+
is_async=is_async,
|
|
571
|
+
with_override=with_override,
|
|
572
|
+
context=context_arg,
|
|
573
|
+
store=False,
|
|
574
|
+
defaults="defaults",
|
|
575
|
+
)
|
|
527
576
|
|
|
528
577
|
lines = create_lines + [""] + resolver_lines + [""] + create_resolver_lines
|
|
529
578
|
|
|
@@ -539,13 +588,19 @@ class Resolver:
|
|
|
539
588
|
"_param_defaults": param_defaults,
|
|
540
589
|
"_param_has_default": param_has_default,
|
|
541
590
|
"_param_resolvers": param_resolvers,
|
|
591
|
+
"_param_shared_scopes": param_shared_scopes,
|
|
542
592
|
"_unresolved_messages": unresolved_messages,
|
|
543
593
|
"_unresolved_interfaces": self._unresolved_interfaces,
|
|
544
594
|
"_NOT_SET": NOT_SET,
|
|
545
595
|
"_contextmanager": contextlib.contextmanager,
|
|
546
596
|
"_is_cm": is_context_manager,
|
|
547
|
-
"_cache":
|
|
597
|
+
"_cache": (
|
|
598
|
+
(self._async_override_cache if is_async else self._override_cache)
|
|
599
|
+
if with_override
|
|
600
|
+
else (self._async_cache if is_async else self._cache)
|
|
601
|
+
),
|
|
548
602
|
"_compile": self._compile_resolver,
|
|
603
|
+
"resolver": self,
|
|
549
604
|
}
|
|
550
605
|
|
|
551
606
|
# Add async-specific namespace entries
|
|
@@ -561,3 +616,59 @@ class Resolver:
|
|
|
561
616
|
creator = ns["_resolver_create"]
|
|
562
617
|
|
|
563
618
|
return CompiledResolver(resolver, creator)
|
|
619
|
+
|
|
620
|
+
def _get_override_for(self, interface: Any) -> Any:
|
|
621
|
+
"""Hook for checking if an interface has an override."""
|
|
622
|
+
return self._override_instances.get(interface, NOT_SET)
|
|
623
|
+
|
|
624
|
+
def _wrap_for_override(self, annotation: Any, value: Any) -> Any:
|
|
625
|
+
"""Hook for wrapping dependencies to enable override patching."""
|
|
626
|
+
return InstanceProxy(value, interface=annotation)
|
|
627
|
+
|
|
628
|
+
def _post_resolve_override(self, interface: Any, instance: Any) -> Any: # noqa: C901
|
|
629
|
+
"""Hook for patching resolved instances to support override."""
|
|
630
|
+
if interface in self._override_instances:
|
|
631
|
+
return self._override_instances[interface]
|
|
632
|
+
|
|
633
|
+
if not hasattr(instance, "__dict__") or hasattr(
|
|
634
|
+
instance, "__resolver_getter__"
|
|
635
|
+
):
|
|
636
|
+
return instance
|
|
637
|
+
|
|
638
|
+
wrapped = {
|
|
639
|
+
name: value.interface
|
|
640
|
+
for name, value in instance.__dict__.items()
|
|
641
|
+
if isinstance(value, InstanceProxy)
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
def __resolver_getter__(name: str) -> Any:
|
|
645
|
+
if name in wrapped:
|
|
646
|
+
_interface = wrapped[name]
|
|
647
|
+
# Resolve the dependency if it's wrapped
|
|
648
|
+
return self._container.resolve(_interface)
|
|
649
|
+
raise LookupError
|
|
650
|
+
|
|
651
|
+
# Attach the resolver getter to the instance
|
|
652
|
+
instance.__resolver_getter__ = __resolver_getter__
|
|
653
|
+
|
|
654
|
+
if not hasattr(instance.__class__, "__getattribute_patched__"):
|
|
655
|
+
|
|
656
|
+
def __getattribute__(_self: Any, name: str) -> Any:
|
|
657
|
+
# Skip the resolver getter
|
|
658
|
+
if name in {"__resolver_getter__", "__class__"}:
|
|
659
|
+
return object.__getattribute__(_self, name)
|
|
660
|
+
|
|
661
|
+
if hasattr(_self, "__resolver_getter__"):
|
|
662
|
+
try:
|
|
663
|
+
return _self.__resolver_getter__(name)
|
|
664
|
+
except LookupError:
|
|
665
|
+
pass
|
|
666
|
+
|
|
667
|
+
# Fall back to default behavior
|
|
668
|
+
return object.__getattribute__(_self, name)
|
|
669
|
+
|
|
670
|
+
# Apply the patched resolver if wrapped attributes exist
|
|
671
|
+
instance.__class__.__getattribute__ = __getattribute__
|
|
672
|
+
instance.__class__.__getattribute_patched__ = True
|
|
673
|
+
|
|
674
|
+
return instance
|
anydi/testing.py
CHANGED
|
@@ -1,125 +1,46 @@
|
|
|
1
|
-
|
|
2
|
-
import logging
|
|
3
|
-
from collections.abc import Iterable, Iterator, Sequence
|
|
4
|
-
from typing import Any
|
|
1
|
+
"""Testing utilities for AnyDI.
|
|
5
2
|
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
.. deprecated:: 0.56.0
|
|
4
|
+
TestContainer is deprecated. Use Container with override() instead.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import warnings
|
|
8
10
|
|
|
9
11
|
from ._container import Container
|
|
10
|
-
from ._module import ModuleDef
|
|
11
|
-
from ._provider import ProviderDef
|
|
12
|
-
from ._types import NOT_SET
|
|
13
12
|
|
|
14
13
|
|
|
15
14
|
class TestContainer(Container):
|
|
15
|
+
"""Test container for dependency injection.
|
|
16
|
+
|
|
17
|
+
.. deprecated:: 0.56.0
|
|
18
|
+
TestContainer is deprecated and will be removed in a future version.
|
|
19
|
+
Use regular Container with override() method instead.
|
|
20
|
+
"""
|
|
21
|
+
|
|
16
22
|
__test__ = False
|
|
17
23
|
|
|
18
|
-
def __init__(
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
super().__init__(
|
|
26
|
-
self._override_instances: dict[Any, Any] = {}
|
|
27
|
-
self._revisions: dict[Any, int] = {}
|
|
24
|
+
def __init__(self, *args, **kwargs): # type: ignore
|
|
25
|
+
warnings.warn(
|
|
26
|
+
"TestContainer is deprecated and will be removed in a future version. "
|
|
27
|
+
"Use regular Container with override() method instead.",
|
|
28
|
+
DeprecationWarning,
|
|
29
|
+
stacklevel=2,
|
|
30
|
+
)
|
|
31
|
+
super().__init__(*args, **kwargs)
|
|
28
32
|
|
|
29
33
|
@classmethod
|
|
30
|
-
def from_container(cls, container: Container) ->
|
|
31
|
-
|
|
32
|
-
providers=[
|
|
33
|
-
ProviderDef(
|
|
34
|
-
interface=provider.interface,
|
|
35
|
-
call=provider.call,
|
|
36
|
-
scope=provider.scope,
|
|
37
|
-
)
|
|
38
|
-
for provider in container.providers.values()
|
|
39
|
-
],
|
|
40
|
-
logger=container.logger,
|
|
41
|
-
)
|
|
34
|
+
def from_container(cls, container: Container) -> Container:
|
|
35
|
+
"""Create a test container from an existing container.
|
|
42
36
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"""
|
|
46
|
-
Override the provider for the specified interface with a specific instance.
|
|
37
|
+
.. deprecated:: 0.56.0
|
|
38
|
+
This method is deprecated. Just use the container directly.
|
|
47
39
|
"""
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
yield
|
|
56
|
-
finally:
|
|
57
|
-
self._override_instances.pop(interface, None)
|
|
58
|
-
self._touch_revision(interface)
|
|
59
|
-
|
|
60
|
-
def _touch_revision(self, interface: Any) -> None:
|
|
61
|
-
self._revisions[interface] = self._revisions.get(interface, 0) + 1
|
|
62
|
-
|
|
63
|
-
def _hook_override_for(self, interface: Any) -> Any:
|
|
64
|
-
return self._override_instances.get(interface, NOT_SET)
|
|
65
|
-
|
|
66
|
-
def _hook_wrap_dependency(self, annotation: Any, value: Any) -> Any:
|
|
67
|
-
return InstanceProxy(value, interface=annotation)
|
|
68
|
-
|
|
69
|
-
def _hook_post_resolve(self, interface: Any, instance: Any) -> Any: # noqa: C901
|
|
70
|
-
"""Patch the test resolver for the instance."""
|
|
71
|
-
if interface in self._override_instances:
|
|
72
|
-
return self._override_instances[interface]
|
|
73
|
-
|
|
74
|
-
if not hasattr(instance, "__dict__") or hasattr(
|
|
75
|
-
instance, "__resolver_getter__"
|
|
76
|
-
):
|
|
77
|
-
return instance
|
|
78
|
-
|
|
79
|
-
wrapped = {
|
|
80
|
-
name: value.interface
|
|
81
|
-
for name, value in instance.__dict__.items()
|
|
82
|
-
if isinstance(value, InstanceProxy)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
def __resolver_getter__(name: str) -> Any:
|
|
86
|
-
if name in wrapped:
|
|
87
|
-
_interface = wrapped[name]
|
|
88
|
-
# Resolve the dependency if it's wrapped
|
|
89
|
-
return self.resolve(_interface)
|
|
90
|
-
raise LookupError
|
|
91
|
-
|
|
92
|
-
# Attach the resolver getter to the instance
|
|
93
|
-
instance.__resolver_getter__ = __resolver_getter__
|
|
94
|
-
|
|
95
|
-
if not hasattr(instance.__class__, "__getattribute_patched__"):
|
|
96
|
-
|
|
97
|
-
def __getattribute__(_self: Any, name: str) -> Any:
|
|
98
|
-
# Skip the resolver getter
|
|
99
|
-
if name in {"__resolver_getter__", "__class__"}:
|
|
100
|
-
return object.__getattribute__(_self, name)
|
|
101
|
-
|
|
102
|
-
if hasattr(_self, "__resolver_getter__"):
|
|
103
|
-
try:
|
|
104
|
-
return _self.__resolver_getter__(name)
|
|
105
|
-
except LookupError:
|
|
106
|
-
pass
|
|
107
|
-
|
|
108
|
-
# Fall back to default behavior
|
|
109
|
-
return object.__getattribute__(_self, name)
|
|
110
|
-
|
|
111
|
-
# Apply the patched resolver if wrapped attributes exist
|
|
112
|
-
instance.__class__.__getattribute__ = __getattribute__
|
|
113
|
-
instance.__class__.__getattribute_patched__ = True
|
|
114
|
-
|
|
115
|
-
return instance
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
class InstanceProxy(wrapt.ObjectProxy): # type: ignore
|
|
119
|
-
def __init__(self, wrapped: Any, *, interface: type[Any]) -> None:
|
|
120
|
-
super().__init__(wrapped) # type: ignore
|
|
121
|
-
self._self_interface = interface
|
|
122
|
-
|
|
123
|
-
@property
|
|
124
|
-
def interface(self) -> type[Any]:
|
|
125
|
-
return self._self_interface
|
|
40
|
+
warnings.warn(
|
|
41
|
+
"TestContainer.from_container() is deprecated. "
|
|
42
|
+
"Use the container directly with override() method.",
|
|
43
|
+
DeprecationWarning,
|
|
44
|
+
stacklevel=2,
|
|
45
|
+
)
|
|
46
|
+
return container
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: anydi
|
|
3
|
+
Version: 0.56.0
|
|
4
|
+
Summary: Dependency Injection library
|
|
5
|
+
Keywords: dependency injection,dependencies,di,async,asyncio,application
|
|
6
|
+
Author: Anton Ruhlov
|
|
7
|
+
Author-email: Anton Ruhlov <antonruhlov@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Classifier: Intended Audience :: Information Technology
|
|
10
|
+
Classifier: Intended Audience :: System Administrators
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Topic :: Internet
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
+
Classifier: Topic :: Software Development
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Classifier: Environment :: Web Environment
|
|
20
|
+
Classifier: Intended Audience :: Developers
|
|
21
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
22
|
+
Classifier: Programming Language :: Python :: 3
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
27
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
28
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
29
|
+
Requires-Dist: typing-extensions>=4.15.0,<5
|
|
30
|
+
Requires-Dist: anyio>=3.7.1
|
|
31
|
+
Requires-Dist: wrapt>=1.17.0
|
|
32
|
+
Requires-Python: >=3.10.0, <3.15
|
|
33
|
+
Project-URL: Repository, https://github.com/antonrh/anydi
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# AnyDI
|
|
37
|
+
|
|
38
|
+
<div style="text-align: center;">
|
|
39
|
+
|
|
40
|
+
Modern, lightweight Dependency Injection library using type annotations.
|
|
41
|
+
|
|
42
|
+
[](https://github.com/antonrh/anydi/actions/workflows/ci.yml)
|
|
43
|
+
[](https://codecov.io/gh/antonrh/anydi)
|
|
44
|
+
[](https://anydi.readthedocs.io/en/latest/)
|
|
45
|
+
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
Documentation
|
|
50
|
+
|
|
51
|
+
http://anydi.readthedocs.io/
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
`AnyDI` is a modern, lightweight Dependency Injection library suitable for any synchronous or asynchronous applications with Python 3.10+, based on type annotations ([PEP 484](https://peps.python.org/pep-0484/)).
|
|
56
|
+
|
|
57
|
+
The key features are:
|
|
58
|
+
|
|
59
|
+
* **Type-safe**: Dependency resolution is driven by type hints.
|
|
60
|
+
* **Async-ready**: Works the same for sync and async providers or injections.
|
|
61
|
+
* **Scoped**: Built-in singleton, transient, and request lifetimes.
|
|
62
|
+
* **Simple**: Small surface area keeps boilerplate low.
|
|
63
|
+
* **Fast**: Resolver still adds only microseconds of overhead.
|
|
64
|
+
* **Named**: `Annotated[...]` makes multiple bindings per type simple.
|
|
65
|
+
* **Managed**: Providers can open/close resources via context managers.
|
|
66
|
+
* **Modular**: Compose containers or modules for large apps.
|
|
67
|
+
* **Scanning**: Auto-discovers injectable callables.
|
|
68
|
+
* **Integrated**: Extensions for popular frameworks.
|
|
69
|
+
* **Testable**: Override providers directly in tests.
|
|
70
|
+
|
|
71
|
+
## Installation
|
|
72
|
+
|
|
73
|
+
```shell
|
|
74
|
+
pip install anydi
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Comprehensive Example
|
|
78
|
+
|
|
79
|
+
### Define a Service (`app/services.py`)
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
class GreetingService:
|
|
83
|
+
def greet(self, name: str) -> str:
|
|
84
|
+
return f"Hello, {name}!"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Create the Container and Providers (`app/container.py`)
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from anydi import Container
|
|
91
|
+
|
|
92
|
+
from app.services import GreetingService
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
container = Container()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@container.provider(scope="singleton")
|
|
99
|
+
def service() -> GreetingService:
|
|
100
|
+
return GreetingService()
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Resolve Dependencies Directly
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from app.container import container
|
|
107
|
+
from app.services import GreetingService
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
service = container.resolve(GreetingService)
|
|
111
|
+
|
|
112
|
+
if __name__ == "__main__":
|
|
113
|
+
print(service.greet("World"))
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Inject Into Functions (`app/main.py`)
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from anydi import Inject
|
|
120
|
+
|
|
121
|
+
from app.container import container
|
|
122
|
+
from app.services import GreetingService
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@container.inject
|
|
126
|
+
def greet(service: GreetingService = Inject()) -> str:
|
|
127
|
+
return service.greet("World")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
if __name__ == "__main__":
|
|
131
|
+
print(greet())
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Test with Overrides (`tests/test_app.py`)
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from unittest import mock
|
|
138
|
+
|
|
139
|
+
from app.container import container
|
|
140
|
+
from app.services import GreetingService
|
|
141
|
+
from app.main import greet
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_greet() -> None:
|
|
145
|
+
service_mock = mock.Mock(spec=GreetingService)
|
|
146
|
+
service_mock.greet.return_value = "Mocked"
|
|
147
|
+
|
|
148
|
+
with container.override(GreetingService, service_mock):
|
|
149
|
+
result = greet()
|
|
150
|
+
|
|
151
|
+
assert result == "Mocked"
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Integrate with FastAPI (`app/api.py`)
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from typing import Annotated
|
|
158
|
+
|
|
159
|
+
import anydi.ext.fastapi
|
|
160
|
+
from fastapi import FastAPI
|
|
161
|
+
from anydi.ext.fastapi import Inject
|
|
162
|
+
|
|
163
|
+
from app.container import container
|
|
164
|
+
from app.services import GreetingService
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
app = FastAPI()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@app.get("/greeting")
|
|
171
|
+
async def greet(
|
|
172
|
+
service: Annotated[GreetingService, Inject()]
|
|
173
|
+
) -> dict[str, str]:
|
|
174
|
+
return {"greeting": service.greet("World")}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
anydi.ext.fastapi.install(app, container)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Test the FastAPI Integration (`test_api.py`)
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
from unittest import mock
|
|
184
|
+
|
|
185
|
+
from fastapi.testclient import TestClient
|
|
186
|
+
|
|
187
|
+
from app.api import app
|
|
188
|
+
from app.container import container
|
|
189
|
+
from app.services import GreetingService
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
client = TestClient(app)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_api_greeting() -> None:
|
|
196
|
+
service_mock = mock.Mock(spec=GreetingService)
|
|
197
|
+
service_mock.greet.return_value = "Mocked"
|
|
198
|
+
|
|
199
|
+
with container.override(GreetingService, service_mock):
|
|
200
|
+
response = client.get("/greeting")
|
|
201
|
+
|
|
202
|
+
assert response.json() == {"greeting": "Mocked"}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Integrate with Django Ninja
|
|
206
|
+
|
|
207
|
+
Install the Django integration extras:
|
|
208
|
+
|
|
209
|
+
```sh
|
|
210
|
+
pip install 'anydi-django[ninja]'
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Expose the container factory (`app/container.py`):
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
from anydi import Container
|
|
217
|
+
|
|
218
|
+
from app.services import GreetingService
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
container = Container()
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@container.provider(scope="singleton")
|
|
225
|
+
def service() -> GreetingService:
|
|
226
|
+
return GreetingService()
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Configure Django (`settings.py`):
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
INSTALLED_APPS = [
|
|
233
|
+
...
|
|
234
|
+
"anydi_django",
|
|
235
|
+
]
|
|
236
|
+
|
|
237
|
+
ANYDI = {
|
|
238
|
+
"CONTAINER_FACTORY": "app.container.container",
|
|
239
|
+
"PATCH_NINJA": True,
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Wire Django Ninja (`urls.py`):
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
from typing import Annotated, Any
|
|
247
|
+
|
|
248
|
+
from anydi import Inject
|
|
249
|
+
from django.http import HttpRequest
|
|
250
|
+
from django.urls import path
|
|
251
|
+
from ninja import NinjaAPI
|
|
252
|
+
|
|
253
|
+
from app.services import GreetingService
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
api = NinjaAPI()
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@api.get("/greeting")
|
|
260
|
+
def greet(request: HttpRequest, service: Annotated[GreetingService, Inject()]) -> Any:
|
|
261
|
+
return {"greeting": service.greet("World")}
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
urlpatterns = [
|
|
265
|
+
path("api/", api.urls),
|
|
266
|
+
]
|
|
267
|
+
```
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
anydi/__init__.py,sha256=Cz-beqReX0d05SFDmYcrzIs3FqQkWAwpy1Aqzd5db34,547
|
|
2
2
|
anydi/_async_lock.py,sha256=3dwZr0KthXFYha0XKMyXf8jMmGb1lYoNC0O5w29V9ic,1104
|
|
3
|
-
anydi/_container.py,sha256=
|
|
3
|
+
anydi/_container.py,sha256=dKRT4FB0ONyEt2-MRz4T0MZziwZAMb0XyulV6lVn04g,24997
|
|
4
4
|
anydi/_context.py,sha256=-9QqeMWo9OpZVXZxZCQgIsswggl3Ch7lgx1KiFX_ezc,3752
|
|
5
5
|
anydi/_decorators.py,sha256=J3W261ZAG7q4XKm4tbAv1wsWr9ysx9_5MUbUvSJB_MQ,2809
|
|
6
6
|
anydi/_module.py,sha256=2kN5uEXLd2Dsc58gz5IWK43wJewr_QgIVGSO3iWp798,2609
|
|
7
|
-
anydi/_provider.py,sha256=
|
|
8
|
-
anydi/_resolver.py,sha256
|
|
7
|
+
anydi/_provider.py,sha256=OV1WFHTYv7W2U0XDk_Kql1r551Vhq8o-pUV5ep1HQcU,1574
|
|
8
|
+
anydi/_resolver.py,sha256=-MF2KsERF5qzU6uqYPF1fI58isgsjxXPLERylzFFDHE,28787
|
|
9
9
|
anydi/_scanner.py,sha256=oycIC9kw9fsIG9qgtRHeBkj3HjmcLK0FTqWLXTLLSWE,3636
|
|
10
10
|
anydi/_types.py,sha256=l3xQ0Zn15gRAwvBoQ9PRfCBigi2rrtSqGV-C50xXrLw,1780
|
|
11
11
|
anydi/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -17,8 +17,8 @@ anydi/ext/pytest_plugin.py,sha256=Es1K1S6_2gIdTUYkbw2d1aZcHnjJutGFafVsLPGcVJc,46
|
|
|
17
17
|
anydi/ext/starlette/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
18
|
anydi/ext/starlette/middleware.py,sha256=MxnzshAs-CMvjJp0r457k52MzBL8O4KAuClnF6exBdU,803
|
|
19
19
|
anydi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
-
anydi/testing.py,sha256=
|
|
21
|
-
anydi-0.
|
|
22
|
-
anydi-0.
|
|
23
|
-
anydi-0.
|
|
24
|
-
anydi-0.
|
|
20
|
+
anydi/testing.py,sha256=cHg3mMScZbEep9smRqSNQ81BZMQOkyugHe8TvKdPnEg,1347
|
|
21
|
+
anydi-0.56.0.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
|
|
22
|
+
anydi-0.56.0.dist-info/entry_points.txt,sha256=AgOcQYM5KyS4D37QcYb00tiid0QA-pD1VrjHHq4QAps,44
|
|
23
|
+
anydi-0.56.0.dist-info/METADATA,sha256=JkjqRSFZ-Vu22IYLnWS2uRTZA7uUbX41o2b-51yp0dA,6561
|
|
24
|
+
anydi-0.56.0.dist-info/RECORD,,
|
anydi-0.55.0.dist-info/METADATA
DELETED
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: anydi
|
|
3
|
-
Version: 0.55.0
|
|
4
|
-
Summary: Dependency Injection library
|
|
5
|
-
Keywords: dependency injection,dependencies,di,async,asyncio,application
|
|
6
|
-
Author: Anton Ruhlov
|
|
7
|
-
Author-email: Anton Ruhlov <antonruhlov@gmail.com>
|
|
8
|
-
License-Expression: MIT
|
|
9
|
-
Classifier: Intended Audience :: Information Technology
|
|
10
|
-
Classifier: Intended Audience :: System Administrators
|
|
11
|
-
Classifier: Operating System :: OS Independent
|
|
12
|
-
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
-
Classifier: Topic :: Internet
|
|
14
|
-
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
15
|
-
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
-
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
-
Classifier: Topic :: Software Development
|
|
18
|
-
Classifier: Typing :: Typed
|
|
19
|
-
Classifier: Environment :: Web Environment
|
|
20
|
-
Classifier: Intended Audience :: Developers
|
|
21
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
22
|
-
Classifier: Programming Language :: Python :: 3
|
|
23
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
24
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
25
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
26
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
27
|
-
Classifier: Programming Language :: Python :: 3.14
|
|
28
|
-
Classifier: Programming Language :: Python :: 3 :: Only
|
|
29
|
-
Requires-Dist: typing-extensions>=4.15.0,<5
|
|
30
|
-
Requires-Dist: anyio>=3.7.1
|
|
31
|
-
Requires-Dist: wrapt>=1.17.0,<2
|
|
32
|
-
Requires-Python: >=3.10.0, <3.15
|
|
33
|
-
Project-URL: Repository, https://github.com/antonrh/anydi
|
|
34
|
-
Description-Content-Type: text/markdown
|
|
35
|
-
|
|
36
|
-
# AnyDI
|
|
37
|
-
|
|
38
|
-
<div style="text-align: center;">
|
|
39
|
-
|
|
40
|
-
Modern, lightweight Dependency Injection library using type annotations.
|
|
41
|
-
|
|
42
|
-
[](https://github.com/antonrh/anydi/actions/workflows/ci.yml)
|
|
43
|
-
[](https://codecov.io/gh/antonrh/anydi)
|
|
44
|
-
[](https://anydi.readthedocs.io/en/latest/)
|
|
45
|
-
|
|
46
|
-
</div>
|
|
47
|
-
|
|
48
|
-
---
|
|
49
|
-
Documentation
|
|
50
|
-
|
|
51
|
-
http://anydi.readthedocs.io/
|
|
52
|
-
|
|
53
|
-
---
|
|
54
|
-
|
|
55
|
-
`AnyDI` is a modern, lightweight Dependency Injection library suitable for any synchronous or asynchronous applications with Python 3.10+, based on type annotations ([PEP 484](https://peps.python.org/pep-0484/)).
|
|
56
|
-
|
|
57
|
-
The key features are:
|
|
58
|
-
|
|
59
|
-
* **Type-safe**: Resolves dependencies using type annotations.
|
|
60
|
-
* **Async Support**: Compatible with both synchronous and asynchronous providers and injections.
|
|
61
|
-
* **Scoping**: Supports singleton, transient, and request scopes.
|
|
62
|
-
* **Easy to Use**: Designed for simplicity and minimal boilerplate.
|
|
63
|
-
* **Named Dependencies**: Supports named dependencies using `Annotated` type.
|
|
64
|
-
* **Resource Management**: Manages resources using context managers.
|
|
65
|
-
* **Modular: Facilitates** a modular design with support for multiple modules.
|
|
66
|
-
* **Scanning**: Automatically scans for injectable functions and classes.
|
|
67
|
-
* **Integrations**: Provides easy integration with popular frameworks and libraries.
|
|
68
|
-
* **Testing**: Simplifies testing by allowing provider overrides.
|
|
69
|
-
|
|
70
|
-
## Installation
|
|
71
|
-
|
|
72
|
-
```shell
|
|
73
|
-
pip install anydi
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
## Quick Example
|
|
77
|
-
|
|
78
|
-
*app.py*
|
|
79
|
-
|
|
80
|
-
```python
|
|
81
|
-
from anydi import auto, Container
|
|
82
|
-
|
|
83
|
-
container = Container()
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
@container.provider(scope="singleton")
|
|
87
|
-
def message() -> str:
|
|
88
|
-
return "Hello, World!"
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
@container.inject
|
|
92
|
-
def say_hello(message: str = auto) -> None:
|
|
93
|
-
print(message)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if __name__ == "__main__":
|
|
97
|
-
say_hello()
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
## FastAPI Example
|
|
101
|
-
|
|
102
|
-
*app.py*
|
|
103
|
-
|
|
104
|
-
```python
|
|
105
|
-
from typing import Annotated
|
|
106
|
-
|
|
107
|
-
from fastapi import FastAPI
|
|
108
|
-
|
|
109
|
-
import anydi.ext.fastapi
|
|
110
|
-
from anydi import Container
|
|
111
|
-
from anydi.ext.fastapi import Inject
|
|
112
|
-
|
|
113
|
-
container = Container()
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
@container.provider(scope="singleton")
|
|
117
|
-
def message() -> str:
|
|
118
|
-
return "Hello, World!"
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
app = FastAPI()
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
@app.get("/hello")
|
|
125
|
-
def say_hello(message: Annotated[str, Inject()]) -> dict[str, str]:
|
|
126
|
-
return {"message": message}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
# Install the container into the FastAPI app
|
|
130
|
-
anydi.ext.fastapi.install(app, container)
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
## Django Ninja Example
|
|
136
|
-
|
|
137
|
-
### Install
|
|
138
|
-
|
|
139
|
-
```sh
|
|
140
|
-
pip install 'anydi-django[ninja]'
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
*container.py*
|
|
144
|
-
|
|
145
|
-
```python
|
|
146
|
-
from anydi import Container
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
def get_container() -> Container:
|
|
150
|
-
container = Container()
|
|
151
|
-
|
|
152
|
-
@container.provider(scope="singleton")
|
|
153
|
-
def message() -> str:
|
|
154
|
-
return "Hello, World!"
|
|
155
|
-
|
|
156
|
-
return container
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
*settings.py*
|
|
160
|
-
|
|
161
|
-
```python
|
|
162
|
-
INSTALLED_APPS = [
|
|
163
|
-
...
|
|
164
|
-
"anydi_django",
|
|
165
|
-
]
|
|
166
|
-
|
|
167
|
-
ANYDI = {
|
|
168
|
-
"CONTAINER_FACTORY": "myapp.container.get_container",
|
|
169
|
-
"PATCH_NINJA": True,
|
|
170
|
-
}
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
*urls.py*
|
|
174
|
-
|
|
175
|
-
```python
|
|
176
|
-
from django.http import HttpRequest
|
|
177
|
-
from django.urls import path
|
|
178
|
-
from ninja import NinjaAPI
|
|
179
|
-
|
|
180
|
-
from anydi import auto
|
|
181
|
-
|
|
182
|
-
api = NinjaAPI()
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
@api.get("/hello")
|
|
186
|
-
def say_hello(request: HttpRequest, message: str = auto) -> dict[str, str]:
|
|
187
|
-
return {"message": message}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
urlpatterns = [
|
|
191
|
-
path("api/", api.urls),
|
|
192
|
-
]
|
|
193
|
-
```
|
|
File without changes
|
|
File without changes
|