pico-ioc 2.0.5__py3-none-any.whl → 2.1.0__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.
@@ -0,0 +1,103 @@
1
+ from typing import Any, Callable, Dict, List, Optional, Tuple, Union, Set
2
+ from .factory import ComponentFactory, ProviderMetadata
3
+ from .locator import ComponentLocator
4
+ from .exceptions import InvalidBindingError
5
+ from .analysis import DependencyRequest
6
+
7
+ KeyT = Union[str, type]
8
+
9
+ def _fmt(k: KeyT) -> str:
10
+ return getattr(k, '__name__', str(k))
11
+
12
+ def _skip_type(t: type) -> bool:
13
+ if t in (str, int, float, bool, bytes):
14
+ return True
15
+ if t is Any:
16
+ return True
17
+ if getattr(t, "_is_protocol", False):
18
+ return True
19
+ return False
20
+
21
+ class DependencyValidator:
22
+ def __init__(self, metadata: Dict[KeyT, ProviderMetadata], factory: ComponentFactory, locator: ComponentLocator):
23
+ self._metadata = metadata
24
+ self._factory = factory
25
+ self._locator = locator
26
+
27
+ def _find_md_for_type(self, t: type) -> Optional[ProviderMetadata]:
28
+ cands: List[ProviderMetadata] = []
29
+ for md in self._metadata.values():
30
+ typ = md.provided_type or md.concrete_class
31
+ if not isinstance(typ, type):
32
+ continue
33
+ try:
34
+ if issubclass(typ, t):
35
+ cands.append(md)
36
+ except TypeError:
37
+ pass
38
+ except Exception:
39
+ continue
40
+ if not cands:
41
+ if getattr(t, "_is_protocol", False):
42
+ for md in self._metadata.values():
43
+ typ = md.provided_type or md.concrete_class
44
+ if isinstance(typ, type) and ComponentLocator._implements_protocol(typ, t):
45
+ cands.append(md)
46
+
47
+ if not cands:
48
+ return None
49
+ prim = [m for m in cands if m.primary]
50
+ return prim[0] if prim else cands[0]
51
+
52
+
53
+ def _find_md_for_name(self, name: str) -> Optional[KeyT]:
54
+ return self._locator.find_key_by_name(name)
55
+
56
+
57
+ def validate_bindings(self) -> None:
58
+ errors: List[str] = []
59
+
60
+ for k, md in self._metadata.items():
61
+ if md.infra == "configuration":
62
+ continue
63
+
64
+ if not md.dependencies and md.infra not in ("configured", "component") and not md.override:
65
+ continue
66
+ if md.infra == "component" and md.concrete_class and md.concrete_class.__init__ is object.__init__:
67
+ continue
68
+
69
+
70
+ loc_name = f"component {_fmt(k)}"
71
+ if md.factory_method:
72
+ loc_name = f"factory method {md.factory_method}"
73
+
74
+ for dep in md.dependencies:
75
+ if dep.is_optional:
76
+ continue
77
+
78
+ if dep.is_list:
79
+ if dep.qualifier:
80
+ if not self._locator.collect_by_type(dep.key, dep.qualifier) and isinstance(dep.key, type) and not _skip_type(dep.key):
81
+ errors.append(f"{_fmt(k)} ({loc_name}) expects List[{_fmt(dep.key)}] with qualifier '{dep.qualifier}' but no matching components exist")
82
+ continue
83
+
84
+ dep_key = dep.key
85
+ if isinstance(dep_key, str):
86
+ key_found_by_name = self._find_md_for_name(dep_key)
87
+ directly_bound = dep_key in self._metadata or self._factory.has(dep_key)
88
+ if not key_found_by_name and not directly_bound:
89
+ errors.append(f"{_fmt(k)} ({loc_name}) depends on string key '{dep_key}' which is not bound")
90
+ continue
91
+
92
+ if isinstance(dep_key, type) and not _skip_type(dep_key):
93
+ dep_key_found = self._factory.has(dep_key) or dep_key in self._metadata
94
+ if not dep_key_found:
95
+ assignable_md = self._find_md_for_type(dep_key)
96
+ if assignable_md is None:
97
+ by_name_key = self._find_md_for_name(getattr(dep_key, "__name__", ""))
98
+ if by_name_key is None:
99
+ errors.append(f"{_fmt(k)} ({loc_name}) depends on {_fmt(dep_key)} which is not bound")
100
+ continue
101
+
102
+ if errors:
103
+ raise InvalidBindingError(errors)
pico_ioc/exceptions.py CHANGED
@@ -14,21 +14,6 @@ class ProviderNotFoundError(PicoError):
14
14
  self.key = key
