vsjetengine 1.2.0__tar.gz → 1.3.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.2.0 → vsjetengine-1.3.0}/PKG-INFO +1 -1
  2. {vsjetengine-1.2.0 → vsjetengine-1.3.0}/tests/test_futures.py +4 -4
  3. {vsjetengine-1.2.0 → vsjetengine-1.3.0}/tests/test_hospice.py +2 -2
  4. {vsjetengine-1.2.0 → vsjetengine-1.3.0}/tests/test_loop_adapters.py +5 -5
  5. {vsjetengine-1.2.0 → vsjetengine-1.3.0}/tests/test_vpy.py +2 -2
  6. {vsjetengine-1.2.0 → vsjetengine-1.3.0}/vsengine/__init__.py +1 -0
  7. {vsjetengine-1.2.0 → vsjetengine-1.3.0}/vsengine/_helpers.py +2 -2
  8. vsjetengine-1.3.0/vsengine/_version.py +2 -0
  9. {vsjetengine-1.2.0 → vsjetengine-1.3.0}/vsengine/adapters/asyncio.py +2 -2
  10. {vsjetengine-1.2.0 → vsjetengine-1.3.0}/vsengine/adapters/trio.py +2 -2
  11. vsjetengine-1.2.0/vsengine/_futures.py → vsjetengine-1.3.0/vsengine/futures.py +200 -31
  12. {vsjetengine-1.2.0 → vsjetengine-1.3.0}/vsengine/loops.py +3 -3
  13. {vsjetengine-1.2.0 → vsjetengine-1.3.0}/vsengine/policy.py +6 -6
  14. {vsjetengine-1.2.0 → vsjetengine-1.3.0}/vsengine/video.py +1 -1
  15. {vsjetengine-1.2.0 → vsjetengine-1.3.0}/vsengine/vpy.py +1 -1
  16. vsjetengine-1.2.0/vsengine/_version.py +0 -2
  17. {vsjetengine-1.2.0 → vsjetengine-1.3.0}/.gitignore +0 -0
  18. {vsjetengine-1.2.0 → vsjetengine-1.3.0}/COPYING +0 -0
  19. {vsjetengine-1.2.0 → vsjetengine-1.3.0}/README.md +0 -0
  20. {vsjetengine-1.2.0 → vsjetengine-1.3.0}/pyproject.toml +0 -0
  21. {vsjetengine-1.2.0 → vsjetengine-1.3.0}/tests/__init__.py +0 -0
  22. {vsjetengine-1.2.0 → vsjetengine-1.3.0}/tests/_testutils.py +0 -0
  23. {vsjetengine-1.2.0 → vsjetengine-1.3.0}/tests/conftest.py +0 -0
  24. {vsjetengine-1.2.0 → vsjetengine-1.3.0}/tests/fixtures/heuristic_examples.json +0 -0
  25. {vsjetengine-1.2.0 → vsjetengine-1.3.0}/tests/fixtures/test.vpy +0 -0
  26. {vsjetengine-1.2.0 → vsjetengine-1.3.0}/tests/test_helpers.py +0 -0
  27. {vsjetengine-1.2.0 → vsjetengine-1.3.0}/tests/test_loops.py +0 -0
  28. {vsjetengine-1.2.0 → vsjetengine-1.3.0}/tests/test_policy.py +0 -0
  29. {vsjetengine-1.2.0 → vsjetengine-1.3.0}/tests/test_policy_store.py +0 -0
  30. {vsjetengine-1.2.0 → vsjetengine-1.3.0}/tests/test_video.py +0 -0
  31. {vsjetengine-1.2.0 → vsjetengine-1.3.0}/vsengine/_hospice.py +0 -0
  32. {vsjetengine-1.2.0 → vsjetengine-1.3.0}/vsengine/_nodes.py +0 -0
  33. {vsjetengine-1.2.0 → vsjetengine-1.3.0}/vsengine/adapters/__init__.py +0 -0
  34. {vsjetengine-1.2.0 → vsjetengine-1.3.0}/vsengine/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vsjetengine
3
- Version: 1.2.0
3
+ Version: 1.3.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
@@ -7,14 +7,14 @@
7
7
 
8
8
  import contextlib
9
9
  import threading
