pico-ioc 2.1.3__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.
Files changed (124) hide show
  1. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/CHANGELOG.md +17 -0
  2. pico_ioc-2.2.0/MANIFEST.in +18 -0
  3. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/PKG-INFO +48 -52
  4. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/README.md +46 -50
  5. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/adr/README.md +1 -0
  6. pico_ioc-2.2.0/docs/adr/adr-0011-custom-scanners.md +81 -0
  7. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/advanced-features/README.md +1 -1
  8. pico_ioc-2.2.0/docs/advanced-features/aop-proxies.md +274 -0
  9. pico_ioc-2.2.0/docs/advanced-features/custom-scanners.md +133 -0
  10. pico_ioc-2.2.0/docs/advanced-features/self-injection.md +94 -0
  11. pico_ioc-2.2.0/docs/api-reference/analysis-api.md +181 -0
  12. pico_ioc-2.2.0/docs/api-reference/constants.md +142 -0
  13. pico_ioc-2.2.0/docs/api-reference/container-context-api.md +119 -0
  14. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/api-reference/container.md +15 -1
  15. pico_ioc-2.2.0/docs/api-reference/exceptions.md +269 -0
  16. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/api-reference/protocols.md +27 -0
  17. pico_ioc-2.2.0/docs/api-reference/provider-selector.md +162 -0
  18. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/architecture/design-principles.md +4 -1
  19. pico_ioc-2.2.0/docs/architecture/docs-assets.md +235 -0
  20. pico_ioc-2.2.0/docs/architecture/runtime-model-scheduling.md +217 -0
  21. pico_ioc-2.2.0/docs/observability/resolution-graph.md +188 -0
  22. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/user-guide/README.md +1 -0
  23. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/mkdocs.yml +16 -1
  24. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/pyproject.toml +19 -2
  25. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/src/pico_ioc/__init__.py +2 -1
  26. pico_ioc-2.2.0/src/pico_ioc/_version.py +1 -0
  27. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/src/pico_ioc/api.py +9 -3
  28. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/src/pico_ioc/component_scanner.py +48 -24
  29. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/src/pico_ioc/container.py +4 -0
  30. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/src/pico_ioc/registrar.py +3 -5
  31. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/src/pico_ioc/scope.py +1 -2
  32. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/src/pico_ioc.egg-info/PKG-INFO +48 -52
  33. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/src/pico_ioc.egg-info/SOURCES.txt +13 -0
  34. pico_ioc-2.2.0/tests/test_custom_scanner.py +63 -0
  35. pico_ioc-2.1.3/MANIFEST.in +0 -24
  36. pico_ioc-2.1.3/src/pico_ioc/_version.py +0 -1
  37. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/.coveragerc +0 -0
  38. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/.github/workflows/ci.yml +0 -0
  39. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/.github/workflows/docs.yml +0 -0
  40. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/.github/workflows/publish-to-pypi.yml +0 -0
  41. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/LICENSE +0 -0
  42. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/LEARN.md +0 -0
  43. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/README.md +0 -0
  44. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/adr/adr-0001-async-native.md +0 -0
  45. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/adr/adr-0002-tree-based-configuration.md +0 -0
  46. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/adr/adr-0003-context-aware-scopes.md +0 -0
  47. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/adr/adr-0004-observability.md +0 -0
  48. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/adr/adr-0005-aop.md +0 -0
  49. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/adr/adr-0006-eager-validation.md +0 -0
  50. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/adr/adr-0007-event_bus.md +0 -0
  51. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/adr/adr-0008-circular-dependencies.md +0 -0
  52. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/adr/adr-0009-flexible-provides.md +0 -0
  53. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/adr/adr-0010-unified-configuration.md +0 -0
  54. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/advanced-features/aop-interceptors.md +0 -0
  55. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/advanced-features/async-resolution.md +0 -0
  56. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/advanced-features/conditional-binding.md +0 -0
  57. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/advanced-features/event-bus.md +0 -0
  58. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/advanced-features/health-checks.md +0 -0
  59. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/api-reference/README.md +0 -0
  60. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/api-reference/decorators.md +0 -0
  61. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/api-reference/event_bus.md +0 -0
  62. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/api-reference/glossary.md +0 -0
  63. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/architecture/README.md +0 -0
  64. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/architecture/comparison.md +0 -0
  65. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/architecture/internals.md +0 -0
  66. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/cookbook/README.md +0 -0
  67. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/cookbook/pattern-aop-feature-toggle.md +0 -0
  68. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/cookbook/pattern-aop-profiling.md +0 -0
  69. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/cookbook/pattern-aop-security.md +0 -0
  70. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/cookbook/pattern-aop-structured-logging.md +0 -0
  71. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/cookbook/pattern-cli-app.md +0 -0
  72. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/cookbook/pattern-config-overrides.md +0 -0
  73. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/cookbook/pattern-cqrs.md +0 -0
  74. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/cookbook/pattern-hot-reload.md +0 -0
  75. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/cookbook/pattern-multi-tenant.md +0 -0
  76. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/getting-started.md +0 -0
  77. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/javascripts/extra.js +0 -0
  78. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/observability/README.md +0 -0
  79. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/observability/container-context.md +0 -0
  80. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/observability/exporting-graph.md +0 -0
  81. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/observability/observers-metrics.md +0 -0
  82. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/overview.md +0 -0
  83. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/requirements.txt +0 -0
  84. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/stylesheets/extra.css +0 -0
  85. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/user-guide/configuration-basic.md +0 -0
  86. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/user-guide/configuration-binding.md +0 -0
  87. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/user-guide/core-concepts.md +0 -0
  88. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/user-guide/qualifiers-lists.md +0 -0
  89. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/user-guide/scopes-lifecycle.md +0 -0
  90. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/docs/user-guide/testing.md +0 -0
  91. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/setup.cfg +0 -0
  92. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/src/pico_ioc/analysis.py +0 -0
  93. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/src/pico_ioc/aop.py +0 -0
  94. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/src/pico_ioc/config_builder.py +0 -0
  95. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/src/pico_ioc/config_registrar.py +0 -0
  96. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/src/pico_ioc/config_runtime.py +0 -0
  97. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/src/pico_ioc/constants.py +0 -0
  98. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/src/pico_ioc/decorators.py +0 -0
  99. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/src/pico_ioc/dependency_validator.py +0 -0
  100. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/src/pico_ioc/event_bus.py +0 -0
  101. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/src/pico_ioc/exceptions.py +0 -0
  102. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/src/pico_ioc/factory.py +0 -0
  103. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/src/pico_ioc/locator.py +0 -0
  104. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/src/pico_ioc/provider_selector.py +0 -0
  105. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/src/pico_ioc.egg-info/dependency_links.txt +0 -0
  106. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/src/pico_ioc.egg-info/requires.txt +0 -0
  107. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/src/pico_ioc.egg-info/top_level.txt +0 -0
  108. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/tests/test_aop.py +0 -0
  109. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/tests/test_collection_injection.py +0 -0
  110. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/tests/test_config_value.py +0 -0
  111. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/tests/test_configured.py +0 -0
  112. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/tests/test_container_context.py +0 -0
  113. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/tests/test_container_runtime.py +0 -0
  114. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/tests/test_container_self_injection.py +0 -0
  115. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/tests/test_event_bus.py +0 -0
  116. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/tests/test_pico_extends.py +0 -0
  117. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/tests/test_pico_integration.py +0 -0
  118. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/tests/test_protocol_resolution_and_graph.py +0 -0
  119. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/tests/test_provides_module_functions.py +0 -0
  120. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/tests/test_provides_static_methods.py +0 -0
  121. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/tests/test_proxy_unit.py +0 -0
  122. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/tests/test_resolution_graph.py +0 -0
  123. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/tests/test_scope.py +0 -0
  124. {pico_ioc-2.1.3 → pico_ioc-2.2.0}/tox.ini +0 -0
