pico-ioc 1.2.0__py3-none-any.whl → 1.4.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.
pico_ioc/proxy.py CHANGED
@@ -1,18 +1,25 @@
1
- # pico_ioc/proxy.py
1
+ # src/pico_ioc/proxy.py
2
+ from __future__ import annotations
3
+
4
+ import inspect
5
+ from functools import lru_cache
6
+ from typing import Any, Callable, Sequence
7
+
8
+ from .interceptors import Invocation, dispatch, MethodInterceptor
2
9
 
3
- from typing import Any, Callable
4
10
 
5
11
  class ComponentProxy:
12
+ """Proxy for lazy components. Creates the real object only when accessed."""
6
13
  def __init__(self, object_creator: Callable[[], Any]):
7
14
  object.__setattr__(self, "_object_creator", object_creator)
8
15
  object.__setattr__(self, "__real_object", None)
9
16
 
10
17
  def _get_real_object(self) -> Any:
11
- real_obj = object.__getattribute__(self, "__real_object")
12
- if real_obj is None:
13
- real_obj = object.__getattribute__(self, "_object_creator")()
14
- object.__setattr__(self, "__real_object", real_obj)
15
- return real_obj
18
+ real = object.__getattribute__(self, "__real_object")
19
+ if real is None:
20
+ real = object.__getattribute__(self, "_object_creator")()
21
+ object.__setattr__(self, "__real_object", real)
22
+ return real
16
23
 
17
24
  @property
18
25
  def __class__(self):
@@ -24,6 +31,8 @@ class ComponentProxy:
24
31
  def __str__(self): return str(self._get_real_object())
25
32
  def __repr__(self): return repr(self._get_real_object())
26
33
  def __dir__(self): return dir(self._get_real_object())
34
+
35
+ # container-like behavior
27
36
  def __len__(self): return len(self._get_real_object())
28
37
  def __getitem__(self, key): return self._get_real_object()[key]
29
38
  def __setitem__(self, key, value): self._get_real_object()[key] = value
@@ -31,6 +40,8 @@ class ComponentProxy:
31
40
  def __iter__(self): return iter(self._get_real_object())
32
41
  def __reversed__(self): return reversed(self._get_real_object())
33
42
  def __contains__(self, item): return item in self._get_real_object()
43
+
44
+ # operators
34
45
  def __add__(self, other): return self._get_real_object() + other
35
46
  def __sub__(self, other): return self._get_real_object() - other
36
47
  def __mul__(self, other): return self._get_real_object() * other
@@ -45,6 +56,8 @@ class ComponentProxy:
45
56
  def __and__(self, other): return self._get_real_object() & other
46
57
  def __xor__(self, other): return self._get_real_object() ^ other
47
58
  def __or__(self, other): return self._get_real_object() | other
59
+
60
+ # reflected operators
48
61
  def __radd__(self, other): return other + self._get_real_object()
49
62
  def __rsub__(self, other): return other - self._get_real_object()
50
63
  def __rmul__(self, other): return other * self._get_real_object()
@@ -59,6 +72,8 @@ class ComponentProxy:
59
72
  def __rand__(self, other): return other & self._get_real_object()
60
73
  def __rxor__(self, other): return other ^ self._get_real_object()
61
74
  def __ror__(self, other): return other | self._get_real_object()
75
+
76
+ # misc
62
77
  def __neg__(self): return -self._get_real_object()
63
78
  def __pos__(self): return +self._get_real_object()
64
79
  def __abs__(self): return abs(self._get_real_object())
@@ -71,7 +86,44 @@ class ComponentProxy:
71
86
  def __ge__(self, other): return self._get_real_object() >= other
72
87
  def __hash__(self): return hash(self._get_real_object())
73
88
  def __bool__(self): return bool(self._get_real_object())
89
+
90
+ # callables & context
74
91
  def __call__(self, *args, **kwargs): return self._get_real_object()(*args, **kwargs)
75
92
  def __enter__(self): return self._get_real_object().__enter__()
76
93
  def __exit__(self, exc_type, exc_val, exc_tb): return self._get_real_object().__exit__(exc_type, exc_val, exc_tb)
77
94
 