15
15
  self.origin = origin
16
16
 
17
- class CircularDependencyError(PicoError):
18
- def __init__(self, chain: Iterable[Any], current: Any, details: str | None = None, hint: str | None = None):
19
- chain_str = " -> ".join(getattr(k, "__name__", str(k)) for k in chain)
20
- cur_str = getattr(current, "__name__", str(current))
21
- base = f"Circular dependency detected: {chain_str} -> {cur_str}"
22
- if details:
23
- base += f"\n\n{details}"
24
- if hint:
25
- base += f"\n\nHint: {hint}"
26
- super().__init__(base)
27
- self.chain = tuple(chain)
28
- self.current = current
29
- self.details = details
30
- self.hint = hint
31
-
32
17
  class ComponentCreationError(PicoError):
33
18
  def __init__(self, key: Any, cause: Exception):
34
19
  k = getattr(key, "__name__", key)
@@ -84,4 +69,3 @@ class EventBusHandlerError(EventBusError):
84
69
  self.event_name = event_name
85
70
  self.handler_name = handler_name
86
71
  self.cause = cause
87
-
pico_ioc/factory.py CHANGED
@@ -2,6 +2,7 @@
2
2
  from dataclasses import dataclass
3
3
  from typing import Any, Callable, Dict, Optional, Set, Tuple, Union
4
4
  from .exceptions import ProviderNotFoundError
5
+ from .analysis import DependencyRequest
5
6
 
6
7
  KeyT = Union[str, type]
7
8
  Provider = Callable[[], Any]
@@ -18,6 +19,7 @@ class ProviderMetadata:
18
19
  lazy: bool
19
20
  infra: Optional[str]
20
21
  pico_name: Optional[Any]
22
+ dependencies: Tuple[DependencyRequest, ...] = ()
21
23
  override: bool = False
22
24
  scope: str = "singleton"
23
25
 
@@ -45,4 +47,3 @@ class DeferredProvider:
45
47
  if self._pico is None or self._locator is None:
46
48
  raise RuntimeError("DeferredProvider must be attached before use")
47
49
  return self._builder(self._pico, self._locator)
48
-
pico_ioc/locator.py CHANGED
@@ -1,45 +1,62 @@
1
- from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Union
1
+ from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Union, get_origin, Annotated, get_args
2
2
  from .factory import ProviderMetadata
3
+ from .decorators import Qualifier
4
+ import inspect
5
+ from .analysis import DependencyRequest
3
6
 
4
7
  KeyT = Union[str, type]
5
8
 
9
+ def _get_signature_static(fn):
10
+ return inspect.signature(fn)
11
+
6
12
  class ComponentLocator:
7
13
  def __init__(self, metadata: Dict[KeyT, ProviderMetadata], indexes: Dict[str, Dict[Any, List[KeyT]]]) -> None:
8
14
  self._metadata = metadata
9
15
  self._indexes = indexes
10
16
  self._candidates: Optional[Set[KeyT]] = None
17
+
11
18
  def _ensure(self) -> Set[KeyT]:
12
19
  return set(self._metadata.keys()) if self._candidates is None else set(self._candidates)
20
+
13
21
  def _select_index(self, name: str, values: Iterable[Any]) -> Set[KeyT]:
14
22
  out: Set[KeyT] = set()
15
23
  idx = self._indexes.get(name, {})
16
24
  for v in values:
17
25
  out.update(idx.get(v, []))
18
26
  return out
27
+
19
28
  def _new(self, candidates: Set[KeyT]) -> "ComponentLocator":
20
29
  nl = ComponentLocator(self._metadata, self._indexes)
21
30
  nl._candidates = candidates
22
31
  return nl
32
+
23
33
  def with_index_any(self, name: str, *values: Any) -> "ComponentLocator":
24
34
  base = self._ensure()
25
35
  sel = self._select_index(name, values)
26
36
  return self._new(base & sel)
37
+
27
38
  def with_index_all(self, name: str, *values: Any) -> "ComponentLocator":
28
39
  base = self._ensure()
29
40
  cur = base
30
41
  for v in values:
31
42
  cur = cur & set(self._indexes.get(name, {}).get(v, []))
32
43
  return self._new(cur)
44
+
33
45
  def with_qualifier_any(self, *qs: Any) -> "ComponentLocator":
34
46
  return self.with_index_any("qualifier", *qs)
