vsjetengine 1.6.2__tar.gz → 1.7.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.6.2 → vsjetengine-1.7.0}/PKG-INFO +1 -1
- {vsjetengine-1.6.2 → vsjetengine-1.7.0}/tests/test_futures.py +241 -0
- {vsjetengine-1.6.2 → vsjetengine-1.7.0}/tests/test_loop_adapters.py +84 -11
- vsjetengine-1.7.0/vsengine/_version.py +2 -0
- {vsjetengine-1.6.2 → vsjetengine-1.7.0}/vsengine/adapters/asyncio.py +15 -9
- {vsjetengine-1.6.2 → vsjetengine-1.7.0}/vsengine/adapters/trio.py +50 -12
- {vsjetengine-1.6.2 → vsjetengine-1.7.0}/vsengine/futures.py +144 -43
- vsjetengine-1.6.2/vsengine/_version.py +0 -2
- {vsjetengine-1.6.2 → vsjetengine-1.7.0}/.gitignore +0 -0
- {vsjetengine-1.6.2 → vsjetengine-1.7.0}/COPYING +0 -0
- {vsjetengine-1.6.2 → vsjetengine-1.7.0}/README.md +0 -0
- {vsjetengine-1.6.2 → vsjetengine-1.7.0}/pyproject.toml +0 -0
- {vsjetengine-1.6.2 → vsjetengine-1.7.0}/tests/__init__.py +0 -0
- {vsjetengine-1.6.2 → vsjetengine-1.7.0}/tests/_testutils.py +0 -0
- {vsjetengine-1.6.2 → vsjetengine-1.7.0}/tests/conftest.py +0 -0
- {vsjetengine-1.6.2 → vsjetengine-1.7.0}/tests/fixtures/heuristic_examples.json +0 -0
- {vsjetengine-1.6.2 → vsjetengine-1.7.0}/tests/fixtures/test.vpy +0 -0
- {vsjetengine-1.6.2 → vsjetengine-1.7.0}/tests/test_helpers.py +0 -0
- {vsjetengine-1.6.2 → vsjetengine-1.7.0}/tests/test_hospice.py +0 -0
- {vsjetengine-1.6.2 → vsjetengine-1.7.0}/tests/test_loops.py +0 -0
- {vsjetengine-1.6.2 → vsjetengine-1.7.0}/tests/test_policy.py +0 -0
- {vsjetengine-1.6.2 → vsjetengine-1.7.0}/tests/test_policy_store.py +0 -0
- {vsjetengine-1.6.2 → vsjetengine-1.7.0}/tests/test_pytest_plugin.py +0 -0
- {vsjetengine-1.6.2 → vsjetengine-1.7.0}/tests/test_video.py +0 -0
- {vsjetengine-1.6.2 → vsjetengine-1.7.0}/tests/test_vpy.py +0 -0
- {vsjetengine-1.6.2 → vsjetengine-1.7.0}/vsengine/__init__.py +0 -0
- {vsjetengine-1.6.2 → vsjetengine-1.7.0}/vsengine/_helpers.py +0 -0
- {vsjetengine-1.6.2 → vsjetengine-1.7.0}/vsengine/_hospice.py +0 -0
- {vsjetengine-1.6.2 → vsjetengine-1.7.0}/vsengine/_nodes.py +0 -0
- {vsjetengine-1.6.2 → vsjetengine-1.7.0}/vsengine/adapters/__init__.py +0 -0
- {vsjetengine-1.6.2 → vsjetengine-1.7.0}/vsengine/loops.py +0 -0
- {vsjetengine-1.6.2 → vsjetengine-1.7.0}/vsengine/policy.py +0 -0
- {vsjetengine-1.6.2 → vsjetengine-1.7.0}/vsengine/py.typed +0 -0
- {vsjetengine-1.6.2 → vsjetengine-1.7.0}/vsengine/pytest.py +0 -0
- {vsjetengine-1.6.2 → vsjetengine-1.7.0}/vsengine/video.py +0 -0
- {vsjetengine-1.6.2 → vsjetengine-1.7.0}/vsengine/vpy.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: vsjetengine
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.7.0
|
|
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,243 @@ 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
|
+
def test_unified_future_from_future_identity() -> None:
|
|
748
|
+
uni = UnifiedFuture.resolve(42)
|
|
749
|
+
assert UnifiedFuture.from_future(uni) is uni
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def test_unified_future_from_already_cancelled_future() -> None:
|
|
753
|
+
raw = Future[int]()
|
|
754
|
+
raw.cancel()
|
|
755
|
+
uni = UnifiedFuture.from_future(raw)
|
|
756
|
+
assert uni.cancelled() is True
|
|
757
|
+
|
|
758
|
+
loop = asyncio.new_event_loop()
|
|
759
|
+
raw_async = loop.create_future()
|
|
760
|
+
raw_async.cancel()
|
|
761
|
+
loop.close()
|
|
762
|
+
uni_async = UnifiedFuture.from_future(raw_async)
|
|
763
|
+
assert uni_async.cancelled() is True
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
def test_unified_future_from_closed_asyncio_future() -> None:
|
|
767
|
+
loop = asyncio.new_event_loop()
|
|
768
|
+
raw = loop.create_future()
|
|
769
|
+
raw.set_result(42)
|
|
770
|
+
loop.close()
|
|
771
|
+
|
|
772
|
+
uni = UnifiedFuture.from_future(raw)
|
|
773
|
+
|
|
774
|
+
assert uni.result() == 42
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
def test_unified_future_from_closed_asyncio_future_incomplete() -> None:
|
|
778
|
+
loop = asyncio.new_event_loop()
|
|
779
|
+
raw = loop.create_future()
|
|
780
|
+
loop.close()
|
|
781
|
+
|
|
782
|
+
uni = UnifiedFuture.from_future(raw)
|
|
783
|
+
assert uni.cancelled() is True
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
def test_unified_future_from_closed_asyncio_future_exception() -> None:
|
|
787
|
+
loop = asyncio.new_event_loop()
|
|
788
|
+
raw = loop.create_future()
|
|
789
|
+
raw.set_exception(ValueError("closed error"))
|
|
790
|
+
loop.close()
|
|
791
|
+
|
|
792
|
+
uni = UnifiedFuture.from_future(raw)
|
|
793
|
+
assert isinstance(uni.exception(), ValueError)
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
def test_unified_future_cancellation_propagates_to_closed_asyncio_future() -> None:
|
|
797
|
+
loop = asyncio.new_event_loop()
|
|
798
|
+
raw = loop.create_future()
|
|
799
|
+
uni = UnifiedFuture.from_future(raw)
|
|
800
|
+
loop.close()
|
|
801
|
+
|
|
802
|
+
assert uni.cancel() is True
|
|
803
|
+
assert raw.cancelled() is True
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
@pytest.mark.asyncio
|
|
807
|
+
async def test_unified_future_cancellation_prevents_asyncio_task_from_running() -> None:
|
|
808
|
+
ran = False
|
|
809
|
+
|
|
810
|
+
async def source() -> int:
|
|
811
|
+
nonlocal ran
|
|
812
|
+
ran = True
|
|
813
|
+
return 42
|
|
814
|
+
|
|
815
|
+
raw = asyncio.create_task(source())
|
|
816
|
+
uni = UnifiedFuture.from_future(raw)
|
|
817
|
+
|
|
818
|
+
assert uni.cancel() is True
|
|
819
|
+
await asyncio.sleep(0)
|
|
820
|
+
|
|
821
|
+
assert raw.cancelled() is True
|
|
822
|
+
assert ran is False
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
@pytest.mark.asyncio
|
|
826
|
+
async def test_unified_future_from_asyncio_future_exception() -> None:
|
|
827
|
+
set_loop(AsyncIOLoop())
|
|
828
|
+
raw = asyncio.get_running_loop().create_future()
|
|
829
|
+
uni = UnifiedFuture.from_future(raw)
|
|
830
|
+
raw.set_exception(ZeroDivisionError("division by zero"))
|
|
831
|
+
|
|
832
|
+
with pytest.raises(ZeroDivisionError):
|
|
833
|
+
await uni
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
def test_unified_future_from_asyncio_foreign_thread() -> None:
|
|
837
|
+
loop = asyncio.new_event_loop()
|
|
838
|
+
t = threading.Thread(target=loop.run_forever, daemon=True)
|
|
839
|
+
t.start()
|
|
840
|
+
|
|
841
|
+
try:
|
|
842
|
+
fut = loop.create_future()
|
|
843
|
+
uni = UnifiedFuture[str].from_future(fut)
|
|
844
|
+
|
|
845
|
+
loop.call_soon_threadsafe(fut.set_result, "cross-thread-result")
|
|
846
|
+
assert uni.result(timeout=2.0) == "cross-thread-result"
|
|
847
|
+
finally:
|
|
848
|
+
loop.call_soon_threadsafe(loop.stop)
|
|
849
|
+
t.join()
|
|
850
|
+
loop.close()
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
def test_unified_future_from_asyncio_foreign_thread_cancellation() -> None:
|
|
854
|
+
loop = asyncio.new_event_loop()
|
|
855
|
+
t = threading.Thread(target=loop.run_forever, daemon=True)
|
|
856
|
+
t.start()
|
|
857
|
+
|
|
858
|
+
try:
|
|
859
|
+
fut = loop.create_future()
|
|
860
|
+
uni = UnifiedFuture[str].from_future(fut)
|
|
861
|
+
|
|
862
|
+
assert uni.cancel() is True
|
|
863
|
+
assert uni.cancelled() is True
|
|
864
|
+
|
|
865
|
+
ev = threading.Event()
|
|
866
|
+
loop.call_soon_threadsafe(ev.set)
|
|
867
|
+
assert ev.wait(timeout=2.0)
|
|
868
|
+
|
|
869
|
+
assert fut.cancelled() is True
|
|
870
|
+
finally:
|
|
871
|
+
loop.call_soon_threadsafe(loop.stop)
|
|
872
|
+
t.join()
|
|
873
|
+
loop.close()
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
def test_unified_future_from_asyncio_concurrent_close_done(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
877
|
+
loop = asyncio.new_event_loop()
|
|
878
|
+
fut = loop.create_future()
|
|
879
|
+
fut.set_result(123)
|
|
880
|
+
|
|
881
|
+
def fake_call_soon_threadsafe(*args: Any, **kwargs: Any) -> None:
|
|
882
|
+
raise RuntimeError("Event loop is closed")
|
|
883
|
+
|
|
884
|
+
monkeypatch.setattr(loop, "call_soon_threadsafe", fake_call_soon_threadsafe)
|
|
885
|
+
|
|
886
|
+
uni = UnifiedFuture.from_future(fut)
|
|
887
|
+
assert uni.result() == 123
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
def test_unified_future_from_asyncio_concurrent_close_incomplete(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
891
|
+
loop = asyncio.new_event_loop()
|
|
892
|
+
fut = loop.create_future()
|
|
893
|
+
|
|
894
|
+
def fake_call_soon_threadsafe(*args: Any, **kwargs: Any) -> None:
|
|
895
|
+
raise RuntimeError("Event loop is closed")
|
|
896
|
+
|
|
897
|
+
monkeypatch.setattr(loop, "call_soon_threadsafe", fake_call_soon_threadsafe)
|
|
898
|
+
|
|
899
|
+
uni = UnifiedFuture.from_future(fut)
|
|
900
|
+
assert uni.cancelled() is True
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
def test_unified_future_from_asyncio_concurrent_close_cancellation(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
904
|
+
loop = asyncio.new_event_loop()
|
|
905
|
+
fut = loop.create_future()
|
|
906
|
+
uni = UnifiedFuture.from_future(fut)
|
|
907
|
+
|
|
908
|
+
def fake_call_soon_threadsafe(*args: Any, **kwargs: Any) -> None:
|
|
909
|
+
raise RuntimeError("Event loop is closed")
|
|
910
|
+
|
|
911
|
+
monkeypatch.setattr(loop, "call_soon_threadsafe", fake_call_soon_threadsafe)
|
|
912
|
+
|
|
913
|
+
assert uni.cancel() is True
|
|
914
|
+
|
|
915
|
+
|
|
916
|
+
@pytest.mark.asyncio
|
|
917
|
+
async def test_unified_future_add_loop_callback_cancellation_propagation() -> None:
|
|
918
|
+
set_loop(AsyncIOLoop())
|
|
919
|
+
raw = UnifiedFuture[int]()
|
|
920
|
+
chained = raw.add_loop_callback(lambda f: None)
|
|
921
|
+
|
|
922
|
+
assert chained.cancel() is True
|
|
923
|
+
assert chained.cancelled() is True
|
|
924
|
+
assert raw.cancelled() is True
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
@pytest.mark.asyncio
|
|
928
|
+
async def test_unified_future_add_loop_callback_custom_on_cancel() -> None:
|
|
929
|
+
set_loop(AsyncIOLoop())
|
|
930
|
+
cancelled_calls = list[threading.Thread]()
|
|
931
|
+
loop_thread = threading.current_thread()
|
|
932
|
+
raw = UnifiedFuture[int]()
|
|
933
|
+
chained = raw.add_loop_callback(
|
|
934
|
+
lambda f: None,
|
|
935
|
+
cancel_cb=lambda: cancelled_calls.append(threading.current_thread()),
|
|
936
|
+
)
|
|
937
|
+
|
|
938
|
+
assert chained.cancel() is True
|
|
939
|
+
assert chained.cancelled() is True
|
|
940
|
+
await asyncio.sleep(0.01)
|
|
941
|
+
assert cancelled_calls == [loop_thread]
|
|
942
|
+
assert raw.cancelled() is False
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
@pytest.mark.asyncio
|
|
946
|
+
async def test_unified_future_then_custom_on_cancel_on_loop() -> None:
|
|
947
|
+
set_loop(AsyncIOLoop())
|
|
948
|
+
cancelled_calls = list[threading.Thread]()
|
|
949
|
+
loop_thread = threading.current_thread()
|
|
950
|
+
raw = UnifiedFuture[int]()
|
|
951
|
+
chained = raw.then(
|
|
952
|
+
lambda x: x * 2,
|
|
953
|
+
cancel_cb=lambda: cancelled_calls.append(threading.current_thread()),
|
|
954
|
+
on_loop=True,
|
|
955
|
+
)
|
|
956
|
+
|
|
957
|
+
# Cancel from a background thread
|
|
958
|
+
t = threading.Thread(target=chained.cancel)
|
|
959
|
+
t.start()
|
|
960
|
+
t.join()
|
|
961
|
+
|
|
962
|
+
await asyncio.sleep(0.01)
|
|
963
|
+
assert chained.cancelled() is True
|
|
964
|
+
assert cancelled_calls == [loop_thread]
|
|
965
|
+
assert raw.cancelled() is False
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
@pytest.mark.asyncio
|
|
969
|
+
async def test_unified_future_add_loop_callback_parent_cancelled() -> None:
|
|
970
|
+
set_loop(AsyncIOLoop())
|
|
971
|
+
raw = UnifiedFuture[int]()
|
|
972
|
+
called = list[bool]()
|
|
973
|
+
|
|
974
|
+
def cb(f: Future[int]) -> None:
|
|
975
|
+
called.append(f.cancelled())
|
|
976
|
+
|
|
977
|
+
chained = raw.add_loop_callback(cb)
|
|
978
|
+
raw.cancel()
|
|
979
|
+
|
|
980
|
+
with pytest.raises(asyncio.CancelledError):
|
|
981
|
+
await chained
|
|
982
|
+
|
|
983
|
+
assert chained.cancelled() is True
|
|
984
|
+
assert called == [True]
|
|
@@ -9,6 +9,8 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import asyncio
|
|
11
11
|
import contextlib
|
|
12
|
+
import contextvars
|
|
13
|
+
import functools
|
|
12
14
|
import threading
|
|
13
15
|
import unittest.mock
|
|
14
16
|
from collections.abc import Awaitable, Callable, Generator, Iterator
|
|
@@ -311,19 +313,90 @@ class TestTrio(AsyncAdapterTest):
|
|
|
311
313
|
yield
|
|
312
314
|
|
|
313
315
|
|
|
314
|
-
def
|
|
315
|
-
"""Ensure AsyncIOLoop
|
|
316
|
+
def test_asyncio_methods_without_running_loop() -> None:
|
|
317
|
+
"""Ensure AsyncIOLoop raises RuntimeError when no asyncio loop is running."""
|
|
316
318
|
loop = AsyncIOLoop()
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
assert fut.result() == 42
|
|
319
|
+
with pytest.raises(RuntimeError, match="No running asyncio event loop"):
|
|
320
|
+
loop.from_thread(lambda x: x * 2, 21)
|
|
320
321
|
|
|
322
|
+
with pytest.raises(RuntimeError, match="No running asyncio event loop"):
|
|
323
|
+
loop.to_thread(lambda x: x * 2, 21)
|
|
321
324
|
|
|
322
|
-
|
|
323
|
-
|
|
325
|
+
with pytest.raises(RuntimeError, match="No running asyncio event loop"):
|
|
326
|
+
loop.next_cycle()
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def test_trio_methods_without_running_token() -> None:
|
|
330
|
+
"""Ensure TrioEventLoop raises RuntimeError when no Trio token or nursery is active."""
|
|
331
|
+
loop = TrioEventLoop()
|
|
332
|
+
with pytest.raises(RuntimeError, match="No running Trio event loop"):
|
|
333
|
+
loop.from_thread(lambda x: x + 1, 10)
|
|
334
|
+
|
|
335
|
+
with pytest.raises(RuntimeError, match="Trio nursery is not set"):
|
|
336
|
+
loop.to_thread(lambda x: x + 1, 10)
|
|
337
|
+
|
|
338
|
+
with pytest.raises(RuntimeError, match="No running Trio event loop"):
|
|
339
|
+
loop.next_cycle()
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def test_trio_from_thread_contextvars() -> None:
|
|
343
|
+
"""Ensure TrioEventLoop propagates contextvars from the calling thread."""
|
|
344
|
+
var: contextvars.ContextVar[str] = contextvars.ContextVar("var", default="default")
|
|
345
|
+
|
|
346
|
+
async def main() -> None:
|
|
347
|
+
async with trio.open_nursery() as nursery:
|
|
348
|
+
loop = TrioEventLoop(nursery)
|
|
349
|
+
results = list[str]()
|
|
350
|
+
|
|
351
|
+
def worker() -> None:
|
|
352
|
+
var.set("custom_val")
|
|
353
|
+
fut = loop.from_thread(var.get)
|
|
354
|
+
results.append(fut.result(timeout=0.5))
|
|
355
|
+
|
|
356
|
+
await trio.to_thread.run_sync(worker)
|
|
357
|
+
assert results == ["custom_val"]
|
|
358
|
+
|
|
359
|
+
trio.run(main)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def test_to_thread_with_partial() -> None:
|
|
363
|
+
"""Ensure to_thread handles functools.partial and callables without __name__."""
|
|
364
|
+
|
|
365
|
+
def add(a: int, b: int) -> int:
|
|
366
|
+
return a + b
|
|
367
|
+
|
|
368
|
+
part = functools.partial(add, 10, 20)
|
|
369
|
+
|
|
370
|
+
# Test with TrioEventLoop
|
|
371
|
+
async def trio_main() -> None:
|
|
372
|
+
async with trio.open_nursery() as nursery:
|
|
373
|
+
loop = TrioEventLoop(nursery)
|
|
374
|
+
fut = loop.to_thread(part)
|
|
375
|
+
while not fut.done():
|
|
376
|
+
await trio.sleep(0.01)
|
|
377
|
+
assert fut.result() == 30
|
|
378
|
+
|
|
379
|
+
trio.run(trio_main)
|
|
380
|
+
|
|
381
|
+
# Test with AsyncIOLoop
|
|
382
|
+
async def asyncio_main() -> None:
|
|
383
|
+
loop = AsyncIOLoop()
|
|
384
|
+
fut = loop.to_thread(part)
|
|
385
|
+
while not fut.done():
|
|
386
|
+
await asyncio.sleep(0.01)
|
|
387
|
+
assert fut.result() == 30
|
|
388
|
+
|
|
389
|
+
asyncio.run(asyncio_main())
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def test_trio_detach_lifecycle() -> None:
|
|
393
|
+
"""Ensure detach cleanly resets token and limiter."""
|
|
324
394
|
nursery = unittest.mock.MagicMock()
|
|
325
|
-
nursery.cancel_scope.cancel_called = False
|
|
326
395
|
loop = TrioEventLoop(nursery)
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
396
|
+
loop.token = unittest.mock.MagicMock()
|
|
397
|
+
loop.limiter = unittest.mock.MagicMock()
|
|
398
|
+
|
|
399
|
+
loop.detach()
|
|
400
|
+
nursery.cancel_scope.cancel.assert_called_once()
|
|
401
|
+
assert loop._token is None
|
|
402
|
+
assert loop._limiter is None
|
|
@@ -20,6 +20,9 @@ class AsyncIOLoop(EventLoop):
|
|
|
20
20
|
|
|
21
21
|
def __init__(self, loop: asyncio.AbstractEventLoop | None = None) -> None:
|
|
22
22
|
self._loop = loop
|
|
23
|
+
if self._loop is None:
|
|
24
|
+
with contextlib.suppress(RuntimeError):
|
|
25
|
+
self._loop = asyncio.get_running_loop()
|
|
23
26
|
|
|
24
27
|
@property
|
|
25
28
|
def loop(self) -> asyncio.AbstractEventLoop | None:
|
|
@@ -43,6 +46,9 @@ class AsyncIOLoop(EventLoop):
|
|
|
43
46
|
self._loop = None
|
|
44
47
|
|
|
45
48
|
def from_thread[**P, R](self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]:
|
|
49
|
+
if (loop := self.loop) is None or loop.is_closed():
|
|
50
|
+
raise RuntimeError("No running asyncio event loop or loop is closed.")
|
|
51
|
+
|
|
46
52
|
future = Future[R]()
|
|
47
53
|
|
|
48
54
|
def wrap() -> Future[R]:
|
|
@@ -58,32 +64,32 @@ class AsyncIOLoop(EventLoop):
|
|
|
58
64
|
|
|
59
65
|
return future
|
|
60
66
|
|
|
61
|
-
if (loop := self.loop) is None or loop.is_closed():
|
|
62
|
-
return wrap()
|
|
63
|
-
|
|
64
67
|
loop.call_soon_threadsafe(wrap, context=contextvars.copy_context())
|
|
65
68
|
return future
|
|
66
69
|
|
|
67
70
|
def to_thread[**P, R](self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]:
|
|
68
71
|
if (loop := self.loop) is None or loop.is_closed():
|
|
69
|
-
|
|
72
|
+
raise RuntimeError("No running asyncio event loop or loop is closed.")
|
|
70
73
|
|
|
71
74
|
future = Future[R]()
|
|
72
75
|
|
|
73
76
|
async def run() -> None:
|
|
77
|
+
if not future.set_running_or_notify_cancel():
|
|
78
|
+
return
|
|
79
|
+
|
|
74
80
|
try:
|
|
75
|
-
result = await asyncio.to_thread(
|
|
81
|
+
result = await asyncio.to_thread(func, *args, **kwargs)
|
|
76
82
|
except BaseException as e:
|
|
77
83
|
future.set_exception(e)
|
|
78
84
|
else:
|
|
79
85
|
future.set_result(result)
|
|
80
86
|
|
|
81
|
-
loop.create_task(run(), name=func
|
|
87
|
+
loop.create_task(run(), name=getattr(func, "__name__", None), context=contextvars.copy_context())
|
|
82
88
|
return future
|
|
83
89
|
|
|
84
90
|
def next_cycle(self) -> Future[None]:
|
|
85
91
|
if (loop := self.loop) is None or loop.is_closed():
|
|
86
|
-
|
|
92
|
+
raise RuntimeError("No running asyncio event loop or loop is closed.")
|
|
87
93
|
|
|
88
94
|
future = Future[None]()
|
|
89
95
|
task = asyncio.current_task(loop)
|
|
@@ -97,7 +103,7 @@ class AsyncIOLoop(EventLoop):
|
|
|
97
103
|
loop.call_soon(continuation)
|
|
98
104
|
return future
|
|
99
105
|
|
|
100
|
-
async def await_future[T](self, future: Future[T]) -> T:
|
|
106
|
+
async def await_future[T](self, future: Future[T] | asyncio.Future[T]) -> T:
|
|
101
107
|
with self.wrap_cancelled():
|
|
102
108
|
return await asyncio.wrap_future(future)
|
|
103
109
|
|
|
@@ -106,4 +112,4 @@ class AsyncIOLoop(EventLoop):
|
|
|
106
112
|
try:
|
|
107
113
|
yield
|
|
108
114
|
except Cancelled:
|
|
109
|
-
raise asyncio.CancelledError
|
|
115
|
+
raise asyncio.CancelledError from None
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import contextlib
|
|
10
|
+
import contextvars
|
|
10
11
|
from collections.abc import Callable, Generator
|
|
11
12
|
from concurrent.futures import Future
|
|
12
13
|
|
|
@@ -20,10 +21,12 @@ class TrioEventLoop(EventLoop):
|
|
|
20
21
|
Bridges vs-engine to Trio.
|
|
21
22
|
"""
|
|
22
23
|
|
|
23
|
-
def __init__(self, nursery: trio.Nursery, limiter: trio.CapacityLimiter | None = None) -> None:
|
|
24
|
+
def __init__(self, nursery: trio.Nursery | None = None, limiter: trio.CapacityLimiter | None = None) -> None:
|
|
24
25
|
self.nursery = nursery
|
|
25
26
|
self._limiter = limiter
|
|
26
27
|
self._token: trio.lowlevel.TrioToken | None = None
|
|
28
|
+
with contextlib.suppress(RuntimeError):
|
|
29
|
+
self._token = trio.lowlevel.current_trio_token()
|
|
27
30
|
|
|
28
31
|
@property
|
|
29
32
|
def limiter(self) -> trio.CapacityLimiter | None:
|
|
@@ -56,9 +59,16 @@ class TrioEventLoop(EventLoop):
|
|
|
56
59
|
self._token = trio.lowlevel.current_trio_token()
|
|
57
60
|
|
|
58
61
|
def detach(self) -> None:
|
|
59
|
-
self.nursery
|
|
62
|
+
if self.nursery is not None:
|
|
63
|
+
with contextlib.suppress(RuntimeError):
|
|
64
|
+
self.nursery.cancel_scope.cancel()
|
|
65
|
+
self._token = None
|
|
66
|
+
self._limiter = None
|
|
60
67
|
|
|
61
68
|
def from_thread[**P, R](self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]:
|
|
69
|
+
if (token := self.token) is None:
|
|
70
|
+
raise RuntimeError("No running Trio event loop or token is inactive.")
|
|
71
|
+
|
|
62
72
|
future = Future[R]()
|
|
63
73
|
|
|
64
74
|
def executor() -> Future[R]:
|
|
@@ -73,37 +83,58 @@ class TrioEventLoop(EventLoop):
|
|
|
73
83
|
future.set_result(result)
|
|
74
84
|
return future
|
|
75
85
|
|
|
76
|
-
|
|
77
|
-
token.run_sync_soon(executor)
|
|
78
|
-
|
|
86
|
+
try:
|
|
87
|
+
token.run_sync_soon(contextvars.copy_context().run, executor)
|
|
88
|
+
except (trio.RunFinishedError, trio.ClosedResourceError) as e:
|
|
89
|
+
self._token = None
|
|
90
|
+
raise RuntimeError("Trio event loop has already finished or is closed.") from e
|
|
79
91
|
|
|
80
|
-
return
|
|
92
|
+
return future
|
|
81
93
|
|
|
82
94
|
def to_thread[**P, R](self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]:
|
|
95
|
+
if self.nursery is None:
|
|
96
|
+
raise RuntimeError("Trio nursery is not set.")
|
|
83
97
|
if self.nursery.cancel_scope.cancel_called:
|
|
84
|
-
|
|
98
|
+
raise RuntimeError("Trio nursery has been cancelled.")
|
|
85
99
|
|
|
86
100
|
future = Future[R]()
|
|
87
101
|
|
|
88
102
|
async def run() -> None:
|
|
89
103
|
def executor() -> None:
|
|
104
|
+
if not future.set_running_or_notify_cancel():
|
|
105
|
+
return
|
|
106
|
+
|
|
90
107
|
try:
|
|
91
108
|
result = func(*args, **kwargs)
|
|
92
|
-
future.set_result(result)
|
|
93
109
|
except BaseException as e:
|
|
94
110
|
future.set_exception(e)
|
|
111
|
+
else:
|
|
112
|
+
future.set_result(result)
|
|
95
113
|
|
|
96
|
-
|
|
114
|
+
try:
|
|
115
|
+
await trio.to_thread.run_sync(executor, limiter=self.limiter)
|
|
116
|
+
except BaseException as e:
|
|
117
|
+
if not future.done():
|
|
118
|
+
future.set_exception(Cancelled() if isinstance(e, trio.Cancelled) else e)
|
|
119
|
+
raise
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
self.nursery.start_soon(run, name=getattr(func, "__name__", None))
|
|
123
|
+
except RuntimeError as e:
|
|
124
|
+
raise RuntimeError("Trio nursery is closed or cannot start tasks.") from e
|
|
97
125
|
|
|
98
|
-
self.nursery.start_soon(run, name=func.__name__)
|
|
99
126
|
return future
|
|
100
127
|
|
|
101
128
|
def next_cycle(self) -> Future[None]:
|
|
102
|
-
|
|
129
|
+
if self.token is None:
|
|
130
|
+
raise RuntimeError("No running Trio event loop or token is inactive.")
|
|
131
|
+
if self.nursery is None or self.nursery.cancel_scope.cancel_called:
|
|
132
|
+
raise RuntimeError("Trio nursery is not set or has been cancelled.")
|
|
133
|
+
|
|
103
134
|
future = Future[None]()
|
|
104
135
|
|
|
105
136
|
def continuation() -> None:
|
|
106
|
-
if
|
|
137
|
+
if self.nursery is not None and self.nursery.cancel_scope.cancel_called:
|
|
107
138
|
future.set_exception(Cancelled())
|
|
108
139
|
else:
|
|
109
140
|
future.set_result(None)
|
|
@@ -112,6 +143,13 @@ class TrioEventLoop(EventLoop):
|
|
|
112
143
|
return future
|
|
113
144
|
|
|
114
145
|
async def await_future[T](self, future: Future[T]) -> T:
|
|
146
|
+
if future.done():
|
|
147
|
+
try:
|
|
148
|
+
return future.result()
|
|
149
|
+
except BaseException:
|
|
150
|
+
with self.wrap_cancelled():
|
|
151
|
+
raise
|
|
152
|
+
|
|
115
153
|
event = trio.Event()
|
|
116
154
|
|
|
117
155
|
def _when_done(_: Future[T]) -> None:
|
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
# SPDX-License-Identifier: EUPL-1.2
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
+
import asyncio
|
|
9
|
+
import contextvars
|
|
8
10
|
import traceback
|
|
9
11
|
from collections.abc import AsyncIterator, Awaitable, Callable, Generator, Iterator
|
|
10
|
-
from concurrent.futures import Future
|
|
11
|
-
from contextlib import AbstractAsyncContextManager, AbstractContextManager
|
|
12
|
+
from concurrent.futures import CancelledError, Future
|
|
13
|
+
from contextlib import AbstractAsyncContextManager, AbstractContextManager, suppress
|
|
12
14
|
from functools import wraps
|
|
13
15
|
from inspect import isgeneratorfunction
|
|
14
16
|
from types import TracebackType
|
|
@@ -63,7 +65,7 @@ class UnifiedFuture[T](Future[T], AbstractContextManager[Any], AbstractAsyncCont
|
|
|
63
65
|
return cls.from_future(future)
|
|
64
66
|
|
|
65
67
|
@classmethod
|
|
66
|
-
def from_future[S](cls: type[UnifiedFutureLike[S]], future: Future[S]) -> UnifiedFutureLike[S]:
|
|
68
|
+
def from_future[S](cls: type[UnifiedFutureLike[S]], future: Future[S] | asyncio.Future[S]) -> UnifiedFutureLike[S]:
|
|
67
69
|
"""
|
|
68
70
|
Wrap an existing `Future` as a `UnifiedFuture`.
|
|
69
71
|
|
|
@@ -77,17 +79,47 @@ class UnifiedFuture[T](Future[T], AbstractContextManager[Any], AbstractAsyncCont
|
|
|
77
79
|
|
|
78
80
|
result = cls()
|
|
79
81
|
|
|
80
|
-
def
|
|
82
|
+
def receive(_: Future[S] | asyncio.Future[S]) -> None:
|
|
83
|
+
if result.done():
|
|
84
|
+
return
|
|
81
85
|
if future.cancelled():
|
|
82
86
|
result.cancel()
|
|
83
87
|
return
|
|
84
|
-
|
|
88
|
+
try:
|
|
89
|
+
exc = future.exception()
|
|
90
|
+
except (CancelledError, asyncio.CancelledError):
|
|
91
|
+
# Catch any cancellation race condition
|
|
92
|
+
result.cancel()
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
if exc is not None:
|
|
85
96
|
result.set_exception(exc)
|
|
86
|
-
|
|
97
|
+
else:
|
|
87
98
|
result.set_result(future.result())
|
|
88
99
|
|
|
89
|
-
future
|
|
90
|
-
|
|
100
|
+
# Safely propagate cancellation back to the original future
|
|
101
|
+
def on_result_cancelled(f: Future[S]) -> None:
|
|
102
|
+
if not f.cancelled() or future.done():
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
if isinstance(future, asyncio.Future):
|
|
106
|
+
cls._dispatch_to_loop(future, future.cancel)
|
|
107
|
+
else:
|
|
108
|
+
future.cancel()
|
|
109
|
+
|
|
110
|
+
result.add_done_callback(on_result_cancelled)
|
|
111
|
+
|
|
112
|
+
# Safely register the completion callback
|
|
113
|
+
if isinstance(future, asyncio.Future):
|
|
114
|
+
cls._dispatch_to_loop(
|
|
115
|
+
future,
|
|
116
|
+
future.add_done_callback,
|
|
117
|
+
receive,
|
|
118
|
+
on_closed_done=receive,
|
|
119
|
+
on_closed_incomplete=result.cancel,
|
|
120
|
+
)
|
|
121
|
+
else:
|
|
122
|
+
future.add_done_callback(receive)
|
|
91
123
|
return result
|
|
92
124
|
|
|
93
125
|
@classmethod
|
|
@@ -127,7 +159,11 @@ class UnifiedFuture[T](Future[T], AbstractContextManager[Any], AbstractAsyncCont
|
|
|
127
159
|
# The done_callback should inherit the environment of the current call.
|
|
128
160
|
super().add_done_callback(keep_environment(fn))
|
|
129
161
|
|
|
130
|
-
def add_loop_callback(
|
|
162
|
+
def add_loop_callback(
|
|
163
|
+
self,
|
|
164
|
+
func: Callable[[Future[T]], Any],
|
|
165
|
+
cancel_cb: Callable[[], None] | None = None,
|
|
166
|
+
) -> UnifiedFuture[T]:
|
|
131
167
|
"""
|
|
132
168
|
Register a callback that is guaranteed to run on the event-loop thread.
|
|
133
169
|
|
|
@@ -135,25 +171,28 @@ class UnifiedFuture[T](Future[T], AbstractContextManager[Any], AbstractAsyncCont
|
|
|
135
171
|
this method marshals `func` back to the main event loop via `EventLoop.from_thread`.
|
|
136
172
|
|
|
137
173
|
:param func: A callable that receives the completed future.
|
|
174
|
+
:param cancel_cb: Optional callback invoked when the returned future is cancelled.
|
|
175
|
+
Defaults to cancelling the parent future.
|
|
138
176
|
:return: A UnifiedFuture that resolves to the original future's result once the callback completes
|
|
139
177
|
on the event loop.
|
|
140
178
|
"""
|
|
141
179
|
result = UnifiedFuture[T]()
|
|
142
180
|
|
|
143
|
-
def
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
result.set_exception(orig_exc)
|
|
151
|
-
else:
|
|
152
|
-
result.set_result(future.result())
|
|
181
|
+
def forward_parent(_: Future[Any]) -> None:
|
|
182
|
+
if self.cancelled():
|
|
183
|
+
result.cancel()
|
|
184
|
+
elif (orig_exc := self.exception()) is not None:
|
|
185
|
+
result.set_exception(orig_exc)
|
|
186
|
+
else:
|
|
187
|
+
result.set_result(self.result())
|
|
153
188
|
|
|
154
|
-
|
|
189
|
+
def wrapper(future: Future[T]) -> None:
|
|
190
|
+
if result.cancelled():
|
|
191
|
+
return
|
|
192
|
+
self._forward_loop_task(get_loop().from_thread(func, future), result, forward_parent)
|
|
155
193
|
|
|
156
|
-
self.add_done_callback(
|
|
194
|
+
self.add_done_callback(wrapper)
|
|
195
|
+
self._link_cancellation(result, cancel_cb, on_loop=True)
|
|
157
196
|
return result
|
|
158
197
|
|
|
159
198
|
# Manipulating futures
|
|
@@ -227,23 +266,11 @@ class UnifiedFuture[T](Future[T], AbstractContextManager[Any], AbstractAsyncCont
|
|
|
227
266
|
"""
|
|
228
267
|
result = UnifiedFuture[Any]()
|
|
229
268
|
|
|
230
|
-
def
|
|
269
|
+
def run_cb(cb: Callable[[Any], Any], v: T | BaseException) -> None:
|
|
231
270
|
if result.cancelled():
|
|
232
271
|
return
|
|
233
272
|
if on_loop:
|
|
234
|
-
|
|
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)
|
|
273
|
+
self._forward_loop_task(get_loop().from_thread(cb, v), result)
|
|
247
274
|
else:
|
|
248
275
|
try:
|
|
249
276
|
r = cb(v)
|
|
@@ -252,26 +279,23 @@ class UnifiedFuture[T](Future[T], AbstractContextManager[Any], AbstractAsyncCont
|
|
|
252
279
|
else:
|
|
253
280
|
result.set_result(r)
|
|
254
281
|
|
|
255
|
-
def
|
|
282
|
+
def done(_: Future[T]) -> None:
|
|
256
283
|
if self.cancelled():
|
|
257
284
|
result.cancel()
|
|
258
285
|
return
|
|
259
286
|
if (exc := self.exception()) is not None:
|
|
260
287
|
if err_cb is not None:
|
|
261
|
-
|
|
288
|
+
run_cb(err_cb, exc)
|
|
262
289
|
else:
|
|
263
290
|
result.set_exception(exc)
|
|
264
291
|
elif not result.cancelled():
|
|
265
292
|
if success_cb is not None:
|
|
266
|
-
|
|
293
|
+
run_cb(success_cb, self.result())
|
|
267
294
|
else:
|
|
268
295
|
result.set_result(self.result())
|
|
269
296
|
|
|
270
|
-
self.add_done_callback(
|
|
271
|
-
|
|
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)
|
|
297
|
+
self.add_done_callback(done)
|
|
298
|
+
self._link_cancellation(result, cancel_cb, on_loop=on_loop)
|
|
275
299
|
return result
|
|
276
300
|
|
|
277
301
|
def map[V](
|
|
@@ -369,6 +393,83 @@ class UnifiedFuture[T](Future[T], AbstractContextManager[Any], AbstractAsyncCont
|
|
|
369
393
|
|
|
370
394
|
raise NotImplementedError("(async) with is not implemented for this object")
|
|
371
395
|
|
|
396
|
+
@staticmethod
|
|
397
|
+
def _dispatch_to_loop[*Ts](
|
|
398
|
+
fut: asyncio.Future[Any],
|
|
399
|
+
action: Callable[[*Ts], Any],
|
|
400
|
+
*args: *Ts,
|
|
401
|
+
on_closed_done: Callable[[asyncio.Future[Any]], Any] | None = None,
|
|
402
|
+
on_closed_incomplete: Callable[[], Any] | None = None,
|
|
403
|
+
) -> None:
|
|
404
|
+
f_loop = fut.get_loop()
|
|
405
|
+
try:
|
|
406
|
+
current_loop = asyncio.get_running_loop()
|
|
407
|
+
except RuntimeError:
|
|
408
|
+
current_loop = None
|
|
409
|
+
|
|
410
|
+
if current_loop is f_loop:
|
|
411
|
+
action(*args)
|
|
412
|
+
elif f_loop.is_closed():
|
|
413
|
+
if fut.done() and on_closed_done:
|
|
414
|
+
on_closed_done(fut)
|
|
415
|
+
elif on_closed_incomplete:
|
|
416
|
+
on_closed_incomplete()
|
|
417
|
+
else:
|
|
418
|
+
with suppress(RuntimeError):
|
|
419
|
+
action(*args)
|
|
420
|
+
else:
|
|
421
|
+
try:
|
|
422
|
+
f_loop.call_soon_threadsafe(action, *args, context=contextvars.copy_context())
|
|
423
|
+
except RuntimeError:
|
|
424
|
+
# Loop closed concurrently
|
|
425
|
+
if fut.done() and on_closed_done is not None:
|
|
426
|
+
on_closed_done(fut)
|
|
427
|
+
elif on_closed_incomplete is not None:
|
|
428
|
+
on_closed_incomplete()
|
|
429
|
+
else:
|
|
430
|
+
with suppress(RuntimeError):
|
|
431
|
+
action(*args)
|
|
432
|
+
|
|
433
|
+
def _link_cancellation(
|
|
434
|
+
self,
|
|
435
|
+
target: Future[Any],
|
|
436
|
+
cancel_cb: Callable[[], None] | None = None,
|
|
437
|
+
*,
|
|
438
|
+
on_loop: bool = False,
|
|
439
|
+
) -> None:
|
|
440
|
+
if cancel_cb is not None:
|
|
441
|
+
|
|
442
|
+
def on_cancel(f: Future[Any]) -> None:
|
|
443
|
+
if f.cancelled():
|
|
444
|
+
if on_loop:
|
|
445
|
+
get_loop().from_thread(cancel_cb)
|
|
446
|
+
else:
|
|
447
|
+
cancel_cb()
|
|
448
|
+
|
|
449
|
+
target.add_done_callback(on_cancel)
|
|
450
|
+
else:
|
|
451
|
+
target.add_done_callback(lambda f: self.cancel() if f.cancelled() else None)
|
|
452
|
+
|
|
453
|
+
@staticmethod
|
|
454
|
+
def _forward_loop_task(
|
|
455
|
+
loop_fut: Future[Any],
|
|
456
|
+
target: UnifiedFuture[Any],
|
|
457
|
+
on_done: Callable[[Future[Any]], None] | None = None,
|
|
458
|
+
) -> None:
|
|
459
|
+
def forward(lf: Future[Any]) -> None:
|
|
460
|
+
if target.cancelled():
|
|
461
|
+
return
|
|
462
|
+
if lf.cancelled():
|
|
463
|
+
target.cancel()
|
|
464
|
+
elif (exc := lf.exception()) is not None:
|
|
465
|
+
target.set_exception(exc)
|
|
466
|
+
elif on_done is not None:
|
|
467
|
+
on_done(lf)
|
|
468
|
+
else:
|
|
469
|
+
target.set_result(lf.result())
|
|
470
|
+
|
|
471
|
+
loop_fut.add_done_callback(forward)
|
|
472
|
+
|
|
372
473
|
|
|
373
474
|
class UnifiedIterator[T](Iterator[T], AsyncIterator[T]):
|
|
374
475
|
"""
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|