pico-ioc 2.1.2__tar.gz → 2.2.0__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.2.0}/CHANGELOG.md +79 -0
- pico_ioc-2.2.0/MANIFEST.in +18 -0
- {pico_ioc-2.1.2/src/pico_ioc.egg-info → pico_ioc-2.2.0}/PKG-INFO +52 -47
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/README.md +50 -45
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/adr/README.md +1 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/adr/adr-0002-tree-based-configuration.md +2 -1
- pico_ioc-2.2.0/docs/adr/adr-0003-context-aware-scopes.md +107 -0
- pico_ioc-2.2.0/docs/adr/adr-0011-custom-scanners.md +81 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/advanced-features/README.md +1 -1
- pico_ioc-2.2.0/docs/advanced-features/aop-proxies.md +274 -0
- pico_ioc-2.2.0/docs/advanced-features/async-resolution.md +465 -0
- pico_ioc-2.2.0/docs/advanced-features/custom-scanners.md +133 -0
- pico_ioc-2.2.0/docs/advanced-features/self-injection.md +94 -0
- pico_ioc-2.2.0/docs/api-reference/analysis-api.md +181 -0
- pico_ioc-2.2.0/docs/api-reference/constants.md +142 -0
- pico_ioc-2.2.0/docs/api-reference/container-context-api.md +119 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/api-reference/container.md +15 -1
- pico_ioc-2.2.0/docs/api-reference/exceptions.md +269 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/api-reference/protocols.md +27 -0
- pico_ioc-2.2.0/docs/api-reference/provider-selector.md +162 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/architecture/design-principles.md +4 -1
- pico_ioc-2.2.0/docs/architecture/docs-assets.md +235 -0
- pico_ioc-2.2.0/docs/architecture/runtime-model-scheduling.md +217 -0
- pico_ioc-2.2.0/docs/observability/resolution-graph.md +188 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/user-guide/README.md +1 -0
- pico_ioc-2.2.0/docs/user-guide/scopes-lifecycle.md +571 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/mkdocs.yml +16 -1
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/pyproject.toml +19 -2
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/src/pico_ioc/__init__.py +2 -1
- pico_ioc-2.2.0/src/pico_ioc/_version.py +1 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/src/pico_ioc/analysis.py +3 -4
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/src/pico_ioc/aop.py +52 -17
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/src/pico_ioc/api.py +9 -3
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/src/pico_ioc/component_scanner.py +48 -24
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/src/pico_ioc/config_runtime.py +5 -2
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/src/pico_ioc/container.py +20 -6
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/src/pico_ioc/event_bus.py +22 -19
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/src/pico_ioc/registrar.py +5 -7
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/src/pico_ioc/scope.py +22 -16
- {pico_ioc-2.1.2 → pico_ioc-2.2.0/src/pico_ioc.egg-info}/PKG-INFO +52 -47
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/src/pico_ioc.egg-info/SOURCES.txt +14 -0
- pico_ioc-2.2.0/tests/test_aop.py +121 -0
- pico_ioc-2.2.0/tests/test_custom_scanner.py +63 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/tests/test_pico_extends.py +7 -4
- pico_ioc-2.2.0/tests/test_scope.py +128 -0
- pico_ioc-2.1.2/MANIFEST.in +0 -24
- 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.2.0}/.coveragerc +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/.github/workflows/ci.yml +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/.github/workflows/docs.yml +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/.github/workflows/publish-to-pypi.yml +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/LICENSE +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/LEARN.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/README.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/adr/adr-0001-async-native.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/adr/adr-0004-observability.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/adr/adr-0005-aop.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/adr/adr-0006-eager-validation.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/adr/adr-0007-event_bus.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/adr/adr-0008-circular-dependencies.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/adr/adr-0009-flexible-provides.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/adr/adr-0010-unified-configuration.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/advanced-features/aop-interceptors.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/advanced-features/conditional-binding.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/advanced-features/event-bus.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/advanced-features/health-checks.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/api-reference/README.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/api-reference/decorators.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/api-reference/event_bus.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/api-reference/glossary.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/architecture/README.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/architecture/comparison.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/architecture/internals.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/cookbook/README.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/cookbook/pattern-aop-feature-toggle.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/cookbook/pattern-aop-profiling.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/cookbook/pattern-aop-security.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/cookbook/pattern-aop-structured-logging.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/cookbook/pattern-cli-app.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/cookbook/pattern-config-overrides.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/cookbook/pattern-cqrs.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/cookbook/pattern-hot-reload.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/cookbook/pattern-multi-tenant.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/getting-started.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/javascripts/extra.js +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/observability/README.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/observability/container-context.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/observability/exporting-graph.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/observability/observers-metrics.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/overview.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/requirements.txt +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/stylesheets/extra.css +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/user-guide/configuration-basic.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/user-guide/configuration-binding.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/user-guide/core-concepts.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/user-guide/qualifiers-lists.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/docs/user-guide/testing.md +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/setup.cfg +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/src/pico_ioc/config_builder.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/src/pico_ioc/config_registrar.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/src/pico_ioc/constants.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/src/pico_ioc/decorators.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/src/pico_ioc/dependency_validator.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/src/pico_ioc/exceptions.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/src/pico_ioc/factory.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/src/pico_ioc/locator.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/src/pico_ioc/provider_selector.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/src/pico_ioc.egg-info/dependency_links.txt +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/src/pico_ioc.egg-info/requires.txt +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/src/pico_ioc.egg-info/top_level.txt +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/tests/test_collection_injection.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/tests/test_config_value.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/tests/test_configured.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/tests/test_container_context.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/tests/test_container_runtime.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/tests/test_container_self_injection.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/tests/test_event_bus.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/tests/test_pico_integration.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/tests/test_protocol_resolution_and_graph.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/tests/test_provides_module_functions.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/tests/test_provides_static_methods.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/tests/test_proxy_unit.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/tests/test_resolution_graph.py +0 -0
- {pico_ioc-2.1.2 → pico_ioc-2.2.0}/tox.ini +0 -0
|
@@ -7,6 +7,85 @@ and this project adheres to Semantic Versioning (https://semver.org/spec/v2.0.ht
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [2.2.0] - 2025-11-29
|
|
11
|
+
|
|
12
|
+
### Added ✨
|
|
13
|
+
- **Extensible Component Scanning (ADR-0011):** Introduced the `CustomScanner` protocol and `custom_scanners` parameter in `init()`. This opens the discovery phase to third-party extensions, allowing registration of components based on custom decorators or base classes.
|
|
14
|
+
- **Function-First Scanning:** Updated `ComponentScanner` to evaluate `CustomScanner` logic against *all* module members (classes and functions) before applying native logic. This enables extensions to register standalone functions (e.g., `@task`) as components.
|
|
15
|
+
- **Async Shutdown:** Added `container.ashutdown()` (awaitable). This fills a critical gap in the async lifecycle, ensuring that `async def @cleanup` methods are properly awaited during application teardown.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- **Internal Cleanup:** Removed the deprecated `max_scopes_per_type` parameter from `ScopedCaches`, reflecting the removal of LRU eviction in v2.1.3.
|
|
19
|
+
|
|
20
|
+
### Docs 📚
|
|
21
|
+
- **Major Restructuring:** Rebuilt `mkdocs.yml` navigation to include all available documentation pages.
|
|
22
|
+
- **New Content:** Added missing guides for **Integrations** (Celery), **Advanced Features** (Self-Injection, AOP Proxies, Custom Scanners), and **Architecture** (Runtime Model).
|
|
23
|
+
- **Corrections:** Fixed discrepancies in asset filenames (`extra.js`) and standardized the format of Architecture Decision Records (ADRs).
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## [2.1.3] - 2025-11-18
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
- **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.
|
|
31
|
+
- **Critical:** Fixed a race condition in `UnifiedComponentProxy` (AOP) where the underlying object creation wasn't fully thread-safe.
|
|
32
|
+
- **Critical:** Fixed a race condition in `EventBus.post()` where the queue reference could be lost during shutdown.
|
|
33
|
+
- **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`.
|
|
34
|
+
- Fixed `AsyncResolutionError` when accessing `lazy=True` components that require asynchronous `@configure` methods. `aget()` now hydrates proxies immediately if needed.
|
|
35
|
+
- Fixed a swallowed exception in `analyze_callable_dependencies` that hid configuration errors. Now logs debug information.
|
|
36
|
+
- Fixed recursion error in `UnifiedComponentProxy.__setattr__` when setting internal attributes.
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
- **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.
|
|
40
|
+
- Improved integer configuration parsing to support formats like `1_000` or scientific notation in `config_runtime.py`.
|
|
41
|
+
- `init()` now fails fast with an `AsyncResolutionError` if a component returns an awaitable from a synchronous `@configure` method, instead of just logging a warning.
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
- Architectural support for asynchronous hydration of lazy proxies via `_async_init_if_needed`.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## [2.1.2] - 2025-11-10
|
|
49
|
+
|
|
50
|
+
### Added ✨
|
|
51
|
+
|
|
52
|
+
* **Collection & Mapping Injection:** Constructors can now request collection-like dependencies and dictionaries:
|
|
53
|
+
|
|
54
|
+
* Supported collection origins: `List`, `Set`, `Iterable`, `Sequence`, `Collection`, `Deque` (resolved as concrete **lists** at injection time).
|
|
55
|
+
* Supported mappings: `Dict[K, V]` / `Mapping[K, V]` where `K ∈ {str, type, Any}` and `V` is a component/protocol.
|
|
56
|
+
* `Annotated[..., Qualifier("q")]` on the element type is honored for both collections and dict values.
|
|
57
|
+
* **Element-type analysis for dicts:** Dependency analysis records the dict key type and value element type for correct resolution.
|
|
58
|
+
|
|
59
|
+
### Changed
|
|
60
|
+
|
|
61
|
+
* **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.
|
|
62
|
+
* **Container:** Dictionary injection computes keys from component metadata:
|
|
63
|
+
|
|
64
|
+
* `Dict[str, V]` → uses `pico_name` (or string key/fallback to class `__name__`).
|
|
65
|
+
* `Dict[type, V]` → uses the concrete class (or provided type).
|
|
66
|
+
* `Dict[Any, V]` → chooses sensible defaults (`pico_name` → string key → class name).
|
|
67
|
+
* **Type imports & internals:** Expanded typing/runtime imports to support the new analysis and resolution paths.
|
|
68
|
+
|
|
69
|
+
### Fixed 🧩
|
|
70
|
+
|
|
71
|
+
* **Protocol matching:** `ComponentLocator` now checks attribute presence (including annotated attributes), reducing false positives when matching `Protocol` types.
|
|
72
|
+
* **Resolution guard:** `_resolve_args` safely no-ops when the locator is unavailable, avoiding edge-case errors during early initialization.
|
|
73
|
+
|
|
74
|
+
### Docs 📚
|
|
75
|
+
|
|
76
|
+
* **README:** Removed the deprecated *Integrations* entry from the docs index.
|
|
77
|
+
* **Architecture:** Corrected ADR links to `../adr/README.md` and references to the ADR workflow.
|
|
78
|
+
|
|
79
|
+
### Tests 🧪
|
|
80
|
+
|
|
81
|
+
* **New:** `tests/test_collection_injection.py` covering:
|
|
82
|
+
|
|
83
|
+
* Analyzer plans for collections/dicts (including qualifiers).
|
|
84
|
+
* Container resolution of lists/sets/iterables/sequences/deques as lists.
|
|
85
|
+
* Dictionary injection for `Dict[str, V]` and `Dict[type, V]`.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
10
89
|
## [2.1.1] - 2025-11-02
|
|
11
90
|
|
|
12
91
|
### Added
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
include README.md
|
|
2
|
+
include LICENSE
|
|
3
|
+
|
|
4
|
+
recursive-include src *.py
|
|
5
|
+
|
|
6
|
+
recursive-include tests *.py
|
|
7
|
+
|
|
8
|
+
exclude .gitignore
|
|
9
|
+
exclude Dockerfile*
|
|
10
|
+
exclude docker-compose*.yml
|
|
11
|
+
exclude Makefile
|
|
12
|
+
|
|
13
|
+
prune build
|
|
14
|
+
prune dist
|
|
15
|
+
prune .tox
|
|
16
|
+
prune .pytest_cache
|
|
17
|
+
prune __pycache__
|
|
18
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pico-ioc
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
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
|
|
@@ -28,7 +28,7 @@ License: MIT License
|
|
|
28
28
|
Project-URL: Homepage, https://github.com/dperezcabrera/pico-ioc
|
|
29
29
|
Project-URL: Repository, https://github.com/dperezcabrera/pico-ioc
|
|
30
30
|
Project-URL: Issue Tracker, https://github.com/dperezcabrera/pico-ioc/issues
|
|
31
|
-
Keywords: ioc,di,dependency
|
|
31
|
+
Keywords: python,ioc,dependency-injection,di-container,inversion-of-control,ioc-container,zero-dependency,minimalistic,async,asyncio,modular,pluggable,ioc-framework,ioc-containers,inversion-of-control-container
|
|
32
32
|
Classifier: Development Status :: 4 - Beta
|
|
33
33
|
Classifier: Programming Language :: Python :: 3
|
|
34
34
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
@@ -62,7 +62,6 @@ Dynamic: license-file
|
|
|
62
62
|
[](https://dperezcabrera.github.io/pico-ioc/)
|
|
63
63
|
[](https://dperezcabrera.github.io/learn-pico-ioc/)
|
|
64
64
|
|
|
65
|
-
|
|
66
65
|
**Pico-IoC** is a **lightweight, async-ready, decorator-driven IoC container** built for clarity, testability, and performance.
|
|
67
66
|
It brings Inversion of Control and dependency injection to Python in a deterministic, modern, and framework-agnostic way.
|
|
68
67
|
|
|
@@ -97,14 +96,14 @@ Pico-IoC eliminates that friction by letting you declare how components relate
|
|
|
97
96
|
|
|
98
97
|
---
|
|
99
98
|
|
|
100
|
-
## 🧩 Highlights (v2.
|
|
99
|
+
## 🧩 Highlights (v2.2+)
|
|
101
100
|
|
|
102
|
-
- Unified Configuration
|
|
103
|
-
-
|
|
104
|
-
-
|
|
105
|
-
-
|
|
106
|
-
- Tree-based configuration
|
|
107
|
-
- Observable
|
|
101
|
+
- **Unified Configuration**: Use `@configured` to bind both flat (ENV-like) and tree (YAML/JSON) sources via the `configuration(...)` builder (ADR-0010).
|
|
102
|
+
- **Extensible Scanning**: Use `CustomScanner` to hook into the discovery phase and register functions or custom decorators (ADR-0011).
|
|
103
|
+
- **Async-aware AOP**: Method interceptors via `@intercepted_by`.
|
|
104
|
+
- **Scoped resolution**: singleton, prototype, request, session, transaction, and custom scopes.
|
|
105
|
+
- **Tree-based configuration**: Advanced mapping with reusable adapters (`Annotated[Union[...], Discriminator(...)]`).
|
|
106
|
+
- **Observable context**: Built-in stats, health checks (`@health`), observer hooks (`ContainerObserver`), and dependency graph export.
|
|
108
107
|
|
|
109
108
|
---
|
|
110
109
|
|
|
@@ -116,11 +115,21 @@ pip install pico-ioc
|
|
|
116
115
|
|
|
117
116
|
Optional extras:
|
|
118
117
|
|
|
119
|
-
- YAML configuration support (requires PyYAML)
|
|
118
|
+
- YAML configuration support (requires PyYAML)
|
|
120
119
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
120
|
+
```bash
|
|
121
|
+
pip install pico-ioc[yaml]
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
-----
|
|
125
|
+
|
|
126
|
+
### ⚠️ Important Note
|
|
127
|
+
|
|
128
|
+
**Breaking Behavior in Scope Management (v2.1.3+):**
|
|
129
|
+
**Scope LRU Eviction has been removed** to guarantee data integrity.
|
|
130
|
+
|
|
131
|
+
* **Frameworks (pico-fastapi):** Handled automatically.
|
|
132
|
+
* **Manual usage:** You **must** explicitly call `container._caches.cleanup_scope("scope_name", scope_id)` when a context ends to prevent memory leaks.
|
|
124
133
|
|
|
125
134
|
-----
|
|
126
135
|
|
|
@@ -238,12 +247,16 @@ async def main():
|
|
|
238
247
|
container = init(modules=[__name__])
|
|
239
248
|
repo = await container.aget(AsyncRepo) # Async resolution
|
|
240
249
|
print(await repo.fetch())
|
|
250
|
+
|
|
251
|
+
# Graceful async shutdown (calls @cleanup async methods)
|
|
252
|
+
await container.ashutdown()
|
|
241
253
|
|
|
242
254
|
asyncio.run(main())
|
|
243
255
|
```
|
|
244
256
|
|
|
245
|
-
- `__ainit__` runs after construction if defined.
|
|
246
|
-
- Use `container.aget(Type)` to resolve components that require async initialization
|
|
257
|
+
- `__ainit__` runs after construction if defined.
|
|
258
|
+
- Use `container.aget(Type)` to resolve components that require async initialization.
|
|
259
|
+
- Use `await container.ashutdown()` to close resources cleanly.
|
|
247
260
|
|
|
248
261
|
-----
|
|
249
262
|
|
|
@@ -283,35 +296,26 @@ result = c.get(Demo).work()
|
|
|
283
296
|
print(f"Result: {result}")
|
|
284
297
|
```
|
|
285
298
|
|
|
286
|
-
Output:
|
|
287
|
-
|
|
288
|
-
```
|
|
289
|
-
→ calling Demo.work
|
|
290
|
-
Working...
|
|
291
|
-
← Demo.work done (10.xxms)
|
|
292
|
-
Result: ok
|
|
293
|
-
```
|
|
294
|
-
|
|
295
299
|
-----
|
|
296
300
|
|
|
297
301
|
## 👁️ Observability & Cleanup
|
|
298
302
|
|
|
299
|
-
- Export a dependency graph in DOT format:
|
|
303
|
+
- Export a dependency graph in DOT format:
|
|
300
304
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
f.write(dot)
|
|
306
|
-
```
|
|
305
|
+
```python
|
|
306
|
+
c = init(modules=[...])
|
|
307
|
+
c.export_graph("dependencies.dot") # Writes directly to file
|
|
308
|
+
```
|
|
307
309
|
|
|
308
|
-
- Health checks:
|
|
309
|
-
- Annotate health probes inside components with `@health` for container-level reporting.
|
|
310
|
-
- The container exposes health information that can be queried in observability tooling.
|
|
310
|
+
- Health checks:
|
|
311
311
|
|
|
312
|
-
-
|
|
313
|
-
|
|
314
|
-
|
|
312
|
+
- Annotate health probes inside components with `@health` for container-level reporting.
|
|
313
|
+
- The container exposes health information that can be queried in observability tooling.
|
|
314
|
+
|
|
315
|
+
- Container cleanup:
|
|
316
|
+
|
|
317
|
+
- For sync apps: `container.shutdown()`
|
|
318
|
+
- For async apps: `await container.ashutdown()`
|
|
315
319
|
|
|
316
320
|
Use cleanup in application shutdown hooks to release resources deterministically.
|
|
317
321
|
|
|
@@ -321,14 +325,14 @@ Use cleanup in application shutdown hooks to release resources deterministically
|
|
|
321
325
|
|
|
322
326
|
The full documentation is available within the `docs/` directory of the project repository. Start with `docs/README.md` for navigation.
|
|
323
327
|
|
|
324
|
-
- Getting Started: `docs/getting-started.md`
|
|
325
|
-
- User Guide: `docs/user-guide/README.md`
|
|
326
|
-
- Advanced Features: `docs/advanced-features/README.md`
|
|
327
|
-
- Observability: `docs/observability/README.md`
|
|
328
|
-
- Cookbook (Patterns): `docs/cookbook/README.md`
|
|
329
|
-
- Architecture: `docs/architecture/README.md`
|
|
330
|
-
- API Reference: `docs/api-reference/README.md`
|
|
331
|
-
- ADR Index: `docs/adr/README.md`
|
|
328
|
+
- Getting Started: `docs/getting-started.md`
|
|
329
|
+
- User Guide: `docs/user-guide/README.md`
|
|
330
|
+
- Advanced Features: `docs/advanced-features/README.md`
|
|
331
|
+
- Observability: `docs/observability/README.md`
|
|
332
|
+
- Cookbook (Patterns): `docs/cookbook/README.md`
|
|
333
|
+
- Architecture: `docs/architecture/README.md`
|
|
334
|
+
- API Reference: `docs/api-reference/README.md`
|
|
335
|
+
- ADR Index: `docs/adr/README.md`
|
|
332
336
|
|
|
333
337
|
-----
|
|
334
338
|
|
|
@@ -343,10 +347,11 @@ tox
|
|
|
343
347
|
|
|
344
348
|
## 🧾 Changelog
|
|
345
349
|
|
|
346
|
-
See [CHANGELOG.md](
|
|
350
|
+
See [CHANGELOG.md](https://www.google.com/search?q=./CHANGELOG.md) — Significant redesigns and features in v2.0+.
|
|
347
351
|
|
|
348
352
|
-----
|
|
349
353
|
|
|
350
354
|
## 📜 License
|
|
351
355
|
|
|
352
356
|
MIT — [LICENSE](https://opensource.org/licenses/MIT)
|
|
357
|
+
|
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
[](https://dperezcabrera.github.io/pico-ioc/)
|
|
13
13
|
[](https://dperezcabrera.github.io/learn-pico-ioc/)
|
|
14
14
|
|
|
15
|
-
|
|
16
15
|
**Pico-IoC** is a **lightweight, async-ready, decorator-driven IoC container** built for clarity, testability, and performance.
|
|
17
16
|
It brings Inversion of Control and dependency injection to Python in a deterministic, modern, and framework-agnostic way.
|
|
18
17
|
|
|
@@ -47,14 +46,14 @@ Pico-IoC eliminates that friction by letting you declare how components relate
|
|
|
47
46
|
|
|
48
47
|
---
|
|
49
48
|
|
|
50
|
-
## 🧩 Highlights (v2.
|
|
49
|
+
## 🧩 Highlights (v2.2+)
|
|
51
50
|
|
|
52
|
-
- Unified Configuration
|
|
53
|
-
-
|
|
54
|
-
-
|
|
55
|
-
-
|
|
56
|
-
- Tree-based configuration
|
|
57
|
-
- Observable
|
|
51
|
+
- **Unified Configuration**: Use `@configured` to bind both flat (ENV-like) and tree (YAML/JSON) sources via the `configuration(...)` builder (ADR-0010).
|
|
52
|
+
- **Extensible Scanning**: Use `CustomScanner` to hook into the discovery phase and register functions or custom decorators (ADR-0011).
|
|
53
|
+
- **Async-aware AOP**: Method interceptors via `@intercepted_by`.
|
|
54
|
+
- **Scoped resolution**: singleton, prototype, request, session, transaction, and custom scopes.
|
|
55
|
+
- **Tree-based configuration**: Advanced mapping with reusable adapters (`Annotated[Union[...], Discriminator(...)]`).
|
|
56
|
+
- **Observable context**: Built-in stats, health checks (`@health`), observer hooks (`ContainerObserver`), and dependency graph export.
|
|
58
57
|
|
|
59
58
|
---
|
|
60
59
|
|
|
@@ -66,11 +65,21 @@ pip install pico-ioc
|
|
|
66
65
|
|
|
67
66
|
Optional extras:
|
|
68
67
|
|
|
69
|
-
- YAML configuration support (requires PyYAML)
|
|
68
|
+
- YAML configuration support (requires PyYAML)
|
|
70
69
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
```bash
|
|
71
|
+
pip install pico-ioc[yaml]
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
-----
|
|
75
|
+
|
|
76
|
+
### ⚠️ Important Note
|
|
77
|
+
|
|
78
|
+
**Breaking Behavior in Scope Management (v2.1.3+):**
|
|
79
|
+
**Scope LRU Eviction has been removed** to guarantee data integrity.
|
|
80
|
+
|
|
81
|
+
* **Frameworks (pico-fastapi):** Handled automatically.
|
|
82
|
+
* **Manual usage:** You **must** explicitly call `container._caches.cleanup_scope("scope_name", scope_id)` when a context ends to prevent memory leaks.
|
|
74
83
|
|
|
75
84
|
-----
|
|
76
85
|
|
|
@@ -188,12 +197,16 @@ async def main():
|
|
|
188
197
|
container = init(modules=[__name__])
|
|
189
198
|
repo = await container.aget(AsyncRepo) # Async resolution
|
|
190
199
|
print(await repo.fetch())
|
|
200
|
+
|
|
201
|
+
# Graceful async shutdown (calls @cleanup async methods)
|
|
202
|
+
await container.ashutdown()
|
|
191
203
|
|
|
192
204
|
asyncio.run(main())
|
|
193
205
|
```
|
|
194
206
|
|
|
195
|
-
- `__ainit__` runs after construction if defined.
|
|
196
|
-
- Use `container.aget(Type)` to resolve components that require async initialization
|
|
207
|
+
- `__ainit__` runs after construction if defined.
|
|
208
|
+
- Use `container.aget(Type)` to resolve components that require async initialization.
|
|
209
|
+
- Use `await container.ashutdown()` to close resources cleanly.
|
|
197
210
|
|
|
198
211
|
-----
|
|
199
212
|
|
|
@@ -233,35 +246,26 @@ result = c.get(Demo).work()
|
|
|
233
246
|
print(f"Result: {result}")
|
|
234
247
|
```
|
|
235
248
|
|
|
236
|
-
Output:
|
|
237
|
-
|
|
238
|
-
```
|
|
239
|
-
→ calling Demo.work
|
|
240
|
-
Working...
|
|
241
|
-
← Demo.work done (10.xxms)
|
|
242
|
-
Result: ok
|
|
243
|
-
```
|
|
244
|
-
|
|
245
249
|
-----
|
|
246
250
|
|
|
247
251
|
## 👁️ Observability & Cleanup
|
|
248
252
|
|
|
249
|
-
- Export a dependency graph in DOT format:
|
|
253
|
+
- Export a dependency graph in DOT format:
|
|
250
254
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
f.write(dot)
|
|
256
|
-
```
|
|
255
|
+
```python
|
|
256
|
+
c = init(modules=[...])
|
|
257
|
+
c.export_graph("dependencies.dot") # Writes directly to file
|
|
258
|
+
```
|
|
257
259
|
|
|
258
|
-
- Health checks:
|
|
259
|
-
- Annotate health probes inside components with `@health` for container-level reporting.
|
|
260
|
-
- The container exposes health information that can be queried in observability tooling.
|
|
260
|
+
- Health checks:
|
|
261
261
|
|
|
262
|
-
-
|
|
263
|
-
|
|
264
|
-
|
|
262
|
+
- Annotate health probes inside components with `@health` for container-level reporting.
|
|
263
|
+
- The container exposes health information that can be queried in observability tooling.
|
|
264
|
+
|
|
265
|
+
- Container cleanup:
|
|
266
|
+
|
|
267
|
+
- For sync apps: `container.shutdown()`
|
|
268
|
+
- For async apps: `await container.ashutdown()`
|
|
265
269
|
|
|
266
270
|
Use cleanup in application shutdown hooks to release resources deterministically.
|
|
267
271
|
|
|
@@ -271,14 +275,14 @@ Use cleanup in application shutdown hooks to release resources deterministically
|
|
|
271
275
|
|
|
272
276
|
The full documentation is available within the `docs/` directory of the project repository. Start with `docs/README.md` for navigation.
|
|
273
277
|
|
|
274
|
-
- Getting Started: `docs/getting-started.md`
|
|
275
|
-
- User Guide: `docs/user-guide/README.md`
|
|
276
|
-
- Advanced Features: `docs/advanced-features/README.md`
|
|
277
|
-
- Observability: `docs/observability/README.md`
|
|
278
|
-
- Cookbook (Patterns): `docs/cookbook/README.md`
|
|
279
|
-
- Architecture: `docs/architecture/README.md`
|
|
280
|
-
- API Reference: `docs/api-reference/README.md`
|
|
281
|
-
- ADR Index: `docs/adr/README.md`
|
|
278
|
+
- Getting Started: `docs/getting-started.md`
|
|
279
|
+
- User Guide: `docs/user-guide/README.md`
|
|
280
|
+
- Advanced Features: `docs/advanced-features/README.md`
|
|
281
|
+
- Observability: `docs/observability/README.md`
|
|
282
|
+
- Cookbook (Patterns): `docs/cookbook/README.md`
|
|
283
|
+
- Architecture: `docs/architecture/README.md`
|
|
284
|
+
- API Reference: `docs/api-reference/README.md`
|
|
285
|
+
- ADR Index: `docs/adr/README.md`
|
|
282
286
|
|
|
283
287
|
-----
|
|
284
288
|
|
|
@@ -293,10 +297,11 @@ tox
|
|
|
293
297
|
|
|
294
298
|
## 🧾 Changelog
|
|
295
299
|
|
|
296
|
-
See [CHANGELOG.md](
|
|
300
|
+
See [CHANGELOG.md](https://www.google.com/search?q=./CHANGELOG.md) — Significant redesigns and features in v2.0+.
|
|
297
301
|
|
|
298
302
|
-----
|
|
299
303
|
|
|
300
304
|
## 📜 License
|
|
301
305
|
|
|
302
306
|
MIT — [LICENSE](https://opensource.org/licenses/MIT)
|
|
307
|
+
|
|
@@ -13,6 +13,7 @@ ADR Index:
|
|
|
13
13
|
- ADR-008: Explicit Handling of Circular Dependencies — Accepted — ./adr-0008-circular-dependencies.md
|
|
14
14
|
- ADR-009: Flexible @provides for Static and Module-level Functions — Accepted — ./adr-0009-flexible-provides.md
|
|
15
15
|
- ADR-010: Unified Configuration via @configured and ContextConfig — Accepted — ./adr-0010-unified-configuration.md
|
|
16
|
+
- ADR-011: Extensible Component Scanning via Custom Scanners — Accepted — ./adr-0011-custom-scanners.md
|
|
16
17
|
|
|
17
18
|
Status legend:
|
|
18
19
|
- Proposed: Under discussion, not yet binding.
|
|
@@ -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,81 @@
|
|
|
1
|
+
# ADR-0011: Extensible Component Scanning via Custom Scanners
|
|
2
|
+
|
|
3
|
+
Status: Accepted
|
|
4
|
+
|
|
5
|
+
## Context
|
|
6
|
+
|
|
7
|
+
The original component scanning mechanism in `pico-ioc` was designed as a closed system. It strictly looked for specific internal decorators (`@component`, `@factory`, `@provides`, `@configured`) and hardcoded logic within `ComponentScanner`.
|
|
8
|
+
|
|
9
|
+
This rigidity created significant challenges for third-party extensions (such as `pico-agent` or web frameworks):
|
|
10
|
+
1. **Global Mutable State:** Extensions were forced to use global registries (e.g., `_PENDING_AGENTS` lists) to track decorated objects, leading to thread-safety issues and test contamination.
|
|
11
|
+
2. **Fragile Hacks:** Extensions often relied on stack frame inspection to guess the caller's module, which is unreliable.
|
|
12
|
+
3. **Lack of Hooks:** There was no clean way to intercept the scanning phase to register objects based on custom logic (e.g., registering a function decorated with `@task` as a prototype component).
|
|
13
|
+
|
|
14
|
+
We needed a standardized, stateless extension point to allow third-party libraries to participate in the discovery phase.
|
|
15
|
+
|
|
16
|
+
## Decision
|
|
17
|
+
|
|
18
|
+
We introduce the `CustomScanner` protocol and expose a new `custom_scanners` argument in the `init()` API.
|
|
19
|
+
|
|
20
|
+
1. **Protocol Definition:** We define a `CustomScanner` protocol with `should_scan(obj)` and `scan(obj)` methods. This delegates the responsibility of pattern matching and metadata construction to the extension author.
|
|
21
|
+
2. **Priority Scanning:** The `ComponentScanner` iteration logic is modified to prioritize these custom scanners.
|
|
22
|
+
* The scanner iterates through all module members once.
|
|
23
|
+
* For **every** member (whether it is a Class, Function, or other object), it first checks the registered `custom_scanners`.
|
|
24
|
+
* If a custom scanner claims the object (returns a binding), the built-in native scanning logic is skipped for that object.
|
|
25
|
+
3. **Injection via Init:** Users or frameworks pass instances of these scanners into the container via `init(..., custom_scanners=[...])`.
|
|
26
|
+
|
|
27
|
+
## Details
|
|
28
|
+
|
|
29
|
+
### The Protocol
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
class CustomScanner(Protocol):
|
|
33
|
+
def should_scan(self, obj: Any) -> bool:
|
|
34
|
+
"""Return True if this scanner handles the given object."""
|
|
35
|
+
...
|
|
36
|
+
|
|
37
|
+
def scan(self, obj: Any) -> Optional[Tuple[KeyT, Provider, ProviderMetadata]]:
|
|
38
|
+
"""
|
|
39
|
+
Constructs the binding artifacts.
|
|
40
|
+
Returns (key, provider, metadata) or None.
|
|
41
|
+
"""
|
|
42
|
+
...
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Scanning Logic
|
|
46
|
+
|
|
47
|
+
The internal loop in `ComponentScanner.scan_module` effectively works as follows:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
for name, obj in inspect.getmembers(module):
|
|
51
|
+
# 1. Custom Scanners take precedence over everything
|
|
52
|
+
if self._try_custom_scanners(obj):
|
|
53
|
+
continue
|
|
54
|
+
|
|
55
|
+
# 2. Native logic (Component, Factory, Configured, Provides)
|
|
56
|
+
# ...
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
This ensures that a custom scanner can override default behavior or register objects that `pico-ioc` would normally ignore (like standalone functions decorated with custom markers).
|
|
60
|
+
|
|
61
|
+
## Consequences
|
|
62
|
+
|
|
63
|
+
### Positive
|
|
64
|
+
|
|
65
|
+
- **Decoupling:** Extensions no longer need to depend on `pico-ioc` internals or global state.
|
|
66
|
+
- **Flexibility:** Enables support for function-based components (e.g., tasks, agents) and custom class decorators.
|
|
67
|
+
- **Safety:** Scanners are scoped to the container instance, ensuring thread safety and isolation during tests.
|
|
68
|
+
- **Performance:** Single-pass iteration over module members allows for efficient discovery without repeated `inspect` calls.
|
|
69
|
+
|
|
70
|
+
### Negative
|
|
71
|
+
|
|
72
|
+
- **Complexity:** Increases the API surface area of `init()`.
|
|
73
|
+
- **Manual Wiring:** Without a wrapper (like `pico-stack`), users must manually instantiate and pass scanner instances to `init()`.
|
|
74
|
+
|
|
75
|
+
## Alternatives Considered
|
|
76
|
+
|
|
77
|
+
- **Global Registry Hooks:** Rejected due to testing isolation issues and "magic" global state.
|
|
78
|
+
- **Inheritance (`class MyScanner(ComponentScanner)`):** Rejected because it tightly couples extensions to the internal implementation of the default scanner and makes composing multiple extensions difficult.
|
|
79
|
+
|
|
80
|
+
<!-- end list -->
|
|
81
|
+
|
|
@@ -11,6 +11,6 @@ Welcome to the Advanced Features guide. You've mastered the core concepts. This
|
|
|
11
11
|
* [3. The Event Bus: `EventBus`, `@subscribe`](./event-bus.md) 📢
|
|
12
12
|
* [4. Conditional Binding: `primary`, `on_missing_selector`, `conditional_*`](./conditional-binding.md) 🤔
|
|
13
13
|
* [5. Health Checks: `@health`](./health-checks.md) ❤️🩹
|
|
14
|
-
|
|
14
|
+
* [6. Custom Component Scanners](./custom-scanners.md) 🔎
|
|
15
15
|
|
|
16
16
|
---
|