pico-ioc 0.3.0__py3-none-any.whl → 0.3.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
@@ -1,63 +1,50 @@
1
1
  import functools, inspect, pkgutil, importlib, logging
2
- from typing import Callable, Any, Optional
2
+ from typing import Callable, Any, Optional, Dict
3
3
  from contextvars import ContextVar
4
4
 
5
5
  try:
6
- # written at build time by setuptools-scm
7
6
  from ._version import __version__
8
- except Exception: # pragma: no cover
7
+ except Exception:
9
8
  __version__ = "0.0.0"
10
9
 
11
10
  __all__ = ["__version__"]
12
11
 
13
- # ------------------------------------------------------------------------------
14
- # Re-entrancy guards
15
- # ------------------------------------------------------------------------------
16
- # True while init/scan is running. Blocks userland container access during scan.
17
12
  _scanning: ContextVar[bool] = ContextVar("pico_scanning", default=False)
18
-
19
- # True while the container is resolving deps for a component (internal use allowed).
20
13
  _resolving: ContextVar[bool] = ContextVar("pico_resolving", default=False)
21
14
 
22
- # ==============================================================================
23
- # --- 1. Container and Chameleon Proxy (Framework-Agnostic) ---
24
- # ==============================================================================
15
+
25
16
  class PicoContainer:
26
17
  def __init__(self):
27
- self._providers: dict[Any, Callable[[], Any]] = {}
28
- self._singletons: dict[Any, Any] = {}
18
+ self._providers: Dict[Any, Dict[str, Any]] = {}
19
+ self._singletons: Dict[Any, Any] = {}
29
20
 
30
- def bind(self, key: Any, provider: Callable[[], Any]):
31
- self._providers[key] = provider
21
+ def bind(self, key: Any, provider: Callable[[], Any], *, lazy: bool):
22
+ self._providers[key] = {"factory": provider, "lazy": bool(lazy)}
32
23
 
33
24
  def has(self, key: Any) -> bool:
34
25
  return key in self._providers or key in self._singletons
35
26
 
36
27
  def get(self, key: Any) -> Any:
37
- # Forbid user code calling container.get() while the scanner is importing modules.
38
- # Allow only internal calls performed during dependency resolution.
39
28
  if _scanning.get() and not _resolving.get():
40
29
  raise RuntimeError(
41
- "pico-ioc: re-entrant container access during scan. "
42
- "Avoid calling init()/get() at import time (e.g., in a module body). "
43
- "Move resolution to runtime (e.g., under if __name__ == '__main__':) "
44
- "or delay it until pico-ioc init completes."
30
+ "pico-ioc: re-entrant container access during scan."
45
31
  )
46
-
47
32
  if key in self._singletons:
48
33
  return self._singletons[key]
49
- if key in self._providers:
50
- instance = self._providers[key]()
51
- self._singletons[key] = instance
52
- return instance
53
- raise NameError(f"No provider found for key: {key}")
34
+ prov = self._providers.get(key)
35
+ if prov is None:
36
+ raise NameError(f"No provider found for key: {key}")
37
+ instance = prov["factory"]()
38
+ self._singletons[key] = instance
39
+ return instance
40
+
41
+ def _eager_instantiate_all(self):
42
+ for key, meta in list(self._providers.items()):
43
+ if not meta.get("lazy", False) and key not in self._singletons:
44
+ self.get(key)
45
+
54
46
 
55
- class LazyProxy:
56
- """
57
- A full-fledged lazy proxy that delegates almost all operations
58
- to the real object, which is created only on first access.
59
- It is completely framework-agnostic.
60
- """
47
+ class ComponentProxy:
61
48
  def __init__(self, object_creator: Callable[[], Any]):
62
49
  object.__setattr__(self, "_object_creator", object_creator)
63
50
  object.__setattr__(self, "__real_object", None)
@@ -69,30 +56,16 @@ class LazyProxy:
69
56
  object.__setattr__(self, "__real_object", real_obj)
70
57
  return real_obj
71
58
 
72
- # --- Core Proxying and Representation ---
73
59
  @property
74
60
  def __class__(self):
75
61
  return self._get_real_object().__class__
76
62
 