10
- from collections.abc import AsyncIterator, Iterator
10
+ from collections.abc import AsyncGenerator, Generator, Iterator
11
11
  from concurrent.futures import Future
12
12
  from typing import Any
13
13
 
14
14
  import pytest
15
15
 
16
- from vsengine._futures import UnifiedFuture, UnifiedIterator, unified
17
16
  from vsengine.adapters.asyncio import AsyncIOLoop
17
+ from vsengine.futures import UnifiedFuture, UnifiedIterator, unified
18
18
  from vsengine.loops import NO_LOOP, set_loop
19
19
 
20
20
 
@@ -32,7 +32,7 @@ def reject(err: BaseException) -> Future[Any]:
32
32
 
33
33
  def contextmanager_helper() -> Future[Any]:
34
34
  @contextlib.contextmanager
35
- def noop() -> Iterator[int]:
35
+ def noop() -> Generator[int]:
36
36
  yield 1
37
37
 
38
38
  return resolve(noop())
@@ -40,7 +40,7 @@ def contextmanager_helper() -> Future[Any]:
40
40
 
41
41
  def asynccontextmanager_helper() -> Future[Any]:
42
42
  @contextlib.asynccontextmanager
43
- async def noop() -> AsyncIterator[int]:
43
+ async def noop() -> AsyncGenerator[int]:
44
44
  yield 2
45
45
 
46
46
  return resolve(noop())
@@ -9,7 +9,7 @@ import contextlib
9
9
  import gc
10
10
  import logging
11
11
  import weakref
12
- from collections.abc import Iterator
12
+ from collections.abc import Generator, Iterator
13
13
  from typing import Any
14
14
 
15
15
  import pytest
@@ -53,7 +53,7 @@ class MockEnv:
53
53
 
54
54
 
55
55
  @contextlib.contextmanager
56
- def hide_logs() -> Iterator[None]:
56
+ def hide_logs() -> Generator[None]:
57
57
  logging.disable(logging.CRITICAL)
58
58
  try:
59
59
  yield
@@ -42,7 +42,7 @@ class AdapterTest:
42
42
  """Base class for event loop adapter tests."""
43
43
 
44
44
  @contextlib.contextmanager
45
- def with_loop(self) -> Iterator[EventLoop]:
45
+ def with_loop(self) -> Generator[EventLoop]:
46
46
  loop = self.make_loop()
47
47
  set_loop(loop)
48
48
  try:
@@ -60,7 +60,7 @@ class AdapterTest:
60
60
  raise NotImplementedError
61
61
 
62
62
  @contextlib.contextmanager
63
- def assert_cancelled(self) -> Iterator[None]:
63
+ def assert_cancelled(self) -> Generator[None]:
64
64
  raise NotImplementedError
65
65
 
66
66
  @make_async
@@ -216,7 +216,7 @@ class TestNoLoop(AdapterTest):
216
216
  pass
217
217
 
218
218
  @contextlib.contextmanager
219
- def assert_cancelled(self) -> Iterator[None]:
219
+ def assert_cancelled(self) -> Generator[None]:
220
220
  with pytest.raises(CancelledError):
221
221
  yield
222
222
 
@@ -244,7 +244,7 @@ class TestAsyncIO(AsyncAdapterTest):
244
244
  return await asyncio.wait_for(coro, timeout)
245
245
 
246
246
  @contextlib.contextmanager
247
- def assert_cancelled(self) -> Iterator[None]:
247
+ def assert_cancelled(self) -> Generator[None]:
248
248
  with pytest.raises(asyncio.CancelledError):
249
249
  yield
250
250
 
@@ -309,6 +309,6 @@ else:
309
309
  return await coro
310
310
 
311
311
  @contextlib.contextmanager
312
- def assert_cancelled(self) -> Iterator[None]:
312
+ def assert_cancelled(self) -> Generator[None]:
313
313
  with pytest.raises(trio.Cancelled):
314
314
  yield
@@ -15,7 +15,7 @@ import textwrap
15
15
  import threading
16
16
  import types
17
17
  import weakref
18
- from collections.abc import Callable, Iterator
18
+ from collections.abc import Callable, Generator
19
19
  from pathlib import Path
