pico-ioc 0.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.
- pico_ioc/__init__.py +213 -0
- pico_ioc/_version.py +1 -0
- pico_ioc-0.1.0.dist-info/METADATA +22 -0
- pico_ioc-0.1.0.dist-info/RECORD +6 -0
- pico_ioc-0.1.0.dist-info/WHEEL +5 -0
- pico_ioc-0.1.0.dist-info/top_level.txt +1 -0
pico_ioc/__init__.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# src/pico_ioc/__init__.py
|
|
2
|
+
|
|
3
|
+
import functools, inspect, pkgutil, importlib, logging, sys
|
|
4
|
+
from typing import Callable, Any, Iterator, AsyncIterator
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
# written at build time by setuptools-scm
|
|
8
|
+
from ._version import __version__
|
|
9
|
+
except Exception: # pragma: no cover
|
|
10
|
+
__version__ = "0.0.0"
|
|
11
|
+
|
|
12
|
+
__all__ = ["__version__"]
|
|
13
|
+
|
|
14
|
+
# ==============================================================================
|
|
15
|
+
# --- 1. Container and Chameleon Proxy (Framework-Agnostic) ---
|
|
16
|
+
# ==============================================================================
|
|
17
|
+
class PicoContainer:
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self._providers = {}
|
|
20
|
+
self._singletons = {}
|
|
21
|
+
|
|
22
|
+
def bind(self, key: Any, provider: Callable[[], Any]):
|
|
23
|
+
self._providers[key] = provider
|
|
24
|
+
|
|
25
|
+
def get(self, key: Any) -> Any:
|
|
26
|
+
if key in self._singletons:
|
|
27
|
+
return self._singletons[key]
|
|
28
|
+
if key in self._providers:
|
|
29
|
+
instance = self._providers[key]()
|
|
30
|
+
self._singletons[key] = instance
|
|
31
|
+
return instance
|
|
32
|
+
raise NameError(f"No provider found for key: {key}")
|
|
33
|
+
|
|
34
|
+
class LazyProxy:
|
|
35
|
+
"""
|
|
36
|
+
A full-fledged lazy proxy that delegates almost all operations
|
|
37
|
+
to the real object, which is created only on first access.
|
|
38
|
+
It is completely framework-agnostic.
|
|
39
|
+
"""
|
|
40
|
+
def __init__(self, object_creator: Callable[[], Any]):
|
|
41
|
+
object.__setattr__(self, "_object_creator", object_creator)
|
|
42
|
+
object.__setattr__(self, "__real_object", None)
|
|
43
|
+
|
|
44
|
+
def _get_real_object(self) -> Any:
|
|
45
|
+
real_obj = object.__getattribute__(self, "__real_object")
|
|
46
|
+
if real_obj is None:
|
|
47
|
+
real_obj = object.__getattribute__(self, "_object_creator")()
|
|
48
|
+
object.__setattr__(self, "__real_object", real_obj)
|
|
49
|
+
return real_obj
|
|
50
|
+
|
|
51
|
+
# --- Core Proxying and Representation ---
|
|
52
|
+
@property
|
|
53
|
+
def __class__(self):
|
|
54
|
+
return self._get_real_object().__class__
|
|
55
|
+
|
|
56
|
+
def __getattr__(self, name):
|
|
57
|
+
return getattr(self._get_real_object(), name)
|
|
58
|
+
|
|
59
|
+
def __setattr__(self, name, value):
|
|
60
|
+
setattr(self._get_real_object(), name, value)
|
|
61
|
+
|
|
62
|
+
def __delattr__(self, name):
|
|
63
|
+
delattr(self._get_real_object(), name)
|
|
64
|
+
|
|
65
|
+
def __str__(self):
|
|
66
|
+
return str(self._get_real_object())
|
|
67
|
+
|
|
68
|
+
def __repr__(self):
|
|
69
|
+
return repr(self._get_real_object())
|
|
70
|
+
|
|
71
|
+
def __dir__(self):
|
|
72
|
+
return dir(self._get_real_object())
|
|
73
|
+
|
|
74
|
+
# --- Emulation of container types ---
|
|
75
|
+
def __len__(self): return len(self._get_real_object())
|
|
76
|
+
def __getitem__(self, key): return self._get_real_object()[key]
|
|
77
|
+
def __setitem__(self, key, value): self._get_real_object()[key] = value
|
|
78
|
+
def __delitem__(self, key): del self._get_real_object()[key]
|
|
79
|
+
def __iter__(self): return iter(self._get_real_object())
|
|
80
|
+
def __reversed__(self): return reversed(self._get_real_object())
|
|
81
|
+
def __contains__(self, item): return item in self._get_real_object()
|
|
82
|
+
|
|
83
|
+
# --- Emulation of numeric types and operators ---
|
|
84
|
+
def __add__(self, other): return self._get_real_object() + other
|
|
85
|
+
def __sub__(self, other): return self._get_real_object() - other
|
|
86
|
+
def __mul__(self, other): return self._get_real_object() * other
|
|
87
|
+
def __matmul__(self, other): return self._get_real_object() @ other
|
|
88
|
+
def __truediv__(self, other): return self._get_real_object() / other
|
|
89
|
+
def __floordiv__(self, other): return self._get_real_object() // other
|
|
90
|
+
def __mod__(self, other): return self._get_real_object() % other
|
|
91
|
+
def __divmod__(self, other): return divmod(self._get_real_object(), other)
|
|
92
|
+
def __pow__(self, other, modulo=None): return pow(self._get_real_object(), other, modulo)
|
|
93
|
+
def __lshift__(self, other): return self._get_real_object() << other
|
|
94
|
+
def __rshift__(self, other): return self._get_real_object() >> other
|
|
95
|
+
def __and__(self, other): return self._get_real_object() & other
|
|
96
|
+
def __xor__(self, other): return self._get_real_object() ^ other
|
|
97
|
+
def __or__(self, other): return self._get_real_object() | other
|
|
98
|
+
|
|
99
|
+
# --- Right-hand side numeric operators ---
|
|
100
|
+
def __radd__(self, other): return other + self._get_real_object()
|
|
101
|
+
def __rsub__(self, other): return other - self._get_real_object()
|
|
102
|
+
def __rmul__(self, other): return other * self._get_real_object()
|
|
103
|
+
def __rmatmul__(self, other): return other @ self._get_real_object()
|
|
104
|
+
def __rtruediv__(self, other): return other / self._get_real_object()
|
|
105
|
+
def __rfloordiv__(self, other): return other // self._get_real_object()
|
|
106
|
+
def __rmod__(self, other): return other % self._get_real_object()
|
|
107
|
+
def __rdivmod__(self, other): return divmod(other, self._get_real_object())
|
|
108
|
+
def __rpow__(self, other): return pow(other, self._get_real_object())
|
|
109
|
+
def __rlshift__(self, other): return other << self._get_real_object()
|
|
110
|
+
def __rrshift__(self, other): return other >> self._get_real_object()
|
|
111
|
+
def __rand__(self, other): return other & self._get_real_object()
|
|
112
|
+
def __rxor__(self, other): return other ^ self._get_real_object()
|
|
113
|
+
def __ror__(self, other): return other | self._get_real_object()
|
|
114
|
+
|
|
115
|
+
# --- Unary operators ---
|
|
116
|
+
def __neg__(self): return -self._get_real_object()
|
|
117
|
+
def __pos__(self): return +self._get_real_object()
|
|
118
|
+
def __abs__(self): return abs(self._get_real_object())
|
|
119
|
+
def __invert__(self): return ~self._get_real_object()
|
|
120
|
+
|
|
121
|
+
# --- Comparison operators ---
|
|
122
|
+
def __eq__(self, other): return self._get_real_object() == other
|
|
123
|
+
def __ne__(self, other): return self._get_real_object() != other
|
|
124
|
+
def __lt__(self, other): return self._get_real_object() < other
|
|
125
|
+
def __le__(self, other): return self._get_real_object() <= other
|
|
126
|
+
def __gt__(self, other): return self._get_real_object() > other
|
|
127
|
+
def __ge__(self, other): return self._get_real_object() >= other
|
|
128
|
+
def __hash__(self): return hash(self._get_real_object())
|
|
129
|
+
|
|
130
|
+
# --- Truthiness, Callability and Context Management ---
|
|
131
|
+
def __bool__(self): return bool(self._get_real_object())
|
|
132
|
+
def __call__(self, *args, **kwargs): return self._get_real_object()(*args, **kwargs)
|
|
133
|
+
def __enter__(self): return self._get_real_object().__enter__()
|
|
134
|
+
def __exit__(self, exc_type, exc_val, exc_tb): return self._get_real_object().__exit__(exc_type, exc_val, exc_tb)
|
|
135
|
+
|
|
136
|
+
# ==============================================================================
|
|
137
|
+
# --- 2. The Scanner and `init` Facade ---
|
|
138
|
+
# ==============================================================================
|
|
139
|
+
def _scan_and_configure(package_or_name, container: PicoContainer):
|
|
140
|
+
package = importlib.import_module(package_or_name) if isinstance(package_or_name, str) else package_or_name
|
|
141
|
+
logging.info(f"🚀 Scanning in '{package.__name__}'...")
|
|
142
|
+
component_classes, factory_classes = [], []
|
|
143
|
+
for _, name, _ in pkgutil.walk_packages(package.__path__, package.__name__ + '.'):
|
|
144
|
+
try:
|
|
145
|
+
module = importlib.import_module(name)
|
|
146
|
+
for _, obj in inspect.getmembers(module, inspect.isclass):
|
|
147
|
+
if hasattr(obj, '_is_component'):
|
|
148
|
+
component_classes.append(obj)
|
|
149
|
+
elif hasattr(obj, '_is_factory_component'):
|
|
150
|
+
factory_classes.append(obj)
|
|
151
|
+
except Exception as e:
|
|
152
|
+
logging.warning(f" ⚠️ Module {name} not processed: {e}")
|
|
153
|
+
|
|
154
|
+
for factory_cls in factory_classes:
|
|
155
|
+
try:
|
|
156
|
+
sig = inspect.signature(factory_cls.__init__)
|
|
157
|
+
instance = factory_cls(container) if 'container' in sig.parameters else factory_cls()
|
|
158
|
+
for _, method in inspect.getmembers(instance, inspect.ismethod):
|
|
159
|
+
if hasattr(method, '_provides_name'):
|
|
160
|
+
container.bind(getattr(method, '_provides_name'), method)
|
|
161
|
+
except Exception as e:
|
|
162
|
+
logging.error(f" ❌ Error in factory {factory_cls.__name__}: {e}", exc_info=True)
|
|
163
|
+
|
|
164
|
+
for component_cls in component_classes:
|
|
165
|
+
key = getattr(component_cls, '_component_key', component_cls)
|
|
166
|
+
def create_component(cls=component_cls):
|
|
167
|
+
sig = inspect.signature(cls.__init__)
|
|
168
|
+
deps = {}
|
|
169
|
+
for p in sig.parameters.values():
|
|
170
|
+
if p.name == 'self' or p.kind in (
|
|
171
|
+
inspect.Parameter.VAR_POSITIONAL, # *args
|
|
172
|
+
inspect.Parameter.VAR_KEYWORD, # **kwargs
|
|
173
|
+
):
|
|
174
|
+
continue
|
|
175
|
+
dep_key = p.annotation if p.annotation is not inspect._empty else p.name
|
|
176
|
+
deps[p.name] = container.get(dep_key)
|
|
177
|
+
return cls(**deps)
|
|
178
|
+
container.bind(key, create_component)
|
|
179
|
+
|
|
180
|
+
_container = None
|
|
181
|
+
def init(root_package):
|
|
182
|
+
global _container
|
|
183
|
+
if _container:
|
|
184
|
+
return _container
|
|
185
|
+
_container = PicoContainer()
|
|
186
|
+
logging.info("🔌 Initializing 'pico-ioc'...")
|
|
187
|
+
_scan_and_configure(root_package, _container)
|
|
188
|
+
logging.info("✅ Container configured and ready.")
|
|
189
|
+
return _container
|
|
190
|
+
|
|
191
|
+
# ==============================================================================
|
|
192
|
+
# --- 3. The Decorators ---
|
|
193
|
+
# ==============================================================================
|
|
194
|
+
def factory_component(cls):
|
|
195
|
+
setattr(cls, '_is_factory_component', True)
|
|
196
|
+
return cls
|
|
197
|
+
|
|
198
|
+
def provides(name: str, lazy: bool = True):
|
|
199
|
+
def decorator(func):
|
|
200
|
+
@functools.wraps(func)
|
|
201
|
+
def wrapper(*args, **kwargs):
|
|
202
|
+
return LazyProxy(lambda: func(*args, **kwargs)) if lazy else func(*args, **kwargs)
|
|
203
|
+
setattr(wrapper, '_provides_name', name)
|
|
204
|
+
return wrapper
|
|
205
|
+
return decorator
|
|
206
|
+
|
|
207
|
+
def component(cls=None, *, name: str = None):
|
|
208
|
+
def decorator(cls):
|
|
209
|
+
setattr(cls, '_is_component', True)
|
|
210
|
+
setattr(cls, '_component_key', name if name is not None else cls)
|
|
211
|
+
return cls
|
|
212
|
+
return decorator(cls) if cls else decorator
|
|
213
|
+
|
pico_ioc/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '0.1.0'
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pico-ioc
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A minimalist, zero-dependency Inversion of Control (IoC) container for Python.
|
|
5
|
+
Author-email: David Perez Cabrera <dperezcabrera@gmail.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/dperezcabrera/pico-ioc
|
|
7
|
+
Project-URL: Repository, https://github.com/dperezcabrera/pico-ioc
|
|
8
|
+
Project-URL: Issue Tracker, https://github.com/dperezcabrera/pico-ioc/issues
|
|
9
|
+
Keywords: ioc,di,dependency injection,inversion of control,decorator
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
20
|
+
Classifier: Operating System :: OS Independent
|
|
21
|
+
Requires-Python: >=3.8
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
pico_ioc/__init__.py,sha256=SI-ee8rpiNU0f3uTndpTmBmZJ5RZG8MUSGOJXroJfKk,9710
|
|
2
|
+
pico_ioc/_version.py,sha256=IMjkMO3twhQzluVTo8Z6rE7Eg-9U79_LGKMcsWLKBkY,22
|
|
3
|
+
pico_ioc-0.1.0.dist-info/METADATA,sha256=VZHRU852qmS5KyavUYx2DJFjVRurY96bJy_I4qKNOsI,1090
|
|
4
|
+
pico_ioc-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
5
|
+
pico_ioc-0.1.0.dist-info/top_level.txt,sha256=_7_RLu616z_dtRw16impXn4Mw8IXe2J4BeX5912m5dQ,9
|
|
6
|
+
pico_ioc-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pico_ioc
|