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.
Files changed (34) hide show
  1. {vsjetengine-1.0.0 → vsjetengine-1.2.0}/PKG-INFO +3 -3
  2. {vsjetengine-1.0.0 → vsjetengine-1.2.0}/README.md +1 -1
  3. {vsjetengine-1.0.0 → vsjetengine-1.2.0}/pyproject.toml +1 -1
  4. {vsjetengine-1.0.0 → vsjetengine-1.2.0}/tests/_testutils.py +4 -3
  5. {vsjetengine-1.0.0 → vsjetengine-1.2.0}/tests/conftest.py +19 -0
  6. {vsjetengine-1.0.0 → vsjetengine-1.2.0}/tests/test_hospice.py +2 -18
  7. {vsjetengine-1.0.0 → vsjetengine-1.2.0}/tests/test_policy.py +2 -1
  8. {vsjetengine-1.0.0 → vsjetengine-1.2.0}/tests/test_vpy.py +192 -6
  9. vsjetengine-1.2.0/vsengine/_version.py +2 -0
  10. {vsjetengine-1.0.0 → vsjetengine-1.2.0}/vsengine/vpy.py +93 -28
  11. vsjetengine-1.0.0/vsengine/_version.py +0 -2
  12. {vsjetengine-1.0.0 → vsjetengine-1.2.0}/.gitignore +0 -0
  13. {vsjetengine-1.0.0 → vsjetengine-1.2.0}/COPYING +0 -0
  14. {vsjetengine-1.0.0 → vsjetengine-1.2.0}/tests/__init__.py +0 -0
  15. {vsjetengine-1.0.0 → vsjetengine-1.2.0}/tests/fixtures/heuristic_examples.json +0 -0
  16. {vsjetengine-1.0.0 → vsjetengine-1.2.0}/tests/fixtures/test.vpy +0 -0
  17. {vsjetengine-1.0.0 → vsjetengine-1.2.0}/tests/test_futures.py +0 -0
  18. {vsjetengine-1.0.0 → vsjetengine-1.2.0}/tests/test_helpers.py +0 -0
  19. {vsjetengine-1.0.0 → vsjetengine-1.2.0}/tests/test_loop_adapters.py +0 -0
  20. {vsjetengine-1.0.0 → vsjetengine-1.2.0}/tests/test_loops.py +0 -0
  21. {vsjetengine-1.0.0 → vsjetengine-1.2.0}/tests/test_policy_store.py +0 -0
  22. {vsjetengine-1.0.0 → vsjetengine-1.2.0}/tests/test_video.py +0 -0
  23. {vsjetengine-1.0.0 → vsjetengine-1.2.0}/vsengine/__init__.py +0 -0
  24. {vsjetengine-1.0.0 → vsjetengine-1.2.0}/vsengine/_futures.py +0 -0
  25. {vsjetengine-1.0.0 → vsjetengine-1.2.0}/vsengine/_helpers.py +0 -0
  26. {vsjetengine-1.0.0 → vsjetengine-1.2.0}/vsengine/_hospice.py +0 -0
  27. {vsjetengine-1.0.0 → vsjetengine-1.2.0}/vsengine/_nodes.py +0 -0
  28. {vsjetengine-1.0.0 → vsjetengine-1.2.0}/vsengine/adapters/__init__.py +0 -0
  29. {vsjetengine-1.0.0 → vsjetengine-1.2.0}/vsengine/adapters/asyncio.py +0 -0
  30. {vsjetengine-1.0.0 → vsjetengine-1.2.0}/vsengine/adapters/trio.py +0 -0
  31. {vsjetengine-1.0.0 → vsjetengine-1.2.0}/vsengine/loops.py +0 -0
  32. {vsjetengine-1.0.0 → vsjetengine-1.2.0}/vsengine/policy.py +0 -0
  33. {vsjetengine-1.0.0 → vsjetengine-1.2.0}/vsengine/py.typed +0 -0
  34. {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.0.0
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
  [![Tests](https://github.com/Jaded-Encoding-Thaumaturgy/vs-engine/actions/workflows/test.yml/badge.svg)](https://github.com/Jaded-Encoding-Thaumaturgy/vs-engine/actions/workflows/test.yml)
318
318
  [![Coverage Status](https://coveralls.io/repos/github/Jaded-Encoding-Thaumaturgy/vs-engine/badge.svg?branch=main)](https://coveralls.io/github/Jaded-Encoding-Thaumaturgy/vs-engine?branch=main)
319
319
 
320
- An engine for vapoursynth previewers, renderers and script analyis tools.
320
+ An engine for vapoursynth previewers, renderers and script analysis tools.
321
321
 
322
322
  ## Installation
323
323
 
@@ -4,7 +4,7 @@
4
4
  [![Tests](https://github.com/Jaded-Encoding-Thaumaturgy/vs-engine/actions/workflows/test.yml/badge.svg)](https://github.com/Jaded-Encoding-Thaumaturgy/vs-engine/actions/workflows/test.yml)
5
5
  [![Coverage Status](https://coveralls.io/repos/github/Jaded-Encoding-Thaumaturgy/vs-engine/badge.svg?branch=main)](https://coveralls.io/github/Jaded-Encoding-Thaumaturgy/vs-engine?branch=main)
6
6
 
7
- An engine for vapoursynth previewers, renderers and script analyis tools.
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
- raise RuntimeError("This proxy is not attached to a policy.")
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
- raise RuntimeError("This proxy is not attached to a policy.")
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
- raise RuntimeError("This proxy is not attached to a policy.")
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 reset_hospice_state() -> Iterator[None]:
23
- """Reset hospice module state before each test to ensure isolation."""
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 TestError(Exception):
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 TestError()
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, TestError)
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 TestError
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, TestError)
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
@@ -0,0 +1,2 @@
1
+ __version__ = "1.2.0"
2
+ __version_tuple__ = (1, 2, 0)
@@ -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, filename: str) -> None:
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
- 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
- )
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
- 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
-
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)
@@ -1,2 +0,0 @@
1
- __version__ = "1.0.0"
2
- __version_tuple__ = (1, 0, 0)
File without changes
File without changes