pico-ioc 2.1.0__py3-none-any.whl → 2.1.2__py3-none-any.whl
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/__init__.py +2 -1
- pico_ioc/_version.py +1 -1
- pico_ioc/analysis.py +47 -4
- pico_ioc/aop.py +18 -7
- pico_ioc/api.py +50 -18
- pico_ioc/config_builder.py +1 -5
- pico_ioc/config_registrar.py +21 -4
- pico_ioc/config_runtime.py +39 -13
- pico_ioc/container.py +88 -33
- pico_ioc/decorators.py +2 -1
- pico_ioc/event_bus.py +24 -25
- pico_ioc/locator.py +9 -5
- pico_ioc/registrar.py +20 -1
- pico_ioc/scope.py +13 -4
- {pico_ioc-2.1.0.dist-info → pico_ioc-2.1.2.dist-info}/METADATA +123 -63
- pico_ioc-2.1.2.dist-info/RECORD +25 -0
- pico_ioc-2.1.0.dist-info/RECORD +0 -25
- {pico_ioc-2.1.0.dist-info → pico_ioc-2.1.2.dist-info}/WHEEL +0 -0
- {pico_ioc-2.1.0.dist-info → pico_ioc-2.1.2.dist-info}/licenses/LICENSE +0 -0
- {pico_ioc-2.1.0.dist-info → pico_ioc-2.1.2.dist-info}/top_level.txt +0 -0
pico_ioc/__init__.py
CHANGED
|
@@ -29,7 +29,7 @@ from .factory import ComponentFactory, ProviderMetadata, DeferredProvider
|
|
|
29
29
|
from .aop import MethodCtx, MethodInterceptor, intercepted_by, UnifiedComponentProxy, health, ContainerObserver
|
|
30
30
|
from .container import PicoContainer
|
|
31
31
|
from .event_bus import EventBus, ExecPolicy, ErrorPolicy, Event, subscribe, AutoSubscriberMixin
|
|
32
|
-
from .config_runtime import JsonTreeSource, YamlTreeSource, DictSource, Discriminator
|
|
32
|
+
from .config_runtime import JsonTreeSource, YamlTreeSource, DictSource, Discriminator, Value
|
|
33
33
|
from .analysis import DependencyRequest, analyze_callable_dependencies
|
|
34
34
|
|
|
35
35
|
__all__ = [
|
|
@@ -88,6 +88,7 @@ __all__ = [
|
|
|
88
88
|
"YamlTreeSource",
|
|
89
89
|
"DictSource",
|
|
90
90
|
"Discriminator",
|
|
91
|
+
"Value",
|
|
91
92
|
"DependencyRequest",
|
|
92
93
|
"analyze_callable_dependencies",
|
|
93
94
|
]
|
pico_ioc/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = '2.1.
|
|
1
|
+
__version__ = '2.1.2'
|
pico_ioc/analysis.py
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import inspect
|
|
2
2
|
from dataclasses import dataclass
|
|
3
|
-
|
|
3
|
+
import collections
|
|
4
|
+
import collections.abc
|
|
5
|
+
from typing import (
|
|
6
|
+
Any, Callable, List, Optional, Tuple, Union, get_args, get_origin, Annotated,
|
|
7
|
+
Iterable, Set, Sequence, Collection, Deque, FrozenSet, MutableSequence, MutableSet,
|
|
8
|
+
Dict, Mapping
|
|
9
|
+
)
|
|
4
10
|
from .decorators import Qualifier
|
|
5
11
|
|
|
6
12
|
KeyT = Union[str, type]
|
|
@@ -12,6 +18,8 @@ class DependencyRequest:
|
|
|
12
18
|
is_list: bool = False
|
|
13
19
|
qualifier: Optional[str] = None
|
|
14
20
|
is_optional: bool = False
|
|
21
|
+
is_dict: bool = False
|
|
22
|
+
dict_key_type: Any = None
|
|
15
23
|
|
|
16
24
|
def _extract_annotated(ann: Any) -> Tuple[Any, Optional[str]]:
|
|
17
25
|
qualifier = None
|
|
@@ -44,6 +52,24 @@ def analyze_callable_dependencies(callable_obj: Callable[..., Any]) -> Tuple[Dep
|
|
|
44
52
|
|
|
45
53
|
plan: List[DependencyRequest] = []
|
|
46
54
|
|
|
55
|
+
SUPPORTED_COLLECTION_ORIGINS = (
|
|
56
|
+
# Runtime types
|
|
57
|
+
list,
|
|
58
|
+
set,
|
|
59
|
+
tuple,
|
|
60
|
+
frozenset,
|
|
61
|
+
collections.deque,
|
|
62
|
+
|
|
63
|
+
# Typing ABCs (from get_origin)
|
|
64
|
+
collections.abc.Iterable,
|
|
65
|
+
collections.abc.Collection,
|
|
66
|
+
collections.abc.Sequence,
|
|
67
|
+
collections.abc.MutableSequence,
|
|
68
|
+
collections.abc.MutableSet
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
SUPPORTED_DICT_ORIGINS = (dict, collections.abc.Mapping)
|
|
72
|
+
|
|
47
73
|
for name, param in sig.parameters.items():
|
|
48
74
|
if name in ("self", "cls"):
|
|
49
75
|
continue
|
|
@@ -56,19 +82,35 @@ def analyze_callable_dependencies(callable_obj: Callable[..., Any]) -> Tuple[Dep
|
|
|
56
82
|
base_type, qualifier = _extract_annotated(base_type)
|
|
57
83
|
|
|
58
84
|
is_list = False
|
|
85
|
+
is_dict = False
|
|
59
86
|
elem_t = None
|
|
87
|
+
dict_key_t = None
|
|
60
88
|
|
|
61
89
|
origin = get_origin(base_type)
|
|
62
|
-
|
|
90
|
+
|
|
91
|
+
if origin in SUPPORTED_COLLECTION_ORIGINS:
|
|
63
92
|
is_list = True
|
|
64
93
|
elem_t = get_args(base_type)[0] if get_args(base_type) else Any
|
|
65
94
|
elem_t, list_qualifier = _extract_annotated(elem_t)
|
|
66
95
|
if qualifier is None:
|
|
67
96
|
qualifier = list_qualifier
|
|
97
|
+
elif origin in SUPPORTED_DICT_ORIGINS:
|
|
98
|
+
is_dict = True
|
|
99
|
+
args = get_args(base_type)
|
|
100
|
+
dict_key_t = args[0] if args else Any
|
|
101
|
+
elem_t = args[1] if len(args) > 1 else Any
|
|
102
|
+
elem_t, dict_qualifier = _extract_annotated(elem_t)
|
|
103
|
+
if qualifier is None:
|
|
104
|
+
qualifier = dict_qualifier
|
|
68
105
|
|
|
69
106
|
final_key: KeyT
|
|
107
|
+
final_dict_key_type: Any = None
|
|
108
|
+
|
|
70
109
|
if is_list:
|
|
71
110
|
final_key = elem_t if isinstance(elem_t, type) else Any
|
|
111
|
+
elif is_dict:
|
|
112
|
+
final_key = elem_t if isinstance(elem_t, type) else Any
|
|
113
|
+
final_dict_key_type = dict_key_t
|
|
72
114
|
elif isinstance(base_type, type):
|
|
73
115
|
final_key = base_type
|
|
74
116
|
elif isinstance(base_type, str):
|
|
@@ -84,9 +126,10 @@ def analyze_callable_dependencies(callable_obj: Callable[..., Any]) -> Tuple[Dep
|
|
|
84
126
|
key=final_key,
|
|
85
127
|
is_list=is_list,
|
|
86
128
|
qualifier=qualifier,
|
|
87
|
-
is_optional=is_optional or (param.default is not inspect._empty)
|
|
129
|
+
is_optional=is_optional or (param.default is not inspect._empty),
|
|
130
|
+
is_dict=is_dict,
|
|
131
|
+
dict_key_type=final_dict_key_type
|
|
88
132
|
)
|
|
89
133
|
)
|
|
90
134
|
|
|
91
135
|
return tuple(plan)
|
|
92
|
-
|
pico_ioc/aop.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
# src/pico_ioc/aop.py
|
|
2
|
+
|
|
1
3
|
import inspect
|
|
2
4
|
import pickle
|
|
3
5
|
import threading
|
|
4
6
|
from typing import Any, Callable, Dict, List, Tuple, Protocol, Union
|
|
5
|
-
from .exceptions import SerializationError
|
|
7
|
+
from .exceptions import SerializationError, AsyncResolutionError
|
|
6
8
|
|
|
7
9
|
KeyT = Union[str, type]
|
|
8
10
|
|
|
@@ -119,9 +121,19 @@ class UnifiedComponentProxy:
|
|
|
119
121
|
tgt = creator()
|
|
120
122
|
if tgt is None:
|
|
121
123
|
raise RuntimeError("UnifiedComponentProxy object_creator returned None")
|
|
124
|
+
|
|
125
|
+
container = object.__getattribute__(self, "_container")
|
|
126
|
+
if container and hasattr(container, "_run_configure_methods"):
|
|
127
|
+
res = container._run_configure_methods(tgt)
|
|
128
|
+
if inspect.isawaitable(res):
|
|
129
|
+
raise AsyncResolutionError(
|
|
130
|
+
f"Lazy component {type(tgt).__name__} requires async "
|
|
131
|
+
"@configure but was resolved via sync get()"
|
|
132
|
+
)
|
|
133
|
+
|
|
122
134
|
object.__setattr__(self, "_target", tgt)
|
|
123
135
|
return tgt
|
|
124
|
-
|
|
136
|
+
|
|
125
137
|
def _scope_signature(self) -> Tuple[Any, ...]:
|
|
126
138
|
container = object.__getattribute__(self, "_container")
|
|
127
139
|
target = object.__getattribute__(self, "_target")
|
|
@@ -138,7 +150,7 @@ class UnifiedComponentProxy:
|
|
|
138
150
|
return ()
|
|
139
151
|
return (container.scopes.get_id(sc),)
|
|
140
152
|
return ()
|
|
141
|
-
|
|
153
|
+
|
|
142
154
|
def _build_wrapped(self, name: str, bound: Callable[..., Any], interceptors_cls: Tuple[type, ...]):
|
|
143
155
|
container = object.__getattribute__(self, "_container")
|
|
144
156
|
interceptors = [container.get(cls) for cls in interceptors_cls]
|
|
@@ -182,11 +194,11 @@ class UnifiedComponentProxy:
|
|
|
182
194
|
raise RuntimeError(f"Async interceptor returned awaitable on sync method: {name}")
|
|
183
195
|
return res
|
|
184
196
|
return sig, sw, interceptors_cls
|
|
185
|
-
|
|
197
|
+
|
|
186
198
|
@property
|
|
187
199
|
def __class__(self):
|
|
188
200
|
return self._get_real_object().__class__
|
|
189
|
-
|
|
201
|
+
|
|
190
202
|
def __getattr__(self, name: str) -> Any:
|
|
191
203
|
target = self._get_real_object()
|
|
192
204
|
attr = getattr(target, name)
|
|
@@ -211,7 +223,7 @@ class UnifiedComponentProxy:
|
|
|
211
223
|
sig, wrapped, cls_tuple = self._build_wrapped(name, attr, interceptors_cls)
|
|
212
224
|
cache[name] = (sig, wrapped, cls_tuple)
|
|
213
225
|
return wrapped
|
|
214
|
-
|
|
226
|
+
|
|
215
227
|
def __setattr__(self, name, value): setattr(self._get_real_object(), name, value)
|
|
216
228
|
def __delattr__(self, name): delattr(self._get_real_object(), name)
|
|
217
229
|
def __str__(self): return str(self._get_real_object())
|
|
@@ -275,4 +287,3 @@ class UnifiedComponentProxy:
|
|
|
275
287
|
return (pickle.loads, (data,))
|
|
276
288
|
except Exception as e:
|
|
277
289
|
raise SerializationError(f"Proxy target is not serializable: {e}")
|
|
278
|
-
|
pico_ioc/api.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
# src/pico_ioc/api.py
|
|
2
|
+
|
|
1
3
|
import importlib
|
|
2
4
|
import pkgutil
|
|
3
5
|
import logging
|
|
4
6
|
import inspect
|
|
5
|
-
from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Union
|
|
7
|
+
from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Union
|
|
6
8
|
from .exceptions import ConfigurationError, InvalidBindingError
|
|
7
9
|
from .factory import ComponentFactory, ProviderMetadata
|
|
8
10
|
from .locator import ComponentLocator
|
|
@@ -10,6 +12,7 @@ from .scope import ScopeManager, ScopedCaches
|
|
|
10
12
|
from .container import PicoContainer
|
|
11
13
|
from .decorators import component, factory, provides, Qualifier, configure, cleanup, configured
|
|
12
14
|
from .config_builder import ContextConfig, configuration
|
|
15
|
+
from .registrar import Registrar
|
|
13
16
|
|
|
14
17
|
KeyT = Union[str, type]
|
|
15
18
|
Provider = Callable[[], Any]
|
|
@@ -26,7 +29,6 @@ def _iter_input_modules(inputs: Union[Any, Iterable[Any]]) -> Iterable[Any]:
|
|
|
26
29
|
mod = importlib.import_module(it)
|
|
27
30
|
else:
|
|
28
31
|
mod = it
|
|
29
|
-
|
|
30
32
|
if hasattr(mod, "__path__"):
|
|
31
33
|
for sub in _scan_package(mod):
|
|
32
34
|
name = getattr(sub, "__name__", None)
|
|
@@ -49,28 +51,37 @@ def _normalize_override_provider(v: Any):
|
|
|
49
51
|
return (lambda f=v: f()), False
|
|
50
52
|
return (lambda inst=v: inst), False
|
|
51
53
|
|
|
52
|
-
def init(
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
def init(
|
|
55
|
+
modules: Union[Any, Iterable[Any]],
|
|
56
|
+
*,
|
|
57
|
+
profiles: Tuple[str, ...] = (),
|
|
58
|
+
allowed_profiles: Optional[Iterable[str]] = None,
|
|
59
|
+
environ: Optional[Dict[str, str]] = None,
|
|
60
|
+
overrides: Optional[Dict[KeyT, Any]] = None,
|
|
61
|
+
logger: Optional[logging.Logger] = None,
|
|
62
|
+
config: Optional[ContextConfig] = None,
|
|
63
|
+
custom_scopes: Optional[Iterable[str]] = None,
|
|
64
|
+
validate_only: bool = False,
|
|
65
|
+
container_id: Optional[str] = None,
|
|
66
|
+
observers: Optional[List["ContainerObserver"]] = None,
|
|
67
|
+
) -> PicoContainer:
|
|
55
68
|
active = tuple(p.strip() for p in profiles if p)
|
|
56
|
-
allowed_set = set(a.strip() for a in allowed_profiles) if allowed_profiles is not None else None
|
|
57
69
|
|
|
70
|
+
allowed_set = set(a.strip() for a in allowed_profiles) if allowed_profiles is not None else None
|
|
58
71
|
if allowed_set is not None:
|
|
59
72
|
unknown = set(active) - allowed_set
|
|
60
73
|
if unknown:
|
|
61
74
|
raise ConfigurationError(f"Unknown profiles: {sorted(unknown)}; allowed: {sorted(allowed_set)}")
|
|
62
|
-
|
|
75
|
+
|
|
63
76
|
factory = ComponentFactory()
|
|
64
77
|
caches = ScopedCaches()
|
|
65
78
|
scopes = ScopeManager()
|
|
66
|
-
|
|
67
79
|
if custom_scopes:
|
|
68
|
-
for
|
|
69
|
-
scopes.register_scope(
|
|
80
|
+
for name in custom_scopes:
|
|
81
|
+
scopes.register_scope(name)
|
|
70
82
|
|
|
71
83
|
pico = PicoContainer(factory, caches, scopes, container_id=container_id, profiles=active, observers=observers or [])
|
|
72
84
|
registrar = Registrar(factory, profiles=active, environ=environ, logger=logger, config=config)
|
|
73
|
-
|
|
74
85
|
for m in _iter_input_modules(modules):
|
|
75
86
|
registrar.register_module(m)
|
|
76
87
|
|
|
@@ -79,8 +90,7 @@ def init(modules: Union[Any, Iterable[Any]], *, profiles: Tuple[str, ...] = (),
|
|
|
79
90
|
prov, _ = _normalize_override_provider(v)
|
|
80
91
|
factory.bind(k, prov)
|
|
81
92
|
|
|
82
|
-
registrar.finalize(overrides)
|
|
83
|
-
|
|
93
|
+
registrar.finalize(overrides, pico_instance=pico)
|
|
84
94
|
if validate_only:
|
|
85
95
|
locator = registrar.locator()
|
|
86
96
|
pico.attach_locator(locator)
|
|
@@ -91,13 +101,39 @@ def init(modules: Union[Any, Iterable[Any]], *, profiles: Tuple[str, ...] = (),
|
|
|
91
101
|
registrar.attach_runtime(pico, locator)
|
|
92
102
|
pico.attach_locator(locator)
|
|
93
103
|
_fail_fast_cycle_check(pico)
|
|
104
|
+
|
|
105
|
+
if not validate_only:
|
|
106
|
+
eager_singletons = []
|
|
107
|
+
for key, md in locator._metadata.items():
|
|
108
|
+
if md.scope == "singleton" and not md.lazy:
|
|
109
|
+
cache = pico._cache_for(key)
|
|
110
|
+
instance = cache.get(key)
|
|
111
|
+
if instance is None:
|
|
112
|
+
instance = pico.get(key)
|
|
113
|
+
eager_singletons.append(instance)
|
|
114
|
+
else:
|
|
115
|
+
eager_singletons.append(instance)
|
|
116
|
+
|
|
117
|
+
configure_awaitables = []
|
|
118
|
+
for instance in eager_singletons:
|
|
119
|
+
res = pico._run_configure_methods(instance)
|
|
120
|
+
if inspect.isawaitable(res):
|
|
121
|
+
configure_awaitables.append(res)
|
|
122
|
+
|
|
123
|
+
if configure_awaitables:
|
|
124
|
+
raise ConfigurationError(
|
|
125
|
+
"Sync init() found eagerly loaded singletons with async @configure methods. "
|
|
126
|
+
"This can be caused by an async __ainit__ or async @configure. "
|
|
127
|
+
"Use an async main function and await pico.aget() for those components, "
|
|
128
|
+
"or mark them as lazy=True."
|
|
129
|
+
)
|
|
130
|
+
|
|
94
131
|
return pico
|
|
95
132
|
|
|
96
133
|
def _find_cycle(graph: Dict[KeyT, Tuple[KeyT, ...]]) -> Optional[Tuple[KeyT, ...]]:
|
|
97
134
|
temp: Set[KeyT] = set()
|
|
98
135
|
perm: Set[KeyT] = set()
|
|
99
136
|
stack: List[KeyT] = []
|
|
100
|
-
|
|
101
137
|
def visit(n: KeyT) -> Optional[Tuple[KeyT, ...]]:
|
|
102
138
|
if n in perm:
|
|
103
139
|
return None
|
|
@@ -107,20 +143,16 @@ def _find_cycle(graph: Dict[KeyT, Tuple[KeyT, ...]]) -> Optional[Tuple[KeyT, ...
|
|
|
107
143
|
return tuple(stack[idx:] + [n])
|
|
108
144
|
except ValueError:
|
|
109
145
|
return tuple([n, n])
|
|
110
|
-
|
|
111
146
|
temp.add(n)
|
|
112
147
|
stack.append(n)
|
|
113
|
-
|
|
114
148
|
for m in graph.get(n, ()):
|
|
115
149
|
c = visit(m)
|
|
116
150
|
if c:
|
|
117
151
|
return c
|
|
118
|
-
|
|
119
152
|
stack.pop()
|
|
120
153
|
temp.remove(n)
|
|
121
154
|
perm.add(n)
|
|
122
155
|
return None
|
|
123
|
-
|
|
124
156
|
for node in graph.keys():
|
|
125
157
|
c = visit(node)
|
|
126
158
|
if c:
|
pico_ioc/config_builder.py
CHANGED
|
@@ -3,13 +3,9 @@ import json
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from typing import Any, Optional, Protocol, Mapping, List, Tuple, Dict, Union
|
|
5
5
|
|
|
6
|
-
from .config_runtime import TreeSource, DictSource, JsonTreeSource, YamlTreeSource
|
|
6
|
+
from .config_runtime import TreeSource, DictSource, JsonTreeSource, YamlTreeSource, Value
|
|
7
7
|
from .exceptions import ConfigurationError
|
|
8
8
|
|
|
9
|
-
class Value:
|
|
10
|
-
def __init__(self, value: Any):
|
|
11
|
-
self.value = value
|
|
12
|
-
|
|
13
9
|
class ConfigSource(Protocol):
|
|
14
10
|
pass
|
|
15
11
|
|
pico_ioc/config_registrar.py
CHANGED
|
@@ -4,7 +4,7 @@ from typing import Any, Callable, Dict, List, Optional, Tuple, Union, get_args,
|
|
|
4
4
|
from .constants import PICO_INFRA, PICO_NAME, PICO_META
|
|
5
5
|
from .exceptions import ConfigurationError
|
|
6
6
|
from .factory import ProviderMetadata, DeferredProvider
|
|
7
|
-
from .config_builder import ContextConfig, ConfigSource, FlatDictSource
|
|
7
|
+
from .config_builder import ContextConfig, ConfigSource, FlatDictSource, Value
|
|
8
8
|
from .config_runtime import ConfigResolver, TypeAdapterRegistry, ObjectGraphBuilder, TreeSource
|
|
9
9
|
from .analysis import analyze_callable_dependencies, DependencyRequest
|
|
10
10
|
|
|
@@ -68,6 +68,22 @@ class ConfigurationManager:
|
|
|
68
68
|
raise ConfigurationError(f"Configuration class {getattr(cls, '__name__', str(cls))} must be a dataclass")
|
|
69
69
|
values: Dict[str, Any] = {}
|
|
70
70
|
for f in fields(cls):
|
|
71
|
+
field_type = f.type
|
|
72
|
+
value_override = None
|
|
73
|
+
|
|
74
|
+
if get_origin(field_type) is Annotated:
|
|
75
|
+
args = get_args(field_type)
|
|
76
|
+
field_type = args[0] if args else Any
|
|
77
|
+
metas = args[1:] if len(args) > 1 else ()
|
|
78
|
+
for m in metas:
|
|
79
|
+
if isinstance(m, Value):
|
|
80
|
+
value_override = m.value
|
|
81
|
+
break
|
|
82
|
+
|
|
83
|
+
if value_override is not None:
|
|
84
|
+
values[f.name] = value_override
|
|
85
|
+
continue
|
|
86
|
+
|
|
71
87
|
base_key = _upper_key(f.name)
|
|
72
88
|
keys_to_try = []
|
|
73
89
|
if prefix:
|
|
@@ -84,7 +100,7 @@ class ConfigurationManager:
|
|
|
84
100
|
if f.default is not MISSING or f.default_factory is not MISSING:
|
|
85
101
|
continue
|
|
86
102
|
raise ConfigurationError(f"Missing configuration key: {(prefix or '') + base_key}")
|
|
87
|
-
values[f.name] = _coerce(raw,
|
|
103
|
+
values[f.name] = _coerce(raw, field_type if isinstance(field_type, type) or get_origin(field_type) else str)
|
|
88
104
|
return cls(**values)
|
|
89
105
|
|
|
90
106
|
def _auto_detect_mapping(self, target_type: type) -> str:
|
|
@@ -159,7 +175,7 @@ class ConfigurationManager:
|
|
|
159
175
|
factory_method=None,
|
|
160
176
|
qualifiers=qset,
|
|
161
177
|
primary=True,
|
|
162
|
-
lazy=False,
|
|
178
|
+
lazy=bool(meta.get("lazy", False)),
|
|
163
179
|
infra="configured",
|
|
164
180
|
pico_name=prefix,
|
|
165
181
|
scope=sc,
|
|
@@ -180,7 +196,7 @@ class ConfigurationManager:
|
|
|
180
196
|
factory_method=None,
|
|
181
197
|
qualifiers=qset,
|
|
182
198
|
primary=True,
|
|
183
|
-
lazy=False,
|
|
199
|
+
lazy=bool(meta.get("lazy", False)),
|
|
184
200
|
infra="configured",
|
|
185
201
|
pico_name=prefix,
|
|
186
202
|
scope=sc,
|
|
@@ -217,3 +233,4 @@ class ConfigurationManager:
|
|
|
217
233
|
prefix = md.pico_name or ""
|
|
218
234
|
keys = [_upper_key(f.name) for f in fields(target_type)]
|
|
219
235
|
return any(self._lookup_flat(prefix + k) is not None for k in keys)
|
|
236
|
+
|
pico_ioc/config_runtime.py
CHANGED
|
@@ -8,6 +8,10 @@ from typing import Any, Dict, Iterable, List, Mapping, Optional, Tuple, Union, g
|
|
|
8
8
|
from .exceptions import ConfigurationError
|
|
9
9
|
from .constants import PICO_META
|
|
10
10
|
|
|
11
|
+
class Value:
|
|
12
|
+
def __init__(self, value: Any):
|
|
13
|
+
self.value = value
|
|
14
|
+
|
|
11
15
|
class Discriminator:
|
|
12
16
|
def __init__(self, name: str):
|
|
13
17
|
self.name = name
|
|
@@ -160,8 +164,8 @@ class ObjectGraphBuilder:
|
|
|
160
164
|
org = get_origin(t)
|
|
161
165
|
|
|
162
166
|
if org is Annotated:
|
|
163
|
-
base,
|
|
164
|
-
return self._build_discriminated(node, base,
|
|
167
|
+
base, metas = self._split_annotated(t)
|
|
168
|
+
return self._build_discriminated(node, base, metas, path)
|
|
165
169
|
|
|
166
170
|
if org in (list, List):
|
|
167
171
|
elem_t = get_args(t)[0] if get_args(t) else Any
|
|
@@ -255,20 +259,42 @@ class ObjectGraphBuilder:
|
|
|
255
259
|
|
|
256
260
|
def _build_discriminated(self, node: Any, base: Any, metas: Tuple[Any, ...], path: Tuple[str, ...]) -> Any:
|
|
257
261
|
disc_name = None
|
|
262
|
+
disc_value = None
|
|
263
|
+
has_value = False
|
|
264
|
+
|
|
258
265
|
for m in metas:
|
|
259
266
|
if isinstance(m, Discriminator):
|
|
260
267
|
disc_name = m.name
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
268
|
+
if isinstance(m, Value):
|
|
269
|
+
disc_value = m.value
|
|
270
|
+
has_value = True
|
|
271
|
+
|
|
272
|
+
tn: Optional[str] = None
|
|
273
|
+
|
|
274
|
+
if disc_name and has_value:
|
|
275
|
+
tn = str(disc_value)
|
|
276
|
+
elif disc_name and isinstance(node, dict) and disc_name in node:
|
|
277
|
+
tn = str(node[disc_name])
|
|
278
|
+
|
|
279
|
+
if tn is not None and get_origin(base) is Union:
|
|
280
|
+
for cand in get_args(base):
|
|
281
|
+
if isinstance(cand, type) and getattr(cand, "__name__", "") == tn:
|
|
282
|
+
|
|
283
|
+
cleaned_node = {k: v for k, v in node.items() if k != disc_name}
|
|
284
|
+
|
|
285
|
+
if has_value:
|
|
286
|
+
cleaned_node[disc_name] = tn
|
|
287
|
+
|
|
288
|
+
return self._build(cleaned_node, cand, path)
|
|
289
|
+
|
|
290
|
+
raise ConfigurationError(
|
|
291
|
+
f"Discriminator value '{tn}' for field '{disc_name}' "
|
|
292
|
+
f"did not match any type in Union {base} at {'.'.join(path)}"
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
if has_value and not disc_name:
|
|
296
|
+
return disc_value
|
|
297
|
+
|
|
272
298
|
return self._build(node, base, path)
|
|
273
299
|
|
|
274
300
|
def _coerce_prim(self, node: Any, t: type, path: Tuple[str, ...]) -> Any:
|
pico_ioc/container.py
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import inspect
|
|
2
2
|
import contextvars
|
|
3
3
|
import functools
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import (
|
|
5
|
+
Any, Dict, List, Optional, Tuple, overload, Union, Callable,
|
|
6
|
+
Iterable, Set, get_args, get_origin, Annotated, Protocol, Mapping, Type
|
|
7
|
+
)
|
|
5
8
|
from contextlib import contextmanager
|
|
6
9
|
from .constants import LOGGER, PICO_META
|
|
7
|
-
from .exceptions import ComponentCreationError, ProviderNotFoundError, AsyncResolutionError
|
|
10
|
+
from .exceptions import ComponentCreationError, ProviderNotFoundError, AsyncResolutionError, ConfigurationError
|
|
8
11
|
from .factory import ComponentFactory, ProviderMetadata
|
|
9
12
|
from .locator import ComponentLocator
|
|
10
13
|
from .scope import ScopedCaches, ScopeManager
|
|
@@ -195,6 +198,26 @@ class PicoContainer:
|
|
|
195
198
|
finally:
|
|
196
199
|
self.deactivate(token_container)
|
|
197
200
|
|
|
201
|
+
def _run_configure_methods(self, instance: Any) -> Any:
|
|
202
|
+
if not _needs_async_configure(instance):
|
|
203
|
+
for m in _iter_configure_methods(instance):
|
|
204
|
+
configure_deps = analyze_callable_dependencies(m)
|
|
205
|
+
args = self._resolve_args(configure_deps)
|
|
206
|
+
res = m(**args)
|
|
207
|
+
if inspect.isawaitable(res):
|
|
208
|
+
LOGGER.warning(f"Async configure method {m} called during sync get. Awaitable ignored.")
|
|
209
|
+
return instance
|
|
210
|
+
|
|
211
|
+
async def runner():
|
|
212
|
+
for m in _iter_configure_methods(instance):
|
|
213
|
+
configure_deps = analyze_callable_dependencies(m)
|
|
214
|
+
args = self._resolve_args(configure_deps)
|
|
215
|
+
r = m(**args)
|
|
216
|
+
if inspect.isawaitable(r):
|
|
217
|
+
await r
|
|
218
|
+
return instance
|
|
219
|
+
return runner()
|
|
220
|
+
|
|
198
221
|
@overload
|
|
199
222
|
def get(self, key: type) -> Any: ...
|
|
200
223
|
@overload
|
|
@@ -210,6 +233,14 @@ class PicoContainer:
|
|
|
210
233
|
key_name = getattr(key, '__name__', str(key))
|
|
211
234
|
raise AsyncResolutionError(key)
|
|
212
235
|
|
|
236
|
+
md = self._locator._metadata.get(key) if self._locator else None
|
|
237
|
+
scope = (md.scope if md else "singleton")
|
|
238
|
+
if scope != "singleton":
|
|
239
|
+
instance_or_awaitable_configured = self._run_configure_methods(instance)
|
|
240
|
+
if inspect.isawaitable(instance_or_awaitable_configured):
|
|
241
|
+
raise AsyncResolutionError(key)
|
|
242
|
+
instance = instance_or_awaitable_configured
|
|
243
|
+
|
|
213
244
|
final_instance = self._maybe_wrap_with_aspects(key, instance)
|
|
214
245
|
cache = self._cache_for(key)
|
|
215
246
|
cache.put(key, final_instance)
|
|
@@ -228,6 +259,15 @@ class PicoContainer:
|
|
|
228
259
|
if inspect.isawaitable(instance_or_awaitable):
|
|
229
260
|
instance = await instance_or_awaitable
|
|
230
261
|
|
|
262
|
+
md = self._locator._metadata.get(key) if self._locator else None
|
|
263
|
+
scope = (md.scope if md else "singleton")
|
|
264
|
+
if scope != "singleton":
|
|
265
|
+
instance_or_awaitable_configured = self._run_configure_methods(instance)
|
|
266
|
+
if inspect.isawaitable(instance_or_awaitable_configured):
|
|
267
|
+
instance = await instance_or_awaitable_configured
|
|
268
|
+
else:
|
|
269
|
+
instance = instance_or_awaitable_configured
|
|
270
|
+
|
|
231
271
|
final_instance = self._maybe_wrap_with_aspects(key, instance)
|
|
232
272
|
cache = self._cache_for(key)
|
|
233
273
|
cache.put(key, final_instance)
|
|
@@ -394,19 +434,61 @@ class PicoContainer:
|
|
|
394
434
|
|
|
395
435
|
def _resolve_args(self, dependencies: Tuple[DependencyRequest, ...]) -> Dict[str, Any]:
|
|
396
436
|
kwargs: Dict[str, Any] = {}
|
|
397
|
-
if not dependencies:
|
|
437
|
+
if not dependencies or self._locator is None:
|
|
398
438
|
return kwargs
|
|
399
439
|
|
|
400
440
|
for dep in dependencies:
|
|
401
441
|
if dep.is_list:
|
|
402
442
|
keys: Tuple[KeyT, ...] = ()
|
|
403
|
-
if
|
|
443
|
+
if isinstance(dep.key, type):
|
|
404
444
|
keys = tuple(self._locator.collect_by_type(dep.key, dep.qualifier))
|
|
405
445
|
kwargs[dep.parameter_name] = [self.get(k) for k in keys]
|
|
406
446
|
continue
|
|
447
|
+
|
|
448
|
+
if dep.is_dict:
|
|
449
|
+
value_type = dep.key
|
|
450
|
+
key_type = dep.dict_key_type
|
|
451
|
+
result_map: Dict[Any, Any] = {}
|
|
452
|
+
|
|
453
|
+
keys_to_resolve: Tuple[KeyT, ...] = ()
|
|
454
|
+
if isinstance(value_type, type):
|
|
455
|
+
keys_to_resolve = tuple(self._locator.collect_by_type(value_type, dep.qualifier))
|
|
456
|
+
|
|
457
|
+
for comp_key in keys_to_resolve:
|
|
458
|
+
instance = self.get(comp_key)
|
|
459
|
+
md = self._locator._metadata.get(comp_key)
|
|
460
|
+
if md is None:
|
|
461
|
+
continue
|
|
462
|
+
|
|
463
|
+
dict_key: Any = None
|
|
464
|
+
if key_type is str:
|
|
465
|
+
dict_key = md.pico_name
|
|
466
|
+
if dict_key is None:
|
|
467
|
+
if isinstance(comp_key, str):
|
|
468
|
+
dict_key = comp_key
|
|
469
|
+
else:
|
|
470
|
+
dict_key = getattr(comp_key, "__name__", str(comp_key))
|
|
471
|
+
elif key_type is type or key_type is Type:
|
|
472
|
+
dict_key = md.concrete_class or md.provided_type
|
|
473
|
+
elif key_type is Any:
|
|
474
|
+
dict_key = md.pico_name
|
|
475
|
+
if dict_key is None:
|
|
476
|
+
if isinstance(comp_key, str):
|
|
477
|
+
dict_key = comp_key
|
|
478
|
+
else:
|
|
479
|
+
dict_key = getattr(comp_key, "__name__", str(comp_key))
|
|
480
|
+
|
|
481
|
+
if dict_key is not None:
|
|
482
|
+
if (key_type is type or key_type is Type) and not isinstance(dict_key, type):
|
|
483
|
+
continue
|
|
484
|
+
|
|
485
|
+
result_map[dict_key] = instance
|
|
486
|
+
|
|
487
|
+
kwargs[dep.parameter_name] = result_map
|
|
488
|
+
continue
|
|
407
489
|
|
|
408
490
|
primary_key = dep.key
|
|
409
|
-
if isinstance(primary_key, str)
|
|
491
|
+
if isinstance(primary_key, str):
|
|
410
492
|
mapped = self._locator.find_key_by_name(primary_key)
|
|
411
493
|
primary_key = mapped if mapped is not None else primary_key
|
|
412
494
|
|
|
@@ -432,7 +514,7 @@ class PicoContainer:
|
|
|
432
514
|
inst = cls(**deps)
|
|
433
515
|
|
|
434
516
|
ainit = getattr(inst, "__ainit__", None)
|
|
435
|
-
has_async = (callable(ainit) and inspect.iscoroutinefunction(ainit))
|
|
517
|
+
has_async = (callable(ainit) and inspect.iscoroutinefunction(ainit))
|
|
436
518
|
|
|
437
519
|
if has_async:
|
|
438
520
|
async def runner():
|
|
@@ -446,39 +528,12 @@ class PicoContainer:
|
|
|
446
528
|
res = ainit(**kwargs)
|
|
447
529
|
if inspect.isawaitable(res):
|
|
448
530
|
await res
|
|
449
|
-
for m in _iter_configure_methods(inst):
|
|
450
|
-
configure_deps = analyze_callable_dependencies(m)
|
|
451
|
-
args = self._resolve_args(configure_deps)
|
|
452
|
-
r = m(**args)
|
|
453
|
-
if inspect.isawaitable(r):
|
|
454
|
-
await r
|
|
455
531
|
return inst
|
|
456
532
|
return runner()
|
|
457
533
|
|
|
458
|
-
for m in _iter_configure_methods(inst):
|
|
459
|
-
configure_deps = analyze_callable_dependencies(m)
|
|
460
|
-
args = self._resolve_args(configure_deps)
|
|
461
|
-
m(**args)
|
|
462
534
|
return inst
|
|
463
535
|
|
|
464
536
|
def build_method(self, fn: Callable[..., Any], locator: ComponentLocator, dependencies: Tuple[DependencyRequest, ...]) -> Any:
|
|
465
537
|
deps = self._resolve_args(dependencies)
|
|
466
538
|
obj = fn(**deps)
|
|
467
|
-
|
|
468
|
-
has_async = _needs_async_configure(obj)
|
|
469
|
-
if has_async:
|
|
470
|
-
async def runner():
|
|
471
|
-
for m in _iter_configure_methods(obj):
|
|
472
|
-
configure_deps = analyze_callable_dependencies(m)
|
|
473
|
-
args = self._resolve_args(configure_deps)
|
|
474
|
-
r = m(**args)
|
|
475
|
-
if inspect.isawaitable(r):
|
|
476
|
-
await r
|
|
477
|
-
return obj
|
|
478
|
-
return runner()
|
|
479
|
-
|
|
480
|
-
for m in _iter_configure_methods(obj):
|
|
481
|
-
configure_deps = analyze_callable_dependencies(m)
|
|
482
|
-
args = self._resolve_args(configure_deps)
|
|
483
|
-
m(**args)
|
|
484
539
|
return obj
|
pico_ioc/decorators.py
CHANGED
|
@@ -172,13 +172,14 @@ def cleanup(fn):
|
|
|
172
172
|
m["cleanup"] = True
|
|
173
173
|
return fn
|
|
174
174
|
|
|
175
|
-
def configured(target: Any, *, prefix: str = "", mapping: str = "auto"):
|
|
175
|
+
def configured(target: Any = "self", *, prefix: str = "", mapping: str = "auto", **kwargs):
|
|
176
176
|
if mapping not in ("auto", "flat", "tree"):
|
|
177
177
|
raise ValueError("mapping must be one of 'auto', 'flat', or 'tree'")
|
|
178
178
|
def dec(cls):
|
|
179
179
|
setattr(cls, PICO_INFRA, "configured")
|
|
180
180
|
m = _meta_get(cls)
|
|
181
181
|
m["configured"] = {"target": target, "prefix": prefix, "mapping": mapping}
|
|
182
|
+
_apply_common_metadata(cls, **kwargs)
|
|
182
183
|
return cls
|
|
183
184
|
return dec
|
|
184
185
|
|
pico_ioc/event_bus.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# src/pico_ioc/event_bus.py
|
|
2
1
|
import asyncio
|
|
3
2
|
import inspect
|
|
4
3
|
import logging
|
|
@@ -151,29 +150,30 @@ class EventBus:
|
|
|
151
150
|
self._worker_loop = None
|
|
152
151
|
|
|
153
152
|
def post(self, event: Event) -> None:
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
153
|
+
with self._lock:
|
|
154
|
+
if self._closed:
|
|
155
|
+
raise EventBusClosedError()
|
|
156
|
+
if self._queue is None:
|
|
157
|
+
raise EventBusError("Worker queue not initialized. Call start_worker().")
|
|
158
|
+
loop = self._worker_loop
|
|
159
|
+
if loop and loop.is_running():
|
|
160
|
+
try:
|
|
161
|
+
current_loop = asyncio.get_running_loop()
|
|
162
|
+
if current_loop is loop:
|
|
163
|
+
try:
|
|
164
|
+
self._queue.put_nowait(event)
|
|
165
|
+
return
|
|
166
|
+
except asyncio.QueueFull:
|
|
167
|
+
raise EventBusQueueFullError()
|
|
168
|
+
except RuntimeError:
|
|
169
|
+
pass
|
|
170
|
+
try:
|
|
171
|
+
loop.call_soon_threadsafe(self._queue.put_nowait, event)
|
|
172
|
+
return
|
|
173
|
+
except asyncio.QueueFull:
|
|
174
|
+
raise EventBusQueueFullError()
|
|
175
|
+
else:
|
|
176
|
+
raise EventBusError("Worker queue not initialized or loop not running. Call start_worker().")
|
|
177
177
|
|
|
178
178
|
async def aclose(self) -> None:
|
|
179
179
|
await self.stop_worker()
|
|
@@ -220,4 +220,3 @@ class PicoEventBusProvider:
|
|
|
220
220
|
loop.create_task(event_bus.aclose())
|
|
221
221
|
else:
|
|
222
222
|
asyncio.run(event_bus.aclose())
|
|
223
|
-
|
pico_ioc/locator.py
CHANGED
|
@@ -80,9 +80,13 @@ class ComponentLocator:
|
|
|
80
80
|
return isinstance(inst, proto)
|
|
81
81
|
except Exception:
|
|
82
82
|
pass
|
|
83
|
+
|
|
83
84
|
for name, val in proto.__dict__.items():
|
|
84
|
-
if name.startswith("_") or not callable(val):
|
|
85
|
+
if name.startswith("_") or not (callable(val) or name in getattr(proto, "__annotations__", {})):
|
|
85
86
|
continue
|
|
87
|
+
|
|
88
|
+
if not hasattr(typ, name):
|
|
89
|
+
return False
|
|
86
90
|
return True
|
|
87
91
|
|
|
88
92
|
def collect_by_type(self, t: type, q: Optional[str]) -> List[KeyT]:
|
|
@@ -115,10 +119,6 @@ class ComponentLocator:
|
|
|
115
119
|
return k
|
|
116
120
|
return None
|
|
117
121
|
|
|
118
|
-
def _compile_argplan_static(self, callable_obj):
|
|
119
|
-
raise NotImplementedError("This method is obsolete and replaced by analysis module")
|
|
120
|
-
|
|
121
|
-
|
|
122
122
|
def dependency_keys_for_static(self, md: ProviderMetadata):
|
|
123
123
|
deps: List[KeyT] = []
|
|
124
124
|
for dep in md.dependencies:
|
|
@@ -126,6 +126,10 @@ class ComponentLocator:
|
|
|
126
126
|
if isinstance(dep.key, type):
|
|
127
127
|
keys = self.collect_by_type(dep.key, dep.qualifier)
|
|
128
128
|
deps.extend(keys)
|
|
129
|
+
elif dep.is_dict:
|
|
130
|
+
if isinstance(dep.key, type):
|
|
131
|
+
keys = self.collect_by_type(dep.key, dep.qualifier)
|
|
132
|
+
deps.extend(keys)
|
|
129
133
|
else:
|
|
130
134
|
deps.append(dep.key)
|
|
131
135
|
return tuple(deps)
|
pico_ioc/registrar.py
CHANGED
|
@@ -17,6 +17,7 @@ from .provider_selector import ProviderSelector
|
|
|
17
17
|
from .dependency_validator import DependencyValidator
|
|
18
18
|
from .component_scanner import ComponentScanner
|
|
19
19
|
from .analysis import analyze_callable_dependencies, DependencyRequest
|
|
20
|
+
from .container import PicoContainer
|
|
20
21
|
|
|
21
22
|
KeyT = Union[str, type]
|
|
22
23
|
Provider = Callable[[], Any]
|
|
@@ -133,7 +134,7 @@ class Registrar:
|
|
|
133
134
|
add("pico_name", md.pico_name, k)
|
|
134
135
|
|
|
135
136
|
|
|
136
|
-
def finalize(self, overrides: Optional[Dict[KeyT, Any]]) -> None:
|
|
137
|
+
def finalize(self, overrides: Optional[Dict[KeyT, Any]], *, pico_instance: PicoContainer) -> None:
|
|
137
138
|
candidates, on_missing, deferred_providers, provides_functions = self._scanner.get_scan_results()
|
|
138
139
|
self._deferred = deferred_providers
|
|
139
140
|
self._provides_functions = provides_functions
|
|
@@ -142,6 +143,24 @@ class Registrar:
|
|
|
142
143
|
for key, (provider, md) in winners.items():
|
|
143
144
|
self._bind_if_absent(key, provider)
|
|
144
145
|
self._metadata[key] = md
|
|
146
|
+
|
|
147
|
+
if PicoContainer not in self._metadata:
|
|
148
|
+
self._factory.bind(PicoContainer, lambda: pico_instance)
|
|
149
|
+
self._metadata[PicoContainer] = ProviderMetadata(
|
|
150
|
+
key=PicoContainer,
|
|
151
|
+
provided_type=PicoContainer,
|
|
152
|
+
concrete_class=PicoContainer,
|
|
153
|
+
factory_class=None,
|
|
154
|
+
factory_method=None,
|
|
155
|
+
qualifiers=set(),
|
|
156
|
+
primary=True,
|
|
157
|
+
lazy=False,
|
|
158
|
+
infra="component",
|
|
159
|
+
pico_name="PicoContainer",
|
|
160
|
+
override=True,
|
|
161
|
+
scope="singleton",
|
|
162
|
+
dependencies=()
|
|
163
|
+
)
|
|
145
164
|
|
|
146
165
|
self._promote_scopes()
|
|
147
166
|
self._rebuild_indexes()
|
pico_ioc/scope.py
CHANGED
|
@@ -3,6 +3,7 @@ import contextvars
|
|
|
3
3
|
import inspect
|
|
4
4
|
from typing import Any, Dict, Optional, Tuple
|
|
5
5
|
from collections import OrderedDict
|
|
6
|
+
from .exceptions import ScopeError
|
|
6
7
|
|
|
7
8
|
class ScopeProtocol:
|
|
8
9
|
def get_id(self) -> Any | None: ...
|
|
@@ -42,16 +43,24 @@ class ScopeManager:
|
|
|
42
43
|
self._scopes: Dict[str, ScopeProtocol] = {
|
|
43
44
|
"request": ContextVarScope(contextvars.ContextVar("pico_request_id", default=None)),
|
|
44
45
|
"session": ContextVarScope(contextvars.ContextVar("pico_session_id", default=None)),
|
|
46
|
+
"websocket": ContextVarScope(contextvars.ContextVar("pico_websocket_id", default=None)),
|
|
45
47
|
"transaction": ContextVarScope(contextvars.ContextVar("pico_tx_id", default=None)),
|
|
46
48
|
}
|
|
47
|
-
|
|
49
|
+
|
|
50
|
+
def register_scope(self, name: str) -> None:
|
|
48
51
|
if not isinstance(name, str) or not name:
|
|
49
|
-
from .exceptions import ScopeError
|
|
50
52
|
raise ScopeError("Scope name must be a non-empty string")
|
|
51
53
|
if name in ("singleton", "prototype"):
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
raise ScopeError(f"Cannot register reserved scope: '{name}'")
|
|
55
|
+
if name in self._scopes:
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
var_name = f"pico_{name}_id"
|
|
59
|
+
context_var = contextvars.ContextVar(var_name, default=None)
|
|
60
|
+
implementation = ContextVarScope(context_var)
|
|
54
61
|
self._scopes[name] = implementation
|
|
62
|
+
|
|
63
|
+
|
|
55
64
|
def get_id(self, name: str) -> Any | None:
|
|
56
65
|
if name in ("singleton", "prototype"):
|
|
57
66
|
return None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pico-ioc
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.2
|
|
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
|
|
@@ -39,7 +39,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
39
39
|
Classifier: Programming Language :: Python :: 3.14
|
|
40
40
|
Classifier: License :: OSI Approved :: MIT License
|
|
41
41
|
Classifier: Operating System :: OS Independent
|
|
42
|
-
Requires-Python: >=3.
|
|
42
|
+
Requires-Python: >=3.10
|
|
43
43
|
Description-Content-Type: text/markdown
|
|
44
44
|
License-File: LICENSE
|
|
45
45
|
Provides-Extra: yaml
|
|
@@ -58,49 +58,53 @@ Dynamic: license-file
|
|
|
58
58
|
[](https://sonarcloud.io/summary/new_code?id=dperezcabrera_pico-ioc)
|
|
59
59
|
[](https://sonarcloud.io/summary/new_code?id=dperezcabrera_pico-ioc)
|
|
60
60
|
[](https://sonarcloud.io/summary/new_code?id=dperezcabrera_pico-ioc)
|
|
61
|
+
[](https://pepy.tech/projects/pico-ioc)
|
|
62
|
+
[](https://dperezcabrera.github.io/pico-ioc/)
|
|
63
|
+
[](https://dperezcabrera.github.io/learn-pico-ioc/)
|
|
64
|
+
|
|
61
65
|
|
|
62
66
|
**Pico-IoC** is a **lightweight, async-ready, decorator-driven IoC container** built for clarity, testability, and performance.
|
|
63
|
-
It brings
|
|
67
|
+
It brings Inversion of Control and dependency injection to Python in a deterministic, modern, and framework-agnostic way.
|
|
64
68
|
|
|
65
|
-
> 🐍 Requires
|
|
69
|
+
> 🐍 Requires Python 3.10+
|
|
66
70
|
|
|
67
71
|
---
|
|
68
72
|
|
|
69
73
|
## ⚖️ Core Principles
|
|
70
74
|
|
|
71
|
-
-
|
|
72
|
-
-
|
|
73
|
-
-
|
|
74
|
-
-
|
|
75
|
-
-
|
|
76
|
-
-
|
|
77
|
-
-
|
|
75
|
+
- Single Purpose – Do one thing: dependency management.
|
|
76
|
+
- Declarative – Use simple decorators (`@component`, `@factory`, `@provides`, `@configured`) instead of complex config files.
|
|
77
|
+
- Deterministic – No hidden scanning or side-effects; everything flows from an explicit `init()`.
|
|
78
|
+
- Async-Native – Fully supports async providers, async lifecycle hooks (`__ainit__`), and async interceptors.
|
|
79
|
+
- Fail-Fast – Detects missing bindings and circular dependencies at bootstrap (`init()`).
|
|
80
|
+
- Testable by Design – Use `overrides` and `profiles` to swap components instantly.
|
|
81
|
+
- Zero Core Dependencies – Built entirely on the Python standard library. Optional features may require external packages (see Installation).
|
|
78
82
|
|
|
79
83
|
---
|
|
80
84
|
|
|
81
85
|
## 🚀 Why Pico-IoC?
|
|
82
86
|
|
|
83
87
|
As Python systems evolve, wiring dependencies by hand becomes fragile and unmaintainable.
|
|
84
|
-
|
|
88
|
+
Pico-IoC eliminates that friction by letting you declare how components relate — not how they’re created.
|
|
85
89
|
|
|
86
|
-
| Feature | Manual Wiring
|
|
87
|
-
| :-------------- |
|
|
88
|
-
| Object creation | `svc = Service(Repo(Config()))` | `svc = container.get(Service)`
|
|
89
|
-
| Replacing deps | Monkey-patch
|
|
90
|
-
| Coupling | Tight
|
|
91
|
-
| Testing | Painful
|
|
92
|
-
| Async support | Manual
|
|
90
|
+
| Feature | Manual Wiring | With Pico-IoC |
|
|
91
|
+
| :-------------- | :----------------------------- | :------------------------------ |
|
|
92
|
+
| Object creation | `svc = Service(Repo(Config()))` | `svc = container.get(Service)` |
|
|
93
|
+
| Replacing deps | Monkey-patch | `overrides={Repo: FakeRepo()}` |
|
|
94
|
+
| Coupling | Tight | Loose |
|
|
95
|
+
| Testing | Painful | Instant |
|
|
96
|
+
| Async support | Manual | Built-in (`aget`, `__ainit__`) |
|
|
93
97
|
|
|
94
98
|
---
|
|
95
99
|
|
|
96
100
|
## 🧩 Highlights (v2.0+)
|
|
97
101
|
|
|
98
|
-
-
|
|
99
|
-
-
|
|
100
|
-
-
|
|
101
|
-
-
|
|
102
|
-
-
|
|
103
|
-
-
|
|
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.
|
|
104
108
|
|
|
105
109
|
---
|
|
106
110
|
|
|
@@ -108,26 +112,15 @@ As Python systems evolve, wiring dependencies by hand becomes fragile and unmain
|
|
|
108
112
|
|
|
109
113
|
```bash
|
|
110
114
|
pip install pico-ioc
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
For optional features, you can install extras:
|
|
114
|
-
|
|
115
|
-
* **YAML Configuration:**
|
|
116
|
-
|
|
117
|
-
```bash
|
|
118
|
-
pip install pico-ioc[yaml]
|
|
119
|
-
```
|
|
115
|
+
```
|
|
120
116
|
|
|
121
|
-
|
|
117
|
+
Optional extras:
|
|
122
118
|
|
|
123
|
-
|
|
119
|
+
- YAML configuration support (requires PyYAML)
|
|
124
120
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
# as export_graph generates the .dot file content directly.
|
|
129
|
-
# pip install pico-ioc[graphviz] # Consider removing if not used by code
|
|
130
|
-
```
|
|
121
|
+
```bash
|
|
122
|
+
pip install pico-ioc[yaml]
|
|
123
|
+
```
|
|
131
124
|
|
|
132
125
|
-----
|
|
133
126
|
|
|
@@ -139,7 +132,7 @@ from dataclasses import dataclass
|
|
|
139
132
|
from pico_ioc import component, configured, configuration, init, EnvSource
|
|
140
133
|
|
|
141
134
|
# 1. Define configuration with @configured
|
|
142
|
-
@configured(prefix="APP_", mapping="auto")
|
|
135
|
+
@configured(prefix="APP_", mapping="auto") # Auto-detects flat mapping
|
|
143
136
|
@dataclass
|
|
144
137
|
class Config:
|
|
145
138
|
db_url: str = "sqlite:///demo.db"
|
|
@@ -147,14 +140,14 @@ class Config:
|
|
|
147
140
|
# 2. Define components
|
|
148
141
|
@component
|
|
149
142
|
class Repo:
|
|
150
|
-
def __init__(self, cfg: Config):
|
|
143
|
+
def __init__(self, cfg: Config): # Inject config
|
|
151
144
|
self.cfg = cfg
|
|
152
145
|
def fetch(self):
|
|
153
146
|
return f"fetching from {self.cfg.db_url}"
|
|
154
147
|
|
|
155
148
|
@component
|
|
156
149
|
class Service:
|
|
157
|
-
def __init__(self, repo: Repo):
|
|
150
|
+
def __init__(self, repo: Repo): # Inject Repo
|
|
158
151
|
self.repo = repo
|
|
159
152
|
def run(self):
|
|
160
153
|
return self.repo.fetch()
|
|
@@ -164,11 +157,11 @@ os.environ['APP_DB_URL'] = 'postgresql://user:pass@host/db'
|
|
|
164
157
|
|
|
165
158
|
# 3. Build configuration context
|
|
166
159
|
config_ctx = configuration(
|
|
167
|
-
EnvSource(prefix="")
|
|
160
|
+
EnvSource(prefix="") # Read APP_DB_URL from environment
|
|
168
161
|
)
|
|
169
162
|
|
|
170
163
|
# 4. Initialize container
|
|
171
|
-
container = init(modules=[__name__], config=config_ctx)
|
|
164
|
+
container = init(modules=[__name__], config=config_ctx) # Pass context via 'config'
|
|
172
165
|
|
|
173
166
|
# 5. Get and use the service
|
|
174
167
|
svc = container.get(Service)
|
|
@@ -178,7 +171,7 @@ print(svc.run())
|
|
|
178
171
|
del os.environ['APP_DB_URL']
|
|
179
172
|
```
|
|
180
173
|
|
|
181
|
-
|
|
174
|
+
Output:
|
|
182
175
|
|
|
183
176
|
```
|
|
184
177
|
fetching from postgresql://user:pass@host/db
|
|
@@ -199,7 +192,7 @@ test_config_ctx = configuration()
|
|
|
199
192
|
container = init(
|
|
200
193
|
modules=[__name__],
|
|
201
194
|
config=test_config_ctx,
|
|
202
|
-
overrides={Repo: FakeRepo()}
|
|
195
|
+
overrides={Repo: FakeRepo()} # Replace Repo with FakeRepo
|
|
203
196
|
)
|
|
204
197
|
|
|
205
198
|
svc = container.get(Service)
|
|
@@ -208,10 +201,56 @@ assert svc.run() == "fake-data"
|
|
|
208
201
|
|
|
209
202
|
-----
|
|
210
203
|
|
|
204
|
+
## 🧰 Profiles
|
|
205
|
+
|
|
206
|
+
Use profiles to enable/disable components or configuration branches conditionally.
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
# Enable "test" profile when bootstrapping the container
|
|
210
|
+
container = init(
|
|
211
|
+
modules=[__name__],
|
|
212
|
+
profiles=["test"]
|
|
213
|
+
)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Profiles are typically referenced in decorators or configuration mappings to include/exclude components and bindings.
|
|
217
|
+
|
|
218
|
+
-----
|
|
219
|
+
|
|
220
|
+
## ⚡ Async Components
|
|
221
|
+
|
|
222
|
+
Pico-IoC supports async lifecycle and resolution.
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
import asyncio
|
|
226
|
+
from pico_ioc import component, init
|
|
227
|
+
|
|
228
|
+
@component
|
|
229
|
+
class AsyncRepo:
|
|
230
|
+
async def __ainit__(self):
|
|
231
|
+
# e.g., open async connections
|
|
232
|
+
self.ready = True
|
|
233
|
+
|
|
234
|
+
async def fetch(self):
|
|
235
|
+
return "async-data"
|
|
236
|
+
|
|
237
|
+
async def main():
|
|
238
|
+
container = init(modules=[__name__])
|
|
239
|
+
repo = await container.aget(AsyncRepo) # Async resolution
|
|
240
|
+
print(await repo.fetch())
|
|
241
|
+
|
|
242
|
+
asyncio.run(main())
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
- `__ainit__` runs after construction if defined.
|
|
246
|
+
- Use `container.aget(Type)` to resolve components that require async initialization or whose providers are async.
|
|
247
|
+
|
|
248
|
+
-----
|
|
249
|
+
|
|
211
250
|
## 🩺 Lifecycle & AOP
|
|
212
251
|
|
|
213
252
|
```python
|
|
214
|
-
import time
|
|
253
|
+
import time
|
|
215
254
|
from pico_ioc import component, init, intercepted_by, MethodInterceptor, MethodCtx
|
|
216
255
|
|
|
217
256
|
# Define an interceptor component
|
|
@@ -232,7 +271,7 @@ class LogInterceptor(MethodInterceptor):
|
|
|
232
271
|
|
|
233
272
|
@component
|
|
234
273
|
class Demo:
|
|
235
|
-
@intercepted_by(LogInterceptor)
|
|
274
|
+
@intercepted_by(LogInterceptor) # Apply the interceptor
|
|
236
275
|
def work(self):
|
|
237
276
|
print(" Working...")
|
|
238
277
|
time.sleep(0.01)
|
|
@@ -244,7 +283,7 @@ result = c.get(Demo).work()
|
|
|
244
283
|
print(f"Result: {result}")
|
|
245
284
|
```
|
|
246
285
|
|
|
247
|
-
|
|
286
|
+
Output:
|
|
248
287
|
|
|
249
288
|
```
|
|
250
289
|
→ calling Demo.work
|
|
@@ -255,19 +294,41 @@ Result: ok
|
|
|
255
294
|
|
|
256
295
|
-----
|
|
257
296
|
|
|
297
|
+
## 👁️ Observability & Cleanup
|
|
298
|
+
|
|
299
|
+
- Export a dependency graph in DOT format:
|
|
300
|
+
|
|
301
|
+
```python
|
|
302
|
+
c = init(modules=[...])
|
|
303
|
+
dot = c.export_graph() # Returns DOT graph as a string
|
|
304
|
+
with open("dependencies.dot", "w") as f:
|
|
305
|
+
f.write(dot)
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
- Health checks:
|
|
309
|
+
- Annotate health probes inside components with `@health` for container-level reporting.
|
|
310
|
+
- The container exposes health information that can be queried in observability tooling.
|
|
311
|
+
|
|
312
|
+
- Container cleanup:
|
|
313
|
+
- For sync components: `container.close()`
|
|
314
|
+
- For async components/resources: `await container.aclose()`
|
|
315
|
+
|
|
316
|
+
Use cleanup in application shutdown hooks to release resources deterministically.
|
|
317
|
+
|
|
318
|
+
-----
|
|
319
|
+
|
|
258
320
|
## 📖 Documentation
|
|
259
321
|
|
|
260
322
|
The full documentation is available within the `docs/` directory of the project repository. Start with `docs/README.md` for navigation.
|
|
261
323
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
* **ADR Index:** `docs/adr/README.md`
|
|
324
|
+
- Getting Started: `docs/getting-started.md`
|
|
325
|
+
- User Guide: `docs/user-guide/README.md`
|
|
326
|
+
- Advanced Features: `docs/advanced-features/README.md`
|
|
327
|
+
- Observability: `docs/observability/README.md`
|
|
328
|
+
- Cookbook (Patterns): `docs/cookbook/README.md`
|
|
329
|
+
- Architecture: `docs/architecture/README.md`
|
|
330
|
+
- API Reference: `docs/api-reference/README.md`
|
|
331
|
+
- ADR Index: `docs/adr/README.md`
|
|
271
332
|
|
|
272
333
|
-----
|
|
273
334
|
|
|
@@ -282,11 +343,10 @@ tox
|
|
|
282
343
|
|
|
283
344
|
## 🧾 Changelog
|
|
284
345
|
|
|
285
|
-
See [CHANGELOG.md](./CHANGELOG.md) —
|
|
346
|
+
See [CHANGELOG.md](./CHANGELOG.md) — Significant redesigns and features in v2.0+.
|
|
286
347
|
|
|
287
348
|
-----
|
|
288
349
|
|
|
289
350
|
## 📜 License
|
|
290
351
|
|
|
291
352
|
MIT — [LICENSE](https://opensource.org/licenses/MIT)
|
|
292
|
-
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
pico_ioc/__init__.py,sha256=i25Obx7aH_Oy5b6yjjnCswDgni7InIjrGEcG6vLAw6I,2414
|
|
2
|
+
pico_ioc/_version.py,sha256=m5qImnzcnIhayvILFVqEnXPYsN-vE0vxokygykKhRfw,22
|
|
3
|
+
pico_ioc/analysis.py,sha256=Iy3fuXCVLV8xtT-qp-uxsb1QptHBLLrLYbTSfDkQ-OA,4145
|
|
4
|
+
pico_ioc/aop.py,sha256=XcyzsuKPrVPk1_Jad7Mn-qwoL1y0ZuVWwRZBA-CslJk,13301
|
|
5
|
+
pico_ioc/api.py,sha256=0pcRFHzhDcX8ijd67xAsVrTejwXuJKz7kTKRUrIuX2s,6161
|
|
6
|
+
pico_ioc/component_scanner.py,sha256=S-9XNxrgyq_JFdc4Uqn2bEb-HxafSgIWylIurxyN_UA,7955
|
|
7
|
+
pico_ioc/config_builder.py,sha256=7kcYIq1Yrb46Tic7uLeaCDvLA-Sa_p1PIoGF00mivso,2848
|
|
8
|
+
pico_ioc/config_registrar.py,sha256=34iNQY1TUEPTXbb-QV1T-c5VKAn18hBcNt5MLhzDSfY,8456
|
|
9
|
+
pico_ioc/config_runtime.py,sha256=hiL1kCxhpjbfOdUaH71jMGNESDpWsaJkQXh7q1T71bg,12781
|
|
10
|
+
pico_ioc/constants.py,sha256=AhIt0ieDZ9Turxb_YbNzj11wUbBbzjKfWh1BDlSx2Nw,183
|
|
11
|
+
pico_ioc/container.py,sha256=Ys1yLjiB3Qxxm_fvWCEYLSeaJ18LseWmXueAW8kHunk,20874
|
|
12
|
+
pico_ioc/decorators.py,sha256=ru_YeqyJ3gbfb6M8WeJZlBxfcBBEuGDvxpHJGzU6FIs,6412
|
|
13
|
+
pico_ioc/dependency_validator.py,sha256=BIR6pKntACiabF6CjNZ3m00RMnet9BPK1_9y1iCJ5KQ,4144
|
|
14
|
+
pico_ioc/event_bus.py,sha256=nOL91JLYxap9kbb-HBGEhOVwtXN_bfI4q0mtSRZFlHk,8434
|
|
15
|
+
pico_ioc/exceptions.py,sha256=FBuajj5g29hAGODt2tAWuy2sG5mQojdSddaqFzim-aY,2383
|
|
16
|
+
pico_ioc/factory.py,sha256=oJXx_BYJuvV8oxYzs5I3gx9WM6uLYZ8GCc43gukNanc,1671
|
|
17
|
+
pico_ioc/locator.py,sha256=JD6psgdGGsBoCwov-G76BrmTfKUoJ22sdwa6wVdmQV8,5064
|
|
18
|
+
pico_ioc/provider_selector.py,sha256=pU7NbI5vifvUlJEjlRJmvveQUZVD47T24QmiP0CHRw0,1213
|
|
19
|
+
pico_ioc/registrar.py,sha256=hIk48nXghTdA3WBljCbw2q8J_6F_hCk1ljSi4Pb8P3A,8368
|
|
20
|
+
pico_ioc/scope.py,sha256=hOdTmjjfrRt8APXoS3lbTbSPxILi7flBXz_qpIkpoKw,6137
|
|
21
|
+
pico_ioc-2.1.2.dist-info/licenses/LICENSE,sha256=N1_nOvHTM6BobYnOTNXiQkroDqCEi6EzfGBv8lWtyZ0,1077
|
|
22
|
+
pico_ioc-2.1.2.dist-info/METADATA,sha256=yerxK_c9JcZxnKqB-nWQL6bSovNLse9Qa67o2jD9R3I,12339
|
|
23
|
+
pico_ioc-2.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
24
|
+
pico_ioc-2.1.2.dist-info/top_level.txt,sha256=_7_RLu616z_dtRw16impXn4Mw8IXe2J4BeX5912m5dQ,9
|
|
25
|
+
pico_ioc-2.1.2.dist-info/RECORD,,
|
pico_ioc-2.1.0.dist-info/RECORD
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
pico_ioc/__init__.py,sha256=pxYvrunT9cFAXxGbow5TRlzRXDVvm4E7L6PT1UkPeUw,2394
|
|
2
|
-
pico_ioc/_version.py,sha256=LbU43-7hsLmdXWI0wTAl3y6D3Tr7CbJiDOLXwl7clj8,22
|
|
3
|
-
pico_ioc/analysis.py,sha256=k49R-HcDyvpSNid8mxv7Fc6fPHnDu1C_b4HxrGLNF2g,2780
|
|
4
|
-
pico_ioc/aop.py,sha256=r8JCPsTTF1hmrQMtnj-_Lj99fIy5J97RAJjiR2LjVPQ,12769
|
|
5
|
-
pico_ioc/api.py,sha256=tR0pm6YEnDTA62EIT_Cpw1EaPfdnBFY9Dvpc38ypP-o,5061
|
|
6
|
-
pico_ioc/component_scanner.py,sha256=S-9XNxrgyq_JFdc4Uqn2bEb-HxafSgIWylIurxyN_UA,7955
|
|
7
|
-
pico_ioc/config_builder.py,sha256=ROBvbnm2Zv8apRdtZJHtebp-2cOMiuENFyiqaX1T2Ik,2918
|
|
8
|
-
pico_ioc/config_registrar.py,sha256=8Xpl-CNRFny3EIRlcNx6oMJQldXkj_jdaPbC17qk2Ec,7803
|
|
9
|
-
pico_ioc/config_runtime.py,sha256=Q8jVFQ1b6h1ZOkdacm70u0Q7TlEiUGENFkG-YWRkBxA,12133
|
|
10
|
-
pico_ioc/constants.py,sha256=AhIt0ieDZ9Turxb_YbNzj11wUbBbzjKfWh1BDlSx2Nw,183
|
|
11
|
-
pico_ioc/container.py,sha256=1gRjHH5kq_lOlnGUh5QYA4vUHF3ScQpwXoT9PWiyJ2g,18352
|
|
12
|
-
pico_ioc/decorators.py,sha256=qrx4oMjRmmgwqT4cdp6FxBoqex3L1yNGlCUxO865z14,6347
|
|
13
|
-
pico_ioc/dependency_validator.py,sha256=BIR6pKntACiabF6CjNZ3m00RMnet9BPK1_9y1iCJ5KQ,4144
|
|
14
|
-
pico_ioc/event_bus.py,sha256=E8Qb8KZ6K1CuXSbMlG0MNPHkGoWlssLLPzHq1QYdADQ,8346
|
|
15
|
-
pico_ioc/exceptions.py,sha256=FBuajj5g29hAGODt2tAWuy2sG5mQojdSddaqFzim-aY,2383
|
|
16
|
-
pico_ioc/factory.py,sha256=oJXx_BYJuvV8oxYzs5I3gx9WM6uLYZ8GCc43gukNanc,1671
|
|
17
|
-
pico_ioc/locator.py,sha256=UDyXl3oSK-sZ8ouOlhsDM91shMP0atMxyMoQlaSYvfE,4885
|
|
18
|
-
pico_ioc/provider_selector.py,sha256=pU7NbI5vifvUlJEjlRJmvveQUZVD47T24QmiP0CHRw0,1213
|
|
19
|
-
pico_ioc/registrar.py,sha256=cexXFDsuxBwM8FvW7mwmuyqcTcOkSP6ELecwklcs0F4,7625
|
|
20
|
-
pico_ioc/scope.py,sha256=GDsDJWw7e5Vpiys-M4vQfKMJWSCiorRsT5cPo6z34Mk,5924
|
|
21
|
-
pico_ioc-2.1.0.dist-info/licenses/LICENSE,sha256=N1_nOvHTM6BobYnOTNXiQkroDqCEi6EzfGBv8lWtyZ0,1077
|
|
22
|
-
pico_ioc-2.1.0.dist-info/METADATA,sha256=lYR54Lhdwmn5tDXol2M5ReJxBsXpdZ9bH0KrcYOlq2s,10672
|
|
23
|
-
pico_ioc-2.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
24
|
-
pico_ioc-2.1.0.dist-info/top_level.txt,sha256=_7_RLu616z_dtRw16impXn4Mw8IXe2J4BeX5912m5dQ,9
|
|
25
|
-
pico_ioc-2.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|