pico-ioc 2.0.2__py3-none-any.whl → 2.0.3__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
@@ -11,6 +11,7 @@ from .exceptions import (
11
11
  ValidationError,
12
12
  InvalidBindingError,
13
13
  EventBusClosedError,
14
+ AsyncResolutionError,
14
15
  )
15
16
  from .api import (
16
17
  component,
@@ -51,6 +52,7 @@ __all__ = [
51
52
  "SerializationError",
52
53
  "ValidationError",
53
54
  "InvalidBindingError",
55
+ "AsyncResolutionError",
54
56
  "EventBusClosedError",
55
57
  "component",
56
58
  "factory",
pico_ioc/_version.py CHANGED
@@ -1 +1 @@
1
- __version__ = '2.0.2'
1
+ __version__ = '2.0.3'
pico_ioc/api.py CHANGED
@@ -9,7 +9,6 @@ from dataclasses import is_dataclass, fields, dataclass, MISSING
9
9
  from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Union, get_args, get_origin, Annotated, Protocol, Mapping
10
10
  from .constants import LOGGER, PICO_INFRA, PICO_NAME, PICO_KEY, PICO_META
11
11
  from .exceptions import (
12
- ProviderNotFoundError,
13
12
  CircularDependencyError,
14
13
  ScopeError,
15
14
  ConfigurationError,
@@ -409,8 +408,18 @@ def _resolve_args(callable_obj: Callable[..., Any], pico: "PicoContainer") -> Di
409
408
  try:
410
409
  for kind, name, data in plan:
411
410
  if kind == "key":
412
- tracer.note_param(data if isinstance(data, (str, type)) else name, name)
413
- kwargs[name] = pico.get(data)
411
+ primary_key = data
412
+ tracer.note_param(primary_key, name)
413
+ try:
414
+ kwargs[name] = pico.get(primary_key)
415
+ except ProviderNotFoundError as first_error:
416
+ if primary_key != name:
417
+ try:
418
+ kwargs[name] = pico.get(name)
419
+ except ProviderNotFoundError:
420
+ raise first_error from None
421
+ else:
422
+ raise first_error from None
414
423
  else:
415
424
  vals = [pico.get(k) for k in data]
416
425
  kwargs[name] = vals
@@ -574,7 +583,7 @@ class Registrar:
574
583
  deferred.attach(pico, locator)
575
584
  for key, md in list(self._metadata.items()):
576
585
  if md.lazy:
577
- original = self._factory.get(key)
586
+ original = self._factory.get(key, origin='lazy')
578
587
  def lazy_proxy_provider(_orig=original, _p=pico):
579
588
  return UnifiedComponentProxy(container=_p, object_creator=_orig)
580
589
  self._factory.bind(key, lazy_proxy_provider)
pico_ioc/container.py CHANGED
@@ -4,7 +4,7 @@ import contextvars
4
4
  from typing import Any, Dict, List, Optional, Tuple, overload, Union
5
5
  from contextlib import contextmanager
6
6
  from .constants import LOGGER, PICO_META
7
- from .exceptions import CircularDependencyError, ComponentCreationError, ProviderNotFoundError
7
+ from .exceptions import CircularDependencyError, ComponentCreationError, ProviderNotFoundError, AsyncResolutionError
8
8
  from .factory import ComponentFactory
9
9
  from .locator import ComponentLocator
10
10
  from .scope import ScopedCaches, ScopeManager
@@ -173,85 +173,88 @@ class PicoContainer:
173
173
  return k
174
174
  return key
175
175
 
176
- @overload
177
- def get(self, key: type) -> Any: ...
178
- @overload
179
- def get(self, key: str) -> Any: ...
180
- def get(self, key: KeyT) -> Any:
176
+ def _resolve_or_create_internal(self, key: KeyT) -> Tuple[Any, float, bool]:
181
177
  key = self._canonical_key(key)
182
178
  cache = self._cache_for(key)
183
179
  cached = cache.get(key)
184
180
  if cached is not None:
185
181
  self.context.cache_hit_count += 1
186
182
  for o in self._observers: o.on_cache_hit(key)
187
- return cached
183
+ return cached, 0.0, True
184
+
188
185
  import time as _tm
189
186
  t0 = _tm.perf_counter()
190
187
  chain = list(_resolve_chain.get())
191
- for k in chain:
192
- if k == key:
188
+
189
+ for k_in_chain in chain:
190
+ if k_in_chain == key:
193
191
  details = self._tracer.describe_cycle(tuple(chain), key, self._locator)
194
192
  raise ComponentCreationError(key, CircularDependencyError(chain, key, details=details))
193
+
195
194
  token_chain = _resolve_chain.set(tuple(chain + [key]))
196
195
  token_container = self.activate()
197
196
  token_tracer = self._tracer.enter(key, via="provider")
197
+
198
+ requester = chain[-1] if chain else None
199
+ instance_or_awaitable = None
200
+
198
201
  try:
199
- provider = self._factory.get(key)
202
+ provider = self._factory.get(key, origin=requester)
200
203
  try:
201
- instance = provider()
204
+ instance_or_awaitable = provider()
202
205
  except ProviderNotFoundError as e:
203
206
  raise
204
- except Exception as e:
205
- raise ComponentCreationError(key, e) from e
206
- instance = self._maybe_wrap_with_aspects(key, instance)
207
- cache.put(key, instance)
208
- self.context.resolve_count += 1
207
+ except Exception as creation_error:
208
+ raise ComponentCreationError(key, creation_error) from creation_error
209
+
209
210
  took_ms = (_tm.perf_counter() - t0) * 1000
210
- for o in self._observers: o.on_resolve(key, took_ms)
211
- return instance
211
+ return instance_or_awaitable, took_ms, False
212
+
212
213
  finally:
213
214
  self._tracer.leave(token_tracer)
214
215
  _resolve_chain.reset(token_chain)
215
216
  self.deactivate(token_container)
217
+
218
+ @overload
219
+ def get(self, key: type) -> Any: ...
220
+ @overload
221
+ def get(self, key: str) -> Any: ...
222
+ def get(self, key: KeyT) -> Any:
223
+ instance_or_awaitable, took_ms, was_cached = self._resolve_or_create_internal(key)
224
+
225
+ if was_cached:
226
+ return instance_or_awaitable
227
+
228
+ instance = instance_or_awaitable
229
+ if inspect.isawaitable(instance):
230
+ key_name = getattr(key, '__name__', str(key))
231
+ raise AsyncResolutionError(key)
232
+
233
+ final_instance = self._maybe_wrap_with_aspects(key, instance)
234
+ cache = self._cache_for(key)
235
+ cache.put(key, final_instance)
236
+ self.context.resolve_count += 1
237
+ for o in self._observers: o.on_resolve(key, took_ms)
238
+
239
+ return final_instance
216
240
 
217
241
  async def aget(self, key: KeyT) -> Any:
218
- key = self._canonical_key(key)
242
+ instance_or_awaitable, took_ms, was_cached = self._resolve_or_create_internal(key)
243
+
244
+ if was_cached:
245
+ return instance_or_awaitable
246
+
247
+ instance = instance_or_awaitable
248
+ if inspect.isawaitable(instance_or_awaitable):
249
+ instance = await instance_or_awaitable
250
+
251
+ final_instance = self._maybe_wrap_with_aspects(key, instance)
219
252
  cache = self._cache_for(key)
220
- cached = cache.get(key)
221
- if cached is not None:
222
- self.context.cache_hit_count += 1
223
- for o in self._observers: o.on_cache_hit(key)
224
- return cached
225
- import time as _tm
226
- t0 = _tm.perf_counter()
227
- chain = list(_resolve_chain.get())
228
- for k in chain:
229
- if k == key:
230
- details = self._tracer.describe_cycle(tuple(chain), key, self._locator)
231
- raise ComponentCreationError(key, CircularDependencyError(chain, key, details=details))
232
- token_chain = _resolve_chain.set(tuple(chain + [key]))
233
- token_container = self.activate()
234
- token_tracer = self._tracer.enter(key, via="provider")
235
- try:
236
- provider = self._factory.get(key)
237
- try:
238
- instance = provider()
239
- if inspect.isawaitable(instance):
240
- instance = await instance
241
- except ProviderNotFoundError as e:
242
- raise
243
- except Exception as e:
244
- raise ComponentCreationError(key, e) from e
245
- instance = self._maybe_wrap_with_aspects(key, instance)
246
- cache.put(key, instance)
247
- self.context.resolve_count += 1
248
- took_ms = (_tm.perf_counter() - t0) * 1000
249
- for o in self._observers: o.on_resolve(key, took_ms)
250
- return instance
251
- finally:
252
- self._tracer.leave(token_tracer)
253
- _resolve_chain.reset(token_chain)
254
- self.deactivate(token_container)
253
+ cache.put(key, final_instance)
254
+ self.context.resolve_count += 1
255
+ for o in self._observers: o.on_resolve(key, took_ms)
256
+
257
+ return final_instance
255
258
 
256
259
  def _resolve_type_key(self, key: type):
257
260
  if not self._locator:
pico_ioc/exceptions.py CHANGED
@@ -4,9 +4,15 @@ class PicoError(Exception):
4
4
  pass
5
5
 
6
6
  class ProviderNotFoundError(PicoError):
7
- def __init__(self, key: Any):
8
- super().__init__(f"Provider not found for key: {getattr(key, '__name__', key)}")
7
+ def __init__(self, key: Any, origin: Any | None = None):
8
+ key_name = getattr(key, '__name__', str(key))
9
+ origin_name = getattr(origin, '__name__', str(origin)) if origin else "init"
10
+ super().__init__(
11
+ f"Provider for key '{key_name}' not found "
12
+ f"(required by: '{origin_name}')"
13
+ )
9
14
  self.key = key
15
+ self.origin = origin
10
16
 
11
17
  class CircularDependencyError(PicoError):
12
18
  def __init__(self, chain: Iterable[Any], current: Any, details: str | None = None, hint: str | None = None):
@@ -51,6 +57,15 @@ class InvalidBindingError(ValidationError):
51
57
  super().__init__("Invalid bindings:\n" + "\n".join(f"- {e}" for e in errors))
52
58
  self.errors = errors
53
59
 
60
+ class AsyncResolutionError(PicoError):
61
+ def __init__(self, key: Any):
62
+ key_name = getattr(key, '__name__', str(key))
63
+ super().__init__(
64
+ f"Synchronous get() received an awaitable for key '{key_name}'. "
65
+ "Use aget() instead."
66
+ )
67
+ self.key = key
68
+
54
69
  class EventBusError(PicoError):
55
70
  def __init__(self, msg: str):
56
71
  super().__init__(msg)
pico_ioc/factory.py CHANGED
@@ -28,9 +28,9 @@ class ComponentFactory:
28
28
  self._providers[key] = provider
29
29
  def has(self, key: KeyT) -> bool:
30
30
  return key in self._providers
31
- def get(self, key: KeyT) -> Provider:
31
+ def get(self, key: KeyT, origin: KeyT) -> Provider:
32
32
  if key not in self._providers:
33
- raise ProviderNotFoundError(key)
33
+ raise ProviderNotFoundError(key, origin)
34
34
  return self._providers[key]
35
35
 
36
36
  class DeferredProvider:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pico-ioc
3
- Version: 2.0.2
3
+ Version: 2.0.3
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
  License: MIT License
@@ -0,0 +1,17 @@
1
+ pico_ioc/__init__.py,sha256=x7Sg0i4P0aXN3L5WH9IoTkyELPk-NQBdaRq8GuoZqO4,2294
2
+ pico_ioc/_version.py,sha256=fcRMgwI40iVFwre_3iKQA-N6sHUo3pAErXcKJS63DRg,22
3
+ pico_ioc/aop.py,sha256=prFSlZC6vJYUfTbkMvlSc1T9UvvdEHr94Z0HAvjZ1fg,12985
4
+ pico_ioc/api.py,sha256=rV8_xa6rjARHND_G1vGSNPBplzaaTV3qm3CEp9ntH54,47444
5
+ pico_ioc/config_runtime.py,sha256=z1cHDb5PbM8PMLYRFf5c2dmze8V22xwEzpWcBhtmMpA,11950
6
+ pico_ioc/constants.py,sha256=AhIt0ieDZ9Turxb_YbNzj11wUbBbzjKfWh1BDlSx2Nw,183
7
+ pico_ioc/container.py,sha256=-0o3T2uiKCnYVHTXP4MnUnA-atNeITjz0CtvUc_rY_E,17237
8
+ pico_ioc/event_bus.py,sha256=E8Qb8KZ6K1CuXSbMlG0MNPHkGoWlssLLPzHq1QYdADQ,8346
9
+ pico_ioc/exceptions.py,sha256=PnIY60r8O-eYXEVk7XPghz-hHNx_012ZLw9ikDwQhqQ,3019
10
+ pico_ioc/factory.py,sha256=RMJJrD91HJdb7R3C6y48Fnia7YRQnQuMuYTC2cIAm34,1579
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.3.dist-info/licenses/LICENSE,sha256=N1_nOvHTM6BobYnOTNXiQkroDqCEi6EzfGBv8lWtyZ0,1077
14
+ pico_ioc-2.0.3.dist-info/METADATA,sha256=h7J1Q3I0qZUUqKPLWGrS59TM-yx74-qXjbYH47rn8t8,8741
15
+ pico_ioc-2.0.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
16
+ pico_ioc-2.0.3.dist-info/top_level.txt,sha256=_7_RLu616z_dtRw16impXn4Mw8IXe2J4BeX5912m5dQ,9
17
+ pico_ioc-2.0.3.dist-info/RECORD,,
@@ -1,17 +0,0 @@
1
- pico_ioc/__init__.py,sha256=mx1AVEZ-m0jntrWlMJGKQn_kaVVrKBY1NVYG8utT6yI,2240
2
- pico_ioc/_version.py,sha256=kumiGImhzOTlTrRM-6jDo2mNnVHGO_2vxtrhB0nzAiw,22
3
- pico_ioc/aop.py,sha256=prFSlZC6vJYUfTbkMvlSc1T9UvvdEHr94Z0HAvjZ1fg,12985
4
- pico_ioc/api.py,sha256=mVS-Y4amJ_k-GPy9RUldpHNWDcEvj5ZLkf5DimdMzB4,47045
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.2.dist-info/licenses/LICENSE,sha256=N1_nOvHTM6BobYnOTNXiQkroDqCEi6EzfGBv8lWtyZ0,1077
14
- pico_ioc-2.0.2.dist-info/METADATA,sha256=lLkoQGYiIygMjZ9TauYNSZJjRHyGjuaE4e19oUJemx4,8741
15
- pico_ioc-2.0.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
16
- pico_ioc-2.0.2.dist-info/top_level.txt,sha256=_7_RLu616z_dtRw16impXn4Mw8IXe2J4BeX5912m5dQ,9
17
- pico_ioc-2.0.2.dist-info/RECORD,,