anydi 0.59.0__py3-none-any.whl → 0.60.1__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 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,139 @@
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
+ from anydi._decorators import is_provided
19
+
20
+
21
+ def _wrap_async_callback_no_injection(callback: Callable[..., Any]) -> Any:
22
+ """Wrap async callback without injection in anyio.run()."""
23
+
24
+ @functools.wraps(callback)
25
+ def async_no_injection_wrapper(*args: Any, **kwargs: Any) -> Any:
26
+ return anyio.run(callback, *args, **kwargs)
27
+
28
+ return async_no_injection_wrapper
29
+
30
+
31
+ def _wrap_async_callback_with_injection(
32
+ callback: Callable[..., Awaitable[Any]],
33
+ container: Container,
34
+ sig: inspect.Signature,
35
+ non_injected_params: set[inspect.Parameter],
36
+ scopes: set[Scope],
37
+ ) -> Any:
38
+ """Wrap async callback with injection in anyio.run()."""
39
+
40
+ @functools.wraps(callback)
41
+ def async_wrapper(*args: Any, **kwargs: Any) -> Any:
42
+ async def _run() -> Any:
43
+ ordered_scopes = container.get_ordered_scopes(scopes)
44
+
45
+ async with contextlib.AsyncExitStack() as stack:
46
+ # Start scoped contexts in dependency order
47
+ for scope in ordered_scopes:
48
+ if scope == "singleton":
49
+ await stack.enter_async_context(container)
50
+ else:
51
+ await stack.enter_async_context(
52
+ container.ascoped_context(scope)
53
+ )
54
+
55
+ return await container.run(callback, *args, **kwargs)
56
+
57
+ return anyio.run(_run)
58
+
59
+ # Update the wrapper's signature to only show non-injected parameters to Typer
60
+ async_wrapper.__signature__ = sig.replace(parameters=non_injected_params) # type: ignore
61
+
62
+ return async_wrapper
63
+
64
+
65
+ def _process_callback(callback: Callable[..., Any], container: Container) -> Any: # noqa: C901
66
+ """Validate and wrap a callback for dependency injection."""
67
+ sig = inspect.signature(callback, eval_str=True)
68
+ injected_param_names: set[str] = set()
69
+ non_injected_params: set[inspect.Parameter] = set()
70
+ scopes: set[Scope] = set()
71
+
72
+ # Validate parameters and collect which ones need injection
73
+ for parameter in sig.parameters.values():
74
+ interface, should_inject = container.validate_injected_parameter(
75
+ parameter, call=callback
76
+ )
77
+ if should_inject:
78
+ injected_param_names.add(parameter.name)
79
+ try:
80
+ scopes.add(container.providers[interface].scope)
81
+ except KeyError:
82
+ if inspect.isclass(interface) and is_provided(interface):
83
+ scopes.add(interface.__provided__["scope"])
84
+ else:
85
+ non_injected_params.add(parameter)
86
+
87
+ # If no parameters need injection and callback is not async, return original
88
+ if not injected_param_names and not inspect.iscoroutinefunction(callback):
89
+ return callback
90
+
91
+ # If async callback with no injection, just wrap in anyio.run()
92
+ if not injected_param_names and inspect.iscoroutinefunction(callback):
93
+ return _wrap_async_callback_no_injection(callback)
94
+
95
+ # Handle async callbacks - wrap them in anyio.run() for Typer
96
+ if inspect.iscoroutinefunction(callback):
97
+ return _wrap_async_callback_with_injection(
98
+ callback, container, sig, non_injected_params, scopes
99
+ )
100
+
101
+ @functools.wraps(callback)
102
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
103
+ ordered_scopes = container.get_ordered_scopes(scopes)
104
+
105
+ with contextlib.ExitStack() as stack:
106
+ # Start scoped contexts in dependency order
107
+ for scope in ordered_scopes:
108
+ if scope == "singleton":
109
+ stack.enter_context(container)
110
+ else:
111
+ stack.enter_context(container.scoped_context(scope))
112
+
113
+ return container.run(callback, *args, **kwargs)
114
+
115
+ # Update the wrapper's signature to only show non-injected parameters to Typer
116
+ wrapper.__signature__ = sig.replace(parameters=non_injected_params) # type: ignore
117
+
118
+ return wrapper
119
+
120
+
121
+ def install(app: Typer, container: Container) -> None:
122
+ """Install AnyDI into a Typer application."""
123
+ # Process main callback if exists
124
+ if app.registered_callback:
125
+ callback = app.registered_callback.callback
126
+ if callback:
127
+ app.registered_callback.callback = _process_callback(callback, container)
128
+
129
+ # Process all registered commands
130
+ for command_info in app.registered_commands:
131
+ callback = command_info.callback
132
+ if callback:
133
+ command_info.callback = _process_callback(callback, container)
134
+
135
+ # Process nested Typer groups
136
+ for group_info in app.registered_groups:
137
+ # Recursively install for nested Typer apps
138
+ if group_info.typer_instance:
139
+ install(group_info.typer_instance, container)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anydi
3
- Version: 0.59.0
3
+ Version: 0.60.1
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=j8XM5UYw3PVwPq92TMtEwLwqcMObs-9ZTGAEsTj4caE,26023
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=__Ve50wRcxmLoCHxHg-75hs6frySGoRvv2Q2TnLElOg,5006
20
21
  anydi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
22
  anydi/testing.py,sha256=cHg3mMScZbEep9smRqSNQ81BZMQOkyugHe8TvKdPnEg,1347
22
- anydi-0.59.0.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
23
- anydi-0.59.0.dist-info/entry_points.txt,sha256=AgOcQYM5KyS4D37QcYb00tiid0QA-pD1VrjHHq4QAps,44
24
- anydi-0.59.0.dist-info/METADATA,sha256=Hp5PaKyiY5MTVyJiaUG7KH6B7n_74IXnxTUdRVypHUY,7901
25
- anydi-0.59.0.dist-info/RECORD,,
23
+ anydi-0.60.1.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
24
+ anydi-0.60.1.dist-info/entry_points.txt,sha256=AgOcQYM5KyS4D37QcYb00tiid0QA-pD1VrjHHq4QAps,44
25
+ anydi-0.60.1.dist-info/METADATA,sha256=35zPm0rrXV9B_swYvWOzRjCKH36M4tONVeeGlgaXOMI,8007
26
+ anydi-0.60.1.dist-info/RECORD,,
File without changes