pico-ioc 2.0.1__tar.gz → 2.0.3__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.
Files changed (93) hide show
  1. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/CHANGELOG.md +85 -31
  2. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/PKG-INFO +1 -1
  3. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/src/pico_ioc/__init__.py +4 -0
  4. pico_ioc-2.0.3/src/pico_ioc/_version.py +1 -0
  5. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/src/pico_ioc/api.py +63 -29
  6. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/src/pico_ioc/container.py +57 -54
  7. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/src/pico_ioc/exceptions.py +17 -2
  8. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/src/pico_ioc/factory.py +2 -2
  9. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/src/pico_ioc.egg-info/PKG-INFO +1 -1
  10. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/tests/test_pico_extends.py +2 -2
  11. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/tests/test_pico_integration.py +31 -1
  12. pico_ioc-2.0.1/src/pico_ioc/_version.py +0 -1
  13. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/.coveragerc +0 -0
  14. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/.github/workflows/ci.yml +0 -0
  15. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/.github/workflows/publish-to-pypi.yml +0 -0
  16. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/LICENSE +0 -0
  17. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/MANIFEST.in +0 -0
  18. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/README.md +0 -0
  19. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/README.md +0 -0
  20. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/adr/README.md +0 -0
  21. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/adr/adr-0001-async-native.md +0 -0
  22. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/adr/adr-0002-tree-based-configuration.md +0 -0
  23. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/adr/adr-0003-context-aware-scopes.md +0 -0
  24. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/adr/adr-0004-observability.md +0 -0
  25. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/adr/adr-0005-aop.md +0 -0
  26. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/adr/adr-0006-eager-validation.md +0 -0
  27. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/adr/adr-0007-event_bus.md +0 -0
  28. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/adr/adr-0008-circular-dependencies.md +0 -0
  29. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/adr/adr-0009-flexible-provides.md +0 -0
  30. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/advanced-features/README.md +0 -0
  31. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/advanced-features/aop-interceptors.md +0 -0
  32. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/advanced-features/async-resolution.md +0 -0
  33. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/advanced-features/conditional-binding.md +0 -0
  34. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/advanced-features/event-bus.md +0 -0
  35. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/advanced-features/health-checks.md +0 -0
  36. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/api-reference/README.md +0 -0
  37. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/api-reference/container.md +0 -0
  38. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/api-reference/decorators.md +0 -0
  39. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/api-reference/glossary.md +0 -0
  40. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/api-reference/protocols.md +0 -0
  41. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/architecture/README.md +0 -0
  42. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/architecture/comparison.md +0 -0
  43. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/architecture/design-principles.md +0 -0
  44. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/architecture/internals.md +0 -0
  45. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/cookbook/README.md +0 -0
  46. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/cookbook/pattern-aop-feature-toggle.md +0 -0
  47. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/cookbook/pattern-aop-profiling.md +0 -0
  48. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/cookbook/pattern-aop-security.md +0 -0
  49. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/cookbook/pattern-aop-structured-logging.md +0 -0
  50. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/cookbook/pattern-cli-app.md +0 -0
  51. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/cookbook/pattern-cqrs.md +0 -0
  52. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/cookbook/pattern-dynamic-langchain.md +0 -0
  53. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/cookbook/pattern-hot-reload.md +0 -0
  54. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/cookbook/pattern-multi-tenant.md +0 -0
  55. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/getting-started.md +0 -0
  56. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/integrations/README.md +0 -0
  57. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/integrations/ai-langchain.md +0 -0
  58. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/integrations/web-django.md +0 -0
  59. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/integrations/web-fastapi.md +0 -0
  60. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/integrations/web-flask.md +0 -0
  61. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/observability/README.md +0 -0
  62. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/observability/container-context.md +0 -0
  63. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/observability/exporting-graph.md +0 -0
  64. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/observability/observers-metrics.md +0 -0
  65. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/overview.md +0 -0
  66. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/user-guide/README.md +0 -0
  67. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/user-guide/configuration-basic.md +0 -0
  68. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/user-guide/configuration-binding.md +0 -0
  69. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/user-guide/core-concepts.md +0 -0
  70. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/user-guide/qualifiers-lists.md +0 -0
  71. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/user-guide/scopes-lifecycle.md +0 -0
  72. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/user-guide/testing.md +0 -0
  73. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/pyproject.toml +0 -0
  74. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/setup.cfg +0 -0
  75. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/src/pico_ioc/aop.py +0 -0
  76. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/src/pico_ioc/config_runtime.py +0 -0
  77. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/src/pico_ioc/constants.py +0 -0
  78. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/src/pico_ioc/event_bus.py +0 -0
  79. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/src/pico_ioc/locator.py +0 -0
  80. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/src/pico_ioc/scope.py +0 -0
  81. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/src/pico_ioc.egg-info/SOURCES.txt +0 -0
  82. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/src/pico_ioc.egg-info/dependency_links.txt +0 -0
  83. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/src/pico_ioc.egg-info/requires.txt +0 -0
  84. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/src/pico_ioc.egg-info/top_level.txt +0 -0
  85. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/test.txt +0 -0
  86. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/tests/test_configured.py +0 -0
  87. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/tests/test_container_context.py +0 -0
  88. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/tests/test_container_runtime.py +0 -0
  89. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/tests/test_event_bus.py +0 -0
  90. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/tests/test_provides_module_functions.py +0 -0
  91. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/tests/test_provides_static_methods.py +0 -0
  92. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/tests/test_resolution_graph.py +0 -0
  93. {pico_ioc-2.0.1 → pico_ioc-2.0.3}/tox.ini +0 -0