77
- def __getattr__(self, name):
78
- return getattr(self._get_real_object(), name)
79
-
80
- def __setattr__(self, name, value):
81
- setattr(self._get_real_object(), name, value)
82
-
83
- def __delattr__(self, name):
84
- delattr(self._get_real_object(), name)
85
-
86
- def __str__(self):
87
- return str(self._get_real_object())
88
-
89
- def __repr__(self):
90
- return repr(self._get_real_object())
91
-
92
- def __dir__(self):
93
- return dir(self._get_real_object())
94
-
95
- # --- Emulation of container types ---
63
+ def __getattr__(self, name): return getattr(self._get_real_object(), name)
64
+ def __setattr__(self, name, value): setattr(self._get_real_object(), name, value)
65
+ def __delattr__(self, name): delattr(self._get_real_object(), name)
66
+ def __str__(self): return str(self._get_real_object())
67
+ def __repr__(self): return repr(self._get_real_object())
68
+ def __dir__(self): return dir(self._get_real_object())
96
69
  def __len__(self): return len(self._get_real_object())
97
70
  def __getitem__(self, key): return self._get_real_object()[key]
98
71
  def __setitem__(self, key, value): self._get_real_object()[key] = value
@@ -100,8 +73,6 @@ class LazyProxy:
100
73
  def __iter__(self): return iter(self._get_real_object())
101
74
  def __reversed__(self): return reversed(self._get_real_object())
102
75
  def __contains__(self, item): return item in self._get_real_object()
103
-
104
- # --- Emulation of numeric types and operators ---
105
76
  def __add__(self, other): return self._get_real_object() + other
106
77
  def __sub__(self, other): return self._get_real_object() - other
107
78
  def __mul__(self, other): return self._get_real_object() * other
@@ -116,8 +87,6 @@ class LazyProxy:
116
87
  def __and__(self, other): return self._get_real_object() & other
117
88
  def __xor__(self, other): return self._get_real_object() ^ other
118
89
  def __or__(self, other): return self._get_real_object() | other
119
-
120
- # --- Right-hand side numeric operators ---
121
90
  def __radd__(self, other): return other + self._get_real_object()
122
91
  def __rsub__(self, other): return other - self._get_real_object()
123
92
  def __rmul__(self, other): return other * self._get_real_object()
@@ -132,14 +101,10 @@ class LazyProxy:
132
101
  def __rand__(self, other): return other & self._get_real_object()
133
102
  def __rxor__(self, other): return other ^ self._get_real_object()
134
103
  def __ror__(self, other): return other | self._get_real_object()
135
-
136
- # --- Unary operators ---
137
104
  def __neg__(self): return -self._get_real_object()
138
105
  def __pos__(self): return +self._get_real_object()
139
106
  def __abs__(self): return abs(self._get_real_object())
140
107
  def __invert__(self): return ~self._get_real_object()
141
-
142
- # --- Comparison operators ---
143
108
  def __eq__(self, other): return self._get_real_object() == other
144
109
  def __ne__(self, other): return self._get_real_object() != other
145
110
  def __lt__(self, other): return self._get_real_object() < other
@@ -147,31 +112,20 @@ class LazyProxy:
147
112
  def __gt__(self, other): return self._get_real_object() > other
148
113
  def __ge__(self, other): return self._get_real_object() >= other
149
114
  def __hash__(self): return hash(self._get_real_object())
150
-
151
- # --- Truthiness, Callability and Context Management ---
152
115
  def __bool__(self): return bool(self._get_real_object())
153
116
  def __call__(self, *args, **kwargs): return self._get_real_object()(*args, **kwargs)
154
117
  def __enter__(self): return self._get_real_object().__enter__()
155
118
  def __exit__(self, exc_type, exc_val, exc_tb): return self._get_real_object().__exit__(exc_type, exc_val, exc_tb)
156
119
 
157
- # ==============================================================================
158
- # --- 2. The Scanner and `init` Facade ---
159
- # ==============================================================================
120
+
160
121
  def _resolve_param(container: PicoContainer, p: inspect.Parameter) -> Any:
161
122
  if p.name == 'self' or p.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
162
123
  raise RuntimeError("Invalid param for resolution")
163
-
164
- # 1) NAME
165
124
  if container.has(p.name):
166
125
  return container.get(p.name)
167
-
168
126
  ann = p.annotation
