af-di-core 0.0.2__py3-none-any.whl

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.
@@ -0,0 +1,173 @@
1
+ Metadata-Version: 2.4
2
+ Name: af-di-core
3
+ Version: 0.0.2
4
+ Summary: Reusable framework-agnostic DI container & @component auto-discovery
5
+ License: MIT
6
+ Author: Allfly
7
+ Author-email: engineering@allfly.io
8
+ Requires-Python: >=3.13,<4.0
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Requires-Dist: dependency-injector (>=4.48.1,<5.0.0)
14
+ Requires-Dist: loguru (>=0.7.2,<0.8.0)
15
+ Description-Content-Type: text/markdown
16
+
17
+ # af-di-core
18
+
19
+ A small, framework-agnostic dependency-injection framework for Python. Decorate your classes with `@component`, point the auto-discovery scanner at your package, and get a wired global container — no manual registration boilerplate. Works in a FastAPI app, a plain script, an AWS Lambda, or anywhere else.
20
+
21
+ Built on [`dependency-injector`](https://python-dependency-injector.ets-labs.org/) for the underlying provider machinery.
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ pip install af-di-core
27
+ # or with Poetry:
28
+ poetry add af-di-core
29
+ ```
30
+
31
+ ## Quickstart
32
+
33
+ Mark the classes you want managed with `@component` (pairs naturally with `@attrs.define`):
34
+
35
+ ```python
36
+ import attrs
37
+ from allfly.di.core import component
38
+
39
+
40
+ @component
41
+ @attrs.define
42
+ class GreetingRepository:
43
+ def greeting(self) -> str:
44
+ return "hello"
45
+
46
+
47
+ @component
48
+ @attrs.define
49
+ class GreetingService:
50
+ _repo: GreetingRepository
51
+
52
+ def greet(self) -> str:
53
+ return self._repo.greeting()
54
+ ```
55
+
56
+ At startup, scan your package once and then resolve anything:
57
+
58
+ ```python
59
+ from allfly.di.core import auto_discover_components, di_provide
60
+
61
+ auto_discover_components(base_package="myapp")
62
+
63
+ service = di_provide(GreetingService) # GreetingRepository injected automatically
64
+ service.greet()
65
+ ```
66
+
67
+ Constructor dependencies are resolved from their type hints. Registration is multi-pass, so the order in which components are discovered does not matter. Parameters **with default values are treated as optional** and skipped, and an `Optional[T]` / `T | None` dependency is skipped when no provider for `T` exists.
68
+
69
+ ## How resolution works
70
+
71
+ `auto_discover_components(base_package, registrars=None)` runs in three steps:
72
+
73
+ 1. **`@settings` functions** are registered first as singletons (see below).
74
+ 2. **`registrars`** — optional callbacks for manual singletons that need special construction (e.g. third-party clients) — are invoked.
75
+ 3. **`@component` classes** are registered with multi-pass dependency resolution.
76
+
77
+ ```python
78
+ from allfly.di.core import auto_discover_components, di_register_singleton
79
+
80
+
81
+ def register_external_clients() -> None:
82
+ di_register_singleton(SomeClient, api_key="...")
83
+
84
+
85
+ auto_discover_components(base_package="myapp", registrars=[register_external_clients])
86
+ ```
87
+
88
+ ## Settings providers
89
+
90
+ Use `@settings` on a function whose return type is the type to register. Combine with `functools.lru_cache` for single instantiation:
91
+
92
+ ```python
93
+ from functools import lru_cache
94
+ from allfly.di.core import settings
95
+
96
+
97
+ @settings
98
+ @lru_cache
99
+ def get_db_settings() -> DatabaseSettings:
100
+ return DatabaseSettings()
101
+ ```
102
+
103
+ The returned instance is registered as a singleton keyed by the return annotation, so any `@component` depending on `DatabaseSettings` receives it.
104
+
105
+ ## Core providers (app-supplied)
106
+
107
+ `af-di-core` ships **no** opinionated providers — it is deliberately decoupled from databases, sessions, and web frameworks. Your application supplies its own "core" providers (things that must exist before anything is resolved, e.g. a DB session factory) by registering one or more callables on the container. They run **once**, lazily, the first time anything is provided:
108
+
109
+ ```python
110
+ from allfly.di.core import register_core_provider, di_register, di_provide
111
+
112
+
113
+ def register_db_providers() -> None:
114
+ di_register(DatabaseSettings, providers.Object(get_db_settings()))
115
+ di_register_singleton(SessionFactory, settings=get_provider(DatabaseSettings))
116
+
117
+
118
+ register_core_provider(register_db_providers)
119
+
120
+ # First di_provide(...) anywhere triggers ensure_core_providers() internally.
121
+ di_provide(SessionFactory)
122
+ ```
123
+
124
+ You can also drive this explicitly via `ensure_core_providers()`. Initialization is guarded by a lock and a one-time flag, so it is safe to call repeatedly. Register core providers at startup, before the first `di_provide`.
125
+
126
+ ## Public API
127
+
128
+ ```python
129
+ from allfly.di.core import (
130
+ component, # class decorator → register for auto-discovery
131
+ settings, # function decorator → register a singleton by return type
132
+ auto_discover_components, # scan a package and wire the container
133
+
134
+ di_provide, # resolve an instance by type
135
+ di_register, # register a raw dependency-injector provider
136
+ di_register_factory, # register a Factory provider
137
+ di_register_singleton, # register a Singleton provider
138
+ get_provider, # get the provider (not the instance) for chaining
139
+ provider_exists, # check whether a type is registered
140
+
141
+ register_core_provider, # add an app-supplied core provider callable
142
+ ensure_core_providers, # run core providers once (called lazily by di_provide)
143
+
144
+ get_global_container, # the GlobalDependencyContainer singleton
145
+ dependency_container, # alias for the same singleton
146
+ GlobalDependencyContainer, # the container type
147
+ get_component_registry, # introspection: everything @component/@settings collected
148
+ analyze_component_dependencies, # introspection: a class's required constructor deps
149
+ )
150
+ ```
151
+
152
+ ## FastAPI
153
+
154
+ The framework intentionally does **not** import FastAPI. To expose a component to routes, write the small glue in your app:
155
+
156
+ ```python
157
+ from typing import Annotated
158
+ from fastapi import Depends, Request
159
+ from allfly.di.core import get_global_container
160
+
161
+ # Make the container available on app.state at startup:
162
+ # application.state.provide = get_global_container().provide
163
+
164
+ def build(request: Request) -> GreetingService:
165
+ return request.app.state.provide(GreetingService)
166
+
167
+ GreetingServiceDI = Annotated[GreetingService, Depends(build)]
168
+ ```
169
+
170
+ ## Logging
171
+
172
+ The library logs through [loguru](https://github.com/Delgan/loguru) (mostly at `trace`/`debug`). If your app does not configure loguru, these messages are simply silent by default at higher levels.
173
+
@@ -0,0 +1,8 @@
1
+ allfly/di/core/__init__.py,sha256=Pe3oWMXkrvfg1f2kMUk33CY5e3igUf1YmufFdvbqkYM,960
2
+ allfly/di/core/auto_discovery.py,sha256=0qS-EVQhM2rU8AVSPQcbWrH0c9_17Xc-Uq-c-51uQBg,9968
3
+ allfly/di/core/container.py,sha256=27rNBgGfkGF_lOyNIJK753miW2kDonitQxAK869q6pk,3632
4
+ allfly/di/core/decorators.py,sha256=IhAjykpQQFFAhcRvbp8bG5Y-H1HQe6QQyvDwLgr8TSk,2508
5
+ allfly/di/core/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ af_di_core-0.0.2.dist-info/METADATA,sha256=XHBtP5VtMyDT7jWCG9SBOpSx49gUdeediibg36yok90,6475
7
+ af_di_core-0.0.2.dist-info/WHEEL,sha256=EGEvSphFYqXKs23-kQBeyNoJP1nrT8ZJKQoi5p5DYL8,88
8
+ af_di_core-0.0.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.4.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,41 @@
1
+ from allfly.di.core.auto_discovery import auto_discover_components
2
+ from allfly.di.core.container import (
3
+ GlobalDependencyContainer,
4
+ dependency_container,
5
+ di_provide,
6
+ di_register,
7
+ di_register_factory,
8
+ di_register_singleton,
9
+ ensure_core_providers,
10
+ get_global_container,
11
+ get_provider,
12
+ provider_exists,
13
+ register_core_provider,
14
+ )
15
+ from allfly.di.core.decorators import (
16
+ analyze_component_dependencies,
17
+ component,
18
+ get_component_registry,
19
+ settings,
20
+ )
21
+
22
+ __version__ = "0.0.0"
23
+
24
+ __all__ = [
25
+ "GlobalDependencyContainer",
26
+ "analyze_component_dependencies",
27
+ "auto_discover_components",
28
+ "component",
29
+ "dependency_container",
30
+ "di_provide",
31
+ "di_register",
32
+ "di_register_factory",
33
+ "di_register_singleton",
34
+ "ensure_core_providers",
35
+ "get_component_registry",
36
+ "get_global_container",
37
+ "get_provider",
38
+ "provider_exists",
39
+ "register_core_provider",
40
+ "settings",
41
+ ]
@@ -0,0 +1,269 @@
1
+ import importlib
2
+ import pkgutil
3
+ import types
4
+ from collections.abc import Callable
5
+ from dataclasses import dataclass
6
+ from typing import Type, get_args, get_origin
7
+ from typing import Union as TypingUnion
8
+
9
+ from dependency_injector import providers
10
+ from loguru import logger
11
+
12
+ from allfly.di.core.container import (
13
+ GlobalDependencyContainer,
14
+ dependency_container,
15
+ di_register,
16
+ di_register_factory,
17
+ provider_exists,
18
+ )
19
+ from allfly.di.core.decorators import analyze_component_dependencies, get_component_registry
20
+
21
+
22
+ @dataclass
23
+ class DependencyAnalysis:
24
+ type: Type
25
+ available: bool
26
+ suggestion: str
27
+
28
+
29
+ @dataclass
30
+ class ComponentDependencyAnalysis:
31
+ dependencies: dict[str, DependencyAnalysis]
32
+
33
+ def get_missing_dependencies(self) -> dict[str, DependencyAnalysis]:
34
+ return {name: analysis for name, analysis in self.dependencies.items() if not analysis.available}
35
+
36
+ def get_available_dependencies(self) -> dict[str, DependencyAnalysis]:
37
+ return {name: analysis for name, analysis in self.dependencies.items() if analysis.available}
38
+
39
+ def has_missing_dependencies(self) -> bool:
40
+ return any(not analysis.available for analysis in self.dependencies.values())
41
+
42
+
43
+ def auto_discover_components(
44
+ base_package: str = "src", registrars: list[Callable[[], None]] | None = None
45
+ ) -> GlobalDependencyContainer:
46
+ logger.info(f"Starting auto-discovery of components in package '{base_package}'")
47
+
48
+ _import_all_modules(base_package)
49
+
50
+ registry = get_component_registry()
51
+
52
+ function_components = [item for item in registry if callable(item) and not isinstance(item, type)]
53
+ class_components = [item for item in registry if isinstance(item, type)]
54
+
55
+ logger.trace(f"Found {len(function_components)} callable components and {len(class_components)} class components")
56
+
57
+ logger.trace(f"Step 1: Registering {len(function_components)} callable components")
58
+ for func in function_components:
59
+ _register_component_function(func)
60
+
61
+ if registrars:
62
+ logger.trace(f"Step 2: Calling {len(registrars)} custom registrar(s)")
63
+ for idx, registrar in enumerate(registrars, 1):
64
+ logger.trace(f"Calling registrar {idx}/{len(registrars)}: {registrar.__name__}")
65
+ try:
66
+ registrar()
67
+ except Exception as e:
68
+ logger.error(f"Failed to execute registrar {registrar.__name__}: {e}")
69
+ raise
70
+
71
+ logger.trace(f"Step 3: Registering {len(class_components)} class components")
72
+
73
+ registered_count = _register_component_classes_with_resolution(class_components)
74
+
75
+ logger.info(
76
+ f"Auto-discovery complete: {len(function_components)} functions + {registered_count} classes registered"
77
+ )
78
+ return dependency_container()
79
+
80
+
81
+ def _import_all_modules(package_name: str) -> None:
82
+ try:
83
+ package = importlib.import_module(package_name)
84
+ package_path = getattr(package, "__path__", None)
85
+
86
+ if package_path is None:
87
+ return
88
+
89
+ for _, module_name, _ in pkgutil.walk_packages(package_path, package_name + "."):
90
+ try:
91
+ importlib.import_module(module_name)
92
+ except Exception as e:
93
+ logger.trace(f"Could not import module {module_name}: {e}")
94
+ continue
95
+
96
+ except Exception as e:
97
+ logger.warning(f"Could not scan package {package_name}: {e}")
98
+
99
+
100
+ def _register_component(component_cls: Type) -> None:
101
+ dependencies = analyze_component_dependencies(component_cls)
102
+
103
+ if not dependencies:
104
+ di_register_factory(component_cls)
105
+ return
106
+
107
+ for param_name, param_type in dependencies.items():
108
+ base_type, is_optional = _unwrap_union_non_none(param_type)
109
+ if is_optional and not provider_exists(base_type):
110
+ continue
111
+ if not provider_exists(base_type):
112
+ available_types = _get_registered_types()
113
+ available_names = [_get_type_name(cls) for cls in available_types]
114
+ raise ValueError(
115
+ f"Cannot resolve dependency '{param_name}: {_get_type_name(param_type)}' for {component_cls.__name__}. "
116
+ f"Available types: {available_names}"
117
+ )
118
+
119
+ def lazy_factory():
120
+ container = dependency_container()
121
+ resolved_deps = {}
122
+ for item_name, item_type in dependencies.items():
123
+ base_type, is_optional = _unwrap_union_non_none(item_type)
124
+ if is_optional and not provider_exists(base_type):
125
+ continue
126
+ resolved_deps[item_name] = container.provide(base_type)
127
+ return component_cls(**resolved_deps)
128
+
129
+ factory_provider = providers.Callable(lazy_factory)
130
+ di_register(component_cls, factory_provider)
131
+
132
+
133
+ def _unwrap_union_non_none(tp: type) -> tuple[type, bool]:
134
+ origin = None
135
+ try:
136
+ origin = get_origin(tp)
137
+ except Exception:
138
+ pass
139
+ if origin in (types.UnionType, TypingUnion):
140
+ args = get_args(tp)
141
+ non_none = [a for a in args if a is not type(None)]
142
+ if len(non_none) == 1:
143
+ return non_none[0], True
144
+ return tp, False
145
+
146
+
147
+ def _get_registered_types() -> set[Type]:
148
+ try:
149
+ container = dependency_container()
150
+ return set(container.get_registry_keys())
151
+ except Exception:
152
+ return set()
153
+
154
+
155
+ def _get_type_name(param_type: type) -> str:
156
+ if isinstance(param_type, types.UnionType):
157
+ args = get_args(param_type)
158
+ arg_names = [_get_type_name(arg) for arg in args]
159
+ return " | ".join(arg_names)
160
+ elif hasattr(param_type, "__name__"):
161
+ return param_type.__name__
162
+ elif hasattr(param_type, "_name"):
163
+ return param_type._name # noqa: SLF001
164
+ else:
165
+ return str(param_type)
166
+
167
+
168
+ def _register_component_function(func) -> None:
169
+ registration_type = getattr(func, "__di_registration_type__", None)
170
+ if not registration_type:
171
+ raise ValueError(f"Component function {func.__name__} missing __di_registration_type__ metadata")
172
+
173
+ try:
174
+ component_instance = func()
175
+ di_register(registration_type, providers.Object(component_instance))
176
+
177
+ logger.trace(f"Registered settings: {registration_type.__name__} from {func.__name__}()")
178
+ except Exception as e:
179
+ logger.error(f"Failed to register settings from {func.__name__}: {e}")
180
+ raise
181
+
182
+
183
+ def _register_component_classes_with_resolution(class_components: list[Type]) -> int:
184
+ remaining_components = set(class_components)
185
+ registered_count = 0
186
+ pass_number = 1
187
+ max_passes = len(class_components) + 1
188
+
189
+ while remaining_components and pass_number <= max_passes:
190
+ logger.trace(f"Registration pass {pass_number}: {len(remaining_components)} components remaining")
191
+
192
+ registered_this_pass = []
193
+
194
+ for component_cls in list(remaining_components):
195
+ try:
196
+ _register_component(component_cls)
197
+ registered_this_pass.append(component_cls)
198
+ registered_count += 1
199
+ logger.trace(f"Successfully registered component: {component_cls.__name__}")
200
+ except Exception as e:
201
+ logger.trace(f"Pass {pass_number}: {component_cls.__name__} not ready: {e}")
202
+ continue
203
+
204
+ for component_cls in registered_this_pass:
205
+ remaining_components.remove(component_cls)
206
+
207
+ if not registered_this_pass:
208
+ logger.trace(f"No progress in pass {pass_number}, stopping registration")
209
+ break
210
+
211
+ pass_number += 1
212
+
213
+ if remaining_components:
214
+ _report_unregistered_components(remaining_components)
215
+
216
+ return registered_count
217
+
218
+
219
+ def _report_unregistered_components(unregistered: set[Type]) -> None:
220
+ logger.warning(f"Could not register {len(unregistered)} components due to missing dependencies:")
221
+ logger.warning("")
222
+
223
+ for component_cls in unregistered:
224
+ analysis = _analyze_missing_dependencies(component_cls)
225
+
226
+ if not analysis.dependencies:
227
+ logger.warning(f" - {component_cls.__name__} (could not analyze dependencies)")
228
+ continue
229
+
230
+ total_count = len(analysis.dependencies)
231
+
232
+ logger.warning(f" - {component_cls.__name__} requires {total_count} dependencies:")
233
+
234
+ for param_name, dep_info in analysis.get_available_dependencies().items():
235
+ logger.warning(f" ✓ {param_name}: {_get_type_name(dep_info.type)} (available)")
236
+
237
+ for param_name, dep_info in analysis.get_missing_dependencies().items():
238
+ suggestion = f" - {dep_info.suggestion}" if dep_info.suggestion else ""
239
+ logger.warning(f" ✗ {param_name}: {_get_type_name(dep_info.type)} (not registered{suggestion})")
240
+
241
+ logger.warning("")
242
+
243
+ logger.warning("To fix these issues:")
244
+ logger.warning(" 1. Add @component decorator to missing service/repository classes")
245
+ logger.warning(" 2. Or register missing dependencies manually as singletons before discovery")
246
+ logger.warning(" 3. Ensure all imported dependencies are accessible")
247
+
248
+
249
+ def _analyze_missing_dependencies(component_cls: Type) -> ComponentDependencyAnalysis:
250
+ try:
251
+ dependencies = analyze_component_dependencies(component_cls)
252
+ registered_types = _get_registered_types()
253
+
254
+ analysis = {}
255
+ for param_name, param_type in dependencies.items():
256
+ is_available = param_type in registered_types
257
+
258
+ suggestion = ""
259
+ if not is_available:
260
+ type_name = _get_type_name(param_type)
261
+ suggestion = f"Register {type_name} with @component decorator or manually as a singleton"
262
+
263
+ analysis[param_name] = DependencyAnalysis(type=param_type, available=is_available, suggestion=suggestion)
264
+
265
+ return ComponentDependencyAnalysis(dependencies=analysis)
266
+
267
+ except Exception as e:
268
+ logger.error(f"Failed to analyze dependencies for {component_cls.__name__}: {e}")
269
+ return ComponentDependencyAnalysis(dependencies={})
@@ -0,0 +1,118 @@
1
+ import threading
2
+ from collections.abc import Callable
3
+ from typing import Type, TypeVar
4
+
5
+ from dependency_injector import containers, providers
6
+ from loguru import logger
7
+
8
+ T = TypeVar("T")
9
+
10
+
11
+ class GlobalDependencyContainer(containers.DynamicContainer):
12
+ def __init__(self):
13
+ super().__init__()
14
+ self._type_registry: dict[Type, str] = {}
15
+ self._core_providers: list[Callable[[], None]] = []
16
+ self._core_providers_initialized = False
17
+ self._init_lock = threading.Lock()
18
+
19
+ def register(self, cls: Type[T], provider: providers.Provider):
20
+ name = f"_type_{cls.__module__}.{cls.__qualname__}"
21
+ self.set_provider(name, provider)
22
+ self._type_registry[cls] = name
23
+ logger.trace(f"Registered provider for {cls}")
24
+
25
+ def register_factory(self, cls: Type[T], **kwargs):
26
+ factory = providers.Factory(cls, **kwargs)
27
+ self.register(cls, factory)
28
+
29
+ def register_singleton(self, cls: Type[T], **kwargs):
30
+ singleton = providers.Singleton(cls, **kwargs)
31
+ self.register(cls, singleton)
32
+
33
+ def provide(self, cls: Type[T]) -> T:
34
+ name = self._type_registry.get(cls)
35
+ if not name:
36
+ raise KeyError(f"No provider for {cls}")
37
+ logger.trace(f"Providing {cls}")
38
+ return self.providers[name]()
39
+
40
+ def get_provider(self, cls: Type[T]) -> providers.Provider:
41
+ name = self._type_registry.get(cls)
42
+ if not name:
43
+ raise KeyError(f"No provider for {cls}")
44
+ return self.providers[name]
45
+
46
+ def get_registry_keys(self):
47
+ return self._type_registry.keys()
48
+
49
+ def get_registered_type_by_name(self, class_name: str) -> type | None:
50
+ for registered_class in self.get_registry_keys():
51
+ if registered_class.__name__ == class_name:
52
+ return registered_class
53
+ return None
54
+
55
+ def register_core_provider(self, registrar: Callable[[], None]) -> None:
56
+ self._core_providers.append(registrar)
57
+
58
+ def ensure_core_providers(self) -> None:
59
+ with self._init_lock:
60
+ if self._core_providers_initialized:
61
+ return
62
+
63
+ for registrar in self._core_providers:
64
+ registrar()
65
+
66
+ self._core_providers_initialized = True
67
+
68
+
69
+ logger.debug("Creating global dependency container")
70
+ __global_container = GlobalDependencyContainer()
71
+ logger.debug("Global dependency container created")
72
+
73
+
74
+ def dependency_container() -> GlobalDependencyContainer:
75
+ return __global_container
76
+
77
+
78
+ def get_global_container() -> GlobalDependencyContainer:
79
+ return __global_container
80
+
81
+
82
+ def register_core_provider(registrar: Callable[[], None]) -> None:
83
+ dependency_container().register_core_provider(registrar)
84
+
85
+
86
+ def ensure_core_providers() -> None:
87
+ dependency_container().ensure_core_providers()
88
+
89
+
90
+ def di_register(cls: Type[T], provider: providers.Provider):
91
+ dependency_container().register(cls, provider)
92
+
93
+
94
+ def di_register_factory(cls: Type[T], **kwargs):
95
+ dependency_container().register_factory(cls, **kwargs)
96
+
97
+
98
+ def di_register_singleton(cls: Type[T], **kwargs):
99
+ dependency_container().register_singleton(cls, **kwargs)
100
+
101
+
102
+ def di_provide(cls: Type[T]) -> T:
103
+ ensure_core_providers()
104
+ try:
105
+ return dependency_container().provide(cls)
106
+ except KeyError as e:
107
+ logger.error(f"No provider for {cls}")
108
+ raise e
109
+
110
+
111
+ def get_provider(cls: Type[T]) -> providers.Provider:
112
+ ensure_core_providers()
113
+ return dependency_container().get_provider(cls)
114
+
115
+
116
+ def provider_exists(cls: Type[T]) -> bool:
117
+ ensure_core_providers()
118
+ return cls in dependency_container().get_registry_keys()
@@ -0,0 +1,86 @@
1
+ import inspect
2
+ from typing import Callable, Type, TypeVar, Union, get_type_hints
3
+
4
+ from loguru import logger
5
+
6
+ T = TypeVar("T")
7
+
8
+ _component_registry: set[Union[Type, Callable]] = set()
9
+
10
+
11
+ def component(cls: Type[T]) -> Type[T]:
12
+ logger.trace(f"Marking {cls.__name__} as component")
13
+
14
+ if not _has_valid_constructor(cls):
15
+ raise ValueError(f"@component class {cls.__name__} must have a proper __init__ method")
16
+
17
+ _component_registry.add(cls)
18
+
19
+ return cls
20
+
21
+
22
+ def get_component_registry() -> set[Union[Type, Callable]]:
23
+ return _component_registry.copy()
24
+
25
+
26
+ def analyze_component_dependencies(cls: Type) -> dict[str, Type]:
27
+ try:
28
+ type_hints = get_type_hints(cls.__init__)
29
+ function_signature = inspect.signature(cls.__init__)
30
+
31
+ dependencies = {}
32
+ for name, hint in type_hints.items():
33
+ if name in ("return", "self"):
34
+ continue
35
+
36
+ param = function_signature.parameters.get(name)
37
+ if param and param.default is not inspect.Parameter.empty:
38
+ logger.trace(f"Skipping {cls.__name__}.{name} - has default value")
39
+ continue
40
+
41
+ dependencies[name] = hint
42
+
43
+ logger.trace(f"Analyzed {cls.__name__} dependencies: {list(dependencies.keys())}")
44
+
45
+ return dependencies
46
+
47
+ except Exception as e:
48
+ raise ValueError(f"Failed to analyze dependencies for {cls.__name__}: {e}") from e
49
+
50
+
51
+ def _has_valid_constructor(cls: Type) -> bool:
52
+ try:
53
+ init_method = getattr(cls, "__init__", None)
54
+ if not init_method or not callable(init_method):
55
+ return False
56
+
57
+ signature = inspect.signature(init_method)
58
+
59
+ params = list(signature.parameters.keys())
60
+ return len(params) >= 1 and params[0] == "self"
61
+
62
+ except Exception:
63
+ return False
64
+
65
+
66
+ def settings(func: Callable) -> Callable:
67
+ registration_type = _extract_return_type(func)
68
+
69
+ if not registration_type:
70
+ raise ValueError(f"@settings on {func.__name__} requires a return type annotation")
71
+
72
+ func.__di_registration_type__ = registration_type
73
+
74
+ _component_registry.add(func)
75
+ logger.trace(f"Marked {func.__name__} as settings provider for {registration_type.__name__}")
76
+
77
+ return func
78
+
79
+
80
+ def _extract_return_type(func: Callable) -> Type | None:
81
+ try:
82
+ hints = get_type_hints(func)
83
+ return hints.get("return")
84
+ except Exception as e:
85
+ logger.warning(f"Could not extract return type from {func.__name__}: {e}")
86
+ return None
File without changes