anydi 0.57.0__tar.gz → 0.58.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anydi
3
- Version: 0.57.0
3
+ Version: 0.58.0
4
4
  Summary: Dependency Injection library
5
5
  Keywords: dependency injection,dependencies,di,async,asyncio,application
6
6
  Author: Anton Ruhlov
@@ -58,9 +58,9 @@ The key features are:
58
58
 
59
59
  * **Type-safe**: Dependency resolution is driven by type hints.
60
60
  * **Async-ready**: Works the same for sync and async providers or injections.
61
- * **Scoped**: Built-in singleton, transient, and request lifetimes.
61
+ * **Scoped**: Built-in singleton, transient, and request scopes, plus custom scopes.
62
62
  * **Simple**: Small surface area keeps boilerplate low.
63
- * **Fast**: Resolver still adds only microseconds of overhead.
63
+ * **Fast**: Has minimal overhead and resolves dependencies quickly.
64
64
  * **Named**: `Annotated[...]` makes multiple bindings per type simple.
65
65
  * **Managed**: Providers can open/close resources via context managers.
66
66
  * **Modular**: Compose containers or modules for large apps.
@@ -74,7 +74,7 @@ The key features are:
74
74
  pip install anydi
75
75
  ```
76
76
 
77
- ## Comprehensive Example
77
+ ## Quick Example
78
78
 
79
79
  ### Define a Service (`app/services.py`)
80
80
 
@@ -264,3 +264,23 @@ urlpatterns = [
264
264
  path("api/", api.urls),
265
265
  ]
266
266
  ```