169
-
170
- # 2) TYPE
171
127
  if ann is not inspect._empty and container.has(ann):
172
128
  return container.get(ann)
173
-
174
- # 3) TYPE MRO
175
129
  if ann is not inspect._empty:
176
130
  try:
177
131
  for base in getattr(ann, "__mro__", ())[1:]:
@@ -181,27 +135,19 @@ def _resolve_param(container: PicoContainer, p: inspect.Parameter) -> Any:
181
135
  return container.get(base)
182
136
  except Exception:
183
137
  pass
184
-
185
- # 4) str(NAME)
186
138
  if container.has(str(p.name)):
187
139
  return container.get(str(p.name))
188
-
189
140
  key = p.name if ann is inspect._empty else ann
190
141
  return container.get(key)
191
142
 
192
143
 
193
- def _scan_and_configure(
194
- package_or_name,
195
- container: PicoContainer,
196
- exclude: Optional[Callable[[str], bool]] = None
197
- ):
144
+ def _scan_and_configure(package_or_name, container: PicoContainer, exclude: Optional[Callable[[str], bool]] = None):
198
145
  package = importlib.import_module(package_or_name) if isinstance(package_or_name, str) else package_or_name
199
- logging.info(f"🚀 Scanning in '{package.__name__}'...")
146
+ logging.info(f"Scanning in '{package.__name__}'...")
200
147
  component_classes, factory_classes = [], []
201
-
202
148
  for _, name, _ in pkgutil.walk_packages(package.__path__, package.__name__ + '.'):
203
149
  if exclude and exclude(name):
204
- logging.info(f" ⏭️ Skipping module {name} (excluded)")
150
+ logging.info(f"Skipping module {name} (excluded)")
205
151
  continue
206
152
  try:
207
153
  module = importlib.import_module(name)
