pico-ioc 0.1.1__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pico-ioc
3
- Version: 0.1.1
3
+ Version: 0.2.1
4
4
  Summary: A minimalist, zero-dependency Inversion of Control (IoC) container for Python.
5
5
  Author-email: David Perez Cabrera <dperezcabrera@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/dperezcabrera/pico-ioc
@@ -21,32 +21,35 @@ Classifier: Operating System :: OS Independent
21
21
  Requires-Python: >=3.8
22
22
  Description-Content-Type: text/markdown
23
23
 
24
- # Pico-IoC: A Minimalist IoC Container for Python
24
+
25
+ # 📦 Pico-IoC: A Minimalist IoC Container for Python
25
26
 
26
27
  [![PyPI](https://img.shields.io/pypi/v/pico-ioc.svg)](https://pypi.org/project/pico-ioc/)
27
28
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
28
29
  ![CI (tox matrix)](https://github.com/dperezcabrera/pico-ioc/actions/workflows/ci.yml/badge.svg)
29
30
 
30
- **Pico-IoC** is a tiny, zero-dependency, decorator-based Inversion of Control (IoC) container for Python.
31
+ **Pico-IoC** is a tiny, zero-dependency, decorator-based Inversion of Control (IoC) container for Python.
31
32
  It helps you manage dependencies in a clean, intuitive, and *Pythonic* way.
32
33
 
33
- The core idea is to let you build loosely coupled, easily testable applications without manually wiring components.
34
+ The core idea is to let you build loosely coupled, easily testable applications without manually wiring components.
34
35
  *Inspired by the IoC philosophy popularized by the Spring Framework.*
35
36
 
36
37
  ---
37
38
 
38
- ## Key Features
39
+ ## Key Features
39
40
 
40
- * **Zero Dependencies:** Pure Python, no external libraries.
41
- * 🚀 **Decorator-Based API:** Simple decorators like `@component` and `@provides`.
42
- * 🔍 **Automatic Discovery:** Scans your package to auto-register components.
43
- * 🧩 **Lazy Instantiation:** Objects are created on first use.
44
- * 🏭 **Flexible Factories:** Encapsulate complex creation logic.
45
- * 🤝 **Framework-Agnostic:** Works with Flask, FastAPI, CLIs, scripts, etc.
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.
46
49
 
47
50
  ---
48
51
 
49
- ## Installation
52
+ ## 📦 Installation
50
53
 
51
54
  ```bash
52
55
  pip install pico-ioc
@@ -54,9 +57,7 @@ pip install pico-ioc
54
57
 
55
58
  ---
56
59
 
57
- ## Quick Start
58
-
59
- Getting started is simple. Decorate your classes and let Pico-IoC wire them up.
60
+ ## 🚀 Quick Start
60
61
 
61
62
  ```python
62
63
  from pico_ioc import component, init
@@ -83,21 +84,22 @@ print(db.get_data()) # Data from postgresql://user:pass@host/db
83
84
 
84
85
  ---
85
86
 
86
- ## More Examples
87
+ ## 🧩 Custom Component Keys
87
88
 
88
- ### 🧩 Custom Component Name
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.
89
91
 
90
92
  ```python
91
93
  from pico_ioc import component, init
92
94
 
93
- @component(name="config")
95
+ @component(name="config") # still supported for legacy code
94
96
  class AppConfig:
95
97
  def __init__(self):
96
98
  self.db_url = "postgresql://user:pass@localhost/db"
97
99
 
98
100
  @component
99
101
  class Repository:
100
- def __init__(self, config: "config"): # refer by custom name if you prefer
102
+ def __init__(self, config: "config"): # resolve by name
101
103
  self._url = config.db_url
102
104
 
103
105
  container = init(__name__)
@@ -106,9 +108,12 @@ print(repo._url) # postgresql://user:pass@localhost/db
106
108
  print(container.get("config").db_url)
107
109
  ```
108
110
 
109
- > Pico-IoC prefers **type annotations** to resolve deps; if missing, it falls back to the **parameter name**.
111
+ ---
110
112
 
111
- ### 💤 Lazy Factories (only build when used)
113
+ ## 🏭 Factory Components and `@provides`
114
+
115
+ Factories can provide components under a specific **key**.
116
+ Default is lazy creation (via `LazyProxy`).
112
117
 
113
118
  ```python
114
119
  from pico_ioc import factory_component, provides, init
@@ -117,7 +122,7 @@ CREATION_COUNTER = {"value": 0}
117
122
 
118
123
  @factory_component
119
124
  class ServicesFactory:
120
- @provides(name="heavy_service") # returns a LazyProxy by default
125
+ @provides(key="heavy_service") # preferred
121
126
  def make_heavy(self):
122
127
  CREATION_COUNTER["value"] += 1
123
128
  return {"payload": "Hello from heavy service"}
@@ -130,7 +135,9 @@ print(svc["payload"]) # triggers creation
130
135
  print(CREATION_COUNTER["value"]) # 1
131
136
  ```
132
137
 
133
- ### 📦 Project-Style Package Scanning
138
+ ---
139
+
140
+ ## 📦 Project-Style Scanning
134
141
 
135
142
  ```
136
143
  project_root/
@@ -165,61 +172,62 @@ class ApiClient:
165
172
  import pico_ioc
166
173
  from myapp.services import ApiClient
167
174
 
168
- # Scan the whole 'myapp' package
169
175
  container = pico_ioc.init("myapp")
170
-
171
176
  client = container.get(ApiClient)
172
177
  print(client.get("status")) # GET https://api.example.com/status
173
178
  ```
174
179
 
175
180
  ---
176
181
 
177
- ## API Reference (mini)
182
+ ## 🧠 Dependency Resolution Order
178
183
 
179
- ### `init(root_package_or_module) -> PicoContainer`
184
+ When Pico-IoC instantiates a component, it tries to resolve each parameter in this order:
180
185
 
181
- Initialize the container by scanning a root **package** (str) or **module**. Returns the configured container.
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
182
190
 
183
- ### `@component(cls=None, *, name: str | None = None)`
191
+ ---
184
192
 
185
- Mark a class as a component. Registered by **class type** by default, or by **name** if provided.
193
+ ## 🛠 API Reference
186
194
 
187
- ### `@factory_component`
195
+ ### `init(root_package_or_module, *, exclude=None, auto_exclude_caller=True) -> PicoContainer`
188
196
 
189
- Mark a class as a factory holder. Methods inside can be `@provides(...)`.
197
+ Scan the given root **package** (str) or **module**.
198
+ By default, excludes the calling module.
190
199
 
191
- ### `@provides(name: str, lazy: bool = True)`
200
+ ### `@component(cls=None, *, name=None)`
192
201
 
193
- Declare that a factory method **produces** a component registered under `name`. By default it’s **lazy** (a proxy that creates on first real use).
202
+ Register a class as a component.
203
+ If `name` is given, registers under that string; otherwise under the class type.
194
204
 
195
- ---
205
+ ### `@factory_component`
196
206
 
197
- ## Testing
207
+ Register a class as a factory of components.
198
208
 
199
- `pico-ioc` ships with `pytest` tests and a `tox` matrix.
209
+ ### `@provides(key=None, *, name=None, lazy=True)`
200
210
 
201
- ```bash
202
- pip install tox
203
- tox -e py311 # run on a specific version
204
- tox # run all configured envs
205
- ```
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.
206
214
 
207
215
  ---
208
216
 
209
- ## Contributing
217
+ ## 🧪 Testing
210
218
 
211
- Issues and PRs are welcome. If you spot a bug or have an idea, open an issue!
219
+ ```bash
220
+ pip install tox
221
+ tox -e py311
222
+ ```
212
223
 
213
224
  ---
214
225
 
215
- ## License
226
+ ## 📜 License
216
227
 
217
- MIT — see the [LICENSE](https://opensource.org/licenses/MIT).
228
+ MIT — see [LICENSE](https://opensource.org/licenses/MIT)
218
229
 
219
230
  ---
220
231
 
221
- ## Authors
232
+ ¿Quieres que también te prepare **un ejemplo completo en el README** con `fast_model` y `BaseChatModel` para que quede documentado el nuevo orden de resolución? Así quedaría clarísimo para cualquiera que lo use.
222
233
 
223
- * **David Perez Cabrera**
224
- * **Gemini 2.5-Pro**
225
- * **GPT-5**
@@ -1,29 +1,32 @@
1
- # Pico-IoC: A Minimalist IoC Container for Python
1
+
2
+ # 📦 Pico-IoC: A Minimalist IoC Container for Python
2
3
 
3
4
  [![PyPI](https://img.shields.io/pypi/v/pico-ioc.svg)](https://pypi.org/project/pico-ioc/)
4
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
5
6
  ![CI (tox matrix)](https://github.com/dperezcabrera/pico-ioc/actions/workflows/ci.yml/badge.svg)
6
7
 
7
- **Pico-IoC** is a tiny, zero-dependency, decorator-based Inversion of Control (IoC) container for Python.
8
+ **Pico-IoC** is a tiny, zero-dependency, decorator-based Inversion of Control (IoC) container for Python.
8
9
  It helps you manage dependencies in a clean, intuitive, and *Pythonic* way.
9
10
 
10
- The core idea is to let you build loosely coupled, easily testable applications without manually wiring components.
11
+ The core idea is to let you build loosely coupled, easily testable applications without manually wiring components.
11
12
  *Inspired by the IoC philosophy popularized by the Spring Framework.*
12
13
 
13
14
  ---
14
15
 
15
- ## Key Features
16
+ ## Key Features
16
17
 
17
- * **Zero Dependencies:** Pure Python, no external libraries.
18
- * 🚀 **Decorator-Based API:** Simple decorators like `@component` and `@provides`.
19
- * 🔍 **Automatic Discovery:** Scans your package to auto-register components.
20
- * 🧩 **Lazy Instantiation:** Objects are created on first use.
21
- * 🏭 **Flexible Factories:** Encapsulate complex creation logic.
22
- * 🤝 **Framework-Agnostic:** Works with Flask, FastAPI, CLIs, scripts, etc.
18
+ * **Zero Dependencies:** Pure Python, no external libraries.
19
+ * **Decorator-Based API:** Simple decorators like `@component` and `@provides`.
20
+ * **Automatic Discovery:** Scans your package to auto-register components.
21
+ * **Lazy Instantiation:** Objects are created on first use.
22
+ * **Flexible Factories:** Encapsulate complex creation logic.
23
+ * **Framework-Agnostic:** Works with Flask, FastAPI, CLIs, scripts, etc.
24
+ * **Smart Dependency Resolution:** Resolves by **parameter name**, then **type annotation**, then **MRO fallback**.
25
+ * **Auto-Exclude Caller:** `init()` automatically skips the calling module to avoid double-initialization during scans.
23
26
 
24
27
  ---
25
28
 
26
- ## Installation
29
+ ## 📦 Installation
27
30
 
28
31
  ```bash
29
32
  pip install pico-ioc
@@ -31,9 +34,7 @@ pip install pico-ioc
31
34
 
32
35
  ---
33
36
 
34
- ## Quick Start
35
-
36
- Getting started is simple. Decorate your classes and let Pico-IoC wire them up.
37
+ ## 🚀 Quick Start
37
38
 
38
39
  ```python
39
40
  from pico_ioc import component, init
@@ -60,21 +61,22 @@ print(db.get_data()) # Data from postgresql://user:pass@host/db
60
61
 
61
62
  ---
62
63
 
63
- ## More Examples
64
+ ## 🧩 Custom Component Keys
64
65
 
65
- ### 🧩 Custom Component Name
66
+ You can register a component with a **custom key** (string, class, enum…).
67
+ `key=` is the preferred syntax. For backwards compatibility, `name=` still works.
66
68
 
67
69
  ```python
68
70
  from pico_ioc import component, init
69
71
 
70
- @component(name="config")
72
+ @component(name="config") # still supported for legacy code
71
73
  class AppConfig:
72
74
  def __init__(self):
73
75
  self.db_url = "postgresql://user:pass@localhost/db"
74
76
 
75
77
  @component
76
78
  class Repository:
77
- def __init__(self, config: "config"): # refer by custom name if you prefer
79
+ def __init__(self, config: "config"): # resolve by name
78
80
  self._url = config.db_url
79
81
 
80
82
  container = init(__name__)
@@ -83,9 +85,12 @@ print(repo._url) # postgresql://user:pass@localhost/db
83
85
  print(container.get("config").db_url)
84
86
  ```
85
87
 
86
- > Pico-IoC prefers **type annotations** to resolve deps; if missing, it falls back to the **parameter name**.
88
+ ---
87
89
 
88
- ### 💤 Lazy Factories (only build when used)
90
+ ## 🏭 Factory Components and `@provides`
91
+
92
+ Factories can provide components under a specific **key**.
93
+ Default is lazy creation (via `LazyProxy`).
89
94
 
90
95
  ```python
91
96
  from pico_ioc import factory_component, provides, init
@@ -94,7 +99,7 @@ CREATION_COUNTER = {"value": 0}
94
99
 
95
100
  @factory_component
96
101
  class ServicesFactory:
97
- @provides(name="heavy_service") # returns a LazyProxy by default
102
+ @provides(key="heavy_service") # preferred
98
103
  def make_heavy(self):
99
104
  CREATION_COUNTER["value"] += 1
100
105
  return {"payload": "Hello from heavy service"}
@@ -107,7 +112,9 @@ print(svc["payload"]) # triggers creation
107
112
  print(CREATION_COUNTER["value"]) # 1
108
113
  ```
109
114
 
110
- ### 📦 Project-Style Package Scanning
115
+ ---
116
+
117
+ ## 📦 Project-Style Scanning
111
118
 
112
119
  ```
113
120
  project_root/
@@ -142,61 +149,62 @@ class ApiClient:
142
149
  import pico_ioc
143
150
  from myapp.services import ApiClient
144
151
 
145
- # Scan the whole 'myapp' package
146
152
  container = pico_ioc.init("myapp")
147
-
148
153
  client = container.get(ApiClient)
149
154
  print(client.get("status")) # GET https://api.example.com/status
150
155
  ```
151
156
 
152
157
  ---
153
158
 
154
- ## API Reference (mini)
159
+ ## 🧠 Dependency Resolution Order
155
160
 
156
- ### `init(root_package_or_module) -> PicoContainer`
161
+ When Pico-IoC instantiates a component, it tries to resolve each parameter in this order:
157
162
 
158
- Initialize the container by scanning a root **package** (str) or **module**. Returns the configured container.
163
+ 1. **Exact parameter name** (string key in container)
164
+ 2. **Exact type annotation** (class key in container)
165
+ 3. **MRO fallback** (walk base classes until match)
166
+ 4. **String version** of the parameter name
159
167
 
160
- ### `@component(cls=None, *, name: str | None = None)`
168
+ ---
161
169
 
162
- Mark a class as a component. Registered by **class type** by default, or by **name** if provided.
170
+ ## 🛠 API Reference
163
171
 
164
- ### `@factory_component`
172
+ ### `init(root_package_or_module, *, exclude=None, auto_exclude_caller=True) -> PicoContainer`
165
173
 
166
- Mark a class as a factory holder. Methods inside can be `@provides(...)`.
174
+ Scan the given root **package** (str) or **module**.
175
+ By default, excludes the calling module.
167
176
 
168
- ### `@provides(name: str, lazy: bool = True)`
177
+ ### `@component(cls=None, *, name=None)`
169
178
 
170
- Declare that a factory method **produces** a component registered under `name`. By default it’s **lazy** (a proxy that creates on first real use).
179
+ Register a class as a component.
180
+ If `name` is given, registers under that string; otherwise under the class type.
171
181
 
172
- ---
182
+ ### `@factory_component`
173
183
 
174
- ## Testing
184
+ Register a class as a factory of components.
175
185
 
176
- `pico-ioc` ships with `pytest` tests and a `tox` matrix.
186
+ ### `@provides(key=None, *, name=None, lazy=True)`
177
187
 
178
- ```bash
179
- pip install tox
180
- tox -e py311 # run on a specific version
181
- tox # run all configured envs
182
- ```
188
+ Declare that a factory method provides a component under `key`.
189
+ `name` is accepted for backwards compatibility.
190
+ If `lazy=True`, returns a `LazyProxy` that instantiates on first real use.
183
191
 
184
192
  ---
185
193
 
186
- ## Contributing
194
+ ## 🧪 Testing
187
195
 
188
- Issues and PRs are welcome. If you spot a bug or have an idea, open an issue!
196
+ ```bash
197
+ pip install tox
198
+ tox -e py311
199
+ ```
189
200
 
190
201
  ---
191
202
 
192
- ## License
203
+ ## 📜 License
193
204
 
194
- MIT — see the [LICENSE](https://opensource.org/licenses/MIT).
205
+ MIT — see [LICENSE](https://opensource.org/licenses/MIT)
195
206
 
196
207
  ---
197
208
 
198
- ## Authors
209
+ ¿Quieres que también te prepare **un ejemplo completo en el README** con `fast_model` y `BaseChatModel` para que quede documentado el nuevo orden de resolución? Así quedaría clarísimo para cualquiera que lo use.
199
210
 
200
- * **David Perez Cabrera**
201
- * **Gemini 2.5-Pro**
202
- * **GPT-5**
@@ -1,7 +1,6 @@
1
- # src/pico_ioc/__init__.py
2
-
3
- import functools, inspect, pkgutil, importlib, logging, sys
4
- from typing import Callable, Any, Iterator, AsyncIterator
1
+ import functools, inspect, pkgutil, importlib, logging
2
+ from typing import Callable, Any, Optional
3
+ from contextvars import ContextVar
5
4
 
6
5
  try:
7
6
  # written at build time by setuptools-scm
@@ -11,18 +10,40 @@ except Exception: # pragma: no cover
11
10
 
12
11
  __all__ = ["__version__"]
13
12
 
13
+ # ------------------------------------------------------------------------------
14
+ # Re-entrancy guards
15
+ # ------------------------------------------------------------------------------
16
+ # True while init/scan is running. Blocks userland container access during scan.
17
+ _scanning: ContextVar[bool] = ContextVar("pico_scanning", default=False)
18
+
19
+ # True while the container is resolving deps for a component (internal use allowed).
20
+ _resolving: ContextVar[bool] = ContextVar("pico_resolving", default=False)
21
+
14
22
  # ==============================================================================
15
23
  # --- 1. Container and Chameleon Proxy (Framework-Agnostic) ---
16
24
  # ==============================================================================
17
25
  class PicoContainer:
18
26
  def __init__(self):
19
- self._providers = {}
20
- self._singletons = {}
27
+ self._providers: dict[Any, Callable[[], Any]] = {}
28
+ self._singletons: dict[Any, Any] = {}
21
29
 
22
30
  def bind(self, key: Any, provider: Callable[[], Any]):
23
31
  self._providers[key] = provider
24
32
 
33
+ def has(self, key: Any) -> bool:
34
+ return key in self._providers or key in self._singletons
35
+
25
36
  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
+ if _scanning.get() and not _resolving.get():
40
+ 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."
45
+ )
46
+
26
47
  if key in self._singletons:
27
48
  return self._singletons[key]
28
49
  if key in self._providers:
@@ -136,21 +157,63 @@ class LazyProxy:
136
157
  # ==============================================================================
137
158
  # --- 2. The Scanner and `init` Facade ---
138
159
  # ==============================================================================
139
- def _scan_and_configure(package_or_name, container: PicoContainer):
160
+ def _resolve_param(container: PicoContainer, p: inspect.Parameter) -> Any:
161
+ if p.name == 'self' or p.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
162
+ raise RuntimeError("Invalid param for resolution")
163
+
164
+ # 1) NAME
165
+ if container.has(p.name):
166
+ return container.get(p.name)
167
+
168
+ ann = p.annotation
169
+
170
+ # 2) TYPE
171
+ if ann is not inspect._empty and container.has(ann):
172
+ return container.get(ann)
173
+
174
+ # 3) TYPE MRO
175
+ if ann is not inspect._empty:
176
+ try:
177
+ for base in getattr(ann, "__mro__", ())[1:]:
178
+ if base is object:
179
+ break
180
+ if container.has(base):
181
+ return container.get(base)
182
+ except Exception:
183
+ pass
184
+
185
+ # 4) str(NAME)
186
+ if container.has(str(p.name)):
187
+ return container.get(str(p.name))
188
+
189
+ key = p.name if ann is inspect._empty else ann
190
+ return container.get(key)
191
+
192
+
193
+ def _scan_and_configure(
194
+ package_or_name,
195
+ container: PicoContainer,
196
+ exclude: Optional[Callable[[str], bool]] = None
197
+ ):
140
198
  package = importlib.import_module(package_or_name) if isinstance(package_or_name, str) else package_or_name
141
199
  logging.info(f"🚀 Scanning in '{package.__name__}'...")
142
200
  component_classes, factory_classes = [], []
201
+
143
202
  for _, name, _ in pkgutil.walk_packages(package.__path__, package.__name__ + '.'):
203
+ if exclude and exclude(name):
204
+ logging.info(f" ⏭️ Skipping module {name} (excluded)")
205
+ continue
144
206
  try:
145
207
  module = importlib.import_module(name)
146
208
  for _, obj in inspect.getmembers(module, inspect.isclass):
147
- if hasattr(obj, '_is_component'):
209
+ if getattr(obj, '_is_component', False):
148
210
  component_classes.append(obj)
149
- elif hasattr(obj, '_is_factory_component'):
211
+ elif getattr(obj, '_is_factory_component', False):
150
212
  factory_classes.append(obj)
151
213
  except Exception as e:
152
214
  logging.warning(f" ⚠️ Module {name} not processed: {e}")
153
215
 
216
+ # Register factories
154
217
  for factory_cls in factory_classes:
155
218
  try:
156
219
  sig = inspect.signature(factory_cls.__init__)
@@ -161,30 +224,64 @@ def _scan_and_configure(package_or_name, container: PicoContainer):
161
224
  except Exception as e:
162
225
  logging.error(f" ❌ Error in factory {factory_cls.__name__}: {e}", exc_info=True)
163
226
 
227
+ # Register components
164
228
  for component_cls in component_classes:
165
229
  key = getattr(component_cls, '_component_key', component_cls)
230
+
166
231
  def create_component(cls=component_cls):
167
232
  sig = inspect.signature(cls.__init__)
168
233
  deps = {}
169
- for p in sig.parameters.values():
170
- if p.name == 'self' or p.kind in (
171
- inspect.Parameter.VAR_POSITIONAL, # *args
172
- inspect.Parameter.VAR_KEYWORD, # **kwargs
173
- ):
174
- continue
175
- dep_key = p.annotation if p.annotation is not inspect._empty else p.name
176
- deps[p.name] = container.get(dep_key)
234
+ tok = _resolving.set(True)
235
+ try:
236
+ for p in sig.parameters.values():
237
+ if p.name == 'self' or p.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
238
+ continue
239
+ deps[p.name] = _resolve_param(container, p)
240
+ finally:
241
+ _resolving.reset(tok)
177
242
  return cls(**deps)
243
+
178
244
  container.bind(key, create_component)
179
245
 
180
246
  _container = None
181
- def init(root_package):
247
+
248
+ 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
+ """
182
254
  global _container
183
255
  if _container:
184
256
  return _container
257
+
258
+ combined_exclude = exclude
259
+ if auto_exclude_caller:
260
+ try:
261
+ caller_frame = inspect.stack()[1].frame
262
+ caller_module = inspect.getmodule(caller_frame)
263
+ caller_name = getattr(caller_module, "__name__", None)
264
+ except Exception:
265
+ caller_name = None
266
+
267
+ if caller_name:
268
+ if combined_exclude is None:
269
+ def combined_exclude(mod: str, _caller=caller_name):
270
+ return mod == _caller
271
+ else:
272
+ prev = combined_exclude
273
+ def combined_exclude(mod: str, _caller=caller_name, _prev=prev):
274
+ return mod == _caller or _prev(mod)
275
+
185
276
  _container = PicoContainer()
186
277
  logging.info("🔌 Initializing 'pico-ioc'...")
187
- _scan_and_configure(root_package, _container)
278
+
279
+ tok = _scanning.set(True)
280
+ try:
281
+ _scan_and_configure(root_package, _container, exclude=combined_exclude)
282
+ finally:
283
+ _scanning.reset(tok)
284
+
188
285
  logging.info("✅ Container configured and ready.")
189
286
  return _container
190
287
 
@@ -195,19 +292,27 @@ def factory_component(cls):
195
292
  setattr(cls, '_is_factory_component', True)
196
293
  return cls
197
294
 
198
- def provides(name: str, lazy: bool = True):
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
+ """
199
300
  def decorator(func):
200
301
  @functools.wraps(func)
201
302
  def wrapper(*args, **kwargs):
202
303
  return LazyProxy(lambda: func(*args, **kwargs)) if lazy else func(*args, **kwargs)
203
- setattr(wrapper, '_provides_name', name)
304
+ setattr(wrapper, '_provides_name', key) # legacy-compatible storage
204
305
  return wrapper
205
306
  return decorator
206
307
 
207
308
  def component(cls=None, *, name: str = None):
208
- def decorator(cls):
209
- setattr(cls, '_is_component', True)
210
- setattr(cls, '_component_key', name if name is not None else cls)
211
- return cls
309
+ """
310
+ Mark a class as a component. Registered by class type by default,
311
+ or by 'name' if provided.
312
+ """
313
+ def decorator(c):
314
+ setattr(c, '_is_component', True)
315
+ setattr(c, '_component_key', name if name is not None else c)
316
+ return c
212
317
  return decorator(cls) if cls else decorator
213
318
 
@@ -0,0 +1 @@
1
+ __version__ = '0.2.1'