pico-ioc 0.4.0__py3-none-any.whl → 0.5.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 CHANGED
@@ -186,18 +186,43 @@ def resolve_param(container: PicoContainer, p: inspect.Parameter) -> Any:
186
186
  return container.get(key)
187
187
 
188
188
  def create_instance(cls: type, container: PicoContainer) -> Any:
189
+ """
190
+ Instantiate `cls` with constructor DI from `container`.
191
+
192
+ Resolution rules:
193
+ - Skip `self`, *args, **kwargs.
194
+ - Try resolve_param(...) for each parameter.
195
+ - If resolve_param raises NameError AND the parameter has a default,
196
+ skip injecting it so Python uses the default value.
197
+ - Otherwise, propagate the error.
198
+
199
+ This preserves the "name > type > MRO > str(name)" precedence in resolve_param,
200
+ while making defaulted, annotated parameters truly optional.
201
+ """
189
202
  sig = inspect.signature(cls.__init__)
190
- deps = {}
203
+ deps: Dict[str, Any] = {}
204
+
191
205
  tok = _resolving.set(True)
192
206
  try:
193
207
  for p in sig.parameters.values():
194
- if p.name == 'self' or p.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
208
+ if p.name == 'self' or p.kind in (
209
+ inspect.Parameter.VAR_POSITIONAL,
210
+ inspect.Parameter.VAR_KEYWORD,
211
+ ):
195
212
  continue
196
- deps[p.name] = resolve_param(container, p)
213
+
214
+ try:
215
+ deps[p.name] = resolve_param(container, p)
216
+ except NameError:
217
+ if p.default is not inspect._empty:
218
+ continue
219
+ raise
197
220
  finally:
198
221
  _resolving.reset(tok)
222
+
199
223
  return cls(**deps)
200
224
 
225
+
201
226
  @runtime_checkable
202
227
  class PicoPlugin(Protocol):
203
228
  def before_scan(self, package: Any, binder: Binder) -> None: ...