@@ -211,23 +157,27 @@ def _scan_and_configure(
211
157
  elif getattr(obj, '_is_factory_component', False):
212
158
  factory_classes.append(obj)
213
159
  except Exception as e:
214
- logging.warning(f" ⚠️ Module {name} not processed: {e}")
215
-
216
- # Register factories
160
+ logging.warning(f"Module {name} not processed: {e}")
217
161
  for factory_cls in factory_classes:
218
162
  try:
219
163
  sig = inspect.signature(factory_cls.__init__)
220
164
  instance = factory_cls(container) if 'container' in sig.parameters else factory_cls()
221
165
  for _, method in inspect.getmembers(instance, inspect.ismethod):
222
166
  if hasattr(method, '_provides_name'):
223
- container.bind(getattr(method, '_provides_name'), method)
167
+ key = getattr(method, '_provides_name')
168
+ is_lazy = bool(getattr(method, '_pico_lazy', False))
169
+ def make_provider(m=method, lazy=is_lazy):
170
+ def _factory():
171
+ if lazy:
172
+ return ComponentProxy(lambda: m())
173
+ return m()
174
+ return _factory
175
+ container.bind(key, make_provider(), lazy=is_lazy)
224
176
  except Exception as e:
225
- logging.error(f"Error in factory {factory_cls.__name__}: {e}", exc_info=True)
226
-
227
- # Register components
177
+ logging.error(f"Error in factory {factory_cls.__name__}: {e}", exc_info=True)
228
178
  for component_cls in component_classes:
229
179
  key = getattr(component_cls, '_component_key', component_cls)
230
-
180
+ is_lazy = bool(getattr(component_cls, '_component_lazy', False))
231
181
  def create_component(cls=component_cls):
232
182
  sig = inspect.signature(cls.__init__)
233
183
  deps = {}
@@ -240,21 +190,22 @@ def _scan_and_configure(
240
190
  finally:
241
191
  _resolving.reset(tok)
242
192
  return cls(**deps)
193
+ def provider_factory(lazy=is_lazy, maker=create_component):
194
+ def _factory():
195
+ if lazy:
196
+ return ComponentProxy(maker)
197
+ return maker()
198
+ return _factory
199
+ container.bind(key, provider_factory(), lazy=is_lazy)
243
200
 
244
- container.bind(key, create_component)
245
201
 
246
202
  _container = None
247
203
 
204
+
248
205
  def init(root_package, *, exclude: Optional[Callable[[str], bool]] = None, auto_exclude_caller: bool = True):
249
- """
250
- Initialize the global container and scan the given root package/module.
251
- While scanning, re-entrant userland access to container.get() is blocked
252
- to avoid import-time side effects.
253
- """
254
206
  global _container
255
207
  if _container:
256
208
  return _container
257
-
258
209
  combined_exclude = exclude
259
210
  if auto_exclude_caller:
260
211
  try:
@@ -263,7 +214,6 @@ def init(root_package, *, exclude: Optional[Callable[[str], bool]] = None, auto_
263
214
  caller_name = getattr(caller_module, "__name__", None)
264
215
  except Exception:
265
216
  caller_name = None
266
-
267
217
  if caller_name:
268
218
  if combined_exclude is None:
269
219
  def combined_exclude(mod: str, _caller=caller_name):
@@ -272,47 +222,39 @@ def init(root_package, *, exclude: Optional[Callable[[str], bool]] = None, auto_
272
222
  prev = combined_exclude
273
223
  def combined_exclude(mod: str, _caller=caller_name, _prev=prev):
274
224
  return mod == _caller or _prev(mod)
275
-
276
225
  _container = PicoContainer()
277
- logging.info("🔌 Initializing 'pico-ioc'...")
278
-
226
+ logging.info("Initializing pico-ioc...")
279
227
  tok = _scanning.set(True)
280
228
  try:
281
229
  _scan_and_configure(root_package, _container, exclude=combined_exclude)
282
230
  finally:
283
231
  _scanning.reset(tok)
284
-
285
- logging.info("Container configured and ready.")
232
+ _container._eager_instantiate_all()
233
+ logging.info("Container configured and ready.")
286
234
  return _container
287
235
 
288
- # ==============================================================================
289
- # --- 3. The Decorators ---
290
- # ==============================================================================
236
+
291
237
  def factory_component(cls):
292
238
  setattr(cls, '_is_factory_component', True)
293
239
  return cls
294
240
 
295
- def provides(key: Any, *, lazy: bool = True):
296
- """
297
- Declare that a factory method provides a component under 'key'.
298
- By default, returns a LazyProxy that instantiates upon first real use.
299
- """
241
+
242
+ def provides(key: Any, *, lazy: bool = False):
300
243
  def decorator(func):
301
244
  @functools.wraps(func)
302
245
  def wrapper(*args, **kwargs):
303
- return LazyProxy(lambda: func(*args, **kwargs)) if lazy else func(*args, **kwargs)
304
- setattr(wrapper, '_provides_name', key) # legacy-compatible storage
246
+ return func(*args, **kwargs)
247
+ setattr(wrapper, '_provides_name', key)
248
+ setattr(wrapper, '_pico_lazy', bool(lazy))
305
249
  return wrapper
306
250
  return decorator
307
251
 
308
- def component(cls=None, *, name: str = None):
309
- """
310
- Mark a class as a component. Registered by class type by default,
311
- or by 'name' if provided.
312
- """
252
+
253
+ def component(cls=None, *, name: Any = None, lazy: bool = False):
313
254
  def decorator(c):
314
255
  setattr(c, '_is_component', True)
315
256
  setattr(c, '_component_key', name if name is not None else c)
257
+ setattr(c, '_component_lazy', bool(lazy))
316
258
  return c
317
259
  return decorator(cls) if cls else decorator
318
260
 
pico_ioc/_version.py CHANGED
@@ -1 +1 @@
1
- __version__ = '0.3.0'
1
+ __version__ = '0.3.1'
@@ -0,0 +1,239 @@
1
+ Metadata-Version: 2.4
2
+ Name: pico-ioc
3
+ Version: 0.3.1
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
+ ## ❓ FAQ
223
+
224
+ **Q: Can I make the container lenient at startup?**
225
+ A: By design it’s strict. Prefer `lazy=True` on specific bindings or exclude problem modules from the scan.
226
+
227
+ **Q: Thread safety?**
228
+ 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.
229
+
230
+ **Q: Frameworks?**
231
+ A: Framework-agnostic. Works with Flask, FastAPI, CLIs, scripts, etc.
232
+
233
+ ---
234
+
235
+ ## 📜 License
236
+
237
+ MIT — see [LICENSE](https://opensource.org/licenses/MIT)
238
+
239
+
@@ -0,0 +1,6 @@
1
+ pico_ioc/__init__.py,sha256=8VKuQM7hgXNrkPNMnpZ8ixg6LK4DpKCU-hvSSidGzEU,11874
2
+ pico_ioc/_version.py,sha256=TZkGuMIRSRmUY3XCIs5owt2o60vXyqYMHWIkhx65uYE,22
3
+ pico_ioc-0.3.1.dist-info/METADATA,sha256=oR-ft50RlnmX5BB_bFOoq5tIuaVflb3GTGXU9rI5oFM,7751
4
+ pico_ioc-0.3.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
5
+ pico_ioc-0.3.1.dist-info/top_level.txt,sha256=_7_RLu616z_dtRw16impXn4Mw8IXe2J4BeX5912m5dQ,9
6
+ pico_ioc-0.3.1.dist-info/RECORD,,
@@ -1,228 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: pico-ioc
3
- Version: 0.3.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
-
25
- # 📦 Pico-IoC: A Minimalist IoC Container for Python
26
-
27
- [![PyPI](https://img.shields.io/pypi/v/pico-ioc.svg)](https://pypi.org/project/pico-ioc/)
28
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
29
- ![CI (tox matrix)](https://github.com/dperezcabrera/pico-ioc/actions/workflows/ci.yml/badge.svg)
30
-
31
- **Pico-IoC** is a tiny, zero-dependency, decorator-based Inversion of Control (IoC) container for Python.
32
- It helps you manage dependencies in a clean, intuitive, and *Pythonic* way.
33
-
34
- The core idea is to let you build loosely coupled, easily testable applications without manually wiring components.
35
- *Inspired by the IoC philosophy popularized by the Spring Framework.*
36
-
37
- ---
38
-
39
- ## ✨ Key Features
40
-
41
- * **Zero Dependencies:** Pure Python, no external libraries.
42
- * **Decorator-Based API:** Simple decorators like `@component` and `@provides`.
43
- * **Automatic Discovery:** Scans your package to auto-register components.
44
- * **Lazy Instantiation:** Objects are created on first use.
45
- * **Flexible Factories:** Encapsulate complex creation logic.
46
- * **Framework-Agnostic:** Works with Flask, FastAPI, CLIs, scripts, etc.
47
- * **Smart Dependency Resolution:** Resolves by **parameter name**, then **type annotation**, then **MRO fallback**.
48
- * **Auto-Exclude Caller:** `init()` automatically skips the calling module to avoid double-initialization during scans.
49
-
50
- ---
51
-
52
- ## 📦 Installation
53
-
54
- ```bash
55
- pip install pico-ioc
56
- ```
57
-
58
- ---
59
-
60
- ## 🚀 Quick Start
61
-
62
- ```python
63
- from pico_ioc import component, init
64
-
65
- @component
66
- class AppConfig:
67
- def get_db_url(self):
68
- return "postgresql://user:pass@host/db"
69
-
70
- @component
71
- class DatabaseService:
72
- def __init__(self, config: AppConfig):
73
- self._cs = config.get_db_url()
74
-
75
- def get_data(self):
76
- return f"Data from {self._cs}"
77
-
78
- # Initialize the container scanning the current module
79
- container = init(__name__)
80
-
81
- db = container.get(DatabaseService)
82
- print(db.get_data()) # Data from postgresql://user:pass@host/db
83
- ```
84
-
85
- ---
86
-
87
- ## 🧩 Custom Component Keys
88
-
89
- You can register a component with a **custom key** (string, class, enum…).
90
- `key=` is the preferred syntax. For backwards compatibility, `name=` still works.
91
-
92
- ```python
93
- from pico_ioc import component, init
94
-
95
- @component(name="config") # still supported for legacy code
96
- class AppConfig:
97
- def __init__(self):
98
- self.db_url = "postgresql://user:pass@localhost/db"
99
-
100
- @component
101
- class Repository:
102
- def __init__(self, config: "config"): # resolve by name
103
- self._url = config.db_url
104
-
105
- container = init(__name__)
106
- repo = container.get(Repository)
107
- print(repo._url) # postgresql://user:pass@localhost/db
108
- print(container.get("config").db_url)
109
- ```
110
-
111
- ---
112
-
113
- ## 🏭 Factory Components and `@provides`
114
-
115
- Factories can provide components under a specific **key**.
116
- Default is lazy creation (via `LazyProxy`).
117
-
118
- ```python
119
- from pico_ioc import factory_component, provides, init
120
-
121
- CREATION_COUNTER = {"value": 0}
122
-
123
- @factory_component
124
- class ServicesFactory:
125
- @provides(key="heavy_service") # preferred
126
- def make_heavy(self):
127
- CREATION_COUNTER["value"] += 1
128
- return {"payload": "Hello from heavy service"}
129
-
130
- container = init(__name__)
131
- svc = container.get("heavy_service")
132
- print(CREATION_COUNTER["value"]) # 0 (not created yet)
133
-
134
- print(svc["payload"]) # triggers creation
135
- print(CREATION_COUNTER["value"]) # 1
136
- ```
137
-
138
- ---
139
-
140
- ## 📦 Project-Style Scanning
141
-
142
- ```
143
- project_root/
144
- └── myapp/
145
- ├── __init__.py
146
- ├── services.py
147
- └── main.py
148
- ```
149
-
150
- **myapp/services.py**
151
-
152
- ```python
153
- from pico_ioc import component
154
-
155
- @component
156
- class Config:
157
- def __init__(self):
158
- self.base_url = "https://api.example.com"
159
-
160
- @component
161
- class ApiClient:
162
- def __init__(self, config: Config):
163
- self.base_url = config.base_url
164
-
165
- def get(self, path: str):
166
- return f"GET {self.base_url}/{path}"
167
- ```
168
-
169
- **myapp/main.py**
170
-
171
- ```python
172
- import pico_ioc
173
- from myapp.services import ApiClient
174
-
175
- container = pico_ioc.init("myapp")
176
- client = container.get(ApiClient)
177
- print(client.get("status")) # GET https://api.example.com/status
178
- ```
179
-
180
- ---
181
-
182
- ## 🧠 Dependency Resolution Order
183
-
184
- When Pico-IoC instantiates a component, it tries to resolve each parameter in this order:
185
-
186
- 1. **Exact parameter name** (string key in container)
187
- 2. **Exact type annotation** (class key in container)
188
- 3. **MRO fallback** (walk base classes until match)
189
- 4. **String version** of the parameter name
190
-
191
- ---
192
-
193
- ## 🛠 API Reference
194
-
195
- ### `init(root_package_or_module, *, exclude=None, auto_exclude_caller=True) -> PicoContainer`
196
-
197
- Scan the given root **package** (str) or **module**.
198
- By default, excludes the calling module.
199
-
200
- ### `@component(cls=None, *, name=None)`
201
-
202
- Register a class as a component.
203
- If `name` is given, registers under that string; otherwise under the class type.
204
-
205
- ### `@factory_component`
206
-
207
- Register a class as a factory of components.
208
-
209
- ### `@provides(key=None, *, name=None, lazy=True)`
210
-
211
- Declare that a factory method provides a component under `key`.
212
- `name` is accepted for backwards compatibility.
213
- If `lazy=True`, returns a `LazyProxy` that instantiates on first real use.
214
-
215
- ---
216
-
217
- ## 🧪 Testing
218
-
219
- ```bash
220
- pip install tox
221
- tox -e py311
222
- ```
223
-
224
- ---
225
-
226
- ## 📜 License
227
-
228
- MIT — see [LICENSE](https://opensource.org/licenses/MIT)
@@ -1,6 +0,0 @@
1
- pico_ioc/__init__.py,sha256=wCgi07l_0ZqhgZRfoRgWld_Q3_0qkqazUPWsO0XbPQI,13474
2
- pico_ioc/_version.py,sha256=3wVEs2QD_7OcTlD97cZdCeizd2hUbJJ0GeIO8wQIjrk,22
3
- pico_ioc-0.3.0.dist-info/METADATA,sha256=nzmDf0ZLTn0OkaoYDytJDnPQstVgVf2ExGZv0nMs15g,6374
4
- pico_ioc-0.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
5
- pico_ioc-0.3.0.dist-info/top_level.txt,sha256=_7_RLu616z_dtRw16impXn4Mw8IXe2J4BeX5912m5dQ,9
6
- pico_ioc-0.3.0.dist-info/RECORD,,