pico-ioc 2.1.3__tar.gz → 2.2.1__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.3 → pico_ioc-2.2.1}/CHANGELOG.md +31 -0
- pico_ioc-2.2.1/MANIFEST.in +18 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/PKG-INFO +47 -51
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/README.md +45 -49
- pico_ioc-2.2.1/docs/README.md +142 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/adr/README.md +1 -0
- pico_ioc-2.2.1/docs/adr/adr-0011-custom-scanners.md +81 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/advanced-features/README.md +1 -1
- pico_ioc-2.2.1/docs/advanced-features/aop-proxies.md +274 -0
- pico_ioc-2.2.1/docs/advanced-features/custom-scanners.md +133 -0
- pico_ioc-2.2.1/docs/advanced-features/self-injection.md +94 -0
- pico_ioc-2.2.1/docs/api-reference/analysis-api.md +181 -0
- pico_ioc-2.2.1/docs/api-reference/constants.md +142 -0
- pico_ioc-2.2.1/docs/api-reference/container-context-api.md +119 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/api-reference/container.md +15 -1
- pico_ioc-2.2.1/docs/api-reference/exceptions.md +269 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/api-reference/protocols.md +27 -0
- pico_ioc-2.2.1/docs/api-reference/provider-selector.md +162 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/architecture/design-principles.md +4 -1
- pico_ioc-2.2.1/docs/architecture/docs-assets.md +235 -0
- pico_ioc-2.2.1/docs/architecture/runtime-model-scheduling.md +217 -0
- pico_ioc-2.2.1/docs/observability/resolution-graph.md +188 -0
- pico_ioc-2.2.1/docs/overview.md +5 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/user-guide/README.md +1 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/mkdocs.yml +12 -1
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/pyproject.toml +19 -2
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/src/pico_ioc/__init__.py +2 -1
- pico_ioc-2.2.1/src/pico_ioc/_version.py +1 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/src/pico_ioc/aop.py +13 -11
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/src/pico_ioc/api.py +9 -3
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/src/pico_ioc/component_scanner.py +48 -24
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/src/pico_ioc/container.py +5 -1
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/src/pico_ioc/registrar.py +3 -5
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/src/pico_ioc/scope.py +15 -8
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/src/pico_ioc.egg-info/PKG-INFO +47 -51
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/src/pico_ioc.egg-info/SOURCES.txt +28 -1
- pico_ioc-2.2.1/tests/test_aop_coverage.py +72 -0
- pico_ioc-2.2.1/tests/test_aop_extended.py +573 -0
- pico_ioc-2.2.1/tests/test_bugfixes_v221.py +396 -0
- pico_ioc-2.2.1/tests/test_config_builder.py +119 -0
- pico_ioc-2.2.1/tests/test_config_registrar_coverage.py +49 -0
- pico_ioc-2.2.1/tests/test_config_runtime_coverage.py +75 -0
- pico_ioc-2.2.1/tests/test_container_coverage.py +57 -0
- pico_ioc-2.2.1/tests/test_container_coverage_boost.py +558 -0
- pico_ioc-2.2.1/tests/test_container_extended.py +590 -0
- pico_ioc-2.2.1/tests/test_container_integrated_coverage.py +319 -0
- pico_ioc-2.2.1/tests/test_custom_scanner.py +63 -0
- pico_ioc-2.2.1/tests/test_event_bus_coverage_boost.py +470 -0
- pico_ioc-2.2.1/tests/test_event_bus_extended.py +434 -0
- pico_ioc-2.2.1/tests/test_locator.py +163 -0
- pico_ioc-2.2.1/tests/test_scope_coverage.py +66 -0
- pico_ioc-2.1.3/MANIFEST.in +0 -24
- pico_ioc-2.1.3/docs/README.md +0 -154
- pico_ioc-2.1.3/docs/overview.md +0 -191
- pico_ioc-2.1.3/src/pico_ioc/_version.py +0 -1
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/.coveragerc +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/.github/workflows/ci.yml +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/.github/workflows/docs.yml +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/.github/workflows/publish-to-pypi.yml +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/LICENSE +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/LEARN.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/adr/adr-0001-async-native.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/adr/adr-0002-tree-based-configuration.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/adr/adr-0003-context-aware-scopes.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/adr/adr-0004-observability.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/adr/adr-0005-aop.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/adr/adr-0006-eager-validation.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/adr/adr-0007-event_bus.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/adr/adr-0008-circular-dependencies.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/adr/adr-0009-flexible-provides.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/adr/adr-0010-unified-configuration.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/advanced-features/aop-interceptors.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/advanced-features/async-resolution.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/advanced-features/conditional-binding.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/advanced-features/event-bus.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/advanced-features/health-checks.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/api-reference/README.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/api-reference/decorators.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/api-reference/event_bus.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/api-reference/glossary.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/architecture/README.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/architecture/comparison.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/architecture/internals.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/cookbook/README.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/cookbook/pattern-aop-feature-toggle.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/cookbook/pattern-aop-profiling.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/cookbook/pattern-aop-security.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/cookbook/pattern-aop-structured-logging.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/cookbook/pattern-cli-app.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/cookbook/pattern-config-overrides.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/cookbook/pattern-cqrs.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/cookbook/pattern-hot-reload.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/cookbook/pattern-multi-tenant.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/getting-started.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/javascripts/extra.js +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/observability/README.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/observability/container-context.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/observability/exporting-graph.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/observability/observers-metrics.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/requirements.txt +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/stylesheets/extra.css +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/user-guide/configuration-basic.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/user-guide/configuration-binding.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/user-guide/core-concepts.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/user-guide/qualifiers-lists.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/user-guide/scopes-lifecycle.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/docs/user-guide/testing.md +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/setup.cfg +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/src/pico_ioc/analysis.py +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/src/pico_ioc/config_builder.py +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/src/pico_ioc/config_registrar.py +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/src/pico_ioc/config_runtime.py +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/src/pico_ioc/constants.py +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/src/pico_ioc/decorators.py +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/src/pico_ioc/dependency_validator.py +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/src/pico_ioc/event_bus.py +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/src/pico_ioc/exceptions.py +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/src/pico_ioc/factory.py +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/src/pico_ioc/locator.py +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/src/pico_ioc/provider_selector.py +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/src/pico_ioc.egg-info/dependency_links.txt +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/src/pico_ioc.egg-info/requires.txt +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/src/pico_ioc.egg-info/top_level.txt +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/tests/test_aop.py +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/tests/test_collection_injection.py +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/tests/test_config_value.py +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/tests/test_configured.py +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/tests/test_container_context.py +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/tests/test_container_runtime.py +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/tests/test_container_self_injection.py +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/tests/test_event_bus.py +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/tests/test_pico_extends.py +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/tests/test_pico_integration.py +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/tests/test_protocol_resolution_and_graph.py +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/tests/test_provides_module_functions.py +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/tests/test_provides_static_methods.py +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/tests/test_proxy_unit.py +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/tests/test_resolution_graph.py +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/tests/test_scope.py +0 -0
- {pico_ioc-2.1.3 → pico_ioc-2.2.1}/tox.ini +0 -0
|
@@ -7,6 +7,37 @@ and this project adheres to Semantic Versioning (https://semver.org/spec/v2.0.ht
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [2.2.1] - 2026-02-03
|
|
11
|
+
|
|
12
|
+
### Fixed 🧩
|
|
13
|
+
|
|
14
|
+
- **DOT Graph Export**: Fixed incorrect variable reference in `export_graph()` where dependency edges used `{child}` instead of `{cid}`, causing malformed DOT output when visualizing the dependency graph.
|
|
15
|
+
- **Race Condition in Lazy Proxy**: Fixed a race condition in `UnifiedComponentProxy._async_init_if_needed()` where concurrent async access could trigger duplicate object creation. The fix implements proper double-check locking and moves `@configure` execution outside the critical section to prevent deadlocks.
|
|
16
|
+
- **Silent Cleanup Failures**: Replaced silent `except Exception: pass` blocks in `ScopedCaches` cleanup methods with proper logging. Cleanup method failures are now logged at WARNING level, aiding debugging of resource leaks without crashing the application.
|
|
17
|
+
|
|
18
|
+
### Internal 🔧
|
|
19
|
+
|
|
20
|
+
- Added `logging` import and `_logger` instance to `scope.py` for structured error reporting during component cleanup.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## [2.2.0] - 2025-11-29
|
|
25
|
+
|
|
26
|
+
### Added ✨
|
|
27
|
+
- **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.
|
|
28
|
+
- **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.
|
|
29
|
+
- **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.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- **Internal Cleanup:** Removed the deprecated `max_scopes_per_type` parameter from `ScopedCaches`, reflecting the removal of LRU eviction in v2.1.3.
|
|
33
|
+
|
|
34
|
+
### Docs 📚
|
|
35
|
+
- **Major Restructuring:** Rebuilt `mkdocs.yml` navigation to include all available documentation pages.
|
|
36
|
+
- **New Content:** Added missing guides for **Integrations** (Celery), **Advanced Features** (Self-Injection, AOP Proxies, Custom Scanners), and **Architecture** (Runtime Model).
|
|
37
|
+
- **Corrections:** Fixed discrepancies in asset filenames (`extra.js`) and standardized the format of Architecture Decision Records (ADRs).
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
10
41
|
## [2.1.3] - 2025-11-18
|
|
11
42
|
|
|
12
43
|
### Fixed
|
|
@@ -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.1
|
|
3
|
+
Version: 2.2.1
|
|
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,20 +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
|
+
```
|
|
124
123
|
|
|
125
124
|
-----
|
|
126
125
|
|
|
127
|
-
### ⚠️ Important Note
|
|
126
|
+
### ⚠️ Important Note
|
|
127
|
+
|
|
128
|
+
**Breaking Behavior in Scope Management (v2.1.3+):**
|
|
129
|
+
**Scope LRU Eviction has been removed** to guarantee data integrity.
|
|
128
130
|
|
|
129
|
-
**
|
|
130
|
-
|
|
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.
|
|
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.
|
|
133
133
|
|
|
134
134
|
-----
|
|
135
135
|
|
|
@@ -247,12 +247,16 @@ async def main():
|
|
|
247
247
|
container = init(modules=[__name__])
|
|
248
248
|
repo = await container.aget(AsyncRepo) # Async resolution
|
|
249
249
|
print(await repo.fetch())
|
|
250
|
+
|
|
251
|
+
# Graceful async shutdown (calls @cleanup async methods)
|
|
252
|
+
await container.ashutdown()
|
|
250
253
|
|
|
251
254
|
asyncio.run(main())
|
|
252
255
|
```
|
|
253
256
|
|
|
254
|
-
- `__ainit__` runs after construction if defined.
|
|
255
|
-
- 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.
|
|
256
260
|
|
|
257
261
|
-----
|
|
258
262
|
|
|
@@ -292,35 +296,26 @@ result = c.get(Demo).work()
|
|
|
292
296
|
print(f"Result: {result}")
|
|
293
297
|
```
|
|
294
298
|
|
|
295
|
-
Output:
|
|
296
|
-
|
|
297
|
-
```
|
|
298
|
-
→ calling Demo.work
|
|
299
|
-
Working...
|
|
300
|
-
← Demo.work done (10.xxms)
|
|
301
|
-
Result: ok
|
|
302
|
-
```
|
|
303
|
-
|
|
304
299
|
-----
|
|
305
300
|
|
|
306
301
|
## 👁️ Observability & Cleanup
|
|
307
302
|
|
|
308
|
-
- Export a dependency graph in DOT format:
|
|
303
|
+
- Export a dependency graph in DOT format:
|
|
304
|
+
|
|
305
|
+
```python
|
|
306
|
+
c = init(modules=[...])
|
|
307
|
+
c.export_graph("dependencies.dot") # Writes directly to file
|
|
308
|
+
```
|
|
309
309
|
|
|
310
|
-
|
|
311
|
-
c = init(modules=[...])
|
|
312
|
-
dot = c.export_graph() # Returns DOT graph as a string
|
|
313
|
-
with open("dependencies.dot", "w") as f:
|
|
314
|
-
f.write(dot)
|
|
315
|
-
```
|
|
310
|
+
- Health checks:
|
|
316
311
|
|
|
317
|
-
-
|
|
318
|
-
|
|
319
|
-
- The container exposes health information that can be queried in observability tooling.
|
|
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.
|
|
320
314
|
|
|
321
|
-
- Container cleanup:
|
|
322
|
-
|
|
323
|
-
|
|
315
|
+
- Container cleanup:
|
|
316
|
+
|
|
317
|
+
- For sync apps: `container.shutdown()`
|
|
318
|
+
- For async apps: `await container.ashutdown()`
|
|
324
319
|
|
|
325
320
|
Use cleanup in application shutdown hooks to release resources deterministically.
|
|
326
321
|
|
|
@@ -330,14 +325,14 @@ Use cleanup in application shutdown hooks to release resources deterministically
|
|
|
330
325
|
|
|
331
326
|
The full documentation is available within the `docs/` directory of the project repository. Start with `docs/README.md` for navigation.
|
|
332
327
|
|
|
333
|
-
- Getting Started: `docs/getting-started.md`
|
|
334
|
-
- User Guide: `docs/user-guide/README.md`
|
|
335
|
-
- Advanced Features: `docs/advanced-features/README.md`
|
|
336
|
-
- Observability: `docs/observability/README.md`
|
|
337
|
-
- Cookbook (Patterns): `docs/cookbook/README.md`
|
|
338
|
-
- Architecture: `docs/architecture/README.md`
|
|
339
|
-
- API Reference: `docs/api-reference/README.md`
|
|
340
|
-
- 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`
|
|
341
336
|
|
|
342
337
|
-----
|
|
343
338
|
|
|
@@ -359,3 +354,4 @@ See [CHANGELOG.md](./CHANGELOG.md) — Significant redesigns and features in v2.
|
|
|
359
354
|
## 📜 License
|
|
360
355
|
|
|
361
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,20 +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
|
+
```
|
|
74
73
|
|
|
75
74
|
-----
|
|
76
75
|
|
|
77
|
-
### ⚠️ Important Note
|
|
76
|
+
### ⚠️ Important Note
|
|
77
|
+
|
|
78
|
+
**Breaking Behavior in Scope Management (v2.1.3+):**
|
|
79
|
+
**Scope LRU Eviction has been removed** to guarantee data integrity.
|
|
78
80
|
|
|
79
|
-
**
|
|
80
|
-
|
|
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.
|
|
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.
|
|
83
83
|
|
|
84
84
|
-----
|
|
85
85
|
|
|
@@ -197,12 +197,16 @@ async def main():
|
|
|
197
197
|
container = init(modules=[__name__])
|
|
198
198
|
repo = await container.aget(AsyncRepo) # Async resolution
|
|
199
199
|
print(await repo.fetch())
|
|
200
|
+
|
|
201
|
+
# Graceful async shutdown (calls @cleanup async methods)
|
|
202
|
+
await container.ashutdown()
|
|
200
203
|
|
|
201
204
|
asyncio.run(main())
|
|
202
205
|
```
|
|
203
206
|
|
|
204
|
-
- `__ainit__` runs after construction if defined.
|
|
205
|
-
- 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.
|
|
206
210
|
|
|
207
211
|
-----
|
|
208
212
|
|
|
@@ -242,35 +246,26 @@ result = c.get(Demo).work()
|
|
|
242
246
|
print(f"Result: {result}")
|
|
243
247
|
```
|
|
244
248
|
|
|
245
|
-
Output:
|
|
246
|
-
|
|
247
|
-
```
|
|
248
|
-
→ calling Demo.work
|
|
249
|
-
Working...
|
|
250
|
-
← Demo.work done (10.xxms)
|
|
251
|
-
Result: ok
|
|
252
|
-
```
|
|
253
|
-
|
|
254
249
|
-----
|
|
255
250
|
|
|
256
251
|
## 👁️ Observability & Cleanup
|
|
257
252
|
|
|
258
|
-
- Export a dependency graph in DOT format:
|
|
253
|
+
- Export a dependency graph in DOT format:
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
c = init(modules=[...])
|
|
257
|
+
c.export_graph("dependencies.dot") # Writes directly to file
|
|
258
|
+
```
|
|
259
259
|
|
|
260
|
-
|
|
261
|
-
c = init(modules=[...])
|
|
262
|
-
dot = c.export_graph() # Returns DOT graph as a string
|
|
263
|
-
with open("dependencies.dot", "w") as f:
|
|
264
|
-
f.write(dot)
|
|
265
|
-
```
|
|
260
|
+
- Health checks:
|
|
266
261
|
|
|
267
|
-
-
|
|
268
|
-
|
|
269
|
-
- The container exposes health information that can be queried in observability tooling.
|
|
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.
|
|
270
264
|
|
|
271
|
-
- Container cleanup:
|
|
272
|
-
|
|
273
|
-
|
|
265
|
+
- Container cleanup:
|
|
266
|
+
|
|
267
|
+
- For sync apps: `container.shutdown()`
|
|
268
|
+
- For async apps: `await container.ashutdown()`
|
|
274
269
|
|
|
275
270
|
Use cleanup in application shutdown hooks to release resources deterministically.
|
|
276
271
|
|
|
@@ -280,14 +275,14 @@ Use cleanup in application shutdown hooks to release resources deterministically
|
|
|
280
275
|
|
|
281
276
|
The full documentation is available within the `docs/` directory of the project repository. Start with `docs/README.md` for navigation.
|
|
282
277
|
|
|
283
|
-
- Getting Started: `docs/getting-started.md`
|
|
284
|
-
- User Guide: `docs/user-guide/README.md`
|
|
285
|
-
- Advanced Features: `docs/advanced-features/README.md`
|
|
286
|
-
- Observability: `docs/observability/README.md`
|
|
287
|
-
- Cookbook (Patterns): `docs/cookbook/README.md`
|
|
288
|
-
- Architecture: `docs/architecture/README.md`
|
|
289
|
-
- API Reference: `docs/api-reference/README.md`
|
|
290
|
-
- 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`
|
|
291
286
|
|
|
292
287
|
-----
|
|
293
288
|
|
|
@@ -309,3 +304,4 @@ See [CHANGELOG.md](./CHANGELOG.md) — Significant redesigns and features in v2.
|
|
|
309
304
|
## 📜 License
|
|
310
305
|
|
|
311
306
|
MIT — [LICENSE](https://opensource.org/licenses/MIT)
|
|
307
|
+
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Pico-IoC Documentation
|
|
2
|
+
|
|
3
|
+
`pico-ioc` is a lightweight, async-native Inversion of Control (IoC) container for Python 3.10+. It brings enterprise-grade dependency injection, configuration binding, and AOP to the Python ecosystem.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Quick Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install pico-ioc
|
|
11
|
+
|
|
12
|
+
# Optional: YAML configuration support
|
|
13
|
+
pip install pico-ioc[yaml]
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 30-Second Example
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from pico_ioc import component, init
|
|
22
|
+
|
|
23
|
+
@component
|
|
24
|
+
class Database:
|
|
25
|
+
def query(self) -> str:
|
|
26
|
+
return "data from DB"
|
|
27
|
+
|
|
28
|
+
@component
|
|
29
|
+
class UserService:
|
|
30
|
+
def __init__(self, db: Database): # Auto-injected
|
|
31
|
+
self.db = db
|
|
32
|
+
|
|
33
|
+
def get_users(self) -> str:
|
|
34
|
+
return self.db.query()
|
|
35
|
+
|
|
36
|
+
# Initialize and use
|
|
37
|
+
container = init(modules=[__name__])
|
|
38
|
+
service = container.get(UserService)
|
|
39
|
+
print(service.get_users()) # "data from DB"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Key Features
|
|
45
|
+
|
|
46
|
+
| Feature | Description |
|
|
47
|
+
|---------|-------------|
|
|
48
|
+
| **Async-Native** | Full `async`/`await` support: `aget()`, `__ainit__`, async `@cleanup` |
|
|
49
|
+
| **Unified Configuration** | `@configured` maps ENV vars or YAML/JSON to dataclasses |
|
|
50
|
+
| **Fail-Fast Validation** | All wiring errors detected at `init()`, not runtime |
|
|
51
|
+
| **AOP Interceptors** | `@intercepted_by` for logging, caching, security |
|
|
52
|
+
| **Scoped Lifecycles** | singleton, prototype, request, session, transaction |
|
|
53
|
+
| **Observable** | Built-in stats, health checks, dependency graph export |
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Documentation Structure
|
|
58
|
+
|
|
59
|
+
| # | Section | Description | Link |
|
|
60
|
+
|---|---------|-------------|------|
|
|
61
|
+
| 1 | **Getting Started** | 5-minute tutorial | [getting-started.md](./getting-started.md) |
|
|
62
|
+
| 2 | **User Guide** | Core concepts, configuration, scopes, testing | [user-guide/](./user-guide/README.md) |
|
|
63
|
+
| 3 | **Advanced Features** | Async, AOP, Event Bus, conditional binding | [advanced-features/](./advanced-features/README.md) |
|
|
64
|
+
| 4 | **Observability** | Metrics, tracing, graph export | [observability/](./observability/README.md) |
|
|
65
|
+
| 5 | **Cookbook** | Real-world patterns (multi-tenant, CQRS, etc.) | [cookbook/](./cookbook/README.md) |
|
|
66
|
+
| 6 | **Architecture** | Design principles, internals | [architecture/](./architecture/README.md) |
|
|
67
|
+
| 7 | **API Reference** | Decorators, exceptions, protocols | [api-reference/](./api-reference/README.md) |
|
|
68
|
+
| 8 | **FAQ** | Common questions and solutions | [faq.md](./faq.md) |
|
|
69
|
+
| 9 | **Examples** | Complete runnable applications | [examples/](./examples/README.md) |
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Core APIs at a Glance
|
|
74
|
+
|
|
75
|
+
### Registration
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from pico_ioc import component, factory, provides
|
|
79
|
+
|
|
80
|
+
@component # Register a class
|
|
81
|
+
class MyService: ...
|
|
82
|
+
|
|
83
|
+
@factory # Group related providers
|
|
84
|
+
class ClientFactory:
|
|
85
|
+
@provides(RedisClient) # Provide third-party types
|
|
86
|
+
def build_redis(self, config: RedisConfig) -> RedisClient:
|
|
87
|
+
return RedisClient(config.url)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Configuration
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from dataclasses import dataclass
|
|
94
|
+
from pico_ioc import configured, configuration, init
|
|
95
|
+
|
|
96
|
+
@configured(prefix="DB_")
|
|
97
|
+
@dataclass
|
|
98
|
+
class DBConfig:
|
|
99
|
+
host: str = "localhost"
|
|
100
|
+
port: int = 5432
|
|
101
|
+
|
|
102
|
+
container = init(
|
|
103
|
+
modules=[__name__],
|
|
104
|
+
config=configuration() # Reads from ENV: DB_HOST, DB_PORT
|
|
105
|
+
)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Async Support
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
@component
|
|
112
|
+
class AsyncService:
|
|
113
|
+
async def __ainit__(self):
|
|
114
|
+
self.conn = await open_connection()
|
|
115
|
+
|
|
116
|
+
@cleanup
|
|
117
|
+
async def close(self):
|
|
118
|
+
await self.conn.close()
|
|
119
|
+
|
|
120
|
+
# Resolve async components
|
|
121
|
+
service = await container.aget(AsyncService)
|
|
122
|
+
|
|
123
|
+
# Cleanup
|
|
124
|
+
await container.ashutdown()
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Testing with Overrides
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
container = init(
|
|
131
|
+
modules=[__name__],
|
|
132
|
+
overrides={Database: FakeDatabase()} # Replace for tests
|
|
133
|
+
)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Next Steps
|
|
139
|
+
|
|
140
|
+
1. **New to pico-ioc?** Start with the [Getting Started](./getting-started.md) tutorial
|
|
141
|
+
2. **Coming from Spring/Guice?** Check the [Architecture Comparison](./architecture/comparison.md)
|
|
142
|
+
3. **Building a real app?** See the [Cookbook](./cookbook/README.md) patterns
|
|
@@ -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.
|
|
@@ -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
|
---
|