pico-ioc 1.5.0__py3-none-any.whl → 2.0.1__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.
- pico_ioc/__init__.py +82 -57
- pico_ioc/_version.py +1 -1
- pico_ioc/aop.py +281 -0
- pico_ioc/api.py +1137 -198
- pico_ioc/config_runtime.py +289 -0
- pico_ioc/constants.py +10 -0
- pico_ioc/container.py +420 -148
- pico_ioc/event_bus.py +223 -0
- pico_ioc/exceptions.py +72 -0
- pico_ioc/factory.py +48 -0
- pico_ioc/locator.py +53 -0
- pico_ioc/scope.py +155 -39
- pico_ioc-2.0.1.dist-info/METADATA +243 -0
- pico_ioc-2.0.1.dist-info/RECORD +17 -0
- pico_ioc/_state.py +0 -75
- pico_ioc/builder.py +0 -210
- pico_ioc/config.py +0 -332
- pico_ioc/decorators.py +0 -120
- pico_ioc/infra.py +0 -196
- pico_ioc/interceptors.py +0 -76
- pico_ioc/plugins.py +0 -28
- pico_ioc/policy.py +0 -245
- pico_ioc/proxy.py +0 -115
- pico_ioc/public_api.py +0 -76
- pico_ioc/resolver.py +0 -101
- pico_ioc/scanner.py +0 -178
- pico_ioc/utils.py +0 -25
- pico_ioc-1.5.0.dist-info/METADATA +0 -249
- pico_ioc-1.5.0.dist-info/RECORD +0 -23
- {pico_ioc-1.5.0.dist-info → pico_ioc-2.0.1.dist-info}/WHEEL +0 -0
- {pico_ioc-1.5.0.dist-info → pico_ioc-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {pico_ioc-1.5.0.dist-info → pico_ioc-2.0.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pico-ioc
|
|
3
|
+
Version: 2.0.1
|
|
4
|
+
Summary: A minimalist, zero-dependency Inversion of Control (IoC) container for Python.
|
|
5
|
+
Author-email: David Perez Cabrera <dperezcabrera@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2025 David Pérez Cabrera
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/dperezcabrera/pico-ioc
|
|
29
|
+
Project-URL: Repository, https://github.com/dperezcabrera/pico-ioc
|
|
30
|
+
Project-URL: Issue Tracker, https://github.com/dperezcabrera/pico-ioc/issues
|
|
31
|
+
Keywords: ioc,di,dependency injection,inversion of control,decorator
|
|
32
|
+
Classifier: Development Status :: 4 - Beta
|
|
33
|
+
Classifier: Programming Language :: Python :: 3
|
|
34
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
40
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
41
|
+
Classifier: Operating System :: OS Independent
|
|
42
|
+
Requires-Python: >=3.8
|
|
43
|
+
Description-Content-Type: text/markdown
|
|
44
|
+
License-File: LICENSE
|
|
45
|
+
Provides-Extra: yaml
|
|
46
|
+
Requires-Dist: PyYAML; extra == "yaml"
|
|
47
|
+
Provides-Extra: graphviz
|
|
48
|
+
Requires-Dist: graphviz; extra == "graphviz"
|
|
49
|
+
Dynamic: license-file
|
|
50
|
+
|
|
51
|
+
# 📦 Pico-IoC: A Robust, Async-Native IoC Container for Python
|
|
52
|
+
|
|
53
|
+
[](https://pypi.org/project/pico-ioc/)
|
|
54
|
+
[](https://deepwiki.com/dperezcabrera/pico-ioc)
|
|
55
|
+
[](https://opensource.org/licenses/MIT)
|
|
56
|
+

|
|
57
|
+
[](https://codecov.io/gh/dperezcabrera/pico-ioc)
|
|
58
|
+
[](https://sonarcloud.io/summary/new_code?id=dperezcabrera_pico-ioc)
|
|
59
|
+
[](https://sonarcloud.io/summary/new_code?id=dperezcabrera_pico-ioc)
|
|
60
|
+
[](https://sonarcloud.io/summary/new_code?id=dperezcabrera_pico-ioc)
|
|
61
|
+
|
|
62
|
+
**Pico-IoC** is a **lightweight, async-ready, decorator-driven IoC container** built for clarity, testability, and performance.
|
|
63
|
+
It brings *Inversion of Control* and *dependency injection* to Python in a deterministic, modern, and framework-agnostic way.
|
|
64
|
+
|
|
65
|
+
> 🐍 Requires **Python 3.10+**
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## ⚖️ Core Principles
|
|
70
|
+
|
|
71
|
+
- **Single Purpose** – Do one thing: dependency management.
|
|
72
|
+
- **Declarative** – Use simple decorators (`@component`, `@factory`, `@configuration`) instead of config files or YAML magic.
|
|
73
|
+
- **Deterministic** – No hidden scanning or side-effects; everything flows from an explicit `init()`.
|
|
74
|
+
- **Async-Native** – Fully supports async providers, async lifecycle hooks, and async interceptors.
|
|
75
|
+
- **Fail-Fast** – Detects missing bindings and circular dependencies at bootstrap.
|
|
76
|
+
- **Testable by Design** – Use `overrides` and `profiles` to swap components instantly.
|
|
77
|
+
- **Zero Core Dependencies** – Built entirely on the Python standard library. Optional features may require external packages (see Installation).
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## 🚀 Why Pico-IoC?
|
|
82
|
+
|
|
83
|
+
As Python systems evolve, wiring dependencies by hand becomes fragile and unmaintainable.
|
|
84
|
+
**Pico-IoC** eliminates that friction by letting you declare how components relate — not how they’re created.
|
|
85
|
+
|
|
86
|
+
| Feature | Manual Wiring | With Pico-IoC |
|
|
87
|
+
| :------------- | :------------------------- | :------------------------------ |
|
|
88
|
+
| Object creation| `svc = Service(Repo(Config()))` | `svc = container.get(Service)` |
|
|
89
|
+
| Replacing deps | Monkey-patch | `overrides={Repo: FakeRepo()}` |
|
|
90
|
+
| Coupling | Tight | Loose |
|
|
91
|
+
| Testing | Painful | Instant |
|
|
92
|
+
| Async support | Manual | Built-in |
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## 🧩 Highlights (v2.0.0)
|
|
97
|
+
|
|
98
|
+
- **Full redesign:** unified architecture with simpler, more powerful APIs.
|
|
99
|
+
- **Async-aware AOP system** — method interceptors via `@intercepted_by`.
|
|
100
|
+
- **Typed configuration** — dataclasses with JSON/YAML/env sources.
|
|
101
|
+
- **Scoped resolution** — singleton, prototype, request, session, transaction.
|
|
102
|
+
- **UnifiedComponentProxy** — transparent lazy/AOP proxy supporting serialization.
|
|
103
|
+
- **Tree-based configuration runtime** with reusable adapters and discriminators.
|
|
104
|
+
- **Observable container context** with stats, health checks, and async cleanup.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## 📦 Installation
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
pip install pico-ioc
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
For optional features, you can install extras:
|
|
115
|
+
|
|
116
|
+
* **YAML Configuration:**
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
pip install pico-ioc[yaml]
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
(Requires `PyYAML`)
|
|
123
|
+
|
|
124
|
+
* **Dependency Graph Export:**
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
pip install pico-ioc[graphviz]
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
(Requires the `graphviz` Python package and the Graphviz command-line tools)
|
|
131
|
+
|
|
132
|
+
-----
|
|
133
|
+
|
|
134
|
+
## ⚙️ Quick Example
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from dataclasses import dataclass
|
|
138
|
+
from pico_ioc import component, configuration, init
|
|
139
|
+
|
|
140
|
+
@configuration
|
|
141
|
+
@dataclass
|
|
142
|
+
class Config:
|
|
143
|
+
db_url: str = "sqlite:///demo.db"
|
|
144
|
+
|
|
145
|
+
@component
|
|
146
|
+
class Repo:
|
|
147
|
+
def __init__(self, cfg: Config):
|
|
148
|
+
self.cfg = cfg
|
|
149
|
+
def fetch(self):
|
|
150
|
+
return f"fetching from {self.cfg.db_url}"
|
|
151
|
+
|
|
152
|
+
@component
|
|
153
|
+
class Service:
|
|
154
|
+
def __init__(self, repo: Repo):
|
|
155
|
+
self.repo = repo
|
|
156
|
+
def run(self):
|
|
157
|
+
return self.repo.fetch()
|
|
158
|
+
|
|
159
|
+
container = init(modules=[__name__])
|
|
160
|
+
svc = container.get(Service)
|
|
161
|
+
print(svc.run())
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**Output:**
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
fetching from sqlite:///demo.db
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
-----
|
|
171
|
+
|
|
172
|
+
## 🧪 Testing with Overrides
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
class FakeRepo:
|
|
176
|
+
def fetch(self): return "fake-data"
|
|
177
|
+
|
|
178
|
+
container = init(modules=[__name__], overrides={Repo: FakeRepo()})
|
|
179
|
+
svc = container.get(Service)
|
|
180
|
+
assert svc.run() == "fake-data"
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
-----
|
|
184
|
+
|
|
185
|
+
## 🩺 Lifecycle & AOP
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
from pico_ioc import intercepted_by, MethodInterceptor, MethodCtx
|
|
189
|
+
|
|
190
|
+
class LogInterceptor(MethodInterceptor):
|
|
191
|
+
def invoke(self, ctx: MethodCtx, call_next):
|
|
192
|
+
print(f"→ calling {ctx.name}")
|
|
193
|
+
res = call_next(ctx)
|
|
194
|
+
print(f"← {ctx.name} done")
|
|
195
|
+
return res
|
|
196
|
+
|
|
197
|
+
@component
|
|
198
|
+
class Demo:
|
|
199
|
+
@intercepted_by(LogInterceptor)
|
|
200
|
+
def work(self):
|
|
201
|
+
return "ok"
|
|
202
|
+
|
|
203
|
+
c = init(modules=[__name__])
|
|
204
|
+
c.get(Demo).work()
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
-----
|
|
208
|
+
|
|
209
|
+
## 📖 Documentation
|
|
210
|
+
|
|
211
|
+
The full documentation is available within the `docs/` directory of the project repository. Start with `docs/README.md` for navigation.
|
|
212
|
+
|
|
213
|
+
* **Getting Started:** `docs/getting-started.md`
|
|
214
|
+
* **User Guide:** `docs/user-guide/README.md`
|
|
215
|
+
* **Advanced Features:** `docs/advanced-features/README.md`
|
|
216
|
+
* **Observability:** `docs/observability/README.md`
|
|
217
|
+
* **Integrations:** `docs/integrations/README.md`
|
|
218
|
+
* **Cookbook (Patterns):** `docs/cookbook/README.md`
|
|
219
|
+
* **Architecture:** `docs/architecture/README.md`
|
|
220
|
+
* **API Reference:** `docs/api-reference/README.md`
|
|
221
|
+
* **ADR Index:** `docs/adr/README.md`
|
|
222
|
+
|
|
223
|
+
-----
|
|
224
|
+
|
|
225
|
+
## 🧩 Development
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
pip install tox
|
|
229
|
+
tox
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
-----
|
|
233
|
+
|
|
234
|
+
## 🧾 Changelog
|
|
235
|
+
|
|
236
|
+
See [CHANGELOG.md](./CHANGELOG.md) — *Full redesign for v2.0.0.*
|
|
237
|
+
|
|
238
|
+
-----
|
|
239
|
+
|
|
240
|
+
## 📜 License
|
|
241
|
+
|
|
242
|
+
MIT — [LICENSE](https://opensource.org/licenses/MIT)
|
|
243
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
pico_ioc/__init__.py,sha256=AfHqcDJaXLChLJhxej_0gMClHUThUrKABFlIKYVrVtc,2198
|
|
2
|
+
pico_ioc/_version.py,sha256=HVx0XJJ9OYFWBBPBCUFYb8Nm43ChPg9GZLh_dkxh9qI,22
|
|
3
|
+
pico_ioc/aop.py,sha256=prFSlZC6vJYUfTbkMvlSc1T9UvvdEHr94Z0HAvjZ1fg,12985
|
|
4
|
+
pico_ioc/api.py,sha256=Be3bFMPKtkFpHUuToEhDtSriVwyuBg1-b3vUs6WpsQ8,45753
|
|
5
|
+
pico_ioc/config_runtime.py,sha256=z1cHDb5PbM8PMLYRFf5c2dmze8V22xwEzpWcBhtmMpA,11950
|
|
6
|
+
pico_ioc/constants.py,sha256=AhIt0ieDZ9Turxb_YbNzj11wUbBbzjKfWh1BDlSx2Nw,183
|
|
7
|
+
pico_ioc/container.py,sha256=5hLPwoVNY_PsN6XYbbZ6_j1I8IBnteCcahus1vCI_JY,17514
|
|
8
|
+
pico_ioc/event_bus.py,sha256=E8Qb8KZ6K1CuXSbMlG0MNPHkGoWlssLLPzHq1QYdADQ,8346
|
|
9
|
+
pico_ioc/exceptions.py,sha256=GT8flzyXeUWetguc8RRkB4p56waTXMdeNhSKQQ8rh4w,2468
|
|
10
|
+
pico_ioc/factory.py,sha256=Q3aLwZ-MWbXKjm8unr871vlWSeVUDmzFQZ1mXzPkY5I,1557
|
|
11
|
+
pico_ioc/locator.py,sha256=PBxZYO_xCOxG7aJZ0adDtINrJass_ZDNYmPD2O_oNqM,2401
|
|
12
|
+
pico_ioc/scope.py,sha256=GDsDJWw7e5Vpiys-M4vQfKMJWSCiorRsT5cPo6z34Mk,5924
|
|
13
|
+
pico_ioc-2.0.1.dist-info/licenses/LICENSE,sha256=N1_nOvHTM6BobYnOTNXiQkroDqCEi6EzfGBv8lWtyZ0,1077
|
|
14
|
+
pico_ioc-2.0.1.dist-info/METADATA,sha256=U6L0obv__5poIDJvadj9z9w56B1-1HWz8Q3yiCStFAI,8741
|
|
15
|
+
pico_ioc-2.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
16
|
+
pico_ioc-2.0.1.dist-info/top_level.txt,sha256=_7_RLu616z_dtRw16impXn4Mw8IXe2J4BeX5912m5dQ,9
|
|
17
|
+
pico_ioc-2.0.1.dist-info/RECORD,,
|
pico_ioc/_state.py
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
from threading import RLock
|
|
5
|
-
from contextvars import ContextVar
|
|
6
|
-
from contextlib import contextmanager
|
|
7
|
-
from typing import Optional, TYPE_CHECKING
|
|
8
|
-
|
|
9
|
-
# Type-only import to avoid cycles
|
|
10
|
-
if TYPE_CHECKING:
|
|
11
|
-
from .container import PicoContainer
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
# ---- Task/process context for the active container ----
|
|
15
|
-
|
|
16
|
-
@dataclass(frozen=True, slots=True)
|
|
17
|
-
class ContainerContext:
|
|
18
|
-
"""Immutable snapshot for the active container state."""
|
|
19
|
-
container: "PicoContainer"
|
|
20
|
-
fingerprint: tuple
|
|
21
|
-
root_name: Optional[str]
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
# Process-wide fallback (for non-async code) guarded by a lock
|
|
25
|
-
_lock = RLock()
|
|
26
|
-
_current_context: Optional[ContainerContext] = None
|
|
27
|
-
|
|
28
|
-
# Task-local context (for async isolation)
|
|
29
|
-
_ctxvar: ContextVar[Optional[ContainerContext]] = ContextVar("pico_ioc_ctx", default=None)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def get_context() -> Optional[ContainerContext]:
|
|
33
|
-
"""Return the current context (task-local first, then process-global)."""
|
|
34
|
-
ctx = _ctxvar.get()
|
|
35
|
-
return ctx if ctx is not None else _current_context
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def set_context(ctx: Optional[ContainerContext]) -> None:
|
|
39
|
-
"""Atomically set both task-local and process-global context."""
|
|
40
|
-
with _lock:
|
|
41
|
-
_ctxvar.set(ctx)
|
|
42
|
-
globals()["_current_context"] = ctx
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
# Optional compatibility helpers (only used by legacy API paths)
|
|
46
|
-
def get_fingerprint() -> Optional[tuple]:
|
|
47
|
-
ctx = get_context()
|
|
48
|
-
return ctx.fingerprint if ctx else None
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def set_fingerprint(fp: Optional[tuple]) -> None:
|
|
52
|
-
"""Compatibility shim: setting None clears the active context."""
|
|
53
|
-
if fp is None:
|
|
54
|
-
set_context(None)
|
|
55
|
-
return
|
|
56
|
-
ctx = get_context()
|
|
57
|
-
if ctx is not None:
|
|
58
|
-
set_context(ContainerContext(container=ctx.container, fingerprint=fp, root_name=ctx.root_name))
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
# ---- Scan/resolve guards (kept as-is) ----
|
|
62
|
-
|
|
63
|
-
_scanning: ContextVar[bool] = ContextVar("pico_scanning", default=False)
|
|
64
|
-
_resolving: ContextVar[bool] = ContextVar("pico_resolving", default=False)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
@contextmanager
|
|
68
|
-
def scanning_flag():
|
|
69
|
-
"""Mark scanning=True within the block."""
|
|
70
|
-
tok = _scanning.set(True)
|
|
71
|
-
try:
|
|
72
|
-
yield
|
|
73
|
-
finally:
|
|
74
|
-
_scanning.reset(tok)
|
|
75
|
-
|
pico_ioc/builder.py
DELETED
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
import inspect as _inspect
|
|
3
|
-
import logging
|
|
4
|
-
import os
|
|
5
|
-
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple
|
|
6
|
-
from typing import get_origin, get_args, Annotated
|
|
7
|
-
|
|
8
|
-
from .interceptors import MethodInterceptor, ContainerInterceptor
|
|
9
|
-
from .container import PicoContainer, _is_compatible
|
|
10
|
-
from .policy import apply_policy, _conditional_active
|
|
11
|
-
from .plugins import PicoPlugin, run_plugin_hook
|
|
12
|
-
from .scanner import scan_and_configure
|
|
13
|
-
from .resolver import Resolver, _get_hints
|
|
14
|
-
from . import _state
|
|
15
|
-
from .config import ConfigRegistry
|
|
16
|
-
|
|
17
|
-
class PicoContainerBuilder:
|
|
18
|
-
def __init__(self):
|
|
19
|
-
self._scan_plan: List[Tuple[Any, Optional[Callable[[str], bool]], Tuple[PicoPlugin, ...]]] = []
|
|
20
|
-
self._overrides: Dict[Any, Any] = {}
|
|
21
|
-
self._profiles: Optional[List[str]] = None
|
|
22
|
-
self._plugins: Tuple[PicoPlugin, ...] = ()
|
|
23
|
-
self._include_tags: Optional[set[str]] = None
|
|
24
|
-
self._exclude_tags: Optional[set[str]] = None
|
|
25
|
-
self._roots: Iterable[type] = ()
|
|
26
|
-
self._providers: Dict[Any, Dict] = {}
|
|
27
|
-
self._eager: bool = True
|
|
28
|
-
self._config_registry: ConfigRegistry | None = None
|
|
29
|
-
|
|
30
|
-
def with_config(self, registry: ConfigRegistry) -> "PicoContainerBuilder":
|
|
31
|
-
self._config_registry = registry
|
|
32
|
-
return self
|
|
33
|
-
|
|
34
|
-
def with_plugins(self, plugins: Tuple[PicoPlugin, ...]) -> "PicoContainerBuilder":
|
|
35
|
-
self._plugins = plugins or ()
|
|
36
|
-
return self
|
|
37
|
-
|
|
38
|
-
def with_profiles(self, profiles: Optional[List[str]]) -> "PicoContainerBuilder":
|
|
39
|
-
self._profiles = profiles
|
|
40
|
-
return self
|
|
41
|
-
|
|
42
|
-
def add_scan_package(self, package: Any, exclude: Optional[Callable[[str], bool]] = None) -> "PicoContainerBuilder":
|
|
43
|
-
self._scan_plan.append((package, exclude, self._plugins))
|
|
44
|
-
return self
|
|
45
|
-
|
|
46
|
-
def with_overrides(self, overrides: Optional[Dict[Any, Any]]) -> "PicoContainerBuilder":
|
|
47
|
-
self._overrides = overrides or {}
|
|
48
|
-
return self
|
|
49
|
-
|
|
50
|
-
def with_tag_filters(self, include: Optional[set[str]], exclude: Optional[set[str]]) -> "PicoContainerBuilder":
|
|
51
|
-
self._include_tags = include
|
|
52
|
-
self._exclude_tags = exclude
|
|
53
|
-
return self
|
|
54
|
-
|
|
55
|
-
def with_roots(self, roots: Iterable[type]) -> "PicoContainerBuilder":
|
|
56
|
-
self._roots = roots or ()
|
|
57
|
-
return self
|
|
58
|
-
|
|
59
|
-
def with_eager(self, eager: bool) -> "PicoContainerBuilder":
|
|
60
|
-
self._eager = bool(eager)
|
|
61
|
-
return self
|
|
62
|
-
|
|
63
|
-
def build(self) -> PicoContainer:
|
|
64
|
-
requested_profiles = _resolve_profiles(self._profiles)
|
|
65
|
-
container = PicoContainer(providers=self._providers)
|
|
66
|
-
container._active_profiles = tuple(requested_profiles)
|
|
67
|
-
setattr(container, "_config_registry", self._config_registry)
|
|
68
|
-
all_infras: list[tuple[type, dict]] = []
|
|
69
|
-
for pkg, exclude, scan_plugins in self._scan_plan:
|
|
70
|
-
with _state.scanning_flag():
|
|
71
|
-
c, f, infra_decls = scan_and_configure(pkg, container, exclude=exclude, plugins=scan_plugins)
|
|
72
|
-
logging.info("Scanned '%s' (components: %d, factories: %d)", getattr(pkg, "__name__", pkg), c, f)
|
|
73
|
-
all_infras.extend(infra_decls)
|
|
74
|
-
_run_infrastructure(container=container, infra_decls=all_infras, profiles=requested_profiles)
|
|
75
|
-
binder = container.binder()
|
|
76
|
-
if self._overrides:
|
|
77
|
-
_apply_overrides(container, self._overrides)
|
|
78
|
-
run_plugin_hook(self._plugins, "after_bind", container, binder)
|
|
79
|
-
run_plugin_hook(self._plugins, "before_eager", container, binder)
|
|
80
|
-
apply_policy(container, profiles=requested_profiles)
|
|
81
|
-
_filter_by_tags(container, self._include_tags, self._exclude_tags)
|
|
82
|
-
if self._roots:
|
|
83
|
-
_restrict_to_subgraph(container, self._roots, self._overrides)
|
|
84
|
-
run_plugin_hook(self._plugins, "after_ready", container, binder)
|
|
85
|
-
if self._eager:
|
|
86
|
-
container.eager_instantiate_all()
|
|
87
|
-
logging.info("Container configured and ready.")
|
|
88
|
-
return container
|
|
89
|
-
|
|
90
|
-
def _resolve_profiles(profiles: Optional[List[str]]) -> List[str]:
|
|
91
|
-
if profiles is not None:
|
|
92
|
-
return list(profiles)
|
|
93
|
-
env_val = os.getenv("PICO_PROFILE", "")
|
|
94
|
-
return [p.strip() for p in env_val.split(",") if p.strip()]
|
|
95
|
-
|
|
96
|
-
def _as_provider(val):
|
|
97
|
-
if isinstance(val, tuple) and len(val) == 2 and callable(val[0]) and isinstance(val[1], bool):
|
|
98
|
-
return val[0], val[1]
|
|
99
|
-
if callable(val):
|
|
100
|
-
return val, False
|
|
101
|
-
return (lambda v=val: v), False
|
|
102
|
-
|
|
103
|
-
def _apply_overrides(container: PicoContainer, overrides: Dict[Any, Any]) -> None:
|
|
104
|
-
for key, val in overrides.items():
|
|
105
|
-
provider, lazy = _as_provider(val)
|
|
106
|
-
container.bind(key, provider, lazy=lazy)
|
|
107
|
-
|
|
108
|
-
def _filter_by_tags(container: PicoContainer, include_tags: Optional[set[str]], exclude_tags: Optional[set[str]]) -> None:
|
|
109
|
-
if not include_tags and not exclude_tags:
|
|
110
|
-
return
|
|
111
|
-
def _tag_ok(meta: dict) -> bool:
|
|
112
|
-
tags = set(meta.get("tags", ()))
|
|
113
|
-
if include_tags and not tags.intersection(include_tags):
|
|
114
|
-
return False
|
|
115
|
-
if exclude_tags and tags.intersection(exclude_tags):
|
|
116
|
-
return False
|
|
117
|
-
return True
|
|
118
|
-
container._providers = {k: v for k, v in container._providers.items() if _tag_ok(v)}
|
|
119
|
-
|
|
120
|
-
def _compute_allowed_subgraph(container: PicoContainer, roots: Iterable[type]) -> set:
|
|
121
|
-
allowed: set[Any] = set(roots)
|
|
122
|
-
stack = list(roots or ())
|
|
123
|
-
def _add_impls_for_base(base_t):
|
|
124
|
-
for prov_key, meta in container._providers.items():
|
|
125
|
-
cls = prov_key if isinstance(prov_key, type) else None
|
|
126
|
-
if cls is not None and _is_compatible(cls, base_t):
|
|
127
|
-
if prov_key not in allowed:
|
|
128
|
-
allowed.add(prov_key)
|
|
129
|
-
stack.append(prov_key)
|
|
130
|
-
while stack:
|
|
131
|
-
k = stack.pop()
|
|
132
|
-
allowed.add(k)
|
|
133
|
-
if isinstance(k, type):
|
|
134
|
-
_add_impls_for_base(k)
|
|
135
|
-
cls = k if isinstance(k, type) else None
|
|
136
|
-
if cls is None or not container.has(k):
|
|
137
|
-
continue
|
|
138
|
-
try:
|
|
139
|
-
sig = _inspect.signature(cls.__init__)
|
|
140
|
-
hints = _get_hints(cls.__init__, owner_cls=cls)
|
|
141
|
-
except Exception:
|
|
142
|
-
continue
|
|
143
|
-
for pname, param in sig.parameters.items():
|
|
144
|
-
if pname == "self":
|
|
145
|
-
continue
|
|
146
|
-
ann = hints.get(pname, param.annotation)
|
|
147
|
-
origin = get_origin(ann) or ann
|
|
148
|
-
if origin in (list, tuple):
|
|
149
|
-
inner = (get_args(ann) or (object,))[0]
|
|
150
|
-
if get_origin(inner) is Annotated:
|
|
151
|
-
inner = (get_args(inner) or (object,))[0]
|
|
152
|
-
if isinstance(inner, type):
|
|
153
|
-
if inner not in allowed:
|
|
154
|
-
stack.append(inner)
|
|
155
|
-
continue
|
|
156
|
-
if isinstance(ann, type) and ann not in allowed:
|
|
157
|
-
stack.append(ann)
|
|
158
|
-
elif container.has(pname) and pname not in allowed:
|
|
159
|
-
stack.append(pname)
|
|
160
|
-
return allowed
|
|
161
|
-
|
|
162
|
-
def _restrict_to_subgraph(container: PicoContainer, roots: Iterable[type], overrides: Optional[Dict[Any, Any]]) -> None:
|
|
163
|
-
allowed = _compute_allowed_subgraph(container, roots)
|
|
164
|
-
keep_keys: set[Any] = allowed | (set(overrides.keys()) if overrides else set())
|
|
165
|
-
container._providers = {k: v for k, v in container._providers.items() if k in keep_keys}
|
|
166
|
-
|
|
167
|
-
def _run_infrastructure(*, container: PicoContainer, infra_decls: List[tuple[type, dict]], profiles: List[str]) -> None:
|
|
168
|
-
def _active(meta: dict) -> bool:
|
|
169
|
-
profs = tuple(meta.get("profiles", ())) or ()
|
|
170
|
-
if profs and (not profiles or not any(p in profs for p in profiles)):
|
|
171
|
-
return False
|
|
172
|
-
req_env = tuple(meta.get("require_env", ())) or ()
|
|
173
|
-
if req_env:
|
|
174
|
-
import os
|
|
175
|
-
if not all(os.getenv(k) not in (None, "") for k in req_env):
|
|
176
|
-
return False
|
|
177
|
-
pred = meta.get("predicate", None)
|
|
178
|
-
if callable(pred):
|
|
179
|
-
try:
|
|
180
|
-
if not bool(pred()):
|
|
181
|
-
return False
|
|
182
|
-
except Exception:
|
|
183
|
-
return False
|
|
184
|
-
return True
|
|
185
|
-
from .resolver import Resolver
|
|
186
|
-
from .infra import Infra
|
|
187
|
-
resolver = Resolver(container)
|
|
188
|
-
active_infras: List[tuple[int, type]] = []
|
|
189
|
-
for cls, meta in infra_decls:
|
|
190
|
-
if not _active(meta):
|
|
191
|
-
continue
|
|
192
|
-
order = int(meta.get("order", 0))
|
|
193
|
-
active_infras.append((order, cls))
|
|
194
|
-
active_infras.sort(key=lambda t: (t[0], getattr(t[1], "__qualname__", "")))
|
|
195
|
-
for _ord, cls in active_infras:
|
|
196
|
-
try:
|
|
197
|
-
inst = resolver.create_instance(cls)
|
|
198
|
-
except Exception:
|
|
199
|
-
import logging
|
|
200
|
-
logging.exception("Failed to construct infrastructure %r", cls)
|
|
201
|
-
continue
|
|
202
|
-
infra = Infra(container=container, profiles=tuple(profiles))
|
|
203
|
-
fn = getattr(inst, "configure", None)
|
|
204
|
-
if callable(fn):
|
|
205
|
-
try:
|
|
206
|
-
fn(infra)
|
|
207
|
-
except Exception:
|
|
208
|
-
import logging
|
|
209
|
-
logging.exception("Infrastructure configure() failed for %r", cls)
|
|
210
|
-
|