dinkleberg 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.
- dinkleberg/__init__.py +5 -0
- dinkleberg/dependency.py +2 -0
- dinkleberg/dependency_configurator.py +286 -0
- dinkleberg/dependency_scope.py +15 -0
- dinkleberg/descriptor.py +9 -0
- dinkleberg/typing.py +26 -0
- dinkleberg-0.1.0.dist-info/METADATA +34 -0
- dinkleberg-0.1.0.dist-info/RECORD +9 -0
- dinkleberg-0.1.0.dist-info/WHEEL +4 -0
dinkleberg/__init__.py
ADDED
dinkleberg/dependency.py
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import inspect
|
|
3
|
+
import logging
|
|
4
|
+
from inspect import Signature
|
|
5
|
+
from types import MappingProxyType
|
|
6
|
+
from typing import AsyncGenerator, Callable, overload, get_type_hints, Mapping, get_origin
|
|
7
|
+
|
|
8
|
+
from .dependency import Dependency
|
|
9
|
+
from .dependency_scope import DependencyScope
|
|
10
|
+
from .descriptor import Descriptor, Lifetime
|
|
11
|
+
from .typing import get_static_params, get_public_methods
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# noinspection PyShadowingBuiltins
|
|
17
|
+
class DependencyConfigurator(DependencyScope):
|
|
18
|
+
def __init__(self, parent: 'DependencyConfigurator' = None) -> None:
|
|
19
|
+
super().__init__()
|
|
20
|
+
self._parent = parent
|
|
21
|
+
self._descriptors: dict[type, Descriptor] = {}
|
|
22
|
+
self._singleton_instances = {}
|
|
23
|
+
self._scoped_instances = {}
|
|
24
|
+
self._active_generators = []
|
|
25
|
+
self._scopes = []
|
|
26
|
+
self._closed = False
|
|
27
|
+
|
|
28
|
+
async def close(self) -> None:
|
|
29
|
+
if self._closed:
|
|
30
|
+
return
|
|
31
|
+
self._closed = True
|
|
32
|
+
|
|
33
|
+
exceptions = []
|
|
34
|
+
|
|
35
|
+
# TODO close generators in reverse order of creation (LIFO)
|
|
36
|
+
for generator in self._active_generators:
|
|
37
|
+
try:
|
|
38
|
+
await generator.__anext__()
|
|
39
|
+
raise RuntimeError('Generator did not stop after yielding a single value.')
|
|
40
|
+
except StopAsyncIteration:
|
|
41
|
+
pass
|
|
42
|
+
except Exception as e:
|
|
43
|
+
exceptions.append(e)
|
|
44
|
+
|
|
45
|
+
for scope in self._scopes:
|
|
46
|
+
try:
|
|
47
|
+
await scope.close()
|
|
48
|
+
except Exception as e:
|
|
49
|
+
exceptions.append(e)
|
|
50
|
+
|
|
51
|
+
self._singleton_instances.clear()
|
|
52
|
+
self._active_generators.clear()
|
|
53
|
+
self._scoped_instances.clear()
|
|
54
|
+
self._descriptors.clear()
|
|
55
|
+
self._scopes.clear()
|
|
56
|
+
|
|
57
|
+
if exceptions:
|
|
58
|
+
raise ExceptionGroup('Errors occurred during closing DependencyConfigurator', exceptions)
|
|
59
|
+
|
|
60
|
+
def _add(self, lifetime: Lifetime, *, t: type = None, generator: Callable[..., AsyncGenerator] = None,
|
|
61
|
+
callable: Callable = None):
|
|
62
|
+
if generator is None and callable is None:
|
|
63
|
+
raise ValueError('Invalid dependency configuration.')
|
|
64
|
+
if t is None:
|
|
65
|
+
t = self._infer_type(generator=generator, callable=callable)
|
|
66
|
+
self._descriptors[t] = Descriptor(generator=generator, callable=callable, lifetime=lifetime)
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def _infer_type(*, generator: Callable[..., AsyncGenerator], callable: Callable) -> type:
|
|
70
|
+
# noinspection PyBroadException
|
|
71
|
+
try:
|
|
72
|
+
hints = get_type_hints(generator or callable)
|
|
73
|
+
return_hint = hints.get('return')
|
|
74
|
+
|
|
75
|
+
if not return_hint:
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
# This looks for the generic arguments of the return type
|
|
79
|
+
# e.g., if hint is AsyncGenerator[User, None], it extracts User
|
|
80
|
+
if hasattr(return_hint, '__args__'):
|
|
81
|
+
return return_hint.__args__[0]
|
|
82
|
+
return return_hint
|
|
83
|
+
except Exception:
|
|
84
|
+
pass
|
|
85
|
+
raise ValueError('Could not infer type from generator. Please provide the type explicitly.')
|
|
86
|
+
|
|
87
|
+
def _raise_if_closed(self):
|
|
88
|
+
if self._closed:
|
|
89
|
+
raise RuntimeError('DependencyConfigurator is already closed.')
|
|
90
|
+
|
|
91
|
+
def scope(self) -> 'DependencyConfigurator':
|
|
92
|
+
self._raise_if_closed()
|
|
93
|
+
scope = DependencyConfigurator(self)
|
|
94
|
+
scope._descriptors = self._descriptors.copy()
|
|
95
|
+
self._scopes.append(scope)
|
|
96
|
+
return scope
|
|
97
|
+
|
|
98
|
+
def _lookup_singleton(self, t: type):
|
|
99
|
+
if t in self._singleton_instances:
|
|
100
|
+
return self._singleton_instances[t]
|
|
101
|
+
if self._parent:
|
|
102
|
+
return self._parent._lookup_singleton(t)
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
# TODO circular dependency detection
|
|
106
|
+
async def resolve[T](self, t: type[T], **kwargs) -> T:
|
|
107
|
+
self._raise_if_closed()
|
|
108
|
+
|
|
109
|
+
if t == DependencyScope:
|
|
110
|
+
return self
|
|
111
|
+
|
|
112
|
+
singleton = self._lookup_singleton(t)
|
|
113
|
+
if singleton is not None:
|
|
114
|
+
return singleton
|
|
115
|
+
if t in self._scoped_instances:
|
|
116
|
+
return self._scoped_instances[t]
|
|
117
|
+
|
|
118
|
+
if t in self._descriptors:
|
|
119
|
+
descriptor = self._descriptors[t]
|
|
120
|
+
lifetime = descriptor['lifetime']
|
|
121
|
+
if lifetime == 'singleton' and self._parent:
|
|
122
|
+
# we need to resolve singleton from the root scope
|
|
123
|
+
return await self._parent.resolve(t, **kwargs)
|
|
124
|
+
|
|
125
|
+
is_generator = descriptor['generator'] is not None
|
|
126
|
+
factory = descriptor['generator'] or descriptor['callable']
|
|
127
|
+
deps = await self._resolve_deps(factory)
|
|
128
|
+
else:
|
|
129
|
+
origin = get_origin(t)
|
|
130
|
+
if origin is not None:
|
|
131
|
+
raise ValueError(f'Cannot resolve generic type {t} without explicit registration.')
|
|
132
|
+
|
|
133
|
+
is_generator = False
|
|
134
|
+
lifetime = 'transient'
|
|
135
|
+
factory = t
|
|
136
|
+
deps = await self._resolve_deps(t.__init__)
|
|
137
|
+
|
|
138
|
+
if is_generator:
|
|
139
|
+
generator = factory(**deps, **kwargs)
|
|
140
|
+
try:
|
|
141
|
+
instance = await generator.__anext__()
|
|
142
|
+
except StopAsyncIteration:
|
|
143
|
+
raise RuntimeError(f'Generator {t} did not yield any value.')
|
|
144
|
+
|
|
145
|
+
self._active_generators.append(generator)
|
|
146
|
+
else:
|
|
147
|
+
instance = factory(**deps, **kwargs)
|
|
148
|
+
|
|
149
|
+
self._wrap_instance(instance)
|
|
150
|
+
|
|
151
|
+
if lifetime == 'singleton':
|
|
152
|
+
self._singleton_instances[t] = instance
|
|
153
|
+
elif lifetime == 'scoped':
|
|
154
|
+
self._scoped_instances[t] = instance
|
|
155
|
+
|
|
156
|
+
return instance
|
|
157
|
+
|
|
158
|
+
async def _resolve_deps(self, func: Callable) -> dict:
|
|
159
|
+
params = get_static_params(func)
|
|
160
|
+
tasks = []
|
|
161
|
+
names = []
|
|
162
|
+
|
|
163
|
+
for param in params:
|
|
164
|
+
if not param.annotation or param.annotation is inspect.Parameter.empty:
|
|
165
|
+
continue
|
|
166
|
+
|
|
167
|
+
# TODO handle more complex cases (e.g., Union, Optional, etc.)
|
|
168
|
+
# TODO handle native types (int, str, etc.)
|
|
169
|
+
names.append(param.name)
|
|
170
|
+
tasks.append(self.resolve(param.annotation))
|
|
171
|
+
|
|
172
|
+
results = await asyncio.gather(*tasks)
|
|
173
|
+
return dict(zip(names, results))
|
|
174
|
+
|
|
175
|
+
async def _resolve_kwargs(self, signature: Signature, name: str, args: tuple, kwargs: dict,
|
|
176
|
+
dep_params: Mapping[str, inspect.Parameter]) -> dict:
|
|
177
|
+
bound_args = signature.bind_partial(*args, **kwargs)
|
|
178
|
+
actual_kwargs = kwargs.copy()
|
|
179
|
+
|
|
180
|
+
params_to_resolve = []
|
|
181
|
+
for p_name, p_obj in dep_params.items():
|
|
182
|
+
if p_name not in bound_args.arguments:
|
|
183
|
+
if p_obj.annotation is inspect.Parameter.empty:
|
|
184
|
+
raise TypeError(f'Parameter "{p_name}" in {name} is marked as a Dependency but lacks a type hint.')
|
|
185
|
+
params_to_resolve.append((p_name, p_obj.annotation))
|
|
186
|
+
|
|
187
|
+
if not params_to_resolve:
|
|
188
|
+
return actual_kwargs
|
|
189
|
+
|
|
190
|
+
names, types = zip(*params_to_resolve)
|
|
191
|
+
resolved_values = await asyncio.gather(*(self.resolve(t, **kwargs) for t in types))
|
|
192
|
+
|
|
193
|
+
actual_kwargs.update(dict(zip(names, resolved_values)))
|
|
194
|
+
|
|
195
|
+
return actual_kwargs
|
|
196
|
+
|
|
197
|
+
# TODO handle __slots__
|
|
198
|
+
def _wrap_instance(self, instance):
|
|
199
|
+
if getattr(instance, '__di_wrapped__', False):
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
methods = get_public_methods(instance)
|
|
203
|
+
for name, value in methods:
|
|
204
|
+
signature = inspect.signature(value)
|
|
205
|
+
|
|
206
|
+
dep_params = MappingProxyType({
|
|
207
|
+
param_name: param
|
|
208
|
+
for param_name, param in signature.parameters.items()
|
|
209
|
+
if isinstance(param.default, Dependency)
|
|
210
|
+
})
|
|
211
|
+
if not dep_params:
|
|
212
|
+
continue
|
|
213
|
+
|
|
214
|
+
instance_method = getattr(instance, name)
|
|
215
|
+
if asyncio.iscoroutinefunction(instance_method):
|
|
216
|
+
async def wrapped_method(*args, __m=instance_method, __s=signature, __n=name, __d=dep_params, **kwargs):
|
|
217
|
+
new_kwargs = await self._resolve_kwargs(__s, __n, args, kwargs, __d)
|
|
218
|
+
return await __m(*args, **new_kwargs)
|
|
219
|
+
|
|
220
|
+
setattr(instance, name, wrapped_method)
|
|
221
|
+
else:
|
|
222
|
+
raise NotImplementedError('Synchronous methods with Dependency() defaults are not supported.')
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
setattr(instance, '__di_wrapped__', True)
|
|
226
|
+
except (AttributeError, TypeError):
|
|
227
|
+
# Some objects (like those with __slots__) might not allow new attributes
|
|
228
|
+
pass
|
|
229
|
+
|
|
230
|
+
@overload
|
|
231
|
+
def add_singleton[I](self, *, instance: I):
|
|
232
|
+
...
|
|
233
|
+
|
|
234
|
+
@overload
|
|
235
|
+
def add_singleton[T, I](self, *, t: type[T], instance: I):
|
|
236
|
+
...
|
|
237
|
+
|
|
238
|
+
@overload
|
|
239
|
+
def add_singleton[I](self, *, callable: Callable[..., I]):
|
|
240
|
+
...
|
|
241
|
+
|
|
242
|
+
@overload
|
|
243
|
+
def add_singleton[T, I](self, *, t: type[T], callable: Callable[..., I]):
|
|
244
|
+
...
|
|
245
|
+
|
|
246
|
+
@overload
|
|
247
|
+
def add_singleton[I](self, *, generator: Callable[..., AsyncGenerator[I]]):
|
|
248
|
+
...
|
|
249
|
+
|
|
250
|
+
@overload
|
|
251
|
+
def add_singleton[T, I](self, *, t: type[T], generator: Callable[..., AsyncGenerator[I]]):
|
|
252
|
+
...
|
|
253
|
+
|
|
254
|
+
def add_singleton[T, I](self, *, t: type[T] = None, generator: Callable[..., AsyncGenerator[I]] = None,
|
|
255
|
+
callable: Callable[..., I] = None, instance: I = None):
|
|
256
|
+
self._raise_if_closed()
|
|
257
|
+
if instance is None:
|
|
258
|
+
self._add('singleton', t=t, generator=generator, callable=callable)
|
|
259
|
+
return
|
|
260
|
+
elif t is None:
|
|
261
|
+
t = type(instance)
|
|
262
|
+
|
|
263
|
+
self._wrap_instance(instance)
|
|
264
|
+
|
|
265
|
+
self._singleton_instances[t] = instance
|
|
266
|
+
|
|
267
|
+
@overload
|
|
268
|
+
def add_scoped[I](self, *, callable: Callable[..., I]):
|
|
269
|
+
...
|
|
270
|
+
|
|
271
|
+
@overload
|
|
272
|
+
def add_scoped[T, I](self, *, t: type[T], callable: Callable[..., I]):
|
|
273
|
+
...
|
|
274
|
+
|
|
275
|
+
@overload
|
|
276
|
+
def add_scoped[I](self, *, generator: Callable[..., AsyncGenerator[I]]):
|
|
277
|
+
...
|
|
278
|
+
|
|
279
|
+
@overload
|
|
280
|
+
def add_scoped[T, I](self, *, t: type[T], generator: Callable[..., AsyncGenerator[I]]):
|
|
281
|
+
...
|
|
282
|
+
|
|
283
|
+
def add_scoped[T, I](self, *, t: type[T] = None, generator: Callable[..., AsyncGenerator[I]] = None,
|
|
284
|
+
callable: Callable[..., I] = None):
|
|
285
|
+
self._raise_if_closed()
|
|
286
|
+
self._add('scoped', t=t, generator=generator, callable=callable)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class DependencyScope(ABC):
|
|
5
|
+
@abstractmethod
|
|
6
|
+
async def resolve[T](self, t: type[T], **kwargs) -> T:
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
@abstractmethod
|
|
10
|
+
async def close(self) -> None:
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
@abstractmethod
|
|
14
|
+
def scope(self) -> 'DependencyScope':
|
|
15
|
+
pass
|
dinkleberg/descriptor.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from typing import Optional, TypedDict, Callable, AsyncGenerator, Literal, Union
|
|
2
|
+
|
|
3
|
+
Lifetime = Union[Literal['singleton'], Literal['scoped'], Literal['transient']]
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Descriptor(TypedDict):
|
|
7
|
+
generator: Optional[Callable[..., AsyncGenerator]]
|
|
8
|
+
callable: Optional[Callable]
|
|
9
|
+
lifetime: Lifetime
|
dinkleberg/typing.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from inspect import Parameter, signature
|
|
3
|
+
from typing import Callable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_static_params(func: Callable) -> list[Parameter]:
|
|
7
|
+
sig = signature(func)
|
|
8
|
+
|
|
9
|
+
params = list(sig.parameters.values())
|
|
10
|
+
|
|
11
|
+
if params and params[0].name in ('self', 'cls'):
|
|
12
|
+
params = params[1:]
|
|
13
|
+
|
|
14
|
+
return params
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_public_methods(obj: object) -> list[tuple[str, Callable]]:
|
|
18
|
+
methods = []
|
|
19
|
+
|
|
20
|
+
for name in dir(obj.__class__):
|
|
21
|
+
attr = getattr(obj.__class__, name)
|
|
22
|
+
|
|
23
|
+
if inspect.isroutine(attr) and not isinstance(attr, property) and not name.startswith('_'):
|
|
24
|
+
methods.append((name, attr))
|
|
25
|
+
|
|
26
|
+
return methods
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dinkleberg
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Your friendly neighbour when it comes to dependency management.
|
|
5
|
+
Project-URL: Homepage, https://github.com/DavidVollmers/dinkleberg
|
|
6
|
+
Project-URL: Documentation, https://github.com/DavidVollmers/dinkleberg/blob/main/libs/dinkleberg/README.md
|
|
7
|
+
Project-URL: Repository, https://github.com/DavidVollmers/dinkleberg.git
|
|
8
|
+
Project-URL: Issues, https://github.com/DavidVollmers/dinkleberg/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/DavidVollmers/dinkleberg/blob/main/CHANGELOG.md
|
|
10
|
+
Keywords: automation,dependency,package-management
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Python: >=3.13
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# dinkleberg
|
|
22
|
+
|
|
23
|
+
> "And this is where I'd put my working dependencies... IF I HAD ANY!"
|
|
24
|
+
|
|
25
|
+
**dinkleberg** is a lightweight Python utility designed to make dependency management less of a neighborhood feud. Built
|
|
26
|
+
to work seamlessly in any project, it ensures your environment stays green—unlike the guy's next door.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install dinkleberg
|
|
32
|
+
|
|
33
|
+
uv add dinkleberg
|
|
34
|
+
```
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
dinkleberg/__init__.py,sha256=HGu8mjYt-Fa0y9_aMtJZFYo3s9Yb0q5dmbMKtHrAV_k,217
|
|
2
|
+
dinkleberg/dependency.py,sha256=YcGvuvhoBRxShgSRaznKyZg49GCgOWy_L8HD0sXAMYo,29
|
|
3
|
+
dinkleberg/dependency_configurator.py,sha256=IeGSUUbaaxbcb1I6NcIqWyqGs0iWygVzjXzsj0VmOGg,10473
|
|
4
|
+
dinkleberg/dependency_scope.py,sha256=sufXjQ6F62ZfohItUnrV7Fvh8gSSc7a6orO6PXu19Vw,318
|
|
5
|
+
dinkleberg/descriptor.py,sha256=mVI00K1M2m4Rl5ANatGMeB_HpzeFONoULwg0UmNxfQ0,313
|
|
6
|
+
dinkleberg/typing.py,sha256=9AGDNz_aQ8p7ToyJwkj2fg92o0L3-zXwZBEmoTNS1to,670
|
|
7
|
+
dinkleberg-0.1.0.dist-info/METADATA,sha256=vU4SQ-0fafwgtOETUAtr_-zuPTEJX4Haesa4i8OT8L8,1396
|
|
8
|
+
dinkleberg-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
9
|
+
dinkleberg-0.1.0.dist-info/RECORD,,
|