20
20
  from typing import Any
21
21
 
@@ -44,7 +44,7 @@ PATH: str = os.path.join(DIR, "fixtures", "test.vpy")
44
44
 
45
45
 
46
46
  @contextlib.contextmanager
47
- def noop() -> Iterator[None]:
47
+ def noop() -> Generator[None]:
48
48
  yield
49
49
 
50
50
 
@@ -13,6 +13,7 @@ Parts:
13
13
  - vpy: Run .vpy-scripts in your application.
14
14
  """
15
15
 
16
+ from vsengine.futures import *
16
17
  from vsengine.loops import *
17
18
  from vsengine.policy import *
18
19
  from vsengine.video import *
@@ -4,7 +4,7 @@
4
4
  # This project is licensed under the EUPL-1.2
5
5
  # SPDX-License-Identifier: EUPL-1.2
6
6
  import contextlib
7
- from collections.abc import Iterator
7
+ from collections.abc import Generator
8
8
 
9
9
  import vapoursynth as vs
10
10
 
@@ -13,7 +13,7 @@ from vsengine.policy import ManagedEnvironment
13
13
 
14
14
  # Automatically set the environment within that block.
15
15
  @contextlib.contextmanager
16
- def use_inline(function_name: str, env: vs.Environment | ManagedEnvironment | None) -> Iterator[None]:
16
+ def use_inline(function_name: str, env: vs.Environment | ManagedEnvironment | None) -> Generator[None]:
17
17
  if env is None:
18
18
  # Ensure there is actually an environment set in this block.
19
19
  try:
@@ -0,0 +1,2 @@
1
+ __version__ = "1.3.0"
2
+ __version_tuple__ = (1, 3, 0)
@@ -7,7 +7,7 @@
7
7
  import asyncio
8
8
  import contextlib
9
9
  import contextvars
10
- from collections.abc import Callable, Iterator
10
+ from collections.abc import Callable, Generator
11
11
  from concurrent.futures import Future
12
12
 
13
13
  from vsengine.loops import Cancelled, EventLoop
@@ -78,7 +78,7 @@ class AsyncIOLoop(EventLoop):
78
78
  return await asyncio.wrap_future(future, loop=self.loop)
79
79
 
80
80
  @contextlib.contextmanager
81
- def wrap_cancelled(self) -> Iterator[None]:
81
+ def wrap_cancelled(self) -> Generator[None]:
82
82
  try:
83
83
  yield
84
84
  except Cancelled:
@@ -5,7 +5,7 @@
5
5
  # SPDX-License-Identifier: EUPL-1.2
6
6
 
7
7
  import contextlib
8
- from collections.abc import Callable, Iterator
8
+ from collections.abc import Callable, Generator
9
9
  from concurrent.futures import Future
10
10
 
11
11
  import trio
@@ -100,7 +100,7 @@ class TrioEventLoop(EventLoop):
100
100
  raise exc
101
101
 
102
102
  @contextlib.contextmanager
103
- def wrap_cancelled(self) -> Iterator[None]:
103
+ def wrap_cancelled(self) -> Generator[None]:
104
104
  try:
105
105
  yield
106
106
  except Cancelled:
@@ -5,20 +5,47 @@
5
5
  # SPDX-License-Identifier: EUPL-1.2
6
6
  from __future__ import annotations
7
7
 
8
+ import traceback
8
9
  from collections.abc import AsyncIterator, Awaitable, Callable, Generator, Iterator
9
10
  from concurrent.futures import Future
10
11
  from contextlib import AbstractAsyncContextManager, AbstractContextManager
11
12
  from functools import wraps
12
13
  from inspect import isgeneratorfunction
13
14
  from types import TracebackType
14
- from typing import Any, Literal, Self, overload
15
+ from typing import Any, Literal, Protocol, Self, overload
15
16
 
16
- from vsengine.loops import Cancelled, get_loop, keep_environment
17
+ from vsengine.loops import get_loop, keep_environment
17
18
 
19
+ __all__ = ["UnifiedFuture", "UnifiedIterator", "unified"]
20
+
21
+
22
+ class FutureLike[V](Protocol):
23
+ def result(self) -> V: ...
24
+
25
+
26
+ class AsyncFutureLike[V](Protocol):
27
+ async def awaitable(self) -> V: ...
28
+
29
+
30
+ class UnifiedFuture[T](Future[T], AbstractContextManager[Any], AbstractAsyncContextManager[Any], Awaitable[T]):
31
+ """
32
+ A Promise-inspired Future that unifies concurrent.futures.Future
33
+ with Python's synchronous and asynchronous context manager and awaitable protocols.
34
+ """
18
35
 
19
- class UnifiedFuture[T](Future[T], AbstractContextManager[T, Any], AbstractAsyncContextManager[T, Any], Awaitable[T]):
20
36
  @classmethod
21
37
  def from_call[**P](cls, func: Callable[P, Future[T]], *args: P.args, **kwargs: P.kwargs) -> Self:
38
+ """
39
+ Call `func` and wrap the returned `Future` as a `UnifiedFuture`.
40
+
41
+ Any exception raised synchronously by `func` is captured and stored as a rejection
42
+ on the returned future rather than propagating to the caller.
43
+
44
+ :param func: A callable that returns a `Future`.
45
+ :param args: Positional arguments forwarded to `func`.
46
+ :param kwargs: Keyword arguments forwarded to `func`.
47
+ :return: A `UnifiedFuture` that mirrors the result of `func`.
48
+ """
22
49
  try:
23
50
  future = func(*args, **kwargs)
24
51
  except Exception as e:
@@ -28,6 +55,14 @@ class UnifiedFuture[T](Future[T], AbstractContextManager[T, Any], AbstractAsyncC
28
55
 
29
56
  @classmethod
30
57
  def from_future(cls, future: Future[T]) -> Self:
58
+ """
59
+ Wrap an existing `Future` as a `UnifiedFuture`.
60
+
61
+ If `future` is already an instance of this class it is returned unchanged.
62
+
63
+ :param future: The future to wrap.
64
+ :return: A `UnifiedFuture` that mirrors `future`.
65
+ """
31
66
  if isinstance(future, cls):
32
67
  return future
33
68
 
@@ -44,22 +79,51 @@ class UnifiedFuture[T](Future[T], AbstractContextManager[T, Any], AbstractAsyncC
44
79
 
45
80
  @classmethod
46
81
  def resolve(cls, value: T) -> Self:
82
+ """
83
+ Return an already-resolved `UnifiedFuture` carrying `value`.
84
+
85
+ :param value: The value to resolve with.
86
+ :return: A resolved `UnifiedFuture`.
87
+ """
47
88
  future = cls()
48
89
  future.set_result(value)
49
90
  return future
50
91
 
51
92
  @classmethod
52
93
  def reject(cls, error: BaseException) -> Self:
94
+ """
95
+ Return an already-rejected `UnifiedFuture` carrying `error`.
96
+
97
+ :param error: The exception to reject with.
98
+ :return: A rejected `UnifiedFuture`.
99
+ """
53
100
  future = cls()
54
101
  future.set_exception(error)
55
102
  return future
56
103
 
57
104
  # Adding callbacks
58
105
  def add_done_callback(self, fn: Callable[[Future[T]], Any]) -> None:
106
+ """
107
+ Register a callback to be called when this future completes.
108
+
109
+ Wraps the callback in `keep_environment` so that the VapourSynth environment active at registration time
110
+ is restored when the callback fires (potentially from a worker thread).
111
+
112
+ :param fn: A callable that receives the completed future.
113
+ """
59
114
  # The done_callback should inherit the environment of the current call.
60
115
  super().add_done_callback(keep_environment(fn))
61
116
 
62
- def add_loop_callback(self, func: Callable[[Future[T]], None]) -> None:
117
+ def add_loop_callback(self, func: Callable[[Future[T]], Any]) -> None:
118
+ """
119
+ Register a callback that is guaranteed to run on the event-loop thread.
120
+
121
+ Unlike `add_done_callback`, which may fire from any thread,
122
+ this method marshals `func` back to the main event loop via `EventLoop.from_thread`.
123
+
124
+ :param func: A callable that receives the completed future.
125
+ """
126
+
63
127
  def _wrapper(future: Future[T]) -> None:
64
128
  get_loop().from_thread(func, future)
65
129
 
@@ -67,18 +131,36 @@ class UnifiedFuture[T](Future[T], AbstractContextManager[T, Any], AbstractAsyncC
67
131
 
68
132
  # Manipulating futures
69
133
  @overload
70
- def then[V](self, success_cb: Callable[[T], V], err_cb: None) -> UnifiedFuture[V]: ...
134
+ def then[S](self, success_cb: Callable[[T], S]) -> UnifiedFuture[S]: ...
135
+ @overload
136
+ def then[S](self, success_cb: Callable[[T], S], err_cb: None = ...) -> UnifiedFuture[S]: ...
71
137
  @overload
72
138
  def then[V](self, success_cb: None, err_cb: Callable[[BaseException], V]) -> UnifiedFuture[T | V]: ...
73
- def then[V](
74
- self, success_cb: Callable[[T], V] | None, err_cb: Callable[[BaseException], V] | None
75
- ) -> UnifiedFuture[V] | UnifiedFuture[T | V]:
76
- result = UnifiedFuture[T | V]()
139
+ @overload
140
+ def then[V](self, *, err_cb: Callable[[BaseException], V]) -> UnifiedFuture[T | V]: ...
141
+ @overload
142
+ def then[S, V](self, success_cb: Callable[[T], S], err_cb: Callable[[BaseException], V]) -> UnifiedFuture[S | V]: ... # fmt: skip # noqa: E501
143
+ def then[S, V](self, success_cb: Callable[[T], S] | None = None, err_cb: Callable[[BaseException], V] | None = None) -> Any: # fmt: skip # noqa: E501
144
+ """
145
+ Attach fulfilment and/or rejection handlers, returning a new future.
146
+
147
+ * If this future resolves successfully, `success_cb` is called with the result value.
148
+ If `success_cb` is `None` the result is forwarded as-is.
149
+ * If this future is rejected, `err_cb` is called with the exception.
150
+ If `err_cb` is `None` the exception is forwarded as-is.
151
+
152
+ Exceptions raised inside either callback are captured and stored as rejections on the returned future.
153
+
154
+ :param success_cb: Called with the resolved value, or `None` to passthrough.
155
+ :param err_cb: Called with the exception, or `None` to passthrough.
156
+ :return: A new `UnifiedFuture` carrying the callback's return value.
157
+ """
158
+ result = UnifiedFuture[Any]()
77
159
 
78
- def _run_cb(cb: Callable[[Any], V], v: Any) -> None:
160
+ def _run_cb(cb: Callable[[Any], Any], v: T | BaseException) -> None:
79
161
  try:
80
162
  r = cb(v)
81
- except BaseException as e:
163
+ except Exception as e:
82
164
  result.set_exception(e)
83
165
  else:
84
166
  result.set_result(r)
@@ -99,13 +181,25 @@ class UnifiedFuture[T](Future[T], AbstractContextManager[T, Any], AbstractAsyncC
99
181
  return result
100
182
 
101
183
  def map[V](self, cb: Callable[[T], V]) -> UnifiedFuture[V]:
184
+ """
185
+ Transform the resolved value with `cb`, returning a new future.
186
+
187
+ :param cb: A callable that transforms the resolved value.
188
+ :return: A new `UnifiedFuture` carrying the transformed value.
189
+ """
102
190
  return self.then(cb, None)
103
191
 
104
192
  def catch[V](self, cb: Callable[[BaseException], V]) -> UnifiedFuture[T | V]:
193
+ """
194
+ Recover from a rejection by handling the exception with `cb`.
195
+
196
+ :param cb: A callable that handles the exception and returns a recovery value.
197
+ :return: A new `UnifiedFuture` carrying the recovery value on error, or the original resolved value on success.
198
+ """
105
199
  return self.then(None, cb)
106
200
 
107
201
  # Nicer Syntax
108
- def __enter__(self) -> T:
202
+ def __enter__[EnterT](self: FutureLike[AbstractContextManager[EnterT, Any]]) -> EnterT:
109
203
  obj = self.result()
110
204
 
111
205
  if isinstance(obj, AbstractContextManager):
@@ -113,7 +207,12 @@ class UnifiedFuture[T](Future[T], AbstractContextManager[T, Any], AbstractAsyncC
113
207
 
114
208
  raise NotImplementedError("(async) with is not implemented for this object")
115
209
 
116
- def __exit__(self, exc: type[BaseException] | None, val: BaseException | None, tb: TracebackType | None) -> None:
210
+ def __exit__(
211
+ self,
212
+ exc: type[BaseException] | None,
213
+ val: BaseException | None,
214
+ tb: TracebackType | None,
215
+ ) -> bool | None:
117
216
  obj = self.result()
118
217
 
119
218
  if isinstance(obj, AbstractContextManager):
@@ -122,12 +221,20 @@ class UnifiedFuture[T](Future[T], AbstractContextManager[T, Any], AbstractAsyncC
122
221
  raise NotImplementedError("(async) with is not implemented for this object")
123
222
 
124
223
  async def awaitable(self) -> T:
224
+ """
225
+ Await this future using the currently active event loop.
226
+
227
+ :return: The resolved value of this future.
228
+ :raises: Whatever exception this future was rejected with.
229
+ """
125
230
  return await get_loop().await_future(self)
126
231
 
127
232
  def __await__(self) -> Generator[Any, None, T]:
128
233
  return self.awaitable().__await__()
129
234
 
130
- async def __aenter__(self) -> T:
235
+ async def __aenter__[EnterT](
236
+ self: AsyncFutureLike[AbstractAsyncContextManager[EnterT, Any] | AbstractContextManager[EnterT, Any]],
237
+ ) -> EnterT:
131
238
  result = await self.awaitable()
132
239
 
133
240
  if isinstance(result, AbstractAsyncContextManager):
@@ -138,8 +245,11 @@ class UnifiedFuture[T](Future[T], AbstractContextManager[T, Any], AbstractAsyncC
138
245
  raise NotImplementedError("(async) with is not implemented for this object")
139
246
 
140
247
  async def __aexit__(
141
- self, exc: type[BaseException] | None, val: BaseException | None, tb: TracebackType | None
142
- ) -> None:
248
+ self,
249
+ exc: type[BaseException] | None,
250
+ val: BaseException | None,
251
+ tb: TracebackType | None,
252
+ ) -> bool | None:
143
253
  result = await self.awaitable()
144
254
 
145
255
  if isinstance(result, AbstractAsyncContextManager):
@@ -151,30 +261,61 @@ class UnifiedFuture[T](Future[T], AbstractContextManager[T, Any], AbstractAsyncC
151
261
 
152
262
 
153
263
  class UnifiedIterator[T](Iterator[T], AsyncIterator[T]):
264
+ """
265
+ A dual-mode iterator that wraps an `Iterator[Future[T]]`
266
+ and exposes it as both a synchronous `Iterator` and an asynchronous `AsyncIterator`.
267
+
268
+ In synchronous mode (`__next__`), each future is resolved by calling `Future.result()`.
269
+ This blocks if the future is not yet done.
270
+
271
+ In asynchronous mode (`__anext__`), each future is awaited via `EventLoop.await_future`,
272
+ cooperating with the active event loop.
273
+ """
274
+
154
275
  def __init__(self, future_iterable: Iterator[Future[T]]) -> None:
155
276
  self.future_iterable = future_iterable
156
277
 
157
278
  @classmethod
158
279
  def from_call[**P](cls, func: Callable[P, Iterator[Future[T]]], *args: P.args, **kwargs: P.kwargs) -> Self:
280
+ """
281
+ Call `func` and wrap the returned iterator as a `UnifiedIterator`.
282
+
283
+ :param func: A callable that returns an `Iterator[Future[T]]`.
284
+ :param args: Positional arguments forwarded to `func`.
285
+ :param kwargs: Keyword arguments forwarded to `func`.
286
+ :return: A `UnifiedIterator` wrapping the returned iterator.
287
+ """
159
288
  return cls(func(*args, **kwargs))
160
289
 
161
290
  @property
162
291
  def futures(self) -> Iterator[Future[T]]:
292
+ """The raw underlying `Iterator[Future[T]]`."""
163
293
  return self.future_iterable
164
294
 
165
295
  def run_as_completed(self, callback: Callable[[Future[T]], Any]) -> UnifiedFuture[None]:
166
- state = UnifiedFuture[None]()
296
+ """
297
+ Consume the iterator and invoke `callback` for each future as it completes.
167
298
 
