anydi 0.55.1__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 +11 -7
- anydi/_resolver.py +225 -122
- anydi/testing.py +34 -113
- anydi-0.56.0.dist-info/METADATA +267 -0
- {anydi-0.55.1.dist-info → anydi-0.56.0.dist-info}/RECORD +7 -7
- anydi-0.55.1.dist-info/METADATA +0 -193
- {anydi-0.55.1.dist-info → anydi-0.56.0.dist-info}/WHEEL +0 -0
- {anydi-0.55.1.dist-info → anydi-0.56.0.dist-info}/entry_points.txt +0 -0
anydi/_container.py
CHANGED
|
@@ -671,13 +671,17 @@ class Container:
|
|
|
671
671
|
) -> None:
|
|
672
672
|
self._scanner.scan(packages=packages, tags=tags)
|
|
673
673
|
|
|
674
|
-
# == Testing ==
|
|
674
|
+
# == Testing / Override Support ==
|
|
675
675
|
|
|
676
676
|
@contextlib.contextmanager
|
|
677
677
|
def override(self, interface: Any, instance: Any) -> Iterator[None]:
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
)
|
|
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/_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,20 +88,58 @@ 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
|
|
@@ -84,7 +149,11 @@ class Resolver:
|
|
|
84
149
|
param_shared_scopes: list[bool] = [False] * num_params
|
|
85
150
|
unresolved_messages: list[str] = [""] * num_params
|
|
86
151
|
|
|
87
|
-
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
|
+
)
|
|
88
157
|
|
|
89
158
|
for idx, p in enumerate(provider.parameters):
|
|
90
159
|
param_annotations[idx] = p.annotation
|
|
@@ -100,13 +169,13 @@ class Resolver:
|
|
|
100
169
|
cache[p.provider.interface] = compiled
|
|
101
170
|
param_resolvers[idx] = compiled.resolve
|
|
102
171
|
|
|
103
|
-
|
|
172
|
+
unresolved_message = (
|
|
104
173
|
f"You are attempting to get the parameter `{p.name}` with the "
|
|
105
174
|
f"annotation `{type_repr(p.annotation)}` as a dependency into "
|
|
106
175
|
f"`{type_repr(provider.call)}` which is not registered or set in the "
|
|
107
176
|
"scoped context."
|
|
108
177
|
)
|
|
109
|
-
unresolved_messages[idx] =
|
|
178
|
+
unresolved_messages[idx] = unresolved_message
|
|
110
179
|
|
|
111
180
|
scope = provider.scope
|
|
112
181
|
is_generator = provider.is_generator
|
|
@@ -117,11 +186,13 @@ class Resolver:
|
|
|
117
186
|
create_lines: list[str] = []
|
|
118
187
|
if is_async:
|
|
119
188
|
create_lines.append(
|
|
120
|
-
"async def _create_instance(
|
|
189
|
+
"async def _create_instance("
|
|
190
|
+
"container, context, store, defaults, override_mode):"
|
|
121
191
|
)
|
|
122
192
|
else:
|
|
123
193
|
create_lines.append(
|
|
124
|
-
"def _create_instance(
|
|
194
|
+
"def _create_instance("
|
|
195
|
+
"container, context, store, defaults, override_mode):"
|
|
125
196
|
)
|
|
126
197
|
|
|
127
198
|
if no_params:
|
|
@@ -170,8 +241,10 @@ class Resolver:
|
|
|
170
241
|
create_lines.append(
|
|
171
242
|
f" raise LookupError(_unresolved_messages[{idx}])"
|
|
172
243
|
)
|
|
173
|
-
create_lines.append(
|
|
174
|
-
|
|
244
|
+
create_lines.append(
|
|
245
|
+
f" _dep_resolver = _param_resolvers[{idx}]"
|
|
246
|
+
)
|
|
247
|
+
create_lines.append(" if _dep_resolver is None:")
|
|
175
248
|
create_lines.append(" try:")
|
|
176
249
|
if is_async:
|
|
177
250
|
create_lines.append(
|
|
@@ -230,21 +303,23 @@ class Resolver:
|
|
|
230
303
|
create_lines.append(" else:")
|
|
231
304
|
if is_async:
|
|
232
305
|
create_lines.append(
|
|
233
|
-
f" arg_{idx} = await
|
|
306
|
+
f" arg_{idx} = await _dep_resolver("
|
|
234
307
|
f"container, "
|
|
235
308
|
f"context if _param_shared_scopes[{idx}] else None)"
|
|
236
309
|
)
|
|
237
310
|
else:
|
|
238
311
|
create_lines.append(
|
|
239
|
-
f" arg_{idx} =
|
|
312
|
+
f" arg_{idx} = _dep_resolver("
|
|
240
313
|
f"container, "
|
|
241
314
|
f"context if _param_shared_scopes[{idx}] else None)"
|
|
242
315
|
)
|
|
243
316
|
create_lines.append(" else:")
|
|
244
317
|
create_lines.append(f" arg_{idx} = cached")
|
|
245
|
-
if
|
|
318
|
+
# Wrap dependencies if in override mode (only for override version)
|
|
319
|
+
if with_override:
|
|
320
|
+
create_lines.append(" if override_mode:")
|
|
246
321
|
create_lines.append(
|
|
247
|
-
f"
|
|
322
|
+
f" arg_{idx} = resolver._wrap_for_override("
|
|
248
323
|
f"_param_annotations[{idx}], arg_{idx})"
|
|
249
324
|
)
|
|
250
325
|
|
|
@@ -380,9 +455,11 @@ class Resolver:
|
|
|
380
455
|
create_lines.append(" if context is not None and store:")
|
|
381
456
|
create_lines.append(" context.set(_interface, inst)")
|
|
382
457
|
|
|
383
|
-
if
|
|
458
|
+
# Wrap instance if in override mode (only for override version)
|
|
459
|
+
if with_override:
|
|
460
|
+
create_lines.append(" if override_mode:")
|
|
384
461
|
create_lines.append(
|
|
385
|
-
"
|
|
462
|
+
" inst = resolver._post_resolve_override(_interface, inst)"
|
|
386
463
|
)
|
|
387
464
|
create_lines.append(" return inst")
|
|
388
465
|
|
|
@@ -393,7 +470,7 @@ class Resolver:
|
|
|
393
470
|
resolver_lines.append("def _resolver(container, context=None):")
|
|
394
471
|
|
|
395
472
|
# Only define NOT_SET_ if we actually need it
|
|
396
|
-
needs_not_set =
|
|
473
|
+
needs_not_set = scope in ("singleton", "request")
|
|
397
474
|
if needs_not_set:
|
|
398
475
|
resolver_lines.append(" NOT_SET_ = _NOT_SET")
|
|
399
476
|
|
|
@@ -406,22 +483,14 @@ class Resolver:
|
|
|
406
483
|
else:
|
|
407
484
|
resolver_lines.append(" context = None")
|
|
408
485
|
|
|
409
|
-
if has_override_support:
|
|
410
|
-
resolver_lines.append(
|
|
411
|
-
" override = container._hook_override_for(_interface)"
|
|
412
|
-
)
|
|
413
|
-
resolver_lines.append(" if override is not NOT_SET_:")
|
|
414
|
-
resolver_lines.append(" return override")
|
|
415
|
-
|
|
416
486
|
if scope == "singleton":
|
|
487
|
+
if with_override:
|
|
488
|
+
self._add_override_check(resolver_lines)
|
|
489
|
+
|
|
490
|
+
# Fast path: check cached instance
|
|
417
491
|
resolver_lines.append(" inst = context.get(_interface)")
|
|
418
492
|
resolver_lines.append(" if inst is not NOT_SET_:")
|
|
419
|
-
|
|
420
|
-
resolver_lines.append(
|
|
421
|
-
" return container._hook_post_resolve(_provider, inst)"
|
|
422
|
-
)
|
|
423
|
-
else:
|
|
424
|
-
resolver_lines.append(" return inst")
|
|
493
|
+
resolver_lines.append(" return inst")
|
|
425
494
|
|
|
426
495
|
if is_async:
|
|
427
496
|
resolver_lines.append(" async with context.alock():")
|
|
@@ -429,47 +498,43 @@ class Resolver:
|
|
|
429
498
|
resolver_lines.append(" with context.lock():")
|
|
430
499
|
resolver_lines.append(" inst = context.get(_interface)")
|
|
431
500
|
resolver_lines.append(" if inst is not NOT_SET_:")
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
"_create_instance(container, context, True, None)"
|
|
442
|
-
)
|
|
443
|
-
else:
|
|
444
|
-
resolver_lines.append(
|
|
445
|
-
" return _create_instance(container, context, True, None)"
|
|
446
|
-
)
|
|
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
|
+
)
|
|
447
510
|
elif scope == "request":
|
|
511
|
+
if with_override:
|
|
512
|
+
self._add_override_check(resolver_lines)
|
|
513
|
+
|
|
514
|
+
# Fast path: check cached instance
|
|
448
515
|
resolver_lines.append(" inst = context.get(_interface)")
|
|
449
516
|
resolver_lines.append(" if inst is not NOT_SET_:")
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
)
|
|
460
|
-
else:
|
|
461
|
-
resolver_lines.append(
|
|
462
|
-
" return _create_instance(container, context, True, None)"
|
|
463
|
-
)
|
|
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
|
+
)
|
|
464
526
|
else:
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
resolver_lines
|
|
471
|
-
|
|
472
|
-
|
|
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
|
+
)
|
|
473
538
|
|
|
474
539
|
create_resolver_lines: list[str] = []
|
|
475
540
|
if is_async:
|
|
@@ -481,9 +546,9 @@ class Resolver:
|
|
|
481
546
|
"def _resolver_create(container, defaults=None):"
|
|
482
547
|
)
|
|
483
548
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
create_resolver_lines.append("
|
|
549
|
+
if with_override:
|
|
550
|
+
# Cache override mode check
|
|
551
|
+
create_resolver_lines.append(" override_mode = resolver.override_mode")
|
|
487
552
|
|
|
488
553
|
if scope == "singleton":
|
|
489
554
|
create_resolver_lines.append(" context = container._singleton_context")
|
|
@@ -494,43 +559,20 @@ class Resolver:
|
|
|
494
559
|
else:
|
|
495
560
|
create_resolver_lines.append(" context = None")
|
|
496
561
|
|
|
497
|
-
if
|
|
498
|
-
|
|
499
|
-
" override = container._hook_override_for(_interface)"
|
|
500
|
-
)
|
|
501
|
-
create_resolver_lines.append(" if override is not NOT_SET_:")
|
|
502
|
-
create_resolver_lines.append(" return override")
|
|
562
|
+
if with_override:
|
|
563
|
+
self._add_override_check(create_resolver_lines, include_not_set=True)
|
|
503
564
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
if is_async:
|
|
516
|
-
create_resolver_lines.append(
|
|
517
|
-
" return await "
|
|
518
|
-
"_create_instance(container, context, False, defaults)"
|
|
519
|
-
)
|
|
520
|
-
else:
|
|
521
|
-
create_resolver_lines.append(
|
|
522
|
-
" return _create_instance(container, context, False, defaults)"
|
|
523
|
-
)
|
|
524
|
-
else:
|
|
525
|
-
if is_async:
|
|
526
|
-
create_resolver_lines.append(
|
|
527
|
-
" return await "
|
|
528
|
-
"_create_instance(container, None, False, defaults)"
|
|
529
|
-
)
|
|
530
|
-
else:
|
|
531
|
-
create_resolver_lines.append(
|
|
532
|
-
" return _create_instance(container, None, False, defaults)"
|
|
533
|
-
)
|
|
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
|
+
)
|
|
534
576
|
|
|
535
577
|
lines = create_lines + [""] + resolver_lines + [""] + create_resolver_lines
|
|
536
578
|
|
|
@@ -552,8 +594,13 @@ class Resolver:
|
|
|
552
594
|
"_NOT_SET": NOT_SET,
|
|
553
595
|
"_contextmanager": contextlib.contextmanager,
|
|
554
596
|
"_is_cm": is_context_manager,
|
|
555
|
-
"_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
|
+
),
|
|
556
602
|
"_compile": self._compile_resolver,
|
|
603
|
+
"resolver": self,
|
|
557
604
|
}
|
|
558
605
|
|
|
559
606
|
# Add async-specific namespace entries
|
|
@@ -569,3 +616,59 @@ class Resolver:
|
|
|
569
616
|
creator = ns["_resolver_create"]
|
|
570
617
|
|
|
571
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
7
|
anydi/_provider.py,sha256=OV1WFHTYv7W2U0XDk_Kql1r551Vhq8o-pUV5ep1HQcU,1574
|
|
8
|
-
anydi/_resolver.py,sha256
|
|
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.1.dist-info/METADATA
DELETED
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: anydi
|
|
3
|
-
Version: 0.55.1
|
|
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
|