anydi 0.42.0__tar.gz → 0.43.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.
Files changed (99) hide show
  1. {anydi-0.42.0 → anydi-0.43.0}/PKG-INFO +1 -1
  2. {anydi-0.42.0 → anydi-0.43.0}/anydi/_container.py +7 -10
  3. {anydi-0.42.0 → anydi-0.43.0}/anydi/_decorators.py +49 -7
  4. {anydi-0.42.0 → anydi-0.43.0}/anydi/_module.py +4 -3
  5. {anydi-0.42.0 → anydi-0.43.0}/anydi/_scan.py +25 -25
  6. {anydi-0.42.0 → anydi-0.43.0}/anydi/ext/django/_utils.py +1 -41
  7. {anydi-0.42.0 → anydi-0.43.0}/pyproject.toml +3 -2
  8. {anydi-0.42.0 → anydi-0.43.0}/tests/test_container.py +31 -47
  9. {anydi-0.42.0 → anydi-0.43.0}/tests/test_decorators.py +21 -11
  10. anydi-0.43.0/tests/test_scan.py +68 -0
  11. {anydi-0.42.0 → anydi-0.43.0}/tests/test_testing.py +3 -5
  12. {anydi-0.42.0 → anydi-0.43.0}/uv.lock +15 -1
  13. anydi-0.42.0/tests/test_scan.py +0 -41
  14. {anydi-0.42.0 → anydi-0.43.0}/.editorconfig +0 -0
  15. {anydi-0.42.0 → anydi-0.43.0}/.github/workflows/ci.yml +0 -0
  16. {anydi-0.42.0 → anydi-0.43.0}/.gitignore +0 -0
  17. {anydi-0.42.0 → anydi-0.43.0}/.readthedocs.yaml +0 -0
  18. {anydi-0.42.0 → anydi-0.43.0}/LICENSE +0 -0
  19. {anydi-0.42.0 → anydi-0.43.0}/Makefile +0 -0
  20. {anydi-0.42.0 → anydi-0.43.0}/README.md +0 -0
  21. {anydi-0.42.0 → anydi-0.43.0}/anydi/__init__.py +0 -0
  22. {anydi-0.42.0 → anydi-0.43.0}/anydi/_async.py +0 -0
  23. {anydi-0.42.0 → anydi-0.43.0}/anydi/_context.py +0 -0
  24. {anydi-0.42.0 → anydi-0.43.0}/anydi/_provider.py +0 -0
  25. {anydi-0.42.0 → anydi-0.43.0}/anydi/_scope.py +0 -0
  26. {anydi-0.42.0 → anydi-0.43.0}/anydi/_typing.py +0 -0
  27. {anydi-0.42.0 → anydi-0.43.0}/anydi/ext/__init__.py +0 -0
  28. {anydi-0.42.0 → anydi-0.43.0}/anydi/ext/_utils.py +0 -0
  29. {anydi-0.42.0 → anydi-0.43.0}/anydi/ext/django/__init__.py +0 -0
  30. {anydi-0.42.0 → anydi-0.43.0}/anydi/ext/django/_container.py +0 -0
  31. {anydi-0.42.0 → anydi-0.43.0}/anydi/ext/django/_settings.py +0 -0
  32. {anydi-0.42.0 → anydi-0.43.0}/anydi/ext/django/apps.py +0 -0
  33. {anydi-0.42.0 → anydi-0.43.0}/anydi/ext/django/middleware.py +0 -0
  34. {anydi-0.42.0 → anydi-0.43.0}/anydi/ext/django/ninja/__init__.py +0 -0
  35. {anydi-0.42.0 → anydi-0.43.0}/anydi/ext/django/ninja/_operation.py +0 -0
  36. {anydi-0.42.0 → anydi-0.43.0}/anydi/ext/django/ninja/_signature.py +0 -0
  37. {anydi-0.42.0 → anydi-0.43.0}/anydi/ext/fastapi.py +0 -0
  38. {anydi-0.42.0 → anydi-0.43.0}/anydi/ext/faststream.py +0 -0
  39. {anydi-0.42.0 → anydi-0.43.0}/anydi/ext/pydantic_settings.py +0 -0
  40. {anydi-0.42.0 → anydi-0.43.0}/anydi/ext/pytest_plugin.py +0 -0
  41. {anydi-0.42.0 → anydi-0.43.0}/anydi/ext/starlette/__init__.py +0 -0
  42. {anydi-0.42.0 → anydi-0.43.0}/anydi/ext/starlette/middleware.py +0 -0
  43. {anydi-0.42.0 → anydi-0.43.0}/anydi/py.typed +0 -0
  44. {anydi-0.42.0 → anydi-0.43.0}/anydi/testing.py +0 -0
  45. {anydi-0.42.0 → anydi-0.43.0}/docs/examples/basic.md +0 -0
  46. {anydi-0.42.0 → anydi-0.43.0}/docs/extensions/django.md +0 -0
  47. {anydi-0.42.0 → anydi-0.43.0}/docs/extensions/fastapi.md +0 -0
  48. {anydi-0.42.0 → anydi-0.43.0}/docs/extensions/faststream.md +0 -0
  49. {anydi-0.42.0 → anydi-0.43.0}/docs/extensions/pydantic_settings.md +0 -0
  50. {anydi-0.42.0 → anydi-0.43.0}/docs/index.md +0 -0
  51. {anydi-0.42.0 → anydi-0.43.0}/docs/usage.md +0 -0
  52. {anydi-0.42.0 → anydi-0.43.0}/mkdocs.yml +0 -0
  53. {anydi-0.42.0 → anydi-0.43.0}/override.py +0 -0
  54. {anydi-0.42.0 → anydi-0.43.0}/tests/__init__.py +0 -0
  55. {anydi-0.42.0 → anydi-0.43.0}/tests/conftest.py +0 -0
  56. {anydi-0.42.0 → anydi-0.43.0}/tests/ext/__init__.py +0 -0
  57. {anydi-0.42.0 → anydi-0.43.0}/tests/ext/django/__init__.py +0 -0
  58. {anydi-0.42.0 → anydi-0.43.0}/tests/ext/django/api/__init__.py +0 -0
  59. {anydi-0.42.0 → anydi-0.43.0}/tests/ext/django/api/router.py +0 -0
  60. {anydi-0.42.0 → anydi-0.43.0}/tests/ext/django/api/test_router.py +0 -0
  61. {anydi-0.42.0 → anydi-0.43.0}/tests/ext/django/api/urls.py +0 -0
  62. {anydi-0.42.0 → anydi-0.43.0}/tests/ext/django/conftest.py +0 -0
  63. {anydi-0.42.0 → anydi-0.43.0}/tests/ext/django/container.py +0 -0
  64. {anydi-0.42.0 → anydi-0.43.0}/tests/ext/django/scan/__init__.py +0 -0
  65. {anydi-0.42.0 → anydi-0.43.0}/tests/ext/django/services.py +0 -0
  66. {anydi-0.42.0 → anydi-0.43.0}/tests/ext/django/settings.py +0 -0
  67. {anydi-0.42.0 → anydi-0.43.0}/tests/ext/django/test_views.py +0 -0
  68. {anydi-0.42.0 → anydi-0.43.0}/tests/ext/django/urls.py +0 -0
  69. {anydi-0.42.0 → anydi-0.43.0}/tests/ext/django/views.py +0 -0
  70. {anydi-0.42.0 → anydi-0.43.0}/tests/ext/fastapi/__init__.py +0 -0
  71. {anydi-0.42.0 → anydi-0.43.0}/tests/ext/fastapi/app.py +0 -0
  72. {anydi-0.42.0 → anydi-0.43.0}/tests/ext/fastapi/conftest.py +0 -0
  73. {anydi-0.42.0 → anydi-0.43.0}/tests/ext/fastapi/test_auto_register.py +0 -0
  74. {anydi-0.42.0 → anydi-0.43.0}/tests/ext/fastapi/test_ext.py +0 -0
  75. {anydi-0.42.0 → anydi-0.43.0}/tests/ext/fastapi/test_routes.py +0 -0
  76. {anydi-0.42.0 → anydi-0.43.0}/tests/ext/faststream/__init__.py +0 -0
  77. {anydi-0.42.0 → anydi-0.43.0}/tests/ext/faststream/test_ext.py +0 -0
  78. {anydi-0.42.0 → anydi-0.43.0}/tests/ext/faststream/test_subscribers.py +0 -0
  79. {anydi-0.42.0 → anydi-0.43.0}/tests/ext/fixtures.py +0 -0
  80. {anydi-0.42.0 → anydi-0.43.0}/tests/ext/starlette/__init__.py +0 -0
  81. {anydi-0.42.0 → anydi-0.43.0}/tests/ext/starlette/app.py +0 -0
  82. {anydi-0.42.0 → anydi-0.43.0}/tests/ext/starlette/conftest.py +0 -0
  83. {anydi-0.42.0 → anydi-0.43.0}/tests/ext/starlette/test_routes.py +0 -0
  84. {anydi-0.42.0 → anydi-0.43.0}/tests/ext/test_pydantic.py +0 -0
  85. {anydi-0.42.0 → anydi-0.43.0}/tests/ext/test_pytest_plugin.py +0 -0
  86. {anydi-0.42.0 → anydi-0.43.0}/tests/fixtures.py +0 -0
  87. {anydi-0.42.0 → anydi-0.43.0}/tests/scan_app/__init__.py +0 -0
  88. {anydi-0.42.0 → anydi-0.43.0}/tests/scan_app/a/__init__.py +0 -0
  89. {anydi-0.42.0 → anydi-0.43.0}/tests/scan_app/a/a1/__init__.py +0 -0
  90. {anydi-0.42.0 → anydi-0.43.0}/tests/scan_app/a/a1/handlers.py +0 -0
  91. {anydi-0.42.0 → anydi-0.43.0}/tests/scan_app/a/a2/__init__.py +0 -0
  92. {anydi-0.42.0 → anydi-0.43.0}/tests/scan_app/a/a2/a21/__init__.py +0 -0
  93. {anydi-0.42.0 → anydi-0.43.0}/tests/scan_app/a/a2/a21/handlers.py +0 -0
  94. {anydi-0.42.0 → anydi-0.43.0}/tests/scan_app/a/a3/__init__.py +0 -0
  95. {anydi-0.42.0 → anydi-0.43.0}/tests/scan_app/a/a3/handlers.py +0 -0
  96. {anydi-0.42.0 → anydi-0.43.0}/tests/scan_app/b/__init__.py +0 -0
  97. {anydi-0.42.0 → anydi-0.43.0}/tests/scan_app/b/handlers.py +0 -0
  98. {anydi-0.42.0 → anydi-0.43.0}/tests/test_module.py +0 -0
  99. {anydi-0.42.0 → anydi-0.43.0}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anydi