168
- def _is_done_or_cancelled() -> bool:
169
- if state.done():
170
- return True
171
- if state.cancelled():
172
- state.set_exception(Cancelled())
173
- return True
174
- return False
299
+ The loop is event-loop-cooperative:
300
+ after each callback it calls `EventLoop.next_cycle` to yield control back to the event loop
301
+ so that other work can proceed.
302
+
303
+ The returned `UnifiedFuture` resolves to `None` when all futures have been processed, or is rejected if:
304
+
305
+ * the iterator raises,
306
+ * `callback` raises, or
307
+ * `callback` returns a falsy non-`None` value (signals an early stop).
308
+
309
+ Cancellation is detected by checking `Future.cancelled()` on the state future; raise `Cancelled` to abort.
310
+
311
+ :param callback: Called for each completed `Future`.
312
+ Return `None` or a truthy value to continue; return a falsy value to stop iteration early.
313
+ :return: A `UnifiedFuture` that resolves when iteration is complete.
314
+ """
315
+ state = UnifiedFuture[None]()
175
316
 
176
317
  def _get_next_future() -> Future[T] | None:
177
- if _is_done_or_cancelled():
318
+ if state.done():
178
319
  return None
179
320
 
180
321
  try:
@@ -211,8 +352,6 @@ class UnifiedIterator[T](Iterator[T], AsyncIterator[T]):
211
352
  state.set_exception(next_cycle.exception())
212
353
  return
213
354
  except Exception as e:
214
- import traceback
215
-
216
355
  traceback.print_exception(e)
217
356
  state.set_exception(e)
218
357
 
@@ -234,12 +373,12 @@ class UnifiedIterator[T](Iterator[T], AsyncIterator[T]):
234
373
  def _run_single_callback(fut: Future[T]) -> bool:
235
374
  # True => Schedule next future.
236
375
  # False => Cancel the loop.
237
- if _is_done_or_cancelled():
376
+ if state.done():
238
377
  return False
239
378
 
240
379
  try:
241
380
  result = callback(fut)
242
- except BaseException as e:
381
+ except Exception as e:
243
382
  state.set_exception(e)
244
383
  return False
245
384
  else:
@@ -335,8 +474,38 @@ def unified[T, **P](
335
474
  future_class: type[UnifiedFuture[Any]] = UnifiedFuture[Any],
336
475
  ) -> Any:
337
476
  """
