vsjetengine 1.6.3__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.3 → vsjetengine-1.7.0}/PKG-INFO +1 -1
- {vsjetengine-1.6.3 → vsjetengine-1.7.0}/tests/test_futures.py +169 -0
- {vsjetengine-1.6.3 → vsjetengine-1.7.0}/tests/test_loop_adapters.py +84 -11
- vsjetengine-1.7.0/vsengine/_version.py +2 -0
- {vsjetengine-1.6.3 → vsjetengine-1.7.0}/vsengine/adapters/asyncio.py +15 -9
- {vsjetengine-1.6.3 → vsjetengine-1.7.0}/vsengine/adapters/trio.py +50 -12
- {vsjetengine-1.6.3 → vsjetengine-1.7.0}/vsengine/futures.py +77 -8
- vsjetengine-1.6.3/vsengine/_version.py +0 -2
- {vsjetengine-1.6.3 → vsjetengine-1.7.0}/.gitignore +0 -0
- {vsjetengine-1.6.3 → vsjetengine-1.7.0}/COPYING +0 -0
- {vsjetengine-1.6.3 → vsjetengine-1.7.0}/README.md +0 -0
- {vsjetengine-1.6.3 → vsjetengine-1.7.0}/pyproject.toml +0 -0
- {vsjetengine-1.6.3 → vsjetengine-1.7.0}/tests/__init__.py +0 -0
- {vsjetengine-1.6.3 → vsjetengine-1.7.0}/tests/_testutils.py +0 -0
- {vsjetengine-1.6.3 → vsjetengine-1.7.0}/tests/conftest.py +0 -0
- {vsjetengine-1.6.3 → vsjetengine-1.7.0}/tests/fixtures/heuristic_examples.json +0 -0
- {vsjetengine-1.6.3 → vsjetengine-1.7.0}/tests/fixtures/test.vpy +0 -0
- {vsjetengine-1.6.3 → vsjetengine-1.7.0}/tests/test_helpers.py +0 -0
- {vsjetengine-1.6.3 → vsjetengine-1.7.0}/tests/test_hospice.py +0 -0
- {vsjetengine-1.6.3 → vsjetengine-1.7.0}/tests/test_loops.py +0 -0
- {vsjetengine-1.6.3 → vsjetengine-1.7.0}/tests/test_policy.py +0 -0
- {vsjetengine-1.6.3 → vsjetengine-1.7.0}/tests/test_policy_store.py +0 -0
- {vsjetengine-1.6.3 → vsjetengine-1.7.0}/tests/test_pytest_plugin.py +0 -0
- {vsjetengine-1.6.3 → vsjetengine-1.7.0}/tests/test_video.py +0 -0
- {vsjetengine-1.6.3 → vsjetengine-1.7.0}/tests/test_vpy.py +0 -0
- {vsjetengine-1.6.3 → vsjetengine-1.7.0}/vsengine/__init__.py +0 -0
- {vsjetengine-1.6.3 → vsjetengine-1.7.0}/vsengine/_helpers.py +0 -0
- {vsjetengine-1.6.3 → vsjetengine-1.7.0}/vsengine/_hospice.py +0 -0
- {vsjetengine-1.6.3 → vsjetengine-1.7.0}/vsengine/_nodes.py +0 -0
- {vsjetengine-1.6.3 → vsjetengine-1.7.0}/vsengine/adapters/__init__.py +0 -0
- {vsjetengine-1.6.3 → vsjetengine-1.7.0}/vsengine/loops.py +0 -0
- {vsjetengine-1.6.3 → vsjetengine-1.7.0}/vsengine/policy.py +0 -0
- {vsjetengine-1.6.3 → vsjetengine-1.7.0}/vsengine/py.typed +0 -0
- {vsjetengine-1.6.3 → vsjetengine-1.7.0}/vsengine/pytest.py +0 -0
- {vsjetengine-1.6.3 → vsjetengine-1.7.0}/vsengine/video.py +0 -0
- {vsjetengine-1.6.3 → 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
|
|
@@ -744,6 +744,175 @@ def test_unified_future_from_future_cancellation() -> None:
|
|
|
744
744
|
assert raw.cancelled() is True
|
|
745
745
|
|
|
746
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
|
+
|
|
747
916
|
@pytest.mark.asyncio
|
|
748
917
|
async def test_unified_future_add_loop_callback_cancellation_propagation() -> None:
|
|
749
918
|
set_loop(AsyncIOLoop())
|
|
@@ -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
|
|
@@ -361,6 +393,43 @@ class UnifiedFuture[T](Future[T], AbstractContextManager[Any], AbstractAsyncCont
|
|
|
361
393
|
|
|
362
394
|
raise NotImplementedError("(async) with is not implemented for this object")
|
|
363
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
|
+
|
|
364
433
|
def _link_cancellation(
|
|
365
434
|
self,
|
|
366
435
|
target: Future[Any],
|
|
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
|