pythonboot 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.
- pythonboot/__init__.py +41 -0
- pythonboot/aop/__init__.py +11 -0
- pythonboot/aop/decorators.py +75 -0
- pythonboot/aop/joinpoint.py +33 -0
- pythonboot/aop/pointcut.py +81 -0
- pythonboot/aop/proxy.py +196 -0
- pythonboot/beans/__init__.py +7 -0
- pythonboot/beans/conditions.py +108 -0
- pythonboot/beans/definitions.py +108 -0
- pythonboot/beans/lifecycle.py +82 -0
- pythonboot/beans/scanner.py +164 -0
- pythonboot/boot/__init__.py +5 -0
- pythonboot/boot/autoconfiguration.py +120 -0
- pythonboot/boot/task_configuration.py +38 -0
- pythonboot/config/__init__.py +6 -0
- pythonboot/config/logging.py +90 -0
- pythonboot/config/properties.py +148 -0
- pythonboot/context/__init__.py +6 -0
- pythonboot/context/container.py +427 -0
- pythonboot/context/injection.py +60 -0
- pythonboot/exceptions/__init__.py +40 -0
- pythonboot/task/__init__.py +7 -0
- pythonboot/task/executor.py +241 -0
- pythonboot/transaction/__init__.py +24 -0
- pythonboot/transaction/decorators.py +48 -0
- pythonboot/transaction/definition.py +90 -0
- pythonboot/transaction/exceptions.py +31 -0
- pythonboot/transaction/interceptor.py +71 -0
- pythonboot/transaction/manager.py +114 -0
- pythonboot/util/__init__.py +5 -0
- pythonboot/util/context_util.py +59 -0
- pythonboot-0.1.0.dist-info/METADATA +502 -0
- pythonboot-0.1.0.dist-info/RECORD +34 -0
- pythonboot-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
"""Application container, bean scopes, dependency resolution, and lifecycle."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import inspect
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from threading import RLock
|
|
7
|
+
from types import UnionType
|
|
8
|
+
from typing import Annotated, Any, Mapping, Union, get_args, get_origin, get_type_hints
|
|
9
|
+
|
|
10
|
+
from pythonboot.aop.proxy import AopEngine
|
|
11
|
+
from pythonboot.beans import BeanDefinition
|
|
12
|
+
from pythonboot.beans.lifecycle import get_ondestroy_methods, get_postconstruct_methods
|
|
13
|
+
from pythonboot.context.injection import Autowired, LazyDependency
|
|
14
|
+
from pythonboot.config.properties import use_configuration_values
|
|
15
|
+
from pythonboot.exceptions import (
|
|
16
|
+
BeanDefinitionError, CircularDependencyError, ContainerStateError,
|
|
17
|
+
DuplicateBeanError, MissingBeanError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(eq=False)
|
|
22
|
+
class _BeanState:
|
|
23
|
+
definition: BeanDefinition
|
|
24
|
+
instance: Any
|
|
25
|
+
dependencies: list["_BeanState"] = field(default_factory=list)
|
|
26
|
+
explicit_dependencies: list["_BeanState"] = field(default_factory=list)
|
|
27
|
+
complete: bool = False
|
|
28
|
+
initialized: bool = False
|
|
29
|
+
initializing: bool = False
|
|
30
|
+
failure: BaseException | None = None
|
|
31
|
+
init_methods: tuple[str, ...] = ()
|
|
32
|
+
destroy_methods: tuple[str, ...] = ()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ApplicationContext:
|
|
36
|
+
def __init__(
|
|
37
|
+
self, definitions: tuple[BeanDefinition, ...] = (), *, configuration: Mapping[str, Any] | None = None,
|
|
38
|
+
) -> None:
|
|
39
|
+
self._definitions: dict[str, BeanDefinition] = {}
|
|
40
|
+
self._singletons: dict[str, _BeanState] = {}
|
|
41
|
+
self._creation_order: list[_BeanState] = []
|
|
42
|
+
self._startup_states: list[_BeanState] = []
|
|
43
|
+
self._resolving: list[str] = []
|
|
44
|
+
self._failures: dict[str, BaseException] = {}
|
|
45
|
+
self._active = False
|
|
46
|
+
self._started = False
|
|
47
|
+
self._starting = False
|
|
48
|
+
self._lock = RLock()
|
|
49
|
+
self._async_init_lock = asyncio.Lock()
|
|
50
|
+
self._init_owner: asyncio.Task[Any] | None = None
|
|
51
|
+
self._configuration = configuration or {}
|
|
52
|
+
for definition in definitions:
|
|
53
|
+
if definition.name in self._definitions:
|
|
54
|
+
raise DuplicateBeanError(f"Duplicate bean name: {definition.name}")
|
|
55
|
+
self._definitions[definition.name] = definition
|
|
56
|
+
self._validate_depends_on()
|
|
57
|
+
self._aop = AopEngine(
|
|
58
|
+
tuple((item.name, item.bean_type) for item in self._definitions.values()),
|
|
59
|
+
lambda bean_type, name: self.get_bean(bean_type, name=name),
|
|
60
|
+
async_resolver=lambda bean_type, name: self.aget_bean(bean_type, name=name),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def _validate_depends_on(self) -> None:
|
|
64
|
+
visited: set[str] = set()
|
|
65
|
+
path: list[str] = []
|
|
66
|
+
|
|
67
|
+
def visit(name: str) -> None:
|
|
68
|
+
if name in path:
|
|
69
|
+
raise CircularDependencyError("Circular depends_on dependency: " + " -> ".join([*path, name]))
|
|
70
|
+
if name in visited:
|
|
71
|
+
return
|
|
72
|
+
definition = self._definitions.get(name)
|
|
73
|
+
if definition is None:
|
|
74
|
+
raise MissingBeanError(f"Bean specified in depends_on does not exist: {name}")
|
|
75
|
+
path.append(name)
|
|
76
|
+
for dependency in definition.depends_on:
|
|
77
|
+
visit(dependency)
|
|
78
|
+
path.pop()
|
|
79
|
+
visited.add(name)
|
|
80
|
+
|
|
81
|
+
for name in self._definitions:
|
|
82
|
+
visit(name)
|
|
83
|
+
|
|
84
|
+
def get_bean(
|
|
85
|
+
self, bean_type: type | str, *, name: str | None = None, qualifier: str | None = None,
|
|
86
|
+
) -> Any:
|
|
87
|
+
with self._lock:
|
|
88
|
+
self._check_active()
|
|
89
|
+
state = self._resolve(self._find_definition(bean_type, name=name, qualifier=qualifier))
|
|
90
|
+
if self._started or self._init_owner is not None:
|
|
91
|
+
self._initialize_sync(state, set())
|
|
92
|
+
return state.instance
|
|
93
|
+
|
|
94
|
+
async def aget_bean(
|
|
95
|
+
self, bean_type: type | str, *, name: str | None = None, qualifier: str | None = None,
|
|
96
|
+
) -> Any:
|
|
97
|
+
"""Get a bean and await dependency initialization; create prototypes per lookup."""
|
|
98
|
+
if self._init_owner is asyncio.current_task():
|
|
99
|
+
with self._lock:
|
|
100
|
+
self._check_active()
|
|
101
|
+
state = self._resolve(self._find_definition(bean_type, name=name, qualifier=qualifier))
|
|
102
|
+
await self._initialize_async(state, set())
|
|
103
|
+
return state.instance
|
|
104
|
+
async with self._async_init_lock:
|
|
105
|
+
self._init_owner = asyncio.current_task()
|
|
106
|
+
try:
|
|
107
|
+
with self._lock:
|
|
108
|
+
self._check_active()
|
|
109
|
+
state = self._resolve(self._find_definition(bean_type, name=name, qualifier=qualifier))
|
|
110
|
+
await self._initialize_async(state, set())
|
|
111
|
+
return state.instance
|
|
112
|
+
finally:
|
|
113
|
+
self._init_owner = None
|
|
114
|
+
|
|
115
|
+
def _check_active(self) -> None:
|
|
116
|
+
if not self._active:
|
|
117
|
+
raise ContainerStateError("The container is not active; obtain beans during application lifespan or requests")
|
|
118
|
+
|
|
119
|
+
def _find_definition(
|
|
120
|
+
self, bean_type: type | str, *, name: str | None = None, qualifier: str | None = None,
|
|
121
|
+
) -> BeanDefinition:
|
|
122
|
+
if isinstance(bean_type, str):
|
|
123
|
+
if name is not None and name != bean_type:
|
|
124
|
+
raise BeanDefinitionError("Named bean lookup cannot specify a different name")
|
|
125
|
+
definition = self._definitions.get(bean_type)
|
|
126
|
+
if definition is None or (
|
|
127
|
+
qualifier is not None and qualifier != definition.name
|
|
128
|
+
and qualifier not in definition.qualifiers
|
|
129
|
+
):
|
|
130
|
+
raise MissingBeanError(f"Missing bean: {bean_type}, qualifier: {qualifier}")
|
|
131
|
+
return definition
|
|
132
|
+
if not isinstance(bean_type, type):
|
|
133
|
+
raise BeanDefinitionError(f"Dependency types must be concrete classes: {bean_type}")
|
|
134
|
+
candidates = [
|
|
135
|
+
item for item in self._definitions.values()
|
|
136
|
+
if issubclass(item.bean_type, bean_type)
|
|
137
|
+
and (name is None or item.name == name)
|
|
138
|
+
and (qualifier is None or qualifier == item.name or qualifier in item.qualifiers)
|
|
139
|
+
]
|
|
140
|
+
if not candidates:
|
|
141
|
+
raise MissingBeanError(
|
|
142
|
+
f"Missing bean: {bean_type.__qualname__}, name: {name}, qualifier: {qualifier}"
|
|
143
|
+
)
|
|
144
|
+
if len(candidates) > 1:
|
|
145
|
+
primary = [item for item in candidates if item.primary]
|
|
146
|
+
if len(primary) == 1:
|
|
147
|
+
return primary[0]
|
|
148
|
+
raise DuplicateBeanError(
|
|
149
|
+
f"Multiple candidates for bean type {bean_type.__qualname__}: "
|
|
150
|
+
+ ", ".join(item.name for item in candidates)
|
|
151
|
+
)
|
|
152
|
+
return candidates[0]
|
|
153
|
+
|
|
154
|
+
@staticmethod
|
|
155
|
+
def _dependency_type(annotation: Any) -> Any:
|
|
156
|
+
if get_origin(annotation) is Annotated:
|
|
157
|
+
annotation = get_args(annotation)[0]
|
|
158
|
+
if get_origin(annotation) in (Union, UnionType):
|
|
159
|
+
types = tuple(item for item in get_args(annotation) if item is not type(None))
|
|
160
|
+
if len(types) == 1:
|
|
161
|
+
annotation = types[0]
|
|
162
|
+
return annotation
|
|
163
|
+
|
|
164
|
+
def _resolve(self, definition: BeanDefinition) -> _BeanState:
|
|
165
|
+
name = definition.name
|
|
166
|
+
singleton = definition.scope == "singleton"
|
|
167
|
+
if singleton and name in self._failures:
|
|
168
|
+
raise self._failures[name]
|
|
169
|
+
if singleton and name in self._singletons:
|
|
170
|
+
state = self._singletons[name]
|
|
171
|
+
if state.failure is not None:
|
|
172
|
+
raise state.failure
|
|
173
|
+
return state
|
|
174
|
+
if name in self._resolving:
|
|
175
|
+
raise CircularDependencyError("Circular dependency: " + " -> ".join([*self._resolving, name]))
|
|
176
|
+
self._resolving.append(name)
|
|
177
|
+
state: _BeanState | None = None
|
|
178
|
+
try:
|
|
179
|
+
explicit_dependencies = [self._resolve(self._definitions[item]) for item in definition.depends_on]
|
|
180
|
+
dependencies = list(explicit_dependencies)
|
|
181
|
+
factory = definition.factory
|
|
182
|
+
if definition.owner is not None:
|
|
183
|
+
owner = self._resolve(self._find_definition(definition.owner))
|
|
184
|
+
dependencies.append(owner)
|
|
185
|
+
factory = factory.__get__(owner.instance, definition.owner)
|
|
186
|
+
kwargs: dict[str, Any] = {}
|
|
187
|
+
configuration_properties = (
|
|
188
|
+
inspect.isclass(factory)
|
|
189
|
+
and "__pythonboot_configuration_properties__" in vars(factory)
|
|
190
|
+
)
|
|
191
|
+
hint_source = factory.__init__ if inspect.isclass(factory) else factory
|
|
192
|
+
try:
|
|
193
|
+
hints = {} if configuration_properties else get_type_hints(hint_source, include_extras=True)
|
|
194
|
+
except (NameError, TypeError) as error:
|
|
195
|
+
raise BeanDefinitionError(f"Cannot resolve dependency type hints for bean {name}") from error
|
|
196
|
+
parameters = () if configuration_properties else inspect.signature(factory).parameters.values()
|
|
197
|
+
for parameter in parameters:
|
|
198
|
+
if parameter.kind in (parameter.VAR_POSITIONAL, parameter.VAR_KEYWORD):
|
|
199
|
+
continue
|
|
200
|
+
if parameter.kind == parameter.POSITIONAL_ONLY:
|
|
201
|
+
raise BeanDefinitionError(f"Bean {name} does not support positional-only parameter: {parameter.name}")
|
|
202
|
+
annotation = hints.get(parameter.name)
|
|
203
|
+
marker = None
|
|
204
|
+
if get_origin(annotation) is Annotated:
|
|
205
|
+
marker = next((item for item in get_args(annotation)[1:] if isinstance(item, Autowired)), None)
|
|
206
|
+
if marker is not None and marker.lazy:
|
|
207
|
+
raise BeanDefinitionError("Lazy constructor injection is not supported; use an autowired(lazy=True) attribute")
|
|
208
|
+
dependency_type = self._dependency_type(annotation)
|
|
209
|
+
if dependency_type is None:
|
|
210
|
+
if parameter.default is not parameter.empty:
|
|
211
|
+
continue
|
|
212
|
+
raise BeanDefinitionError(f"Parameter {parameter.name} of bean {name} has no type annotation")
|
|
213
|
+
try:
|
|
214
|
+
dependency_definition = self._find_definition(
|
|
215
|
+
dependency_type,
|
|
216
|
+
name=marker.name if marker else None,
|
|
217
|
+
qualifier=marker.qualifier if marker else None,
|
|
218
|
+
)
|
|
219
|
+
except MissingBeanError:
|
|
220
|
+
if parameter.default is not parameter.empty:
|
|
221
|
+
continue
|
|
222
|
+
if marker is not None and not marker.required:
|
|
223
|
+
kwargs[parameter.name] = None
|
|
224
|
+
continue
|
|
225
|
+
raise
|
|
226
|
+
dependency = self._resolve(dependency_definition)
|
|
227
|
+
dependencies.append(dependency)
|
|
228
|
+
kwargs[parameter.name] = dependency.instance
|
|
229
|
+
with use_configuration_values(self._configuration):
|
|
230
|
+
instance = factory(**kwargs)
|
|
231
|
+
if not isinstance(instance, definition.bean_type):
|
|
232
|
+
raise BeanDefinitionError(f"Factory return type mismatch for bean {name}")
|
|
233
|
+
init_methods = tuple(dict.fromkeys((
|
|
234
|
+
*get_postconstruct_methods(type(instance)),
|
|
235
|
+
*((definition.init_method,) if definition.init_method is not None else ()),
|
|
236
|
+
)))
|
|
237
|
+
destroy_methods = tuple(dict.fromkeys((
|
|
238
|
+
*get_ondestroy_methods(type(instance)),
|
|
239
|
+
*((definition.destroy_method,) if definition.destroy_method is not None else ()),
|
|
240
|
+
)))
|
|
241
|
+
proxy = self._aop.create_proxy(
|
|
242
|
+
instance, name, excluded_methods=tuple(method for method in (
|
|
243
|
+
*init_methods, *destroy_methods,
|
|
244
|
+
) if method is not None),
|
|
245
|
+
)
|
|
246
|
+
state = _BeanState(
|
|
247
|
+
definition, proxy, dependencies, explicit_dependencies,
|
|
248
|
+
init_methods=init_methods, destroy_methods=destroy_methods,
|
|
249
|
+
)
|
|
250
|
+
if singleton:
|
|
251
|
+
# Publish singleton proxies early for attribute cycles; do not expose prototypes early.
|
|
252
|
+
self._singletons[name] = state
|
|
253
|
+
self._creation_order.append(state)
|
|
254
|
+
if self._starting:
|
|
255
|
+
self._startup_states.append(state)
|
|
256
|
+
self._inject_properties(instance, state)
|
|
257
|
+
state.complete = True
|
|
258
|
+
return state
|
|
259
|
+
except BaseException as error:
|
|
260
|
+
if state is not None:
|
|
261
|
+
state.failure = error
|
|
262
|
+
if singleton:
|
|
263
|
+
self._failures[name] = error
|
|
264
|
+
raise
|
|
265
|
+
finally:
|
|
266
|
+
self._resolving.pop()
|
|
267
|
+
|
|
268
|
+
def _inject_properties(self, instance: Any, state: _BeanState) -> None:
|
|
269
|
+
members: dict[str, Any] = {}
|
|
270
|
+
for cls in reversed(type(instance).__mro__):
|
|
271
|
+
members.update(vars(cls))
|
|
272
|
+
fields = {name: value for name, value in members.items() if isinstance(value, Autowired)}
|
|
273
|
+
if not fields:
|
|
274
|
+
return
|
|
275
|
+
try:
|
|
276
|
+
hints = get_type_hints(type(instance), include_extras=True)
|
|
277
|
+
except (NameError, TypeError) as error:
|
|
278
|
+
raise BeanDefinitionError(f"Cannot resolve attribute type hints for bean {state.definition.name}") from error
|
|
279
|
+
for attribute, marker in fields.items():
|
|
280
|
+
dependency_type = self._dependency_type(hints.get(attribute))
|
|
281
|
+
if not isinstance(dependency_type, type):
|
|
282
|
+
raise BeanDefinitionError(
|
|
283
|
+
f"Injected attribute {attribute} of bean {state.definition.name} must declare a concrete dependency type"
|
|
284
|
+
)
|
|
285
|
+
try:
|
|
286
|
+
definition = self._find_definition(dependency_type, name=marker.name, qualifier=marker.qualifier)
|
|
287
|
+
except MissingBeanError:
|
|
288
|
+
if marker.required:
|
|
289
|
+
raise
|
|
290
|
+
value = None
|
|
291
|
+
else:
|
|
292
|
+
if marker.lazy:
|
|
293
|
+
value = LazyDependency(
|
|
294
|
+
lambda bean_type=dependency_type, name=definition.name:
|
|
295
|
+
self.get_bean(bean_type, name=name)
|
|
296
|
+
)
|
|
297
|
+
else:
|
|
298
|
+
dependency = self._resolve(definition)
|
|
299
|
+
state.dependencies.append(dependency)
|
|
300
|
+
value = dependency.instance
|
|
301
|
+
setattr(instance, attribute, value)
|
|
302
|
+
|
|
303
|
+
@staticmethod
|
|
304
|
+
def _hook(state: _BeanState, method_name: str | None) -> Any:
|
|
305
|
+
if method_name is None:
|
|
306
|
+
return None
|
|
307
|
+
method = getattr(state.instance, method_name, None)
|
|
308
|
+
if not callable(method):
|
|
309
|
+
raise BeanDefinitionError(f"Bean {state.definition.name} has no callable lifecycle method: {method_name}")
|
|
310
|
+
return method
|
|
311
|
+
|
|
312
|
+
def _initialize_sync(self, state: _BeanState, visiting: set[_BeanState]) -> None:
|
|
313
|
+
if state.failure is not None:
|
|
314
|
+
raise state.failure
|
|
315
|
+
if state.initialized or state in visiting:
|
|
316
|
+
return
|
|
317
|
+
if not state.complete:
|
|
318
|
+
raise ContainerStateError(f"Attribute injection is incomplete for bean {state.definition.name}")
|
|
319
|
+
if self._init_owner is not None:
|
|
320
|
+
try:
|
|
321
|
+
task = asyncio.current_task()
|
|
322
|
+
except RuntimeError:
|
|
323
|
+
task = None
|
|
324
|
+
if task is not self._init_owner:
|
|
325
|
+
raise ContainerStateError("Bean initialization is running asynchronously; use await context.aget_bean(...)")
|
|
326
|
+
if state.initializing:
|
|
327
|
+
return
|
|
328
|
+
methods = [self._hook(state, name) for name in state.init_methods]
|
|
329
|
+
if any(inspect.iscoroutinefunction(method) for method in methods):
|
|
330
|
+
raise ContainerStateError(
|
|
331
|
+
f"Bean {state.definition.name} requires asynchronous initialization; use await context.aget_bean(...)"
|
|
332
|
+
)
|
|
333
|
+
visiting.add(state)
|
|
334
|
+
state.initializing = True
|
|
335
|
+
try:
|
|
336
|
+
for dependency in state.dependencies:
|
|
337
|
+
self._initialize_sync(dependency, visiting)
|
|
338
|
+
if any(not dependency.initialized for dependency in state.explicit_dependencies):
|
|
339
|
+
raise CircularDependencyError(f"depends_on and instance dependencies form an initialization cycle for bean {state.definition.name}")
|
|
340
|
+
for method in methods:
|
|
341
|
+
result = method()
|
|
342
|
+
if inspect.isawaitable(result):
|
|
343
|
+
if inspect.iscoroutine(result):
|
|
344
|
+
result.close()
|
|
345
|
+
raise BeanDefinitionError("Synchronous initialization returned an awaitable; declare the initializer as async")
|
|
346
|
+
state.initialized = True
|
|
347
|
+
except ContainerStateError:
|
|
348
|
+
raise
|
|
349
|
+
except BaseException as error:
|
|
350
|
+
state.failure = error
|
|
351
|
+
raise
|
|
352
|
+
finally:
|
|
353
|
+
state.initializing = False
|
|
354
|
+
visiting.discard(state)
|
|
355
|
+
|
|
356
|
+
async def _initialize_async(self, state: _BeanState, visiting: set[_BeanState]) -> None:
|
|
357
|
+
if state.failure is not None:
|
|
358
|
+
raise state.failure
|
|
359
|
+
if state.initialized or state.initializing or state in visiting:
|
|
360
|
+
return
|
|
361
|
+
if not state.complete:
|
|
362
|
+
raise ContainerStateError(f"Attribute injection is incomplete for bean {state.definition.name}")
|
|
363
|
+
visiting.add(state)
|
|
364
|
+
state.initializing = True
|
|
365
|
+
try:
|
|
366
|
+
for dependency in state.dependencies:
|
|
367
|
+
await self._initialize_async(dependency, visiting)
|
|
368
|
+
if any(not dependency.initialized for dependency in state.explicit_dependencies):
|
|
369
|
+
raise CircularDependencyError(f"depends_on and instance dependencies form an initialization cycle for bean {state.definition.name}")
|
|
370
|
+
for name in state.init_methods:
|
|
371
|
+
method = self._hook(state, name)
|
|
372
|
+
result = method()
|
|
373
|
+
if inspect.isawaitable(result):
|
|
374
|
+
await result
|
|
375
|
+
state.initialized = True
|
|
376
|
+
except BaseException as error:
|
|
377
|
+
state.failure = error
|
|
378
|
+
raise
|
|
379
|
+
finally:
|
|
380
|
+
state.initializing = False
|
|
381
|
+
visiting.discard(state)
|
|
382
|
+
|
|
383
|
+
async def start(self) -> None:
|
|
384
|
+
if self._active:
|
|
385
|
+
raise ContainerStateError("The container is already active")
|
|
386
|
+
self._active = True
|
|
387
|
+
self._starting = True
|
|
388
|
+
try:
|
|
389
|
+
for definition in self._definitions.values():
|
|
390
|
+
if definition.scope == "singleton" and not definition.lazy:
|
|
391
|
+
self._resolve(definition)
|
|
392
|
+
async with self._async_init_lock:
|
|
393
|
+
self._init_owner = asyncio.current_task()
|
|
394
|
+
try:
|
|
395
|
+
for state in self._startup_states:
|
|
396
|
+
await self._initialize_async(state, set())
|
|
397
|
+
finally:
|
|
398
|
+
self._init_owner = None
|
|
399
|
+
self._started = True
|
|
400
|
+
except BaseException:
|
|
401
|
+
await self.close()
|
|
402
|
+
raise
|
|
403
|
+
finally:
|
|
404
|
+
self._starting = False
|
|
405
|
+
self._startup_states.clear()
|
|
406
|
+
|
|
407
|
+
async def close(self) -> None:
|
|
408
|
+
self._active = False
|
|
409
|
+
self._started = False
|
|
410
|
+
errors: list[Exception] = []
|
|
411
|
+
try:
|
|
412
|
+
for state in reversed(self._creation_order):
|
|
413
|
+
try:
|
|
414
|
+
for name in state.destroy_methods:
|
|
415
|
+
method = self._hook(state, name)
|
|
416
|
+
result = method() if method is not None else None
|
|
417
|
+
if inspect.isawaitable(result):
|
|
418
|
+
await result
|
|
419
|
+
except Exception as error:
|
|
420
|
+
errors.append(error)
|
|
421
|
+
finally:
|
|
422
|
+
self._singletons.clear()
|
|
423
|
+
self._creation_order.clear()
|
|
424
|
+
self._startup_states.clear()
|
|
425
|
+
self._failures.clear()
|
|
426
|
+
if errors:
|
|
427
|
+
raise ExceptionGroup("Bean destruction failed", errors)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Explicit attribute injection declarations."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable
|
|
4
|
+
|
|
5
|
+
from pythonboot.exceptions import BeanDefinitionError, ContainerStateError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LazyDependency:
|
|
9
|
+
def __init__(self, resolver: Callable[[], Any]) -> None:
|
|
10
|
+
self._resolver = resolver
|
|
11
|
+
|
|
12
|
+
def resolve(self) -> Any:
|
|
13
|
+
return self._resolver()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Autowired:
|
|
17
|
+
def __init__(
|
|
18
|
+
self, *, name: str | None = None, required: bool = True, lazy: bool = False,
|
|
19
|
+
qualifier: str | None = None,
|
|
20
|
+
) -> None:
|
|
21
|
+
self.name = name
|
|
22
|
+
self.required = required
|
|
23
|
+
self.lazy = lazy
|
|
24
|
+
self.qualifier = qualifier
|
|
25
|
+
self._attribute_name = ""
|
|
26
|
+
|
|
27
|
+
def __set_name__(self, owner: type, name: str) -> None:
|
|
28
|
+
self._attribute_name = name
|
|
29
|
+
|
|
30
|
+
def __get__(self, instance: Any, owner: type | None = None) -> Any:
|
|
31
|
+
if instance is None:
|
|
32
|
+
return self
|
|
33
|
+
try:
|
|
34
|
+
attributes = vars(instance)
|
|
35
|
+
except TypeError as error:
|
|
36
|
+
raise BeanDefinitionError("Attribute injection requires instances with __dict__") from error
|
|
37
|
+
if self._attribute_name not in attributes:
|
|
38
|
+
raise ContainerStateError(
|
|
39
|
+
f"Attribute {type(instance).__name__}.{self._attribute_name} has not been injected; "
|
|
40
|
+
"access it in initialization or business methods, not in the constructor"
|
|
41
|
+
)
|
|
42
|
+
value = attributes[self._attribute_name]
|
|
43
|
+
if isinstance(value, LazyDependency):
|
|
44
|
+
value = value.resolve()
|
|
45
|
+
attributes[self._attribute_name] = value
|
|
46
|
+
return value
|
|
47
|
+
|
|
48
|
+
def __set__(self, instance: Any, value: Any) -> None:
|
|
49
|
+
try:
|
|
50
|
+
vars(instance)[self._attribute_name] = value
|
|
51
|
+
except TypeError as error:
|
|
52
|
+
raise BeanDefinitionError("Attribute injection requires instances with __dict__") from error
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def autowired(
|
|
56
|
+
*, name: str | None = None, required: bool = True, lazy: bool = False,
|
|
57
|
+
qualifier: str | None = None,
|
|
58
|
+
) -> Any:
|
|
59
|
+
"""Declare a dependency injected by attribute type, with an optional bean name."""
|
|
60
|
+
return Autowired(name=name, required=required, lazy=lazy, qualifier=qualifier)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Framework exceptions."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class PythonBootError(Exception):
|
|
5
|
+
"""Base class for framework exceptions."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BeanDefinitionError(PythonBootError):
|
|
9
|
+
"""Invalid bean declaration."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DuplicateBeanError(PythonBootError):
|
|
13
|
+
"""Duplicate bean name or ambiguous type candidates."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MissingBeanError(PythonBootError):
|
|
17
|
+
"""No bean matches the requested dependency."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CircularDependencyError(PythonBootError):
|
|
21
|
+
"""Bean dependencies form a cycle."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ContainerStateError(PythonBootError):
|
|
25
|
+
"""The container is inactive or shutting down."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AopError(PythonBootError):
|
|
29
|
+
"""Invalid aspect declaration or incompatible advice invocation."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ConfigurationError(PythonBootError):
|
|
33
|
+
"""Invalid configuration source or property binding."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"PythonBootError", "BeanDefinitionError", "DuplicateBeanError",
|
|
38
|
+
"MissingBeanError", "CircularDependencyError", "ContainerStateError",
|
|
39
|
+
"AopError", "ConfigurationError",
|
|
40
|
+
]
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Public task execution interfaces."""
|
|
2
|
+
|
|
3
|
+
from pythonboot.task.executor import (
|
|
4
|
+
RejectionPolicy, TaskDecorator, TaskExecutor, TaskRejectedError, ThreadPoolTaskExecutor,
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
__all__ = ["TaskExecutor", "ThreadPoolTaskExecutor", "TaskRejectedError", "RejectionPolicy", "TaskDecorator"]
|