anydi 0.67.0__tar.gz → 0.67.2__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.
Files changed (26) hide show
  1. {anydi-0.67.0 → anydi-0.67.2}/PKG-INFO +1 -1
  2. {anydi-0.67.0 → anydi-0.67.2}/anydi/_container.py +54 -74
  3. {anydi-0.67.0 → anydi-0.67.2}/anydi/_resolver.py +2 -0
  4. {anydi-0.67.0 → anydi-0.67.2}/anydi/ext/typer.py +3 -3
  5. {anydi-0.67.0 → anydi-0.67.2}/pyproject.toml +2 -2
  6. {anydi-0.67.0 → anydi-0.67.2}/README.md +0 -0
  7. {anydi-0.67.0 → anydi-0.67.2}/anydi/__init__.py +0 -0
  8. {anydi-0.67.0 → anydi-0.67.2}/anydi/_async_lock.py +0 -0
  9. {anydi-0.67.0 → anydi-0.67.2}/anydi/_context.py +0 -0
  10. {anydi-0.67.0 → anydi-0.67.2}/anydi/_decorators.py +0 -0
  11. {anydi-0.67.0 → anydi-0.67.2}/anydi/_injector.py +0 -0
  12. {anydi-0.67.0 → anydi-0.67.2}/anydi/_marker.py +0 -0
  13. {anydi-0.67.0 → anydi-0.67.2}/anydi/_module.py +0 -0
  14. {anydi-0.67.0 → anydi-0.67.2}/anydi/_provider.py +0 -0
  15. {anydi-0.67.0 → anydi-0.67.2}/anydi/_scanner.py +0 -0
  16. {anydi-0.67.0 → anydi-0.67.2}/anydi/_types.py +0 -0
  17. {anydi-0.67.0 → anydi-0.67.2}/anydi/ext/__init__.py +0 -0
  18. {anydi-0.67.0 → anydi-0.67.2}/anydi/ext/django/__init__.py +0 -0
  19. {anydi-0.67.0 → anydi-0.67.2}/anydi/ext/fastapi.py +0 -0
  20. {anydi-0.67.0 → anydi-0.67.2}/anydi/ext/faststream.py +0 -0
  21. {anydi-0.67.0 → anydi-0.67.2}/anydi/ext/pydantic_settings.py +0 -0
  22. {anydi-0.67.0 → anydi-0.67.2}/anydi/ext/pytest_plugin.py +0 -0
  23. {anydi-0.67.0 → anydi-0.67.2}/anydi/ext/starlette/__init__.py +0 -0
  24. {anydi-0.67.0 → anydi-0.67.2}/anydi/ext/starlette/middleware.py +0 -0
  25. {anydi-0.67.0 → anydi-0.67.2}/anydi/py.typed +0 -0
  26. {anydi-0.67.0 → anydi-0.67.2}/anydi/testing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anydi
3
- Version: 0.67.0
3
+ Version: 0.67.2
4
4
  Summary: Dependency Injection library
5
5
  Keywords: dependency injection,dependencies,di,async,asyncio,application
6
6
  Author: Anton Ruhlov
@@ -23,14 +23,7 @@ from ._module import ModuleDef, ModuleRegistrar
23
23
  from ._provider import Provider, ProviderDef, ProviderKind, ProviderParameter
24
24
  from ._resolver import Resolver
25
25
  from ._scanner import PackageOrIterable, Scanner
26
- from ._types import (
27
- NOT_SET,
28
- Event,
29
- Scope,
30
- is_event_type,
31
- is_iterator_type,
32
- is_none_type,
33
- )
26
+ from ._types import NOT_SET, Event, Scope, is_event_type, is_iterator_type, is_none_type
34
27
 
35
28
  T = TypeVar("T", bound=Any)
36
29
  P = ParamSpec("P")
@@ -390,28 +383,27 @@ class Container:
390
383
  name = type_repr(call)
391
384
  kind = ProviderKind.from_call(call)
392
385
  is_class = kind == ProviderKind.CLASS
