-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathdependency_analyzer.py
More file actions
265 lines (215 loc) · 9.93 KB
/
Copy pathdependency_analyzer.py
File metadata and controls
265 lines (215 loc) · 9.93 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
"""
Dependency analysis utilities for TestModule to automatically resolve module dependencies.
"""
import inspect
import typing as t
from typing import get_type_hints
import injector
from ellar.di.constants import Tag
from ellar.utils.importer import import_from_string
if t.TYPE_CHECKING: # pragma: no cover
from ellar.common import ControllerBase
from ellar.core import ForwardRefModule, ModuleBase, ModuleSetup
from ellar.di import ModuleTreeManager
class DependencyResolutionError(Exception):
"""Raised when a dependency cannot be resolved"""
def __init__(
self,
dependency_type: t.Type,
controller: t.Type["ControllerBase"],
application_module: t.Optional[t.Type["ModuleBase"]] = None,
):
if application_module:
message = (
f"Controller '{controller.__name__}' requires '{dependency_type.__name__}', "
f"but it's not found in ApplicationModule '{application_module.__name__}'. "
f"Please either:\n"
f" 1. Register the module providing '{dependency_type.__name__}' in ApplicationModule\n"
f" 2. Register it explicitly in Test.create_test_module(modules=[...])\n"
f" 3. Provide it as a mock in providers=[...]"
)
else:
message = (
f"Controller '{controller.__name__}' requires '{dependency_type.__name__}', "
f"but no application_module was provided to resolve it from. "
f"Please provide application_module parameter or register dependencies manually."
)
super().__init__(message)
class ApplicationModuleDependencyAnalyzer:
"""Analyzes ApplicationModule to find and resolve dependencies"""
def __init__(self, application_module: t.Union[t.Type["ModuleBase"], str]):
# Support both module type and import string
if isinstance(application_module, str):
from ellar.utils.importer import import_from_string
self.application_module = t.cast(
t.Type["ModuleBase"], import_from_string(application_module)
)
else:
self.application_module = application_module
self._module_tree = self._build_module_tree()
def get_application_module_providers(self) -> t.List[t.Type]:
"""Get all provider types from the ApplicationModule tree"""
mod_data = self._module_tree.get_app_module()
if mod_data:
return list(mod_data.providers.values())
return []
def _build_module_tree(self) -> "ModuleTreeManager":
"""Build complete module tree for ApplicationModule"""
from ellar.app import AppFactory
from ellar.core.modules import ModuleSetup
module_setup = ModuleSetup(self.application_module)
return AppFactory.read_all_module(module_setup)
def get_type_from_tag(self, tag: t.Any) -> t.Optional[t.Type[t.Any]]:
"""
Get the type/interface associated with a given tag from ApplicationModule tree
When a controller uses InjectByTag('tag_name'), this method finds the provider
registered with that tag and returns its actual type/interface.
:param tag: NewType created by InjectByTag function
:return: The type/interface of the provider with matching tag, or None if not found
"""
# Extract the tag string from NewType
# InjectByTag returns t.NewType(Tag(tag_string), Tag)
# where Tag(tag_string) becomes the __name__ and Tag class is __supertype__
# Since Tag is a subclass of str, the Tag instance itself is the tag string
if not hasattr(tag, "__name__"):
return None
tag_instance = getattr(tag, "__name__", None)
if not isinstance(tag_instance, Tag):
return None
# Tag is a str subclass, so tag_instance itself is the tag string
tag_string = str(tag_instance)
for module_data in self._module_tree.modules.values():
module_setup = module_data.value
for _k, v in module_setup.providers.items():
if v.tag == tag_string:
return v.get_type()
return None
def find_module_providing_service(
self, service_type: t.Type
) -> t.Optional[t.Type["ModuleBase"]]:
"""
Search ApplicationModule tree for module that provides/exports a service
Returns the module that exports the service, or None if not found
"""
result = self._module_tree.search_module_tree(
filter_item=lambda data: True,
find_predicate=lambda data: (
service_type in data.exports or service_type in data.providers.keys()
),
)
return t.cast(t.Type["ModuleBase"], result.value.module) if result else None
def get_all_modules(self) -> t.List[t.Type["ModuleBase"]]:
"""Get all modules in the ApplicationModule tree"""
return [
t.cast(t.Type["ModuleBase"], data.value.module)
for data in self._module_tree.modules.values()
]
def get_module_dependencies(
self, module: t.Type["ModuleBase"]
) -> t.List[t.Type["ModuleBase"]]:
"""
Get all module dependencies for a given module recursively
This returns all modules that the given module depends on,
including nested dependencies.
"""
module_data = self._module_tree.get_module(module)
if not module_data:
return []
dependencies = []
visited = set()
def collect_dependencies(mod: t.Type["ModuleBase"]) -> None:
if mod in visited:
return
visited.add(mod)
mod_data = self._module_tree.get_module(mod)
if not mod_data:
return
# Get direct dependencies
dep_modules = self._module_tree.get_module_dependencies(mod)
for dep in dep_modules:
dep_module = t.cast(t.Type["ModuleBase"], dep.value.module)
if dep_module not in dependencies:
dependencies.append(dep_module)
# Recursively collect nested dependencies
collect_dependencies(dep_module)
collect_dependencies(module)
return dependencies
def resolve_forward_ref(
self, forward_ref: "ForwardRefModule"
) -> t.Optional["ModuleSetup"]:
"""
Resolve a ForwardRefModule to its actual module from ApplicationModule tree
ForwardRefModule can reference by:
- module_name: e.g., ForwardRefModule(module_name="UserModule")
- module (type): e.g., ForwardRefModule(module=UserModule)
- module (string): e.g., ForwardRefModule(module="app.users:UserModule")
"""
# ForwardRefModule has either 'module' or 'module_name', not both
if hasattr(forward_ref, "module_name") and forward_ref.module_name:
# Search by module name (uses @Module(name="..."))
result = self._module_tree.search_module_tree(
filter_item=lambda data: True,
find_predicate=lambda data: data.name == forward_ref.module_name,
)
return t.cast("ModuleSetup", result.value) if result else None
elif hasattr(forward_ref, "module") and forward_ref.module:
# Module can be a Type or a string import path
if isinstance(forward_ref.module, str):
# Import string path, e.g., "app.users:UserModule"
try:
module_cls = import_from_string(forward_ref.module)
except Exception:
return None
else:
# Direct type reference
module_cls = forward_ref.module
# Search for this module type in the tree
module_data = self._module_tree.get_module(module_cls)
if module_data:
return t.cast("ModuleSetup", module_data.value)
return None
class ControllerDependencyAnalyzer:
"""Analyzes controllers to extract their dependencies"""
@staticmethod
def get_dependencies(controller: t.Type["ControllerBase"]) -> t.List[t.Type]:
"""
Extract dependency types from controller __init__ signature
Uses injector.get_bindings() if available, falls back to type hint inspection.
Returns list of types that need to be injected.
"""
dependencies = []
# Try to use injector's get_bindings first (more robust for @injectable classes)
try:
bindings = injector.get_bindings(controller)
if bindings: # Only use if it found bindings
dependencies = [
binding.interface
for binding in bindings
if hasattr(binding, "interface")
and not isinstance(binding.interface, str)
]
return dependencies
except Exception:
pass # Fall through to manual inspection
# Fallback to type hint inspection (for controllers without @injectable)
if not hasattr(controller, "__init__"):
return dependencies
try:
type_hints = get_type_hints(controller.__init__)
except Exception:
# Fallback to inspect if get_type_hints fails
sig = inspect.signature(controller.__init__)
type_hints = {
name: param.annotation
for name, param in sig.parameters.items()
if param.annotation != inspect.Parameter.empty
}
# Extract non-primitive dependencies (skip 'self', 'return', primitives)
for param_name, param_type in type_hints.items():
if param_name in ("self", "return"):
continue
# Skip primitive types
if param_type in (str, int, float, bool, dict, list, type(None)):
continue
dependencies.append(param_type)
return dependencies