95
+
96
+ class IoCProxy:
97
+ """Proxy that wraps an object and applies MethodInterceptors on method calls."""
98
+ __slots__ = ("_target", "_interceptors")
99
+
100
+ def __init__(self, target: object, interceptors: Sequence[MethodInterceptor]):
101
+ self._target = target
102
+ self._interceptors = tuple(interceptors)
103
+
104
+ def __getattr__(self, name: str) -> Any:
105
+ attr = getattr(self._target, name)
106
+ if not callable(attr):
107
+ return attr
108
+ if hasattr(attr, "__get__"):
109
+ bound_fn = attr.__get__(self._target, type(self._target))
110
+ else:
111
+ bound_fn = attr
112
+
113
+ @lru_cache(maxsize=None)
114
+ def _wrap(fn: Callable[..., Any]):
115
+ if inspect.iscoroutinefunction(fn):
116
+ async def aw(*args, **kwargs):
117
+ inv = Invocation(self._target, name, fn, args, kwargs)
118
+ return await dispatch(self._interceptors, inv)
119
+ return aw
120
+ else:
121
+ def sw(*args, **kwargs):
122
+ inv = Invocation(self._target, name, fn, args, kwargs)
123
+ res = dispatch(self._interceptors, inv)
124
+ if inspect.isawaitable(res):
125
+ raise RuntimeError(f"Async interceptor on sync method: {name}")
126
+ return res
127
+ return sw
128
+ return _wrap(bound_fn)
129
+
pico_ioc/resolver.py CHANGED
@@ -1,14 +1,16 @@
1
- # pico_ioc/resolver.py (Python 3.10+)
2
-
1
+ # src/pico_ioc/resolver.py
3
2
  from __future__ import annotations
3
+
4
4
  import inspect
5
- from typing import Any, Annotated, get_args, get_origin, get_type_hints
5
+ from typing import Any, Annotated, Callable, get_args, get_origin, get_type_hints
6
6
  from contextvars import ContextVar
7
7
 
8
+
8
9
  _path: ContextVar[list[tuple[str, str]]] = ContextVar("pico_resolve_path", default=[])
9
10
 
11
+
10
12
  def _get_hints(obj, owner_cls=None) -> dict:
11
- """type hints with include_extras=True and correct globals/locals."""
13
+ """Return type hints with include_extras=True, using correct globals/locals."""
12
14
  mod = inspect.getmodule(obj)
13
15
  g = getattr(mod, "__dict__", {})
14
16
  l = vars(owner_cls) if owner_cls is not None else None
@@ -16,15 +18,14 @@ def _get_hints(obj, owner_cls=None) -> dict:
16
18
 
17
19
 
18
20
  def _is_collection_hint(tp) -> bool:
19
- """True if tp is a list[...] or tuple[...]."""
20
21
  origin = get_origin(tp) or tp
21
22
  return origin in (list, tuple)
22
23
 
23
24
 
24
25
  def _base_and_qualifiers_from_hint(tp):
25
26
  """
26
- Extract (base, qualifiers, container_kind) from a collection hint.
27
- Supports list[T] / tuple[T] and Annotated[T, "qual1", ...].
27
+ Extract (base, qualifiers, container_kind) from a type hint.
28
+ Supports list[T], tuple[T], Annotated[T, "qual1", ...].
28
29
  """
29
30
  origin = get_origin(tp) or tp
30
31
  args = get_args(tp) or ()
@@ -47,78 +48,85 @@ class Resolver:
47
48
  self.c = container
48
49
  self._prefer_name_first = bool(prefer_name_first)
49
50
 
50
- def create_instance(self, cls):
51
- sig = inspect.signature(cls.__init__)
52
- hints = _get_hints(cls.__init__, owner_cls=cls)
53
- kwargs = {}
54
- for name, param in sig.parameters.items():
55
- if name == "self" or param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
56
- continue
57
- ann = hints.get(name, param.annotation)
58
- st = _path.get()
59
- _path.set(st + [(cls.__name__, name)])
60
- try:
61
- value = self._resolve_param(name, ann)
62
- except NameError as e:
63
- # ⬅️ Important: skip if parameter has a default
64
- if param.default is not inspect._empty:
65
- continue
66
- chain = " -> ".join(f"{c}.__init__.{p}" for c, p in _path.get())
67
- raise NameError(f"{e} (required by {chain})") from e
68
- finally:
69
- cur = _path.get()
70
- _path.set(cur[:-1] if cur else [])
71
- kwargs[name] = value
72
- return cls(**kwargs)
51
+ # --- core resolution ---
73
52
 