393
- is_resource = kind in (ProviderKind.GENERATOR, ProviderKind.ASYNC_GENERATOR)
386
+ is_coroutine = kind == ProviderKind.COROUTINE
387
+ is_generator = kind == ProviderKind.GENERATOR
388
+ is_async_generator = kind == ProviderKind.ASYNC_GENERATOR
389
+ is_resource = is_generator or is_async_generator
394
390
 
395
- # Validate scope if it provided
391
+ # Validate scope
396
392
  self._validate_provider_scope(scope, name, is_resource)
397
393
 
398
- # Get the signature
394
+ # Get signature and detect interface
399
395
  signature = inspect.signature(call, eval_str=True)
400
396
 
401
- # Detect the interface
402
397
  if interface is NOT_SET:
403
- if is_class:
404
- interface = call
405
- else:
406
- interface = signature.return_annotation
407
- if interface is inspect.Signature.empty:
408
- interface = None
398
+ interface = call if is_class else signature.return_annotation
399
+ if interface is inspect.Signature.empty:
400
+ interface = None
409
401
 
410
- # If the callable is an iterator, return the actual type
411
- if is_iterator_type(interface) or is_iterator_type(get_origin(interface)):
402
+ # Handle iterator types for resources
403
+ interface_origin = get_origin(interface)
404
+ if is_iterator_type(interface) or is_iterator_type(interface_origin):
412
405
  if args := get_args(interface):
413
406
  interface = args[0]
414
- # If the callable is a generator, return the resource type
415
407
  if is_none_type(interface):
416
408
  interface = type(f"Event_{uuid.uuid4().hex}", (Event,), {})
417
409
  else:
@@ -420,27 +412,27 @@ class Container:
420
412
  "without actual type argument."
421
413
  )
422
414
 
423
- # None interface is not allowed
415
+ # Validate interface
424
416
  if is_none_type(interface):
425
417
  raise TypeError(f"Missing `{name}` provider return annotation.")
426
418
 
427
- # Check for existing provider
428
419
  if interface in self._providers and not override:
429
420
  raise LookupError(
430
421
  f"The provider interface `{type_repr(interface)}` already registered."
431
422
  )
432
423
 
433
- unresolved_parameter = None
434
- unresolved_exc: LookupError | None = None
424
+ # Process parameters
435
425
  parameters: list[ProviderParameter] = []
436
426
  scope_provider: dict[Scope, Provider] = {}
437
-
438
- # Precompute constant checks
427
+ unresolved_parameter = None
428
+ unresolved_exc: LookupError | None = None
439
429
  is_scoped = scope not in ("singleton", "transient")
440
- has_defaults = defaults is not None
430
+ scope_hierarchy = self._scopes.get(scope, ()) if scope != "transient" else ()
441
431
 
442
432
  for parameter in signature.parameters.values():
443
- if parameter.annotation is inspect.Parameter.empty:
433
+ annotation = parameter.annotation
434
+
435
+ if annotation is inspect.Parameter.empty:
444
436
  raise TypeError(
445
437
  f"Missing provider `{name}` "
446
438
  f"dependency `{parameter.name}` annotation."
@@ -451,26 +443,21 @@ class Container:
451
443
  f"are not allowed in the provider `{name}`."
452
444
  )
453
445
 
454
- default = (
455
- parameter.default
456
- if parameter.default is not inspect.Parameter.empty
457
- else NOT_SET
458
- )
459
- has_default = default is not NOT_SET
446
+ has_default = parameter.default is not inspect.Parameter.empty
447
+ default = parameter.default if has_default else NOT_SET
460
448
 
461
449
  try:
462
- sub_provider = self._get_or_register_provider(parameter.annotation)
450
+ sub_provider = self._get_or_register_provider(annotation)
463
451
  except LookupError as exc:
464
- if (has_defaults and parameter.name in defaults) or has_default:
452
+ if (defaults and parameter.name in defaults) or has_default:
465
453
  continue
466
- # For scoped (request/custom) dependencies, allow unresolved parameters
467
- # that will be provided via context.set()
454
+ # For scoped dependencies, allow unresolved parameters via context.set()
468
455
  if is_scoped:
469
- self._resolver.add_unresolved(parameter.annotation)
456
+ self._resolver.add_unresolved(annotation)
470
457
  parameters.append(
471
458
  ProviderParameter(
472
459
  name=parameter.name,
473
- annotation=parameter.annotation,
460
+ annotation=annotation,
474
461
  default=default,
475
462
  has_default=has_default,
476
463
  provider=None,
@@ -482,22 +469,21 @@ class Container:
482
469
  unresolved_exc = exc
483
470
  continue
484
471
 
485
- # Store first provider for each scope
486
- if sub_provider.scope not in scope_provider:
487
- scope_provider[sub_provider.scope] = sub_provider
472
+ # Track scope providers for validation
473
+ scope_provider.setdefault(sub_provider.scope, sub_provider)
488
474
 
489
- # For scoped dependencies with same scope: if auto-registered provider
490
- # has unresolved parameters, defer to context.set() instead
475
+ # For scoped dependencies with same scope having unresolved params,
476
+ # defer to context.set() instead
491
477
  if (
492
478
  is_scoped
493
479
  and sub_provider.scope == scope
494
480
  and any(p.provider is None for p in sub_provider.parameters)
495
481
  ):
496
- self._resolver.add_unresolved(parameter.annotation)
482
+ self._resolver.add_unresolved(annotation)
497
483
  parameters.append(
498
484
  ProviderParameter(
499
485
  name=parameter.name,
500
- annotation=parameter.annotation,
486
+ annotation=annotation,
501
487
  default=default,
502
488
  has_default=has_default,
503
489
  provider=None,
@@ -506,10 +492,19 @@ class Container:
506
492
  )
507
493
  continue
508
494
 
495
+ # Validate scope compatibility inline
496
+ if scope_hierarchy and sub_provider.scope not in scope_hierarchy:
497
+ raise ValueError(
498
+ f"The provider `{name}` with a `{scope}` scope "
499
+ f"cannot depend on `{sub_provider}` with a "
500
+ f"`{sub_provider.scope}` scope. Please ensure all "
501
+ "providers are registered with matching scopes."
502
+ )
503
+
509
504
  parameters.append(
510
505
  ProviderParameter(
511
506
  name=parameter.name,
512
- annotation=parameter.annotation,
507
+ annotation=annotation,
513
508
  default=default,
514
509
  has_default=has_default,
515
510
  provider=sub_provider,
@@ -517,21 +512,11 @@ class Container:
517
512
  )
518
513
  )
519
514
 
520
- # Check scope compatibility
521
- # Transient scope can use any scoped dependencies
522
- if scope != "transient":
523
- for sub_provider in scope_provider.values():
524
- if sub_provider.scope not in self._scopes.get(scope, []):
525
- raise ValueError(
526
- f"The provider `{name}` with a `{scope}` scope "
527
- f"cannot depend on `{sub_provider}` with a "
528
- f"`{sub_provider.scope}` scope. Please ensure all "
529
- "providers are registered with matching scopes."
530
- )
531
-
532
- # Check for unresolved parameters
515
+ # Handle unresolved parameters
533
516
  if unresolved_parameter:
534
- if scope not in ("singleton", "transient"):
517
+ if is_scoped: # pragma: no cover
518
+ # Note: This branch is currently unreachable because
519
+ # unresolved_parameter is only set when is_scoped=False
535
520
  self._resolver.add_unresolved(interface)
536
521
  else:
537
522
  raise LookupError(
@@ -542,11 +527,7 @@ class Container:
542
527
  f"attempting to use it."
543
528
  ) from unresolved_exc
544
529
 
545
- is_coroutine = kind == ProviderKind.COROUTINE
546
- is_generator = kind == ProviderKind.GENERATOR
547
- is_async_generator = kind == ProviderKind.ASYNC_GENERATOR
548
- is_async = is_coroutine or is_async_generator
549
-
530
+ # Create and register provider
550
531
  provider = Provider(
551
532
  call=call,
552
533
  scope=scope,
@@ -557,13 +538,12 @@ class Container:
557
538
  is_coroutine=is_coroutine,
558
539
  is_generator=is_generator,
559
540
  is_async_generator=is_async_generator,
560
- is_async=is_async,
541
+ is_async=is_coroutine or is_async_generator,
561
542
  is_resource=is_resource,
562
543
  )
563
544
 
564
545
  self._set_provider(provider)
565
546
 
566
- # Clear cached resolvers when overriding
567
547
  if override:
568
548
  self._resolver.clear_caches()
569
549
 
@@ -589,20 +569,20 @@ class Container:
589
569
  """Get provider by interface."""
590
570
  try:
591
571
  return self._providers[interface]
592
- except KeyError as exc:
572
+ except KeyError:
593
573
  raise LookupError(
594
574
  f"The provider interface for `{type_repr(interface)}` has "
595
575
  "not been registered. Please ensure that the provider interface is "
596
576
  "properly registered before attempting to use it."
597
- ) from exc
577
+ ) from None
598
578
 
599
579
  def _get_or_register_provider(
600
580
  self, interface: Any, defaults: dict[str, Any] | None = None
601
581
  ) -> Provider:
602
582
  """Get or register a provider by interface."""
603
583
  try:
604
- return self._providers[interface]
605
- except KeyError:
584
+ return self._get_provider(interface)
585
+ except LookupError:
606
586
  if inspect.isclass(interface) and is_provided(interface):
607
587
  return self._register_provider(
608
588
  interface,
@@ -672,6 +672,8 @@ class Resolver:
672
672
 
673
673
  def _wrap_for_override(self, annotation: Any, value: Any) -> Any:
674
674
  """Hook for wrapping dependencies to enable override patching."""
675
+ if isinstance(value, InstanceProxy):
676
+ return value
675
677
  return InstanceProxy(value, interface=annotation)
676
678
 
677
679
  def _post_resolve_override(self, interface: Any, instance: Any) -> Any: # noqa: C901
@@ -42,7 +42,7 @@ def _wrap_async_callback_with_injection(
42
42
  callback: Callable[..., Awaitable[Any]],
43
43
  container: Container,
44
44
  sig: inspect.Signature,
45
- non_injected_params: set[inspect.Parameter],
45
+ non_injected_params: list[inspect.Parameter],
46
46
  scopes: set[Scope],
47
47
  ) -> Any:
48
48
  """Wrap async callback with injection in anyio.run()."""
@@ -86,7 +86,7 @@ def _process_callback(callback: Callable[..., Any], container: Container) -> Any
86
86
  """Validate and wrap a callback for dependency injection."""
87
87
  sig = inspect.signature(callback, eval_str=True)
88
88
  injected_param_names: set[str] = set()
89
- non_injected_params: set[inspect.Parameter] = set()
89
+ non_injected_params: list[inspect.Parameter] = []
90
90
  scopes: set[Scope] = set()
91
91
 
92
92
  # Validate parameters and collect which ones need injection
@@ -103,7 +103,7 @@ def _process_callback(callback: Callable[..., Any], container: Container) -> Any
103
103
  if inspect.isclass(interface) and is_provided(interface):
104
104
  scopes.add(interface.__provided__["scope"])
105
105
  else:
106
- non_injected_params.add(processed_parameter)
106
+ non_injected_params.append(processed_parameter)
107
107
 
108
108
  # If no parameters need injection and callback is not async, return original
109
109
  if not injected_param_names and not inspect.iscoroutinefunction(callback):
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "anydi"
3
- version = "0.67.0"
3
+ version = "0.67.2"
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"
@@ -51,7 +51,7 @@ anydi = "anydi.ext.pytest_plugin"
51
51
  [dependency-groups]
52
52
  dev = [
53
53
  "ruff>=0.14.0",
54
- "pyright>=1.1.407",
54
+ "pyright>=1.1.408",
55
55
  "pytest>=8.4.0,<9",
56
56
  "pytest-cov>=7.0.0",
57
57
  "pytest-mock>=3.14.1",
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
File without changes
File without changes
File without changes
File without changes