3
- Version: 0.42.0
3
+ Version: 0.43.0
4
4
  Summary: Dependency Injection library
5
5
  Project-URL: Repository, https://github.com/antonrh/anydi
6
6
  Author-email: Anton Ruhlov <antonruhlov@gmail.com>
@@ -11,12 +11,13 @@ import uuid
11
11
  from collections import defaultdict
12
12
  from collections.abc import AsyncIterator, Iterable, Iterator
13
13
  from contextvars import ContextVar
14
- from typing import Annotated, Any, Callable, TypeVar, cast, overload
14
+ from typing import Any, Callable, TypeVar, cast, overload
15
15
 
16
16
  from typing_extensions import ParamSpec, Self, get_args, get_origin
17
17
 
18
18
  from ._async import run_sync
19
19
  from ._context import InstanceContext
20
+ from ._decorators import is_provided
20
21
  from ._module import ModuleDef, ModuleRegistrar
21
22
  from ._provider import (
22
23
  Provider,
@@ -441,17 +442,13 @@ class Container:
441
442
  except LookupError:
442
443
  if self.strict or interface is inspect.Parameter.empty:
443
444
  raise
444
- if get_origin(interface) is Annotated and (args := get_args(interface)):
445
- call = args[0]
446
- else:
447
- call = interface
448
- if inspect.isclass(call) and not is_builtin_type(call):
445
+ if inspect.isclass(interface) and not is_builtin_type(interface):
449
446
  # Try to get defined scope
450
- if hasattr(call, "__scope__"):
451
- scope = getattr(call, "__scope__")
447
+ if is_provided(interface):
448
+ scope = interface.__provided__["scope"]
452
449
  else:
453
450
  scope = parent_scope
454
- return self._register_provider(call, scope, interface, **defaults)
451
+ return self._register_provider(interface, scope, interface, **defaults)
455
452
  raise
456
453
 
457
454
  def _set_provider(self, provider: Provider) -> None:
@@ -793,8 +790,8 @@ class Container:
793
790
  return cast(Callable[P, T], self._inject_cache[call])
794
791
 
795
792
  injected_params = self._get_injected_params(call)
796
-
797
793
  if not injected_params:
794
+ self._inject_cache[call] = call
798
795
  return call
799
796
 
800
797
  if inspect.iscoroutinefunction(call):
@@ -1,22 +1,41 @@
1
1
  from collections.abc import Iterable
2
- from typing import Callable, Concatenate, ParamSpec, TypedDict, TypeVar, overload
2
+ from typing import (
3
+ TYPE_CHECKING,
4
+ Any,
5
+ Callable,
6
+ Concatenate,
7
+ ParamSpec,
8
+ Protocol,
9
+ TypedDict,
10
+ TypeGuard,
11
+ TypeVar,
12
+ overload,
13
+ )
14
+
15
+ if TYPE_CHECKING:
16
+ from ._module import Module
17
+
3
18
 
4
- from ._module import Module
5
19
  from ._scope import Scope
6
20
 
7
21
  T = TypeVar("T")
8
22
  P = ParamSpec("P")
9
23
 
10
24
  ClassT = TypeVar("ClassT", bound=type)
11
- ModuleT = TypeVar("ModuleT", bound=Module)
25
+ ModuleT = TypeVar("ModuleT", bound="Module")
26
+
27
+
28
+ class ProvidedMetadata(TypedDict):
29
+ """Metadata for classes marked as provided by AnyDI."""
30
+
31
+ scope: Scope
12
32
 
13
33
 
14
34
  def provided(*, scope: Scope) -> Callable[[ClassT], ClassT]:
15
35
  """Decorator for marking a class as provided by AnyDI with a specific scope."""
16
36
 
17
37
  def decorator(cls: ClassT) -> ClassT:
18
- cls.__provided__ = True
19
- cls.__scope__ = scope
38
+ cls.__provided__ = ProvidedMetadata(scope=scope)
20
39
  return cls
21
40
 
22
41
  return decorator
@@ -28,6 +47,14 @@ request = provided(scope="request")
28
47
  singleton = provided(scope="singleton")
29
48
 
30
49
 
50
+ class Provided(Protocol):
51
+ __provided__: ProvidedMetadata
52
+
53
+
54
+ def is_provided(cls: Any) -> TypeGuard[type[Provided]]:
55
+ return hasattr(cls, "__provided__")
56
+
57
+
31
58
  class ProviderMetadata(TypedDict):
32
59
  scope: Scope
33
60
  override: bool
@@ -49,8 +76,15 @@ def provider(
49
76
  return decorator
50
77
 
51
78
 
79
+ class Provider(Protocol):
80
+ __provider__: ProviderMetadata
81
+
82
+
83
+ def is_provider(obj: Callable[..., Any]) -> TypeGuard[Provider]:
84
+ return hasattr(obj, "__provider__")
85
+
86
+
52
87
  class InjectableMetadata(TypedDict):
53
- wrapped: bool
54
88
  tags: Iterable[str] | None
55
89
 
56
90
 
@@ -71,10 +105,18 @@ def injectable(
71
105
  """Decorator for marking a function or method as requiring dependency injection."""
72
106
 
73
107
  def decorator(inner: Callable[P, T]) -> Callable[P, T]:
74
- inner.__injectable__ = InjectableMetadata(wrapped=True, tags=tags) # type: ignore
108
+ inner.__injectable__ = InjectableMetadata(tags=tags) # type: ignore
75
109
  return inner
76
110
 
77
111
  if func is None:
78
112
  return decorator
79
113
 
80
114
  return decorator(func)
115
+
116
+
117
+ class Injectable(Protocol):
118
+ __injectable__: InjectableMetadata
119
+
120
+
121
+ def is_injectable(obj: Callable[..., Any]) -> TypeGuard[Injectable]:
122
+ return hasattr(obj, "__injectable__")
@@ -4,9 +4,10 @@ import importlib
4
4
  import inspect
5
5
  from typing import TYPE_CHECKING, Any, Callable
6
6
 
7
+ from ._decorators import ProviderMetadata, is_provider
8
+
7
9
  if TYPE_CHECKING:
8
10
  from ._container import Container
9
- from ._decorators import ProviderMetadata
10
11
 
11
12
 
12
13
  class ModuleMeta(type):
@@ -14,9 +15,9 @@ class ModuleMeta(type):
14
15
 
15
16
  def __new__(cls, name: str, bases: tuple[type, ...], attrs: dict[str, Any]) -> Any:
16
17
  attrs["providers"] = [
17
- (name, getattr(value, "__provider__"))
18
+ (name, value.__provider__)
18
19
  for name, value in attrs.items()
19
- if hasattr(value, "__provider__")
20
+ if is_provider(value)
20
21
  ]
21
22
  return super().__new__(cls, name, bases, attrs)
22
23
 
@@ -6,9 +6,9 @@ import pkgutil
6
6
  from collections.abc import Iterable
7
7
  from dataclasses import dataclass
8
8
  from types import ModuleType
9
- from typing import TYPE_CHECKING, Any, Union
9
+ from typing import TYPE_CHECKING, Any, Callable, Union
10
10
 
11
- from ._decorators import InjectableMetadata
11
+ from ._decorators import is_injectable
12
12
  from ._typing import get_typed_parameters, is_marker
13
13
 
14
14
  if TYPE_CHECKING:
@@ -73,9 +73,8 @@ class Scanner:
73
73
 
74
74
  return dependencies
75
75
 
76
- @staticmethod
77
76
  def _scan_module(
78
- module: ModuleType, *, tags: Iterable[str]
77
+ self, module: ModuleType, *, tags: Iterable[str]
79
78
  ) -> list[ScannedDependency]:
80
79
  """Scan a module for decorated members."""
81
80
  dependencies: list[ScannedDependency] = []
@@ -84,27 +83,28 @@ class Scanner:
84
83
  if getattr(member, "__module__", None) != module.__name__:
85
84
  continue
86
85
 
87
- metadata: InjectableMetadata = getattr(
88
- member,
89
- "__injectable__",
90
- InjectableMetadata(wrapped=False, tags=[]),
91
- )
92
-
93
- should_include = False
94
- if metadata["wrapped"]:
95
- should_include = True
96
- elif tags and metadata["tags"]:
97
- should_include = bool(set(metadata["tags"]) & set(tags))
98
- elif tags and not metadata["tags"]:
99
- continue # tags are provided but member has none
100
-
101
- if not should_include:
102
- for param in get_typed_parameters(member):
103
- if is_marker(param.default):
104
- should_include = True
105
- break
106
-
107
- if should_include:
86
+ if self._should_include_member(member, tags=tags):
108
87
  dependencies.append(ScannedDependency(member=member, module=module))
109
88
 
110
89
  return dependencies
90
+
91
+ @staticmethod
92
+ def _should_include_member(
93
+ member: Callable[..., Any], *, tags: Iterable[str]
94
+ ) -> bool:
95
+ """Determine if a member should be included based on tags or marker defaults."""
96
+
97
+ if is_injectable(member):
98
+ member_tags = set(member.__injectable__["tags"] or [])
99
+ if tags:
100
+ return bool(set(tags) & member_tags)
101
+ return True # No tags passed → include all injectables
102
+
103
+ # If no tags are passed and not explicitly injectable,
104
+ # check for parameter markers
105
+ if not tags:
106
+ for param in get_typed_parameters(member):
107
+ if is_marker(param.default):
108
+ return True
109
+
110
+ return False
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from collections.abc import Iterator
4
- from functools import wraps
5
4
  from typing import Annotated, Any
6
5
 
7
6
  from django.conf import settings
@@ -9,7 +8,6 @@ from django.core.cache import BaseCache, caches
9
8
  from django.db import connections
10
9
  from django.db.backends.base.base import BaseDatabaseWrapper
11
10
  from django.urls import URLPattern, URLResolver, get_resolver
12
- from typing_extensions import get_origin
13
11
 
14
12
  from anydi import Container
15
13
 
@@ -29,14 +27,11 @@ def register_settings(
29
27
  continue
30
28
 
31
29
  container.register(
32
- Annotated[Any, f"{prefix}{setting_name}"],
30
+ Annotated[type(setting_value), f"{prefix}{setting_name}"],
33
31
  _get_setting_value(setting_value),
34
32
  scope="singleton",
35
33
  )
36
34
 
37
- # Patch AnyDI to resolve Any types for annotated settings
38
- _patch_any_typed_annotated(container, prefix=prefix)
39
-
40
35
 
41
36
  def register_components(container: Container) -> None:
42
37
  """Register Django components into the container."""
@@ -91,38 +86,3 @@ def iter_urlpatterns(
91
86
 
92
87
  def _get_setting_value(value: Any) -> Any:
93
88
  return lambda: value
94
-
95
-
96
- def _any_typed_interface(interface: Any, prefix: str) -> Any:
97
- origin = get_origin(interface)
98
- if origin is not Annotated:
99
- return interface # pragma: no cover
100
- named = interface.__metadata__[-1]
101
-
102
- if isinstance(named, str) and named.startswith(prefix):
103
- _, setting_name = named.rsplit(prefix, maxsplit=1)
104
- return Annotated[Any, f"{prefix}{setting_name}"]
105
- return interface
106
-
107
-
108
- def _patch_any_typed_annotated(container: Container, *, prefix: str) -> None:
109
- def _patch_resolve(resolve: Any) -> Any:
110
- @wraps(resolve)
111
- def wrapper(interface: Any) -> Any:
112
- return resolve(_any_typed_interface(interface, prefix))
113
-
114
- return wrapper
115
-
116
- def _patch_aresolve(resolve: Any) -> Any:
117
- @wraps(resolve)
118
- async def wrapper(interface: Any) -> Any:
119
- return await resolve(_any_typed_interface(interface, prefix))
120
-
121
- return wrapper
122
-
123
- container.resolve = _patch_resolve( # type: ignore
124
- container.resolve
125
- )
126
- container.aresolve = _patch_aresolve( # type: ignore
127
- container.aresolve
128
- )
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "anydi"
3
- version = "0.42.0"
3
+ version = "0.43.0"
4
4
  description = "Dependency Injection library"
5
5
  authors = [{ name = "Anton Ruhlov", email = "antonruhlov@gmail.com" }]
6
6
  requires-python = "~=3.9"
@@ -65,6 +65,7 @@ dev = [
65
65
  "pydantic-settings>=2.4.0,<3",
66
66
  "bump-my-version>=1.1.4",
67
67
  "pyright>=1.1.401",
68
+ "pytest-mock>=3.14.1",
68
69
  ]
69
70
  docs = [
70
71
  "mkdocs>=1.4.2,<2",
@@ -136,7 +137,7 @@ omit = [
136
137
  ]
137
138
 
138
139
  [tool.bumpversion]
139
- current_version = "0.42.0"
140
+ current_version = "0.43.0"
140
141
  parse = """(?x)
141
142
  (?P<major>0|[1-9]\\d*)\\.
142
143
  (?P<minor>0|[1-9]\\d*)\\.
@@ -10,7 +10,7 @@ from typing import Annotated, Any, Callable, Union
10
10
  import pytest
11
11
  from typing_extensions import Self
12
12
 
13
- from anydi import Container, Provider, Scope, auto
13
+ from anydi import Container, Provider, Scope, auto, request, singleton, transient
14
14
  from anydi._provider import ProviderKind
15
15
  from anydi._typing import Event
16
16
 
@@ -979,8 +979,9 @@ class TestContainer:
979
979
  def test_resolve_non_strict_provider_scope_defined(
980
980
  self, container: Container
981
981
  ) -> None:
982
+ @singleton
982
983
  class Service:
983
- __scope__ = "singleton"
984
+ pass
984
985
 
985
986
  _ = container.resolve(Service)
986
987
 
@@ -990,17 +991,6 @@ class TestContainer:
990
991
  assert provider.scope == "singleton"
991
992
  assert provider.interface == Service
992
993
 
993
- def test_resolve_non_strict_annotated(self, container: Container) -> None:
994
- class Service:
995
- pass
996
-
997
- service_1 = container.resolve(Annotated[Service, "service_1"])
998
- service_2 = container.resolve(Annotated[Service, "service_2"])
999
-
1000
- assert service_1 != service_2
1001
- assert container.is_registered(Annotated[Service, "service_1"])
1002
- assert container.is_registered(Annotated[Service, "service_2"])
1003
-
1004
994
  def test_resolve_non_strict_provider_scope_from_sub_provider_request(
1005
995
  self,
1006
996
  container: Container,
@@ -1039,13 +1029,13 @@ class TestContainer:
1039
1029
  def test_resolve_non_strict_nested_singleton_provider(
1040
1030
  self, container: Container
1041
1031
  ) -> None:
1042
- @dataclass
1032
+ @singleton
1043
1033
  class Repository:
1044
- __scope__ = "singleton"
1034
+ pass
1045
1035
 
1046
- @dataclass
1047
1036
  class Service:
1048
- repository: Repository
1037
+ def __init__(self, repository: Repository) -> None:
1038
+ self.repository = repository
1049
1039
 
1050
1040
  with container.request_context():
1051
1041
  _ = container.resolve(Service)
@@ -1099,9 +1089,8 @@ class TestContainer:
1099
1089
  def test_resolve_non_strict_with_as_context_manager(
1100
1090
  self, container: Container
1101
1091
  ) -> None:
1092
+ @singleton
1102
1093
  class Resource:
1103
- __scope__ = "singleton"
1104
-
1105
1094
  def __init__(self) -> None:
1106
1095
  self.entered = False
1107
1096
  self.exited = False
@@ -1125,9 +1114,8 @@ class TestContainer:
1125
1114
  self,
1126
1115
  container: Container,
1127
1116
  ) -> None:
1117
+ @singleton
1128
1118
  class Service:
1129
- __scope__ = "singleton"
1130
-
1131
1119
  def __init__(self) -> None:
1132
1120
  self.entered = False
1133
1121
  self.exited = False
@@ -1326,11 +1314,10 @@ class TestContainer:
1326
1314
  assert kwargs == {"a": 10, "b": 1.0, "c": "test"}
1327
1315
 
1328
1316
  def test_create_transient_non_strict(self) -> None:
1329
- @dataclass
1317
+ @transient
1330
1318
  class Component:
1331
- __scope__ = "transient"
1332
-
1333
- name: str
1319
+ def __init__(self, name: str) -> None:
1320
+ self.name = name
1334
1321
 
1335
1322
  container = Container(strict=False)
1336
1323
 
@@ -1339,11 +1326,10 @@ class TestContainer:
1339
1326
  assert instance.name == "test"
1340
1327
 
1341
1328
  def test_create_singleton_non_strict(self) -> None:
1342
- @dataclass
1329
+ @singleton
1343
1330
  class Component:
1344
- __scope__ = "singleton"
1345
-
1346
- name: str
1331
+ def __init__(self, name: str) -> None:
1332
+ self.name = name
1347
1333
 
1348
1334
  container = Container(strict=False)
1349
1335
 
@@ -1352,11 +1338,10 @@ class TestContainer:
1352
1338
  assert instance.name == "test"
1353
1339
 
1354
1340
  def test_create_scoped_non_strict(self) -> None:
1355
- @dataclass
1341
+ @request
1356
1342
  class Component:
1357
- __scope__ = "request"
1358
-
1359
- name: str
1343
+ def __init__(self, name: str) -> None:
1344
+ self.name = name
1360
1345
 
1361
1346
  container = Container(strict=False)
1362
1347
 
@@ -1366,8 +1351,9 @@ class TestContainer:
1366
1351
  assert instance.name == "test"
1367
1352
 
1368
1353
  def test_create_non_existing_keyword_arg(self) -> None:
1354
+ @singleton
1369
1355
  class Component:
1370
- __scope__ = "singleton"
1356
+ pass
1371
1357
 
1372
1358
  container = Container(strict=False)
1373
1359
 
@@ -1375,11 +1361,10 @@ class TestContainer:
1375
1361
  container.create(Component, param="test")
1376
1362
 
1377
1363
  async def test_create_async_transient_non_strict(self) -> None:
1378
- @dataclass
1364
+ @transient
1379
1365
  class Component:
1380
- __scope__ = "transient"
1381
-
1382
- name: str
1366
+ def __init__(self, name: str) -> None:
1367
+ self.name = name
1383
1368
 
1384
1369
  container = Container(strict=False)
1385
1370
 
@@ -1388,11 +1373,10 @@ class TestContainer:
1388
1373
  assert instance.name == "test"
1389
1374
 
1390
1375
  async def test_create_async_singleton_non_strict(self) -> None:
1391
- @dataclass
1376
+ @singleton
1392
1377
  class Component:
1393
- __scope__ = "singleton"
1394
-
1395
- name: str
1378
+ def __init__(self, name: str) -> None:
1379
+ self.name = name
1396
1380
 
1397
1381
  container = Container(strict=False)
1398
1382
 
@@ -1401,11 +1385,10 @@ class TestContainer:
1401
1385
  assert instance.name == "test"
1402
1386
 
1403
1387
  async def test_create_async_scoped_non_strict(self) -> None:
1404
- @dataclass
1388
+ @request
1405
1389
  class Component:
1406
- __scope__ = "request"
1407
-
1408
- name: str
1390
+ def __init__(self, name: str) -> None:
1391
+ self.name = name
1409
1392
 
1410
1393
  container = Container(strict=False)
1411
1394
 
@@ -1415,8 +1398,9 @@ class TestContainer:
1415
1398
  assert instance.name == "test"
1416
1399
 
1417
1400
  async def test_create_async_non_existing_keyword_arg(self) -> None:
1401
+ @singleton
1418
1402
  class Component:
1419
- __scope__ = "singleton"
1403
+ pass
1420
1404
 
1421
1405
  container = Container(strict=False)
1422
1406
 
@@ -1,4 +1,5 @@
1
1
  from anydi import Module, injectable, provider, request, singleton, transient
2
+ from anydi._decorators import is_injectable, is_provided
2
3
 
3
4
  from tests.fixtures import Service
4
5
 
@@ -18,22 +19,28 @@ def test_provider_decorator() -> None:
18
19
  def test_request_decorator() -> None:
19
20
  request(Service)
20
21
 
21
- assert getattr(Service, "__scope__") == "request"
22
- assert getattr(Service, "__provided__")
22
+ assert is_provided(Service)
23
+ assert Service.__provided__ == {
24
+ "scope": "request",
25
+ }
23
26
 
24
27
 
25
28
  def test_transient_decorator() -> None:
26
29
  transient(Service)
27
30
 
28
- assert getattr(Service, "__scope__") == "transient"
29
- assert getattr(Service, "__provided__")
31
+ assert is_provided(Service)
32
+ assert Service.__provided__ == {
33
+ "scope": "transient",
34
+ }
30
35
 
31
36
 
32
37
  def test_singleton_decorator() -> None:
33
38
  singleton(Service)
34
39
 
35
- assert getattr(Service, "__scope__") == "singleton"
36
- assert getattr(Service, "__provided__")
40
+ assert is_provided(Service)
41
+ assert Service.__provided__ == {
42
+ "scope": "singleton",
43
+ }
37
44
 
38
45
 
39
46
  def test_injectable_no_args() -> None:
@@ -41,8 +48,8 @@ def test_injectable_no_args() -> None:
41
48
  def my_func() -> None:
42
49
  pass
43
50
 
44
- assert getattr(my_func, "__injectable__") == {
45
- "wrapped": True,
51
+ assert is_injectable(my_func)
52
+ assert my_func.__injectable__ == {
46
53
  "tags": None,
47
54
  }
48
55
 
@@ -52,7 +59,10 @@ def test_injectable_no_args_provided() -> None:
52
59
  def my_func() -> None:
53
60
  pass
54
61
 
55
- assert getattr(my_func, "__injectable__") == {"wrapped": True, "tags": None}
62
+ assert is_injectable(my_func)
63
+ assert my_func.__injectable__ == {
64
+ "tags": None,
65
+ }
56
66
 
57
67
 
58
68
  def test_injectable_with_tags() -> None:
@@ -60,7 +70,7 @@ def test_injectable_with_tags() -> None:
60
70
  def my_func() -> None:
61
71
  pass
62
72
 
63
- assert getattr(my_func, "__injectable__") == {
64
- "wrapped": True,
73
+ assert is_injectable(my_func)
74
+ assert my_func.__injectable__ == {
65
75
  "tags": ["tag1", "tag2"],
66
76
  }
@@ -0,0 +1,68 @@
1
+ import pytest
2
+ from pytest_mock import MockerFixture
3
+
4
+ from anydi import Container
5
+
6
+ from tests.scan_app import ScanAppModule
7
+
8
+
9
+ class TestContainerScan:
10
+ @pytest.fixture
11
+ def container(self) -> Container:
12
+ return Container()
13
+
14
+ def test_scan_registers_all_dependencies(
15
+ self, container: Container, mocker: MockerFixture
16
+ ) -> None:
17
+ inject_spy = mocker.spy(container, "inject")
18
+
19
+ container.register_module(ScanAppModule)
20
+ container.scan(["tests.scan_app"])
21
+
22
+ # Expecting 7 total inject calls from all eligible @injectable functions
23
+ assert inject_spy.call_count == 7
24
+
25
+ from .scan_app.a.a3.handlers import a_a3_handler_1, a_a3_handler_2
26
+
27
+ assert a_a3_handler_1() == "a.a1.str_provider"
28
+ assert a_a3_handler_2().ident == "a.a1.str_provider"
29
+
30
+ def test_scan_single_module_registers_limited_dependencies(
31
+ self, container: Container, mocker: MockerFixture
32
+ ) -> None:
33
+ inject_spy = mocker.spy(container, "inject")
34
+
35
+ container.register_module(ScanAppModule)
36
+ container.scan("tests.scan_app.a.a3.handlers")
37
+
38
+ # Expecting only 2 handlers to be registered from this specific module
39
+ assert inject_spy.call_count == 2
40
+
41
+ from .scan_app.a.a3.handlers import a_a3_handler_1
42
+
43
+ assert a_a3_handler_1() == "a.a1.str_provider"
44
+
45
+ def test_scan_with_unknown_tag_registers_nothing(
46
+ self, container: Container, mocker: MockerFixture
47
+ ) -> None:
48
+ inject_spy = mocker.spy(container, "inject")
49
+
50
+ container.scan(["tests.scan_app"], tags=["non_existing_tag"])
51
+
52
+ # No injectable functions should be picked up with unmatched tags
53
+ inject_spy.assert_not_called()
54
+
55
+ def test_scan_with_matching_tag_registers_only_matching_dependencies(
56
+ self, container: Container, mocker: MockerFixture
57
+ ) -> None:
58
+ inject_spy = mocker.spy(container, "inject")
59
+
60
+ container.register_module(ScanAppModule)
61
+ container.scan(["tests.scan_app"], tags=["inject"])
62
+
63
+ # Only handlers with @injectable(..., tags=["inject"]) should be included
64
+ assert inject_spy.call_count == 1
65
+
66
+ from .scan_app.a.a3.handlers import a_a3_handler_1
67
+
68
+ assert a_a3_handler_1() == "a.a1.str_provider"
@@ -6,7 +6,7 @@ from unittest import mock
6
6
 
7
7
  import pytest
8
8
 
9
- from anydi import Container, auto
9
+ from anydi import Container, auto, singleton
10
10
  from anydi.testing import TestContainer
11
11
 
12
12
  from tests.fixtures import Service
@@ -99,9 +99,8 @@ class TestTestContainer:
99
99
  def get_user(self) -> str:
100
100
  return "user"
101
101
 
102
+ @singleton
102
103
  class UserService:
103
- __scope__ = "singleton"
104
-
105
104
  def __init__(self, repo: UserRepo, param: Annotated[str, "param"]) -> None:
106
105
  self.repo = repo
107
106
  self.param = param
@@ -130,9 +129,8 @@ class TestTestContainer:
130
129
  container = TestContainer(strict=False)
131
130
  container.register(Annotated[str, "param"], lambda: "param", scope="singleton")
132
131
 
132
+ @singleton
133
133
  class UserService:
134
- __scope__ = "singleton"
135
-
136
134
  def __init__(self, param: Annotated[str, "param"]) -> None:
137
135
  self.param = param
138
136
 
@@ -13,7 +13,7 @@ wheels = [
13
13
 
14
14
  [[package]]
15
15
  name = "anydi"
16
- version = "0.42.0"
16
+ version = "0.43.0"
17
17
  source = { editable = "." }
18
18
  dependencies = [
19
19
  { name = "anyio" },
@@ -35,6 +35,7 @@ dev = [
35
35
  { name = "pytest" },
36
36
  { name = "pytest-cov" },
37
37
  { name = "pytest-django" },
38
+ { name = "pytest-mock" },
38
39
  { name = "redis" },
39
40
  { name = "ruff" },
40
41
  { name = "starlette" },
@@ -65,6 +66,7 @@ dev = [
65
66
  { name = "pytest", specifier = ">=8.4.0,<9" },
66
67
  { name = "pytest-cov", specifier = ">=6.0.0,<7" },
67
68
  { name = "pytest-django", specifier = ">=4.8.0,<5" },
69
+ { name = "pytest-mock", specifier = ">=3.14.1" },
68
70
  { name = "redis", specifier = ">=5.0.4,<6" },
69
71
  { name = "ruff", specifier = ">=0.11.10,<1.0" },
70
72
  { name = "starlette", specifier = ">=0.37.2" },
@@ -973,6 +975,18 @@ wheels = [
973
975
  { url = "https://files.pythonhosted.org/packages/47/fe/54f387ee1b41c9ad59e48fb8368a361fad0600fe404315e31a12bacaea7d/pytest_django-4.9.0-py3-none-any.whl", hash = "sha256:1d83692cb39188682dbb419ff0393867e9904094a549a7d38a3154d5731b2b99", size = 23723, upload-time = "2024-09-02T15:49:17.127Z" },
974
976
  ]
975
977
 
978
+ [[package]]
979
+ name = "pytest-mock"
980
+ version = "3.14.1"
981
+ source = { registry = "https://pypi.org/simple" }
982
+ dependencies = [
983
+ { name = "pytest" },
984
+ ]
985
+ sdist = { url = "https://files.pythonhosted.org/packages/71/28/67172c96ba684058a4d24ffe144d64783d2a270d0af0d9e792737bddc75c/pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e", size = 33241, upload-time = "2025-05-26T13:58:45.167Z" }
986
+ wheels = [
987
+ { url = "https://files.pythonhosted.org/packages/b2/05/77b60e520511c53d1c1ca75f1930c7dd8e971d0c4379b7f4b3f9644685ba/pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0", size = 9923, upload-time = "2025-05-26T13:58:43.487Z" },
988
+ ]
989
+
976
990
  [[package]]
977
991
  name = "python-dateutil"
978
992
  version = "2.9.0.post0"
@@ -1,41 +0,0 @@
1
- import pytest
2
-
3
- from anydi import Container
4
-
5
- from tests.scan_app import ScanAppModule
6
-
7
-
8
- class TestContainerScan:
9
- @pytest.fixture
10
- def container(self) -> Container:
11
- return Container()
12
-
13
- def test_scan(self, container: Container) -> None:
14
- container.register_module(ScanAppModule)
15
- container.scan(["tests.scan_app"])
16
-
17
- from .scan_app.a.a3.handlers import a_a3_handler_1, a_a3_handler_2
18
-
19
- assert a_a3_handler_1() == "a.a1.str_provider"
20
- assert a_a3_handler_2().ident == "a.a1.str_provider"
21
-
22
- def test_scan_single_package(self, container: Container) -> None:
23
- container.register_module(ScanAppModule)
24
- container.scan("tests.scan_app.a.a3.handlers")
25
-
26
- from .scan_app.a.a3.handlers import a_a3_handler_1
27
-
28
- assert a_a3_handler_1() == "a.a1.str_provider"
29
-
30
- def test_scan_non_existing_tag(self, container: Container) -> None:
31
- container.scan(["tests.scan_app"], tags=["non_existing_tag"])
32
-
33
- assert not container.providers
34
-
35
- def test_scan_tagged(self, container: Container) -> None:
36
- container.register_module(ScanAppModule)
37
- container.scan(["tests.scan_app.a"], tags=["inject"])
38
-
39
- from .scan_app.a.a3.handlers import a_a3_handler_1
40
-
41
- assert a_a3_handler_1() == "a.a1.str_provider"
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
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
File without changes
File without changes
File without changes
File without changes