74
- def kwargs_for_callable(self, fn, *, owner_cls=None):
53
+ def _resolve_dependencies_for_callable(self, fn: Callable, owner_cls: Any = None) -> dict:
75
54
  sig = inspect.signature(fn)
76
55
  hints = _get_hints(fn, owner_cls=owner_cls)
77
56
  kwargs = {}
78
- owner_name = getattr(owner_cls, "__name__", getattr(fn, "__qualname__", "callable"))
57
+
58
+ path_owner = getattr(owner_cls, "__name__", getattr(fn, "__qualname__", "callable"))
59
+ if fn.__name__ == "__init__" and owner_cls:
60
+ path_owner = f"{path_owner}.__init__"
61
+
79
62
  for name, param in sig.parameters.items():
80
- if name == "self" or param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
63
+ if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD) or name == "self":
81
64
  continue
65
+
82
66
  ann = hints.get(name, param.annotation)
83
67
  st = _path.get()
84
- _path.set(st + [(owner_name, name)])
68
+ _path.set(st + [(path_owner, name)])
85
69
  try:
86
- value = self._resolve_param(name, ann)
70
+ kwargs[name] = self._resolve_param(name, ann)
87
71
  except NameError as e:
88
- # ⬅️ Important: skip if parameter has a default
89
- if param.default is not inspect._empty:
90
- # do not include in kwargs
91
- _path.set(st) # pop before continue
72
+ if param.default is not inspect.Parameter.empty:
73
+ _path.set(st)
92
74
  continue
93
- chain = " -> ".join(f"{c}.__init__.{p}" for c, p in _path.get())
75
+ if "(required by" in str(e):
76
+ raise
77
+ chain = " -> ".join(f"{owner}.{param}" for owner, param in _path.get())
94
78
  raise NameError(f"{e} (required by {chain})") from e
95
79
  finally:
96
80
  cur = _path.get()
97
- _path.set(cur[:-1] if cur else [])
98
- kwargs[name] = value
81
+ if cur:
82
+ _path.set(cur[:-1])
99
83
  return kwargs
100
84
 
85
+ def create_instance(self, cls: type) -> Any:
86
+ """Instantiate a class by resolving its __init__ dependencies."""
87
+ ctor_kwargs = self._resolve_dependencies_for_callable(cls.__init__, owner_cls=cls)
88
+ return cls(**ctor_kwargs)
89
+
90
+ def kwargs_for_callable(self, fn: Callable, *, owner_cls: Any = None) -> dict:
91
+ """Resolve all keyword arguments for any callable."""
92
+ return self._resolve_dependencies_for_callable(fn, owner_cls=owner_cls)
93
+
94
+ # --- param resolution ---
95
+
96
+ def _notify_resolve(self, key, ann, quals=()):
97
+ for ci in getattr(self.c, "_container_interceptors", ()):
98
+ try:
99
+ ci.on_resolve(key, ann, tuple(quals) if quals else ())
100
+ except Exception:
101
+ pass
102
+
101
103
  def _resolve_param(self, name: str, ann: Any):
102
- # collections (list/tuple) with optional qualifiers via Annotated
104
+ # collections
103
105
  if _is_collection_hint(ann):
104
- base, quals, container_kind = _base_and_qualifiers_from_hint(ann)
106
+ base, quals, kind = _base_and_qualifiers_from_hint(ann)
107
+ self._notify_resolve(base, ann, quals)
105
108
  items = self.c._resolve_all_for_base(base, qualifiers=quals)
106
- return list(items) if container_kind is list else tuple(items)
109
+ return list(items) if kind is list else tuple(items)
107
110
 
108
- # precedence: by name > by exact annotation > by MRO > by name again
111
+ # precedence
109
112
  if self._prefer_name_first and self.c.has(name):
113
+ self._notify_resolve(name, ann, ())
110
114
  return self.c.get(name)
111
115
 
112
116
  if ann is not inspect._empty and self.c.has(ann):
117
+ self._notify_resolve(ann, ann, ())
113
118
  return self.c.get(ann)
114
119
 