47
+
35
48
  def primary_only(self) -> "ComponentLocator":
36
49
  return self.with_index_any("primary", True)
50
+
37
51
  def lazy(self, is_lazy: bool = True) -> "ComponentLocator":
38
52
  return self.with_index_any("lazy", True) if is_lazy else self.with_index_any("lazy", False)
53
+
39
54
  def infra(self, *names: Any) -> "ComponentLocator":
40
55
  return self.with_index_any("infra", *names)
56
+
41
57
  def pico_name(self, *names: Any) -> "ComponentLocator":
42
58
  return self.with_index_any("pico_name", *names)
59
+
43
60
  def by_key_type(self, t: type) -> "ComponentLocator":
44
61
  base = self._ensure()
45
62
  if t is str:
@@ -49,5 +66,66 @@ class ComponentLocator:
49
66
  else:
50
67
  c = {k for k in base if isinstance(k, t)}
51
68
  return self._new(c)
69
+
52
70
  def keys(self) -> List[KeyT]:
53
- return list(self._ensure())
71
+ return list(self._ensure())
72
+
73
+ @staticmethod
74
+ def _implements_protocol(typ: type, proto: type) -> bool:
75
+ if not getattr(proto, "_is_protocol", False):
76
+ return False
77
+ try:
78
+ if getattr(proto, "__runtime_protocol__", False) or getattr(proto, "__annotations__", None) is not None:
79
+ inst = object.__new__(typ)
80
+ return isinstance(inst, proto)
81
+ except Exception:
82
+ pass
83
+ for name, val in proto.__dict__.items():
84
+ if name.startswith("_") or not callable(val):
85
+ continue
86
+ return True
87
+
88
+ def collect_by_type(self, t: type, q: Optional[str]) -> List[KeyT]:
89
+ keys = list(self._metadata.keys())
90
+ out: List[KeyT] = []
91
+ for k in keys:
92
+ md = self._metadata.get(k)
93
+ if md is None:
94
+ continue
95
+ typ = md.provided_type or md.concrete_class
96
+ if not isinstance(typ, type):
97
+ continue
98
+
99
+ ok = False
100
+ try:
101
+ ok = issubclass(typ, t)
102
+ except Exception:
103
+ ok = ComponentLocator._implements_protocol(typ, t)
104
+
105
+ if ok and (q is None or q in md.qualifiers):
106
+ out.append(k)
107
+ return out
108
+
109
+ def find_key_by_name(self, name: str) -> Optional[KeyT]:
110
+ for k, md in self._metadata.items():
111
+ if md.pico_name == name:
112
+ return k
113
+ typ = md.provided_type or md.concrete_class
114
+ if isinstance(typ, type) and getattr(typ, "__name__", "") == name:
115
+ return k
116
+ return None
117
+
118
+ def _compile_argplan_static(self, callable_obj):
119
+ raise NotImplementedError("This method is obsolete and replaced by analysis module")
120
+
121
+
122
+ def dependency_keys_for_static(self, md: ProviderMetadata):
123
+ deps: List[KeyT] = []
124
+ for dep in md.dependencies:
125
+ if dep.is_list:
126
+ if isinstance(dep.key, type):
127
+ keys = self.collect_by_type(dep.key, dep.qualifier)
128
+ deps.extend(keys)
129
+ else:
130
+ deps.append(dep.key)
131
+ return tuple(deps)
@@ -0,0 +1,35 @@
1
+ from typing import Dict, List, Tuple, Union
2
+ from .factory import Provider, ProviderMetadata
3
+ from .config_registrar import ConfigurationManager
4
+
5
+ KeyT = Union[str, type]
6
+
7
+ class ProviderSelector:
8
+ def __init__(self, config_manager: ConfigurationManager):
9
+ self._config_manager = config_manager
10
+
11
+ def _rank_provider(self, item: Tuple[bool, Provider, ProviderMetadata]) -> Tuple[int, int, int]:
12
+ provider, md = item[1], item[2]
13
+
14
+ is_present = 1 if self._config_manager.prefix_exists(md) else 0
15
+
16
+ pref = str(md.pico_name or "")
17
+ pref_len = len(pref)
18
+
19
+ is_primary = 1 if item[0] else 0
20
+
21
+ return (is_present, pref_len, is_primary)
22
+
23
+ def select_providers(
24
+ self,
25
+ candidates: Dict[KeyT, List[Tuple[bool, Provider, ProviderMetadata]]],
26
+ ) -> Dict[KeyT, Tuple[Provider, ProviderMetadata]]:
27
+
28
+ winners: Dict[KeyT, Tuple[Provider, ProviderMetadata]] = {}
29
+
30
+ for key, lst in candidates.items():
31
+ lst_sorted = sorted(lst, key=self._rank_provider, reverse=True)
32
+ chosen = lst_sorted[0]
33
+ winners[key] = (chosen[1], chosen[2])
34
+
35
+ return winners
pico_ioc/registrar.py ADDED
@@ -0,0 +1,169 @@
1
+ import os
2
+ import inspect
3
+ import logging
4
+ import functools
5
+ from dataclasses import is_dataclass, fields, MISSING
6
+ from typing import Any, Callable, Dict, List, Optional, Tuple, Union, get_args, get_origin, Iterable
7
+ from .constants import LOGGER, PICO_INFRA, PICO_NAME, PICO_KEY, PICO_META
8
+ from .exceptions import ConfigurationError, InvalidBindingError
9
+ from .factory import ComponentFactory, ProviderMetadata, DeferredProvider
10
+ from .locator import ComponentLocator
11
+ from .aop import UnifiedComponentProxy
12
+ from .decorators import Qualifier, get_return_type
13
+ from .config_builder import ContextConfig
14
+ from .config_runtime import TreeSource
15
+ from .config_registrar import ConfigurationManager
16
+ from .provider_selector import ProviderSelector
17
+ from .dependency_validator import DependencyValidator
18
+ from .component_scanner import ComponentScanner
19
+ from .analysis import analyze_callable_dependencies, DependencyRequest
20
+
21
+ KeyT = Union[str, type]
22
+ Provider = Callable[[], Any]
23
+
24
+ def _can_be_selected_for(reg_md: Dict[KeyT, ProviderMetadata], selector: Any) -> bool:
25
+ if not isinstance(selector, type):
26
+ return False
27
+ for md in reg_md.values():
28
+ typ = md.provided_type or md.concrete_class
29
+ if isinstance(typ, type):
30
+ try:
31
+ if issubclass(typ, selector):
32
+ return True
33
+ except Exception:
34
+ continue
35
+ return False
36
+
37
+ class Registrar:
38
+ def __init__(self, factory: ComponentFactory, *, profiles: Tuple[str, ...] = (), environ: Optional[Dict[str, str]] = None, logger: Optional[logging.Logger] = None, config: Optional[ContextConfig] = None) -> None:
39
+ self._factory = factory
40
+ self._profiles = set(p.strip() for p in profiles if p)
41
+ self._environ = environ if environ is not None else os.environ
42
+ self._metadata: Dict[KeyT, ProviderMetadata] = {}
43
+ self._indexes: Dict[str, Dict[Any, List[KeyT]]] = {}
44
+ self._log = logger or LOGGER
45
+ self._config_manager = ConfigurationManager(config)
46
+ self._provider_selector = ProviderSelector(self._config_manager)
47
+ self._scanner = ComponentScanner(self._profiles, self._environ, self._config_manager)
48
+ self._deferred: List[DeferredProvider] = []
49
+ self._provides_functions: Dict[KeyT, Callable[..., Any]] = {}
50
+
51
+
52
+ def locator(self) -> ComponentLocator:
53
+ loc = ComponentLocator(dict(self._metadata), dict(self._indexes))
54
+ setattr(loc, "_provides_functions", dict(self._provides_functions))
55
+ return loc
56
+
57
+ def attach_runtime(self, pico, locator: ComponentLocator) -> None:
58
+ for deferred in self._deferred:
59
+ deferred.attach(pico, locator)
60
+ for key, md in list(self._metadata.items()):
61
+ if md.lazy:
62
+ original = self._factory.get(key, origin='lazy')
63
+ def lazy_proxy_provider(_orig=original, _p=pico):
64
+ return UnifiedComponentProxy(container=_p, object_creator=_orig)
65
+ self._factory.bind(key, lazy_proxy_provider)
66
+
67
+
68
+ def _bind_if_absent(self, key: KeyT, provider: Provider) -> None:
69
+ if not self._factory.has(key):
70
+ self._factory.bind(key, provider)
71
+
72
+
73
+ def register_module(self, module: Any) -> None:
74
+ self._scanner.scan_module(module)
75
+
76
+
77
+ def _find_md_for_type(self, t: type) -> Optional[ProviderMetadata]:
78
+ cands: List[ProviderMetadata] = []
79
+ for md in self._metadata.values():
80
+ typ = md.provided_type or md.concrete_class
81
+ if not isinstance(typ, type):
82
+ continue
83
+ try:
84
+ if issubclass(typ, t):
85
+ cands.append(md)
86
+ except Exception:
87
+ continue
88
+ if not cands:
89
+ return None
90
+ prim = [m for m in cands if m.primary]
91
+ return prim[0] if prim else cands[0]
92
+
93
+ def _find_narrower_scope_from_deps(self, deps: Tuple[DependencyRequest, ...]) -> Optional[str]:
94
+ if not deps:
95
+ return None
96
+
97
+ for dep_req in deps:
98
+ if dep_req.is_list:
99
+ continue
100
+
101
+ dep_md = self._metadata.get(dep_req.key)
102
+ if dep_md is None:
103
+ if isinstance(dep_req.key, type):
104
+ dep_md = self._find_md_for_type(dep_req.key)
105
+
106
+ if dep_md and dep_md.scope != "singleton":
107
+ return dep_md.scope
108
+ return None
109
+
110
+ def _promote_scopes(self) -> None:
111
+ for k, md in list(self._metadata.items()):
112
+ if md.scope == "singleton":
113
+ ns = self._find_narrower_scope_from_deps(md.dependencies)
114
+ if ns and ns != "singleton":
115
+ self._metadata[k] = ProviderMetadata(key=md.key, provided_type=md.provided_type, concrete_class=md.concrete_class, factory_class=md.factory_class, factory_method=md.factory_method, qualifiers=md.qualifiers, primary=md.primary, lazy=md.lazy, infra=md.infra, pico_name=md.pico_name, override=md.override, scope=ns, dependencies=md.dependencies)
116
+
117
+ def _rebuild_indexes(self) -> None:
118
+ self._indexes.clear()
119
+ def add(idx: str, val: Any, key: KeyT):
120
+ b = self._indexes.setdefault(idx, {}).setdefault(val, [])
121
+ if key not in b:
122
+ b.append(key)
123
+
124
+ for k, md in self._metadata.items():
125
+ for q in md.qualifiers:
126
+ add("qualifier", q, k)
127
+ if md.primary:
128
+ add("primary", True, k)
129
+ add("lazy", bool(md.lazy), k)
130
+ if md.infra is not None:
131
+ add("infra", md.infra, k)
132
+ if md.pico_name is not None:
133
+ add("pico_name", md.pico_name, k)
134
+
135
+
136
+ def finalize(self, overrides: Optional[Dict[KeyT, Any]]) -> None:
137
+ candidates, on_missing, deferred_providers, provides_functions = self._scanner.get_scan_results()
138
+ self._deferred = deferred_providers
139
+ self._provides_functions = provides_functions
140
+
141
+ winners = self._provider_selector.select_providers(candidates)
142
+ for key, (provider, md) in winners.items():
143
+ self._bind_if_absent(key, provider)
144
+ self._metadata[key] = md
145
+
146
+ self._promote_scopes()
147
+ self._rebuild_indexes()
148
+
149
+ for _, selector, default_cls in sorted(on_missing, key=lambda x: -x[0]):
150
+ key = selector
151
+ if key in self._metadata or self._factory.has(key) or _can_be_selected_for(self._metadata, selector):
152
+ continue
153
+
154
+ deps = analyze_callable_dependencies(default_cls.__init__)
155
+ provider = DeferredProvider(lambda pico, loc, c=default_cls, d=deps: pico.build_class(c, loc, d))
156
+ qset = set(str(q) for q in getattr(default_cls, PICO_META, {}).get("qualifier", ()))
157
+ sc = getattr(default_cls, PICO_META, {}).get("scope", "singleton")
158
+ md = ProviderMetadata(key=key, provided_type=key if isinstance(key, type) else None, concrete_class=default_cls, factory_class=None, factory_method=None, qualifiers=qset, primary=True, lazy=bool(getattr(default_cls, PICO_META, {}).get("lazy", False)), infra=getattr(default_cls, PICO_INFRA, None), pico_name=getattr(default_cls, PICO_NAME, None), override=True, scope=sc, dependencies=deps)
159
+
160
+ self._bind_if_absent(key, provider)
161
+ self._metadata[key] = md
162
+ if isinstance(provider, DeferredProvider):
163
+ self._deferred.append(provider)
164
+
165
+ self._rebuild_indexes()
166
+
167
+ final_locator = ComponentLocator(self._metadata, self._indexes)
168
+ validator = DependencyValidator(self._metadata, self._factory, final_locator)
169
+ validator.validate_bindings()