338
- Decorator to normalize functions returning Future[T] or Iterator[Future[T]]
339
- into functions returning UnifiedFuture[T] or UnifiedIterator[T].
477
+ Decorator factory to normalize functions that return raw futures or iterators of futures
478
+ into functions that return `UnifiedFuture` or `UnifiedIterator`.
479
+
480
+ :param kind: Controls which wrapper is applied.
481
+
482
+ `"auto"` (default)
483
+ Automatically detects generator functions (via `isgeneratorfunction`)
484
+ and wraps them as `UnifiedIterator`; all other callables are wrapped as
485
+ `UnifiedFuture`.
486
+
487
+ `"future"`
488
+ Always wrap as `UnifiedFuture`.
489
+
490
+ `"generator"`
491
+ Always wrap as `UnifiedIterator`.
492
+
493
+ :param future_class: The concrete `UnifiedFuture` subclass to use when wrapping single-value futures.
494
+ :param iterable_class: The concrete `UnifiedIterator` subclass to use when wrapping generators.
495
+ :return: A decorator that wraps the target function.
496
+
497
+ Example usage:
498
+ ```python
499
+ @unified(kind="future")
500
+ def request_frame(index: int) -> Future[vs.VideoFrame]:
501
+ return node.get_frame_async(index)
502
+
503
+
504
+ @unified(kind="generator")
505
+ def request_all_frames(node: vs.VideoNode) -> Iterator[Future[vs.VideoFrame]]:
506
+ for i in range(node.num_frames):
507
+ yield node.get_frame_async(i)
508
+ ```
340
509
  """
341
510
 
342
511
  def _decorator_generator(func: Callable[P, Iterator[Future[T]]]) -> Callable[P, UnifiedIterator[T]]:
@@ -8,7 +8,7 @@
8
8
 
9
9
  import threading
10
10
  from abc import abstractmethod
11
- from collections.abc import Awaitable, Callable, Iterator
11
+ from collections.abc import Awaitable, Callable, Generator
12
12
  from concurrent.futures import CancelledError, Future
13
13
  from contextlib import contextmanager
14
14
  from functools import wraps
@@ -23,7 +23,7 @@ class Cancelled(BaseException):
23
23
 
24
24
 
25
25
  @contextmanager
26
- def _noop() -> Iterator[None]:
26
+ def _noop() -> Generator[None]:
27
27
  yield
28
28
 
29
29
 
@@ -126,7 +126,7 @@ class EventLoop:
126
126
  raise NotImplementedError
127
127
 
128
128
  @contextmanager
129
- def wrap_cancelled(self) -> Iterator[None]:
129
+ def wrap_cancelled(self) -> Generator[None]:
130
130
  """