267
+
268
+ ## What's Next?
269
+
270
+ Ready to learn more? Check out these resources:
271
+
272
+ **Core Documentation:**
273
+ - [Core Concepts](https://anydi.readthedocs.io/en/latest/concepts/) - Understand containers, providers, scopes, and dependency injection
274
+ - [Providers](https://anydi.readthedocs.io/en/latest/usage/providers/) - Learn about registration, named providers, and resource management
275
+ - [Scopes](https://anydi.readthedocs.io/en/latest/usage/scopes/) - Master lifecycle management with built-in and custom scopes
276
+ - [Dependency Injection](https://anydi.readthedocs.io/en/latest/usage/injection/) - Explore injection patterns and techniques
277
+ - [Testing](https://anydi.readthedocs.io/en/latest/usage/testing/) - Write testable code with provider overrides
278
+
279
+ **Framework Integrations:**
280
+ - [FastAPI](https://anydi.readthedocs.io/en/latest/extensions/fastapi/) - Build modern APIs with automatic dependency injection
281
+ - [Django](https://anydi.readthedocs.io/en/latest/extensions/django/) - Integrate with Django and Django Ninja
282
+ - [FastStream](https://anydi.readthedocs.io/en/latest/extensions/faststream/) - Message broker applications
283
+ - [Pydantic Settings](https://anydi.readthedocs.io/en/latest/extensions/pydantic_settings/) - Configuration management
284
+
285
+ **Full Documentation:**
286
+ - [Read the Docs](https://anydi.readthedocs.io/) - Complete documentation with examples and guides
@@ -23,9 +23,9 @@ The key features are:
23
23
 
24
24
  * **Type-safe**: Dependency resolution is driven by type hints.
25
25
  * **Async-ready**: Works the same for sync and async providers or injections.
26
- * **Scoped**: Built-in singleton, transient, and request lifetimes.
26
+ * **Scoped**: Built-in singleton, transient, and request scopes, plus custom scopes.
27
27
  * **Simple**: Small surface area keeps boilerplate low.
28
- * **Fast**: Resolver still adds only microseconds of overhead.
28
+ * **Fast**: Has minimal overhead and resolves dependencies quickly.
29
29
  * **Named**: `Annotated[...]` makes multiple bindings per type simple.
30
30
  * **Managed**: Providers can open/close resources via context managers.
31
31
  * **Modular**: Compose containers or modules for large apps.
@@ -39,7 +39,7 @@ The key features are:
39
39
  pip install anydi
40
40
  ```
41
41
 
42
- ## Comprehensive Example
42
+ ## Quick Example
43
43
 
44
44
  ### Define a Service (`app/services.py`)
45
45
 
@@ -229,3 +229,23 @@ urlpatterns = [
229
229
  path("api/", api.urls),
230
230
  ]
231
231
  ```
232
+
233
+ ## What's Next?
234
+
235
+ Ready to learn more? Check out these resources:
236
+
237
+ **Core Documentation:**
238
+ - [Core Concepts](https://anydi.readthedocs.io/en/latest/concepts/) - Understand containers, providers, scopes, and dependency injection
239
+ - [Providers](https://anydi.readthedocs.io/en/latest/usage/providers/) - Learn about registration, named providers, and resource management
240
+ - [Scopes](https://anydi.readthedocs.io/en/latest/usage/scopes/) - Master lifecycle management with built-in and custom scopes
241
+ - [Dependency Injection](https://anydi.readthedocs.io/en/latest/usage/injection/) - Explore injection patterns and techniques
242
+ - [Testing](https://anydi.readthedocs.io/en/latest/usage/testing/) - Write testable code with provider overrides
243
+
244
+ **Framework Integrations:**
245
+ - [FastAPI](https://anydi.readthedocs.io/en/latest/extensions/fastapi/) - Build modern APIs with automatic dependency injection
246
+ - [Django](https://anydi.readthedocs.io/en/latest/extensions/django/) - Integrate with Django and Django Ninja
247
+ - [FastStream](https://anydi.readthedocs.io/en/latest/extensions/faststream/) - Message broker applications
248
+ - [Pydantic Settings](https://anydi.readthedocs.io/en/latest/extensions/pydantic_settings/) - Configuration management
249
+
250
+ **Full Documentation:**
251
+ - [Read the Docs](https://anydi.readthedocs.io/) - Complete documentation with examples and guides
@@ -9,7 +9,7 @@ import logging
9
9
  import types
10
10
  import uuid
11
11
  from collections import defaultdict
12
- from collections.abc import AsyncIterator, Callable, Iterable, Iterator
12
+ from collections.abc import AsyncIterator, Callable, Iterable, Iterator, Sequence
13
13
  from contextvars import ContextVar
14
14
  from typing import Any, TypeVar, get_args, get_origin, overload
15
15
 
@@ -22,24 +22,11 @@ from ._module import ModuleDef, ModuleRegistrar
22
22
  from ._provider import Provider, ProviderDef, ProviderKind, ProviderParameter
23
23
  from ._resolver import Resolver
24
24
  from ._scanner import PackageOrIterable, Scanner
25
- from ._types import (
26
- NOT_SET,
27
- Event,
28
- Scope,
29
- is_event_type,
30
- is_iterator_type,
31
- is_none_type,
32
- )
25
+ from ._types import NOT_SET, Event, Scope, is_event_type, is_iterator_type, is_none_type
33
26
 
34
27
  T = TypeVar("T", bound=Any)
35
28
  P = ParamSpec("P")
36
29
 
37
- ALLOWED_SCOPES: dict[Scope, list[Scope]] = {
38
- "singleton": ["singleton"],
39
- "request": ["request", "singleton"],
40
- "transient": ["transient", "request", "singleton"],
41
- }
42
-
43
30
 
44
31
  class Container:
45
32
  """AnyDI is a dependency injection container."""
@@ -53,11 +40,14 @@ class Container:
53
40
  ) -> None:
54
41
  self._providers: dict[Any, Provider] = {}
55
42
  self._logger = logger or logging.getLogger(__name__)
43
+ self._scopes: dict[str, Sequence[str]] = {
44
+ "transient": ("transient", "singleton"),
45
+ "singleton": ("singleton",),
46
+ }
47
+
56
48
  self._resources: dict[str, list[Any]] = defaultdict(list)
57
49
  self._singleton_context = InstanceContext()
58
- self._request_context_var: ContextVar[InstanceContext | None] = ContextVar(
59
- "request_context", default=None
60
- )
50
+ self._scoped_context: dict[str, ContextVar[InstanceContext]] = {}
61
51
 
62
52
  # Components
63
53
  self._resolver = Resolver(self)
@@ -65,6 +55,9 @@ class Container:
65
55
  self._modules = ModuleRegistrar(self)
66
56
  self._scanner = Scanner(self)
67
57
 
58
+ # Register default scopes
59
+ self.register_scope("request")
60
+
68
61
  # Register providers
69
62
  providers = providers or []
70
63
  for provider in providers:
@@ -141,54 +134,128 @@ class Container:
141
134
  await self._singleton_context.aclose()
142
135
 
143
136
  @contextlib.contextmanager
144
- def request_context(self) -> Iterator[InstanceContext]:
137
+ def scoped_context(self, scope: str) -> Iterator[InstanceContext]:
145
138
  """Obtain a context manager for the request-scoped context."""
146
- context = InstanceContext()
139
+ context_var = self._get_scoped_context_var(scope)
147
140
 
148
- token = self._request_context_var.set(context)
141
+ # Check if context already exists (re-entering same scope)
142
+ context = context_var.get(None)
143
+ if context is not None:
144
+ # Reuse existing context, don't create a new one
145
+ yield context
146
+ return
147
+
148
+ # Create new context
149
+ context = InstanceContext()
150
+ token = context_var.set(context)
149
151
 
150
152
  # Resolve all request resources
151
- for interface in self._resources.get("request", []):
153
+ for interface in self._resources.get(scope, []):
152
154
  if not is_event_type(interface):
153
155
  continue
154
156
  self.resolve(interface)
155
157
 
156
158
  with context:
157
159
  yield context
158
- self._request_context_var.reset(token)
160
+ context_var.reset(token)
159
161
 
160
162
  @contextlib.asynccontextmanager
161
- async def arequest_context(self) -> AsyncIterator[InstanceContext]:
162
- """Obtain an async context manager for the request-scoped context."""
163
- context = InstanceContext()
163
+ async def ascoped_context(self, scope: str) -> AsyncIterator[InstanceContext]:
164
+ """Obtain a context manager for the specified scoped context."""
165
+ context_var = self._get_scoped_context_var(scope)
166
+
167
+ # Check if context already exists (re-entering same scope)
168
+ context = context_var.get(None)
169
+ if context is not None:
170
+ # Reuse existing context, don't create a new one
171
+ yield context
172
+ return
164
173
 
165
- token = self._request_context_var.set(context)
174
+ # Create new context
175
+ context = InstanceContext()
176
+ token = context_var.set(context)
166
177
 
167
- for interface in self._resources.get("request", []):
178
+ # Resolve all request resources
179
+ for interface in self._resources.get(scope, []):
168
180
  if not is_event_type(interface):
169
181
  continue
170
182
  await self.aresolve(interface)
171
183
 
172
184
  async with context:
173
185
  yield context
174
- self._request_context_var.reset(token)
186
+ context_var.reset(token)
187
+
188
+ @contextlib.contextmanager
189
+ def request_context(self) -> Iterator[InstanceContext]:
190
+ """Obtain a context manager for the request-scoped context."""
191
+ with self.scoped_context("request") as context:
192
+ yield context
193
+
194
+ @contextlib.asynccontextmanager
195
+ async def arequest_context(self) -> AsyncIterator[InstanceContext]:
196
+ """Obtain an async context manager for the request-scoped context."""
197
+ async with self.ascoped_context("request") as context:
198
+ yield context
175
199
 
176
- def _get_request_context(self) -> InstanceContext:
177
- """Get the current request context."""
178
- request_context = self._request_context_var.get()
179
- if request_context is None:
200
+ def _get_scoped_context(self, scope: str) -> InstanceContext:
201
+ scoped_context_var = self._get_scoped_context_var(scope)
202
+ try:
203
+ scoped_context = scoped_context_var.get()
204
+ except LookupError as exc:
180
205
  raise LookupError(
181
- "The request context has not been started. Please ensure that "
182
- "the request context is properly initialized before attempting "
206
+ f"The {scope} context has not been started. Please ensure that "
207
+ f"the {scope} context is properly initialized before attempting "
183
208
  "to use it."
209
+ ) from exc
210
+ return scoped_context
211
+
212
+ def _get_scoped_context_var(self, scope: str) -> ContextVar[InstanceContext]:
213
+ """Get the context variable for the specified scope."""
214
+ # Validate that scope is registered and not reserved
215
+ if scope in ("transient", "singleton"):
216
+ raise ValueError(
217
+ f"Cannot get context variable for reserved scope `{scope}`."
184
218
  )
185
- return request_context
219
+ if scope not in self._scopes:
220
+ raise ValueError(
221
+ f"Cannot get context variable for not registered scope `{scope}`. "
222
+ f"Please register the scope first using register_scope()."
223
+ )
224
+
225
+ if scope not in self._scoped_context:
226
+ self._scoped_context[scope] = ContextVar(f"{scope}_context")
227
+ return self._scoped_context[scope]
186
228
 
187
229
  def _get_instance_context(self, scope: Scope) -> InstanceContext:
188
230
  """Get the instance context for the specified scope."""
189
231
  if scope == "singleton":
190
232
  return self._singleton_context
191
- return self._get_request_context()
233
+ return self._get_scoped_context(scope)
234
+
235
+ # == Scopes == #
236
+
237
+ def register_scope(
238
+ self, scope: str, *, parents: Sequence[str] | None = None
239
+ ) -> None:
240
+ """Register a new scope with the specified parents."""
241
+ # Check if the scope is reserved
242
+ if scope in ("transient", "singleton"):
243
+ raise ValueError(
244
+ f"The scope `{scope}` is reserved and cannot be overridden."
245
+ )
246
+
247
+ # Check if the scope is already registered
248
+ if scope in self._scopes:
249
+ raise ValueError(f"The scope `{scope}` is already registered.")
250
+
251
+ # Validate parents
252
+ parents = parents or []
253
+ for parent in parents:
254
+ if parent not in self._scopes:
255
+ raise ValueError(f"The parent scope `{parent}` is not registered.")
256
+
257
+ # Register the scope
258
+ self._scopes[scope] = tuple({scope, "singleton"} | set(parents))
192
259
 
193
260
  # == Provider Registry ==
194
261
 
@@ -300,7 +367,7 @@ class Container:
300
367
  unresolved_parameter = None
301
368
  unresolved_exc: LookupError | None = None
302
369
  parameters: list[ProviderParameter] = []
303
- scopes: dict[Scope, Provider] = {}
370
+ scope_provider: dict[Scope, Provider] = {}
304
371
 
305
372
  for parameter in signature.parameters.values():
306
373
  if parameter.annotation is inspect.Parameter.empty:
@@ -331,8 +398,8 @@ class Container:
331
398
  continue
332
399
 
333
400
  # Store first provider for each scope
334
- if sub_provider.scope not in scopes:
335
- scopes[sub_provider.scope] = sub_provider
401
+ if sub_provider.scope not in scope_provider:
402
+ scope_provider[sub_provider.scope] = sub_provider
336
403
 
337
404
  parameters.append(
338
405
  ProviderParameter(
@@ -345,6 +412,18 @@ class Container:
345
412
  )
346
413
  )
347
414
 
415
+ # Check scope compatibility
416
+ # Transient scope can use any scoped dependencies
417
+ if scope != "transient":
418
+ for sub_provider in scope_provider.values():
419
+ if sub_provider.scope not in self._scopes.get(scope, []):
420
+ raise ValueError(
421
+ f"The provider `{name}` with a `{scope}` scope "
422
+ f"cannot depend on `{sub_provider}` with a "
423
+ f"`{sub_provider.scope}` scope. Please ensure all "
424
+ "providers are registered with matching scopes."
425
+ )
426
+
348
427
  # Check for unresolved parameters
349
428
  if unresolved_parameter:
350
429
  if scope not in ("singleton", "transient"):
@@ -358,15 +437,6 @@ class Container:
358
437
  f"attempting to use it."
359
438
  ) from unresolved_exc
360
439
 
361
- # Check scope compatibility
362
- for sub_provider in scopes.values():
363
- if sub_provider.scope not in ALLOWED_SCOPES.get(scope, []):
364
- raise ValueError(
365
- f"The provider `{name}` with a `{scope}` scope cannot "
366
- f"depend on `{sub_provider}` with a `{sub_provider.scope}` scope. "
367
- "Please ensure all providers are registered with matching scopes."
368
- )
369
-
370
440
  is_coroutine = kind == ProviderKind.COROUTINE
371
441
  is_generator = kind == ProviderKind.GENERATOR
372
442
  is_async_generator = kind == ProviderKind.ASYNC_GENERATOR
@@ -389,13 +459,14 @@ class Container:
389
459
  self._set_provider(provider)
390
460
  return provider
391
461
 
392
- @staticmethod
393
- def _validate_provider_scope(scope: Scope, name: str, is_resource: bool) -> None:
462
+ def _validate_provider_scope(
463
+ self, scope: Scope, name: str, is_resource: bool
464
+ ) -> None:
394
465
  """Validate the provider scope."""
395
- if scope not in ALLOWED_SCOPES:
466
+ if scope not in self._scopes:
396
467
  raise ValueError(
397
468
  f"The provider `{name}` scope is invalid. Only the following "
398
- f"scopes are supported: {', '.join(ALLOWED_SCOPES)}. "
469
+ f"scopes are supported: {', '.join(self._scopes.keys())}. "
399
470
  "Please use one of the supported scopes when registering a provider."
400
471
  )
401
472
  if scope == "transient" and is_resource:
@@ -453,7 +453,7 @@ class Resolver:
453
453
  create_lines.append(" context.enter(inst)")
454
454
 
455
455
  create_lines.append(" if context is not None and store:")
456
- create_lines.append(" context.set(_interface, inst)")
456
+ create_lines.append(" context._instances[_interface] = inst")
457
457
 
458
458
  # Wrap instance if in override mode (only for override version)
459
459
  if with_override:
@@ -470,18 +470,28 @@ class Resolver:
470
470
  resolver_lines.append("def _resolver(container, context=None):")
471
471
 
472
472
  # Only define NOT_SET_ if we actually need it
473
- needs_not_set = scope in ("singleton", "request")
473
+ needs_not_set = scope != "transient"
474
474
  if needs_not_set:
475
475
  resolver_lines.append(" NOT_SET_ = _NOT_SET")
476
476
 
477
477
  if scope == "singleton":
478
478
  resolver_lines.append(" if context is None:")
479
479
  resolver_lines.append(" context = container._singleton_context")
480
- elif scope == "request":
481
- resolver_lines.append(" if context is None:")
482
- resolver_lines.append(" context = container._get_request_context()")
483
- else:
480
+ elif scope == "transient":
484
481
  resolver_lines.append(" context = None")
482
+ else:
483
+ # Custom scopes (including "request")
484
+ # Inline context retrieval to avoid method call overhead
485
+ resolver_lines.append(" if context is None:")
486
+ resolver_lines.append(" try:")
487
+ resolver_lines.append(" context = _scoped_context_var.get()")
488
+ resolver_lines.append(" except LookupError:")
489
+ resolver_lines.append(
490
+ f" raise LookupError("
491
+ f"'The {scope} context has not been started. "
492
+ f"Please ensure that the {scope} context is properly initialized "
493
+ f"before attempting to use it.')"
494
+ )
485
495
 
486
496
  if scope == "singleton":
487
497
  if with_override:
@@ -507,33 +517,36 @@ class Resolver:
507
517
  store=True,
508
518
  indent=" ",
509
519
  )
510
- elif scope == "request":
520
+ elif scope == "transient":
521
+ # Transient scope
511
522
  if with_override:
512
- self._add_override_check(resolver_lines)
513
-
514
- # Fast path: check cached instance
515
- resolver_lines.append(" inst = context.get(_interface)")
516
- resolver_lines.append(" if inst is not NOT_SET_:")
517
- resolver_lines.append(" return inst")
523
+ self._add_override_check(resolver_lines, include_not_set=True)
518
524
 
519
525
  self._add_create_call(
520
526
  resolver_lines,
521
527
  is_async=is_async,
522
528
  with_override=with_override,
523
- context="context",
524
- store=True,
529
+ context="",
530
+ store=False,
525
531
  )
526
532
  else:
527
- # Transient scope
533
+ # Custom scopes (including "request")
528
534
  if with_override:
529
- self._add_override_check(resolver_lines, include_not_set=True)
535
+ self._add_override_check(resolver_lines)
536
+
537
+ # Fast path: check cached instance (inline dict access for speed)
538
+ resolver_lines.append(
539
+ " inst = context._instances.get(_interface, NOT_SET_)"
540
+ )
541
+ resolver_lines.append(" if inst is not NOT_SET_:")
542
+ resolver_lines.append(" return inst")
530
543
 
531
544
  self._add_create_call(
532
545
  resolver_lines,
533
546
  is_async=is_async,
534
547
  with_override=with_override,
535
- context="",
536
- store=False,
548
+ context="context",
549
+ store=True,
537
550
  )
538
551
 
539
552
  create_resolver_lines: list[str] = []
@@ -552,18 +565,26 @@ class Resolver:
552
565
 
553
566
  if scope == "singleton":
554
567
  create_resolver_lines.append(" context = container._singleton_context")
555
- elif scope == "request":
568
+ elif scope == "transient":
569
+ create_resolver_lines.append(" context = None")
570
+ else:
571
+ # Custom scopes (including "request")
572
+ # Inline context retrieval to avoid method call overhead
573
+ create_resolver_lines.append(" try:")
574
+ create_resolver_lines.append(" context = _scoped_context_var.get()")
575
+ create_resolver_lines.append(" except LookupError:")
556
576
  create_resolver_lines.append(
557
- " context = container._get_request_context()"
577
+ f" raise LookupError("
578
+ f"'The {scope} context has not been started. "
579
+ f"Please ensure that the {scope} context is properly initialized "
580
+ f"before attempting to use it.')"
558
581
  )
559
- else:
560
- create_resolver_lines.append(" context = None")
561
582
 
562
583
  if with_override:
563
584
  self._add_override_check(create_resolver_lines, include_not_set=True)
564
585
 
565
586
  # Determine context for create call
566
- context_arg = "context" if scope in ("singleton", "request") else ""
587
+ context_arg = "context" if scope != "transient" else ""
567
588
 
568
589
  self._add_create_call(
569
590
  create_resolver_lines,
@@ -603,6 +624,12 @@ class Resolver:
603
624
  "resolver": self,
604
625
  }
605
626
 
627
+ # For custom scopes, cache the ContextVar to avoid dictionary lookups
628
+ if scope not in ("singleton", "transient"):
629
+ ns["_scoped_context_var"] = self._container._get_scoped_context_var( # type: ignore[reportPrivateUsage]
630
+ scope
631
+ )
632
+
606
633
  # Add async-specific namespace entries
607
634
  if is_async:
608
635
  ns["_asynccontextmanager"] = contextlib.asynccontextmanager
@@ -11,7 +11,7 @@ from typing_extensions import Sentinel
11
11
 
12
12
  T = TypeVar("T")
13
13
 
14
- Scope = Literal["transient", "singleton", "request"]
14
+ Scope = Literal["transient", "singleton", "request"] | str
15
15
 
16
16
  NOT_SET = Sentinel("NOT_SET")
17
17
 
@@ -11,8 +11,8 @@ from fastapi.dependencies.models import Dependant
11
11
  from fastapi.routing import APIRoute
12
12
  from starlette.requests import Request
13
13
 
14
- from anydi import Container
15
- from anydi._types import Inject, ProvideMarker, set_provide_factory
14
+ from anydi import Container, Inject
15
+ from anydi._types import ProvideMarker, set_provide_factory
16
16
 
17
17
  from .starlette.middleware import RequestScopedMiddleware
18
18
 
@@ -5,7 +5,7 @@ from starlette.requests import Request
5
5
  from starlette.responses import Response
6
6
  from starlette.types import ASGIApp
7
7
 
8
- from anydi._container import Container
8
+ from anydi import Container
9
9
 
10
10
 
11
11
  class RequestScopedMiddleware(BaseHTTPMiddleware):
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "anydi"
3
- version = "0.57.0"
3
+ version = "0.58.0"
4
4
  description = "Dependency Injection library"
5
5
  authors = [{ name = "Anton Ruhlov", email = "antonruhlov@gmail.com" }]
6
6
  requires-python = ">=3.10.0, <3.15"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes