pico-ioc 2.0.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/container.py CHANGED
@@ -13,6 +13,75 @@ from .aop import UnifiedComponentProxy, ContainerObserver
13
13
  KeyT = Union[str, type]
14
14
  _resolve_chain: contextvars.ContextVar[Tuple[KeyT, ...]] = contextvars.ContextVar("pico_resolve_chain", default=())
15
15
 
16
+ class _TracerFrame:
17
+ __slots__ = ("parent_key", "via")
18
+ def __init__(self, parent_key: KeyT, via: str):
19
+ self.parent_key = parent_key
20
+ self.via = via
21
+
22
+ class ResolutionTracer:
23
+ def __init__(self, container: "PicoContainer") -> None:
24
+ self._container = container
25
+ self._stack_var: contextvars.ContextVar[List[_TracerFrame]] = contextvars.ContextVar("pico_tracer_stack", default=[])
26
+ self._edges: Dict[Tuple[KeyT, KeyT], Tuple[str, str]] = {}
27
+
28
+ def enter(self, parent_key: KeyT, via: str) -> contextvars.Token:
29
+ stack = list(self._stack_var.get())
30
+ stack.append(_TracerFrame(parent_key, via))
31
+ return self._stack_var.set(stack)
32
+
33
+ def leave(self, token: contextvars.Token) -> None:
34
+ self._stack_var.reset(token)
35
+
36
+ def override_via(self, new_via: str) -> Optional[str]:
37
+ stack = self._stack_var.get()
38
+ if not stack:
39
+ return None
40
+ prev = stack[-1].via
41
+ stack[-1].via = new_via
42
+ return prev
43
+
44
+ def restore_via(self, previous: Optional[str]) -> None:
45
+ if previous is None:
46
+ return
47
+ stack = self._stack_var.get()
48
+ if not stack:
49
+ return
50
+ stack[-1].via = previous
51
+
52
+ def note_param(self, child_key: KeyT, param_name: str) -> None:
53
+ stack = self._stack_var.get()
54
+ if not stack:
55
+ return
56
+ parent = stack[-1].parent_key
57
+ via = stack[-1].via
58
+ self._edges[(parent, child_key)] = (via, param_name)
59
+
60
+ def describe_cycle(self, chain: Tuple[KeyT, ...], current: KeyT, locator: Optional[ComponentLocator]) -> str:
61
+ def name_of(k: KeyT) -> str:
62
+ return getattr(k, "__name__", str(k))
63
+ def scope_of(k: KeyT) -> str:
64
+ if not locator:
65
+ return "singleton"
66
+ md = locator._metadata.get(k)
67
+ return md.scope if md else "singleton"
68
+ lines: List[str] = []
69
+ lines.append("Circular dependency detected.")
70
+ lines.append("")
71
+ lines.append("Resolution chain:")
72
+ full = tuple(chain) + (current,)
73
+ for idx, k in enumerate(full, 1):
74
+ mark = " ❌" if idx == len(full) else ""
75
+ lines.append(f" {idx}. {name_of(k)} [scope={scope_of(k)}]{mark}")
76
+ if idx < len(full):
77
+ parent = k
78
+ child = full[idx]
79
+ via, param = self._edges.get((parent, child), ("provider", "?"))
80
+ lines.append(f" └─ via {via} param '{param}' → {name_of(child)}")
81
+ lines.append("")
82
+ lines.append("Hint: break the cycle with a @configure setter or use a factory/provider.")
83
+ return "\n".join(lines)
84
+
16
85
  class PicoContainer:
17
86
  _container_id_var: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar("pico_container_id", default=None)
18
87
  _container_registry: Dict[str, "PicoContainer"] = {}
@@ -35,6 +104,7 @@ class PicoContainer:
35
104
  import time as _t
36
105
  self.context = PicoContainer._Ctx(container_id=self.container_id, profiles=profiles, created_at=_t.time())
37
106
  PicoContainer._container_registry[self.container_id] = self
107
+ self._tracer = ResolutionTracer(self)
38
108
 
39
109
  @staticmethod
40
110
  def _generate_container_id() -> str:
@@ -80,11 +150,35 @@ class PicoContainer:
80
150
  cache = self._cache_for(key)
81
151
  return cache.get(key) is not None or self._factory.has(key)
82
152
 
153
+ def _canonical_key(self, key: KeyT) -> KeyT:
154
+ if self._factory.has(key):
155
+ return key
156
+ if isinstance(key, type) and self._locator:
157
+ cands: List[Tuple[bool, Any]] = []
158
+ for k, md in self._locator._metadata.items():
159
+ typ = md.provided_type or md.concrete_class
160
+ if not isinstance(typ, type):
161
+ continue
162
+ try:
163
+ if typ is not key and issubclass(typ, key):
164
+ cands.append((md.primary, k))
165
+ except Exception:
166
+ continue
167
+ if cands:
168
+ prim = [k for is_p, k in cands if is_p]
169
+ return prim[0] if prim else cands[0][1]
170
+ if isinstance(key, str) and self._locator:
171
+ for k, md in self._locator._metadata.items():
172
+ if md.pico_name == key:
173
+ return k
174
+ return key
175
+
83
176
  @overload
84
177
  def get(self, key: type) -> Any: ...
85
178
  @overload
86
179
  def get(self, key: str) -> Any: ...
87
180
  def get(self, key: KeyT) -> Any:
181
+ key = self._canonical_key(key)
88
182
  cache = self._cache_for(key)
89
183
  cached = cache.get(key)
90
184
  if cached is not None:
@@ -96,28 +190,19 @@ class PicoContainer:
96
190
  chain = list(_resolve_chain.get())
97
191
  for k in chain:
98
192
  if k == key:
99
- raise CircularDependencyError(chain, key)
193
+ details = self._tracer.describe_cycle(tuple(chain), key, self._locator)
194
+ raise ComponentCreationError(key, CircularDependencyError(chain, key, details=details))
100
195
  token_chain = _resolve_chain.set(tuple(chain + [key]))
101
196
  token_container = self.activate()
197
+ token_tracer = self._tracer.enter(key, via="provider")
102
198
  try:
103
- if not self._factory.has(key):
104
- alt = None
105
- if isinstance(key, type):
106
- alt = self._resolve_type_key(key)
107
- elif isinstance(key, str) and self._locator:
108
- for k, md in self._locator._metadata.items():
109
- if md.pico_name == key:
110
- alt = k
111
- break
112
- if alt is not None:
113
- self._factory.bind(key, lambda a=alt: self.get(a))
114
199
  provider = self._factory.get(key)
115
200
  try:
116
201
  instance = provider()
117
202
  except ProviderNotFoundError as e:
118
203
  raise
119
204
  except Exception as e:
120
- raise ComponentCreationError(key, e)
205
+ raise ComponentCreationError(key, e) from e
121
206
  instance = self._maybe_wrap_with_aspects(key, instance)
122
207
  cache.put(key, instance)
123
208
  self.context.resolve_count += 1
@@ -125,10 +210,12 @@ class PicoContainer:
125
210
  for o in self._observers: o.on_resolve(key, took_ms)
126
211
  return instance
127
212
  finally:
213
+ self._tracer.leave(token_tracer)
128
214
  _resolve_chain.reset(token_chain)
129
215
  self.deactivate(token_container)
130
216
 
131
217
  async def aget(self, key: KeyT) -> Any:
218
+ key = self._canonical_key(key)
132
219
  cache = self._cache_for(key)
133
220
  cached = cache.get(key)
134
221
  if cached is not None:
@@ -140,21 +227,12 @@ class PicoContainer:
140
227
  chain = list(_resolve_chain.get())
141
228
  for k in chain:
142
229
  if k == key:
143
- raise CircularDependencyError(chain, key)
230
+ details = self._tracer.describe_cycle(tuple(chain), key, self._locator)
231
+ raise ComponentCreationError(key, CircularDependencyError(chain, key, details=details))
144
232
  token_chain = _resolve_chain.set(tuple(chain + [key]))
145
233
  token_container = self.activate()
234
+ token_tracer = self._tracer.enter(key, via="provider")
146
235
  try:
147
- if not self._factory.has(key):
148
- alt = None
149
- if isinstance(key, type):
150
- alt = self._resolve_type_key(key)
151
- elif isinstance(key, str) and self._locator:
152
- for k, md in self._locator._metadata.items():
153
- if md.pico_name == key:
154
- alt = k
155
- break
156
- if alt is not None:
157
- self._factory.bind(key, lambda a=alt: self.get(a))
158
236
  provider = self._factory.get(key)
159
237
  try:
160
238
  instance = provider()
@@ -163,7 +241,7 @@ class PicoContainer:
163
241
  except ProviderNotFoundError as e:
164
242
  raise
165
243
  except Exception as e:
166
- raise ComponentCreationError(key, e)
244
+ raise ComponentCreationError(key, e) from e
167
245
  instance = self._maybe_wrap_with_aspects(key, instance)
168
246
  cache.put(key, instance)
169
247
  self.context.resolve_count += 1
@@ -171,6 +249,7 @@ class PicoContainer:
171
249
  for o in self._observers: o.on_resolve(key, took_ms)
172
250
  return instance
173
251
  finally:
252
+ self._tracer.leave(token_tracer)
174
253
  _resolve_chain.reset(token_chain)
175
254
  self.deactivate(token_container)
176
255
 
@@ -303,3 +382,59 @@ class PicoContainer:
303
382
  self.cleanup_all()
304
383
  PicoContainer._container_registry.pop(self.container_id, None)
305
384
 
385
+ def export_graph(
386
+ self,
387
+ path: str,
388
+ *,
389
+ include_scopes: bool = True,
390
+ include_qualifiers: bool = False,
391
+ rankdir: str = "LR",
392
+ title: Optional[str] = None,
393
+ ) -> None:
394
+
395
+ if not self._locator:
396
+ raise RuntimeError("No locator attached; cannot export dependency graph.")
397
+
398
+ from .api import _build_resolution_graph
399
+
400
+ md_by_key = self._locator._metadata
401
+ graph = _build_resolution_graph(self)
402
+
403
+ lines: List[str] = []
404
+ lines.append("digraph Pico {")
405
+ lines.append(f' rankdir="{rankdir}";')
406
+ lines.append(" node [shape=box, fontsize=10];")
407
+ if title:
408
+ lines.append(f' labelloc="t";')
409
+ lines.append(f' label="{title}";')
410
+
411
+ def _node_id(k: KeyT) -> str:
412
+ return f'n_{abs(hash(k))}'
413
+
414
+ def _node_label(k: KeyT) -> str:
415
+ name = getattr(k, "__name__", str(k))
416
+ md = md_by_key.get(k)
417
+ parts = [name]
418
+ if md is not None and include_scopes:
419
+ parts.append(f"[scope={md.scope}]")
420
+ if md is not None and include_qualifiers and md.qualifiers:
421
+ q = ",".join(sorted(md.qualifiers))
422
+ parts.append(f"\\n⟨{q}⟩")
423
+ return "\\n".join(parts)
424
+
425
+ for key in md_by_key.keys():
426
+ nid = _node_id(key)
427
+ label = _node_label(key)
428
+ lines.append(f' {nid} [label="{label}"];')
429
+
430
+ for parent, deps in graph.items():
431
+ pid = _node_id(parent)
432
+ for child in deps:
433
+ cid = _node_id(child)
434
+ lines.append(f" {pid} -> {cid};")
435
+
436
+ lines.append("}")
437
+
438
+ with open(path, "w", encoding="utf-8") as f:
439
+ f.write("\n".join(lines))
440
+
pico_ioc/event_bus.py CHANGED
@@ -6,7 +6,7 @@ import threading
6
6
  from dataclasses import dataclass, field
7
7
  from enum import Enum, auto
8
8
  from typing import Any, Awaitable, Callable, Dict, Iterable, List, Optional, Tuple, Type
9
- from .api import factory, provides, configure, cleanup, primary
9
+ from .api import factory, provides, configure, cleanup
10
10
  from .exceptions import EventBusClosedError, EventBusError, EventBusQueueFullError, EventBusHandlerError
11
11
 
12
12
  log = logging.getLogger(__name__)
@@ -204,10 +204,9 @@ class AutoSubscriberMixin:
204
204
  for evt_t, pr, pol, once in subs:
205
205
  event_bus.subscribe(evt_t, attr, priority=pr, policy=pol, once=once)
206
206
 
207
- @factory
208
- @primary
207
+ @factory()
209
208
  class PicoEventBusProvider:
210
- @provides(EventBus)
209
+ @provides(EventBus, primary=True)
211
210
  def build(self) -> EventBus:
212
211
  return EventBus()
213
212
  @cleanup
pico_ioc/exceptions.py CHANGED
@@ -1,4 +1,3 @@
1
- # src/pico_ioc/exceptions.py
2
1
  from typing import Any, Iterable
3
2
 
4
3
  class PicoError(Exception):
@@ -10,12 +9,19 @@ class ProviderNotFoundError(PicoError):
10
9
  self.key = key
11
10
 
12
11
  class CircularDependencyError(PicoError):
13
- def __init__(self, chain: Iterable[Any], current: Any):
12
+ def __init__(self, chain: Iterable[Any], current: Any, details: str | None = None, hint: str | None = None):
14
13
  chain_str = " -> ".join(getattr(k, "__name__", str(k)) for k in chain)