131
131
  Context manager to translate cancellation exceptions.
132
132
 
@@ -12,7 +12,7 @@ from __future__ import annotations
12
12
 
13
13
  import threading
14
14
  from abc import ABC, abstractmethod
15
- from collections.abc import Iterator
15
+ from collections.abc import Generator
16
16
  from contextlib import AbstractContextManager, contextmanager
17
17
  from contextvars import ContextVar
18
18
  from logging import getLogger
@@ -272,7 +272,7 @@ class ManagedEnvironment(AbstractContextManager["ManagedEnvironment"]):
272
272
  del self._data
273
273
 
274
274
  @contextmanager
275
- def inline_section(self) -> Iterator[None]:
275
+ def inline_section(self) -> Generator[None]:
276
276
  """
277
277
  Private API!
278
278
 
@@ -295,7 +295,7 @@ class ManagedEnvironment(AbstractContextManager["ManagedEnvironment"]):
295
295
  self._policy.managed.inline_section_end()
296
296
 
297
297
  @contextmanager
298
- def use(self) -> Iterator[None]:
298
+ def use(self) -> Generator[None]:
299
299
  """
300
300
  Switches to this environment within a block.
301
301
  """
@@ -328,16 +328,16 @@ class Policy(AbstractContextManager["Policy"]):
328
328
 
329
329
  _managed: _ManagedPolicy
330
330
 
331
- def __init__(self, store: EnvironmentStore, flags_creation: int = 0) -> None:
331
+ def __init__(self, store: EnvironmentStore | None = None, flags_creation: int = 0) -> None:
332
332
  """