@@ -7,6 +7,23 @@ 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
+
10
27
  ## [2.1.3] - 2025-11-18
11
28
 
12
29
  ### 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
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 injection,inversion of control,decorator
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
  [![Docs](https://img.shields.io/badge/Docs-pico--ioc-blue?style=flat&logo=readthedocs&logoColor=white)](https://dperezcabrera.github.io/pico-ioc/)
63
63
  [![Interactive Lab](https://img.shields.io/badge/Learn-online-green?style=flat&logo=python&logoColor=white)](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.0+)
99
+ ## 🧩 Highlights (v2.2+)
101
100
 
102
- - Unified Configuration: Use `@configured` to bind both flat (ENV-like) and tree (YAML/JSON) sources via the `configuration(...)` builder (ADR-0010).
103
- - Async-aware AOP system: Method interceptors via `@intercepted_by`.
104
- - Scoped resolution: singleton, prototype, request, session, transaction, and custom scopes.
105
- - `UnifiedComponentProxy`: Transparent `lazy=True` and AOP proxy supporting serialization.
106
- - Tree-based configuration runtime: Advanced mapping with reusable adapters and discriminators (`Annotated[Union[...], Discriminator(...)]`).
107
- - Observable container context: Built-in stats, health checks (`@health`), observer hooks (`ContainerObserver`), dependency graph export (`export_graph`), and async cleanup.
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
- ```bash
122
- pip install pico-ioc[yaml]
123
- ```
120
+ ```bash
121
+ pip install pico-ioc[yaml]
122
+ ```
124
123
 
125
124
  -----
126
125
 
127
- ### ⚠️ Important Note for v2.1.3+
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
- **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.
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 or whose providers are async.
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
- ```python
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
- - Health checks:
318
- - Annotate health probes inside components with `@health` for container-level reporting.
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
- - For sync components: `container.close()`
323
- - For async components/resources: `await container.aclose()`
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
 
@@ -352,10 +347,11 @@ tox
352
347
 
353
348
  ## 🧾 Changelog
354
349
 
355
- See [CHANGELOG.md](./CHANGELOG.md) — Significant redesigns and features in v2.0+.
350
+ See [CHANGELOG.md](https://www.google.com/search?q=./CHANGELOG.md) — Significant redesigns and features in v2.0+.
356
351
 
357
352
  -----
358
353
 
359
354
  ## 📜 License
360
355
 
361
356
  MIT — [LICENSE](https://opensource.org/licenses/MIT)
357
+
@@ -12,7 +12,6 @@
12
12
  [![Docs](https://img.shields.io/badge/Docs-pico--ioc-blue?style=flat&logo=readthedocs&logoColor=white)](https://dperezcabrera.github.io/pico-ioc/)
13
13
  [![Interactive Lab](https://img.shields.io/badge/Learn-online-green?style=flat&logo=python&logoColor=white)](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.0+)
49
+ ## 🧩 Highlights (v2.2+)
51
50
 
52
- - Unified Configuration: Use `@configured` to bind both flat (ENV-like) and tree (YAML/JSON) sources via the `configuration(...)` builder (ADR-0010).
53
- - Async-aware AOP system: Method interceptors via `@intercepted_by`.
54
- - Scoped resolution: singleton, prototype, request, session, transaction, and custom scopes.
55
- - `UnifiedComponentProxy`: Transparent `lazy=True` and AOP proxy supporting serialization.
56
- - Tree-based configuration runtime: Advanced mapping with reusable adapters and discriminators (`Annotated[Union[...], Discriminator(...)]`).
57
- - Observable container context: Built-in stats, health checks (`@health`), observer hooks (`ContainerObserver`), dependency graph export (`export_graph`), and async cleanup.
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
- ```bash
72
- pip install pico-ioc[yaml]
73
- ```
70
+ ```bash
71
+ pip install pico-ioc[yaml]
72
+ ```
74
73
 
75
74
  -----
76
75
 
77
- ### ⚠️ Important Note for v2.1.3+
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
- **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.
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 or whose providers are async.
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
- ```python
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
- - Health checks:
268
- - Annotate health probes inside components with `@health` for container-level reporting.
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
- - For sync components: `container.close()`
273
- - For async components/resources: `await container.aclose()`
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
 
@@ -302,10 +297,11 @@ tox
302
297
 
303
298
  ## 🧾 Changelog
304
299
 
305
- See [CHANGELOG.md](./CHANGELOG.md) — Significant redesigns and features in v2.0+.
300
+ See [CHANGELOG.md](https://www.google.com/search?q=./CHANGELOG.md) — Significant redesigns and features in v2.0+.
306
301
 
307
302
  -----
308
303
 
309
304
  ## 📜 License
310
305
 
311
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.
@@ -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
  ---