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 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.59.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=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=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.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.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