af-di-core 0.0.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.
- af_di_core-0.0.2/PKG-INFO +173 -0
- af_di_core-0.0.2/README.md +156 -0
- af_di_core-0.0.2/pyproject.toml +23 -0
- af_di_core-0.0.2/src/allfly/di/core/__init__.py +41 -0
- af_di_core-0.0.2/src/allfly/di/core/auto_discovery.py +269 -0
- af_di_core-0.0.2/src/allfly/di/core/container.py +118 -0
- af_di_core-0.0.2/src/allfly/di/core/decorators.py +86 -0
- af_di_core-0.0.2/src/allfly/di/core/py.typed +0 -0
|
@@ -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,156 @@
|
|
|
1
|
+
# af-di-core
|
|
2
|
+
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
Built on [`dependency-injector`](https://python-dependency-injector.ets-labs.org/) for the underlying provider machinery.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install af-di-core
|
|
11
|
+
# or with Poetry:
|
|
12
|
+
poetry add af-di-core
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quickstart
|
|
16
|
+
|
|
17
|
+
Mark the classes you want managed with `@component` (pairs naturally with `@attrs.define`):
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
import attrs
|
|
21
|
+
from allfly.di.core import component
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@component
|
|
25
|
+
@attrs.define
|
|
26
|
+
class GreetingRepository:
|
|
27
|
+
def greeting(self) -> str:
|
|
28
|
+
return "hello"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@component
|
|
32
|
+
@attrs.define
|
|
33
|
+
class GreetingService:
|
|
34
|
+
_repo: GreetingRepository
|
|
35
|
+
|
|
36
|
+
def greet(self) -> str:
|
|
37
|
+
return self._repo.greeting()
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
At startup, scan your package once and then resolve anything:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from allfly.di.core import auto_discover_components, di_provide
|
|
44
|
+
|
|
45
|
+
auto_discover_components(base_package="myapp")
|
|
46
|
+
|
|
47
|
+
service = di_provide(GreetingService) # GreetingRepository injected automatically
|
|
48
|
+
service.greet()
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
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.
|
|
52
|
+
|
|
53
|
+
## How resolution works
|
|
54
|
+
|
|
55
|
+
`auto_discover_components(base_package, registrars=None)` runs in three steps:
|
|
56
|
+
|
|
57
|
+
1. **`@settings` functions** are registered first as singletons (see below).
|
|
58
|
+
2. **`registrars`** — optional callbacks for manual singletons that need special construction (e.g. third-party clients) — are invoked.
|
|
59
|
+
3. **`@component` classes** are registered with multi-pass dependency resolution.
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from allfly.di.core import auto_discover_components, di_register_singleton
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def register_external_clients() -> None:
|
|
66
|
+
di_register_singleton(SomeClient, api_key="...")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
auto_discover_components(base_package="myapp", registrars=[register_external_clients])
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Settings providers
|
|
73
|
+
|
|
74
|
+
Use `@settings` on a function whose return type is the type to register. Combine with `functools.lru_cache` for single instantiation:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from functools import lru_cache
|
|
78
|
+
from allfly.di.core import settings
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@settings
|
|
82
|
+
@lru_cache
|
|
83
|
+
def get_db_settings() -> DatabaseSettings:
|
|
84
|
+
return DatabaseSettings()
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The returned instance is registered as a singleton keyed by the return annotation, so any `@component` depending on `DatabaseSettings` receives it.
|
|
88
|
+
|
|
89
|
+
## Core providers (app-supplied)
|
|
90
|
+
|
|
91
|
+
`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:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from allfly.di.core import register_core_provider, di_register, di_provide
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def register_db_providers() -> None:
|
|
98
|
+
di_register(DatabaseSettings, providers.Object(get_db_settings()))
|
|
99
|
+
di_register_singleton(SessionFactory, settings=get_provider(DatabaseSettings))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
register_core_provider(register_db_providers)
|
|
103
|
+
|
|
104
|
+
# First di_provide(...) anywhere triggers ensure_core_providers() internally.
|
|
105
|
+
di_provide(SessionFactory)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
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`.
|
|
109
|
+
|
|
110
|
+
## Public API
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from allfly.di.core import (
|
|
114
|
+
component, # class decorator → register for auto-discovery
|
|
115
|
+
settings, # function decorator → register a singleton by return type
|
|
116
|
+
auto_discover_components, # scan a package and wire the container
|
|
117
|
+
|
|
118
|
+
di_provide, # resolve an instance by type
|
|
119
|
+
di_register, # register a raw dependency-injector provider
|
|
120
|
+
di_register_factory, # register a Factory provider
|
|
121
|
+
di_register_singleton, # register a Singleton provider
|
|
122
|
+
get_provider, # get the provider (not the instance) for chaining
|
|
123
|
+
provider_exists, # check whether a type is registered
|
|
124
|
+
|
|
125
|
+
register_core_provider, # add an app-supplied core provider callable
|
|
126
|
+
ensure_core_providers, # run core providers once (called lazily by di_provide)
|
|
127
|
+
|
|
128
|
+
get_global_container, # the GlobalDependencyContainer singleton
|
|
129
|
+
dependency_container, # alias for the same singleton
|
|
130
|
+
GlobalDependencyContainer, # the container type
|
|
131
|
+
get_component_registry, # introspection: everything @component/@settings collected
|
|
132
|
+
analyze_component_dependencies, # introspection: a class's required constructor deps
|
|
133
|
+
)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## FastAPI
|
|
137
|
+
|
|
138
|
+
The framework intentionally does **not** import FastAPI. To expose a component to routes, write the small glue in your app:
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from typing import Annotated
|
|
142
|
+
from fastapi import Depends, Request
|
|
143
|
+
from allfly.di.core import get_global_container
|
|
144
|
+
|
|
145
|
+
# Make the container available on app.state at startup:
|
|
146
|
+
# application.state.provide = get_global_container().provide
|
|
147
|
+
|
|
148
|
+
def build(request: Request) -> GreetingService:
|
|
149
|
+
return request.app.state.provide(GreetingService)
|
|
150
|
+
|
|
151
|
+
GreetingServiceDI = Annotated[GreetingService, Depends(build)]
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Logging
|
|
155
|
+
|
|
156
|
+
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.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "af-di-core"
|
|
3
|
+
version = "0.0.2"
|
|
4
|
+
description = "Reusable framework-agnostic DI container & @component auto-discovery"
|
|
5
|
+
authors = ["Allfly <engineering@allfly.io>"]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
packages = [
|
|
9
|
+
{include = "allfly", from = "src"},
|
|
10
|
+
{include = "allfly/di/core/py.typed", from = "src"},
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[tool.poetry.dependencies]
|
|
14
|
+
python = ">=3.13,<4.0"
|
|
15
|
+
dependency-injector = "^4.48.1"
|
|
16
|
+
loguru = "^0.7.2"
|
|
17
|
+
|
|
18
|
+
[tool.poetry.group.dev.dependencies]
|
|
19
|
+
pytest = "^8.0"
|
|
20
|
+
|
|
21
|
+
[build-system]
|
|
22
|
+
requires = ["poetry-core"]
|
|
23
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -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
|