@@ -217,12 +242,14 @@ def _scan_and_configure(
217
242
  package = importlib.import_module(package_or_name) if isinstance(package_or_name, str) else package_or_name
218
243
  logging.info(f"Scanning in '{package.__name__}'...")
219
244
  binder = Binder(container)
245
+
220
246
  for pl in plugins:
221
247
  try:
222
248
  if hasattr(pl, "before_scan"):
223
249
  pl.before_scan(package, binder)
224
250
  except Exception:
225
251
  logging.exception("Plugin before_scan failed")
252
+
226
253
  component_classes, factory_classes = [], []
227
254
  for _, name, _ in pkgutil.walk_packages(package.__path__, package.__name__ + '.'):
228
255
  if exclude and exclude(name):
@@ -243,40 +270,84 @@ def _scan_and_configure(
243
270
  factory_classes.append(obj)
244
271
  except Exception as e:
245
272
  logging.warning(f"Module {name} not processed: {e}")
273
+
246
274
  for pl in plugins:
247
275
  try:
248
276
  if hasattr(pl, "after_scan"):
249
277
  pl.after_scan(package, binder)
250
278
  except Exception:
251
279
  logging.exception("Plugin after_scan failed")
252
- for factory_cls in factory_classes:
253
- try:
254
- sig = inspect.signature(factory_cls.__init__)
255
- instance = factory_cls(container) if 'container' in sig.parameters else factory_cls()
256
- for _, method in inspect.getmembers(instance, inspect.ismethod):
257
- if hasattr(method, '_provides_name'):
258
- key = getattr(method, '_provides_name')
259
- is_lazy = bool(getattr(method, '_pico_lazy', False))
260
- def make_provider(m=method, lazy=is_lazy):
261
- def _factory():
262
- if lazy:
263
- return ComponentProxy(lambda: m())
264
- return m()
265
- return _factory
266
- container.bind(key, make_provider(), lazy=is_lazy)
267
- except Exception:
268
- logging.exception(f"Error in factory {factory_cls.__name__}")
280
+
269
281
  for component_cls in component_classes:
270
282
  key = getattr(component_cls, '_component_key', component_cls)
271
283
  is_lazy = bool(getattr(component_cls, '_component_lazy', False))
284
+
272
285
  def provider_factory(lazy=is_lazy, cls=component_cls):
273
286
  def _factory():
274
287
  if lazy:
275
288
  return ComponentProxy(lambda: create_instance(cls, container))
276
289
  return create_instance(cls, container)
277
290
  return _factory
291
+
278
292
  container.bind(key, provider_factory(), lazy=is_lazy)
279
293
 
294
+
295
+ def _resolve_method_kwargs(meth) -> Dict[str, Any]:
296
+ sig = inspect.signature(meth)
297
+ deps = {}
298
+ tok = _resolving.set(True)
299
+ try:
300
+ for p in sig.parameters.values():
301
+ if p.name == 'self' or p.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
302
+ continue
303
+ try:
304
+ deps[p.name] = resolve_param(container, p)
305
+ except NameError:
306
+ if p.default is not inspect._empty:
307
+ continue
308
+ raise
309
+ finally:
310
+ _resolving.reset(tok)
311
+ return deps
312
+
313
+ for factory_cls in factory_classes:
314
+ try:
315
+ factory_instance = create_instance(factory_cls, container)
316
+
317
+ for name, func in inspect.getmembers(factory_cls, predicate=inspect.isfunction):
318
+ provides_key = getattr(func, '_provides_name', None)
319
+ if provides_key is None:
320
+ continue
321
+
322
+ is_lazy = bool(getattr(func, '_pico_lazy', False))
323
+
324
+ bound_method = getattr(factory_instance, name, None)
325
+ if bound_method is None:
326
+ bound_method = func.__get__(factory_instance, factory_cls)
327
+
328
+ fn_meta_src = getattr(bound_method, '__func__', bound_method)
329
+ if getattr(fn_meta_src, '_provides_name', None) is not None:
330
+ provides_key = getattr(fn_meta_src, '_provides_name')
331
+ is_lazy = bool(getattr(fn_meta_src, '_pico_lazy', is_lazy))
332
+
333
+ def make_provider(m=bound_method, lazy=is_lazy):
334
+ def _factory():
335
+ kwargs = _resolve_method_kwargs(m)
336
+
337
+ def _call():
338
+ return m(**kwargs)
339
+
340
+ if lazy:
341
+ return ComponentProxy(lambda: _call())
342
+ return _call()
343
+ return _factory
344
+
345
+ container.bind(provides_key, make_provider(), lazy=is_lazy)
346
+ except Exception:
347
+ logging.exception(f"Error in factory {factory_cls.__name__}")
348
+
349
+
350
+
280
351
  _container: Optional[PicoContainer] = None
281
352
 
282
353
  def init(
@@ -335,29 +406,3 @@ def init(
335
406
  logging.info("Container configured and ready.")
336
407
  return _container
337
408
 
338
-
339
-
340
- def factory_component(cls):
341
- setattr(cls, '_is_factory_component', True)
342
- return cls
343
-
344
-
345
- def provides(key: Any, *, lazy: bool = False):
346
- def decorator(func):
347
- @functools.wraps(func)
348
- def wrapper(*args, **kwargs):
349
- return func(*args, **kwargs)
350
- setattr(wrapper, '_provides_name', key)
351
- setattr(wrapper, '_pico_lazy', bool(lazy))
352
- return wrapper
353
- return decorator
354
-
355
-
356
- def component(cls=None, *, name: Any = None, lazy: bool = False):
357
- def decorator(c):
358
- setattr(c, '_is_component', True)
359
- setattr(c, '_component_key', name if name is not None else c)
360
- setattr(c, '_component_lazy', bool(lazy))
361
- return c
362
- return decorator(cls) if cls else decorator
363
-
pico_ioc/_version.py CHANGED
@@ -1 +1 @@
1
- __version__ = '0.4.0'
1
+ __version__ = '0.5.1'
@@ -0,0 +1,284 @@
1
+ Metadata-Version: 2.4
2
+ Name: pico-ioc
3
+ Version: 0.5.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.8
36
+ Classifier: Programming Language :: Python :: 3.9
37
+ Classifier: Programming Language :: Python :: 3.10
38
+ Classifier: Programming Language :: Python :: 3.11
39
+ Classifier: Programming Language :: Python :: 3.12
40
+ Classifier: Programming Language :: Python :: 3.13
41
+ Classifier: License :: OSI Approved :: MIT License
42
+ Classifier: Operating System :: OS Independent
43
+ Requires-Python: >=3.8
44
+ Description-Content-Type: text/markdown
45
+ License-File: LICENSE
46
+ Dynamic: license-file
47
+
48
+ Got it ✅
49
+ Here’s your **updated README.md in full English**, keeping all original sections but now including the **name-first resolution** feature and new tests section.
50
+
51
+ ---
52
+
53
+ ````markdown
54
+ # 📦 Pico-IoC: A Minimalist IoC Container for Python
55
+
56
+ [![PyPI](https://img.shields.io/pypi/v/pico-ioc.svg)](https://pypi.org/project/pico-ioc/)
57
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
58
+ ![CI (tox matrix)](https://github.com/dperezcabrera/pico-ioc/actions/workflows/ci.yml/badge.svg)
59
+ [![codecov](https://codecov.io/gh/dperezcabrera/pico-ioc/branch/main/graph/badge.svg)](https://codecov.io/gh/dperezcabrera/pico-ioc)
60
+ [![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)
61
+ [![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)
62
+ [![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)
63
+
64
+ **Pico-IoC** is a tiny, zero-dependency, decorator-based Inversion of Control container for Python.
65
+ Build loosely-coupled, testable apps without manual wiring. Inspired by the Spring ecosystem.
66
+
67
+ ---
68
+
69
+ ## ✨ Key Features
70
+
71
+ * **Zero dependencies** — pure Python.
72
+ * **Decorator API** — `@component`, `@factory_component`, `@provides`.
73
+ * **Auto discovery** — scans a package and registers components.
74
+ * **Eager by default, fail-fast** — non-lazy bindings are instantiated immediately after `init()`. Missing deps fail startup.
75
+ * **Opt-in lazy** — set `lazy=True` to defer creation (wrapped in `ComponentProxy`).
76
+ * **Factories** — encapsulate complex creation logic.
77
+ * **Smart resolution order** — **parameter name** takes precedence over **type annotation**, then **MRO fallback**, then **string(name)**.
78
+ * **Re-entrancy guard** — prevents `get()` during scanning.
79
+ * **Auto-exclude caller** — `init()` skips the calling module to avoid double scanning.
80
+
81
+ ---
82
+
83
+ ## 📦 Installation
84
+
85
+ ```bash
86
+ pip install pico-ioc
87
+ ````
88
+
89
+ ---
90
+
91
+ ## 🚀 Quick Start
92
+
93
+ ```python
94
+ from pico_ioc import component, init
95
+
96
+ @component
97
+ class AppConfig:
98
+ def get_db_url(self):
99
+ return "postgresql://user:pass@host/db"
100
+
101
+ @component
102
+ class DatabaseService:
103
+ def __init__(self, config: AppConfig):
104
+ self._cs = config.get_db_url()
105
+ def get_data(self):
106
+ return f"Data from {self._cs}"
107
+
108
+ container = init(__name__) # blueprint runs here (eager + fail-fast)
109
+ db = container.get(DatabaseService)
110
+ print(db.get_data())
111
+ ```
112
+
113
+ ---
114
+
115
+ ## 🧩 Custom Component Keys
116
+
117
+ ```python
118
+ from pico_ioc import component, init
119
+
120
+ @component(name="config") # custom key
121
+ class AppConfig:
122
+ db_url = "postgresql://user:pass@localhost/db"
123
+
124
+ @component
125
+ class Repository:
126
+ def __init__(self, config: "config"): # resolve by NAME
127
+ self.url = config.db_url
128
+
129
+ container = init(__name__)
130
+ print(container.get("config").db_url)
131
+ ```
132
+
133
+ ---
134
+
135
+ ## 🏭 Factories and `@provides`
136
+
137
+ * Default is **eager** (`lazy=False`). Eager bindings are constructed at the end of `init()`.
138
+ * Use `lazy=True` for on-first-use creation via `ComponentProxy`.
139
+
140
+ ```python
141
+ from pico_ioc import factory_component, provides, init
142
+
143
+ COUNTER = {"value": 0}
144
+
145
+ @factory_component
146
+ class ServicesFactory:
147
+ @provides(key="heavy_service", lazy=True)
148
+ def heavy(self):
149
+ COUNTER["value"] += 1
150
+ return {"payload": "hello"}
151
+
152
+ container = init(__name__)
153
+ svc = container.get("heavy_service") # not created yet
154
+ print(COUNTER["value"]) # 0
155
+ print(svc["payload"]) # triggers creation
156
+ print(COUNTER["value"]) # 1
157
+ ```
158
+
159
+ ---
160
+
161
+ ## 🧠 Dependency Resolution Order (Updated in v0.5.0)
162
+
163
+ Starting with **v0.5.0**, Pico-IoC enforces **name-first resolution**:
164
+
165
+ 1. **Parameter name** (highest priority)
166
+ 2. **Exact type annotation**
167
+ 3. **MRO fallback** (walk base classes)
168
+ 4. **String(name)**
169
+
170
+ This means that if a dependency could match both by name and type, **the name match wins**.
171
+
172
+ Example:
173
+
174
+ ```python
175
+ from pico_ioc import component, factory_component, provides, init
176
+
177
+ class BaseType: ...
178
+ class Impl(BaseType): ...
179
+
180
+ @component(name="inject_by_name")
181
+ class InjectByName:
182
+ def __init__(self):
183
+ self.value = "by-name"
184
+
185
+ @factory_component
186
+ class NameVsTypeFactory:
187
+ @provides("choose", lazy=True)
188
+ def make(self, inject_by_name, hint: BaseType = None):
189
+ return inject_by_name.value
190
+
191
+ container = init(__name__)
192
+ assert container.get("choose") == "by-name"
193
+ ```
194
+
195
+ ---
196
+
197
+ ## ⚡ Eager vs. Lazy (Blueprint Behavior)
198
+
199
+ At the end of `init()`, Pico-IoC performs a **blueprint**:
200
+
201
+ * **Eager** (`lazy=False`, default): instantiated immediately; failures stop startup.
202
+ * **Lazy** (`lazy=True`): returns a `ComponentProxy`; instantiated on first real use.
203
+
204
+ **Lifecycle:**
205
+
206
+ ```
207
+ ┌───────────────────────┐
208
+ │ init() │
209
+ └───────────────────────┘
210
+
211
+
212
+ ┌───────────────────────┐
213
+ │ Scan & bind deps │
214
+ └───────────────────────┘
215
+
216
+
217
+ ┌─────────────────────────────┐
218
+ │ Blueprint instantiates all │
219
+ │ non-lazy (eager) beans │
220
+ └─────────────────────────────┘
221
+
222
+ ┌───────────────────────┐
223
+ │ Container ready │
224
+ └───────────────────────┘
225
+ ```
226
+
227
+ ---
228
+
229
+ ## 🛠 API Reference
230
+
231
+ ### `init(root, *, exclude=None, auto_exclude_caller=True) -> PicoContainer`
232
+
233
+ Scan and bind components in `root` (str module name or module).
234
+ Skips the calling module if `auto_exclude_caller=True`.
235
+ Runs blueprint (instantiate all `lazy=False` bindings).
236
+
237
+ ### `@component(cls=None, *, name=None, lazy=False)`
238
+
239
+ Register a class as a component.
240
+ Use `name` for a custom key.
241
+ Set `lazy=True` to defer creation.
242
+
243
+ ### `@factory_component`
244
+
245
+ Mark a class as a component factory (its methods can `@provides` bindings).
246
+
247
+ ### `@provides(key, *, lazy=False)`
248
+
249
+ Declare that a factory method provides a component under `key`.
250
+ Set `lazy=True` for deferred creation (`ComponentProxy`).
251
+
252
+ ---
253
+
254
+ ## 🧪 Testing
255
+
256
+ ```bash
257
+ pip install tox
258
+ tox
259
+ ```
260
+
261
+ **New in v0.5.0:**
262
+ Additional tests verify:
263
+
264
+ * Name vs. type precedence.
265
+ * Mixed binding key resolution in factories.
266
+ * Eager vs. lazy instantiation edge cases.
267
+
268
+ ---
269
+
270
+ ## 🔌 Extensibility: Plugins, Binder, and Lifecycle Hooks
271
+
272
+ From `v0.4.0` onward, Pico-IoC can be cleanly extended without patching the core.
273
+
274
+ *(plugin API docs unchanged from before)*
275
+
276
+ ---
277
+
278
+ ## 📜 License
279
+
280
+ MIT — see [LICENSE](https://opensource.org/licenses/MIT)
281
+
282
+ ```
283
+
284
+
@@ -0,0 +1,7 @@
1
+ pico_ioc/__init__.py,sha256=zWSJSAIgpZ_bXNi_6s88Lmql181ECyIy_Yh7hc5SOd8,16487
2
+ pico_ioc/_version.py,sha256=s8Yq9Om1yBxrMA7xYQ5Y13Paeuxnq99NxhyjuPlnH6A,22
3
+ pico_ioc-0.5.1.dist-info/licenses/LICENSE,sha256=N1_nOvHTM6BobYnOTNXiQkroDqCEi6EzfGBv8lWtyZ0,1077
4
+ pico_ioc-0.5.1.dist-info/METADATA,sha256=dgeFxNxy4tCDXNWgUqirSbjK80CTAlhsNWrumI5z47A,9623
5
+ pico_ioc-0.5.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
6
+ pico_ioc-0.5.1.dist-info/top_level.txt,sha256=_7_RLu616z_dtRw16impXn4Mw8IXe2J4BeX5912m5dQ,9
7
+ pico_ioc-0.5.1.dist-info/RECORD,,
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 David Pérez Cabrera
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1,321 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: pico-ioc
3
- Version: 0.4.0
4
- Summary: A minimalist, zero-dependency Inversion of Control (IoC) container for Python.
5
- Author-email: David Perez Cabrera <dperezcabrera@gmail.com>
6
- Project-URL: Homepage, https://github.com/dperezcabrera/pico-ioc
7
- Project-URL: Repository, https://github.com/dperezcabrera/pico-ioc
8
- Project-URL: Issue Tracker, https://github.com/dperezcabrera/pico-ioc/issues
9
- Keywords: ioc,di,dependency injection,inversion of control,decorator
10
- Classifier: Development Status :: 4 - Beta
11
- Classifier: Programming Language :: Python :: 3
12
- Classifier: Programming Language :: Python :: 3 :: Only
13
- Classifier: Programming Language :: Python :: 3.8
14
- Classifier: Programming Language :: Python :: 3.9
15
- Classifier: Programming Language :: Python :: 3.10
16
- Classifier: Programming Language :: Python :: 3.11
17
- Classifier: Programming Language :: Python :: 3.12
18
- Classifier: Programming Language :: Python :: 3.13
19
- Classifier: License :: OSI Approved :: MIT License
20
- Classifier: Operating System :: OS Independent
21
- Requires-Python: >=3.8
22
- Description-Content-Type: text/markdown
23
-
24
- # 📦 Pico-IoC: A Minimalist IoC Container for Python
25
-
26
- [![PyPI](https://img.shields.io/pypi/v/pico-ioc.svg)](https://pypi.org/project/pico-ioc/)
27
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
28
- ![CI (tox matrix)](https://github.com/dperezcabrera/pico-ioc/actions/workflows/ci.yml/badge.svg)
29
-
30
- **Pico-IoC** is a tiny, zero-dependency, decorator-based Inversion of Control container for Python.
31
- Build loosely-coupled, testable apps without manual wiring. Inspired by the Spring ecosystem.
32
-
33
- ---
34
-
35
- ## ✨ Key Features
36
-
37
- * **Zero dependencies** — pure Python.
38
- * **Decorator API** — `@component`, `@factory_component`, `@provides`.
39
- * **Auto discovery** — scans a package and registers components.
40
- * **Eager by default, fail-fast** — non-lazy bindings are instantiated immediately after `init()`. Missing deps fail startup.
41
- * **Opt-in lazy** — set `lazy=True` to defer creation (wrapped in `ComponentProxy`).
42
- * **Factories** — encapsulate complex creation logic.
43
- * **Smart resolution** — by **parameter name**, then **type annotation**, then **MRO fallback**, then **string(name)**.
44
- * **Re-entrancy guard** — prevents `get()` during scanning.
45
- * **Auto-exclude caller** — `init()` skips the calling module to avoid double scanning.
46
-
47
- ---
48
-
49
- ## 📦 Installation
50
-
51
- ```bash
52
- pip install pico-ioc
53
- ```
54
-
55
- ---
56
-
57
- ## 🚀 Quick Start
58
-
59
- ```python
60
- from pico_ioc import component, init
61
-
62
- @component
63
- class AppConfig:
64
- def get_db_url(self):
65
- return "postgresql://user:pass@host/db"
66
-
67
- @component
68
- class DatabaseService:
69
- def __init__(self, config: AppConfig):
70
- self._cs = config.get_db_url()
71
- def get_data(self):
72
- return f"Data from {self._cs}"
73
-
74
- container = init(__name__) # blueprint runs here (eager + fail-fast)
75
- db = container.get(DatabaseService)
76
- print(db.get_data())
77
- ```
78
-
79
- ---
80
-
81
- ## 🧩 Custom Component Keys
82
-
83
- ```python
84
- from pico_ioc import component, init
85
-
86
- @component(name="config") # custom key
87
- class AppConfig:
88
- db_url = "postgresql://user:pass@localhost/db"
89
-
90
- @component
91
- class Repository:
92
- def __init__(self, config: "config"): # resolve by NAME
93
- self.url = config.db_url
94
-
95
- container = init(__name__)
96
- print(container.get("config").db_url)
97
- ```
98
-
99
- ---
100
-
101
- ## 🏭 Factories and `@provides`
102
-
103
- * Default is **eager** (`lazy=False`). Eager bindings are constructed at the end of `init()`.
104
- * Use `lazy=True` for on-first-use creation via `ComponentProxy`.
105
-
106
- ```python
107
- from pico_ioc import factory_component, provides, init
108
-
109
- COUNTER = {"value": 0}
110
-
111
- @factory_component
112
- class ServicesFactory:
113
- @provides(key="heavy_service", lazy=True)
114
- def heavy(self):
115
- COUNTER["value"] += 1
116
- return {"payload": "hello"}
117
-
118
- container = init(__name__)
119
- svc = container.get("heavy_service") # not created yet
120
- print(COUNTER["value"]) # 0
121
- print(svc["payload"]) # triggers creation
122
- print(COUNTER["value"]) # 1
123
- ```
124
-
125
- ---
126
-
127
- ## 🧠 Dependency Resolution Order
128
-
129
- 1. parameter **name**
130
- 2. exact **type annotation**
131
- 3. **MRO fallback** (walk base classes)
132
- 4. `str(name)`
133
-
134
- ---
135
-
136
- ## ⚡ Eager vs. Lazy (Blueprint Behavior)
137
-
138
- At the end of `init()`, Pico-IoC performs a **blueprint**:
139
-
140
- - **Eager** (`lazy=False`, default): instantiated immediately; failures stop startup.
141
- - **Lazy** (`lazy=True`): returns a `ComponentProxy`; instantiated on first real use.
142
-
143
- **Lifecycle:**
144
-
145
- ┌───────────────────────┐
146
- │ init() │
147
- └───────────────────────┘
148
-
149
-
150
- ┌───────────────────────┐
151
- │ Scan & bind deps │
152
- └───────────────────────┘
153
-
154
-
155
- ┌─────────────────────────────┐
156
- │ Blueprint instantiates all │
157
- │ non-lazy (eager) beans │
158
- └─────────────────────────────┘
159
-
160
- ┌───────────────────────┐
161
- │ Container ready │
162
- └───────────────────────┘
163
-
164
-
165
- **Best practice:** keep eager+fail-fast for production parity with Spring; use lazy only for heavy/optional deps or to support negative tests.
166
-
167
- ---
168
-
169
- ## 🔄 Migration Guide (v0.2.1 → v0.3.0)
170
-
171
- * **Defaults changed:** `@component` and `@provides` now default to `lazy=False` (eager).
172
- * **Proxy renamed:** `LazyProxy` → `ComponentProxy` (only relevant if referenced directly).
173
- * **Tests/fixtures:** components intentionally missing deps should be marked `@component(lazy=True)` (to avoid failing `init()`), or excluded from the scan.
174
-
175
- Example fix for an intentional failure case:
176
-
177
- ```python
178
- @component(lazy=True)
179
- class MissingDep:
180
- def __init__(self, missing):
181
- self.missing = missing
182
- ```
183
-
184
- ---
185
-
186
- ## 🛠 API Reference
187
-
188
- ### `init(root, *, exclude=None, auto_exclude_caller=True) -> PicoContainer`
189
-
190
- Scan and bind components in `root` (str module name or module).
191
- Skips the calling module if `auto_exclude_caller=True`.
192
- Runs blueprint (instantiate all `lazy=False` bindings).
193
-
194
- ### `@component(cls=None, *, name=None, lazy=False)`
195
-
196
- Register a class as a component.
197
- Use `name` for a custom key.
198
- Set `lazy=True` to defer creation.
199
-
200
- ### `@factory_component`
201
-
202
- Mark a class as a component factory (its methods can `@provides` bindings).
203
-
204
- ### `@provides(key, *, lazy=False)`
205
-
206
- Declare that a factory method provides a component under `key`.
207
- Set `lazy=True` for deferred creation (`ComponentProxy`).
208
-
209
- ---
210
-
211
- ## 🧪 Testing
212
-
213
- ```bash
214
- pip install tox
215
- tox -e py311
216
- ```
217
-
218
- Tip: for “missing dependency” tests, mark those components as `lazy=True` so `init()` remains fail-fast for real components while your test still asserts failure on resolution.
219
-
220
- ---
221
-
222
- ## 🔌 Extensibility: Plugins, Binder, and Lifecycle Hooks
223
-
224
- From `v0.4.0` onward, Pico-IoC can be cleanly extended without patching the core.
225
- This enables optional integration layers like `pico-ioc-rest` for Flask, FastAPI, etc., while keeping the core dependency-free.
226
-
227
- ### Plugin Protocol
228
-
229
- A plugin is any object that implements some or all of the following methods:
230
-
231
- ```python
232
- from pico_ioc import PicoPlugin, Binder
233
-
234
- class MyPlugin:
235
- def before_scan(self, package, binder: Binder): ...
236
- def visit_class(self, module, cls, binder: Binder): ...
237
- def after_scan(self, package, binder: Binder): ...
238
- def after_bind(self, container, binder: Binder): ...
239
- def before_eager(self, container, binder: Binder): ...
240
- def after_ready(self, container, binder: Binder): ...
241
- ```
242
-
243
- All hooks are optional. If present, they are called in this order during `init()`:
244
-
245
- 1. **before\_scan** — called before package scanning starts.
246
- 2. **visit\_class** — called for every class discovered during scanning.
247
- 3. **after\_scan** — called after scanning all modules.
248
- 4. **after\_bind** — called after the core has bound all components/factories.
249
- 5. **before\_eager** — called right before eager (non-lazy) instantiation.
250
- 6. **after\_ready** — called after all eager instantiation is complete.
251
-
252
- ### Binder API
253
-
254
- Plugins receive a [`Binder`](#binder-api) instance in each hook, allowing them to:
255
-
256
- * **bind**: register new providers in the container.
257
- * **has**: check if a binding exists.
258
- * **get**: resolve a binding immediately.
259
-
260
- Example plugin that binds a “marker” component when a certain class is discovered:
261
-
262
- ```python
263
- class MarkerPlugin:
264
- def visit_class(self, module, cls, binder):
265
- if cls.__name__ == "SpecialService" and not binder.has("marker"):
266
- binder.bind("marker", lambda: {"ok": True}, lazy=False)
267
-
268
- container = init("my_app", plugins=(MarkerPlugin(),))
269
- assert container.get("marker") == {"ok": True}
270
- ```
271
-
272
- ### Creating Extensions
273
-
274
- With the plugin API, you can build separate packages like `pico-ioc-rest`:
275
-
276
- ```python
277
- from pico_ioc import PicoPlugin, Binder, create_instance, resolve_param
278
- from flask import Flask
279
-
280
- class FlaskRestPlugin:
281
- def __init__(self):
282
- self.controllers = []
283
-
284
- def visit_class(self, module, cls, binder: Binder):
285
- if getattr(cls, "_is_controller", False):
286
- self.controllers.append(cls)
287
-
288
- def after_bind(self, container, binder: Binder):
289
- app: Flask = container.get(Flask)
290
- for ctl_cls in self.controllers:
291
- ctl = create_instance(ctl_cls, container)
292
- # register routes here using `resolve_param` for handler DI
293
- ```
294
-
295
- ### Public Helpers for Extensions
296
-
297
- Plugins can reuse Pico-IoC’s DI logic without duplicating it:
298
-
299
- * **`create_instance(cls, container)`** — instantiate a class with DI, respecting Pico-IoC’s resolution order.
300
- * **`resolve_param(container, parameter)`** — resolve a single function/class parameter via Pico-IoC rules.
301
-
302
- ---
303
-
304
- ## ❓ FAQ
305
-
306
- **Q: Can I make the container lenient at startup?**
307
- A: By design it’s strict. Prefer `lazy=True` on specific bindings or exclude problem modules from the scan.
308
-
309
- **Q: Thread safety?**
310
- A: Container uses `ContextVar` to guard re-entrancy during scanning. Singletons are created once per container; typical usage is in single-threaded app startup, then read-mostly.
311
-
312
- **Q: Frameworks?**
313
- A: Framework-agnostic. Works with Flask, FastAPI, CLIs, scripts, etc.
314
-
315
- ---
316
-
317
- ## 📜 License
318
-
319
- MIT — see [LICENSE](https://opensource.org/licenses/MIT)
320
-
321
-
@@ -1,6 +0,0 @@
1
- pico_ioc/__init__.py,sha256=IqQOM75teE_gplSaemvxrfZjjj3w8-PBoRWRzFtZsz4,15219
2
- pico_ioc/_version.py,sha256=2eiWQI55fd-roDdkt4Hvl9WzrTJ4xQo33VzFud6D03U,22
3
- pico_ioc-0.4.0.dist-info/METADATA,sha256=vpgj38emsZxoJoGcW8-CWBt5K59U8cUF2GZrCzGy2ss,10733
4
- pico_ioc-0.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
5
- pico_ioc-0.4.0.dist-info/top_level.txt,sha256=_7_RLu616z_dtRw16impXn4Mw8IXe2J4BeX5912m5dQ,9
6
- pico_ioc-0.4.0.dist-info/RECORD,,