@@ -5,6 +5,91 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.html).
7
7
 
8
+ ---
9
+
10
+ ## [2.0.3] - 2025-10-26
11
+
12
+ ### Fixed
13
+
14
+ - Injection now falls back from an annotated key to the parameter name when the annotated key is unbound, enabling resolution against string-key providers registered via `@provides("name")`.
15
+ - `ProviderNotFoundError` includes the requesting origin (component or key) to aid debugging and test assertions.
16
+ - Unified sync/async resolution path in `PicoContainer`:
17
+ - `get()` raises `AsyncResolutionError` if a provider returns an awaitable, guiding users to `aget()`.
18
+ - `aget()` awaits awaitables and applies aspects and caching consistently.
19
+ - Observers receive accurate resolve timings in both paths.
20
+
21
+ ### Added
22
+
23
+ - `AsyncResolutionError` to signal misuse of `get()` when a provider is async.
24
+ - More informative tracer notes for parameter binding.
25
+
26
+ ### Internal
27
+
28
+ - `ComponentFactory.get()` now accepts an `origin` to enrich `ProviderNotFoundError` messages.
29
+ - Lazy proxy creation calls `factory.get(key, origin="lazy")` to attribute provenance.
30
+ - Public API exports updated to include `AsyncResolutionError`.
31
+
32
+ ### Compatibility
33
+
34
+ - No public API breaking changes. Internal factory signature changed but remains encapsulated within the container/registrar.
35
+
36
+ ---
37
+
38
+ ## [2.0.2] - 2025-10-26
39
+
40
+ ### Fixed 🧩
41
+
42
+ * **`@provides` Decorator Execution**
43
+ Corrected an issue where the `@provides` decorator executed its wrapped function prematurely during module import, leading to runtime errors like `TypeError: Service() takes no arguments`.
44
+ The decorator now properly registers provider metadata without invoking the function until dependency resolution time.
45
+
46
+ ### Added ✨
47
+
48
+ * **`FlatDictSource` Configuration Provider**
49
+ Introduced a lightweight configuration source for flat in-memory dictionaries.
50
+ Supports optional key prefixing and case sensitivity control for simple, programmatic configuration injection.
51
+
52
+ ### Internal 🔧
53
+
54
+ * Updated type imports and registration logic in `api.py` to support `Mapping` for the new configuration source.
55
+ * Added `FlatDictSource` to the public API (`__all__` and import namespace).
56
+
57
+ ### Notes 📝
58
+
59
+ * Fully backward compatible.
60
+ * This patch release focuses on decorator correctness and configuration flexibility improvements.
61
+
62
+ ---
63
+
64
+ ## [2.0.1] - 2025-10-25
65
+
66
+ ### Added ✨
67
+
68
+ - **ADR-0009: Flexible `@provides` Support**
69
+ Implemented support for using `@provides` in additional contexts:
70
+ - `@staticmethod` methods within `@factory` classes
71
+ - `@classmethod` methods within `@factory` classes
72
+ - Module-level functions
73
+ These new provider types are discovered automatically during module scanning and participate fully in dependency resolution, validation, and graph generation.
74
+
75
+ - **Dependency Graph and Validation Enhancements**
76
+ - `_build_resolution_graph` now includes edges for all `@provides` functions, regardless of where they are defined.
77
+ - Fail-fast validation checks now cover static/class/module-level providers, reporting missing bindings consistently.
78
+ - Scope inference and promotion logic apply equally to these new provider types.
79
+
80
+ ### Documentation 📚
81
+
82
+ - Expanded `docs/overview.md` to document the new flexible provider options (`@staticmethod`, `@classmethod`, module-level functions).
83
+ - Updated `docs/guide.md` with practical examples showing when to use each style of provider.
84
+ - Linked ADR-0009 for design rationale and migration guidance.
85
+
86
+ ### Notes 📝
87
+
88
+ - This is a **minor feature release** introducing a major ergonomics improvement (ADR-0009).
89
+ - Fully backward compatible with existing factories, components, and configuration mechanisms.
90
+ - Encourages a lighter, more Pythonic style for simple provider declarations.
91
+
92
+
8
93
  ---