333
333
  Initializes a new Policy
334
334
 
335
335
  Args:
336
- store: The store to use for managing environments.
336
+ store: The store to use for managing environments. If None, defaults to a GlobalStore.
337
337
  flags_creation: The flags to use when creating environments.
338
338
  See vapoursynth.CoreCreationFlags for more information.
339
339
  """
340
- self._managed = _ManagedPolicy(store)
340
+ self._managed = _ManagedPolicy(store or GlobalStore())
341
341
  self.flags_creation = flags_creation
342
342
 
343
343
  def __enter__(self) -> Self:
@@ -12,9 +12,9 @@ from concurrent.futures import Future
12
12
 
13
13
  import vapoursynth as vs
14
14
 
15
- from ._futures import UnifiedFuture, unified
16
15
  from ._helpers import use_inline
17
16
  from ._nodes import buffer_futures, close_when_needed
17
+ from .futures import UnifiedFuture, unified
18
18
  from .policy import ManagedEnvironment
19
19
 
20
20
  __all__ = ["frame", "frames", "planes", "render"]
@@ -20,7 +20,7 @@ from uuid import uuid4
20
20
 
21
21
  import vapoursynth as vs
22
22
 
23
- from ._futures import UnifiedFuture, unified
23
+ from .futures import UnifiedFuture, unified
24
24
  from .loops import make_awaitable, to_thread
25
25
  from .policy import ManagedEnvironment, Policy
26
26
 
@@ -1,2 +0,0 @@
1
- __version__ = "1.2.0"
2
- __version_tuple__ = (1, 2, 0)
File without changes
File without changes
File without changes
File without changes