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.
@@ -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"]