anydi 0.70.2__tar.gz → 0.71.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.
- {anydi-0.70.2 → anydi-0.71.0}/PKG-INFO +1 -1
- anydi-0.71.0/anydi/_scanner.py +298 -0
- {anydi-0.70.2 → anydi-0.71.0}/pyproject.toml +1 -1
- anydi-0.70.2/anydi/_scanner.py +0 -160
- {anydi-0.70.2 → anydi-0.71.0}/README.md +0 -0
- {anydi-0.70.2 → anydi-0.71.0}/anydi/__init__.py +0 -0
- {anydi-0.70.2 → anydi-0.71.0}/anydi/_async_lock.py +0 -0
- {anydi-0.70.2 → anydi-0.71.0}/anydi/_cli.py +0 -0
- {anydi-0.70.2 → anydi-0.71.0}/anydi/_container.py +0 -0
- {anydi-0.70.2 → anydi-0.71.0}/anydi/_context.py +0 -0
- {anydi-0.70.2 → anydi-0.71.0}/anydi/_decorators.py +0 -0
- {anydi-0.70.2 → anydi-0.71.0}/anydi/_graph.py +0 -0
- {anydi-0.70.2 → anydi-0.71.0}/anydi/_injector.py +0 -0
- {anydi-0.70.2 → anydi-0.71.0}/anydi/_marker.py +0 -0
- {anydi-0.70.2 → anydi-0.71.0}/anydi/_module.py +0 -0
- {anydi-0.70.2 → anydi-0.71.0}/anydi/_provider.py +0 -0
- {anydi-0.70.2 → anydi-0.71.0}/anydi/_resolver.py +0 -0
- {anydi-0.70.2 → anydi-0.71.0}/anydi/_types.py +0 -0
- {anydi-0.70.2 → anydi-0.71.0}/anydi/ext/__init__.py +0 -0
- {anydi-0.70.2 → anydi-0.71.0}/anydi/ext/django/__init__.py +0 -0
- {anydi-0.70.2 → anydi-0.71.0}/anydi/ext/fastapi.py +0 -0
- {anydi-0.70.2 → anydi-0.71.0}/anydi/ext/faststream.py +0 -0
- {anydi-0.70.2 → anydi-0.71.0}/anydi/ext/pydantic_settings.py +0 -0
- {anydi-0.70.2 → anydi-0.71.0}/anydi/ext/pytest_plugin.py +0 -0
- {anydi-0.70.2 → anydi-0.71.0}/anydi/ext/starlette/__init__.py +0 -0
- {anydi-0.70.2 → anydi-0.71.0}/anydi/ext/starlette/middleware.py +0 -0
- {anydi-0.70.2 → anydi-0.71.0}/anydi/ext/typer.py +0 -0
- {anydi-0.70.2 → anydi-0.71.0}/anydi/py.typed +0 -0
- {anydi-0.70.2 → anydi-0.71.0}/anydi/testing.py +0 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import inspect
|
|
5
|
+
import pkgutil
|
|
6
|
+
from collections.abc import Iterable, Iterator
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from types import ModuleType
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
from ._decorators import Provided, is_injectable, is_provided
|
|
12
|
+
from ._types import to_list
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from ._container import Container
|
|
16
|
+
|
|
17
|
+
Package = ModuleType | str
|
|
18
|
+
PackageOrIterable = Package | Iterable[Package]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(kw_only=True)
|
|
22
|
+
class ScannedDependency:
|
|
23
|
+
member: Any
|
|
24
|
+
module: ModuleType
|
|
25
|
+
|
|
26
|
+
def __post_init__(self) -> None:
|
|
27
|
+
# Unwrap decorated functions if necessary
|
|
28
|
+
if hasattr(self.member, "__wrapped__"):
|
|
29
|
+
self.member = self.member.__wrapped__
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Scanner:
|
|
33
|
+
_scanning_packages: set[str] = set()
|
|
34
|
+
|
|
35
|
+
def __init__(self, container: Container) -> None:
|
|
36
|
+
self._container = container
|
|
37
|
+
self._importing_modules: set[str] = set()
|
|
38
|
+
|
|
39
|
+
def scan(
|
|
40
|
+
self,
|
|
41
|
+
/,
|
|
42
|
+
packages: PackageOrIterable,
|
|
43
|
+
*,
|
|
44
|
+
tags: Iterable[str] | None = None,
|
|
45
|
+
ignore: PackageOrIterable | None = None,
|
|
46
|
+
) -> None:
|
|
47
|
+
"""Scan packages or modules for decorated members and inject dependencies.
|
|
48
|
+
|
|
49
|
+
Supports relative package paths (like Python's relative imports):
|
|
50
|
+
- "." scans the caller's package
|
|
51
|
+
- ".submodule" scans a submodule of the caller's package
|
|
52
|
+
- ".." scans the parent package
|
|
53
|
+
- "..sibling" scans a sibling package
|
|
54
|
+
"""
|
|
55
|
+
if isinstance(packages, (ModuleType, str)):
|
|
56
|
+
packages = [packages]
|
|
57
|
+
|
|
58
|
+
# Resolve relative package paths
|
|
59
|
+
caller_package = self._get_caller_package(packages, ignore)
|
|
60
|
+
packages = self._resolve_relative_packages(packages, caller_package)
|
|
61
|
+
ignore = self._resolve_relative_packages(ignore, caller_package)
|
|
62
|
+
|
|
63
|
+
pkg_names = {p if isinstance(p, str) else p.__name__ for p in packages}
|
|
64
|
+
overlap = pkg_names & Scanner._scanning_packages
|
|
65
|
+
if overlap:
|
|
66
|
+
raise RuntimeError(
|
|
67
|
+
f"Circular import detected: scan() called recursively!\n\n"
|
|
68
|
+
f"Already scanning packages: {', '.join(sorted(overlap))}\n\n"
|
|
69
|
+
"This happens when a scanned module triggers container creation "
|
|
70
|
+
"(e.g., via lazy proxy).\n\n"
|
|
71
|
+
"Solutions:\n"
|
|
72
|
+
"- Add the problematic module to scan() ignore list\n"
|
|
73
|
+
"- Move container imports inside functions (lazy import)\n"
|
|
74
|
+
"- Avoid lazy container initialization in scanned modules"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
Scanner._scanning_packages.update(pkg_names)
|
|
78
|
+
try:
|
|
79
|
+
self._do_scan(packages, tags=tags, ignore=ignore)
|
|
80
|
+
finally:
|
|
81
|
+
Scanner._scanning_packages -= pkg_names
|
|
82
|
+
|
|
83
|
+
def _do_scan( # noqa: C901
|
|
84
|
+
self,
|
|
85
|
+
packages: PackageOrIterable,
|
|
86
|
+
*,
|
|
87
|
+
tags: Iterable[str] | None = None,
|
|
88
|
+
ignore: PackageOrIterable | None = None,
|
|
89
|
+
) -> None:
|
|
90
|
+
"""Internal scan implementation."""
|
|
91
|
+
if isinstance(packages, (ModuleType, str)):
|
|
92
|
+
packages = [packages]
|
|
93
|
+
|
|
94
|
+
tags_set: set[str] = set(tags) if tags else set()
|
|
95
|
+
ignore_prefixes = self._normalize_ignore(ignore)
|
|
96
|
+
provided_classes: list[type[Provided]] = []
|
|
97
|
+
injectable_dependencies: list[ScannedDependency] = []
|
|
98
|
+
|
|
99
|
+
# Single pass: collect both @provided classes and @injectable functions
|
|
100
|
+
for module in self._iter_modules(packages, ignore_prefixes=ignore_prefixes):
|
|
101
|
+
module_name = module.__name__
|
|
102
|
+
for name, member in vars(module).items():
|
|
103
|
+
if name.startswith("_"):
|
|
104
|
+
continue
|
|
105
|
+
if getattr(member, "__module__", None) != module_name:
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
if inspect.isclass(member) and is_provided(member):
|
|
109
|
+
provided_classes.append(member)
|
|
110
|
+
elif callable(member) and is_injectable(member):
|
|
111
|
+
member_tags = set(member.__injectable__["tags"] or ())
|
|
112
|
+
if not tags_set or (tags_set & member_tags):
|
|
113
|
+
injectable_dependencies.append(
|
|
114
|
+
ScannedDependency(member=member, module=module)
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# First: register @provided classes
|
|
118
|
+
for cls in provided_classes:
|
|
119
|
+
if not self._container.is_registered(cls):
|
|
120
|
+
scope = cls.__provided__["scope"]
|
|
121
|
+
from_context = cls.__provided__.get("from_context", False)
|
|
122
|
+
self._container.register(
|
|
123
|
+
cls, cls, scope=scope, from_context=from_context
|
|
124
|
+
)
|
|
125
|
+
# Create aliases if specified (alias → cls)
|
|
126
|
+
for alias_type in to_list(cls.__provided__.get("alias")):
|
|
127
|
+
self._container.alias(alias_type, cls)
|
|
128
|
+
|
|
129
|
+
# Second: inject @injectable functions
|
|
130
|
+
for dependency in injectable_dependencies:
|
|
131
|
+
decorated = self._container.inject()(dependency.member)
|
|
132
|
+
setattr(dependency.module, dependency.member.__name__, decorated)
|
|
133
|
+
|
|
134
|
+
def _has_relative_packages(self, *package_lists: PackageOrIterable | None) -> bool:
|
|
135
|
+
"""Check if any package list contains relative paths."""
|
|
136
|
+
for packages in package_lists:
|
|
137
|
+
if packages is None:
|
|
138
|
+
continue
|
|
139
|
+
if isinstance(packages, str):
|
|
140
|
+
if packages.startswith("."):
|
|
141
|
+
return True
|
|
142
|
+
elif isinstance(packages, ModuleType):
|
|
143
|
+
continue
|
|
144
|
+
else:
|
|
145
|
+
for p in packages:
|
|
146
|
+
if isinstance(p, str) and p.startswith("."):
|
|
147
|
+
return True
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
def _get_caller_package(
|
|
151
|
+
self,
|
|
152
|
+
packages: Iterable[Package],
|
|
153
|
+
ignore: PackageOrIterable | None,
|
|
154
|
+
) -> str | None:
|
|
155
|
+
"""Get the package name of the module that called scan()."""
|
|
156
|
+
if not self._has_relative_packages(packages, ignore):
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
frame = inspect.currentframe()
|
|
160
|
+
try:
|
|
161
|
+
while frame is not None:
|
|
162
|
+
frame = frame.f_back
|
|
163
|
+
if frame is None:
|
|
164
|
+
break
|
|
165
|
+
module_name = frame.f_globals.get("__name__")
|
|
166
|
+
if module_name and not module_name.startswith("anydi"):
|
|
167
|
+
# Return package portion (remove module name if present)
|
|
168
|
+
if "." in module_name:
|
|
169
|
+
return module_name.rsplit(".", 1)[0]
|
|
170
|
+
return module_name
|
|
171
|
+
finally:
|
|
172
|
+
del frame
|
|
173
|
+
|
|
174
|
+
raise ValueError(
|
|
175
|
+
"Cannot use relative package paths: unable to determine caller package. "
|
|
176
|
+
"Use absolute package names instead."
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
def _resolve_relative_name(self, relative_name: str, base_package: str) -> str:
|
|
180
|
+
"""Resolve a relative package name to absolute."""
|
|
181
|
+
num_dots = len(relative_name) - len(relative_name.lstrip("."))
|
|
182
|
+
remainder = relative_name[num_dots:]
|
|
183
|
+
|
|
184
|
+
package_parts = base_package.split(".")
|
|
185
|
+
|
|
186
|
+
# Navigate up for parent references (..)
|
|
187
|
+
if num_dots > 1:
|
|
188
|
+
levels_up = num_dots - 1
|
|
189
|
+
if levels_up >= len(package_parts):
|
|
190
|
+
raise ValueError(
|
|
191
|
+
f"Cannot resolve '{relative_name}': "
|
|
192
|
+
f"too many parent levels for base package '{base_package}'"
|
|
193
|
+
)
|
|
194
|
+
package_parts = package_parts[:-levels_up]
|
|
195
|
+
|
|
196
|
+
if remainder:
|
|
197
|
+
return ".".join(package_parts) + "." + remainder
|
|
198
|
+
return ".".join(package_parts)
|
|
199
|
+
|
|
200
|
+
def _resolve_relative_packages(
|
|
201
|
+
self,
|
|
202
|
+
packages: PackageOrIterable | None,
|
|
203
|
+
caller_package: str | None,
|
|
204
|
+
) -> list[Package]:
|
|
205
|
+
"""Resolve relative package names to absolute names."""
|
|
206
|
+
if packages is None:
|
|
207
|
+
return []
|
|
208
|
+
|
|
209
|
+
if isinstance(packages, (ModuleType, str)):
|
|
210
|
+
packages = [packages]
|
|
211
|
+
|
|
212
|
+
resolved: list[Package] = []
|
|
213
|
+
for package in packages:
|
|
214
|
+
if isinstance(package, ModuleType):
|
|
215
|
+
resolved.append(package)
|
|
216
|
+
elif not package.startswith("."):
|
|
217
|
+
resolved.append(package)
|
|
218
|
+
else:
|
|
219
|
+
if caller_package is None:
|
|
220
|
+
raise ValueError(
|
|
221
|
+
"Cannot use relative package paths: "
|
|
222
|
+
"unable to determine caller package. "
|
|
223
|
+
"Use absolute package names instead."
|
|
224
|
+
)
|
|
225
|
+
resolved.append(self._resolve_relative_name(package, caller_package))
|
|
226
|
+
|
|
227
|
+
return resolved
|
|
228
|
+
|
|
229
|
+
def _normalize_ignore(self, ignore: PackageOrIterable | None) -> tuple[str, ...]:
|
|
230
|
+
"""Normalize ignore parameter to a tuple of module name prefixes."""
|
|
231
|
+
if ignore is None:
|
|
232
|
+
return ()
|
|
233
|
+
|
|
234
|
+
if isinstance(ignore, (ModuleType, str)):
|
|
235
|
+
ignore = [ignore]
|
|
236
|
+
|
|
237
|
+
prefixes: list[str] = []
|
|
238
|
+
for item in ignore:
|
|
239
|
+
name = item.__name__ if isinstance(item, ModuleType) else item
|
|
240
|
+
prefixes.append(name)
|
|
241
|
+
prefixes.append(name + ".") # For startswith check
|
|
242
|
+
return tuple(prefixes)
|
|
243
|
+
|
|
244
|
+
def _should_ignore_module(
|
|
245
|
+
self, module_name: str, ignore_prefixes: tuple[str, ...]
|
|
246
|
+
) -> bool:
|
|
247
|
+
"""Check if a module should be ignored based on ignore prefixes."""
|
|
248
|
+
return module_name.startswith(ignore_prefixes) if ignore_prefixes else False
|
|
249
|
+
|
|
250
|
+
def _iter_modules(
|
|
251
|
+
self, packages: Iterable[Package], *, ignore_prefixes: tuple[str, ...]
|
|
252
|
+
) -> Iterator[ModuleType]:
|
|
253
|
+
"""Iterate over all modules in the given packages."""
|
|
254
|
+
for package in packages:
|
|
255
|
+
if isinstance(package, str):
|
|
256
|
+
package = importlib.import_module(package)
|
|
257
|
+
|
|
258
|
+
# Single module (not a package)
|
|
259
|
+
if not hasattr(package, "__path__"):
|
|
260
|
+
if not self._should_ignore_module(package.__name__, ignore_prefixes):
|
|
261
|
+
yield package
|
|
262
|
+
continue
|
|
263
|
+
|
|
264
|
+
# Package - walk all submodules
|
|
265
|
+
for module_info in pkgutil.walk_packages(
|
|
266
|
+
package.__path__, prefix=package.__name__ + "."
|
|
267
|
+
):
|
|
268
|
+
if not self._should_ignore_module(module_info.name, ignore_prefixes):
|
|
269
|
+
yield from self._import_module_with_tracking(module_info.name)
|
|
270
|
+
|
|
271
|
+
def _import_module_with_tracking(self, module_name: str) -> Iterator[ModuleType]:
|
|
272
|
+
"""Import a module while tracking for circular imports."""
|
|
273
|
+
# Check if we're already importing this module (circular import)
|
|
274
|
+
if module_name in self._importing_modules:
|
|
275
|
+
import_chain = " -> ".join(sorted(self._importing_modules))
|
|
276
|
+
raise RuntimeError(
|
|
277
|
+
f"Circular import detected during container scanning!\n"
|
|
278
|
+
f"Module '{module_name}' is being imported while already "
|
|
279
|
+
f"in the import chain.\n"
|
|
280
|
+
f"Import chain: {import_chain} -> {module_name}\n\n"
|
|
281
|
+
f"This usually happens when:\n"
|
|
282
|
+
f"1. A scanned module imports the container at module level\n"
|
|
283
|
+
f"2. The container creation triggers scanning\n"
|
|
284
|
+
f"3. Scanning tries to import the module again\n\n"
|
|
285
|
+
f"Solutions:\n"
|
|
286
|
+
f"- Add '{module_name}' to the ignore list\n"
|
|
287
|
+
f"- Move container imports inside functions (lazy import)\n"
|
|
288
|
+
f"- Check for modules importing the container module"
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
# Track that we're importing this module
|
|
292
|
+
self._importing_modules.add(module_name)
|
|
293
|
+
try:
|
|
294
|
+
module = importlib.import_module(module_name)
|
|
295
|
+
yield module
|
|
296
|
+
finally:
|
|
297
|
+
# Always cleanup, even if import fails
|
|
298
|
+
self._importing_modules.discard(module_name)
|
anydi-0.70.2/anydi/_scanner.py
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import importlib
|
|
4
|
-
import inspect
|
|
5
|
-
import pkgutil
|
|
6
|
-
from collections.abc import Callable, Iterable, Iterator
|
|
7
|
-
from dataclasses import dataclass
|
|
8
|
-
from types import ModuleType
|
|
9
|
-
from typing import TYPE_CHECKING, Any
|
|
10
|
-
|
|
11
|
-
from ._decorators import Provided, is_injectable, is_provided
|
|
12
|
-
from ._types import to_list
|
|
13
|
-
|
|
14
|
-
if TYPE_CHECKING:
|
|
15
|
-
from ._container import Container
|
|
16
|
-
|
|
17
|
-
Package = ModuleType | str
|
|
18
|
-
PackageOrIterable = Package | Iterable[Package]
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
@dataclass(kw_only=True)
|
|
22
|
-
class ScannedDependency:
|
|
23
|
-
member: Any
|
|
24
|
-
module: ModuleType
|
|
25
|
-
|
|
26
|
-
def __post_init__(self) -> None:
|
|
27
|
-
# Unwrap decorated functions if necessary
|
|
28
|
-
if hasattr(self.member, "__wrapped__"):
|
|
29
|
-
self.member = self.member.__wrapped__
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class Scanner:
|
|
33
|
-
def __init__(self, container: Container) -> None:
|
|
34
|
-
self._container = container
|
|
35
|
-
|
|
36
|
-
def scan(
|
|
37
|
-
self,
|
|
38
|
-
/,
|
|
39
|
-
packages: PackageOrIterable,
|
|
40
|
-
*,
|
|
41
|
-
tags: Iterable[str] | None = None,
|
|
42
|
-
ignore: PackageOrIterable | None = None,
|
|
43
|
-
) -> None:
|
|
44
|
-
"""Scan packages or modules for decorated members and inject dependencies."""
|
|
45
|
-
if isinstance(packages, (ModuleType, str)):
|
|
46
|
-
packages = [packages]
|
|
47
|
-
|
|
48
|
-
tags_list = list(tags) if tags else []
|
|
49
|
-
ignore_prefixes = self._normalize_ignore(ignore)
|
|
50
|
-
provided_classes: list[type[Provided]] = []
|
|
51
|
-
injectable_dependencies: list[ScannedDependency] = []
|
|
52
|
-
|
|
53
|
-
# Single pass: collect both @provided classes and @injectable functions
|
|
54
|
-
for module in self._iter_modules(packages, ignore_prefixes=ignore_prefixes):
|
|
55
|
-
provided_classes.extend(self._scan_module_for_provided(module))
|
|
56
|
-
injectable_dependencies.extend(
|
|
57
|
-
self._scan_module_for_injectable(module, tags=tags_list)
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
# First: register @provided classes
|
|
61
|
-
for cls in provided_classes:
|
|
62
|
-
if not self._container.is_registered(cls):
|
|
63
|
-
scope = cls.__provided__["scope"]
|
|
64
|
-
from_context = cls.__provided__.get("from_context", False)
|
|
65
|
-
self._container.register(
|
|
66
|
-
cls, cls, scope=scope, from_context=from_context
|
|
67
|
-
)
|
|
68
|
-
# Create aliases if specified (alias → cls)
|
|
69
|
-
for alias_type in to_list(cls.__provided__.get("alias")):
|
|
70
|
-
self._container.alias(alias_type, cls)
|
|
71
|
-
|
|
72
|
-
# Second: inject @injectable functions
|
|
73
|
-
for dependency in injectable_dependencies:
|
|
74
|
-
decorated = self._container.inject()(dependency.member)
|
|
75
|
-
setattr(dependency.module, dependency.member.__name__, decorated)
|
|
76
|
-
|
|
77
|
-
def _normalize_ignore(self, ignore: PackageOrIterable | None) -> list[str]:
|
|
78
|
-
"""Normalize ignore parameter to a list of module name prefixes."""
|
|
79
|
-
if ignore is None:
|
|
80
|
-
return []
|
|
81
|
-
|
|
82
|
-
if isinstance(ignore, (ModuleType, str)):
|
|
83
|
-
ignore = [ignore]
|
|
84
|
-
|
|
85
|
-
prefixes: list[str] = []
|
|
86
|
-
for item in ignore:
|
|
87
|
-
if isinstance(item, ModuleType):
|
|
88
|
-
prefixes.append(item.__name__)
|
|
89
|
-
else:
|
|
90
|
-
prefixes.append(item)
|
|
91
|
-
return prefixes
|
|
92
|
-
|
|
93
|
-
def _should_ignore_module(
|
|
94
|
-
self, module_name: str, ignore_prefixes: list[str]
|
|
95
|
-
) -> bool:
|
|
96
|
-
"""Check if a module should be ignored based on ignore prefixes."""
|
|
97
|
-
for prefix in ignore_prefixes:
|
|
98
|
-
if module_name == prefix or module_name.startswith(prefix + "."):
|
|
99
|
-
return True
|
|
100
|
-
return False
|
|
101
|
-
|
|
102
|
-
def _iter_modules(
|
|
103
|
-
self, packages: Iterable[Package], *, ignore_prefixes: list[str]
|
|
104
|
-
) -> Iterator[ModuleType]:
|
|
105
|
-
"""Iterate over all modules in the given packages."""
|
|
106
|
-
for package in packages:
|
|
107
|
-
if isinstance(package, str):
|
|
108
|
-
package = importlib.import_module(package)
|
|
109
|
-
|
|
110
|
-
# Single module (not a package)
|
|
111
|
-
if not hasattr(package, "__path__"):
|
|
112
|
-
if not self._should_ignore_module(package.__name__, ignore_prefixes):
|
|
113
|
-
yield package
|
|
114
|
-
continue
|
|
115
|
-
|
|
116
|
-
# Package - walk all submodules
|
|
117
|
-
for module_info in pkgutil.walk_packages(
|
|
118
|
-
package.__path__, prefix=package.__name__ + "."
|
|
119
|
-
):
|
|
120
|
-
if not self._should_ignore_module(module_info.name, ignore_prefixes):
|
|
121
|
-
yield importlib.import_module(module_info.name)
|
|
122
|
-
|
|
123
|
-
def _scan_module_for_provided(self, module: ModuleType) -> list[type[Provided]]:
|
|
124
|
-
"""Scan a module for @provided classes."""
|
|
125
|
-
provided_classes: list[type[Provided]] = []
|
|
126
|
-
|
|
127
|
-
for _, member in inspect.getmembers(module, predicate=inspect.isclass):
|
|
128
|
-
if getattr(member, "__module__", None) != module.__name__:
|
|
129
|
-
continue
|
|
130
|
-
|
|
131
|
-
if is_provided(member):
|
|
132
|
-
provided_classes.append(member)
|
|
133
|
-
|
|
134
|
-
return provided_classes
|
|
135
|
-
|
|
136
|
-
def _scan_module_for_injectable(
|
|
137
|
-
self, module: ModuleType, *, tags: list[str]
|
|
138
|
-
) -> list[ScannedDependency]:
|
|
139
|
-
"""Scan a module for @injectable functions."""
|
|
140
|
-
dependencies: list[ScannedDependency] = []
|
|
141
|
-
|
|
142
|
-
for _, member in inspect.getmembers(module, predicate=callable):
|
|
143
|
-
if getattr(member, "__module__", None) != module.__name__:
|
|
144
|
-
continue
|
|
145
|
-
|
|
146
|
-
if self._should_include_member(member, tags=tags):
|
|
147
|
-
dependencies.append(ScannedDependency(member=member, module=module))
|
|
148
|
-
|
|
149
|
-
return dependencies
|
|
150
|
-
|
|
151
|
-
@staticmethod
|
|
152
|
-
def _should_include_member(member: Callable[..., Any], *, tags: list[str]) -> bool:
|
|
153
|
-
"""Determine if a member should be included based on tags or marker defaults."""
|
|
154
|
-
if is_injectable(member):
|
|
155
|
-
member_tags = set(member.__injectable__["tags"] or [])
|
|
156
|
-
if tags:
|
|
157
|
-
return bool(set(tags) & member_tags)
|
|
158
|
-
return True # No tags passed → include all injectables
|
|
159
|
-
|
|
160
|
-
return False
|
|
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
|