vsjetengine 1.6.2__tar.gz → 1.6.3__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 (36) hide show
  1. {vsjetengine-1.6.2 → vsjetengine-1.6.3}/PKG-INFO +1 -1
  2. {vsjetengine-1.6.2 → vsjetengine-1.6.3}/tests/test_futures.py +72 -0
  3. vsjetengine-1.6.3/vsengine/_version.py +2 -0
  4. {vsjetengine-1.6.2 → vsjetengine-1.6.3}/vsengine/futures.py +67 -35
  5. vsjetengine-1.6.2/vsengine/_version.py +0 -2
  6. {vsjetengine-1.6.2 → vsjetengine-1.6.3}/.gitignore +0 -0
  7. {vsjetengine-1.6.2 → vsjetengine-1.6.3}/COPYING +0 -0
  8. {vsjetengine-1.6.2 → vsjetengine-1.6.3}/README.md +0 -0
  9. {vsjetengine-1.6.2 → vsjetengine-1.6.3}/pyproject.toml +0 -0
  10. {vsjetengine-1.6.2 → vsjetengine-1.6.3}/tests/__init__.py +0 -0
  11. {vsjetengine-1.6.2 → vsjetengine-1.6.3}/tests/_testutils.py +0 -0
  12. {vsjetengine-1.6.2 → vsjetengine-1.6.3}/tests/conftest.py +0 -0
  13. {vsjetengine-1.6.2 → vsjetengine-1.6.3}/tests/fixtures/heuristic_examples.json +0 -0
  14. {vsjetengine-1.6.2 → vsjetengine-1.6.3}/tests/fixtures/test.vpy +0 -0
  15. {vsjetengine-1.6.2 → vsjetengine-1.6.3}/tests/test_helpers.py +0 -0
  16. {vsjetengine-1.6.2 → vsjetengine-1.6.3}/tests/test_hospice.py +0 -0
  17. {vsjetengine-1.6.2 → vsjetengine-1.6.3}/tests/test_loop_adapters.py +0 -0
  18. {vsjetengine-1.6.2 → vsjetengine-1.6.3}/tests/test_loops.py +0 -0
  19. {vsjetengine-1.6.2 → vsjetengine-1.6.3}/tests/test_policy.py +0 -0
  20. {vsjetengine-1.6.2 → vsjetengine-1.6.3}/tests/test_policy_store.py +0 -0
  21. {vsjetengine-1.6.2 → vsjetengine-1.6.3}/tests/test_pytest_plugin.py +0 -0
  22. {vsjetengine-1.6.2 → vsjetengine-1.6.3}/tests/test_video.py +0 -0
  23. {vsjetengine-1.6.2 → vsjetengine-1.6.3}/tests/test_vpy.py +0 -0
  24. {vsjetengine-1.6.2 → vsjetengine-1.6.3}/vsengine/__init__.py +0 -0
  25. {vsjetengine-1.6.2 → vsjetengine-1.6.3}/vsengine/_helpers.py +0 -0
  26. {vsjetengine-1.6.2 → vsjetengine-1.6.3}/vsengine/_hospice.py +0 -0
  27. {vsjetengine-1.6.2 → vsjetengine-1.6.3}/vsengine/_nodes.py +0 -0
  28. {vsjetengine-1.6.2 → vsjetengine-1.6.3}/vsengine/adapters/__init__.py +0 -0
  29. {vsjetengine-1.6.2 → vsjetengine-1.6.3}/vsengine/adapters/asyncio.py +0 -0
  30. {vsjetengine-1.6.2 → vsjetengine-1.6.3}/vsengine/adapters/trio.py +0 -0
  31. {vsjetengine-1.6.2 → vsjetengine-1.6.3}/vsengine/loops.py +0 -0
  32. {vsjetengine-1.6.2 → vsjetengine-1.6.3}/vsengine/policy.py +0 -0
  33. {vsjetengine-1.6.2 → vsjetengine-1.6.3}/vsengine/py.typed +0 -0
  34. {vsjetengine-1.6.2 → vsjetengine-1.6.3}/vsengine/pytest.py +0 -0
  35. {vsjetengine-1.6.2 → vsjetengine-1.6.3}/vsengine/video.py +0 -0
  36. {vsjetengine-1.6.2 → vsjetengine-1.6.3}/vsengine/vpy.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: vsjetengine
3
- Version: 1.6.2
3
+ Version: 1.6.3
4
4
  Summary: An engine for vapoursynth previewers, renderers and script analyis tools.
5
5
  Project-URL: Source Code, https://github.com/Jaded-Encoding-Thaumaturgy/vs-jet-engine
6
6
  Project-URL: Contact, https://discord.gg/XTpc6Fa9eB
@@ -5,6 +5,7 @@
5
5
  # SPDX-License-Identifier: EUPL-1.2
6
6
  """Tests for the unified future system."""
7
7
 
8
+ import asyncio
8
9
  import contextlib
9
10
  import threading
10
11
  from collections.abc import AsyncGenerator, Generator, Iterator
