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.
vsengine/vpy.py ADDED
@@ -0,0 +1,441 @@
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
+ """This module provides functions to load and execute VapourSynth scripts (`.vpy` files) or inline code."""
7
+
8
+ from __future__ import annotations
9
+
10
+ import ast
11
+ import io
12
+ import os
13
+ import sys
14
+ from collections.abc import Awaitable, Buffer, Callable, Generator
15
+ from concurrent.futures import Future
16
+ from contextlib import AbstractContextManager
17
+ from types import CodeType, ModuleType, TracebackType
18
+ from typing import Any, Concatenate, Self, overload
19
+ from uuid import uuid4
20
+
21
+ import vapoursynth as vs
22
+
23
+ from ._futures import UnifiedFuture, unified
24
+ from .loops import make_awaitable, to_thread
25
+ from .policy import ManagedEnvironment, Policy
26
+
27
+ __all__ = ["ExecutionError", "Script", "load_code", "load_script"]
28
+
29
+ type Runner[R] = Callable[[Callable[[], R]], Future[R]]
30
+ type Executor[T] = Callable[[WrapAllErrors, ModuleType], T]
31
+
32
+
33
+ class ExecutionError(Exception):
34
+ """
35
+ Exception raised when script execution fails.
36
+ """
37
+
38
+ parent_error: BaseException
39
+ """The actual exception that has been raised"""
40
+
41
+ def __init__(self, parent_error: BaseException) -> None:
42
+ """
43
+ Initialize the ExecutionError exception.
44
+
45
+ :param parent_error: The original exception that occurred.
46
+ """
47
+ import textwrap
48
+
49
+ msg = textwrap.indent(self.extract_traceback(parent_error), "| ")
50
+ super().__init__(f"An exception was raised while running the script.\n{msg}")
51
+ self.parent_error = parent_error
52
+
53
+ @staticmethod
54
+ def extract_traceback(error: BaseException) -> str:
55
+ """
56
+ Extract and format the traceback from an exception.
57
+
58
+ :param error: The exception to extract the traceback from.
59
+ :return: A formatted string containing the traceback.
60
+ """
61
+ import traceback
62
+
63
+ msg = traceback.format_exception(type(error), error, error.__traceback__)
64
+ msg = "".join(msg)
65
+ return msg
66
+
67
+
68
+ class WrapAllErrors(AbstractContextManager[None]):
69
+ """
70
+ Context manager that wraps exceptions in ExecutionError.
71
+ """
72
+
73
+ def __enter__(self) -> None: ...
74
+
75
+ def __exit__(self, exc: type[BaseException] | None, val: BaseException | None, tb: TracebackType | None) -> None:
76
+ if val is not None:
77
+ raise ExecutionError(val) from None
78
+
79
+
80
+ class _TempModule(AbstractContextManager[None]):
81
+ """
82
+ Temporarily register a module in sys.modules.
83
+
84
+ Ported from runpy.
85
+ That ensures the module is available in sys.modules during execution and restored/cleaned up afterwards.
86
+ """
87
+
88
+ def __init__(self, mod_name: str, filename: str) -> None:
89
+ self.mod_name = mod_name
90
+ self.module = ModuleType(mod_name)
91
+ self.module.__dict__["__file__"] = filename
92
+ self._saved_module = list[ModuleType | None]()
93
+
94
+ def __enter__(self) -> None:
95
+ mod_name = self.mod_name
96
+
97
+ self._saved_module.append(sys.modules.get(mod_name))
98
+
99
+ sys.modules[mod_name] = self.module
100
+
101
+ def __exit__(self, exc: type[BaseException] | None, val: BaseException | None, tb: TracebackType | None) -> None:
102
+ mod = self._saved_module.pop()
103
+
104
+ if mod:
105
+ sys.modules[self.mod_name] = mod
106
+ else:
107
+ del sys.modules[self.mod_name]
108
+
109
+
110
+ def inline_runner[T](func: Callable[[], T]) -> Future[T]:
111
+ """
112
+ Runs a function inline and returns the result as a Future.
113
+
114
+ :param func: The function to run.
115
+ :return: A future containing the result or exception of the function.
116
+ """
117
+ fut = Future[T]()
118
+ try:
119
+ result = func()
120
+ except BaseException as e:
121
+ fut.set_exception(e)
122
+ else:
123
+ fut.set_result(result)
124
+ return fut
125
+
126
+
127
+ def chdir_runner[**P, R](
128
+ dir: str | os.PathLike[str], parent: Runner[R]
129
+ ) -> Callable[Concatenate[Callable[P, R], P], Future[R]]:
130
+ """
131
+ Wraps a runner to change the current working directory during execution.
132
+
133
+ :param dir: The directory to change to.
134
+ :param parent: The runner to wrap.
135
+ :return: A wrapped runner function.
136
+ """
137
+
138
+ def runner(func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]:
139
+ def _wrapped() -> R:
140
+ current = os.getcwd()
141
+ os.chdir(dir)
142
+
143
+ try:
144
+ f = func(*args, **kwargs)
145
+ return f
146
+ except Exception:
147
+ raise
148
+ finally:
149
+ os.chdir(current)
150
+
151
+ return parent(_wrapped)
152
+
153
+ return runner
154
+
155
+
156
+ _missing = object()
157
+
158
+
159
+ class Script[EnvT: (vs.Environment, ManagedEnvironment)](AbstractContextManager["Script[EnvT]"], Awaitable[None]):
160
+ """VapourSynth script wrapper."""
161
+
162
+ def __init__(self, executor: Executor[None], module: ModuleType, environment: EnvT, runner: Runner[None]) -> None:
163
+ self.executor = executor
164
+ self.environment: EnvT = environment
165
+ self.runner = runner
166
+ self.module = module
167
+
168
+ def __enter__(self) -> Self:
169
+ self.result()
170
+ return self
171
+
172
+ def __exit__(self, exc: type[BaseException] | None, val: BaseException | None, tb: TracebackType | None) -> None:
173
+ self.dispose()
174
+
175
+ def __await__(self) -> Generator[Any, None, None]:
176
+ """
177
+ Runs the script and waits until the script has completed.
178
+ """
179
+ return self.run_async().__await__()
180
+
181
+ def run(self) -> Future[None]:
182
+ """
183
+ Runs the script.
184
+
185
+ It returns a future which completes when the script completes.
186
+ When the script fails, it raises a ExecutionError.
187
+ """
188
+ self._future: Future[None]
189
+
190
+ if hasattr(self, "_future"):
191
+ return self._future
192
+
193
+ self._future = self.runner(self._run_inline)
194
+
195
+ return self._future
196
+
197
+ async def run_async(self) -> None:
198
+ """
199
+ Runs the script asynchronously, but it returns a coroutine.
200
+ """
201
+ return await make_awaitable(self.run())
202
+
203
+ def result(self) -> None:
204
+ """
205
+ Runs the script and blocks until the script has finished running.
206
+ """
207
+ return self.run().result()
208
+
209
+ def dispose(self) -> None:
210
+ """Disposes the managed environment and clears the module globals."""
211
+ self.module.__dict__.clear()
212
+
213
+ if isinstance(self.environment, ManagedEnvironment):
214
+ self.environment.dispose()
215
+
216
+ @overload
217
+ @unified(kind="future")
218
+ def get_variable(self, name: str) -> Future[Any]: ...
219
+ @overload
220
+ @unified(kind="future")
221
+ def get_variable[T](self, name: str, default: T) -> Future[Any | T]: ...
222
+ @unified(kind="future")
223
+ def get_variable(self, name: str, default: Any = _missing) -> Future[Any]:
224
+ """
225
+ Retrieve a variable from the script's module.
226
+
227
+ :param name: The name of the variable to retrieve.
228
+ :param default: The default value if the variable is not found.
229
+ :return: A future that resolves to the variable's value.
230
+ """
231
+ return UnifiedFuture[Any].resolve(
232
+ getattr(self.module, name) if default is _missing else getattr(self.module, name, default)
233
+ )
234
+
235
+ def _run_inline(self) -> None:
236
+ with self.environment.use():
237
+ self.executor(WrapAllErrors(), self.module)
238
+
239
+
240
+ @overload
241
+ def load_script(
242
+ script: str | os.PathLike[str],
243
+ environment: vs.Environment | None = None,
244
+ *,
245
+ module: str | ModuleType = "__vapoursynth__",
246
+ inline: bool = True,
247
+ chdir: str | os.PathLike[str] | None = None,
248
+ ) -> Script[vs.Environment]: ...
249
+
250
+
251
+ @overload
252
+ def load_script(
253
+ script: str | os.PathLike[str],
254
+ environment: Script[vs.Environment],
255
+ *,
256
+ inline: bool = True,
257
+ chdir: str | os.PathLike[str] | None = None,
258
+ ) -> Script[vs.Environment]: ...
259
+
260
+
261
+ @overload
262
+ def load_script(
263
+ script: str | os.PathLike[str],
264
+ environment: Policy | ManagedEnvironment,
265
+ *,
266
+ module: str | ModuleType = "__vapoursynth__",
267
+ inline: bool = True,
268
+ chdir: str | os.PathLike[str] | None = None,
269
+ ) -> Script[ManagedEnvironment]: ...
270
+
271
+
272
+ @overload
273
+ def load_script(
274
+ script: str | os.PathLike[str],
275
+ environment: Script[ManagedEnvironment],
276
+ *,
277
+ inline: bool = True,
278
+ chdir: str | os.PathLike[str] | None = None,
279
+ ) -> Script[ManagedEnvironment]: ...
280
+
281
+
282
+ def load_script(
283
+ script: str | os.PathLike[str],
284
+ environment: Policy | vs.Environment | ManagedEnvironment | Script[Any] | None = None,
285
+ *,
286
+ module: str | ModuleType = "__vapoursynth__",
287
+ inline: bool = True,
288
+ chdir: str | os.PathLike[str] | None = None,
289
+ ) -> Script[Any]:
290
+ """
291
+ Runs the script at the given path.
292
+
293
+ :param script: The path to the script file to run.
294
+ :param environment: Defines the environment in which the code should run.
295
+ If passed a Policy, it will create a new environment from the policy,
296
+ which can be accessed using the environment attribute.
297
+ :param module: The name the module should get. Defaults to __vapoursynth__.
298
+ :param inline: Run the code inline, e.g. not in a separate thread.
299
+ :param chdir: Change the currently running directory while the script is running.
300
+ This is unsafe when running multiple scripts at once.
301
+ :returns: A script object. The script starts running when you call run() on it, or await it.
302
+ """
303
+
304
+ def _execute(ctx: WrapAllErrors, module: ModuleType) -> None:
305
+ nonlocal script
306
+
307
+ script = str(script)
308
+
309
+ with ctx, io.open_code(script) as f, _TempModule(module.__name__, script):
310
+ exec(
311
+ compile(f.read(), filename=script, dont_inherit=True, flags=0, mode="exec"),
312
+ module.__dict__,
313
+ module.__dict__,
314
+ )
315
+
316
+ return _load(_execute, environment, module, inline, chdir)
317
+
318
+
319
+ @overload
320
+ def load_code(
321
+ script: str | Buffer | ast.Module | CodeType,
322
+ environment: vs.Environment | None = None,
323
+ *,
324
+ module: str | ModuleType = "__vapoursynth__",
325
+ inline: bool = True,
326
+ chdir: str | os.PathLike[str] | None = None,
327
+ **kwargs: Any,
328
+ ) -> Script[vs.Environment]: ...
329
+
330
+
331
+ @overload
332
+ def load_code(
333
+ script: str | Buffer | ast.Module | CodeType,
334
+ environment: Script[vs.Environment],
335
+ *,
336
+ inline: bool = True,
337
+ chdir: str | os.PathLike[str] | None = None,
338
+ **kwargs: Any,
339
+ ) -> Script[vs.Environment]: ...
340
+
341
+
342
+ @overload
343
+ def load_code(
344
+ script: str | Buffer | ast.Module | CodeType,
345
+ environment: Policy | ManagedEnvironment,
346
+ *,
347
+ module: str | ModuleType = "__vapoursynth__",
348
+ inline: bool = True,
349
+ chdir: str | os.PathLike[str] | None = None,
350
+ **kwargs: Any,
351
+ ) -> Script[ManagedEnvironment]: ...
352
+
353
+
354
+ @overload
355
+ def load_code(
356
+ script: str | Buffer | ast.Module | CodeType,
357
+ environment: Script[ManagedEnvironment],
358
+ *,
359
+ inline: bool = True,
360
+ chdir: str | os.PathLike[str] | None = None,
361
+ **kwargs: Any,
362
+ ) -> Script[ManagedEnvironment]: ...
363
+
364
+
365
+ def load_code(
366
+ script: str | Buffer | ast.Module | CodeType,
367
+ environment: Policy | vs.Environment | ManagedEnvironment | Script[Any] | None = None,
368
+ *,
369
+ module: str | ModuleType = "__vapoursynth__",
370
+ inline: bool = True,
371
+ chdir: str | os.PathLike[str] | None = None,
372
+ **kwargs: Any,
373
+ ) -> Script[Any]:
374
+ """
375
+ Runs the given code snippet.
376
+
377
+ :param script: The code to run. Can be a string, bytes, AST, or compiled code.
378
+ :param environment: Defines the environment in which the code should run. If passed a Policy,
379
+ it will create a new environment from the policy,
380
+ which can be accessed using the environment attribute.
381
+ If the environment is another Script, it will take the environment and module of the script.
382
+ :param module: The name the module should get. Defaults to __vapoursynth__.
383
+ :param inline: Run the code inline, e.g. not in a separate thread.
384
+ :param chdir: Change the currently running directory while the script is running.
385
+ This is unsafe when running multiple scripts at once.
386
+ :returns: A script object. The script starts running when you call run() on it, or await it.
387
+ """
388
+
389
+ def _execute(ctx: WrapAllErrors, module: ModuleType) -> None:
390
+ nonlocal script, kwargs
391
+
392
+ filename = kwargs.pop("filename", f"<runvpy {uuid4().hex[:8]}>")
393
+
394
+ with ctx, _TempModule(module.__name__, filename):
395
+ if isinstance(script, CodeType):
396
+ code = script
397
+ else:
398
+ compile_args: dict[str, Any] = {
399
+ "filename": filename,
400
+ "dont_inherit": True,
401
+ "flags": 0,
402
+ "mode": "exec",
403
+ } | kwargs
404
+ code = compile(script, **compile_args)
405
+
406
+ exec(code, module.__dict__, module.__dict__)
407
+
408
+ return _load(_execute, environment, module, inline, chdir)
409
+
410
+
411
+ def _load(
412
+ executor: Executor[None],
413
+ environment: Policy
414
+ | vs.Environment
415
+ | ManagedEnvironment
416
+ | Script[vs.Environment]
417
+ | Script[ManagedEnvironment]
418
+ | None,
419
+ module: str | ModuleType,
420
+ inline: bool,
421
+ chdir: str | os.PathLike[str] | None,
422
+ ) -> Script[Any]:
423
+ runner = inline_runner if inline else to_thread
424
+
425
+ if chdir is not None:
426
+ runner = chdir_runner(chdir, runner)
427
+
428
+ if isinstance(environment, Script):
429
+ module = environment.module
430
+ environment = environment.environment
431
+ elif isinstance(module, str):
432
+ module = ModuleType(module)
433
+
434
+ if environment is None:
435
+ environment = vs.get_current_environment()
436
+ elif isinstance(environment, vs.Environment):
437
+ return Script(executor, module, environment, runner)
438
+ elif isinstance(environment, Policy):
439
+ environment = environment.new_environment()
440
+
441
+ return Script[Any](executor, module, environment, runner)