vsjetengine 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.
@@ -0,0 +1,85 @@
1
+ # vs-engine
2
+ # Copyright (C) 2022 cid-chan
3
+ # Copyright (C) 2025 Jaded-Encoding-Thaumaturgy
4
+ # This project is licensed under the EUPL-1.2
5
+ # SPDX-License-Identifier: EUPL-1.2
6
+
7
+ import asyncio
8
+ import contextlib
9
+ import contextvars
10
+ from collections.abc import Callable, Iterator
11
+ from concurrent.futures import Future
12
+
13
+ from vsengine.loops import Cancelled, EventLoop
14
+
15
+
16
+ class AsyncIOLoop(EventLoop):
17
+ """
18
+ Bridges vs-engine to AsyncIO.
19
+ """
20
+
21
+ def __init__(self, loop: asyncio.AbstractEventLoop | None = None) -> None:
22
+ if loop is None:
23
+ loop = asyncio.get_event_loop()
24
+ self.loop = loop
25
+
26
+ def from_thread[**P, R](self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]:
27
+ future = Future[R]()
28
+
29
+ ctx = contextvars.copy_context()
30
+
31
+ def _wrap() -> None:
32
+ if not future.set_running_or_notify_cancel():
33
+ return
34
+
35
+ try:
36
+ result = ctx.run(func, *args, **kwargs)
37
+ except BaseException as e:
38
+ future.set_exception(e)
39
+ else:
40
+ future.set_result(result)
41
+
42
+ self.loop.call_soon_threadsafe(_wrap)
43
+ return future
44
+
45
+ def to_thread[**P, R](self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]:
46
+ ctx = contextvars.copy_context()
47
+ future = Future[R]()
48
+
49
+ def _wrap() -> R:
50
+ return ctx.run(func, *args, **kwargs)
51
+
52
+ async def _run() -> None:
53
+ try:
54
+ result = await asyncio.to_thread(_wrap)
55
+ except BaseException as e:
56
+ future.set_exception(e)
57
+ else:
58
+ future.set_result(result)
59
+
60
+ self.loop.create_task(_run())
61
+ return future
62
+
63
+ def next_cycle(self) -> Future[None]:
64
+ future = Future[None]()
65
+ task = asyncio.current_task()
66
+
67
+ def continuation() -> None:
68
+ if task is None or not task.cancelled():
69
+ future.set_result(None)
70
+ else:
71
+ future.set_exception(Cancelled())
72
+
73
+ self.loop.call_soon(continuation)
74
+ return future
75
+
76
+ async def await_future[T](self, future: Future[T]) -> T:
77
+ with self.wrap_cancelled():
78
+ return await asyncio.wrap_future(future, loop=self.loop)
79
+
80
+ @contextlib.contextmanager
81
+ def wrap_cancelled(self) -> Iterator[None]:
82
+ try:
83
+ yield
84
+ except Cancelled:
85
+ raise asyncio.CancelledError() from None
@@ -0,0 +1,107 @@
1
+ # vs-engine
2
+ # Copyright (C) 2022 cid-chan
3
+ # Copyright (C) 2025 Jaded-Encoding-Thaumaturgy
4
+ # This project is licensed under the EUPL-1.2
5
+ # SPDX-License-Identifier: EUPL-1.2
6
+
7
+ import contextlib
8
+ from collections.abc import Callable, Iterator
9
+ from concurrent.futures import Future
10
+
11
+ import trio
12
+
13
+ from vsengine.loops import Cancelled, EventLoop
14
+
15
+
16
+ class TrioEventLoop(EventLoop):
17
+ """
18
+ Bridges vs-engine to Trio.
19
+ """
20
+
21
+ def __init__(self, nursery: trio.Nursery, limiter: trio.CapacityLimiter | None = None) -> None:
22
+ if limiter is None:
23
+ limiter = trio.to_thread.current_default_thread_limiter()
24
+
25
+ self.nursery = nursery
26
+ self.limiter = limiter
27
+ self._token: trio.lowlevel.TrioToken | None = None
28
+
29
+ def attach(self) -> None:
30
+ self._token = trio.lowlevel.current_trio_token()
31
+
32
+ def detach(self) -> None:
33
+ self.nursery.cancel_scope.cancel()
34
+
35
+ def from_thread[**P, R](self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]:
36
+ assert self._token is not None
37
+
38
+ fut = Future[R]()
39
+
40
+ def _executor() -> None:
41
+ if not fut.set_running_or_notify_cancel():
42
+ return
43
+
44
+ try:
45
+ result = func(*args, **kwargs)
46
+ except BaseException as e:
47
+ fut.set_exception(e)
48
+ else:
49
+ fut.set_result(result)
50
+
51
+ self._token.run_sync_soon(_executor)
52
+ return fut
53
+
54
+ def to_thread[**P, R](self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]:
55
+ future = Future[R]()
56
+
57
+ async def _run() -> None:
58
+ def _executor() -> None:
59
+ try:
60
+ result = func(*args, **kwargs)
61
+ future.set_result(result)
62
+ except BaseException as e:
63
+ future.set_exception(e)
64
+
65
+ await trio.to_thread.run_sync(_executor, limiter=self.limiter)
66
+
67
+ self.nursery.start_soon(_run)
68
+ return future
69
+
70
+ def next_cycle(self) -> Future[None]:
71
+ scope = trio.CancelScope()
72
+ future = Future[None]()
73
+
74
+ def continuation() -> None:
75
+ if scope.cancel_called:
76
+ future.set_exception(Cancelled())
77
+ else:
78
+ future.set_result(None)
79
+
80
+ self.from_thread(continuation)
81
+ return future
82
+
83
+ async def await_future[T](self, future: Future[T]) -> T:
84
+ event = trio.Event()
85
+
86
+ def _when_done(_: Future[T]) -> None:
87
+ self.from_thread(event.set)
88
+
89
+ future.add_done_callback(_when_done)
90
+
91
+ try:
92
+ await event.wait()
93
+ except trio.Cancelled:
94
+ raise
95
+
96
+ try:
97
+ return future.result()
98
+ except BaseException as exc:
99
+ with self.wrap_cancelled():
100
+ raise exc
101
+
102
+ @contextlib.contextmanager
103
+ def wrap_cancelled(self) -> Iterator[None]:
104
+ try:
105
+ yield
106
+ except Cancelled:
107
+ raise trio.Cancelled.__new__(trio.Cancelled) from None
vsengine/loops.py ADDED
@@ -0,0 +1,269 @@
1
+ # vs-engine
2
+ # Copyright (C) 2022 cid-chan
3
+ # Copyright (C) 2025 Jaded-Encoding-Thaumaturgy
4
+ # This project is licensed under the EUPL-1.2
5
+ # SPDX-License-Identifier: EUPL-1.2
6
+
7
+ """This module provides an abstraction layer to integrate VapourSynth with any event loop (asyncio, Qt, Trio, etc.)."""
8
+
9
+ import threading
10
+ from abc import abstractmethod
11
+ from collections.abc import Awaitable, Callable, Iterator
12
+ from concurrent.futures import CancelledError, Future
13
+ from contextlib import contextmanager
14
+ from functools import wraps
15
+
16
+ import vapoursynth as vs
17
+
18
+ __all__ = ["Cancelled", "EventLoop", "from_thread", "get_loop", "keep_environment", "set_loop", "to_thread"]
19
+
20
+
21
+ class Cancelled(BaseException):
22
+ """Exception raised when an operation has been cancelled."""
23
+
24
+
25
+ @contextmanager
26
+ def _noop() -> Iterator[None]:
27
+ yield
28
+
29
+
30
+ DONE = Future[None]()
31
+ DONE.set_result(None)
32
+
33
+
34
+ class EventLoop:
35
+ """
36
+ Abstract base class for event loop integration.
37
+
38
+ These functions must be implemented to bridge VapourSynth with the event-loop of your choice (e.g., asyncio, Qt).
39
+ """
40
+
41
+ def attach(self) -> None:
42
+ """
43
+ Initialize the event loop hooks.
44
+
45
+ Called automatically when :func:`set_loop` is run.
46
+ """
47
+
48
+ def detach(self) -> None:
49
+ """
50
+ Clean up event loop hooks.
51
+
52
+ Called when another event-loop takes over, or when the application
53
+ is shutting down/restarting.
54
+ """
55
+
56
+ @abstractmethod
57
+ def from_thread[**P, R](self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]:
58
+ """
59
+ Schedule a function to run on the event loop (usually the main thread).
60
+
61
+ This is typically called from VapourSynth threads to move data or
62
+ logic back to the main application loop.
63
+
64
+ :param func: The callable to execute.
65
+ :param args: Positional arguments for the callable.
66
+ :param kwargs: Keyword arguments for the callable.
67
+ :return: A Future representing the execution result.
68
+ """
69
+
70
+ def to_thread[**P, R](self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]:
71
+ """
72
+ Run a function in a separate worker thread.
73
+
74
+ This is used to offload blocking operations from the main event loop.
75
+ The default implementation utilizes :class:`threading.Thread`.
76
+
77
+ :param func: The callable to execute.
78
+ :param args: Positional arguments for the callable.
79
+ :param kwargs: Keyword arguments for the callable.
80
+ :return: A Future representing the execution result.
81
+ """
82
+ fut = Future[R]()
83
+
84
+ def wrapper() -> None:
85
+ if not fut.set_running_or_notify_cancel():
86
+ return
87
+
88
+ try:
89
+ result = func(*args, **kwargs)
90
+ except BaseException as e:
91
+ fut.set_exception(e)
92
+ else:
93
+ fut.set_result(result)
94
+
95
+ threading.Thread(target=wrapper).start()
96
+
97
+ return fut
98
+
99
+ def next_cycle(self) -> Future[None]:
100
+ """
101
+ Pass control back to the event loop.
102
+
103
+ This allows the event loop to process pending events.
104
+
105
+ * If there is **no** event-loop, the function returns an immediately resolved future.
106
+ * If there **is** an event-loop, the function returns a pending future that
107
+ resolves after the next cycle.
108
+
109
+ :raises vsengine.loops.Cancelled: If the operation has been cancelled.
110
+ :return: A Future that resolves when the cycle is complete.
111
+ """
112
+ future = Future[None]()
113
+ self.from_thread(future.set_result, None)
114
+ return future
115
+
116
+ def await_future[T](self, future: Future[T]) -> Awaitable[T]:
117
+ """
118
+ Convert a concurrent Future into an Awaitable compatible with this loop.
119
+
120
+ This function does not need to be implemented if the event-loop
121
+ does not support ``async`` and ``await`` syntax.
122
+
123
+ :param future: The concurrent.futures.Future to await.
124
+ :return: An awaitable object.
125
+ """
126
+ raise NotImplementedError
127
+
128
+ @contextmanager
129
+ def wrap_cancelled(self) -> Iterator[None]:
130
+ """
131
+ Context manager to translate cancellation exceptions.
132
+
133
+ Wraps :exc:`vsengine.loops.Cancelled` into the native cancellation
134
+ error of the specific event loop implementation (e.g., ``asyncio.CancelledError``).
135
+ """
136
+ try:
137
+ yield
138
+ except Cancelled:
139
+ raise CancelledError from None
140
+
141
+
142
+ class _NoEventLoop(EventLoop):
143
+ """
144
+ The default event-loop implementation.
145
+
146
+ This is used when no specific loop is attached. It runs operations synchronously/inline.
147
+ """
148
+
149
+ def from_thread[**P, R](self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]:
150
+ fut = Future[R]()
151
+ try:
152
+ result = func(*args, **kwargs)
153
+ except BaseException as e:
154
+ fut.set_exception(e)
155
+ else:
156
+ fut.set_result(result)
157
+ return fut
158
+
159
+ def next_cycle(self) -> Future[None]:
160
+ return DONE
161
+
162
+
163
+ NO_LOOP = _NoEventLoop()
164
+ _current_loop: EventLoop = NO_LOOP
165
+
166
+
167
+ def get_loop() -> EventLoop:
168
+ """
169
+ Retrieve the currently active event loop.
170
+
171
+ :return: The currently running EventLoop instance.
172
+ """
173
+ return _current_loop
174
+
175
+
176
+ def set_loop(loop: EventLoop) -> None:
177
+ """
178
+ Set the currently running event loop.
179
+
180
+ This function will detach the previous loop first. If attaching the new
181
+ loop fails, it reverts to the ``_NoEventLoop`` implementation which runs
182
+ everything inline.
183
+
184
+ :param loop: The EventLoop instance to attach.
185
+ """
186
+ global _current_loop
187
+ _current_loop.detach()
188
+
189
+ try:
190
+ _current_loop = loop
191
+ loop.attach()
192
+ except:
193
+ _current_loop = NO_LOOP
194
+ raise
195
+
196
+
197
+ def keep_environment[**P, R](func: Callable[P, R]) -> Callable[P, R]:
198
+ """
199
+ Decorate a function to preserve the VapourSynth environment.
200
+
201
+ The returned function captures the VapourSynth environment active
202
+ at the moment the decorator is applied and restores it when the
203
+ function is executed.
204
+
205
+ :param func: The function to decorate.
206
+ :return: A wrapped function that maintains the captured environment.
207
+ """
208
+ try:
209
+ environment = vs.get_current_environment().use
210
+ except RuntimeError:
211
+ environment = _noop
212
+
213
+ @wraps(func)
214
+ def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
215
+ with environment():
216
+ return func(*args, **kwargs)
217
+
218
+ return _wrapper
219
+
220
+
221
+ def from_thread[**P, R](func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]:
222
+ """
223
+ Run a function inside the current event-loop.
224
+
225
+ This preserves the currently running VapourSynth environment (if any).
226
+
227
+ .. note::
228
+ Depending on the loop implementation, the function might be called inline.
229
+
230
+ :param func: The function to call inside the current event loop.
231
+ :param args: The arguments for the function.
232
+ :param kwargs: The keyword arguments to pass to the function.
233
+ :return: A Future that resolves or rejects depending on the outcome.
234
+ """
235
+
236
+ @keep_environment
237
+ def _wrapper() -> R:
238
+ return func(*args, **kwargs)
239
+
240
+ return get_loop().from_thread(_wrapper)
241
+
242
+
243
+ def to_thread[**P, R](func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]:
244
+ """
245
+ Run a function in a dedicated thread or worker.
246
+
247
+ This preserves the currently running VapourSynth environment (if any).
248
+
249
+ :param func: The function to call in a worker thread.
250
+ :param args: The arguments for the function.
251
+ :param kwargs: The keyword arguments to pass to the function.
252
+ :return: A Future representing the execution result.
253
+ """
254
+
255
+ @keep_environment
256
+ def _wrapper() -> R:
257
+ return func(*args, **kwargs)
258
+
259
+ return get_loop().to_thread(_wrapper)
260
+
261
+
262
+ async def make_awaitable[T](future: Future[T]) -> T:
263
+ """
264
+ Make a standard concurrent Future awaitable in the current loop.
265
+
266
+ :param future: The future object to make awaitable.
267
+ :return: The result of the future, once awaited.
268
+ """
269
+ return await get_loop().await_future(future)