@@ -741,3 +742,74 @@ def test_unified_future_from_future_cancellation() -> None:
741
742
  assert uni.cancel() is True
742
743
  assert uni.cancelled() is True
743
744
  assert raw.cancelled() is True
745
+
746
+
747
+ @pytest.mark.asyncio
748
+ async def test_unified_future_add_loop_callback_cancellation_propagation() -> None:
749
+ set_loop(AsyncIOLoop())
750
+ raw = UnifiedFuture[int]()
751
+ chained = raw.add_loop_callback(lambda f: None)
752
+
753
+ assert chained.cancel() is True
754
+ assert chained.cancelled() is True
755
+ assert raw.cancelled() is True
756
+
757
+
758
+ @pytest.mark.asyncio
759
+ async def test_unified_future_add_loop_callback_custom_on_cancel() -> None:
760
+ set_loop(AsyncIOLoop())
761
+ cancelled_calls = list[threading.Thread]()
762
+ loop_thread = threading.current_thread()
763
+ raw = UnifiedFuture[int]()
764
+ chained = raw.add_loop_callback(
765
+ lambda f: None,
766
+ cancel_cb=lambda: cancelled_calls.append(threading.current_thread()),
767
+ )
768
+
769
+ assert chained.cancel() is True
770
+ assert chained.cancelled() is True
771
+ await asyncio.sleep(0.01)
772
+ assert cancelled_calls == [loop_thread]
773
+ assert raw.cancelled() is False
774
+
775
+
776
+ @pytest.mark.asyncio
777
+ async def test_unified_future_then_custom_on_cancel_on_loop() -> None:
778
+ set_loop(AsyncIOLoop())
779
+ cancelled_calls = list[threading.Thread]()
780
+ loop_thread = threading.current_thread()
781
+ raw = UnifiedFuture[int]()
782
+ chained = raw.then(
783
+ lambda x: x * 2,
784
+ cancel_cb=lambda: cancelled_calls.append(threading.current_thread()),
785
+ on_loop=True,
786
+ )
787
+
788
+ # Cancel from a background thread
789
+ t = threading.Thread(target=chained.cancel)
790
+ t.start()
791
+ t.join()
792
+
793
+ await asyncio.sleep(0.01)
794
+ assert chained.cancelled() is True
795
+ assert cancelled_calls == [loop_thread]
796
+ assert raw.cancelled() is False
797
+
798
+
799
+ @pytest.mark.asyncio
800
+ async def test_unified_future_add_loop_callback_parent_cancelled() -> None:
801
+ set_loop(AsyncIOLoop())
802
+ raw = UnifiedFuture[int]()
803
+ called = list[bool]()
804
+
805
+ def cb(f: Future[int]) -> None:
806
+ called.append(f.cancelled())
807
+
808
+ chained = raw.add_loop_callback(cb)
809
+ raw.cancel()
810
+
811
+ with pytest.raises(asyncio.CancelledError):
812
+ await chained
813
+
814
+ assert chained.cancelled() is True
815
+ assert called == [True]
@@ -0,0 +1,2 @@
1
+ __version__ = "1.6.3"
2
+ __version_tuple__ = (1, 6, 3)
@@ -127,7 +127,11 @@ class UnifiedFuture[T](Future[T], AbstractContextManager[Any], AbstractAsyncCont
127
127
  # The done_callback should inherit the environment of the current call.
128
128
  super().add_done_callback(keep_environment(fn))
129
129
 
130
- def add_loop_callback(self, func: Callable[[Future[T]], Any]) -> UnifiedFuture[T]:
130
+ def add_loop_callback(
131
+ self,
132
+ func: Callable[[Future[T]], Any],
133
+ cancel_cb: Callable[[], None] | None = None,
134
+ ) -> UnifiedFuture[T]:
131
135
  """
132
136
  Register a callback that is guaranteed to run on the event-loop thread.
133
137
 
@@ -135,25 +139,28 @@ class UnifiedFuture[T](Future[T], AbstractContextManager[Any], AbstractAsyncCont
135
139
  this method marshals `func` back to the main event loop via `EventLoop.from_thread`.
136
140
 
137
141
  :param func: A callable that receives the completed future.
142
+ :param cancel_cb: Optional callback invoked when the returned future is cancelled.
143
+ Defaults to cancelling the parent future.
138
144
  :return: A UnifiedFuture that resolves to the original future's result once the callback completes
139
145
  on the event loop.
140
146
  """
141
147
  result = UnifiedFuture[T]()
142
148
 
143
- def _wrapper(future: Future[T]) -> None:
144
- loop_fut = get_loop().from_thread(func, future)
145
-
146
- def _forward(lf: Future[Any]) -> None:
147
- if (exc := lf.exception()) is not None:
148
- result.set_exception(exc)
149
- elif (orig_exc := future.exception()) is not None:
150
- result.set_exception(orig_exc)
151
- else:
152
- result.set_result(future.result())
149
+ def forward_parent(_: Future[Any]) -> None:
150
+ if self.cancelled():
151
+ result.cancel()
152
+ elif (orig_exc := self.exception()) is not None:
153
+ result.set_exception(orig_exc)
154
+ else:
155
+ result.set_result(self.result())
153
156
 
154
- loop_fut.add_done_callback(_forward)
157
+ def wrapper(future: Future[T]) -> None:
158
+ if result.cancelled():
159
+ return
160
+ self._forward_loop_task(get_loop().from_thread(func, future), result, forward_parent)
155
161
 
156
- self.add_done_callback(_wrapper)
162
+ self.add_done_callback(wrapper)
163
+ self._link_cancellation(result, cancel_cb, on_loop=True)
157
164
  return result
158
165
 
159
166
  # Manipulating futures
@@ -227,23 +234,11 @@ class UnifiedFuture[T](Future[T], AbstractContextManager[Any], AbstractAsyncCont
227
234
  """
228
235
  result = UnifiedFuture[Any]()
229
236
 
230
- def _run_cb(cb: Callable[[Any], Any], v: T | BaseException) -> None:
237
+ def run_cb(cb: Callable[[Any], Any], v: T | BaseException) -> None:
231
238
  if result.cancelled():
232
239
  return
233
240
  if on_loop:
234
- loop_fut = get_loop().from_thread(cb, v)
235
-
236
- def _forward(lf: Future[Any]) -> None:
237
- if result.cancelled():
238
- return
239
- if lf.cancelled():
240
- result.cancel()
241
- elif (exc := lf.exception()) is not None:
242
- result.set_exception(exc)
243
- else:
244
- result.set_result(lf.result())
245
-
246
- loop_fut.add_done_callback(_forward)
241
+ self._forward_loop_task(get_loop().from_thread(cb, v), result)
247
242
  else:
248
243
  try:
249
244
  r = cb(v)
@@ -252,26 +247,23 @@ class UnifiedFuture[T](Future[T], AbstractContextManager[Any], AbstractAsyncCont
252
247
  else:
253
248
  result.set_result(r)
254
249
 
255
- def _done(fn: Future[T]) -> None:
250
+ def done(_: Future[T]) -> None:
256
251
  if self.cancelled():
257
252
  result.cancel()
258
253
  return
259
254
  if (exc := self.exception()) is not None:
260
255
  if err_cb is not None:
261
- _run_cb(err_cb, exc)
256
+ run_cb(err_cb, exc)
262
257
  else:
263
258
  result.set_exception(exc)
264
259
  elif not result.cancelled():
265
260
  if success_cb is not None:
266
- _run_cb(success_cb, self.result())
261
+ run_cb(success_cb, self.result())
267
262
  else:
268
263
  result.set_result(self.result())
269
264
 
270
- self.add_done_callback(_done)
271
- if cancel_cb is not None:
272
- result.add_done_callback(lambda f: cancel_cb() if f.cancelled() else None)
273
- else:
274
- result.add_done_callback(lambda f: self.cancel() if f.cancelled() else None)
265
+ self.add_done_callback(done)
266
+ self._link_cancellation(result, cancel_cb, on_loop=on_loop)
275
267
  return result
276
268
 
277
269
  def map[V](
@@ -369,6 +361,46 @@ class UnifiedFuture[T](Future[T], AbstractContextManager[Any], AbstractAsyncCont
369
361
 
370
362
  raise NotImplementedError("(async) with is not implemented for this object")
371
363
 
364
+ def _link_cancellation(
365
+ self,
366
+ target: Future[Any],
367
+ cancel_cb: Callable[[], None] | None = None,
368
+ *,
369
+ on_loop: bool = False,
370
+ ) -> None:
371
+ if cancel_cb is not None:
372
+
373
+ def on_cancel(f: Future[Any]) -> None:
374
+ if f.cancelled():
375
+ if on_loop:
376
+ get_loop().from_thread(cancel_cb)
377
+ else:
378
+ cancel_cb()
379
+
380
+ target.add_done_callback(on_cancel)
381
+ else:
382
+ target.add_done_callback(lambda f: self.cancel() if f.cancelled() else None)
383
+
384
+ @staticmethod
385
+ def _forward_loop_task(
386
+ loop_fut: Future[Any],
387
+ target: UnifiedFuture[Any],
388
+ on_done: Callable[[Future[Any]], None] | None = None,
389
+ ) -> None:
390
+ def forward(lf: Future[Any]) -> None:
391
+ if target.cancelled():
392
+ return
393
+ if lf.cancelled():
394
+ target.cancel()
395
+ elif (exc := lf.exception()) is not None:
396
+ target.set_exception(exc)
397
+ elif on_done is not None:
398
+ on_done(lf)
399
+ else:
400
+ target.set_result(lf.result())
401
+
402
+ loop_fut.add_done_callback(forward)
403
+
372
404
 
373
405
  class UnifiedIterator[T](Iterator[T], AsyncIterator[T]):
374
406
  """
@@ -1,2 +0,0 @@
1
- __version__ = "1.6.2"
2
- __version_tuple__ = (1, 6, 2)
File without changes
File without changes
File without changes
File without changes
File without changes