anydi 0.59.0__py3-none-any.whl → 0.60.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 +26 -0
- anydi/ext/typer.py +133 -0
- {anydi-0.59.0.dist-info → anydi-0.60.0.dist-info}/METADATA +2 -1
- {anydi-0.59.0.dist-info → anydi-0.60.0.dist-info}/RECORD +6 -5
- {anydi-0.59.0.dist-info → anydi-0.60.0.dist-info}/WHEEL +0 -0
- {anydi-0.59.0.dist-info → anydi-0.60.0.dist-info}/entry_points.txt +0 -0
anydi/_container.py
CHANGED
|
@@ -257,6 +257,32 @@ class Container:
|
|
|
257
257
|
# Register the scope
|
|
258
258
|
self._scopes[scope] = tuple({scope, "singleton"} | set(parents))
|
|
259
259
|
|
|
260
|
+
def get_ordered_scopes(self, scopes: set[Scope]) -> list[str]:
|
|
261
|
+
"""Get ordered list of scopes to enter."""
|
|
262
|
+
# Expand scopes to include all parent scopes
|
|
263
|
+
expanded_scopes: set[str] = set()
|
|
264
|
+
for scope in scopes:
|
|
265
|
+
if scope == "transient":
|
|
266
|
+
continue
|
|
267
|
+
elif scope == "singleton":
|
|
268
|
+
expanded_scopes.add("singleton")
|
|
269
|
+
else:
|
|
270
|
+
# Add the scope and all its parents from container._scopes
|
|
271
|
+
expanded_scopes.update(self._scopes[scope])
|
|
272
|
+
|
|
273
|
+
# Separate singleton from other scopes
|
|
274
|
+
has_singleton = "singleton" in expanded_scopes
|
|
275
|
+
other_scopes = expanded_scopes - {"singleton"}
|
|
276
|
+
|
|
277
|
+
# Sort other scopes by dependency depth (parents before children)
|
|
278
|
+
# Scopes with fewer parents come first
|
|
279
|
+
ordered_scopes = sorted(
|
|
280
|
+
other_scopes, key=lambda scope: len(self._scopes[scope])
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
# Return with singleton first if needed
|
|
284
|
+
return ["singleton", *ordered_scopes] if has_singleton else ordered_scopes
|
|
285
|
+
|
|
260
286
|
# == Provider Registry ==
|
|
261
287
|
|
|
262
288
|
def register(
|
anydi/ext/typer.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""AnyDI Typer extension."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import functools
|
|
7
|
+
import inspect
|
|
8
|
+
from collections.abc import Awaitable, Callable
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import anyio
|
|
12
|
+
from typer import Typer
|
|
13
|
+
|
|
14
|
+
from anydi import Container, Scope
|
|
15
|
+
|
|
16
|
+
__all__ = ["install"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _wrap_async_callback_no_injection(callback: Callable[..., Any]) -> Any:
|
|
20
|
+
"""Wrap async callback without injection in anyio.run()."""
|
|
21
|
+
|
|
22
|
+
@functools.wraps(callback)
|
|
23
|
+
def async_no_injection_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
24
|
+
return anyio.run(callback, *args, **kwargs)
|
|
25
|
+
|
|
26
|
+
return async_no_injection_wrapper
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _wrap_async_callback_with_injection(
|
|
30
|
+
callback: Callable[..., Awaitable[Any]],
|
|
31
|
+
container: Container,
|
|
32
|
+
sig: inspect.Signature,
|
|
33
|
+
non_injected_params: set[inspect.Parameter],
|
|
34
|
+
scopes: set[Scope],
|
|
35
|
+
) -> Any:
|
|
36
|
+
"""Wrap async callback with injection in anyio.run()."""
|
|
37
|
+
|
|
38
|
+
@functools.wraps(callback)
|
|
39
|
+
def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
40
|
+
async def _run() -> Any:
|
|
41
|
+
ordered_scopes = container.get_ordered_scopes(scopes)
|
|
42
|
+
|
|
43
|
+
async with contextlib.AsyncExitStack() as stack:
|
|
44
|
+
# Start scoped contexts in dependency order
|
|
45
|
+
for scope in ordered_scopes:
|
|
46
|
+
if scope == "singleton":
|
|
47
|
+
await stack.enter_async_context(container)
|
|
48
|
+
else:
|
|
49
|
+
await stack.enter_async_context(
|
|
50
|
+
container.ascoped_context(scope)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
return await container.run(callback, *args, **kwargs)
|
|
54
|
+
|
|
55
|
+
return anyio.run(_run)
|
|
56
|
+
|
|
57
|
+
# Update the wrapper's signature to only show non-injected parameters to Typer
|
|
58
|
+
async_wrapper.__signature__ = sig.replace(parameters=non_injected_params) # type: ignore
|
|
59
|
+
|
|
60
|
+
return async_wrapper
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _process_callback(callback: Callable[..., Any], container: Container) -> Any:
|
|
64
|
+
"""Validate and wrap a callback for dependency injection."""
|
|
65
|
+
sig = inspect.signature(callback, eval_str=True)
|
|
66
|
+
injected_param_names: set[str] = set()
|
|
67
|
+
non_injected_params: set[inspect.Parameter] = set()
|
|
68
|
+
scopes: set[Scope] = set()
|
|
69
|
+
|
|
70
|
+
# Validate parameters and collect which ones need injection
|
|
71
|
+
for parameter in sig.parameters.values():
|
|
72
|
+
interface, should_inject = container.validate_injected_parameter(
|
|
73
|
+
parameter, call=callback
|
|
74
|
+
)
|
|
75
|
+
if should_inject:
|
|
76
|
+
injected_param_names.add(parameter.name)
|
|
77
|
+
scopes.add(container.providers[interface].scope)
|
|
78
|
+
else:
|
|
79
|
+
non_injected_params.add(parameter)
|
|
80
|
+
|
|
81
|
+
# If no parameters need injection and callback is not async, return original
|
|
82
|
+
if not injected_param_names and not inspect.iscoroutinefunction(callback):
|
|
83
|
+
return callback
|
|
84
|
+
|
|
85
|
+
# If async callback with no injection, just wrap in anyio.run()
|
|
86
|
+
if not injected_param_names and inspect.iscoroutinefunction(callback):
|
|
87
|
+
return _wrap_async_callback_no_injection(callback)
|
|
88
|
+
|
|
89
|
+
# Handle async callbacks - wrap them in anyio.run() for Typer
|
|
90
|
+
if inspect.iscoroutinefunction(callback):
|
|
91
|
+
return _wrap_async_callback_with_injection(
|
|
92
|
+
callback, container, sig, non_injected_params, scopes
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
@functools.wraps(callback)
|
|
96
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
97
|
+
ordered_scopes = container.get_ordered_scopes(scopes)
|
|
98
|
+
|
|
99
|
+
with contextlib.ExitStack() as stack:
|
|
100
|
+
# Start scoped contexts in dependency order
|
|
101
|
+
for scope in ordered_scopes:
|
|
102
|
+
if scope == "singleton":
|
|
103
|
+
stack.enter_context(container)
|
|
104
|
+
else:
|
|
105
|
+
stack.enter_context(container.scoped_context(scope))
|
|
106
|
+
|
|
107
|
+
return container.run(callback, *args, **kwargs)
|
|
108
|
+
|
|
109
|
+
# Update the wrapper's signature to only show non-injected parameters to Typer
|
|
110
|
+
wrapper.__signature__ = sig.replace(parameters=non_injected_params) # type: ignore
|
|
111
|
+
|
|
112
|
+
return wrapper
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def install(app: Typer, container: Container) -> None:
|
|
116
|
+
"""Install AnyDI into a Typer application."""
|
|
117
|
+
# Process main callback if exists
|
|
118
|
+
if app.registered_callback:
|
|
119
|
+
callback = app.registered_callback.callback
|
|
120
|
+
if callback:
|
|
121
|
+
app.registered_callback.callback = _process_callback(callback, container)
|
|
122
|
+
|
|
123
|
+
# Process all registered commands
|
|
124
|
+
for command_info in app.registered_commands:
|
|
125
|
+
callback = command_info.callback
|
|
126
|
+
if callback:
|
|
127
|
+
command_info.callback = _process_callback(callback, container)
|
|
128
|
+
|
|
129
|
+
# Process nested Typer groups
|
|
130
|
+
for group_info in app.registered_groups:
|
|
131
|
+
# Recursively install for nested Typer apps
|
|
132
|
+
if group_info.typer_instance:
|
|
133
|
+
install(group_info.typer_instance, container)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: anydi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.60.0
|
|
4
4
|
Summary: Dependency Injection library
|
|
5
5
|
Keywords: dependency injection,dependencies,di,async,asyncio,application
|
|
6
6
|
Author: Anton Ruhlov
|
|
@@ -280,6 +280,7 @@ Ready to learn more? Check out these resources:
|
|
|
280
280
|
- [FastAPI](https://anydi.readthedocs.io/en/latest/extensions/fastapi/) - Build modern APIs with automatic dependency injection
|
|
281
281
|
- [Django](https://anydi.readthedocs.io/en/latest/extensions/django/) - Integrate with Django and Django Ninja
|
|
282
282
|
- [FastStream](https://anydi.readthedocs.io/en/latest/extensions/faststream/) - Message broker applications
|
|
283
|
+
- [Typer](https://anydi.readthedocs.io/en/latest/extensions/typer/) - CLI applications with async support
|
|
283
284
|
- [Pydantic Settings](https://anydi.readthedocs.io/en/latest/extensions/pydantic_settings/) - Configuration management
|
|
284
285
|
|
|
285
286
|
**Full Documentation:**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
anydi/__init__.py,sha256=bQKzn9qfNnIMi1m3J-DdSknSDwNg8j08fdQg_-Edkto,613
|
|
2
2
|
anydi/_async_lock.py,sha256=3dwZr0KthXFYha0XKMyXf8jMmGb1lYoNC0O5w29V9ic,1104
|
|
3
|
-
anydi/_container.py,sha256=
|
|
3
|
+
anydi/_container.py,sha256=r7qcUCu4KO0YLPaUO5SEgUwnNOcZsP7aVDZ1oDTi0kA,27093
|
|
4
4
|
anydi/_context.py,sha256=-9QqeMWo9OpZVXZxZCQgIsswggl3Ch7lgx1KiFX_ezc,3752
|
|
5
5
|
anydi/_decorators.py,sha256=J3W261ZAG7q4XKm4tbAv1wsWr9ysx9_5MUbUvSJB_MQ,2809
|
|
6
6
|
anydi/_injector.py,sha256=IxKTh2rzMHrsW554tbiJl33Hb5sRGKYY_NU1rC4UvxE,4378
|
|
@@ -17,9 +17,10 @@ anydi/ext/pydantic_settings.py,sha256=jVJZ1wPaPpsxdNPlJj9yq282ebqLZ9tckWpZ0eIwWL
|
|
|
17
17
|
anydi/ext/pytest_plugin.py,sha256=M54DkA-KxD9GqLnXdoCyn-Qur2c44MB6d0AgJuYCZ5w,16171
|
|
18
18
|
anydi/ext/starlette/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
19
|
anydi/ext/starlette/middleware.py,sha256=9CQtGg5ZzUz2gFSzJr8U4BWzwNjK8XMctm3n52M77Z0,792
|
|
20
|
+
anydi/ext/typer.py,sha256=3_3Q1fhxx7qElB6AQ_8dZUe5gIrrBHw7PbznAEzW5wA,4761
|
|
20
21
|
anydi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
22
|
anydi/testing.py,sha256=cHg3mMScZbEep9smRqSNQ81BZMQOkyugHe8TvKdPnEg,1347
|
|
22
|
-
anydi-0.
|
|
23
|
-
anydi-0.
|
|
24
|
-
anydi-0.
|
|
25
|
-
anydi-0.
|
|
23
|
+
anydi-0.60.0.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
|
|
24
|
+
anydi-0.60.0.dist-info/entry_points.txt,sha256=AgOcQYM5KyS4D37QcYb00tiid0QA-pD1VrjHHq4QAps,44
|
|
25
|
+
anydi-0.60.0.dist-info/METADATA,sha256=_KyXL81AfiP_HtBSOQmdVVxPaV7jqt_KPgWOLPmDK-I,8007
|
|
26
|
+
anydi-0.60.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|