pico-ioc 2.2.0__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.2.0 → pico_ioc-2.2.1}/CHANGELOG.md +14 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/PKG-INFO +2 -2
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/README.md +1 -1
- pico_ioc-2.2.1/docs/README.md +142 -0
- pico_ioc-2.2.1/docs/overview.md +5 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/mkdocs.yml +0 -4
- pico_ioc-2.2.1/src/pico_ioc/_version.py +1 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/src/pico_ioc/aop.py +13 -11
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/src/pico_ioc/container.py +1 -1
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/src/pico_ioc/scope.py +14 -6
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/src/pico_ioc.egg-info/PKG-INFO +2 -2
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/src/pico_ioc.egg-info/SOURCES.txt +15 -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_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.2.0/docs/README.md +0 -154
- pico_ioc-2.2.0/docs/overview.md +0 -191
- pico_ioc-2.2.0/src/pico_ioc/_version.py +0 -1
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/.coveragerc +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/.github/workflows/ci.yml +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/.github/workflows/docs.yml +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/.github/workflows/publish-to-pypi.yml +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/LICENSE +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/MANIFEST.in +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/LEARN.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/adr/README.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/adr/adr-0001-async-native.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/adr/adr-0002-tree-based-configuration.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/adr/adr-0003-context-aware-scopes.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/adr/adr-0004-observability.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/adr/adr-0005-aop.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/adr/adr-0006-eager-validation.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/adr/adr-0007-event_bus.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/adr/adr-0008-circular-dependencies.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/adr/adr-0009-flexible-provides.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/adr/adr-0010-unified-configuration.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/adr/adr-0011-custom-scanners.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/advanced-features/README.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/advanced-features/aop-interceptors.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/advanced-features/aop-proxies.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/advanced-features/async-resolution.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/advanced-features/conditional-binding.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/advanced-features/custom-scanners.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/advanced-features/event-bus.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/advanced-features/health-checks.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/advanced-features/self-injection.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/api-reference/README.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/api-reference/analysis-api.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/api-reference/constants.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/api-reference/container-context-api.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/api-reference/container.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/api-reference/decorators.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/api-reference/event_bus.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/api-reference/exceptions.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/api-reference/glossary.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/api-reference/protocols.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/api-reference/provider-selector.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/architecture/README.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/architecture/comparison.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/architecture/design-principles.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/architecture/docs-assets.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/architecture/internals.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/architecture/runtime-model-scheduling.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/cookbook/README.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/cookbook/pattern-aop-feature-toggle.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/cookbook/pattern-aop-profiling.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/cookbook/pattern-aop-security.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/cookbook/pattern-aop-structured-logging.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/cookbook/pattern-cli-app.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/cookbook/pattern-config-overrides.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/cookbook/pattern-cqrs.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/cookbook/pattern-hot-reload.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/cookbook/pattern-multi-tenant.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/getting-started.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/javascripts/extra.js +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/observability/README.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/observability/container-context.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/observability/exporting-graph.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/observability/observers-metrics.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/observability/resolution-graph.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/requirements.txt +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/stylesheets/extra.css +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/user-guide/README.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/user-guide/configuration-basic.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/user-guide/configuration-binding.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/user-guide/core-concepts.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/user-guide/qualifiers-lists.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/user-guide/scopes-lifecycle.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/docs/user-guide/testing.md +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/pyproject.toml +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/setup.cfg +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/src/pico_ioc/__init__.py +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/src/pico_ioc/analysis.py +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/src/pico_ioc/api.py +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/src/pico_ioc/component_scanner.py +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/src/pico_ioc/config_builder.py +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/src/pico_ioc/config_registrar.py +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/src/pico_ioc/config_runtime.py +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/src/pico_ioc/constants.py +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/src/pico_ioc/decorators.py +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/src/pico_ioc/dependency_validator.py +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/src/pico_ioc/event_bus.py +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/src/pico_ioc/exceptions.py +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/src/pico_ioc/factory.py +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/src/pico_ioc/locator.py +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/src/pico_ioc/provider_selector.py +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/src/pico_ioc/registrar.py +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/src/pico_ioc.egg-info/dependency_links.txt +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/src/pico_ioc.egg-info/requires.txt +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/src/pico_ioc.egg-info/top_level.txt +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/tests/test_aop.py +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/tests/test_collection_injection.py +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/tests/test_config_value.py +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/tests/test_configured.py +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/tests/test_container_context.py +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/tests/test_container_runtime.py +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/tests/test_container_self_injection.py +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/tests/test_custom_scanner.py +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/tests/test_event_bus.py +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/tests/test_pico_extends.py +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/tests/test_pico_integration.py +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/tests/test_protocol_resolution_and_graph.py +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/tests/test_provides_module_functions.py +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/tests/test_provides_static_methods.py +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/tests/test_proxy_unit.py +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/tests/test_resolution_graph.py +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/tests/test_scope.py +0 -0
- {pico_ioc-2.2.0 → pico_ioc-2.2.1}/tox.ini +0 -0
|
@@ -7,6 +7,20 @@ 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
|
+
|
|
10
24
|
## [2.2.0] - 2025-11-29
|
|
11
25
|
|
|
12
26
|
### Added ✨
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pico-ioc
|
|
3
|
-
Version: 2.2.
|
|
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
|
|
@@ -347,7 +347,7 @@ tox
|
|
|
347
347
|
|
|
348
348
|
## 🧾 Changelog
|
|
349
349
|
|
|
350
|
-
See [CHANGELOG.md](
|
|
350
|
+
See [CHANGELOG.md](./CHANGELOG.md) — Significant redesigns and features in v2.0+.
|
|
351
351
|
|
|
352
352
|
-----
|
|
353
353
|
|
|
@@ -297,7 +297,7 @@ tox
|
|
|
297
297
|
|
|
298
298
|
## 🧾 Changelog
|
|
299
299
|
|
|
300
|
-
See [CHANGELOG.md](
|
|
300
|
+
See [CHANGELOG.md](./CHANGELOG.md) — Significant redesigns and features in v2.0+.
|
|
301
301
|
|
|
302
302
|
-----
|
|
303
303
|
|
|
@@ -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
|
|
@@ -146,10 +146,6 @@ nav:
|
|
|
146
146
|
- Custom Scanners: advanced-features/custom-scanners.md
|
|
147
147
|
- Self Injection: advanced-features/self-injection.md
|
|
148
148
|
|
|
149
|
-
- Integrations:
|
|
150
|
-
- Overview: integrations.md
|
|
151
|
-
- Celery: integrations-celery.md
|
|
152
|
-
|
|
153
149
|
- Observability:
|
|
154
150
|
- observability/README.md
|
|
155
151
|
- Container Context: observability/container-context.md
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '2.2.1'
|
|
@@ -145,23 +145,25 @@ class UnifiedComponentProxy:
|
|
|
145
145
|
return
|
|
146
146
|
|
|
147
147
|
lock = object.__getattribute__(self, "_lock")
|
|
148
|
-
tgt = object.__getattribute__(self, "_target")
|
|
149
|
-
if tgt is not None:
|
|
150
|
-
return
|
|
151
148
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
149
|
+
with lock:
|
|
150
|
+
# Double-check inside lock to prevent race condition
|
|
151
|
+
tgt = object.__getattribute__(self, "_target")
|
|
152
|
+
if tgt is not None:
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
creator = object.__getattribute__(self, "_creator")
|
|
156
|
+
container = object.__getattribute__(self, "_container")
|
|
157
|
+
|
|
158
|
+
tgt = creator()
|
|
159
|
+
object.__setattr__(self, "_target", tgt)
|
|
160
|
+
|
|
161
|
+
# Run configure methods outside the lock to avoid blocking
|
|
157
162
|
if container and hasattr(container, "_run_configure_methods"):
|
|
158
163
|
res = container._run_configure_methods(tgt)
|
|
159
164
|
if inspect.isawaitable(res):
|
|
160
165
|
await res
|
|
161
166
|
|
|
162
|
-
with lock:
|
|
163
|
-
object.__setattr__(self, "_target", tgt)
|
|
164
|
-
|
|
165
167
|
def _scope_signature(self) -> Tuple[Any, ...]:
|
|
166
168
|
container = object.__getattribute__(self, "_container")
|
|
167
169
|
key = object.__getattribute__(self, "_component_key")
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import contextvars
|
|
2
2
|
import inspect
|
|
3
|
+
import logging
|
|
3
4
|
from typing import Any, Dict, Optional, Tuple
|
|
4
5
|
from collections import OrderedDict
|
|
5
6
|
from .exceptions import ScopeError
|
|
6
7
|
|
|
8
|
+
_logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
7
10
|
class ScopeProtocol:
|
|
8
11
|
def get_id(self) -> Any | None: ...
|
|
9
12
|
|
|
@@ -108,10 +111,15 @@ class ScopedCaches:
|
|
|
108
111
|
if meta.get("cleanup", False):
|
|
109
112
|
try:
|
|
110
113
|
m()
|
|
111
|
-
except Exception:
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
114
|
+
except Exception as e:
|
|
115
|
+
_logger.warning(
|
|
116
|
+
"Cleanup method %s.%s failed: %s",
|
|
117
|
+
type(obj).__name__,
|
|
118
|
+
getattr(m, "__name__", "<unknown>"),
|
|
119
|
+
e
|
|
120
|
+
)
|
|
121
|
+
except Exception as e:
|
|
122
|
+
_logger.debug("Failed to inspect object for cleanup: %s", e)
|
|
115
123
|
|
|
116
124
|
def cleanup_scope(self, scope_name: str, scope_id: Any) -> None:
|
|
117
125
|
bucket = self._by_scope.get(scope_name)
|
|
@@ -123,8 +131,8 @@ class ScopedCaches:
|
|
|
123
131
|
try:
|
|
124
132
|
for _, obj in container.items():
|
|
125
133
|
self._cleanup_object(obj)
|
|
126
|
-
except Exception:
|
|
127
|
-
|
|
134
|
+
except Exception as e:
|
|
135
|
+
_logger.debug("Failed to cleanup container: %s", e)
|
|
128
136
|
|
|
129
137
|
def for_scope(self, scopes: ScopeManager, scope: str) -> ComponentContainer:
|
|
130
138
|
if scope == "singleton":
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pico-ioc
|
|
3
|
-
Version: 2.2.
|
|
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
|
|
@@ -347,7 +347,7 @@ tox
|
|
|
347
347
|
|
|
348
348
|
## 🧾 Changelog
|
|
349
349
|
|
|
350
|
-
See [CHANGELOG.md](
|
|
350
|
+
See [CHANGELOG.md](./CHANGELOG.md) — Significant redesigns and features in v2.0+.
|
|
351
351
|
|
|
352
352
|
-----
|
|
353
353
|
|
|
@@ -102,14 +102,27 @@ src/pico_ioc.egg-info/dependency_links.txt
|
|
|
102
102
|
src/pico_ioc.egg-info/requires.txt
|
|
103
103
|
src/pico_ioc.egg-info/top_level.txt
|
|
104
104
|
tests/test_aop.py
|
|
105
|
+
tests/test_aop_coverage.py
|
|
106
|
+
tests/test_aop_extended.py
|
|
107
|
+
tests/test_bugfixes_v221.py
|
|
105
108
|
tests/test_collection_injection.py
|
|
109
|
+
tests/test_config_builder.py
|
|
110
|
+
tests/test_config_registrar_coverage.py
|
|
111
|
+
tests/test_config_runtime_coverage.py
|
|
106
112
|
tests/test_config_value.py
|
|
107
113
|
tests/test_configured.py
|
|
108
114
|
tests/test_container_context.py
|
|
115
|
+
tests/test_container_coverage.py
|
|
116
|
+
tests/test_container_coverage_boost.py
|
|
117
|
+
tests/test_container_extended.py
|
|
118
|
+
tests/test_container_integrated_coverage.py
|
|
109
119
|
tests/test_container_runtime.py
|
|
110
120
|
tests/test_container_self_injection.py
|
|
111
121
|
tests/test_custom_scanner.py
|
|
112
122
|
tests/test_event_bus.py
|
|
123
|
+
tests/test_event_bus_coverage_boost.py
|
|
124
|
+
tests/test_event_bus_extended.py
|
|
125
|
+
tests/test_locator.py
|
|
113
126
|
tests/test_pico_extends.py
|
|
114
127
|
tests/test_pico_integration.py
|
|
115
128
|
tests/test_protocol_resolution_and_graph.py
|
|
@@ -117,4 +130,5 @@ tests/test_provides_module_functions.py
|
|
|
117
130
|
tests/test_provides_static_methods.py
|
|
118
131
|
tests/test_proxy_unit.py
|
|
119
132
|
tests/test_resolution_graph.py
|
|
120
|
-
tests/test_scope.py
|
|
133
|
+
tests/test_scope.py
|
|
134
|
+
tests/test_scope_coverage.py
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import pickle
|
|
3
|
+
import inspect
|
|
4
|
+
import threading
|
|
5
|
+
from pico_ioc.aop import (
|
|
6
|
+
UnifiedComponentProxy,
|
|
7
|
+
intercepted_by,
|
|
8
|
+
MethodInterceptor,
|
|
9
|
+
MethodCtx,
|
|
10
|
+
_gather_interceptors_for_method
|
|
11
|
+
)
|
|
12
|
+
from pico_ioc.exceptions import SerializationError, AsyncResolutionError
|
|
13
|
+
|
|
14
|
+
def test_intercepted_by_validation():
|
|
15
|
+
with pytest.raises(TypeError, match="requires at least one"):
|
|
16
|
+
intercepted_by()
|
|
17
|
+
|
|
18
|
+
with pytest.raises(TypeError, match="expects interceptor classes"):
|
|
19
|
+
intercepted_by("not_a_class")
|
|
20
|
+
|
|
21
|
+
decorator = intercepted_by(MethodInterceptor)
|
|
22
|
+
with pytest.raises(TypeError, match="only decorate callables"):
|
|
23
|
+
decorator("not_callable")
|
|
24
|
+
|
|
25
|
+
def test_proxy_init_validation():
|
|
26
|
+
with pytest.raises(ValueError, match="non-null container"):
|
|
27
|
+
UnifiedComponentProxy(container=None)
|
|
28
|
+
|
|
29
|
+
with pytest.raises(ValueError, match="requires either a target"):
|
|
30
|
+
UnifiedComponentProxy(container="stub")
|
|
31
|
+
|
|
32
|
+
def test_proxy_creator_errors():
|
|
33
|
+
proxy = UnifiedComponentProxy(container="stub", object_creator="not_callable")
|
|
34
|
+
with pytest.raises(TypeError, match="must be callable"):
|
|
35
|
+
getattr(proxy, "any_attr")
|
|
36
|
+
|
|
37
|
+
proxy = UnifiedComponentProxy(container="stub", object_creator=lambda: None)
|
|
38
|
+
with pytest.raises(RuntimeError, match="returned None"):
|
|
39
|
+
getattr(proxy, "any_attr")
|
|
40
|
+
|
|
41
|
+
def test_proxy_async_error_on_sync_access():
|
|
42
|
+
async def async_configure(obj):
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
class ContainerMock:
|
|
46
|
+
def _run_configure_methods(self, obj):
|
|
47
|
+
return async_configure(obj)
|
|
48
|
+
|
|
49
|
+
proxy = UnifiedComponentProxy(
|
|
50
|
+
container=ContainerMock(),
|
|
51
|
+
object_creator=lambda: "target"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
with pytest.raises(AsyncResolutionError):
|
|
55
|
+
str(proxy)
|
|
56
|
+
|
|
57
|
+
def test_proxy_serialization_error():
|
|
58
|
+
class Unpicklable:
|
|
59
|
+
def __getstate__(self):
|
|
60
|
+
raise ValueError("No pickle")
|
|
61
|
+
|
|
62
|
+
proxy = UnifiedComponentProxy(container="stub", target=Unpicklable())
|
|
63
|
+
|
|
64
|
+
with pytest.raises(SerializationError):
|
|
65
|
+
pickle.dumps(proxy)
|
|
66
|
+
|
|
67
|
+
with pytest.raises(SerializationError):
|
|
68
|
+
proxy.__setstate__({"data": b"bad_data"})
|
|
69
|
+
|
|
70
|
+
def test_gather_interceptors_edge_cases():
|
|
71
|
+
assert _gather_interceptors_for_method(str, "non_existent") == ()
|
|
72
|
+
assert _gather_interceptors_for_method(list, "append") == ()
|