115
120
  if ann is not inspect._empty and isinstance(ann, type):
116
121
  for base in ann.__mro__[1:]:
117
122
  if self.c.has(base):
123
+ self._notify_resolve(base, ann, ())
118
124
  return self.c.get(base)
119
125
 
120
126
  if self.c.has(name):
127
+ self._notify_resolve(name, ann, ())
121
128
  return self.c.get(name)
122
129
 
123
130
  missing = ann if ann is not inspect._empty else name
124
131
  raise NameError(f"No provider found for key {missing!r}")
132
+
pico_ioc/scanner.py CHANGED
@@ -1,4 +1,6 @@
1
- # pico_ioc/scanner.py
1
+ # src/pico_ioc/scanner.py
2
+ from __future__ import annotations
3
+
2
4
  import importlib
3
5
  import inspect
4
6
  import logging
@@ -6,6 +8,7 @@ import pkgutil
6
8
  from types import ModuleType
7
9
  from typing import Any, Callable, Optional, Tuple, List, Iterable
8
10
 
11
+ from .plugins import run_plugin_hook, PicoPlugin
9
12
  from .container import PicoContainer, Binder
10
13
  from .decorators import (
11
14
  COMPONENT_FLAG,
@@ -14,13 +17,15 @@ from .decorators import (
14
17
  FACTORY_FLAG,
15
18
  PROVIDES_KEY,
16
19
  PROVIDES_LAZY,
17
- COMPONENT_TAGS,
20
+ COMPONENT_TAGS,
18
21
  PROVIDES_TAGS,
22
+ INTERCEPTOR_META,
19
23
  )
20
24
  from .proxy import ComponentProxy
21
25
  from .resolver import Resolver
22
- from .plugins import PicoPlugin
23
26
  from . import _state
27
+ from .utils import _provider_from_class, _provider_from_callable
28
+ from .config import is_config_component, build_component_instance, ConfigRegistry
24
29
 
25
30
 
26
31
  def scan_and_configure(
@@ -29,15 +34,15 @@ def scan_and_configure(
29
34
  *,
30
35
  exclude: Optional[Callable[[str], bool]] = None,
31
36
  plugins: Tuple[PicoPlugin, ...] = (),
32
- ) -> None:
37
+ ) -> tuple[int, int, list[tuple[Any, dict]]]:
33
38
  """
34
- Scan a package, discover component classes/factories, and bind them into the container.
39
+ Scan a package, bind components/factories, and collect interceptor declarations.
35
40
 
36
- Args:
37
- package_or_name: Package module or importable package name (str).
38
- container: Target PicoContainer to receive bindings.
39
- exclude: Optional predicate that receives a module name and returns True to skip it.
40
- plugins: Optional lifecycle plugins that receive scan/bind events.
41
+ Returns: (component_count, factory_count, interceptor_decls)
42
+ - interceptor_decls entries:
43
+ (cls, meta) for @interceptor class
44
+ (fn, meta) for @interceptor function
45
+ ((owner_cls, fn), meta) for @interceptor methods
41
46
  """
42
47
  package = _as_module(package_or_name)
43
48
  logging.info("Scanning in '%s'...", getattr(package, "__name__", repr(package)))
@@ -45,34 +50,26 @@ def scan_and_configure(
45
50
  binder = Binder(container)
46
51
  resolver = Resolver(container)
47
52
 
48
- _run_plugin_hook(plugins, "before_scan", package, binder)
53
+ run_plugin_hook(plugins, "before_scan", package, binder)
49
54
 
50
- comp_classes, factory_classes = _collect_decorated_classes(
55
+ comp_classes, factory_classes, interceptor_decls = _collect_decorated(
51
56
  package=package,
52
57
  exclude=exclude,
53
58
  plugins=plugins,
54
59
  binder=binder,
55
60
  )
56
61
 
57
- _run_plugin_hook(plugins, "after_scan", package, binder)
62
+ run_plugin_hook(plugins, "after_scan", package, binder)
58
63
 
59
- _register_component_classes(
60
- classes=comp_classes,
61
- container=container,
62
- resolver=resolver,
63
- )
64
+ _register_component_classes(classes=comp_classes, container=container, resolver=resolver)
65
+ _register_factory_classes(factory_classes=factory_classes, container=container, resolver=resolver)
64
66
 
65
- _register_factory_classes(
66
- factory_classes=factory_classes,
67
- container=container,
68
- resolver=resolver,
69
- )
67
+ return len(comp_classes), len(factory_classes), interceptor_decls
70
68
 
71
69
 
72
- # -------------------- Helpers (private) --------------------
70
+ # -------------------- helpers --------------------
73
71
 
74
72
  def _as_module(package_or_name: Any) -> ModuleType:
75
- """Return a module from either a module object or an importable string name."""
76
73
  if isinstance(package_or_name, str):
77
74
  return importlib.import_module(package_or_name)
78
75
  if hasattr(package_or_name, "__spec__"):
@@ -80,84 +77,68 @@ def _as_module(package_or_name: Any) -> ModuleType:
80
77
  raise TypeError("package_or_name must be a module or importable package name (str).")
81
78
 
82
79
 
83
- def _run_plugin_hook(
84
- plugins: Tuple[PicoPlugin, ...],
85
- hook_name: str,
86
- *args,
87
- **kwargs,
88
- ) -> None:
89
- """Run a lifecycle hook across all plugins, logging (but not raising) exceptions."""
90
- for pl in plugins:
91
- try:
92
- fn = getattr(pl, hook_name, None)
93
- if fn:
94
- fn(*args, **kwargs)
95
- except Exception:
96
- logging.exception("Plugin %s failed", hook_name)
97
-
98
-
99
- def _iter_package_modules(
100
- package: ModuleType,
101
- ) -> Iterable[str]:
102
- """
103
- Yield fully qualified module names under the given package.
104
-
105
- Requires the package to have a __path__ (i.e., be a package, not a single module).
106
- """
80
+ def _iter_package_modules(package: ModuleType) -> Iterable[str]:
81
+ """Yield fully-qualified module names under a package (recursive)."""
107
82
  try:
108
83
  pkg_path = package.__path__ # type: ignore[attr-defined]
109
84
  except Exception:
110
- return # not a package; nothing to iterate
111
-
85
+ return
112
86
  prefix = package.__name__ + "."
113
87
  for _finder, name, _is_pkg in pkgutil.walk_packages(pkg_path, prefix):
114
88
  yield name
115
89
 
116
90
 
117
- def _collect_decorated_classes(
91
+ def _collect_decorated(
118
92
  *,
119
93
  package: ModuleType,
120
94
  exclude: Optional[Callable[[str], bool]],
121
95
  plugins: Tuple[PicoPlugin, ...],
122
96
  binder: Binder,
123
- ) -> Tuple[List[type], List[type]]:
124
- """
125
- Import modules under `package`, visit classes, and collect those marked with
126
- @component or @factory_component decorators.
127
- """
128
- comp_classes: List[type] = []
129
- factory_classes: List[type] = []
97
+ ) -> Tuple[List[type], List[type], List[tuple[Any, dict]]]:
98
+ comps: List[type] = []
99
+ facts: List[type] = []
100
+ interceptors: List[tuple[Any, dict]] = []
101
+
102
+ def _collect_from_class(cls: type):
103
+ if getattr(cls, COMPONENT_FLAG, False):
104
+ comps.append(cls)
105
+ elif getattr(cls, FACTORY_FLAG, False):
106
+ facts.append(cls)
107
+
108
+ meta_class = getattr(cls, INTERCEPTOR_META, None)
109
+ if meta_class:
110
+ interceptors.append((cls, dict(meta_class)))
111
+
112
+ for _nm, fn in inspect.getmembers(cls, predicate=inspect.isfunction):
113
+ meta_m = getattr(fn, INTERCEPTOR_META, None)
114
+ if meta_m:
115
+ interceptors.append(((cls, fn), dict(meta_m)))
130
116
 
131
117
  def _visit_module(module: ModuleType):
132
118
  for _name, obj in inspect.getmembers(module, inspect.isclass):
133
- # Allow plugins to inspect/transform/record classes
134
- _run_plugin_hook(plugins, "visit_class", module, obj, binder)
119
+ run_plugin_hook(plugins, "visit_class", module, obj, binder)
120
+ _collect_from_class(obj)
135
121
 
136
- # Collect decorated classes
137
- if getattr(obj, COMPONENT_FLAG, False):
138
- comp_classes.append(obj)
139
- elif getattr(obj, FACTORY_FLAG, False):
140
- factory_classes.append(obj)
122
+ for _name, fn in inspect.getmembers(module, predicate=inspect.isfunction):
123
+ meta = getattr(fn, INTERCEPTOR_META, None)
124
+ if meta:
125
+ interceptors.append((fn, dict(meta)))
141
126
 
142
- # 1) Si es un paquete, recorrer submódulos
143
127
  for mod_name in _iter_package_modules(package):
144
128
  if exclude and exclude(mod_name):
145
129
  logging.info("Skipping module %s (excluded)", mod_name)
146
130
  continue
147
-
148
131
  try:
149
132
  module = importlib.import_module(mod_name)
150
133
  except Exception as e:
151
134
  logging.warning("Module %s not processed: %s", mod_name, e)
152
135
  continue
153
-
154
136
  _visit_module(module)
155
137
 
156
- # 2) Si el “paquete” raíz es un módulo (sin __path__), también hay que visitarlo.
157
138
  if not hasattr(package, "__path__"):
158
139
  _visit_module(package)
159
140
 
160
- return comp_classes, factory_classes
141
+ return comps, facts, interceptors
161
142
 
162
143
 
163
144
  def _register_component_classes(
@@ -166,23 +147,20 @@ def _register_component_classes(
166
147
  container: PicoContainer,
167
148
  resolver: Resolver,
168
149
  ) -> None:
169
- """
170
- Register @component classes into the container.
171
-
172
- Binding key:
173
- - If the class has COMPONENT_KEY, use it; otherwise, bind by the class itself.
174
- Laziness:
175
- - If COMPONENT_LAZY is True, provide a proxy that defers instantiation.
176
- """
177
150
  for cls in classes:
178
151
  key = getattr(cls, COMPONENT_KEY, cls)
179
152
  is_lazy = bool(getattr(cls, COMPONENT_LAZY, False))
180
153
  tags = tuple(getattr(cls, COMPONENT_TAGS, ()))
181
- def _provider_factory(c=cls, lazy=is_lazy):
182
- def _factory():
183
- return ComponentProxy(lambda: resolver.create_instance(c)) if lazy else resolver.create_instance(c)
184
- return _factory
185
- container.bind(key, _provider_factory(), lazy=is_lazy, tags=tags)
154
+ if is_config_component(cls):
155
+ registry: ConfigRegistry | None = getattr(container, "_config_registry", None)
156
+ def _prov(_c=cls, _reg=registry):
157
+ if _reg is None:
158
+ raise RuntimeError(f"No config registry found to build {_c.__name__}")
159
+ return build_component_instance(_c, _reg)
160
+ provider = (lambda p=_prov: ComponentProxy(p)) if is_lazy else _prov
161
+ else:
162
+ provider = _provider_from_class(cls, resolver=resolver, lazy=is_lazy)
163
+ container.bind(key, provider, lazy=is_lazy, tags=tags)
186
164
 
187
165
 
188
166
  def _register_factory_classes(
@@ -191,19 +169,8 @@ def _register_factory_classes(
191
169
  container: PicoContainer,
192
170
  resolver: Resolver,
193
171
  ) -> None:
194
- """
195
- Register products of @factory_component classes.
196
-
197
- For each factory class:
198
- - Instantiate the factory via the resolver.
199
- - For each method with @provides:
200
- - Bind the provided key to a callable that calls the factory method.
201
- - If PROVIDES_LAZY is True, bind a proxy that defers the method call.
202
- """
203
172
  for fcls in factory_classes:
204
173
  try:
205
- # Durante el escaneo, permitir la resolución de dependencias de la factory
206
- # elevando temporalmente el flag `_resolving` para no chocar con la guardia.
207
174
  tok_res = _state._resolving.set(True)
208
175
  try:
209
176
  finst = resolver.create_instance(fcls)
@@ -217,14 +184,20 @@ def _register_factory_classes(
217
184
  provided_key = getattr(func, PROVIDES_KEY, None)
218
185
  if provided_key is None:
219
186
  continue
187
+
220
188
  is_lazy = bool(getattr(func, PROVIDES_LAZY, False))
221
189
  tags = tuple(getattr(func, PROVIDES_TAGS, ()))
190
+
222
191
  bound = getattr(finst, attr_name, func.__get__(finst, fcls))
223
- def _make_provider(m=bound, owner=fcls, lazy=is_lazy):
224
- def _factory():
225
- kwargs = resolver.kwargs_for_callable(m, owner_cls=owner)
226
- def _call(): return m(**kwargs)
227
- return ComponentProxy(lambda: _call()) if lazy else _call()
228
- return _factory
229
- container.bind(provided_key, _make_provider(), lazy=is_lazy, tags=tags)
192
+ prov = _provider_from_callable(bound, owner_cls=fcls, resolver=resolver, lazy=is_lazy)
193
+
194
+ if isinstance(provided_key, type):
195
+ try:
196
+ setattr(prov, "_pico_alias_for", provided_key)
197
+ except Exception:
198
+ pass
199
+ unique_key = (provided_key, f"{fcls.__name__}.{attr_name}")
200
+ container.bind(unique_key, prov, lazy=is_lazy, tags=tags)
201
+ else:
202
+ container.bind(provided_key, prov, lazy=is_lazy, tags=tags)
230
203
 
pico_ioc/scope.py ADDED
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Optional
4
+
5
+ from .container import PicoContainer
6
+
7
+ class ScopedContainer(PicoContainer):
8
+ def __init__(self, built_container: PicoContainer, base: Optional[PicoContainer], strict: bool):
9
+ super().__init__(providers=getattr(built_container, "_providers", {}).copy())
10
+
11
+ self._active_profiles = getattr(built_container, "_active_profiles", ())
12
+
13
+ base_method_its = getattr(base, "_method_interceptors", ()) if base else ()
14
+ base_container_its = getattr(base, "_container_interceptors", ()) if base else ()
15
+
16
+ self._method_interceptors = base_method_its
17
+ self._container_interceptors = base_container_its
18
+ self._seen_interceptor_types = {type(it) for it in (base_method_its + base_container_its)}
19
+
20
+ for it in getattr(built_container, "_method_interceptors", ()):
21
+ self.add_method_interceptor(it)
22
+ for it in getattr(built_container, "_container_interceptors", ()):
23
+ self.add_container_interceptor(it)
24
+
25
+ self._base = base
26
+ self._strict = strict
27
+
28
+ if base:
29
+ self._singletons.update(getattr(base, "_singletons", {}))
30
+
31
+ def __enter__(self): return self
32
+ def __exit__(self, exc_type, exc, tb): return False
33
+
34
+ def has(self, key: Any) -> bool:
35
+ if super().has(key): return True
36
+ if not self._strict and self._base is not None:
37
+ return self._base.has(key)
38
+ return False
39
+
40
+ def get(self, key: Any):
41
+ try:
42
+ return super().get(key)
43
+ except NameError as e:
44
+ if not self._strict and self._base is not None and self._base.has(key):
45
+ return self._base.get(key)
46
+ raise e
pico_ioc/utils.py ADDED
@@ -0,0 +1,25 @@
1
+ # src/pico_ioc/utils.py
2
+ from typing import Any, Callable
3
+ from .container import PicoContainer
4
+ from .proxy import ComponentProxy
5
+
6
+ def _wrap_if_lazy(provider: Callable, is_lazy: bool) -> Callable:
7
+ """Wraps a provider in a ComponentProxy if it's marked as lazy."""
8
+ return (lambda: ComponentProxy(provider)) if is_lazy else provider
9
+
10
+ def _provider_from_class(cls: type, *, resolver, lazy: bool):
11
+ def _new():
12
+ return resolver.create_instance(cls)
13
+ return _wrap_if_lazy(_new, lazy)
14
+
15
+ def _provider_from_callable(fn, *, owner_cls, resolver, lazy: bool):
16
+ def _invoke():
17
+ kwargs = resolver.kwargs_for_callable(fn, owner_cls=owner_cls)
18
+ return fn(**kwargs)
19
+ return _wrap_if_lazy(_invoke, lazy)
20
+
21
+ def create_alias_provider(container: PicoContainer, target_key: Any) -> Callable[[], Any]:
22
+ """Creates a provider that delegates the get() call to the container for another key."""
23
+ def _provider():
24
+ return container.get(target_key)
25
+ return _provider