pico-ioc 2.0.1__tar.gz → 2.0.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/CHANGELOG.md +85 -31
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/PKG-INFO +1 -1
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/src/pico_ioc/__init__.py +4 -0
- pico_ioc-2.0.3/src/pico_ioc/_version.py +1 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/src/pico_ioc/api.py +63 -29
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/src/pico_ioc/container.py +57 -54
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/src/pico_ioc/exceptions.py +17 -2
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/src/pico_ioc/factory.py +2 -2
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/src/pico_ioc.egg-info/PKG-INFO +1 -1
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/tests/test_pico_extends.py +2 -2
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/tests/test_pico_integration.py +31 -1
- pico_ioc-2.0.1/src/pico_ioc/_version.py +0 -1
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/.coveragerc +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/.github/workflows/ci.yml +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/.github/workflows/publish-to-pypi.yml +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/LICENSE +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/MANIFEST.in +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/README.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/README.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/adr/README.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/adr/adr-0001-async-native.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/adr/adr-0002-tree-based-configuration.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/adr/adr-0003-context-aware-scopes.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/adr/adr-0004-observability.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/adr/adr-0005-aop.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/adr/adr-0006-eager-validation.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/adr/adr-0007-event_bus.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/adr/adr-0008-circular-dependencies.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/adr/adr-0009-flexible-provides.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/advanced-features/README.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/advanced-features/aop-interceptors.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/advanced-features/async-resolution.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/advanced-features/conditional-binding.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/advanced-features/event-bus.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/advanced-features/health-checks.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/api-reference/README.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/api-reference/container.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/api-reference/decorators.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/api-reference/glossary.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/api-reference/protocols.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/architecture/README.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/architecture/comparison.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/architecture/design-principles.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/architecture/internals.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/cookbook/README.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/cookbook/pattern-aop-feature-toggle.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/cookbook/pattern-aop-profiling.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/cookbook/pattern-aop-security.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/cookbook/pattern-aop-structured-logging.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/cookbook/pattern-cli-app.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/cookbook/pattern-cqrs.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/cookbook/pattern-dynamic-langchain.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/cookbook/pattern-hot-reload.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/cookbook/pattern-multi-tenant.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/getting-started.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/integrations/README.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/integrations/ai-langchain.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/integrations/web-django.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/integrations/web-fastapi.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/integrations/web-flask.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/observability/README.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/observability/container-context.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/observability/exporting-graph.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/observability/observers-metrics.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/overview.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/user-guide/README.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/user-guide/configuration-basic.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/user-guide/configuration-binding.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/user-guide/core-concepts.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/user-guide/qualifiers-lists.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/user-guide/scopes-lifecycle.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/docs/user-guide/testing.md +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/pyproject.toml +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/setup.cfg +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/src/pico_ioc/aop.py +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/src/pico_ioc/config_runtime.py +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/src/pico_ioc/constants.py +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/src/pico_ioc/event_bus.py +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/src/pico_ioc/locator.py +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/src/pico_ioc/scope.py +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/src/pico_ioc.egg-info/SOURCES.txt +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/src/pico_ioc.egg-info/dependency_links.txt +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/src/pico_ioc.egg-info/requires.txt +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/src/pico_ioc.egg-info/top_level.txt +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/test.txt +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/tests/test_configured.py +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/tests/test_container_context.py +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/tests/test_container_runtime.py +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/tests/test_event_bus.py +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/tests/test_provides_module_functions.py +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/tests/test_provides_static_methods.py +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/tests/test_resolution_graph.py +0 -0
- {pico_ioc-2.0.1 → pico_ioc-2.0.3}/tox.ini +0 -0
|
@@ -5,6 +5,91 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.html).
|
|
7
7
|
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## [2.0.3] - 2025-10-26
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Injection now falls back from an annotated key to the parameter name when the annotated key is unbound, enabling resolution against string-key providers registered via `@provides("name")`.
|
|
15
|
+
- `ProviderNotFoundError` includes the requesting origin (component or key) to aid debugging and test assertions.
|
|
16
|
+
- Unified sync/async resolution path in `PicoContainer`:
|
|
17
|
+
- `get()` raises `AsyncResolutionError` if a provider returns an awaitable, guiding users to `aget()`.
|
|
18
|
+
- `aget()` awaits awaitables and applies aspects and caching consistently.
|
|
19
|
+
- Observers receive accurate resolve timings in both paths.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- `AsyncResolutionError` to signal misuse of `get()` when a provider is async.
|
|
24
|
+
- More informative tracer notes for parameter binding.
|
|
25
|
+
|
|
26
|
+
### Internal
|
|
27
|
+
|
|
28
|
+
- `ComponentFactory.get()` now accepts an `origin` to enrich `ProviderNotFoundError` messages.
|
|
29
|
+
- Lazy proxy creation calls `factory.get(key, origin="lazy")` to attribute provenance.
|
|
30
|
+
- Public API exports updated to include `AsyncResolutionError`.
|
|
31
|
+
|
|
32
|
+
### Compatibility
|
|
33
|
+
|
|
34
|
+
- No public API breaking changes. Internal factory signature changed but remains encapsulated within the container/registrar.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## [2.0.2] - 2025-10-26
|
|
39
|
+
|
|
40
|
+
### Fixed 🧩
|
|
41
|
+
|
|
42
|
+
* **`@provides` Decorator Execution**
|
|
43
|
+
Corrected an issue where the `@provides` decorator executed its wrapped function prematurely during module import, leading to runtime errors like `TypeError: Service() takes no arguments`.
|
|
44
|
+
The decorator now properly registers provider metadata without invoking the function until dependency resolution time.
|
|
45
|
+
|
|
46
|
+
### Added ✨
|
|
47
|
+
|
|
48
|
+
* **`FlatDictSource` Configuration Provider**
|
|
49
|
+
Introduced a lightweight configuration source for flat in-memory dictionaries.
|
|
50
|
+
Supports optional key prefixing and case sensitivity control for simple, programmatic configuration injection.
|
|
51
|
+
|
|
52
|
+
### Internal 🔧
|
|
53
|
+
|
|
54
|
+
* Updated type imports and registration logic in `api.py` to support `Mapping` for the new configuration source.
|
|
55
|
+
* Added `FlatDictSource` to the public API (`__all__` and import namespace).
|
|
56
|
+
|
|
57
|
+
### Notes 📝
|
|
58
|
+
|
|
59
|
+
* Fully backward compatible.
|
|
60
|
+
* This patch release focuses on decorator correctness and configuration flexibility improvements.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## [2.0.1] - 2025-10-25
|
|
65
|
+
|
|
66
|
+
### Added ✨
|
|
67
|
+
|
|
68
|
+
- **ADR-0009: Flexible `@provides` Support**
|
|
69
|
+
Implemented support for using `@provides` in additional contexts:
|
|
70
|
+
- `@staticmethod` methods within `@factory` classes
|
|
71
|
+
- `@classmethod` methods within `@factory` classes
|
|
72
|
+
- Module-level functions
|
|
73
|
+
These new provider types are discovered automatically during module scanning and participate fully in dependency resolution, validation, and graph generation.
|
|
74
|
+
|
|
75
|
+
- **Dependency Graph and Validation Enhancements**
|
|
76
|
+
- `_build_resolution_graph` now includes edges for all `@provides` functions, regardless of where they are defined.
|
|
77
|
+
- Fail-fast validation checks now cover static/class/module-level providers, reporting missing bindings consistently.
|
|
78
|
+
- Scope inference and promotion logic apply equally to these new provider types.
|
|
79
|
+
|
|
80
|
+
### Documentation 📚
|
|
81
|
+
|
|
82
|
+
- Expanded `docs/overview.md` to document the new flexible provider options (`@staticmethod`, `@classmethod`, module-level functions).
|
|
83
|
+
- Updated `docs/guide.md` with practical examples showing when to use each style of provider.
|
|
84
|
+
- Linked ADR-0009 for design rationale and migration guidance.
|
|
85
|
+
|
|
86
|
+
### Notes 📝
|
|
87
|
+
|
|
88
|
+
- This is a **minor feature release** introducing a major ergonomics improvement (ADR-0009).
|
|
89
|
+
- Fully backward compatible with existing factories, components, and configuration mechanisms.
|
|
90
|
+
- Encourages a lighter, more Pythonic style for simple provider declarations.
|
|
91
|
+
|
|
92
|
+
|
|
8
93
|
---
|
|
9
94
|
|
|
10
95
|
## [2.0.0] - 2025-10-23
|
|
@@ -58,37 +143,6 @@ This version marks a significant redesign and the first major public release, es
|
|
|
58
143
|
* Added comprehensive test suite covering core features, async behavior, AOP, configuration, scopes, and error handling.
|
|
59
144
|
* Introduced patterns for testing with overrides and profiles.
|
|
60
145
|
|
|
61
|
-
---
|
|
62
|
-
|
|
63
|
-
## [2.0.1] - 2025-10-25
|
|
64
|
-
|
|
65
|
-
### Added ✨
|
|
66
|
-
|
|
67
|
-
- **ADR-0009: Flexible `@provides` Support**
|
|
68
|
-
Implemented support for using `@provides` in additional contexts:
|
|
69
|
-
- `@staticmethod` methods within `@factory` classes
|
|
70
|
-
- `@classmethod` methods within `@factory` classes
|
|
71
|
-
- Module-level functions
|
|
72
|
-
These new provider types are discovered automatically during module scanning and participate fully in dependency resolution, validation, and graph generation.
|
|
73
|
-
|
|
74
|
-
- **Dependency Graph and Validation Enhancements**
|
|
75
|
-
- `_build_resolution_graph` now includes edges for all `@provides` functions, regardless of where they are defined.
|
|
76
|
-
- Fail-fast validation checks now cover static/class/module-level providers, reporting missing bindings consistently.
|
|
77
|
-
- Scope inference and promotion logic apply equally to these new provider types.
|
|
78
|
-
|
|
79
|
-
### Documentation 📚
|
|
80
|
-
|
|
81
|
-
- Expanded `docs/overview.md` to document the new flexible provider options (`@staticmethod`, `@classmethod`, module-level functions).
|
|
82
|
-
- Updated `docs/guide.md` with practical examples showing when to use each style of provider.
|
|
83
|
-
- Linked ADR-0009 for design rationale and migration guidance.
|
|
84
|
-
|
|
85
|
-
### Notes 📝
|
|
86
|
-
|
|
87
|
-
- This is a **minor feature release** introducing a major ergonomics improvement (ADR-0009).
|
|
88
|
-
- Fully backward compatible with existing factories, components, and configuration mechanisms.
|
|
89
|
-
- Encourages a lighter, more Pythonic style for simple provider declarations.
|
|
90
|
-
|
|
91
|
-
|
|
92
146
|
---
|
|
93
147
|
|
|
94
148
|
## [<2.0.0]
|
|
@@ -11,6 +11,7 @@ from .exceptions import (
|
|
|
11
11
|
ValidationError,
|
|
12
12
|
InvalidBindingError,
|
|
13
13
|
EventBusClosedError,
|
|
14
|
+
AsyncResolutionError,
|
|
14
15
|
)
|
|
15
16
|
from .api import (
|
|
16
17
|
component,
|
|
@@ -23,6 +24,7 @@ from .api import (
|
|
|
23
24
|
ConfigSource,
|
|
24
25
|
EnvSource,
|
|
25
26
|
FileSource,
|
|
27
|
+
FlatDictSource,
|
|
26
28
|
init,
|
|
27
29
|
configured,
|
|
28
30
|
)
|
|
@@ -50,6 +52,7 @@ __all__ = [
|
|
|
50
52
|
"SerializationError",
|
|
51
53
|
"ValidationError",
|
|
52
54
|
"InvalidBindingError",
|
|
55
|
+
"AsyncResolutionError",
|
|
53
56
|
"EventBusClosedError",
|
|
54
57
|
"component",
|
|
55
58
|
"factory",
|
|
@@ -76,6 +79,7 @@ __all__ = [
|
|
|
76
79
|
"EnvSource",
|
|
77
80
|
"FileSource",
|
|
78
81
|
"ConfigSource",
|
|
82
|
+
"FlatDictSource",
|
|
79
83
|
"init",
|
|
80
84
|
"configured",
|
|
81
85
|
"EventBus",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '2.0.3'
|
|
@@ -6,10 +6,9 @@ import importlib
|
|
|
6
6
|
import pkgutil
|
|
7
7
|
import logging
|
|
8
8
|
from dataclasses import is_dataclass, fields, dataclass, MISSING
|
|
9
|
-
from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Union, get_args, get_origin, Annotated, Protocol
|
|
9
|
+
from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Union, get_args, get_origin, Annotated, Protocol, Mapping
|
|
10
10
|
from .constants import LOGGER, PICO_INFRA, PICO_NAME, PICO_KEY, PICO_META
|
|
11
11
|
from .exceptions import (
|
|
12
|
-
ProviderNotFoundError,
|
|
13
12
|
CircularDependencyError,
|
|
14
13
|
ScopeError,
|
|
15
14
|
ConfigurationError,
|
|
@@ -54,6 +53,31 @@ class FileSource:
|
|
|
54
53
|
return str(v)
|
|
55
54
|
return None
|
|
56
55
|
|
|
56
|
+
class FlatDictSource(ConfigSource):
|
|
57
|
+
def __init__(self, data: Mapping[str, Any], prefix: str = "", case_sensitive: bool = True):
|
|
58
|
+
base = dict(data)
|
|
59
|
+
if case_sensitive:
|
|
60
|
+
self._data = {str(k): v for k, v in base.items()}
|
|
61
|
+
self._prefix = prefix
|
|
62
|
+
else:
|
|
63
|
+
self._data = {str(k).upper(): v for k, v in base.items()}
|
|
64
|
+
self._prefix = prefix.upper()
|
|
65
|
+
self._case_sensitive = case_sensitive
|
|
66
|
+
|
|
67
|
+
def get(self, key: str) -> Optional[str]:
|
|
68
|
+
if not key:
|
|
69
|
+
return None
|
|
70
|
+
k = f"{self._prefix}{key}" if self._prefix else key
|
|
71
|
+
if not self._case_sensitive:
|
|
72
|
+
k = k.upper()
|
|
73
|
+
v = self._data.get(k)
|
|
74
|
+
if v is None:
|
|
75
|
+
return None
|
|
76
|
+
if isinstance(v, (str, int, float, bool)):
|
|
77
|
+
return str(v)
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
57
81
|
def _meta_get(obj: Any) -> Dict[str, Any]:
|
|
58
82
|
m = getattr(obj, PICO_META, None)
|
|
59
83
|
if m is None:
|
|
@@ -132,29 +156,21 @@ def factory(
|
|
|
132
156
|
return c
|
|
133
157
|
return dec(cls) if cls else dec
|
|
134
158
|
|
|
135
|
-
def provides(
|
|
136
|
-
|
|
137
|
-
*,
|
|
138
|
-
name: Any = None,
|
|
139
|
-
qualifiers: Iterable[str] = (),
|
|
140
|
-
scope: str = "singleton",
|
|
141
|
-
primary: bool = False,
|
|
142
|
-
lazy: bool = False,
|
|
143
|
-
conditional_profiles: Iterable[str] = (),
|
|
144
|
-
conditional_require_env: Iterable[str] = (),
|
|
145
|
-
conditional_predicate: Optional[Callable[[], bool]] = None,
|
|
146
|
-
on_missing_selector: Optional[object] = None,
|
|
147
|
-
on_missing_priority: int = 0,
|
|
148
|
-
):
|
|
149
|
-
def dec(fn):
|
|
159
|
+
def provides(*dargs, **dkwargs):
|
|
160
|
+
def _apply(fn, key_hint, *, name=None, qualifiers=(), scope="singleton", primary=False, lazy=False, conditional_profiles=(), conditional_require_env=(), conditional_predicate=None, on_missing_selector=None, on_missing_priority=0):
|
|
150
161
|
target = fn.__func__ if isinstance(fn, (staticmethod, classmethod)) else fn
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
162
|
+
inferred_key = key_hint
|
|
163
|
+
if inferred_key is MISSING:
|
|
164
|
+
rt = _get_return_type(target)
|
|
165
|
+
if isinstance(rt, type):
|
|
166
|
+
inferred_key = rt
|
|
167
|
+
else:
|
|
168
|
+
inferred_key = getattr(target, "__name__", str(target))
|
|
169
|
+
setattr(target, PICO_INFRA, "provides")
|
|
170
|
+
pico_name = name if name is not None else (inferred_key if isinstance(inferred_key, str) else getattr(target, "__name__", str(target)))
|
|
171
|
+
setattr(target, PICO_NAME, pico_name)
|
|
172
|
+
setattr(target, PICO_KEY, inferred_key)
|
|
173
|
+
m = _meta_get(target)
|
|
158
174
|
m["qualifier"] = tuple(str(q) for q in qualifiers or ())
|
|
159
175
|
m["scope"] = scope
|
|
160
176
|
if primary:
|
|
@@ -169,8 +185,16 @@ def provides(
|
|
|
169
185
|
}
|
|
170
186
|
if on_missing_selector is not None:
|
|
171
187
|
m["on_missing"] = {"selector": on_missing_selector, "priority": int(on_missing_priority)}
|
|
172
|
-
return
|
|
173
|
-
|
|
188
|
+
return fn
|
|
189
|
+
|
|
190
|
+
if dargs and len(dargs) == 1 and inspect.isfunction(dargs[0]) and not dkwargs:
|
|
191
|
+
fn = dargs[0]
|
|
192
|
+
return _apply(fn, MISSING)
|
|
193
|
+
else:
|
|
194
|
+
key = dargs[0] if dargs else MISSING
|
|
195
|
+
def _decorator(fn):
|
|
196
|
+
return _apply(fn, key, **dkwargs)
|
|
197
|
+
return _decorator
|
|
174
198
|
|
|
175
199
|
class Qualifier(str):
|
|
176
200
|
__slots__ = ()
|
|
@@ -384,8 +408,18 @@ def _resolve_args(callable_obj: Callable[..., Any], pico: "PicoContainer") -> Di
|
|
|
384
408
|
try:
|
|
385
409
|
for kind, name, data in plan:
|
|
386
410
|
if kind == "key":
|
|
387
|
-
|
|
388
|
-
|
|
411
|
+
primary_key = data
|
|
412
|
+
tracer.note_param(primary_key, name)
|
|
413
|
+
try:
|
|
414
|
+
kwargs[name] = pico.get(primary_key)
|
|
415
|
+
except ProviderNotFoundError as first_error:
|
|
416
|
+
if primary_key != name:
|
|
417
|
+
try:
|
|
418
|
+
kwargs[name] = pico.get(name)
|
|
419
|
+
except ProviderNotFoundError:
|
|
420
|
+
raise first_error from None
|
|
421
|
+
else:
|
|
422
|
+
raise first_error from None
|
|
389
423
|
else:
|
|
390
424
|
vals = [pico.get(k) for k in data]
|
|
391
425
|
kwargs[name] = vals
|
|
@@ -549,7 +583,7 @@ class Registrar:
|
|
|
549
583
|
deferred.attach(pico, locator)
|
|
550
584
|
for key, md in list(self._metadata.items()):
|
|
551
585
|
if md.lazy:
|
|
552
|
-
original = self._factory.get(key)
|
|
586
|
+
original = self._factory.get(key, origin='lazy')
|
|
553
587
|
def lazy_proxy_provider(_orig=original, _p=pico):
|
|
554
588
|
return UnifiedComponentProxy(container=_p, object_creator=_orig)
|
|
555
589
|
self._factory.bind(key, lazy_proxy_provider)
|
|
@@ -4,7 +4,7 @@ import contextvars
|
|
|
4
4
|
from typing import Any, Dict, List, Optional, Tuple, overload, Union
|
|
5
5
|
from contextlib import contextmanager
|
|
6
6
|
from .constants import LOGGER, PICO_META
|
|
7
|
-
from .exceptions import CircularDependencyError, ComponentCreationError, ProviderNotFoundError
|
|
7
|
+
from .exceptions import CircularDependencyError, ComponentCreationError, ProviderNotFoundError, AsyncResolutionError
|
|
8
8
|
from .factory import ComponentFactory
|
|
9
9
|
from .locator import ComponentLocator
|
|
10
10
|
from .scope import ScopedCaches, ScopeManager
|
|
@@ -173,85 +173,88 @@ class PicoContainer:
|
|
|
173
173
|
return k
|
|
174
174
|
return key
|
|
175
175
|
|
|
176
|
-
|
|
177
|
-
def get(self, key: type) -> Any: ...
|
|
178
|
-
@overload
|
|
179
|
-
def get(self, key: str) -> Any: ...
|
|
180
|
-
def get(self, key: KeyT) -> Any:
|
|
176
|
+
def _resolve_or_create_internal(self, key: KeyT) -> Tuple[Any, float, bool]:
|
|
181
177
|
key = self._canonical_key(key)
|
|
182
178
|
cache = self._cache_for(key)
|
|
183
179
|
cached = cache.get(key)
|
|
184
180
|
if cached is not None:
|
|
185
181
|
self.context.cache_hit_count += 1
|
|
186
182
|
for o in self._observers: o.on_cache_hit(key)
|
|
187
|
-
return cached
|
|
183
|
+
return cached, 0.0, True
|
|
184
|
+
|
|
188
185
|
import time as _tm
|
|
189
186
|
t0 = _tm.perf_counter()
|
|
190
187
|
chain = list(_resolve_chain.get())
|
|
191
|
-
|
|
192
|
-
|
|
188
|
+
|
|
189
|
+
for k_in_chain in chain:
|
|
190
|
+
if k_in_chain == key:
|
|
193
191
|
details = self._tracer.describe_cycle(tuple(chain), key, self._locator)
|
|
194
192
|
raise ComponentCreationError(key, CircularDependencyError(chain, key, details=details))
|
|
193
|
+
|
|
195
194
|
token_chain = _resolve_chain.set(tuple(chain + [key]))
|
|
196
195
|
token_container = self.activate()
|
|
197
196
|
token_tracer = self._tracer.enter(key, via="provider")
|
|
197
|
+
|
|
198
|
+
requester = chain[-1] if chain else None
|
|
199
|
+
instance_or_awaitable = None
|
|
200
|
+
|
|
198
201
|
try:
|
|
199
|
-
provider = self._factory.get(key)
|
|
202
|
+
provider = self._factory.get(key, origin=requester)
|
|
200
203
|
try:
|
|
201
|
-
|
|
204
|
+
instance_or_awaitable = provider()
|
|
202
205
|
except ProviderNotFoundError as e:
|
|
203
206
|
raise
|
|
204
|
-
except Exception as
|
|
205
|
-
raise ComponentCreationError(key,
|
|
206
|
-
|
|
207
|
-
cache.put(key, instance)
|
|
208
|
-
self.context.resolve_count += 1
|
|
207
|
+
except Exception as creation_error:
|
|
208
|
+
raise ComponentCreationError(key, creation_error) from creation_error
|
|
209
|
+
|
|
209
210
|
took_ms = (_tm.perf_counter() - t0) * 1000
|
|
210
|
-
|
|
211
|
-
|
|
211
|
+
return instance_or_awaitable, took_ms, False
|
|
212
|
+
|
|
212
213
|
finally:
|
|
213
214
|
self._tracer.leave(token_tracer)
|
|
214
215
|
_resolve_chain.reset(token_chain)
|
|
215
216
|
self.deactivate(token_container)
|
|
217
|
+
|
|
218
|
+
@overload
|
|
219
|
+
def get(self, key: type) -> Any: ...
|
|
220
|
+
@overload
|
|
221
|
+
def get(self, key: str) -> Any: ...
|
|
222
|
+
def get(self, key: KeyT) -> Any:
|
|
223
|
+
instance_or_awaitable, took_ms, was_cached = self._resolve_or_create_internal(key)
|
|
224
|
+
|
|
225
|
+
if was_cached:
|
|
226
|
+
return instance_or_awaitable
|
|
227
|
+
|
|
228
|
+
instance = instance_or_awaitable
|
|
229
|
+
if inspect.isawaitable(instance):
|
|
230
|
+
key_name = getattr(key, '__name__', str(key))
|
|
231
|
+
raise AsyncResolutionError(key)
|
|
232
|
+
|
|
233
|
+
final_instance = self._maybe_wrap_with_aspects(key, instance)
|
|
234
|
+
cache = self._cache_for(key)
|
|
235
|
+
cache.put(key, final_instance)
|
|
236
|
+
self.context.resolve_count += 1
|
|
237
|
+
for o in self._observers: o.on_resolve(key, took_ms)
|
|
238
|
+
|
|
239
|
+
return final_instance
|
|
216
240
|
|
|
217
241
|
async def aget(self, key: KeyT) -> Any:
|
|
218
|
-
|
|
242
|
+
instance_or_awaitable, took_ms, was_cached = self._resolve_or_create_internal(key)
|
|
243
|
+
|
|
244
|
+
if was_cached:
|
|
245
|
+
return instance_or_awaitable
|
|
246
|
+
|
|
247
|
+
instance = instance_or_awaitable
|
|
248
|
+
if inspect.isawaitable(instance_or_awaitable):
|
|
249
|
+
instance = await instance_or_awaitable
|
|
250
|
+
|
|
251
|
+
final_instance = self._maybe_wrap_with_aspects(key, instance)
|
|
219
252
|
cache = self._cache_for(key)
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
import time as _tm
|
|
226
|
-
t0 = _tm.perf_counter()
|
|
227
|
-
chain = list(_resolve_chain.get())
|
|
228
|
-
for k in chain:
|
|
229
|
-
if k == key:
|
|
230
|
-
details = self._tracer.describe_cycle(tuple(chain), key, self._locator)
|
|
231
|
-
raise ComponentCreationError(key, CircularDependencyError(chain, key, details=details))
|
|
232
|
-
token_chain = _resolve_chain.set(tuple(chain + [key]))
|
|
233
|
-
token_container = self.activate()
|
|
234
|
-
token_tracer = self._tracer.enter(key, via="provider")
|
|
235
|
-
try:
|
|
236
|
-
provider = self._factory.get(key)
|
|
237
|
-
try:
|
|
238
|
-
instance = provider()
|
|
239
|
-
if inspect.isawaitable(instance):
|
|
240
|
-
instance = await instance
|
|
241
|
-
except ProviderNotFoundError as e:
|
|
242
|
-
raise
|
|
243
|
-
except Exception as e:
|
|
244
|
-
raise ComponentCreationError(key, e) from e
|
|
245
|
-
instance = self._maybe_wrap_with_aspects(key, instance)
|
|
246
|
-
cache.put(key, instance)
|
|
247
|
-
self.context.resolve_count += 1
|
|
248
|
-
took_ms = (_tm.perf_counter() - t0) * 1000
|
|
249
|
-
for o in self._observers: o.on_resolve(key, took_ms)
|
|
250
|
-
return instance
|
|
251
|
-
finally:
|
|
252
|
-
self._tracer.leave(token_tracer)
|
|
253
|
-
_resolve_chain.reset(token_chain)
|
|
254
|
-
self.deactivate(token_container)
|
|
253
|
+
cache.put(key, final_instance)
|
|
254
|
+
self.context.resolve_count += 1
|
|
255
|
+
for o in self._observers: o.on_resolve(key, took_ms)
|
|
256
|
+
|
|
257
|
+
return final_instance
|
|
255
258
|
|
|
256
259
|
def _resolve_type_key(self, key: type):
|
|
257
260
|
if not self._locator:
|
|
@@ -4,9 +4,15 @@ class PicoError(Exception):
|
|
|
4
4
|
pass
|
|
5
5
|
|
|
6
6
|
class ProviderNotFoundError(PicoError):
|
|
7
|
-
def __init__(self, key: Any):
|
|
8
|
-
|
|
7
|
+
def __init__(self, key: Any, origin: Any | None = None):
|
|
8
|
+
key_name = getattr(key, '__name__', str(key))
|
|
9
|
+
origin_name = getattr(origin, '__name__', str(origin)) if origin else "init"
|
|
10
|
+
super().__init__(
|
|
11
|
+
f"Provider for key '{key_name}' not found "
|
|
12
|
+
f"(required by: '{origin_name}')"
|
|
13
|
+
)
|
|
9
14
|
self.key = key
|
|
15
|
+
self.origin = origin
|
|
10
16
|
|
|
11
17
|
class CircularDependencyError(PicoError):
|
|
12
18
|
def __init__(self, chain: Iterable[Any], current: Any, details: str | None = None, hint: str | None = None):
|
|
@@ -51,6 +57,15 @@ class InvalidBindingError(ValidationError):
|
|
|
51
57
|
super().__init__("Invalid bindings:\n" + "\n".join(f"- {e}" for e in errors))
|
|
52
58
|
self.errors = errors
|
|
53
59
|
|
|
60
|
+
class AsyncResolutionError(PicoError):
|
|
61
|
+
def __init__(self, key: Any):
|
|
62
|
+
key_name = getattr(key, '__name__', str(key))
|
|
63
|
+
super().__init__(
|
|
64
|
+
f"Synchronous get() received an awaitable for key '{key_name}'. "
|
|
65
|
+
"Use aget() instead."
|
|
66
|
+
)
|
|
67
|
+
self.key = key
|
|
68
|
+
|
|
54
69
|
class EventBusError(PicoError):
|
|
55
70
|
def __init__(self, msg: str):
|
|
56
71
|
super().__init__(msg)
|
|
@@ -28,9 +28,9 @@ class ComponentFactory:
|
|
|
28
28
|
self._providers[key] = provider
|
|
29
29
|
def has(self, key: KeyT) -> bool:
|
|
30
30
|
return key in self._providers
|
|
31
|
-
def get(self, key: KeyT) -> Provider:
|
|
31
|
+
def get(self, key: KeyT, origin: KeyT) -> Provider:
|
|
32
32
|
if key not in self._providers:
|
|
33
|
-
raise ProviderNotFoundError(key)
|
|
33
|
+
raise ProviderNotFoundError(key, origin)
|
|
34
34
|
return self._providers[key]
|
|
35
35
|
|
|
36
36
|
class DeferredProvider:
|
|
@@ -188,12 +188,12 @@ def test_provider_not_found_error():
|
|
|
188
188
|
container = init(types.ModuleType("empty_mod"))
|
|
189
189
|
with pytest.raises(ProviderNotFoundError) as e_str:
|
|
190
190
|
container.get("non_existent_key")
|
|
191
|
-
assert "Provider
|
|
191
|
+
assert "Provider for key 'non_existent_key' not found" in str(e_str.value)
|
|
192
192
|
class NonExistentClass:
|
|
193
193
|
pass
|
|
194
194
|
with pytest.raises(ProviderNotFoundError) as e_type:
|
|
195
195
|
container.get(NonExistentClass)
|
|
196
|
-
assert "Provider
|
|
196
|
+
assert "Provider for key 'NonExistentClass' not found" in str(e_type.value)
|
|
197
197
|
|
|
198
198
|
def test_configuration_error_missing_value():
|
|
199
199
|
config_module = types.ModuleType("config_test_mod")
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
# tests/test_pico_integration.py
|
|
1
2
|
import pytest
|
|
2
3
|
import os
|
|
3
4
|
import json
|
|
4
5
|
import contextvars
|
|
6
|
+
import types
|
|
5
7
|
from dataclasses import dataclass
|
|
6
8
|
from typing import List, Optional, Annotated, Callable, Any, Protocol
|
|
7
9
|
import logging
|
|
@@ -193,6 +195,35 @@ for item in definitions:
|
|
|
193
195
|
|
|
194
196
|
test_scopes = {"request": request_scope}
|
|
195
197
|
|
|
198
|
+
class BaseNamedService:
|
|
199
|
+
def get_name(self) -> str:
|
|
200
|
+
return "Base"
|
|
201
|
+
|
|
202
|
+
class ConcreteNamedService(BaseNamedService):
|
|
203
|
+
def get_name(self) -> str:
|
|
204
|
+
return "ConcreteViaStringKey"
|
|
205
|
+
|
|
206
|
+
@component
|
|
207
|
+
class NeedsNamedServiceByTypeWithFallback:
|
|
208
|
+
def __init__(self, named_service: BaseNamedService):
|
|
209
|
+
self.injected_service = named_service
|
|
210
|
+
|
|
211
|
+
def build_fallback_test_module():
|
|
212
|
+
m = types.ModuleType("fallback_test_module")
|
|
213
|
+
@provides("named_service")
|
|
214
|
+
def build_concrete() -> ConcreteNamedService:
|
|
215
|
+
return ConcreteNamedService()
|
|
216
|
+
setattr(m, "NeedsNamedServiceByTypeWithFallback", NeedsNamedServiceByTypeWithFallback)
|
|
217
|
+
setattr(m, "build_concrete", build_concrete)
|
|
218
|
+
return m
|
|
219
|
+
|
|
220
|
+
def test_resolve_fallback_to_parameter_name():
|
|
221
|
+
test_mod = build_fallback_test_module()
|
|
222
|
+
container = init(modules=[test_mod])
|
|
223
|
+
instance = container.get(NeedsNamedServiceByTypeWithFallback)
|
|
224
|
+
assert isinstance(instance.injected_service, ConcreteNamedService)
|
|
225
|
+
assert instance.injected_service.get_name() == "ConcreteViaStringKey"
|
|
226
|
+
|
|
196
227
|
def test_basic_di():
|
|
197
228
|
container = init(test_module)
|
|
198
229
|
instance_b = container.get(ServiceBImpl)
|
|
@@ -314,4 +345,3 @@ def test_cleanup_called():
|
|
|
314
345
|
container.cleanup_all()
|
|
315
346
|
assert holder.closed
|
|
316
347
|
assert "ResourceHolder closed" in log_capture
|
|
317
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = '2.0.1'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|