15
14
  cur_str = getattr(current, "__name__", str(current))
16
- super().__init__(f"Circular dependency detected: {chain_str} -> {cur_str}")
15
+ base = f"Circular dependency detected: {chain_str} -> {cur_str}"
16
+ if details:
17
+ base += f"\n\n{details}"
18
+ if hint:
19
+ base += f"\n\nHint: {hint}"
20
+ super().__init__(base)
17
21
  self.chain = tuple(chain)
18
22
  self.current = current
23
+ self.details = details
24
+ self.hint = hint
19
25
 
20
26
  class ComponentCreationError(PicoError):
21
27
  def __init__(self, key: Any, cause: Exception):
pico_ioc/scope.py CHANGED
@@ -1,4 +1,6 @@
1
+ # src/pico_ioc/scope.py
1
2
  import contextvars
3
+ import inspect
2
4
  from typing import Any, Dict, Optional, Tuple
3
5
  from collections import OrderedDict
4
6
 
@@ -87,6 +89,35 @@ class ScopedCaches:
87
89
  self._by_scope: Dict[str, OrderedDict[Any, ComponentContainer]] = {}
88
90
  self._max = int(max_scopes_per_type)
89
91
  self._no_cache = _NoCacheContainer()
92
+ def _cleanup_object(self, obj: Any) -> None:
93
+ try:
94
+ from .constants import PICO_META
95
+ except Exception:
96
+ PICO_META = "_pico_meta"
97
+ try:
98
+ for _, m in inspect.getmembers(obj, predicate=inspect.ismethod):
99
+ meta = getattr(m, PICO_META, {})
100
+ if meta.get("cleanup", False):
101
+ try:
102
+ m()
103
+ except Exception:
104
+ pass
105
+ except Exception:
106
+ pass
107
+
108
+ def cleanup_scope(self, scope_name: str, scope_id: Any) -> None:
109
+ bucket = self._by_scope.get(scope_name)
110
+ if bucket and scope_id in bucket:
111
+ container = bucket.pop(scope_id)
112
+ self._cleanup_container(container)
113
+
114
+ def _cleanup_container(self, container: "ComponentContainer") -> None:
115
+ try:
116
+ for _, obj in container.items():
117
+ self._cleanup_object(obj)
118
+ except Exception:
119
+ pass
120
+
90
121
  def for_scope(self, scopes: ScopeManager, scope: str) -> ComponentContainer:
91
122
  if scope == "singleton":
92
123
  return self._singleton
@@ -99,14 +130,28 @@ class ScopedCaches:
99
130
  bucket[sid] = c
100
131
  return c
101
132
  if len(bucket) >= self._max:
102
- bucket.popitem(last=False)
133
+ _, old = bucket.popitem(last=False)
134
+ self._cleanup_container(old)
103
135
  c = ComponentContainer()
104
136
  bucket[sid] = c
105
137
  return c
138
+
106
139
  def all_items(self):
107
140
  for item in self._singleton.items():
108
141
  yield item
109
142
  for b in self._by_scope.values():
110
143
  for c in b.values():
111
144
  for item in c.items():
112
- yield item
145
+ yield item
146
+
147
+ def shrink(self, scope: str, keep: int) -> None:
148
+ if scope in ("singleton", "prototype"):
149
+ return
150
+ bucket = self._by_scope.get(scope)
151
+ if not bucket:
152
+ return
153
+ k = max(0, int(keep))
154
+ while len(bucket) > k:
155
+ _, old = bucket.popitem(last=False)
156
+ self._cleanup_container(old)
157
+
@@ -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
+ [![PyPI](https://img.shields.io/pypi/v/pico-ioc.svg)](https://pypi.org/project/pico-ioc/)
54
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/dperezcabrera/pico-ioc)
55
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
56
+ ![CI (tox matrix)](https://github.com/dperezcabrera/pico-ioc/actions/workflows/ci.yml/badge.svg)
57
+ [![codecov](https://codecov.io/gh/dperezcabrera/pico-ioc/branch/main/graph/badge.svg)](https://codecov.io/gh/dperezcabrera/pico-ioc)
58
+ [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=dperezcabrera_pico-ioc&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=dperezcabrera_pico-ioc)
59
+ [![Duplicated Lines (%)](https://sonarcloud.io/api/project_badges/measure?project=dperezcabrera_pico-ioc&metric=duplicated_lines_density)](https://sonarcloud.io/summary/new_code?id=dperezcabrera_pico-ioc)
60
+ [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=dperezcabrera_pico-ioc&metric=sqale_rating)](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,,