pico-ioc 2.0.2__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.
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/CHANGELOG.md +82 -54
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/PKG-INFO +1 -1
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/src/pico_ioc/__init__.py +2 -0
- pico_ioc-2.0.3/src/pico_ioc/_version.py +1 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/src/pico_ioc/api.py +13 -4
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/src/pico_ioc/container.py +57 -54
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/src/pico_ioc/exceptions.py +17 -2
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/src/pico_ioc/factory.py +2 -2
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/src/pico_ioc.egg-info/PKG-INFO +1 -1
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/tests/test_pico_extends.py +2 -2
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/tests/test_pico_integration.py +31 -1
- pico_ioc-2.0.2/src/pico_ioc/_version.py +0 -1
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/.coveragerc +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/.github/workflows/ci.yml +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/.github/workflows/publish-to-pypi.yml +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/LICENSE +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/MANIFEST.in +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/README.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/README.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/adr/README.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/adr/adr-0001-async-native.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/adr/adr-0002-tree-based-configuration.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/adr/adr-0003-context-aware-scopes.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/adr/adr-0004-observability.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/adr/adr-0005-aop.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/adr/adr-0006-eager-validation.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/adr/adr-0007-event_bus.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/adr/adr-0008-circular-dependencies.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/adr/adr-0009-flexible-provides.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/advanced-features/README.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/advanced-features/aop-interceptors.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/advanced-features/async-resolution.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/advanced-features/conditional-binding.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/advanced-features/event-bus.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/advanced-features/health-checks.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/api-reference/README.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/api-reference/container.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/api-reference/decorators.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/api-reference/glossary.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/api-reference/protocols.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/architecture/README.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/architecture/comparison.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/architecture/design-principles.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/architecture/internals.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/cookbook/README.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/cookbook/pattern-aop-feature-toggle.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/cookbook/pattern-aop-profiling.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/cookbook/pattern-aop-security.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/cookbook/pattern-aop-structured-logging.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/cookbook/pattern-cli-app.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/cookbook/pattern-cqrs.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/cookbook/pattern-dynamic-langchain.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/cookbook/pattern-hot-reload.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/cookbook/pattern-multi-tenant.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/getting-started.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/integrations/README.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/integrations/ai-langchain.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/integrations/web-django.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/integrations/web-fastapi.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/integrations/web-flask.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/observability/README.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/observability/container-context.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/observability/exporting-graph.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/observability/observers-metrics.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/overview.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/user-guide/README.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/user-guide/configuration-basic.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/user-guide/configuration-binding.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/user-guide/core-concepts.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/user-guide/qualifiers-lists.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/user-guide/scopes-lifecycle.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/docs/user-guide/testing.md +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/pyproject.toml +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/setup.cfg +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/src/pico_ioc/aop.py +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/src/pico_ioc/config_runtime.py +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/src/pico_ioc/constants.py +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/src/pico_ioc/event_bus.py +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/src/pico_ioc/locator.py +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/src/pico_ioc/scope.py +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/src/pico_ioc.egg-info/SOURCES.txt +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/src/pico_ioc.egg-info/dependency_links.txt +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/src/pico_ioc.egg-info/requires.txt +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/src/pico_ioc.egg-info/top_level.txt +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/test.txt +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/tests/test_configured.py +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/tests/test_container_context.py +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/tests/test_container_runtime.py +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/tests/test_event_bus.py +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/tests/test_provides_module_functions.py +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/tests/test_provides_static_methods.py +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/tests/test_resolution_graph.py +0 -0
- {pico_ioc-2.0.2 → pico_ioc-2.0.3}/tox.ini +0 -0
|
@@ -7,56 +7,57 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.h
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
-
## [2.0.
|
|
10
|
+
## [2.0.3] - 2025-10-26
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
### Fixed
|
|
13
13
|
|
|
14
|
-
|
|
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.
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
* **Tree-Based Configuration:** Added `@configured` decorator and `TreeSource` protocol for binding complex, nested configuration (YAML/JSON) to dataclass graphs, including interpolation and type coercion (ADR-002).
|
|
18
|
-
* **Context-Aware Scopes:** Implemented `contextvars`-based scopes (e.g., `"request"`, `"session"`) for managing component lifecycles tied to specific contexts (ADR-003).
|
|
19
|
-
* **Observability Features:** Integrated container context (`container_id`, `as_current`), basic stats (`container.stats()`), observer protocol (`ContainerObserver`), and dependency graph export (`container.export_graph()`) (ADR-004).
|
|
20
|
-
* **Aspect-Oriented Programming (AOP):** Implemented method interception via `MethodInterceptor` protocol and `@intercepted_by` decorator, using a dynamic proxy (`UnifiedComponentProxy`) (ADR-005).
|
|
21
|
-
* **Eager Startup Validation:** Added fail-fast validation during `init()` to detect missing dependencies and configuration errors before runtime (ADR-006).
|
|
22
|
-
* **Built-in Event Bus:** Included an asynchronous, in-process event bus (`EventBus`, `@subscribe`, `AutoSubscriberMixin`) for decoupled communication (ADR-007).
|
|
23
|
-
* **Explicit Circular Dependency Handling:** Implemented detection and fail-fast for circular dependencies, requiring explicit resolution patterns (ADR-008).
|
|
24
|
-
* **Unified Decorator API:** Consolidated component metadata into parameterized decorators (`@component`, `@factory`, `@provides`), removing older stacked decorators (ADR-009).
|
|
21
|
+
### Added
|
|
25
22
|
|
|
26
|
-
|
|
23
|
+
- `AsyncResolutionError` to signal misuse of `get()` when a provider is async.
|
|
24
|
+
- More informative tracer notes for parameter binding.
|
|
27
25
|
|
|
28
|
-
|
|
29
|
-
* Configuration decorators: `@configuration` (flat key-value) and `@configured` (tree-based).
|
|
30
|
-
* Lifecycle decorators: `@configure`, `@cleanup`.
|
|
31
|
-
* AOP decorator: `@intercepted_by`.
|
|
32
|
-
* Event bus decorator: `@subscribe`.
|
|
33
|
-
* Health check decorator: `@health`.
|
|
34
|
-
* Async resolution: `container.aget()` and `__ainit__` convention.
|
|
35
|
-
* Async cleanup: `container.cleanup_all_async()`.
|
|
36
|
-
* Qualifier support (`Qualifier` class) for list injection (`Annotated[List[Type], Qualifier(...)]`).
|
|
37
|
-
* Support for `lazy=True` parameter for deferred component instantiation.
|
|
38
|
-
* Conditional binding parameters (`conditional_profiles`, `conditional_require_env`, `conditional_predicate`).
|
|
39
|
-
* Fallback binding parameters (`on_missing_selector`, `on_missing_priority`).
|
|
40
|
-
* Primary selection parameter (`primary=True`).
|
|
41
|
-
* Testing support via `init(overrides={...})` and `init(profiles=(...))`.
|
|
42
|
-
* Container context management (`as_current`, `get_current`, `shutdown`, `all_containers`).
|
|
43
|
-
* Scope management API (`activate_scope`, `deactivate_scope`, `scope` context manager).
|
|
44
|
-
* Configuration sources: `EnvSource`, `FileSource` (flat); `JsonTreeSource`, `YamlTreeSource`, `DictSource` (tree).
|
|
45
|
-
* Protocols for extension: `MethodInterceptor`, `ContainerObserver`, `ScopeProtocol`, `ConfigSource`, `TreeSource`.
|
|
26
|
+
### Internal
|
|
46
27
|
|
|
47
|
-
|
|
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`.
|
|
48
31
|
|
|
49
|
-
|
|
50
|
-
* Requires Python 3.10+.
|
|
32
|
+
### Compatibility
|
|
51
33
|
|
|
52
|
-
|
|
34
|
+
- No public API breaking changes. Internal factory signature changed but remains encapsulated within the container/registrar.
|
|
53
35
|
|
|
54
|
-
|
|
36
|
+
---
|
|
55
37
|
|
|
56
|
-
|
|
38
|
+
## [2.0.2] - 2025-10-26
|
|
57
39
|
|
|
58
|
-
|
|
59
|
-
|
|
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.
|
|
60
61
|
|
|
61
62
|
---
|
|
62
63
|
|
|
@@ -91,29 +92,56 @@ This version marks a significant redesign and the first major public release, es
|
|
|
91
92
|
|
|
92
93
|
---
|
|
93
94
|
|
|
94
|
-
## [2.0.
|
|
95
|
+
## [2.0.0] - 2025-10-23
|
|
95
96
|
|
|
96
|
-
|
|
97
|
+
This version marks a significant redesign and the first major public release, establishing the core architecture and feature set based on the principles outlined in the Architecture Decision Records (ADRs).
|
|
97
98
|
|
|
98
|
-
|
|
99
|
-
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`.
|
|
100
|
-
The decorator now properly registers provider metadata without invoking the function until dependency resolution time.
|
|
99
|
+
### 🚀 Highlights
|
|
101
100
|
|
|
102
|
-
|
|
101
|
+
* **Async-Native Core:** Introduced first-class `async`/`await` support across component resolution (`container.aget`), initialization (`__ainit__`), lifecycle hooks (`@configure`, `@cleanup`), AOP interceptors, and the event bus (ADR-001).
|
|
102
|
+
* **Tree-Based Configuration:** Added `@configured` decorator and `TreeSource` protocol for binding complex, nested configuration (YAML/JSON) to dataclass graphs, including interpolation and type coercion (ADR-002).
|
|
103
|
+
* **Context-Aware Scopes:** Implemented `contextvars`-based scopes (e.g., `"request"`, `"session"`) for managing component lifecycles tied to specific contexts (ADR-003).
|
|
104
|
+
* **Observability Features:** Integrated container context (`container_id`, `as_current`), basic stats (`container.stats()`), observer protocol (`ContainerObserver`), and dependency graph export (`container.export_graph()`) (ADR-004).
|
|
105
|
+
* **Aspect-Oriented Programming (AOP):** Implemented method interception via `MethodInterceptor` protocol and `@intercepted_by` decorator, using a dynamic proxy (`UnifiedComponentProxy`) (ADR-005).
|
|
106
|
+
* **Eager Startup Validation:** Added fail-fast validation during `init()` to detect missing dependencies and configuration errors before runtime (ADR-006).
|
|
107
|
+
* **Built-in Event Bus:** Included an asynchronous, in-process event bus (`EventBus`, `@subscribe`, `AutoSubscriberMixin`) for decoupled communication (ADR-007).
|
|
108
|
+
* **Explicit Circular Dependency Handling:** Implemented detection and fail-fast for circular dependencies, requiring explicit resolution patterns (ADR-008).
|
|
109
|
+
* **Unified Decorator API:** Consolidated component metadata into parameterized decorators (`@component`, `@factory`, `@provides`), removing older stacked decorators (ADR-009).
|
|
103
110
|
|
|
104
|
-
|
|
105
|
-
Introduced a lightweight configuration source for flat in-memory dictionaries.
|
|
106
|
-
Supports optional key prefixing and case sensitivity control for simple, programmatic configuration injection.
|
|
111
|
+
### ✨ Added
|
|
107
112
|
|
|
108
|
-
|
|
113
|
+
* Core registration decorators: `@component`, `@factory`, `@provides`.
|
|
114
|
+
* Configuration decorators: `@configuration` (flat key-value) and `@configured` (tree-based).
|
|
115
|
+
* Lifecycle decorators: `@configure`, `@cleanup`.
|
|
116
|
+
* AOP decorator: `@intercepted_by`.
|
|
117
|
+
* Event bus decorator: `@subscribe`.
|
|
118
|
+
* Health check decorator: `@health`.
|
|
119
|
+
* Async resolution: `container.aget()` and `__ainit__` convention.
|
|
120
|
+
* Async cleanup: `container.cleanup_all_async()`.
|
|
121
|
+
* Qualifier support (`Qualifier` class) for list injection (`Annotated[List[Type], Qualifier(...)]`).
|
|
122
|
+
* Support for `lazy=True` parameter for deferred component instantiation.
|
|
123
|
+
* Conditional binding parameters (`conditional_profiles`, `conditional_require_env`, `conditional_predicate`).
|
|
124
|
+
* Fallback binding parameters (`on_missing_selector`, `on_missing_priority`).
|
|
125
|
+
* Primary selection parameter (`primary=True`).
|
|
126
|
+
* Testing support via `init(overrides={...})` and `init(profiles=(...))`.
|
|
127
|
+
* Container context management (`as_current`, `get_current`, `shutdown`, `all_containers`).
|
|
128
|
+
* Scope management API (`activate_scope`, `deactivate_scope`, `scope` context manager).
|
|
129
|
+
* Configuration sources: `EnvSource`, `FileSource` (flat); `JsonTreeSource`, `YamlTreeSource`, `DictSource` (tree).
|
|
130
|
+
* Protocols for extension: `MethodInterceptor`, `ContainerObserver`, `ScopeProtocol`, `ConfigSource`, `TreeSource`.
|
|
109
131
|
|
|
110
|
-
|
|
111
|
-
* Added `FlatDictSource` to the public API (`__all__` and import namespace).
|
|
132
|
+
### ⚠️ Breaking Changes
|
|
112
133
|
|
|
113
|
-
|
|
134
|
+
* Complete redesign compared to any prior internal/unreleased versions. APIs are not backward compatible.
|
|
135
|
+
* Requires Python 3.10+.
|
|
114
136
|
|
|
115
|
-
|
|
116
|
-
|
|
137
|
+
### 📚 Docs
|
|
138
|
+
|
|
139
|
+
* Established new documentation structure including ADRs, Architecture, User Guide, Advanced Features, Cookbook, Integrations, and API Reference.
|
|
140
|
+
|
|
141
|
+
### 🧪 Testing
|
|
142
|
+
|
|
143
|
+
* Added comprehensive test suite covering core features, async behavior, AOP, configuration, scopes, and error handling.
|
|
144
|
+
* Introduced patterns for testing with overrides and profiles.
|
|
117
145
|
|
|
118
146
|
---
|
|
119
147
|
|
|
@@ -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",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '2.0.3'
|
|
@@ -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
|
-
|
|
413
|
-
|
|
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)
|
|
@@ -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
|
-
|
|
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
|
-
|
|
192
|
-
|
|
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
|
-
|
|
204
|
+
instance_or_awaitable = provider()
|
|
202
205
|
except ProviderNotFoundError as e:
|
|
203
206
|
raise
|
|
204
|
-
except Exception as
|
|
205
|
-
raise ComponentCreationError(key,
|
|
206
|
-
|
|
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
|
-
|
|
211
|
-
|
|
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
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
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:
|
|
@@ -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
|
|
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
|
|
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.2'
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|