9
94
 
10
95
  ## [2.0.0] - 2025-10-23
@@ -58,37 +143,6 @@ This version marks a significant redesign and the first major public release, es
58
143
  * Added comprehensive test suite covering core features, async behavior, AOP, configuration, scopes, and error handling.
59
144
  * Introduced patterns for testing with overrides and profiles.
60
145
 
61
- ---
62
-
63
- ## [2.0.1] - 2025-10-25
64
-
65
- ### Added ✨
66
-
67
- - **ADR-0009: Flexible `@provides` Support**
68
- Implemented support for using `@provides` in additional contexts:
69
- - `@staticmethod` methods within `@factory` classes
70
- - `@classmethod` methods within `@factory` classes
71
- - Module-level functions
72
- These new provider types are discovered automatically during module scanning and participate fully in dependency resolution, validation, and graph generation.
73
-
74
- - **Dependency Graph and Validation Enhancements**
75
- - `_build_resolution_graph` now includes edges for all `@provides` functions, regardless of where they are defined.
76
- - Fail-fast validation checks now cover static/class/module-level providers, reporting missing bindings consistently.
77
- - Scope inference and promotion logic apply equally to these new provider types.
78
-
79
- ### Documentation 📚
80
-
81
- - Expanded `docs/overview.md` to document the new flexible provider options (`@staticmethod`, `@classmethod`, module-level functions).
82
- - Updated `docs/guide.md` with practical examples showing when to use each style of provider.
83
- - Linked ADR-0009 for design rationale and migration guidance.
84
-
85
- ### Notes 📝
86
-
87
- - This is a **minor feature release** introducing a major ergonomics improvement (ADR-0009).
88
- - Fully backward compatible with existing factories, components, and configuration mechanisms.
89
- - Encourages a lighter, more Pythonic style for simple provider declarations.
90
-
91
-
92
146
  ---
93
147
 
94
148
  ## [<2.0.0]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pico-ioc
3
- Version: 2.0.1
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
@@ -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,
@@ -23,6 +24,7 @@ from .api import (
23
24
  ConfigSource,
24
25
  EnvSource,
25
26
  FileSource,
27
+ FlatDictSource,
26
28
  init,
27
29
  configured,
28
30
  )
