pico-ioc 0.5.2__py3-none-any.whl → 1.0.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/container.py ADDED
@@ -0,0 +1,155 @@
1
+ # pico_ioc/container.py
2
+ from __future__ import annotations
3
+ import inspect
4
+ from typing import Any, Dict, get_origin, get_args, Annotated
5
+ import typing as _t
6
+
7
+ from .decorators import QUALIFIERS_KEY
8
+ from . import _state # re-entrancy guard
9
+
10
+
11
+ class Binder:
12
+ def __init__(self, container: "PicoContainer"):
13
+ self._c = container
14
+
15
+ def bind(self, key: Any, provider, *, lazy: bool):
16
+ self._c.bind(key, provider, lazy=lazy)
17
+
18
+ def has(self, key: Any) -> bool:
19
+ return self._c.has(key)
20
+
21
+ def get(self, key: Any):
22
+ return self._c.get(key)
23
+
24
+
25
+ class PicoContainer:
26
+ def __init__(self):
27
+ self._providers: Dict[Any, Dict[str, Any]] = {}
28
+ self._singletons: Dict[Any, Any] = {}
29
+
30
+ def bind(self, key: Any, provider, *, lazy: bool):
31
+ meta = {"factory": provider, "lazy": bool(lazy)}
32
+ try:
33
+ q = getattr(key, QUALIFIERS_KEY, ())
34
+ except Exception:
35
+ q = ()
36
+ meta["qualifiers"] = tuple(q) if q else ()
37
+ self._providers[key] = meta
38
+
39
+ def has(self, key: Any) -> bool:
40
+ return key in self._providers
41
+
42
+ def get(self, key: Any):
43
+ # block only when scanning and NOT currently resolving a dependency
44
+ if _state._scanning.get() and not _state._resolving.get():
45
+ raise RuntimeError("re-entrant container access during scan")
46
+
47
+ prov = self._providers.get(key)
48
+ if prov is None:
49
+ raise NameError(f"No provider found for key {key!r}")
50
+
51
+ if key in self._singletons:
52
+ return self._singletons[key]
53
+
54
+ # mark resolving around factory execution
55
+ tok = _state._resolving.set(True)
56
+ try:
57
+ instance = prov["factory"]()
58
+ finally:
59
+ _state._resolving.reset(tok)
60
+
61
+ # memoize always (both lazy and non-lazy after first get)
62
+ self._singletons[key] = instance
63
+ return instance
64
+
65
+ def eager_instantiate_all(self):
66
+ for key, prov in list(self._providers.items()):
67
+ if not prov["lazy"]:
68
+ self.get(key)
69
+
70
+ def get_all(self, base_type: Any):
71
+ return tuple(self._resolve_all_for_base(base_type, qualifiers=()))
72
+
73
+ def get_all_qualified(self, base_type: Any, *qualifiers: str):
74
+ return tuple(self._resolve_all_for_base(base_type, qualifiers=qualifiers))
75
+
76
+ def _resolve_all_for_base(self, base_type: Any, qualifiers=()):
77
+ matches = []
78
+ for provider_key, meta in self._providers.items():
79
+ cls = provider_key if isinstance(provider_key, type) else None
80
+ if cls is None:
81
+ continue
82
+
83
+ # Avoid self-inclusion loops: if the class itself requires a collection
84
+ # of `base_type` in its __init__, don't treat it as an implementation
85
+ # of `base_type` when building that collection.
86
+ if _requires_collection_of_base(cls, base_type):
87
+ continue
88
+
89
+ if _is_compatible(cls, base_type):
90
+ prov_qs = meta.get("qualifiers", ())
91
+ if all(q in prov_qs for q in qualifiers):
92
+ inst = self.get(provider_key)
93
+ matches.append(inst)
94
+ return matches
95
+
96
+
97
+ def _is_protocol(t) -> bool:
98
+ return getattr(t, "_is_protocol", False) is True
99
+
100
+
101
+ def _is_compatible(cls, base) -> bool:
102
+ try:
103
+ if isinstance(base, type) and issubclass(cls, base):
104
+ return True
105
+ except TypeError:
106
+ pass
107
+
108
+ if _is_protocol(base):
109
+ # simple structural check: ensure methods/attrs declared on the Protocol exist on the class
110
+ names = set(getattr(base, "__annotations__", {}).keys())
111
+ names.update(n for n in getattr(base, "__dict__", {}).keys() if not n.startswith("_"))
112
+ for n in names:
113
+ if n.startswith("__") and n.endswith("__"):
114
+ continue
115
+ if not hasattr(cls, n):
116
+ return False
117
+ return True
118
+
119
+ return False
120
+
121
+ def _requires_collection_of_base(cls, base) -> bool:
122
+ """
123
+ Return True if `cls.__init__` has any parameter annotated as a collection
124
+ (list/tuple, including Annotated variants) of `base`. This prevents treating
125
+ `cls` as an implementation of `base` while building that collection,
126
+ avoiding recursion.
127
+ """
128
+ try:
129
+ sig = inspect.signature(cls.__init__)
130
+ except Exception:
131
+ return False
132
+
133
+ try:
134
+ from .resolver import _get_hints # type: ignore
135
+ hints = _get_hints(cls.__init__, owner_cls=cls)
136
+ except Exception:
137
+ hints = {}
138
+
139
+ for name, param in sig.parameters.items():
140
+ if name == "self":
141
+ continue
142
+ ann = hints.get(name, param.annotation)
143
+ origin = get_origin(ann) or ann
144
+ if origin in (list, tuple, _t.List, _t.Tuple):
145
+ inner = (get_args(ann) or (object,))[0]
146
+ # Unwrap Annotated[T, ...] si aparece
147
+ if get_origin(inner) is Annotated:
148
+ args = get_args(inner)
149
+ if args:
150
+ inner = args[0]
151
+ if inner is base:
152
+ return True
153
+ return False
154
+
155
+
pico_ioc/decorators.py ADDED
@@ -0,0 +1,76 @@
1
+ # pico_ioc/decorators.py
2
+ from __future__ import annotations
3
+ import functools
4
+ from typing import Any, Iterable
5
+
6
+ COMPONENT_FLAG = "_is_component"
7
+ COMPONENT_KEY = "_component_key"
8
+ COMPONENT_LAZY = "_component_lazy"
9
+
10
+ FACTORY_FLAG = "_is_factory_component"
11
+ PROVIDES_KEY = "_provides_name"
12
+ PROVIDES_LAZY = "_pico_lazy"
13
+
14
+ PLUGIN_FLAG = "_is_pico_plugin"
15
+ QUALIFIERS_KEY = "_pico_qualifiers"
16
+
17
+
18
+ def factory_component(cls):
19
+ setattr(cls, FACTORY_FLAG, True)
20
+ return cls
21
+
22
+
23
+ def provides(key: Any, *, lazy: bool = False):
24
+ def dec(fn):
25
+ @functools.wraps(fn)
26
+ def w(*a, **k):
27
+ return fn(*a, **k)
28
+ setattr(w, PROVIDES_KEY, key)
29
+ setattr(w, PROVIDES_LAZY, bool(lazy))
30
+ return w
31
+ return dec
32
+
33
+
34
+ def component(cls=None, *, name: Any = None, lazy: bool = False):
35
+ def dec(c):
36
+ setattr(c, COMPONENT_FLAG, True)
37
+ setattr(c, COMPONENT_KEY, name if name is not None else c)
38
+ setattr(c, COMPONENT_LAZY, bool(lazy))
39
+ return c
40
+ return dec(cls) if cls else dec
41
+
42
+
43
+ def plugin(cls):
44
+ setattr(cls, PLUGIN_FLAG, True)
45
+ return cls
46
+
47
+
48
+ class Qualifier(str):
49
+ __slots__ = () # tiny memory win; immutable like str
50
+
51
+
52
+ def qualifier(*qs: Qualifier):
53
+ def dec(cls):
54
+ current: Iterable[Qualifier] = getattr(cls, QUALIFIERS_KEY, ())
55
+ seen = set(current)
56
+ merged = list(current)
57
+ for q in qs:
58
+ if q not in seen:
59
+ merged.append(q)
60
+ seen.add(q)
61
+ setattr(cls, QUALIFIERS_KEY, tuple(merged))
62
+ return cls
63
+ return dec
64
+
65
+
66
+ __all__ = [
67
+ # decorators
68
+ "component", "factory_component", "provides", "plugin", "qualifier",
69
+ # qualifier type
70
+ "Qualifier",
71
+ # metadata keys (exported for advanced use/testing)
72
+ "COMPONENT_FLAG", "COMPONENT_KEY", "COMPONENT_LAZY",
73
+ "FACTORY_FLAG", "PROVIDES_KEY", "PROVIDES_LAZY",
74
+ "PLUGIN_FLAG", "QUALIFIERS_KEY",
75
+ ]
76
+
pico_ioc/plugins.py ADDED
@@ -0,0 +1,12 @@
1
+ # pico_ioc/plugins.py
2
+ from typing import Protocol, Any
3
+ from .container import Binder, PicoContainer
4
+
5
+ class PicoPlugin(Protocol):
6
+ def before_scan(self, package: Any, binder: Binder) -> None: ...
7
+ def visit_class(self, module: Any, cls: type, binder: Binder) -> None: ...
8
+ def after_scan(self, package: Any, binder: Binder) -> None: ...
9
+ def after_bind(self, container: PicoContainer, binder: Binder) -> None: ...
10
+ def before_eager(self, container: PicoContainer, binder: Binder) -> None: ...
11
+ def after_ready(self, container: PicoContainer, binder: Binder) -> None: ...
12
+
pico_ioc/proxy.py ADDED
@@ -0,0 +1,77 @@
1
+ # pico_ioc/proxy.py
2
+
3
+ from typing import Any, Callable
4
+
5
+ class ComponentProxy:
6
+ def __init__(self, object_creator: Callable[[], Any]):
7
+ object.__setattr__(self, "_object_creator", object_creator)
8
+ object.__setattr__(self, "__real_object", None)
9
+
10
+ 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
16
+
17
+ @property
18
+ def __class__(self):
19
+ return self._get_real_object().__class__
20
+
21
+ def __getattr__(self, name): return getattr(self._get_real_object(), name)
22
+ def __setattr__(self, name, value): setattr(self._get_real_object(), name, value)
23
+ def __delattr__(self, name): delattr(self._get_real_object(), name)
24
+ def __str__(self): return str(self._get_real_object())
25
+ def __repr__(self): return repr(self._get_real_object())
26
+ def __dir__(self): return dir(self._get_real_object())
27
+ def __len__(self): return len(self._get_real_object())
28
+ def __getitem__(self, key): return self._get_real_object()[key]
29
+ def __setitem__(self, key, value): self._get_real_object()[key] = value
30
+ def __delitem__(self, key): del self._get_real_object()[key]
31
+ def __iter__(self): return iter(self._get_real_object())
32
+ def __reversed__(self): return reversed(self._get_real_object())
33
+ def __contains__(self, item): return item in self._get_real_object()
34
+ def __add__(self, other): return self._get_real_object() + other
35
+ def __sub__(self, other): return self._get_real_object() - other
36
+ def __mul__(self, other): return self._get_real_object() * other
37
+ def __matmul__(self, other): return self._get_real_object() @ other
38
+ def __truediv__(self, other): return self._get_real_object() / other
39
+ def __floordiv__(self, other): return self._get_real_object() // other
40
+ def __mod__(self, other): return self._get_real_object() % other
41
+ def __divmod__(self, other): return divmod(self._get_real_object(), other)
42
+ def __pow__(self, other, modulo=None): return pow(self._get_real_object(), other, modulo)
43
+ def __lshift__(self, other): return self._get_real_object() << other
44
+ def __rshift__(self, other): return self._get_real_object() >> other
45
+ def __and__(self, other): return self._get_real_object() & other
46
+ def __xor__(self, other): return self._get_real_object() ^ other
47
+ def __or__(self, other): return self._get_real_object() | other
48
+ def __radd__(self, other): return other + self._get_real_object()
49
+ def __rsub__(self, other): return other - self._get_real_object()
50
+ def __rmul__(self, other): return other * self._get_real_object()
51
+ def __rmatmul__(self, other): return other @ self._get_real_object()
52
+ def __rtruediv__(self, other): return other / self._get_real_object()
53
+ def __rfloordiv__(self, other): return other // self._get_real_object()
54
+ def __rmod__(self, other): return other % self._get_real_object()
55
+ def __rdivmod__(self, other): return divmod(other, self._get_real_object())
56
+ def __rpow__(self, other): return pow(other, self._get_real_object())
57
+ def __rlshift__(self, other): return other << self._get_real_object()
58
+ def __rrshift__(self, other): return other >> self._get_real_object()
59
+ def __rand__(self, other): return other & self._get_real_object()
60
+ def __rxor__(self, other): return other ^ self._get_real_object()
61
+ def __ror__(self, other): return other | self._get_real_object()
62
+ def __neg__(self): return -self._get_real_object()
63
+ def __pos__(self): return +self._get_real_object()
64
+ def __abs__(self): return abs(self._get_real_object())
65
+ def __invert__(self): return ~self._get_real_object()
66
+ def __eq__(self, other): return self._get_real_object() == other
67
+ def __ne__(self, other): return self._get_real_object() != other
68
+ def __lt__(self, other): return self._get_real_object() < other
69
+ def __le__(self, other): return self._get_real_object() <= other
70
+ def __gt__(self, other): return self._get_real_object() > other
71
+ def __ge__(self, other): return self._get_real_object() >= other
72
+ def __hash__(self): return hash(self._get_real_object())
73
+ def __bool__(self): return bool(self._get_real_object())
74
+ def __call__(self, *args, **kwargs): return self._get_real_object()(*args, **kwargs)
75
+ def __enter__(self): return self._get_real_object().__enter__()
76
+ def __exit__(self, exc_type, exc_val, exc_tb): return self._get_real_object().__exit__(exc_type, exc_val, exc_tb)
77
+
pico_ioc/public_api.py ADDED
@@ -0,0 +1,76 @@
1
+ # pico_ioc/public_api.py
2
+
3
+ from __future__ import annotations
4
+ import importlib
5
+ import inspect
6
+ import pkgutil
7
+ import sys
8
+ from types import ModuleType
9
+ from typing import Dict, Iterable, Optional, Tuple
10
+
11
+ from .decorators import COMPONENT_FLAG, FACTORY_FLAG, PLUGIN_FLAG
12
+
13
+
14
+ def export_public_symbols_decorated(
15
+ *packages: str,
16
+ include_also: Optional[Iterable[str]] = None,
17
+ include_plugins: bool = True,
18
+ ):
19
+ index: Dict[str, Tuple[str, str]] = {}
20
+
21
+ def _collect(m: ModuleType):
22
+ names = getattr(m, "__all__", None)
23
+ if isinstance(names, (list, tuple, set)):
24
+ for n in names:
25
+ if hasattr(m, n):
26
+ index.setdefault(n, (m.__name__, n))
27
+ return
28
+
29
+ for n, obj in m.__dict__.items():
30
+ if not inspect.isclass(obj):
31
+ continue
32
+ is_component = getattr(obj, COMPONENT_FLAG, False)
33
+ is_factory = getattr(obj, FACTORY_FLAG, False)
34
+ is_plugin = include_plugins and getattr(obj, PLUGIN_FLAG, False)
35
+ if is_component or is_factory or is_plugin:
36
+ index.setdefault(n, (m.__name__, n))
37
+
38
+ for pkg_name in packages:
39
+ try:
40
+ base = importlib.import_module(pkg_name)
41
+ except Exception:
42
+ continue
43
+ if hasattr(base, "__path__"):
44
+ prefix = base.__name__ + "."
45
+ for _, modname, _ in pkgutil.walk_packages(base.__path__, prefix):
46
+ try:
47
+ m = importlib.import_module(modname)
48
+ except Exception:
49
+ continue
50
+ _collect(m)
51
+ else:
52
+ _collect(base)
53
+
54
+ for qual in tuple(include_also or ()):
55
+ modname, _, attr = qual.partition(":")
56
+ if modname and attr:
57
+ try:
58
+ m = importlib.import_module(modname)
59
+ if hasattr(m, attr):
60
+ index.setdefault(attr, (m.__name__, attr))
61
+ except Exception:
62
+ pass
63
+
64
+ def __getattr__(name: str):
65
+ try:
66
+ modname, attr = index[name]
67
+ except KeyError as e:
68
+ raise AttributeError(f"module has no attribute {name!r}") from e
69
+ mod = sys.modules.get(modname) or importlib.import_module(modname)
70
+ return getattr(mod, attr)
71
+
72
+ def __dir__():
73
+ return sorted(index.keys())
74
+
75
+ return __getattr__, __dir__
76
+
pico_ioc/resolver.py ADDED
@@ -0,0 +1,110 @@
1
+ # pico_ioc/resolver.py (Python 3.10+)
2
+
3
+ from __future__ import annotations
4
+ import inspect
5
+ from typing import Any, Annotated, get_args, get_origin, get_type_hints
6
+
7
+
8
+ def _get_hints(obj, owner_cls=None) -> dict:
9
+ """type hints with include_extras=True and correct globals/locals."""
10
+ mod = inspect.getmodule(obj)
11
+ g = getattr(mod, "__dict__", {})
12
+ l = vars(owner_cls) if owner_cls is not None else None
13
+ return get_type_hints(obj, globalns=g, localns=l, include_extras=True)
14
+
15
+
16
+ def _is_collection_hint(tp) -> bool:
17
+ """True if tp is a list[...] or tuple[...]."""
18
+ origin = get_origin(tp) or tp
19
+ return origin in (list, tuple)
20
+
21
+
22
+ def _base_and_qualifiers_from_hint(tp):
23
+ """
24
+ Extract (base, qualifiers, container_kind) from a collection hint.
25
+ Supports list[T] / tuple[T] and Annotated[T, "qual1", ...].
26
+ """
27
+ origin = get_origin(tp) or tp
28
+ args = get_args(tp) or ()
29
+ container_kind = list if origin is list else tuple
30
+
31
+ if not args:
32
+ return (object, (), container_kind)
33
+
34
+ inner = args[0]
35
+ if get_origin(inner) is Annotated:
36
+ base, *extras = get_args(inner)
37
+ quals = tuple(a for a in extras if isinstance(a, str))
38
+ return (base, quals, container_kind)
39
+
40
+ return (inner, (), container_kind)
41
+
42
+
43
+ class Resolver:
44
+ def __init__(self, container, *, prefer_name_first: bool = True):
45
+ self.c = container
46
+ self._prefer_name_first = bool(prefer_name_first)
47
+
48
+ def create_instance(self, cls):
49
+ sig = inspect.signature(cls.__init__)
50
+ hints = _get_hints(cls.__init__, owner_cls=cls)
51
+ kwargs = {}
52
+ for name, param in sig.parameters.items():
53
+ if name == "self":
54
+ continue
55
+ if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
56
+ continue
57
+ ann = hints.get(name, param.annotation)
58
+ try:
59
+ value = self._resolve_param(name, ann)
60
+ except NameError:
61
+ if param.default is not inspect._empty:
62
+ continue
63
+ raise
64
+ kwargs[name] = value
65
+ return cls(**kwargs)
66
+
67
+ def kwargs_for_callable(self, fn, *, owner_cls=None):
68
+ sig = inspect.signature(fn)
69
+ hints = _get_hints(fn, owner_cls=owner_cls)
70
+ kwargs = {}
71
+ for name, param in sig.parameters.items():
72
+ if name == "self":
73
+ continue
74
+ if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
75
+ continue
76
+ ann = hints.get(name, param.annotation)
77
+ try:
78
+ value = self._resolve_param(name, ann)
79
+ except NameError:
80
+ if param.default is not inspect._empty:
81
+ continue
82
+ raise
83
+ kwargs[name] = value
84
+ return kwargs
85
+
86
+ def _resolve_param(self, name: str, ann: Any):
87
+ # collections (list/tuple) with optional qualifiers via Annotated
88
+ if _is_collection_hint(ann):
89
+ base, quals, container_kind = _base_and_qualifiers_from_hint(ann)
90
+ items = self.c._resolve_all_for_base(base, qualifiers=quals)
91
+ return list(items) if container_kind is list else tuple(items)
92
+
93
+ # precedence: by name > by exact annotation > by MRO > by name again
94
+ if self._prefer_name_first and self.c.has(name):
95
+ return self.c.get(name)
96
+
97
+ if ann is not inspect._empty and self.c.has(ann):
98
+ return self.c.get(ann)
99
+
100
+ if ann is not inspect._empty and isinstance(ann, type):
101
+ for base in ann.__mro__[1:]:
102
+ if self.c.has(base):
103
+ return self.c.get(base)
104
+
105
+ if self.c.has(name):
106
+ return self.c.get(name)
107
+
108
+ missing = ann if ann is not inspect._empty else name
109
+ raise NameError(f"No provider found for key {missing!r}")
110
+