vsjetengine 1.0.0__tar.gz → 1.2.0__tar.gz
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.
- {vsjetengine-1.0.0 → vsjetengine-1.2.0}/PKG-INFO +3 -3
- {vsjetengine-1.0.0 → vsjetengine-1.2.0}/README.md +1 -1
- {vsjetengine-1.0.0 → vsjetengine-1.2.0}/pyproject.toml +1 -1
- {vsjetengine-1.0.0 → vsjetengine-1.2.0}/tests/_testutils.py +4 -3
- {vsjetengine-1.0.0 → vsjetengine-1.2.0}/tests/conftest.py +19 -0
- {vsjetengine-1.0.0 → vsjetengine-1.2.0}/tests/test_hospice.py +2 -18
- {vsjetengine-1.0.0 → vsjetengine-1.2.0}/tests/test_policy.py +2 -1
- {vsjetengine-1.0.0 → vsjetengine-1.2.0}/tests/test_vpy.py +192 -6
- vsjetengine-1.2.0/vsengine/_version.py +2 -0
- {vsjetengine-1.0.0 → vsjetengine-1.2.0}/vsengine/vpy.py +93 -28
- vsjetengine-1.0.0/vsengine/_version.py +0 -2
- {vsjetengine-1.0.0 → vsjetengine-1.2.0}/.gitignore +0 -0
- {vsjetengine-1.0.0 → vsjetengine-1.2.0}/COPYING +0 -0
- {vsjetengine-1.0.0 → vsjetengine-1.2.0}/tests/__init__.py +0 -0
- {vsjetengine-1.0.0 → vsjetengine-1.2.0}/tests/fixtures/heuristic_examples.json +0 -0
- {vsjetengine-1.0.0 → vsjetengine-1.2.0}/tests/fixtures/test.vpy +0 -0
- {vsjetengine-1.0.0 → vsjetengine-1.2.0}/tests/test_futures.py +0 -0
- {vsjetengine-1.0.0 → vsjetengine-1.2.0}/tests/test_helpers.py +0 -0
- {vsjetengine-1.0.0 → vsjetengine-1.2.0}/tests/test_loop_adapters.py +0 -0
- {vsjetengine-1.0.0 → vsjetengine-1.2.0}/tests/test_loops.py +0 -0
- {vsjetengine-1.0.0 → vsjetengine-1.2.0}/tests/test_policy_store.py +0 -0
- {vsjetengine-1.0.0 → vsjetengine-1.2.0}/tests/test_video.py +0 -0
- {vsjetengine-1.0.0 → vsjetengine-1.2.0}/vsengine/__init__.py +0 -0
- {vsjetengine-1.0.0 → vsjetengine-1.2.0}/vsengine/_futures.py +0 -0
- {vsjetengine-1.0.0 → vsjetengine-1.2.0}/vsengine/_helpers.py +0 -0
- {vsjetengine-1.0.0 → vsjetengine-1.2.0}/vsengine/_hospice.py +0 -0
- {vsjetengine-1.0.0 → vsjetengine-1.2.0}/vsengine/_nodes.py +0 -0
- {vsjetengine-1.0.0 → vsjetengine-1.2.0}/vsengine/adapters/__init__.py +0 -0
- {vsjetengine-1.0.0 → vsjetengine-1.2.0}/vsengine/adapters/asyncio.py +0 -0
- {vsjetengine-1.0.0 → vsjetengine-1.2.0}/vsengine/adapters/trio.py +0 -0
- {vsjetengine-1.0.0 → vsjetengine-1.2.0}/vsengine/loops.py +0 -0
- {vsjetengine-1.0.0 → vsjetengine-1.2.0}/vsengine/policy.py +0 -0
- {vsjetengine-1.0.0 → vsjetengine-1.2.0}/vsengine/py.typed +0 -0
- {vsjetengine-1.0.0 → vsjetengine-1.2.0}/vsengine/video.py +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vsjetengine
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: An engine for vapoursynth previewers, renderers and script analyis tools.
|
|
5
|
-
Project-URL: Source Code, https://github.com/Jaded-Encoding-Thaumaturgy/vs-engine
|
|
5
|
+
Project-URL: Source Code, https://github.com/Jaded-Encoding-Thaumaturgy/vs-jet-engine
|
|
6
6
|
Project-URL: Contact, https://discord.gg/XTpc6Fa9eB
|
|
7
7
|
Author-email: cid-chan <cid+git@cid-chan.moe>
|
|
8
8
|
Maintainer-email: Vardë <ichunjo.le.terrible@gmail.com>, Jaded Encoding Thaumaturgy <jaded.encoding.thaumaturgy@gmail.com>
|
|
@@ -317,7 +317,7 @@ Description-Content-Type: text/markdown
|
|
|
317
317
|
[](https://github.com/Jaded-Encoding-Thaumaturgy/vs-engine/actions/workflows/test.yml)
|
|
318
318
|
[](https://coveralls.io/github/Jaded-Encoding-Thaumaturgy/vs-engine?branch=main)
|
|
319
319
|
|
|
320
|
-
An engine for vapoursynth previewers, renderers and script
|
|
320
|
+
An engine for vapoursynth previewers, renderers and script analysis tools.
|
|
321
321
|
|
|
322
322
|
## Installation
|
|
323
323
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://github.com/Jaded-Encoding-Thaumaturgy/vs-engine/actions/workflows/test.yml)
|
|
5
5
|
[](https://coveralls.io/github/Jaded-Encoding-Thaumaturgy/vs-engine?branch=main)
|
|
6
6
|
|
|
7
|
-
An engine for vapoursynth previewers, renderers and script
|
|
7
|
+
An engine for vapoursynth previewers, renderers and script analysis tools.
|
|
8
8
|
|
|
9
9
|
## Installation
|
|
10
10
|
|
|
@@ -30,7 +30,7 @@ dependencies = ["vapoursynth>=69"]
|
|
|
30
30
|
dynamic = ["version"]
|
|
31
31
|
|
|
32
32
|
[project.urls]
|
|
33
|
-
"Source Code" = "https://github.com/Jaded-Encoding-Thaumaturgy/vs-engine"
|
|
33
|
+
"Source Code" = "https://github.com/Jaded-Encoding-Thaumaturgy/vs-jet-engine"
|
|
34
34
|
"Contact" = "https://discord.gg/XTpc6Fa9eB"
|
|
35
35
|
|
|
36
36
|
[project.optional-dependencies]
|
|
@@ -94,17 +94,17 @@ class ProxyPolicy(EnvironmentPolicy):
|
|
|
94
94
|
|
|
95
95
|
def get_current_environment(self) -> EnvironmentData | None:
|
|
96
96
|
if self._policy is None:
|
|
97
|
-
|
|
97
|
+
return None
|
|
98
98
|
return self._policy.get_current_environment()
|
|
99
99
|
|
|
100
100
|
def set_environment(self, environment: EnvironmentData | None) -> EnvironmentData | None:
|
|
101
101
|
if self._policy is None:
|
|
102
|
-
|
|
102
|
+
return None
|
|
103
103
|
return self._policy.set_environment(environment)
|
|
104
104
|
|
|
105
105
|
def is_alive(self, environment: EnvironmentData) -> bool:
|
|
106
106
|
if self._policy is None:
|
|
107
|
-
|
|
107
|
+
return False
|
|
108
108
|
return self._policy.is_alive(environment)
|
|
109
109
|
|
|
110
110
|
|
|
@@ -123,6 +123,7 @@ class StandalonePolicy(EnvironmentPolicy):
|
|
|
123
123
|
|
|
124
124
|
def on_policy_cleared(self) -> None:
|
|
125
125
|
admit_environment(self._current, self._core)
|
|
126
|
+
self._api.destroy_environment(self._current)
|
|
126
127
|
|
|
127
128
|
del self._current
|
|
128
129
|
del self._core
|
|
@@ -12,6 +12,7 @@ from collections.abc import Iterator
|
|
|
12
12
|
import pytest
|
|
13
13
|
|
|
14
14
|
from tests._testutils import forcefully_unregister_policy
|
|
15
|
+
from vsengine import _hospice
|
|
15
16
|
from vsengine.loops import NO_LOOP, set_loop
|
|
16
17
|
|
|
17
18
|
|
|
@@ -29,3 +30,21 @@ def clean_policy() -> Iterator[None]:
|
|
|
29
30
|
yield
|
|
30
31
|
forcefully_unregister_policy()
|
|
31
32
|
set_loop(NO_LOOP)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.fixture(autouse=True)
|
|
36
|
+
def reset_hospice_state() -> Iterator[None]:
|
|
37
|
+
"""Reset hospice module state before each test to ensure isolation."""
|
|
38
|
+
_hospice.stage1.clear()
|
|
39
|
+
_hospice.stage2.clear()
|
|
40
|
+
_hospice.stage2_to_add.clear()
|
|
41
|
+
_hospice.hold.clear()
|
|
42
|
+
_hospice.cores.clear()
|
|
43
|
+
_hospice.refnanny.clear()
|
|
44
|
+
yield
|
|
45
|
+
_hospice.stage1.clear()
|
|
46
|
+
_hospice.stage2.clear()
|
|
47
|
+
_hospice.stage2_to_add.clear()
|
|
48
|
+
_hospice.hold.clear()
|
|
49
|
+
_hospice.cores.clear()
|
|
50
|
+
_hospice.refnanny.clear()
|
|
@@ -14,31 +14,15 @@ from typing import Any
|
|
|
14
14
|
|
|
15
15
|
import pytest
|
|
16
16
|
|
|
17
|
-
from vsengine import _hospice
|
|
18
17
|
from vsengine._hospice import admit_environment, any_alive, freeze, unfreeze
|
|
19
18
|
|
|
20
19
|
|
|
21
20
|
@pytest.fixture(autouse=True)
|
|
22
|
-
def
|
|
23
|
-
"""Reset
|
|
24
|
-
# Clear the mock timings registry
|
|
21
|
+
def reset_mock_registry() -> Iterator[None]:
|
|
22
|
+
"""Reset the mock timings registry before each test."""
|
|
25
23
|
_mock_timings_registry.clear()
|
|
26
|
-
# Clear all hospice state before test
|
|
27
|
-
_hospice.stage1.clear()
|
|
28
|
-
_hospice.stage2.clear()
|
|
29
|
-
_hospice.stage2_to_add.clear()
|
|
30
|
-
_hospice.hold.clear()
|
|
31
|
-
_hospice.cores.clear()
|
|
32
|
-
_hospice.refnanny.clear()
|
|
33
24
|
yield
|
|
34
|
-
# Clean up after test as well
|
|
35
25
|
_mock_timings_registry.clear()
|
|
36
|
-
_hospice.stage1.clear()
|
|
37
|
-
_hospice.stage2.clear()
|
|
38
|
-
_hospice.stage2_to_add.clear()
|
|
39
|
-
_hospice.hold.clear()
|
|
40
|
-
_hospice.cores.clear()
|
|
41
|
-
_hospice.refnanny.clear()
|
|
42
26
|
|
|
43
27
|
|
|
44
28
|
# Global registry to simulate CoreTimings holding references to cores
|
|
@@ -42,7 +42,8 @@ class TestPolicy:
|
|
|
42
42
|
|
|
43
43
|
def test_context_manager(self, policy: Policy) -> None:
|
|
44
44
|
with policy:
|
|
45
|
-
policy.api.create_environment()
|
|
45
|
+
env = policy.api.create_environment()
|
|
46
|
+
policy.api.destroy_environment(env)
|
|
46
47
|
|
|
47
48
|
with pytest.raises(RuntimeError):
|
|
48
49
|
policy.api.create_environment()
|
|
@@ -7,11 +7,16 @@
|
|
|
7
7
|
|
|
8
8
|
import ast
|
|
9
9
|
import contextlib
|
|
10
|
+
import gc
|
|
11
|
+
import logging
|
|
10
12
|
import os
|
|
13
|
+
import sys
|
|
11
14
|
import textwrap
|
|
12
15
|
import threading
|
|
13
16
|
import types
|
|
17
|
+
import weakref
|
|
14
18
|
from collections.abc import Callable, Iterator
|
|
19
|
+
from pathlib import Path
|
|
15
20
|
from typing import Any
|
|
16
21
|
|
|
17
22
|
import pytest
|
|
@@ -20,12 +25,14 @@ import vapoursynth
|
|
|
20
25
|
from tests._testutils import BLACKBOARD
|
|
21
26
|
from vsengine.adapters.asyncio import AsyncIOLoop
|
|
22
27
|
from vsengine.loops import set_loop
|
|
23
|
-
from vsengine.policy import GlobalStore, Policy
|
|
28
|
+
from vsengine.policy import GlobalStore, ManagedEnvironment, Policy
|
|
24
29
|
from vsengine.vpy import (
|
|
25
30
|
ExecutionError,
|
|
26
31
|
Script,
|
|
27
32
|
WrapAllErrors,
|
|
28
33
|
_load,
|
|
34
|
+
_ModifiedArgv0,
|
|
35
|
+
_ModifiedPath,
|
|
29
36
|
chdir_runner,
|
|
30
37
|
inline_runner,
|
|
31
38
|
load_code,
|
|
@@ -41,7 +48,7 @@ def noop() -> Iterator[None]:
|
|
|
41
48
|
yield
|
|
42
49
|
|
|
43
50
|
|
|
44
|
-
class
|
|
51
|
+
class VpyTestError(Exception):
|
|
45
52
|
pass
|
|
46
53
|
|
|
47
54
|
|
|
@@ -73,7 +80,7 @@ def test_run_executes_successfully() -> None:
|
|
|
73
80
|
def test_run_wraps_exception() -> None:
|
|
74
81
|
@callback_script
|
|
75
82
|
def test_code(_: types.ModuleType) -> None:
|
|
76
|
-
raise
|
|
83
|
+
raise VpyTestError()
|
|
77
84
|
|
|
78
85
|
with Policy(GlobalStore()) as p, p.new_environment() as env:
|
|
79
86
|
s = Script(test_code, types.ModuleType("__test__"), env.vs_environment, inline_runner)
|
|
@@ -81,7 +88,7 @@ def test_run_wraps_exception() -> None:
|
|
|
81
88
|
|
|
82
89
|
exc = fut.exception()
|
|
83
90
|
assert isinstance(exc, ExecutionError)
|
|
84
|
-
assert isinstance(exc.parent_error,
|
|
91
|
+
assert isinstance(exc.parent_error, VpyTestError)
|
|
85
92
|
|
|
86
93
|
|
|
87
94
|
def test_execute_resolves_immediately() -> None:
|
|
@@ -112,14 +119,14 @@ def test_execute_resolves_to_script() -> None:
|
|
|
112
119
|
def test_execute_resolves_immediately_when_raising() -> None:
|
|
113
120
|
@callback_script
|
|
114
121
|
def test_code(_: types.ModuleType) -> None:
|
|
115
|
-
raise
|
|
122
|
+
raise VpyTestError
|
|
116
123
|
|
|
117
124
|
with Policy(GlobalStore()) as p, p.new_environment() as env:
|
|
118
125
|
s = Script(test_code, types.ModuleType("__test__"), env.vs_environment, inline_runner)
|
|
119
126
|
try:
|
|
120
127
|
s.result()
|
|
121
128
|
except ExecutionError as err:
|
|
122
|
-
assert isinstance(err.parent_error,
|
|
129
|
+
assert isinstance(err.parent_error, VpyTestError)
|
|
123
130
|
except Exception as e:
|
|
124
131
|
pytest.fail(f"Wrong exception: {e!r}")
|
|
125
132
|
else:
|
|
@@ -391,3 +398,182 @@ def test_wrap_exceptions_wraps_exception() -> None:
|
|
|
391
398
|
assert e.parent_error is err
|
|
392
399
|
else:
|
|
393
400
|
pytest.fail("Wrap all errors swallowed the exception")
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
@pytest.mark.parametrize("fail", [False, True])
|
|
404
|
+
def test_dispose_clears_future(fail: bool) -> None:
|
|
405
|
+
code = "raise RuntimeError()" if fail else "pass"
|
|
406
|
+
with Policy(GlobalStore()) as p:
|
|
407
|
+
s = load_code(code, p)
|
|
408
|
+
with contextlib.suppress(ExecutionError):
|
|
409
|
+
s.result()
|
|
410
|
+
|
|
411
|
+
assert hasattr(s, "_future")
|
|
412
|
+
s.dispose()
|
|
413
|
+
assert not hasattr(s, "_future")
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def test_dispose_prevents_hospice_warning_on_error(caplog: pytest.LogCaptureFixture) -> None:
|
|
417
|
+
@callback_script
|
|
418
|
+
def test_code(mod: types.ModuleType) -> None:
|
|
419
|
+
import vapoursynth
|
|
420
|
+
|
|
421
|
+
setattr(mod, "leak", vapoursynth.core.std.BlankClip())
|
|
422
|
+
raise VpyTestError()
|
|
423
|
+
|
|
424
|
+
with Policy(GlobalStore()) as p:
|
|
425
|
+
s = Script(test_code, types.ModuleType("__test__"), p.new_environment(), inline_runner)
|
|
426
|
+
with pytest.raises(ExecutionError):
|
|
427
|
+
s.result()
|
|
428
|
+
|
|
429
|
+
s.dispose()
|
|
430
|
+
|
|
431
|
+
with caplog.at_level(logging.WARNING, logger="vsengine._hospice"):
|
|
432
|
+
for _ in range(5):
|
|
433
|
+
gc.collect()
|
|
434
|
+
|
|
435
|
+
assert not any("Core is still in use" in r.message for r in caplog.records if "vsengine._hospice" in r.name)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def test_hospice_warns_if_future_not_deleted(caplog: pytest.LogCaptureFixture) -> None:
|
|
439
|
+
@callback_script
|
|
440
|
+
def test_code(mod: types.ModuleType) -> None:
|
|
441
|
+
import vapoursynth
|
|
442
|
+
|
|
443
|
+
setattr(mod, "clip", vapoursynth.core.std.BlankClip())
|
|
444
|
+
raise VpyTestError()
|
|
445
|
+
|
|
446
|
+
with Policy(GlobalStore()) as p:
|
|
447
|
+
env = p.new_environment()
|
|
448
|
+
s = Script(test_code, types.ModuleType("__test__"), env, inline_runner)
|
|
449
|
+
fut = s.run()
|
|
450
|
+
with pytest.raises(ExecutionError):
|
|
451
|
+
fut.result()
|
|
452
|
+
|
|
453
|
+
# Monkeypatch dispose to NOT delete the future and NOT clear module dict
|
|
454
|
+
original_dispose = s.dispose
|
|
455
|
+
|
|
456
|
+
def leaky_dispose() -> None:
|
|
457
|
+
# We ONLY dispose the environment, skipping del self._future and module.clear()
|
|
458
|
+
if isinstance(s.environment, ManagedEnvironment):
|
|
459
|
+
s.environment.dispose()
|
|
460
|
+
|
|
461
|
+
setattr(s, "dispose", leaky_dispose)
|
|
462
|
+
|
|
463
|
+
try:
|
|
464
|
+
# Clear local ref to future. It should still be held by s._future
|
|
465
|
+
del fut
|
|
466
|
+
|
|
467
|
+
s.dispose()
|
|
468
|
+
|
|
469
|
+
# Trigger hospice collection
|
|
470
|
+
with caplog.at_level(logging.WARNING, logger="vsengine._hospice"):
|
|
471
|
+
for _ in range(10):
|
|
472
|
+
gc.collect()
|
|
473
|
+
|
|
474
|
+
# There should be a warning from hospice now because s._future still exists
|
|
475
|
+
# and is holding onto the traceback, which holds onto the module, which holds the core (via mod.clip)
|
|
476
|
+
assert any("Core is still in use" in r.message for r in caplog.records)
|
|
477
|
+
finally:
|
|
478
|
+
setattr(s, "dispose", original_dispose)
|
|
479
|
+
s.module.__dict__.clear()
|
|
480
|
+
with contextlib.suppress(AttributeError):
|
|
481
|
+
del s._future
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def test_script_is_collectible_after_dispose() -> None:
|
|
485
|
+
def _run_and_dispose() -> weakref.ReferenceType[Script[Any]]:
|
|
486
|
+
with Policy(GlobalStore()) as p:
|
|
487
|
+
s = load_code("raise RuntimeError()", p)
|
|
488
|
+
s.run()
|
|
489
|
+
s_ref = weakref.ref(s)
|
|
490
|
+
s.dispose()
|
|
491
|
+
return s_ref
|
|
492
|
+
|
|
493
|
+
s_ref = _run_and_dispose()
|
|
494
|
+
|
|
495
|
+
for _ in range(5):
|
|
496
|
+
gc.collect()
|
|
497
|
+
|
|
498
|
+
assert s_ref() is None
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def test_modified_path() -> None:
|
|
502
|
+
original_path = sys.path.copy()
|
|
503
|
+
test_path = os.path.abspath("test_path_dummy")
|
|
504
|
+
with _ModifiedPath(test_path):
|
|
505
|
+
assert sys.path[0] == test_path
|
|
506
|
+
assert test_path in sys.path
|
|
507
|
+
assert sys.path == original_path
|
|
508
|
+
assert test_path not in sys.path
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def test_modified_argv0() -> None:
|
|
512
|
+
original_argv0 = sys.argv[0]
|
|
513
|
+
test_argv0 = "test_script_argv0.py"
|
|
514
|
+
with _ModifiedArgv0(test_argv0):
|
|
515
|
+
assert sys.argv[0] == test_argv0
|
|
516
|
+
assert sys.argv[0] == original_argv0
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def test_load_script_dunder_attributes(tmp_path: Path) -> None:
|
|
520
|
+
script_path = tmp_path / "test_script.vpy"
|
|
521
|
+
script_path.write_text(
|
|
522
|
+
"from tests._testutils import BLACKBOARD; "
|
|
523
|
+
"BLACKBOARD['script_dunders'] = {k: v for k, v in globals().items() if k.startswith('__')}"
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
BLACKBOARD.clear()
|
|
527
|
+
|
|
528
|
+
with Policy(GlobalStore()) as p, p.new_environment() as env, env.use():
|
|
529
|
+
load_script(script_path).result()
|
|
530
|
+
|
|
531
|
+
dunders = BLACKBOARD["script_dunders"]
|
|
532
|
+
assert dunders["__file__"] == os.path.abspath(os.path.normpath(str(script_path)))
|
|
533
|
+
assert dunders["__name__"] == "__vapoursynth__"
|
|
534
|
+
assert "__cached__" in dunders
|
|
535
|
+
assert "__doc__" in dunders
|
|
536
|
+
assert "__loader__" in dunders
|
|
537
|
+
assert "__package__" in dunders
|
|
538
|
+
assert "__spec__" in dunders
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def test_load_code_dunder_attributes() -> None:
|
|
542
|
+
code = (
|
|
543
|
+
"from tests._testutils import BLACKBOARD; "
|
|
544
|
+
"BLACKBOARD['code_dunders'] = {k: v for k, v in globals().items() if k.startswith('__')}"
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
BLACKBOARD.clear()
|
|
548
|
+
filename = "test_code_attributes.py"
|
|
549
|
+
# Create the file so resolve works as expected if it exists check is used
|
|
550
|
+
Path(filename).touch()
|
|
551
|
+
try:
|
|
552
|
+
with Policy(GlobalStore()) as p, p.new_environment() as env, env.use():
|
|
553
|
+
load_code(code, filename=filename).result()
|
|
554
|
+
|
|
555
|
+
dunders = BLACKBOARD["code_dunders"]
|
|
556
|
+
assert dunders["__file__"] == os.path.abspath(os.path.normpath(filename))
|
|
557
|
+
assert dunders["__name__"] == "__vapoursynth__"
|
|
558
|
+
finally:
|
|
559
|
+
if os.path.exists(filename):
|
|
560
|
+
os.remove(filename)
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def test_load_script_adds_to_path(tmp_path: Path) -> None:
|
|
564
|
+
# Create a submodule in a temp directory
|
|
565
|
+
sub_dir = tmp_path / "my_module"
|
|
566
|
+
sub_dir.mkdir()
|
|
567
|
+
(sub_dir / "__init__.py").write_text("VAL = 42")
|
|
568
|
+
|
|
569
|
+
script_path = tmp_path / "main_script.vpy"
|
|
570
|
+
script_path.write_text(
|
|
571
|
+
"from tests._testutils import BLACKBOARD; import my_module; BLACKBOARD['module_val'] = my_module.VAL"
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
BLACKBOARD.clear()
|
|
575
|
+
|
|
576
|
+
with Policy(GlobalStore()) as p, p.new_environment() as env, env.use():
|
|
577
|
+
load_script(script_path).result()
|
|
578
|
+
|
|
579
|
+
assert BLACKBOARD["module_val"] == 42
|
|
@@ -13,7 +13,7 @@ import os
|
|
|
13
13
|
import sys
|
|
14
14
|
from collections.abc import Awaitable, Buffer, Callable, Generator
|
|
15
15
|
from concurrent.futures import Future
|
|
16
|
-
from contextlib import AbstractContextManager
|
|
16
|
+
from contextlib import AbstractContextManager, suppress
|
|
17
17
|
from types import CodeType, ModuleType, TracebackType
|
|
18
18
|
from typing import Any, Concatenate, Self, overload
|
|
19
19
|
from uuid import uuid4
|
|
@@ -85,10 +85,9 @@ class _TempModule(AbstractContextManager[None]):
|
|
|
85
85
|
That ensures the module is available in sys.modules during execution and restored/cleaned up afterwards.
|
|
86
86
|
"""
|
|
87
87
|
|
|
88
|
-
def __init__(self, mod_name: str,
|
|
88
|
+
def __init__(self, mod_name: str, module: ModuleType | None = None) -> None:
|
|
89
89
|
self.mod_name = mod_name
|
|
90
|
-
self.module = ModuleType(mod_name)
|
|
91
|
-
self.module.__dict__["__file__"] = filename
|
|
90
|
+
self.module = module or ModuleType(mod_name)
|
|
92
91
|
self._saved_module = list[ModuleType | None]()
|
|
93
92
|
|
|
94
93
|
def __enter__(self) -> None:
|
|
@@ -107,6 +106,56 @@ class _TempModule(AbstractContextManager[None]):
|
|
|
107
106
|
del sys.modules[self.mod_name]
|
|
108
107
|
|
|
109
108
|
|
|
109
|
+
class _ModifiedPath(AbstractContextManager[None]):
|
|
110
|
+
"""
|
|
111
|
+
Temporarily add a path to sys.path.
|
|
112
|
+
|
|
113
|
+
Ported from runpy.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
def __init__(self, path: str | os.PathLike[str]) -> None:
|
|
117
|
+
self.path = os.path.abspath(path)
|
|
118
|
+
|
|
119
|
+
def __enter__(self) -> None:
|
|
120
|
+
sys.path.insert(0, self.path)
|
|
121
|
+
|
|
122
|
+
def __exit__(self, exc: type[BaseException] | None, val: BaseException | None, tb: TracebackType | None) -> None:
|
|
123
|
+
with suppress(ValueError):
|
|
124
|
+
sys.path.remove(self.path)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class _ModifiedArgv0(AbstractContextManager[None]):
|
|
128
|
+
"""
|
|
129
|
+
Temporarily modify sys.argv[0].
|
|
130
|
+
|
|
131
|
+
Ported from runpy.
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
def __init__(self, value: Any) -> None:
|
|
135
|
+
self.value = str(value)
|
|
136
|
+
self._saved_value = ""
|
|
137
|
+
|
|
138
|
+
def __enter__(self) -> None:
|
|
139
|
+
self._saved_value = sys.argv[0]
|
|
140
|
+
sys.argv[0] = self.value
|
|
141
|
+
|
|
142
|
+
def __exit__(self, exc: type[BaseException] | None, val: BaseException | None, tb: TracebackType | None) -> None:
|
|
143
|
+
sys.argv[0] = self._saved_value
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _set_module_dunders(module: ModuleType, filename: str) -> None:
|
|
147
|
+
for key, value in {
|
|
148
|
+
"__name__": module.__name__,
|
|
149
|
+
"__file__": filename,
|
|
150
|
+
"__cached__": None,
|
|
151
|
+
"__doc__": None,
|
|
152
|
+
"__loader__": None,
|
|
153
|
+
"__package__": None,
|
|
154
|
+
"__spec__": None,
|
|
155
|
+
}.items():
|
|
156
|
+
module.__dict__.setdefault(key, value)
|
|
157
|
+
|
|
158
|
+
|
|
110
159
|
def inline_runner[T](func: Callable[[], T]) -> Future[T]:
|
|
111
160
|
"""
|
|
112
161
|
Runs a function inline and returns the result as a Future.
|
|
@@ -208,6 +257,7 @@ class Script[EnvT: (vs.Environment, ManagedEnvironment)](AbstractContextManager[
|
|
|
208
257
|
|
|
209
258
|
def dispose(self) -> None:
|
|
210
259
|
"""Disposes the managed environment and clears the module globals."""
|
|
260
|
+
self._del_future_refs()
|
|
211
261
|
self.module.__dict__.clear()
|
|
212
262
|
|
|
213
263
|
if isinstance(self.environment, ManagedEnvironment):
|
|
@@ -236,6 +286,14 @@ class Script[EnvT: (vs.Environment, ManagedEnvironment)](AbstractContextManager[
|
|
|
236
286
|
with self.environment.use():
|
|
237
287
|
self.executor(WrapAllErrors(), self.module)
|
|
238
288
|
|
|
289
|
+
def _del_future_refs(self) -> None:
|
|
290
|
+
if (fut := getattr(self, "_future", None)) is not None:
|
|
291
|
+
if fut.done() and not fut.cancelled() and (exc := fut.exception()):
|
|
292
|
+
exc.__traceback__ = None
|
|
293
|
+
if isinstance(exc, ExecutionError) and (parent := exc.parent_error):
|
|
294
|
+
parent.__traceback__ = None
|
|
295
|
+
del self._future
|
|
296
|
+
|
|
239
297
|
|
|
240
298
|
@overload
|
|
241
299
|
def load_script(
|
|
@@ -300,18 +358,22 @@ def load_script(
|
|
|
300
358
|
This is unsafe when running multiple scripts at once.
|
|
301
359
|
:returns: A script object. The script starts running when you call run() on it, or await it.
|
|
302
360
|
"""
|
|
361
|
+
rscript = os.path.abspath(os.path.normpath(str(script)))
|
|
303
362
|
|
|
304
363
|
def _execute(ctx: WrapAllErrors, module: ModuleType) -> None:
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
)
|
|
364
|
+
path = os.path.dirname(rscript)
|
|
365
|
+
|
|
366
|
+
with (
|
|
367
|
+
ctx,
|
|
368
|
+
_TempModule(module.__name__, module),
|
|
369
|
+
_ModifiedPath(path),
|
|
370
|
+
_ModifiedArgv0(rscript),
|
|
371
|
+
io.open_code(rscript) as f,
|
|
372
|
+
):
|
|
373
|
+
_set_module_dunders(module, rscript)
|
|
374
|
+
|
|
375
|
+
code = compile(f.read(), filename=rscript, dont_inherit=True, flags=0, mode="exec")
|
|
376
|
+
exec(code, module.__dict__, module.__dict__)
|
|
315
377
|
|
|
316
378
|
return _load(_execute, environment, module, inline, chdir)
|
|
317
379
|
|
|
@@ -384,25 +446,28 @@ def load_code(
|
|
|
384
446
|
:param chdir: Change the currently running directory while the script is running.
|
|
385
447
|
This is unsafe when running multiple scripts at once.
|
|
386
448
|
:returns: A script object. The script starts running when you call run() on it, or await it.
|
|
449
|
+
:kwargs: Arguments to pass to compile().
|
|
387
450
|
"""
|
|
451
|
+
if "filename" in kwargs:
|
|
452
|
+
kwargs["filename"] = os.path.abspath(os.path.normpath(str(kwargs["filename"])))
|
|
388
453
|
|
|
389
454
|
def _execute(ctx: WrapAllErrors, module: ModuleType) -> None:
|
|
390
|
-
nonlocal script, kwargs
|
|
391
|
-
|
|
392
455
|
filename = kwargs.pop("filename", f"<runvpy {uuid4().hex[:8]}>")
|
|
456
|
+
path = os.path.dirname(filename) if os.path.exists(filename) else None
|
|
457
|
+
|
|
458
|
+
with (
|
|
459
|
+
ctx,
|
|
460
|
+
_TempModule(module.__name__, module),
|
|
461
|
+
_ModifiedPath(path) if path else ctx,
|
|
462
|
+
_ModifiedArgv0(filename) if path else ctx,
|
|
463
|
+
):
|
|
464
|
+
code = (
|
|
465
|
+
compile(script, **{"filename": filename, "dont_inherit": True, "flags": 0, "mode": "exec"} | kwargs)
|
|
466
|
+
if not isinstance(script, CodeType)
|
|
467
|
+
else script
|
|
468
|
+
)
|
|
393
469
|
|
|
394
|
-
|
|
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
|
-
|
|
470
|
+
_set_module_dunders(module, filename)
|
|
406
471
|
exec(code, module.__dict__, module.__dict__)
|
|
407
472
|
|
|
408
473
|
return _load(_execute, environment, module, inline, chdir)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|