@@ -50,6 +52,7 @@ __all__ = [
50
52
  "SerializationError",
51
53
  "ValidationError",
52
54
  "InvalidBindingError",
55
+ "AsyncResolutionError",
53
56
  "EventBusClosedError",
54
57
  "component",
55
58
  "factory",
@@ -76,6 +79,7 @@ __all__ = [
76
79
  "EnvSource",
77
80
  "FileSource",
78
81
  "ConfigSource",
82
+ "FlatDictSource",
79
83
  "init",
80
84
  "configured",
81
85
  "EventBus",
@@ -0,0 +1 @@
1
+ __version__ = '2.0.3'
@@ -6,10 +6,9 @@ import importlib
6
6
  import pkgutil
7
7
  import logging
8
8
  from dataclasses import is_dataclass, fields, dataclass, MISSING
9
- from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Union, get_args, get_origin, Annotated, Protocol
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,
@@ -54,6 +53,31 @@ class FileSource:
54
53
  return str(v)
55
54
  return None
56
55
 
56
+ class FlatDictSource(ConfigSource):
57
+ def __init__(self, data: Mapping[str, Any], prefix: str = "", case_sensitive: bool = True):
58
+ base = dict(data)
59
+ if case_sensitive:
60
+ self._data = {str(k): v for k, v in base.items()}
61
+ self._prefix = prefix
62
+ else:
63
+ self._data = {str(k).upper(): v for k, v in base.items()}
64
+ self._prefix = prefix.upper()
65
+ self._case_sensitive = case_sensitive
66
+
67
+ def get(self, key: str) -> Optional[str]:
68
+ if not key:
69
+ return None
70
+ k = f"{self._prefix}{key}" if self._prefix else key
71
+ if not self._case_sensitive:
72
+ k = k.upper()
73
+ v = self._data.get(k)
74
+ if v is None:
75
+ return None
76
+ if isinstance(v, (str, int, float, bool)):
77
+ return str(v)
78
+ return None
79
+
80
+
57
81
  def _meta_get(obj: Any) -> Dict[str, Any]:
58
82
  m = getattr(obj, PICO_META, None)
59
83
  if m is None:
@@ -132,29 +156,21 @@ def factory(
132
156
  return c
133
157
  return dec(cls) if cls else dec
134
158
 
135
- def provides(
136
- key: Any,
137
- *,
138
- name: Any = None,
139
- qualifiers: Iterable[str] = (),
140
- scope: str = "singleton",
141
- primary: bool = False,
142
- lazy: bool = False,
143
- conditional_profiles: Iterable[str] = (),
144
- conditional_require_env: Iterable[str] = (),
145
- conditional_predicate: Optional[Callable[[], bool]] = None,
146
- on_missing_selector: Optional[object] = None,
147
- on_missing_priority: int = 0,
148
- ):
149
- def dec(fn):
159
+ def provides(*dargs, **dkwargs):
160
+ def _apply(fn, key_hint, *, name=None, qualifiers=(), scope="singleton", primary=False, lazy=False, conditional_profiles=(), conditional_require_env=(), conditional_predicate=None, on_missing_selector=None, on_missing_priority=0):
150
161
  target = fn.__func__ if isinstance(fn, (staticmethod, classmethod)) else fn
151
- @functools.wraps(target)
152
- def w(*a, **k):
153
- return target(*a, **k)
154
- setattr(w, PICO_INFRA, "provides")
155
- setattr(w, PICO_NAME, name if name is not None else key)
156
- setattr(w, PICO_KEY, key)
157
- m = _meta_get(w)
162
+ inferred_key = key_hint
163
+ if inferred_key is MISSING:
164
+ rt = _get_return_type(target)
165
+ if isinstance(rt, type):
166
+ inferred_key = rt
167
+ else:
168
+ inferred_key = getattr(target, "__name__", str(target))
169
+ setattr(target, PICO_INFRA, "provides")
170
+ pico_name = name if name is not None else (inferred_key if isinstance(inferred_key, str) else getattr(target, "__name__", str(target)))
171
+ setattr(target, PICO_NAME, pico_name)
172
+ setattr(target, PICO_KEY, inferred_key)
173
+ m = _meta_get(target)
158
174
  m["qualifier"] = tuple(str(q) for q in qualifiers or ())
159
175
  m["scope"] = scope
160
176
  if primary:
@@ -169,8 +185,16 @@ def provides(
169
185
  }
170
186
  if on_missing_selector is not None:
171
187
  m["on_missing"] = {"selector": on_missing_selector, "priority": int(on_missing_priority)}
172
- return w
173
- return dec
188
+ return fn
189
+
190
+ if dargs and len(dargs) == 1 and inspect.isfunction(dargs[0]) and not dkwargs:
191
+ fn = dargs[0]
192
+ return _apply(fn, MISSING)
193
+ else:
194
+ key = dargs[0] if dargs else MISSING
195
+ def _decorator(fn):
196
+ return _apply(fn, key, **dkwargs)
197
+ return _decorator
174
198
 
175
199
  class Qualifier(str):
176
200
  __slots__ = ()
@@ -384,8 +408,18 @@ def _resolve_args(callable_obj: Callable[..., Any], pico: "PicoContainer") -> Di
384
408
  try:
385
409
  for kind, name, data in plan:
386
410
  if kind == "key":
387
- tracer.note_param(data if isinstance(data, (str, type)) else name, name)
388
- 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
389
423
  else:
390
424
  vals = [pico.get(k) for k in data]
391
425
  kwargs[name] = vals
@@ -549,7 +583,7 @@ class Registrar:
549
583
  deferred.attach(pico, locator)
550
584
  for key, md in list(self._metadata.items()):
551
585
  if md.lazy:
552
- original = self._factory.get(key)
586
+ original = self._factory.get(key, origin='lazy')
553
587
  def lazy_proxy_provider(_orig=original, _p=pico):
554
588
  return UnifiedComponentProxy(container=_p, object_creator=_orig)
555
589
  self._factory.bind(key, lazy_proxy_provider)
@@ -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:
@@ -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)
@@ -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.1
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
@@ -188,12 +188,12 @@ def test_provider_not_found_error():
188
188
  container = init(types.ModuleType("empty_mod"))
189
189
  with pytest.raises(ProviderNotFoundError) as e_str:
190
190
  container.get("non_existent_key")
191
- assert "Provider not found for key: non_existent_key" in str(e_str.value)
191
+ assert "Provider for key 'non_existent_key' not found" in str(e_str.value)
192
192
  class NonExistentClass:
193
193
  pass
194
194
  with pytest.raises(ProviderNotFoundError) as e_type:
195
195
  container.get(NonExistentClass)
196
- assert "Provider not found for key: NonExistentClass" in str(e_type.value)
196
+ assert "Provider for key 'NonExistentClass' not found" in str(e_type.value)
197
197
 
198
198
  def test_configuration_error_missing_value():
199
199
  config_module = types.ModuleType("config_test_mod")
@@ -1,7 +1,9 @@
1
+ # tests/test_pico_integration.py
1
2
  import pytest
2
3
  import os
3
4
  import json
4
5
  import contextvars
6
+ import types
5
7
  from dataclasses import dataclass
6
8
  from typing import List, Optional, Annotated, Callable, Any, Protocol
7
9
  import logging
@@ -193,6 +195,35 @@ for item in definitions:
193
195
 
194
196
  test_scopes = {"request": request_scope}
195
197
 
198
+ class BaseNamedService:
199
+ def get_name(self) -> str:
200
+ return "Base"
201
+
202
+ class ConcreteNamedService(BaseNamedService):
203
+ def get_name(self) -> str:
204
+ return "ConcreteViaStringKey"
205
+
206
+ @component
207
+ class NeedsNamedServiceByTypeWithFallback:
208
+ def __init__(self, named_service: BaseNamedService):
209
+ self.injected_service = named_service
210
+
211
+ def build_fallback_test_module():
212
+ m = types.ModuleType("fallback_test_module")
213
+ @provides("named_service")
214
+ def build_concrete() -> ConcreteNamedService:
215
+ return ConcreteNamedService()
216
+ setattr(m, "NeedsNamedServiceByTypeWithFallback", NeedsNamedServiceByTypeWithFallback)
217
+ setattr(m, "build_concrete", build_concrete)
218
+ return m
219
+
220
+ def test_resolve_fallback_to_parameter_name():
221
+ test_mod = build_fallback_test_module()
222
+ container = init(modules=[test_mod])
223
+ instance = container.get(NeedsNamedServiceByTypeWithFallback)
224
+ assert isinstance(instance.injected_service, ConcreteNamedService)
225
+ assert instance.injected_service.get_name() == "ConcreteViaStringKey"
226
+
196
227
  def test_basic_di():
197
228
  container = init(test_module)
198
229
  instance_b = container.get(ServiceBImpl)
@@ -314,4 +345,3 @@ def test_cleanup_called():
314
345
  container.cleanup_all()
315
346
  assert holder.closed
316
347
  assert "ResourceHolder closed" in log_capture
317
-
@@ -1 +0,0 @@
1
- __version__ = '2.0.1'
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes