pico-ioc 2.1.2__tar.gz → 2.1.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.1.2 → pico_ioc-2.1.3}/CHANGELOG.md +62 -0
- {pico_ioc-2.1.2/src/pico_ioc.egg-info → pico_ioc-2.1.3}/PKG-INFO +10 -1
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/README.md +9 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/adr/adr-0002-tree-based-configuration.md +2 -1
- pico_ioc-2.1.3/docs/adr/adr-0003-context-aware-scopes.md +107 -0
- pico_ioc-2.1.3/docs/advanced-features/async-resolution.md +465 -0
- pico_ioc-2.1.3/docs/user-guide/scopes-lifecycle.md +571 -0
- pico_ioc-2.1.3/src/pico_ioc/_version.py +1 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/src/pico_ioc/analysis.py +3 -4
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/src/pico_ioc/aop.py +52 -17
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/src/pico_ioc/config_runtime.py +5 -2
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/src/pico_ioc/container.py +16 -6
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/src/pico_ioc/event_bus.py +22 -19
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/src/pico_ioc/registrar.py +2 -2
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/src/pico_ioc/scope.py +21 -14
- {pico_ioc-2.1.2 → pico_ioc-2.1.3/src/pico_ioc.egg-info}/PKG-INFO +10 -1
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/src/pico_ioc.egg-info/SOURCES.txt +1 -0
- pico_ioc-2.1.3/tests/test_aop.py +121 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/tests/test_pico_extends.py +7 -4
- pico_ioc-2.1.3/tests/test_scope.py +128 -0
- pico_ioc-2.1.2/docs/adr/adr-0003-context-aware-scopes.md +0 -96
- pico_ioc-2.1.2/docs/advanced-features/async-resolution.md +0 -230
- pico_ioc-2.1.2/docs/user-guide/scopes-lifecycle.md +0 -274
- pico_ioc-2.1.2/src/pico_ioc/_version.py +0 -1
- pico_ioc-2.1.2/tests/test_scope.py +0 -310
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/.coveragerc +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/.github/workflows/ci.yml +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/.github/workflows/docs.yml +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/.github/workflows/publish-to-pypi.yml +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/LICENSE +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/MANIFEST.in +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/LEARN.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/README.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/adr/README.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/adr/adr-0001-async-native.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/adr/adr-0004-observability.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/adr/adr-0005-aop.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/adr/adr-0006-eager-validation.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/adr/adr-0007-event_bus.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/adr/adr-0008-circular-dependencies.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/adr/adr-0009-flexible-provides.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/adr/adr-0010-unified-configuration.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/advanced-features/README.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/advanced-features/aop-interceptors.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/advanced-features/conditional-binding.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/advanced-features/event-bus.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/advanced-features/health-checks.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/api-reference/README.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/api-reference/container.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/api-reference/decorators.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/api-reference/event_bus.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/api-reference/glossary.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/api-reference/protocols.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/architecture/README.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/architecture/comparison.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/architecture/design-principles.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/architecture/internals.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/cookbook/README.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/cookbook/pattern-aop-feature-toggle.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/cookbook/pattern-aop-profiling.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/cookbook/pattern-aop-security.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/cookbook/pattern-aop-structured-logging.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/cookbook/pattern-cli-app.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/cookbook/pattern-config-overrides.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/cookbook/pattern-cqrs.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/cookbook/pattern-hot-reload.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/cookbook/pattern-multi-tenant.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/getting-started.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/javascripts/extra.js +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/observability/README.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/observability/container-context.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/observability/exporting-graph.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/observability/observers-metrics.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/overview.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/requirements.txt +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/stylesheets/extra.css +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/user-guide/README.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/user-guide/configuration-basic.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/user-guide/configuration-binding.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/user-guide/core-concepts.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/user-guide/qualifiers-lists.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/docs/user-guide/testing.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/mkdocs.yml +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/pyproject.toml +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/setup.cfg +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/src/pico_ioc/__init__.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/src/pico_ioc/api.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/src/pico_ioc/component_scanner.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/src/pico_ioc/config_builder.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/src/pico_ioc/config_registrar.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/src/pico_ioc/constants.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/src/pico_ioc/decorators.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/src/pico_ioc/dependency_validator.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/src/pico_ioc/exceptions.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/src/pico_ioc/factory.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/src/pico_ioc/locator.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/src/pico_ioc/provider_selector.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/src/pico_ioc.egg-info/dependency_links.txt +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/src/pico_ioc.egg-info/requires.txt +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/src/pico_ioc.egg-info/top_level.txt +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/tests/test_collection_injection.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/tests/test_config_value.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/tests/test_configured.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/tests/test_container_context.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/tests/test_container_runtime.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/tests/test_container_self_injection.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/tests/test_event_bus.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/tests/test_pico_integration.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/tests/test_protocol_resolution_and_graph.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/tests/test_provides_module_functions.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/tests/test_provides_static_methods.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/tests/test_proxy_unit.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/tests/test_resolution_graph.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.1.3}/tox.ini +0 -0
|
@@ -7,6 +7,68 @@ and this project adheres to Semantic Versioning (https://semver.org/spec/v2.0.ht
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [2.1.3] - 2025-11-18
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Critical:** Removed unsafe LRU eviction in `ScopedCaches`. Previously, under high concurrency (e.g., >2048 requests/websockets), active scopes could be evicted, causing data loss and premature cleanup. Scopes now persist until explicitly cleaned up.
|
|
14
|
+
- **Critical:** Fixed a race condition in `UnifiedComponentProxy` (AOP) where the underlying object creation wasn't fully thread-safe.
|
|
15
|
+
- **Critical:** Fixed a race condition in `EventBus.post()` where the queue reference could be lost during shutdown.
|
|
16
|
+
- **Critical:** Fixed `ScopeManager` returning a bucket for `None` IDs, which could cause state leakage between threads/tasks outside of an active context. Now raises `ScopeError`.
|
|
17
|
+
- Fixed `AsyncResolutionError` when accessing `lazy=True` components that require asynchronous `@configure` methods. `aget()` now hydrates proxies immediately if needed.
|
|
18
|
+
- Fixed a swallowed exception in `analyze_callable_dependencies` that hid configuration errors. Now logs debug information.
|
|
19
|
+
- Fixed recursion error in `UnifiedComponentProxy.__setattr__` when setting internal attributes.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- **Breaking Behavior:** Users using `pico-fastapi` or manual scope management **must** ensure `container.cleanup_scope(...)` is called at the end of the lifecycle to prevent memory leaks, as the automatic LRU safety net has been removed in favor of data integrity.
|
|
23
|
+
- Improved integer configuration parsing to support formats like `1_000` or scientific notation in `config_runtime.py`.
|
|
24
|
+
- `init()` now fails fast with an `AsyncResolutionError` if a component returns an awaitable from a synchronous `@configure` method, instead of just logging a warning.
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
- Architectural support for asynchronous hydration of lazy proxies via `_async_init_if_needed`.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## [2.1.2] - 2025-11-10
|
|
32
|
+
|
|
33
|
+
### Added ✨
|
|
34
|
+
|
|
35
|
+
* **Collection & Mapping Injection:** Constructors can now request collection-like dependencies and dictionaries:
|
|
36
|
+
|
|
37
|
+
* Supported collection origins: `List`, `Set`, `Iterable`, `Sequence`, `Collection`, `Deque` (resolved as concrete **lists** at injection time).
|
|
38
|
+
* Supported mappings: `Dict[K, V]` / `Mapping[K, V]` where `K ∈ {str, type, Any}` and `V` is a component/protocol.
|
|
39
|
+
* `Annotated[..., Qualifier("q")]` on the element type is honored for both collections and dict values.
|
|
40
|
+
* **Element-type analysis for dicts:** Dependency analysis records the dict key type and value element type for correct resolution.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
|
|
44
|
+
* **Analyzer:** `analyze_callable_dependencies` now recognizes a broader set of `collections.abc` origins for collections and supports dict/mapping shapes, including qualifier propagation from `Annotated` element types.
|
|
45
|
+
* **Container:** Dictionary injection computes keys from component metadata:
|
|
46
|
+
|
|
47
|
+
* `Dict[str, V]` → uses `pico_name` (or string key/fallback to class `__name__`).
|
|
48
|
+
* `Dict[type, V]` → uses the concrete class (or provided type).
|
|
49
|
+
* `Dict[Any, V]` → chooses sensible defaults (`pico_name` → string key → class name).
|
|
50
|
+
* **Type imports & internals:** Expanded typing/runtime imports to support the new analysis and resolution paths.
|
|
51
|
+
|
|
52
|
+
### Fixed 🧩
|
|
53
|
+
|
|
54
|
+
* **Protocol matching:** `ComponentLocator` now checks attribute presence (including annotated attributes), reducing false positives when matching `Protocol` types.
|
|
55
|
+
* **Resolution guard:** `_resolve_args` safely no-ops when the locator is unavailable, avoiding edge-case errors during early initialization.
|
|
56
|
+
|
|
57
|
+
### Docs 📚
|
|
58
|
+
|
|
59
|
+
* **README:** Removed the deprecated *Integrations* entry from the docs index.
|
|
60
|
+
* **Architecture:** Corrected ADR links to `../adr/README.md` and references to the ADR workflow.
|
|
61
|
+
|
|
62
|
+
### Tests 🧪
|
|
63
|
+
|
|
64
|
+
* **New:** `tests/test_collection_injection.py` covering:
|
|
65
|
+
|
|
66
|
+
* Analyzer plans for collections/dicts (including qualifiers).
|
|
67
|
+
* Container resolution of lists/sets/iterables/sequences/deques as lists.
|
|
68
|
+
* Dictionary injection for `Dict[str, V]` and `Dict[type, V]`.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
10
72
|
## [2.1.1] - 2025-11-02
|
|
11
73
|
|
|
12
74
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pico-ioc
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.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
|
|
@@ -124,6 +124,15 @@ Optional extras:
|
|
|
124
124
|
|
|
125
125
|
-----
|
|
126
126
|
|
|
127
|
+
### ⚠️ Important Note for v2.1.3+
|
|
128
|
+
|
|
129
|
+
**Breaking Behavior in Custom Integrations:**
|
|
130
|
+
As of version 2.1.3, **Scope LRU Eviction has been removed** to guarantee data integrity under high load.
|
|
131
|
+
* **If you use `pico-fastapi`:** You are safe (the middleware handles cleanup automatically).
|
|
132
|
+
* **If you perform manual scope management:** You **must** explicitly call `container._caches.cleanup_scope("scope_name", scope_id)` when a context ends. Failing to do so will result in a memory leak, as scopes are no longer automatically discarded when the container fills up.
|
|
133
|
+
|
|
134
|
+
-----
|
|
135
|
+
|
|
127
136
|
## ⚙️ Quick Example (Unified Configuration)
|
|
128
137
|
|
|
129
138
|
```python
|
|
@@ -74,6 +74,15 @@ Optional extras:
|
|
|
74
74
|
|
|
75
75
|
-----
|
|
76
76
|
|
|
77
|
+
### ⚠️ Important Note for v2.1.3+
|
|
78
|
+
|
|
79
|
+
**Breaking Behavior in Custom Integrations:**
|
|
80
|
+
As of version 2.1.3, **Scope LRU Eviction has been removed** to guarantee data integrity under high load.
|
|
81
|
+
* **If you use `pico-fastapi`:** You are safe (the middleware handles cleanup automatically).
|
|
82
|
+
* **If you perform manual scope management:** You **must** explicitly call `container._caches.cleanup_scope("scope_name", scope_id)` when a context ends. Failing to do so will result in a memory leak, as scopes are no longer automatically discarded when the container fills up.
|
|
83
|
+
|
|
84
|
+
-----
|
|
85
|
+
|
|
77
86
|
## ⚙️ Quick Example (Unified Configuration)
|
|
78
87
|
|
|
79
88
|
```python
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# ADR-002: Tree-Based Configuration Binding
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> ⚠️ **DEPRECATED**: This decision has been superseded by ADR-010 (Unified Configuration).
|
|
4
|
+
> This document is kept for historical context only. Please refer to the User Guide.
|
|
4
5
|
|
|
5
6
|
Note: While the core concepts of tree-binding logic (`ConfigResolver`, `ObjectGraphBuilder`) and using `@configured` for nested structures remain valid, ADR-0010 unified the configuration system. The mechanism described here using a separate `init(tree_config=...)` argument is no longer current. Configuration sources (including tree sources like `YamlTreeSource`) are now passed to the `configuration(...)` builder, and the resulting `ContextConfig` object is passed to `init(config=...)`. The `@configured` decorator now handles both flat and tree mapping via its `mapping` parameter.
|
|
6
7
|
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# ADR-003: Context-Aware Scopes
|
|
2
|
+
|
|
3
|
+
Status: Accepted
|
|
4
|
+
|
|
5
|
+
## Update (v2.1.3) - Removal of LRU
|
|
6
|
+
|
|
7
|
+
The original decision to use LRU (Least Recently Used) eviction for scoped caches (see point 5 below) proved unsafe for high-concurrency scenarios, such as WebSockets or long-polling. In these cases, active but "quiet" connections could be arbitrarily evicted when the container reached its limit, causing data loss (amnesia) for ongoing sessions.
|
|
8
|
+
|
|
9
|
+
**Decision:** The LRU logic was removed in version 2.1.3.
|
|
10
|
+
**New Behavior:** Scoped caches are now unbounded to guarantee data integrity for all active contexts.
|
|
11
|
+
**Requirement:** Integrators (e.g., web middleware or manual context managers) **must** explicitly call `container._caches.cleanup_scope(...)` at the end of the lifecycle to prevent memory leaks.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
### Context
|
|
16
|
+
|
|
17
|
+
Many applications, especially web services, need components whose lifecycle is tied to a specific context, like an individual HTTP request or a user's session. Imagine needing a unique `UserContext` object for each logged-in user making requests simultaneously. The standard `singleton` (one instance forever) and `prototype` (new instance every time) scopes aren't suitable for managing state within these temporary contexts. We needed a way to create "scoped singletons" – ensuring exactly one instance per active context.
|
|
18
|
+
|
|
19
|
+
### Decision
|
|
20
|
+
|
|
21
|
+
We introduced context-aware scopes built upon Python's `contextvars`:
|
|
22
|
+
|
|
23
|
+
1) **ScopeProtocol:** Defined a minimal interface for custom scope implementations, requiring only `get_id() -> Any | None` to return the identifier of the currently active scope instance.
|
|
24
|
+
|
|
25
|
+
2) **ContextVarScope:** Provided a standard implementation using `contextvars.ContextVar`. This allows tracking the active scope ID within async tasks and threads correctly. It includes helper methods like `activate(id)` and `deactivate(token)`.
|
|
26
|
+
|
|
27
|
+
3) **ScopeManager:** An internal registry holding `ScopeProtocol` implementations for named scopes (e.g., "request", "session"). It provides the core logic for activating, deactivating, and retrieving the current ID for any registered scope.
|
|
28
|
+
|
|
29
|
+
4) **`scope="..."` parameter:** Components are assigned to a specific scope using the `scope` parameter within the main registration decorators (`@component`, `@factory`, `@provides`). For example: `@component(scope="request")`.
|
|
30
|
+
|
|
31
|
+
5) **ScopedCaches:** Modified the internal caching mechanism to handle multiple caches keyed by `(scope_name, scope_id)`.
|
|
32
|
+
* *Historical Note:* Originally, this used an LRU strategy to automatically evict caches. As of v2.1.3, this is an unbounded registry requiring explicit cleanup.
|
|
33
|
+
|
|
34
|
+
6) **Container API:** Added convenient methods for managing scopes:
|
|
35
|
+
- `container.activate_scope(name, id)` and `container.deactivate_scope(name, token)` for manual control.
|
|
36
|
+
- The preferred `with container.scope(name, id):` context manager for easy, safe activation and deactivation within a block.
|
|
37
|
+
- Pre-registered common web scopes like "request", "session", and "transaction" for convenience.
|
|
38
|
+
|
|
39
|
+
### Usage
|
|
40
|
+
|
|
41
|
+
- **Registering a scoped component:**
|
|
42
|
+
- Use `@component(scope="request")` to ensure one instance per active request scope.
|
|
43
|
+
- Factories and providers support scope in the same way: `@factory(scope="session")`, `@provides(scope="transaction")`.
|
|
44
|
+
|
|
45
|
+
- **Activating a scope:**
|
|
46
|
+
- Prefer the context manager: `with container.scope("request", request_id):` resolve components and handle work inside the block.
|
|
47
|
+
- Manual control:
|
|
48
|
+
- `token = container.activate_scope("session", session_id)`
|
|
49
|
+
- ...resolve and use components...
|
|
50
|
+
- `container.deactivate_scope("session", token)`
|
|
51
|
+
|
|
52
|
+
- **Custom scopes:**
|
|
53
|
+
- Define a custom scope by implementing `ScopeProtocol` (providing `get_id()`) or by using `ContextVarScope`.
|
|
54
|
+
- Register at initialization via `init(custom_scopes={"tenant": ContextVarScope(...)})`.
|
|
55
|
+
- Use `@component(scope="tenant")` for components that should be unique per tenant.
|
|
56
|
+
|
|
57
|
+
- **Async compatibility:**
|
|
58
|
+
- Scopes rely on `contextvars`, so active scope IDs propagate correctly across `asyncio` tasks spawned within the same context.
|
|
59
|
+
- For thread pools or manual threading, activate the scope within each worker execution or ensure framework integration propagates context.
|
|
60
|
+
|
|
61
|
+
### Implementation Notes
|
|
62
|
+
|
|
63
|
+
- **Scope identifiers:**
|
|
64
|
+
- Scope IDs must be stable and hashable; they are used as part of the cache key (scope_name, scope_id).
|
|
65
|
+
- Choose IDs that uniquely represent the current context (e.g., a UUID for requests, a user/session ID for sessions).
|
|
66
|
+
|
|
67
|
+
- **Cache management:**
|
|
68
|
+
- `ScopedCaches` maintains a distinct cache per `(scope_name, scope_id)` pairing.
|
|
69
|
+
- *Updated v2.1.3:* Cleanup is manual via `cleanup_scope`.
|
|
70
|
+
|
|
71
|
+
- **Safety:**
|
|
72
|
+
- The context manager ensures scopes are correctly deactivated even if exceptions occur inside the block.
|
|
73
|
+
- Manual activate/deactivate requires pairing the token returned by `activate_scope` with `deactivate_scope` to avoid leaks.
|
|
74
|
+
|
|
75
|
+
### Consequences
|
|
76
|
+
|
|
77
|
+
**Positive:**
|
|
78
|
+
- Enables safe and isolated management of context-specific state (e.g., holding data for a single web request without interfering with others).
|
|
79
|
+
- Integrates naturally with `asyncio` due to the use of `contextvars`, making it suitable for modern async web frameworks.
|
|
80
|
+
- Provides a clean and developer-friendly API for activating/deactivating scopes, especially the `with container.scope():` manager.
|
|
81
|
+
- Extensible: Users can define and register their own custom scopes via `init(custom_scopes={...})`.
|
|
82
|
+
|
|
83
|
+
**Negative:**
|
|
84
|
+
- Relies on `contextvars`, which requires careful handling, especially regarding context propagation across thread boundaries if not managed automatically by frameworks.
|
|
85
|
+
- Requires explicit scope activation/deactivation in the application's entry points (e.g., web middleware). The container itself doesn't automatically detect the start/end of a request; the framework integration needs to call the container's scope methods.
|
|
86
|
+
- **Memory Management:** Without the automatic LRU (removed in v2.1.3), there is a risk of memory leaks if the integration layer fails to call `cleanup_scope` after the context ends.
|
|
87
|
+
|
|
88
|
+
### Alternatives Considered
|
|
89
|
+
|
|
90
|
+
- **Thread-local storage:**
|
|
91
|
+
- Rejected due to poor compatibility with `asyncio` and cross-thread propagation issues in modern Python web runtimes.
|
|
92
|
+
|
|
93
|
+
- **Global registries keyed by request/session:**
|
|
94
|
+
- Rejected for complexity, manual cleanup requirements, and weaker isolation compared to `contextvars`.
|
|
95
|
+
|
|
96
|
+
- **Prototype-only components:**
|
|
97
|
+
- Rejected because they cannot guarantee a single instance per context, leading to duplicated state and higher allocation overhead.
|
|
98
|
+
|
|
99
|
+
### Migration
|
|
100
|
+
|
|
101
|
+
- **Existing singleton components that should be per-request or per-session:**
|
|
102
|
+
- Add `scope="request"` or `scope="session"` to their registration decorators.
|
|
103
|
+
- Ensure the application activates the corresponding scope in middleware/entry points.
|
|
104
|
+
|
|
105
|
+
- **Framework integration:**
|
|
106
|
+
- Web frameworks should activate the "request" scope at the start of request handling and deactivate it at the end.
|
|
107
|
+
- **Update:** Frameworks must also ensure `container._caches.cleanup_scope("request", id)` is called in a `finally` block.
|
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
# Advanced: Async Resolution (`aget`, `__ainit__`)
|
|
2
|
+
|
|
3
|
+
Modern Python applications are increasingly built on `asyncio`. `pico-ioc` is async-native, meaning it fully supports asynchronous operations throughout the component lifecycle, from creation to cleanup.
|
|
4
|
+
|
|
5
|
+
This guide covers how to:
|
|
6
|
+
|
|
7
|
+
* Resolve components asynchronously using `container.aget()`.
|
|
8
|
+
* Define components that require `await` during their creation.
|
|
9
|
+
* Use asynchronous lifecycle hooks like `@cleanup` and `@configure`.
|
|
10
|
+
|
|
11
|
+
-----
|
|
12
|
+
|
|
13
|
+
## 1\. `container.aget()`: The Async `get()`
|
|
14
|
+
|
|
15
|
+
If you are in an `async` function, you should always use `container.aget()` instead of `container.get()`.
|
|
16
|
+
|
|
17
|
+
* `container.get()`: Synchronous. Blocks the event loop if a component needs to be created.
|
|
18
|
+
* `container.aget()`: Asynchronous. Properly awaits the creation of any async components, ensuring the event loop is never blocked.
|
|
19
|
+
|
|
20
|
+
<!-- end list -->
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from pico_ioc import component, init
|
|
24
|
+
|
|
25
|
+
@component
|
|
26
|
+
class MyAsyncService:
|
|
27
|
+
...
|
|
28
|
+
|
|
29
|
+
async def main():
|
|
30
|
+
container = init(modules=[__name__])
|
|
31
|
+
|
|
32
|
+
# Use .aget() inside an async function
|
|
33
|
+
service = await container.aget(MyAsyncService)
|
|
34
|
+
|
|
35
|
+
# This would be bad! It could block.
|
|
36
|
+
# service = container.get(MyAsyncService)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
-----
|
|
40
|
+
|
|
41
|
+
## 2\. Asynchronous Component Creation
|
|
42
|
+
|
|
43
|
+
Your components often need to perform I/O during their initialization (e.g., connect to a database, call an API). `pico-ioc` supports this in two primary ways.
|
|
44
|
+
|
|
45
|
+
### Method 1: Async Factory (`async def @provides`)
|
|
46
|
+
|
|
47
|
+
The cleanest way to create an async component is with a factory. You can decorate an `async def` method with `@provides`. `pico-ioc` will automatically await it when it's resolved via `container.aget()`.
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
import asyncio
|
|
51
|
+
from pico_ioc import component, factory, provides, init
|
|
52
|
+
|
|
53
|
+
# A mock async database client
|
|
54
|
+
class AsyncDatabase:
|
|
55
|
+
def __init__(self):
|
|
56
|
+
self.connected = True
|
|
57
|
+
print("Database connected")
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
async def connect(url: str):
|
|
61
|
+
print(f"Connecting to {url}...")
|
|
62
|
+
await asyncio.sleep(0.01) # Mock I/O
|
|
63
|
+
return AsyncDatabase()
|
|
64
|
+
|
|
65
|
+
@factory
|
|
66
|
+
class DatabaseFactory:
|
|
67
|
+
|
|
68
|
+
# Use 'async def' with @provides
|
|
69
|
+
@provides(AsyncDatabase)
|
|
70
|
+
async def build_db(self) -> AsyncDatabase:
|
|
71
|
+
db = await AsyncDatabase.connect("postgres://...")
|
|
72
|
+
return db
|
|
73
|
+
|
|
74
|
+
@component
|
|
75
|
+
class UserService:
|
|
76
|
+
def __init__(self, db: AsyncDatabase):
|
|
77
|
+
self.db = db
|
|
78
|
+
|
|
79
|
+
# --- In your main async function ---
|
|
80
|
+
async def main():
|
|
81
|
+
container = init(modules=[__name__])
|
|
82
|
+
|
|
83
|
+
# .aget() will correctly await the build_db() factory
|
|
84
|
+
user_service = await container.aget(UserService)
|
|
85
|
+
|
|
86
|
+
assert user_service.db.connected is True
|
|
87
|
+
|
|
88
|
+
# Output:
|
|
89
|
+
# Connecting to postgres://...
|
|
90
|
+
# Database connected
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Method 2: Async Constructor (`__ainit__`)
|
|
94
|
+
|
|
95
|
+
You cannot make `__init__` an `async def` method in Python.
|
|
96
|
+
|
|
97
|
+
To solve this, `pico-ioc` supports a special method: `__ainit__`.
|
|
98
|
+
|
|
99
|
+
If you define an `async def __ainit__` method on a `@component` class, `pico-ioc` will automatically call and await it immediately after `__init__` is finished.
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
import asyncio
|
|
103
|
+
from pico_ioc import component, init
|
|
104
|
+
|
|
105
|
+
@component
|
|
106
|
+
class AsyncService:
|
|
107
|
+
def __init__(self):
|
|
108
|
+
# __init__ remains synchronous
|
|
109
|
+
self.connected = False
|
|
110
|
+
print("Service __init__ (sync)")
|
|
111
|
+
|
|
112
|
+
async def __ainit__(self):
|
|
113
|
+
# This is where you put your async setup code
|
|
114
|
+
print("Service __ainit__ (async) starting...")
|
|
115
|
+
await asyncio.sleep(0.01) # Mock I/O
|
|
116
|
+
self.connected = True
|
|
117
|
+
print("Service __ainit__ finished.")
|
|
118
|
+
|
|
119
|
+
# --- In your main async function ---
|
|
120
|
+
async def main():
|
|
121
|
+
container = init(modules=[__name__])
|
|
122
|
+
|
|
123
|
+
# .aget() will call __init__() and then await __ainit__()
|
|
124
|
+
service = await container.aget(AsyncService)
|
|
125
|
+
|
|
126
|
+
assert service.connected is True
|
|
127
|
+
|
|
128
|
+
# Output:
|
|
129
|
+
# Service __init__ (sync)
|
|
130
|
+
# Service __ainit__ (async) starting...
|
|
131
|
+
# Service __ainit__ finished.
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`__ainit__` can also have its own dependencies injected, just like `@configure`:
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from pico_ioc import component, init
|
|
138
|
+
|
|
139
|
+
class AsyncDatabase:
|
|
140
|
+
async def ping(self): ...
|
|
141
|
+
|
|
142
|
+
@component
|
|
143
|
+
class DependsOnDB:
|
|
144
|
+
def __init__(self):
|
|
145
|
+
self.connected = False
|
|
146
|
+
|
|
147
|
+
async def __ainit__(self, db: AsyncDatabase):
|
|
148
|
+
await db.ping()
|
|
149
|
+
self.connected = True
|
|
150
|
+
|
|
151
|
+
async def main():
|
|
152
|
+
container = init(modules=[__name__])
|
|
153
|
+
service = await container.aget(DependsOnDB)
|
|
154
|
+
assert service.connected is True
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
-----
|
|
158
|
+
|
|
159
|
+
## 3\. Asynchronous Lifecycle Hooks
|
|
160
|
+
|
|
161
|
+
The `@configure` and `@cleanup` decorators also work with `async def` methods.
|
|
162
|
+
|
|
163
|
+
* `async def @configure`: Called and awaited after `__ainit__`.
|
|
164
|
+
* `async def @cleanup`: Called and awaited by `container.cleanup_all_async()`.
|
|
165
|
+
|
|
166
|
+
This is essential for gracefully shutting down async resources.
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from pico_ioc import component, configure, cleanup, init
|
|
170
|
+
|
|
171
|
+
@component
|
|
172
|
+
class AsyncConnectionPool:
|
|
173
|
+
async def __ainit__(self):
|
|
174
|
+
self.pool = await self.create_pool()
|
|
175
|
+
print("Pool created")
|
|
176
|
+
|
|
177
|
+
@configure
|
|
178
|
+
async def warmup(self):
|
|
179
|
+
# Optional post-init async setup
|
|
180
|
+
print("Warming up pool...")
|
|
181
|
+
await self.pool.prepare()
|
|
182
|
+
print("Pool warm")
|
|
183
|
+
|
|
184
|
+
@cleanup
|
|
185
|
+
async def close_pool(self):
|
|
186
|
+
# Use async def with @cleanup
|
|
187
|
+
print("Closing pool (async)...")
|
|
188
|
+
await self.pool.close()
|
|
189
|
+
print("Pool closed.")
|
|
190
|
+
|
|
191
|
+
async def create_pool(self):
|
|
192
|
+
# Mock implementation
|
|
193
|
+
class Pool:
|
|
194
|
+
async def prepare(self): ...
|
|
195
|
+
async def close(self): ...
|
|
196
|
+
return Pool()
|
|
197
|
+
|
|
198
|
+
# --- In your main async function ---
|
|
199
|
+
async def main():
|
|
200
|
+
container = init(modules=[__name__])
|
|
201
|
+
pool = await container.aget(AsyncConnectionPool)
|
|
202
|
+
|
|
203
|
+
print("Application shutting down...")
|
|
204
|
+
|
|
205
|
+
# You MUST call the async version of cleanup
|
|
206
|
+
await container.cleanup_all_async()
|
|
207
|
+
|
|
208
|
+
# Output:
|
|
209
|
+
# Pool created
|
|
210
|
+
# Warming up pool...
|
|
211
|
+
# Pool warm
|
|
212
|
+
# Application shutting down...
|
|
213
|
+
# Closing pool (async)...
|
|
214
|
+
# Pool closed.
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
-----
|
|
218
|
+
|
|
219
|
+
## Summary
|
|
220
|
+
|
|
221
|
+
* Always use `container.aget()` from within an `async` function.
|
|
222
|
+
* Use `async def @provides` in a factory for async creation logic.
|
|
223
|
+
* Use `async def __ainit__` on a `@component` for async initialization logic; it can receive injected dependencies.
|
|
224
|
+
* Use `async def @configure` for post-initialization setup.
|
|
225
|
+
* Use `async def @cleanup` and `container.cleanup_all_async()` to gracefully release async resources.
|
|
226
|
+
|
|
227
|
+
-----
|
|
228
|
+
|
|
229
|
+
## Next Steps
|
|
230
|
+
|
|
231
|
+
Now that you understand how to build and resolve components asynchronously, let's look at a powerful pattern for separating your application's concerns.
|
|
232
|
+
|
|
233
|
+
* AOP & Interceptors: Learn how to intercept method calls for logging, tracing, or caching. See: ./aop-interceptors.md# Advanced: Async Resolution (`aget`, `__ainit__`)
|
|
234
|
+
|
|
235
|
+
Modern Python applications are increasingly built on `asyncio`. `pico-ioc` is async-native, meaning it fully supports asynchronous operations throughout the component lifecycle, from creation to cleanup.
|
|
236
|
+
|
|
237
|
+
This guide covers how to:
|
|
238
|
+
|
|
239
|
+
* Resolve components asynchronously using `container.aget()`.
|
|
240
|
+
* Define components that require `await` during their creation.
|
|
241
|
+
* Use asynchronous lifecycle hooks like `@cleanup` and `@configure`.
|
|
242
|
+
|
|
243
|
+
-----
|
|
244
|
+
|
|
245
|
+
## 1\. `container.aget()`: The Async `get()`
|
|
246
|
+
|
|
247
|
+
If you are in an `async` function, you should always use `container.aget()` instead of `container.get()`.
|
|
248
|
+
|
|
249
|
+
* `container.get()`: Synchronous. Blocks the event loop if a component needs to be created.
|
|
250
|
+
* `container.aget()`: Asynchronous. Properly awaits the creation of any async components, ensuring the event loop is never blocked.
|
|
251
|
+
|
|
252
|
+
<!-- end list -->
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
from pico_ioc import component, init
|
|
256
|
+
|
|
257
|
+
@component
|
|
258
|
+
class MyAsyncService:
|
|
259
|
+
...
|
|
260
|
+
|
|
261
|
+
async def main():
|
|
262
|
+
container = init(modules=[__name__])
|
|
263
|
+
|
|
264
|
+
# Use .aget() inside an async function
|
|
265
|
+
service = await container.aget(MyAsyncService)
|
|
266
|
+
|
|
267
|
+
# This would be bad! It could block.
|
|
268
|
+
# service = container.get(MyAsyncService)
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
-----
|
|
272
|
+
|
|
273
|
+
## 2\. Asynchronous Component Creation
|
|
274
|
+
|
|
275
|
+
Your components often need to perform I/O during their initialization (e.g., connect to a database, call an API). `pico-ioc` supports this in two primary ways.
|
|
276
|
+
|
|
277
|
+
### Method 1: Async Factory (`async def @provides`)
|
|
278
|
+
|
|
279
|
+
The cleanest way to create an async component is with a factory. You can decorate an `async def` method with `@provides`. `pico-ioc` will automatically await it when it's resolved via `container.aget()`.
|
|
280
|
+
|
|
281
|
+
```python
|
|
282
|
+
import asyncio
|
|
283
|
+
from pico_ioc import component, factory, provides, init
|
|
284
|
+
|
|
285
|
+
# A mock async database client
|
|
286
|
+
class AsyncDatabase:
|
|
287
|
+
def __init__(self):
|
|
288
|
+
self.connected = True
|
|
289
|
+
print("Database connected")
|
|
290
|
+
|
|
291
|
+
@staticmethod
|
|
292
|
+
async def connect(url: str):
|
|
293
|
+
print(f"Connecting to {url}...")
|
|
294
|
+
await asyncio.sleep(0.01) # Mock I/O
|
|
295
|
+
return AsyncDatabase()
|
|
296
|
+
|
|
297
|
+
@factory
|
|
298
|
+
class DatabaseFactory:
|
|
299
|
+
|
|
300
|
+
# Use 'async def' with @provides
|
|
301
|
+
@provides(AsyncDatabase)
|
|
302
|
+
async def build_db(self) -> AsyncDatabase:
|
|
303
|
+
db = await AsyncDatabase.connect("postgres://...")
|
|
304
|
+
return db
|
|
305
|
+
|
|
306
|
+
@component
|
|
307
|
+
class UserService:
|
|
308
|
+
def __init__(self, db: AsyncDatabase):
|
|
309
|
+
self.db = db
|
|
310
|
+
|
|
311
|
+
# --- In your main async function ---
|
|
312
|
+
async def main():
|
|
313
|
+
container = init(modules=[__name__])
|
|
314
|
+
|
|
315
|
+
# .aget() will correctly await the build_db() factory
|
|
316
|
+
user_service = await container.aget(UserService)
|
|
317
|
+
|
|
318
|
+
assert user_service.db.connected is True
|
|
319
|
+
|
|
320
|
+
# Output:
|
|
321
|
+
# Connecting to postgres://...
|
|
322
|
+
# Database connected
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Method 2: Async Constructor (`__ainit__`)
|
|
326
|
+
|
|
327
|
+
You cannot make `__init__` an `async def` method in Python.
|
|
328
|
+
|
|
329
|
+
To solve this, `pico-ioc` supports a special method: `__ainit__`.
|
|
330
|
+
|
|
331
|
+
If you define an `async def __ainit__` method on a `@component` class, `pico-ioc` will automatically call and await it immediately after `__init__` is finished.
|
|
332
|
+
|
|
333
|
+
```python
|
|
334
|
+
import asyncio
|
|
335
|
+
from pico_ioc import component, init
|
|
336
|
+
|
|
337
|
+
@component
|
|
338
|
+
class AsyncService:
|
|
339
|
+
def __init__(self):
|
|
340
|
+
# __init__ remains synchronous
|
|
341
|
+
self.connected = False
|
|
342
|
+
print("Service __init__ (sync)")
|
|
343
|
+
|
|
344
|
+
async def __ainit__(self):
|
|
345
|
+
# This is where you put your async setup code
|
|
346
|
+
print("Service __ainit__ (async) starting...")
|
|
347
|
+
await asyncio.sleep(0.01) # Mock I/O
|
|
348
|
+
self.connected = True
|
|
349
|
+
print("Service __ainit__ finished.")
|
|
350
|
+
|
|
351
|
+
# --- In your main async function ---
|
|
352
|
+
async def main():
|
|
353
|
+
container = init(modules=[__name__])
|
|
354
|
+
|
|
355
|
+
# .aget() will call __init__() and then await __ainit__()
|
|
356
|
+
service = await container.aget(AsyncService)
|
|
357
|
+
|
|
358
|
+
assert service.connected is True
|
|
359
|
+
|
|
360
|
+
# Output:
|
|
361
|
+
# Service __init__ (sync)
|
|
362
|
+
# Service __ainit__ (async) starting...
|
|
363
|
+
# Service __ainit__ finished.
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
`__ainit__` can also have its own dependencies injected, just like `@configure`:
|
|
367
|
+
|
|
368
|
+
```python
|
|
369
|
+
from pico_ioc import component, init
|
|
370
|
+
|
|
371
|
+
class AsyncDatabase:
|
|
372
|
+
async def ping(self): ...
|
|
373
|
+
|
|
374
|
+
@component
|
|
375
|
+
class DependsOnDB:
|
|
376
|
+
def __init__(self):
|
|
377
|
+
self.connected = False
|
|
378
|
+
|
|
379
|
+
async def __ainit__(self, db: AsyncDatabase):
|
|
380
|
+
await db.ping()
|
|
381
|
+
self.connected = True
|
|
382
|
+
|
|
383
|
+
async def main():
|
|
384
|
+
container = init(modules=[__name__])
|
|
385
|
+
service = await container.aget(DependsOnDB)
|
|
386
|
+
assert service.connected is True
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
-----
|
|
390
|
+
|
|
391
|
+
## 3\. Asynchronous Lifecycle Hooks
|
|
392
|
+
|
|
393
|
+
The `@configure` and `@cleanup` decorators also work with `async def` methods.
|
|
394
|
+
|
|
395
|
+
* `async def @configure`: Called and awaited after `__ainit__`.
|
|
396
|
+
* `async def @cleanup`: Called and awaited by `container.cleanup_all_async()`.
|
|
397
|
+
|
|
398
|
+
This is essential for gracefully shutting down async resources.
|
|
399
|
+
|
|
400
|
+
```python
|
|
401
|
+
from pico_ioc import component, configure, cleanup, init
|
|
402
|
+
|
|
403
|
+
@component
|
|
404
|
+
class AsyncConnectionPool:
|
|
405
|
+
async def __ainit__(self):
|
|
406
|
+
self.pool = await self.create_pool()
|
|
407
|
+
print("Pool created")
|
|
408
|
+
|
|
409
|
+
@configure
|
|
410
|
+
async def warmup(self):
|
|
411
|
+
# Optional post-init async setup
|
|
412
|
+
print("Warming up pool...")
|
|
413
|
+
await self.pool.prepare()
|
|
414
|
+
print("Pool warm")
|
|
415
|
+
|
|
416
|
+
@cleanup
|
|
417
|
+
async def close_pool(self):
|
|
418
|
+
# Use async def with @cleanup
|
|
419
|
+
print("Closing pool (async)...")
|
|
420
|
+
await self.pool.close()
|
|
421
|
+
print("Pool closed.")
|
|
422
|
+
|
|
423
|
+
async def create_pool(self):
|
|
424
|
+
# Mock implementation
|
|
425
|
+
class Pool:
|
|
426
|
+
async def prepare(self): ...
|
|
427
|
+
async def close(self): ...
|
|
428
|
+
return Pool()
|
|
429
|
+
|
|
430
|
+
# --- In your main async function ---
|
|
431
|
+
async def main():
|
|
432
|
+
container = init(modules=[__name__])
|
|
433
|
+
pool = await container.aget(AsyncConnectionPool)
|
|
434
|
+
|
|
435
|
+
print("Application shutting down...")
|
|
436
|
+
|
|
437
|
+
# You MUST call the async version of cleanup
|
|
438
|
+
await container.cleanup_all_async()
|
|
439
|
+
|
|
440
|
+
# Output:
|
|
441
|
+
# Pool created
|
|
442
|
+
# Warming up pool...
|
|
443
|
+
# Pool warm
|
|
444
|
+
# Application shutting down...
|
|
445
|
+
# Closing pool (async)...
|
|
446
|
+
# Pool closed.
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
-----
|
|
450
|
+
|
|
451
|
+
## Summary
|
|
452
|
+
|
|
453
|
+
* Always use `container.aget()` from within an `async` function.
|
|
454
|
+
* Use `async def @provides` in a factory for async creation logic.
|
|
455
|
+
* Use `async def __ainit__` on a `@component` for async initialization logic; it can receive injected dependencies.
|
|
456
|
+
* Use `async def @configure` for post-initialization setup.
|
|
457
|
+
* Use `async def @cleanup` and `container.cleanup_all_async()` to gracefully release async resources.
|
|
458
|
+
|
|
459
|
+
-----
|
|
460
|
+
|
|
461
|
+
## Next Steps
|
|
462
|
+
|
|
463
|
+
Now that you understand how to build and resolve components asynchronously, let's look at a powerful pattern for separating your application's concerns.
|
|
464
|
+
|
|
465
|
+
* AOP & Interceptors: Learn how to intercept method calls for logging, tracing, or caching. See: ./aop-interceptors.md
|